-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathNokoular.py
221 lines (179 loc) · 7.67 KB
/
Nokoular.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import logging
from datetime import datetime, timedelta
from textwrap import shorten
import rumps
from humanfriendly import format_timespan
from AppKit import NSTextField, NSApplication
from noko import Noko
from timeular import Zei
from config import NOKO_TOKEN, ORIENTATION_MAPPING
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(threadName)s %(levelname)s %(name)s %(message)s",
)
log = logging.getLogger(__name__)
rumps.debug_mode(True)
app = None
NSEventModifierFlagDeviceIndependentFlagsMask = 0xFFFF0000
NSEventModifierFlagCommand = 1 << 20
NSEventTypeKeyDown = 10
class MyTextField(NSTextField):
def performKeyEquivalent_(self, event):
_app = NSApplication.sharedApplication()
selectors = {
"a": "selectAll_",
"v": "paste_",
"c": "paste",
}
if event.type() == NSEventTypeKeyDown:
flags = event.modifierFlags()
if (
flags & NSEventModifierFlagDeviceIndependentFlagsMask
== NSEventModifierFlagCommand
):
log.debug(f"⌘{event.charactersIgnoringModifiers()}")
if selector := selectors.get(event.charactersIgnoringModifiers()):
log.debug(selector)
r = _app.sendAction_to_from_(selector, None, self)
log.debug(r)
if r:
return True
return super().performKeyEquivalent_(event)
class NokoularApp(rumps.App):
zei = None
noko = None
_current_state = None
_description_window = None
def __init__(self):
super().__init__("Z:!")
self.menu = ["Not logging", "..."]
self.zei = Zei.alloc().initWithDelegate_(self)
self.noko = Noko(NOKO_TOKEN)
self._setup_windows()
def _setup_windows(self):
self._description_window = rumps.Window(
title="Description",
cancel=True,
dimensions=(320, 40),
# textfield_cls=MyTextField,
)
self._project_window = rumps.Window(
title="Which project?",
dimensions=(320, 40),
options=self.noko.projects.keys(),
options_empty="-- Select a project --",
)
@rumps.clicked("Save entry...")
def save_entry(self, _):
# Forces the current entry to be saved and the timer stopped
# Acts as if the Zei has been upended to pause tracking.
self.zei_didUpdateOrientation_(self.zei, 0)
@rumps.clicked("...")
def set_description(self, _):
project, description, start_time = self._current_state
self._description_window.title = f"Project: {project}"
self._description_window.default_text = description
response = self._description_window.run()
if response.clicked:
self.noko.set_timer_description(project, response.text)
self._current_state = (project, response.text, start_time)
self.update_menu_titles()
def update_menu_titles(self):
if self._current_state is None:
project, description, start_time = (None, None, None)
else:
project, description, start_time = self._current_state
description = description or "---"
project = project or "Not logging"
self.menu.values()[0].title = project
self.menu.values()[1].title = shorten(
description, len(project), placeholder="..."
)
def start_timer_for_project(self, project, tags):
if project == "<Other>":
# Zei is upside down, which is a special state to indicate
# no specific project has been assigned, so ask the user what
# project it is
log.debug("on other side, running window")
response = self._project_window.run()
log.debug("ran window")
project = response.option
log.debug(f"project: {project}")
if response.text:
tags = response.text.strip()
if project == "-- Select a project --":
project = None
start_time = datetime.utcnow()
if project:
log.debug(f"project is {project}, starting timer")
running_seconds, tags = self.noko.start_timer(project, description=tags)
start_time -= timedelta(seconds=running_seconds)
self._current_state = (project, tags, start_time)
log.debug("updating menu titles")
self.update_menu_titles()
log.debug("updating menu titles; done")
def save_timer_for_project(self, project, description):
self.noko.stop_timer(project, description)
def switch_timer(self, old_project, description, new_project, new_tags):
self.save_timer_for_project(old_project, description)
self.start_timer_for_project(new_project, new_tags)
def zei_didUpdateOrientation_(self, zei, orientation):
self.title = f"Z:{orientation}"
new_project, new_tags = ORIENTATION_MAPPING.get(orientation, (None, ""))
# if the Zei has been rotated from a side with a project/tags associated,
# need to capture info from the user and stop the timer on Noko.
# Once that's done we need to start a timer for the new side
if self._current_state is None:
# it's the first time we've been called, so all we need to do is
# set the current state and start a timer for this side.
log.debug("first call, starting timer for the first time")
self.start_timer_for_project(new_project, new_tags)
else:
# we've moved away from an project which needs to have its time logged
old_project, old_tags, old_start_time = self._current_state
log.debug(f"Just moved away from side {self._current_state}")
# if the old_project is None then it's not a side of the Zei which
# has a project associated, so we don't need to do any notification
# just start the timer for the new project
if old_project is None:
log.debug(
"old_project is None, no timer to stop, and not showing notification."
)
self.start_timer_for_project(new_project, new_tags)
else:
# we need to show a notification for the log which we've just
# moved away from, to capture any tags and description from
# the user.
log.debug(f"old_project is not None: {old_project}")
elapsed = datetime.utcnow() - old_start_time
log.debug("showing notification")
rumps.notification(
f"Log {format_timespan(elapsed)}",
f"Project: {old_project}",
f"Description: {old_tags}",
placeholder=old_tags,
data={
"old_project": old_project,
"old_tags": old_tags,
"new_project": new_project,
"new_tags": new_tags,
},
has_reply_button=True,
)
def zei_didChangeConnectionState_(self, zei, connected):
self.title = "Z:OK" if connected else "Z:!"
@rumps.notifications
def notification_handler(data):
log.debug(f"notification_handler: {data}")
description = data["response"].string() if data["response"] else ""
if data["old_tags"]:
description = f"{data['old_tags']} {description}"
app.switch_timer(
data["old_project"], description, data["new_project"], data["new_tags"]
)
def main():
global app
app = NokoularApp()
app.run()
if __name__ == "__main__":
main()