NowFocus 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. nowfocus/__init__.py +68 -0
  2. nowfocus/__main__.py +838 -0
  3. nowfocus/conf.py +181 -0
  4. nowfocus/connectors/activitywatch.py +31 -0
  5. nowfocus/connectors/caldav.py +174 -0
  6. nowfocus/connectors/csv.py +25 -0
  7. nowfocus/connectors/psc_timetracker.py +290 -0
  8. nowfocus/connectors/taskwarrior.py +119 -0
  9. nowfocus/connectors/timewarrior.py +31 -0
  10. nowfocus/connectors/todo_template.py +68 -0
  11. nowfocus/connectors/todotxt.py +160 -0
  12. nowfocus/connectors/trello.py +233 -0
  13. nowfocus/connectors/txt.py +244 -0
  14. nowfocus/connectors/vikunja.py +307 -0
  15. nowfocus/example-todo.txt +13 -0
  16. nowfocus/icon/cancel.png +0 -0
  17. nowfocus/icon/cancel.svg +47 -0
  18. nowfocus/icon/edit.png +0 -0
  19. nowfocus/icon/edit.svg +1 -0
  20. nowfocus/icon/icon-0.png +0 -0
  21. nowfocus/icon/icon-0.svg +86 -0
  22. nowfocus/icon/icon-1.svg +86 -0
  23. nowfocus/icon/icon-2.svg +86 -0
  24. nowfocus/icon/icon-3.svg +86 -0
  25. nowfocus/icon/icon-4.svg +86 -0
  26. nowfocus/icon/icon-5.svg +86 -0
  27. nowfocus/icon/icon-6.svg +86 -0
  28. nowfocus/icon/icon-7.svg +86 -0
  29. nowfocus/icon/icon-8.svg +86 -0
  30. nowfocus/icon/icon-9.svg +86 -0
  31. nowfocus/icon/icon.png +0 -0
  32. nowfocus/icon/icon.svg +188 -0
  33. nowfocus/icon/mark-done.png +0 -0
  34. nowfocus/icon/mark-done.svg +41 -0
  35. nowfocus/icon/pause.png +0 -0
  36. nowfocus/icon/pause.svg +48 -0
  37. nowfocus/icon/settings.png +0 -0
  38. nowfocus/icon/settings.svg +4 -0
  39. nowfocus/icon/unused/action-unavailable-symbolic.svg +3 -0
  40. nowfocus/icon/unused/applications-engineering-symbolic.svg +3 -0
  41. nowfocus/icon/unused/background-selected-symbolic.svg +5 -0
  42. nowfocus/icon/unused/cancel-operation-symbolic.svg +1 -0
  43. nowfocus/icon/unused/checkbox-checked-symbolic (2).svg +7 -0
  44. nowfocus/icon/unused/checkbox-checked-symbolic.svg +4 -0
  45. nowfocus/icon/unused/checkbox-mixed-symbolic (2).svg +6 -0
  46. nowfocus/icon/unused/checkbox-mixed-symbolic.svg +4 -0
  47. nowfocus/icon/unused/checkbox-symbolic (2).svg +6 -0
  48. nowfocus/icon/unused/checkbox-symbolic.svg +4 -0
  49. nowfocus/icon/unused/clock-app-symbolic.svg +5 -0
  50. nowfocus/icon/unused/document-edit-symbolic.svg +3 -0
  51. nowfocus/icon/unused/document-new-symbolic.svg +5 -0
  52. nowfocus/icon/unused/document-open-recent-symbolic.svg +3 -0
  53. nowfocus/icon/unused/edit-find-symbolic.svg +3 -0
  54. nowfocus/icon/unused/edit-symbolic.svg +5 -0
  55. nowfocus/icon/unused/filter-flagged-symbolic.svg +6 -0
  56. nowfocus/icon/unused/format-unordered-list-symbolic.svg +5 -0
  57. nowfocus/icon/unused/help-about-symbolic.svg +6 -0
  58. nowfocus/icon/unused/list-add-symbolic.svg +3 -0
  59. nowfocus/icon/unused/list-remove-symbolic.svg +3 -0
  60. nowfocus/icon/unused/media-playback-pause.svg +46 -0
  61. nowfocus/icon/unused/media-playback-start-symbolic.svg +3 -0
  62. nowfocus/icon/unused/no-event-symbolic.svg +6 -0
  63. nowfocus/icon/unused/object-select-symbolic.svg +5 -0
  64. nowfocus/icon/unused/org.gnome.Settings-about-symbolic-2.svg +4 -0
  65. nowfocus/icon/unused/org.gnome.Settings-about-symbolic.svg +1 -0
  66. nowfocus/icon/unused/org.gnome.Settings-keyboard-symbolic.svg +1 -0
  67. nowfocus/icon/unused/org.gnome.Settings-notifications-symbolic (2).svg +4 -0
  68. nowfocus/icon/unused/org.gnome.Settings-notifications-symbolic.svg +1 -0
  69. nowfocus/icon/unused/org.gnome.Settings-search-symbolic-2.svg +4 -0
  70. nowfocus/icon/unused/org.gnome.Settings-search-symbolic.svg +1 -0
  71. nowfocus/icon/unused/org.gnome.Settings-system-symbolic.svg +4 -0
  72. nowfocus/icon/unused/org.gnome.Settings-time-symbolic.svg +7 -0
  73. nowfocus/icon/unused/org.gnome.Settings-wacom-symbolic.svg +1 -0
  74. nowfocus/icon/unused/org.gnome.clocks-symbolic.svg +5 -0
  75. nowfocus/icon/unused/pause-large-symbolic.svg +3 -0
  76. nowfocus/icon/unused/preferences-system-time-symbolic.svg +6 -0
  77. nowfocus/icon/unused/process-stop-symbolic.svg +3 -0
  78. nowfocus/icon/unused/selection-checked.svg +71 -0
  79. nowfocus/icon/unused/software-installed-symbolic.svg +3 -0
  80. nowfocus/icon/unused/stopwatch-symbolic.svg +8 -0
  81. nowfocus/icon/unused/system-settings-symbolic.svg +7 -0
  82. nowfocus/icon/unused/timer-sand-symbolic.svg +8 -0
  83. nowfocus/icon/unused/timer-symbolic.svg +3 -0
  84. nowfocus/icon/unused/user-trash-symbolic (2).svg +3 -0
  85. nowfocus/icon/unused/user-trash-symbolic.svg +10 -0
  86. nowfocus/icon/unused/view-fullscreen-symbolic.svg +8 -0
  87. nowfocus/icon/unused/view-more-horizontal-symbolic.svg +7 -0
  88. nowfocus/icon/unused/view-more-symbolic.svg +7 -0
  89. nowfocus/icon/unused/view-restore-symbolic.svg +3 -0
  90. nowfocus/icon/unused/view-sort-ascending-symbolic.svg +3 -0
  91. nowfocus/icon/unused/view-sort-descending-symbolic.svg +3 -0
  92. nowfocus/icon/unused/view-tasks-unscheduled-symbolic.svg +12 -0
  93. nowfocus/new_task_dialog.py +130 -0
  94. nowfocus/runner.sh +8 -0
  95. nowfocus/session_edit_dialog.py +216 -0
  96. nowfocus/session_options.py +169 -0
  97. nowfocus/sessions.csv +1 -0
  98. nowfocus/settings.py +982 -0
  99. nowfocus/sound/bell-xylophone-g.mp3 +0 -0
  100. nowfocus/sound/dinner-bell.mp3 +0 -0
  101. nowfocus/sound/xylophone-chord.mp3 +0 -0
  102. nowfocus/styles.css +45 -0
  103. nowfocus/task_window.py +640 -0
  104. nowfocus/utils.py +1210 -0
  105. nowfocus-0.2.0.dist-info/METADATA +158 -0
  106. nowfocus-0.2.0.dist-info/RECORD +110 -0
  107. nowfocus-0.2.0.dist-info/WHEEL +5 -0
  108. nowfocus-0.2.0.dist-info/entry_points.txt +2 -0
  109. nowfocus-0.2.0.dist-info/licenses/LICENSE +674 -0
  110. nowfocus-0.2.0.dist-info/top_level.txt +1 -0
nowfocus/__main__.py ADDED
@@ -0,0 +1,838 @@
1
+ import os
2
+ import subprocess, sys
3
+ import signal
4
+ import json
5
+ import time
6
+ from datetime import datetime, timezone, timedelta
7
+ import importlib
8
+ from urllib.request import urlopen
9
+ import requests
10
+ import threading
11
+ import copy
12
+ from playsound3 import playsound
13
+ import setproctitle
14
+ import psutil
15
+ import argparse
16
+
17
+ import gi
18
+ gi.require_version('Gtk', '3.0')
19
+ from gi.repository import Gtk, GLib, Gdk, Gio
20
+
21
+ gi.require_version('Notify', '0.7')
22
+ from gi.repository import Notify as notify
23
+
24
+ try:
25
+ gi.require_version('AyatanaAppIndicator3', '0.1')
26
+ from gi.repository import AyatanaAppIndicator3 as appindicator
27
+ except Exception as e:
28
+ gi.require_version('AppIndicator3', '0.1')
29
+ from gi.repository import AppIndicator3 as appindicator
30
+
31
+ from dbus_idle import IdleMonitor
32
+
33
+ # Set working dir to file location
34
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
35
+
36
+ # Add working dir to path
37
+ sys.path.append(os.path.dirname(__file__))
38
+
39
+ # from . import conf # this works in module context but not running as pile-of-files
40
+ import conf # this works running as pile-of-files but not in module context without sys.path.append
41
+
42
+ import utils
43
+ from utils import *
44
+
45
+ from settings import SettingsWindow
46
+ from task_window import TaskWindow
47
+ from new_task_dialog import NewTaskWDialog
48
+ from session_options import SessionOptionsDialog
49
+
50
+ dbg(conf.user,l=3,s='user_settings')
51
+
52
+ setproctitle.setproctitle(conf.app_name)
53
+ pipe_file = os.path.dirname(os.path.realpath(__file__))+"/"+conf.app_id+"_pipe"
54
+
55
+ class Application(Gtk.Application):
56
+ def __init__(self, *args, **kwargs):
57
+ super().__init__(*args, application_id="org.example.myapp", **kwargs)
58
+
59
+ self.window = None
60
+
61
+ self.is_running = False
62
+ self.session = {
63
+ "label":"Randomness",
64
+ "extended_label": 'Randomness',
65
+ "start_time":datetime.now(),
66
+ "duration":0,
67
+ 'task':{},
68
+ 'notes':'',
69
+ }
70
+
71
+ self.menu_tasks = {}
72
+ self.list_menus = {}
73
+
74
+ self.indicator = appindicator.Indicator.new(conf.app_name, os.path.abspath('icon/icon-0.svg'), appindicator.IndicatorCategory.APPLICATION_STATUS)
75
+
76
+ self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
77
+
78
+ self.menu = Gtk.Menu()
79
+ # menu.set_reserve_toggle_size(False) # skip menu left padding, doesn't work
80
+
81
+ utils.db_init()
82
+ utils.db_update()
83
+
84
+
85
+ # self.update_menu()
86
+ self.async_refresh()
87
+
88
+ # utils.db_cleanup()
89
+
90
+ self.indicator.set_menu(self.menu)
91
+
92
+ # main_tick_timer = GLib.timeout_add_seconds(conf.user['tick_interval'], self.tick)
93
+ main_tick_timer = GLib.timeout_add_seconds(1, self.tick)
94
+
95
+ try:
96
+ db_session = db_query("SELECT value FROM system WHERE field = 'session'")
97
+
98
+ if db_session:
99
+ s = json.loads(db_session[0]['value'])
100
+ s['start_time'] = datetime.strptime(s['start_time'],'%Y-%m-%d %H:%M:%S.%f')
101
+ self.session = s
102
+ self.is_running = True
103
+ dbg("resuming session",s['label'],l=2, s='session')
104
+ except Exception as e:
105
+ dbg("Error resuming session",e,l=1)
106
+
107
+ self.pipethread = threading.Thread(target=self.check_pipe)
108
+ self.pipethread.daemon = True
109
+ self.pipethread.start()
110
+
111
+ # Testing
112
+ # time.sleep(3)
113
+ # self.open_session_options_dialog('test_param')
114
+
115
+ # self.open_settings_window() #for testing
116
+ # self.open_task_window() #for testing
117
+
118
+ # time.sleep(2)
119
+ # self.open_new_task_dialog() #for testing
120
+
121
+ # time.sleep(2)
122
+ # self.print_time_totals()
123
+
124
+ # signal.signal(signal.SIGINT, signal.SIG_DFL)
125
+ signal.signal(signal.SIGINT, self.quit)
126
+ signal.signal(signal.SIGUSR1, self.signal_handler)
127
+ signal.signal(signal.SIGUSR2, self.signal_handler)
128
+
129
+ Gtk.main()
130
+
131
+
132
+
133
+
134
+ def print_time_totals(self = None, widget = None):
135
+
136
+ # SELECT extended_label, (SUM(duration) / 60 ) FROM sessions GROUP BY extended_label
137
+ # SELECT extended_label, (SUM(duration) / 60 ) FROM sessions GROUP BY parent_id
138
+
139
+ timeframe = hours_search_timeframes(conf.user['hours_search_timeframe'])
140
+
141
+ print("\n\n\n #### SESSION TOTALS "+utils.sessions_timeframe_sql()+" ### \n\n\n")
142
+
143
+ l = ''
144
+
145
+ for t in db_query("SELECT DISTINCT id, parent_id, label, parent_label FROM tasks WHERE id IN(SELECT DISTINCT task_id FROM sessions WHERE "+utils.sessions_timeframe_sql()+") ORDER BY extended_label ASC "):
146
+ if t['parent_id'] and l != t['parent_id']:
147
+ l = t['parent_id']
148
+
149
+ print(extended_label(db_get_item_by_id(t['parent_id'],'lists')) +": "+ sec_to_time(get_total_time(l,'lists',timeframe[0],timeframe[1])))
150
+
151
+ print(" "+ t['label'] +": "+ sec_to_time(get_total_time(t['id'],'tasks',timeframe[0],timeframe[1])))
152
+ # def get_total_time(id, category = 'tasks', start_time = None, end_time = None, get_minutes = None):
153
+
154
+
155
+ def quit(self, widget_or_signal_source=None, condition=None):
156
+ print("Adios ", conf.app_name)
157
+ print("widget_or_signal_source ", widget_or_signal_source)
158
+ print("condition ", condition)
159
+
160
+ if self.is_running:
161
+ print('Caching active session', self.session['label'])
162
+ db_set_session_cache(self.session)
163
+ try:
164
+ # print("before os.remove(pipe_file)")
165
+ os.remove(pipe_file)
166
+ print("Pipe removed")
167
+ except Exception as e:
168
+ print("Error removing pipe_file in quit",e)
169
+
170
+ notify.uninit()
171
+ Gtk.main_quit()
172
+ exit()
173
+
174
+
175
+ def check_afk_time(self):
176
+ # returns seconds of inactivity
177
+ # See https://stackoverflow.com/questions/67083083/how-to-get-idle-time-in-linux or
178
+
179
+ # Works on x11 but not wayland
180
+ # Requires xprintidle (sudo apt install xprintidle)
181
+ # idle_time = int(int(subprocess.getoutput('xprintidle')) / 1000)
182
+
183
+ # Currently using: https://github.com/bkbilly/dbus_idle
184
+ # Requires:
185
+ # sudo apt install meson libdbus-glib-1-dev patchelf
186
+ # pip install dbus-idle
187
+
188
+ idle_time = int(int(IdleMonitor().get_dbus_idle()) / 1000)
189
+ return idle_time
190
+
191
+
192
+ def toggle_do_not_disturb(self, widget):
193
+ if 'do-not-disturb' in self.session:
194
+ del self.session['do-not-disturb']
195
+ self.do_not_disturb_menu_item.set_label("Enable Do Not Disturb")
196
+ self.indicator.set_label("Do Not Disturb Disabled", "")
197
+
198
+ else:
199
+ self.session['do-not-disturb'] = True
200
+ self.indicator.set_label("Do Not Disturb Enabled", "")
201
+ self.do_not_disturb_menu_item.set_label("Disable Do Not Disturb")
202
+
203
+ return True
204
+
205
+ def tock(self):
206
+
207
+ if 'do-not-disturb' in self.session:
208
+ return None
209
+
210
+ afk_time = self.check_afk_time()
211
+ # print("Idle time: "+str(afk_time))
212
+
213
+ dbg('Last todo todo_sync_time', conf.todo_sync_time, 'Time diff',int(time_difference(conf.todo_sync_time)),'Auto refresh interval * 60', (conf.user['todolist_refresh_interval'] * 60), s="todoloading", l=3 )
214
+
215
+ if (int(time_difference(conf.todo_sync_time)) / 60) > conf.user['todolist_refresh_interval'] * 60 :
216
+ self.async_refresh()
217
+
218
+ minutes = (int(self.session['duration']) / 60)
219
+
220
+ if(self.is_running == False):
221
+
222
+ if float(minutes / conf.user['randomness_interrupt_interval']).is_integer():
223
+ if afk_time < 30:
224
+
225
+ notify.Notification.new("What Am I doing?","Your randomness timer is at "+str(minutes)+" minutes. ", None).show()
226
+
227
+ self.open_task_window()
228
+
229
+ playsound('sound/dinner-bell.mp3',False)
230
+
231
+ elif afk_time > 120:
232
+ self.session['duration'] = 0
233
+ self.session['start_time'] = now()
234
+ print("Idle time reset. afk:", afk_time, self.session)
235
+
236
+ else:
237
+ if afk_time > 120:
238
+ # only show this once
239
+ if afk_time < 181:
240
+ self.open_task_window(None,{'afk_time':afk_time})
241
+ # session_options_dialog(None, 'test input_data')
242
+ else:
243
+ if self.session['label'] in conf.user['custom_pomodoro_intervals']:
244
+ check = conf.user['custom_pomodoro_intervals'][self.session['label']]
245
+ else:
246
+ check = conf.user['pomodoro_interval']
247
+
248
+ if float(minutes / check ).is_integer():
249
+ notify.Notification.new("Time for a Break?","You've been working on "+self.session['label']+" for "+str(minutes)+" minutes. ", None).show()
250
+
251
+ playsound('sound/bell-xylophone-g.mp3',False)
252
+
253
+
254
+ if 'target' in self.session:
255
+ t = self.session['target']
256
+
257
+ t['percent'] = round(( (t['starting_value'] + minutes) / t['value'] ) * 100,1)
258
+ print("At ", t['percent'], "% of ", t['scope'], " target")
259
+
260
+ if t['type'] == 'max':
261
+ if t['percent'] >= 100:
262
+
263
+ notify.Notification.new("Time is up for this "+t['scope'],"You'r at "+str(round(t['percent']))+"% of your "+str(t['value'])+" minutes in the last "+str(t['within_value'])+" "+ t['within_unit'], None).show()
264
+
265
+ playsound('sound/dinner-bell.mp3',False)
266
+
267
+ elif t['type'] == 'min' and round(t['starting_value'] + minutes) == t['value']:
268
+ notify.Notification.new("Good job on doing "+self.session['label'],"You've reached your target of "+str(t['value'])+" minutes "+str(t['within_value'])+" "+ t['within_unit'], None).show()
269
+
270
+ playsound('sound/xylophone-chord.mp3',False)
271
+
272
+
273
+ # maybe add a target % to the session and show with tick
274
+
275
+ icon_tick_number = 0
276
+
277
+ def tick(self):
278
+ menu = self.menu
279
+ indicator = self.indicator
280
+
281
+ # check for suspend indicated by gap in tick intervals
282
+ time_since_last_tick = round(time_difference(self.session['start_time']) - self.session['duration'])
283
+ if time_since_last_tick > 10:
284
+ print(time_since_last_tick, " seconds since last tick. Probably just woke from suspend. ")
285
+
286
+ if self.is_running:
287
+ self.open_task_window(None,{'afk_time':time_since_last_tick})
288
+ else:
289
+ print("resetting randomness timer")
290
+ self.session['duration'] = 0
291
+ self.session['start_time'] = now()
292
+
293
+ # print("tick!")
294
+ self.session['duration'] = int(time_difference(self.session['start_time']))
295
+
296
+ if(self.session['duration'] > 2 and (int(self.session['duration']) / 60).is_integer()):
297
+ self.tock()
298
+
299
+ if(self.is_running == True):
300
+ self.icon_tick_number = self.icon_tick_number + 1
301
+
302
+ if self.icon_tick_number > 9:
303
+ self.icon_tick_number = 1
304
+
305
+ label = self.session['label'] + ": " + sec_to_time(self.session['duration'])
306
+
307
+ indicator.set_icon_full(os.path.abspath('icon/icon-'+str(self.icon_tick_number)+'.svg'),label)
308
+
309
+ else:
310
+
311
+ # label = random.choice(conf.idle_messages) # Cool but makes menu bounce around #Could be paused when the menu opens
312
+ label = conf.user['default_text']
313
+
314
+ indicator.set_icon_full(os.path.abspath('icon/icon-0.svg'),label)
315
+
316
+ # https://lazka.github.io/pgi-docs/#AyatanaAppIndicator3-0.1/classes/Indicator.html#AyatanaAppIndicator3.Indicator.set_label
317
+ indicator.set_label(label, "Wide")
318
+
319
+ return True
320
+
321
+
322
+ def start_task(self, w = None, task_data_or_id = None, transfer_current_session_time = False):
323
+
324
+ if isinstance(task_data_or_id, dict):
325
+ task_data = task_data_or_id
326
+ else:
327
+ task_data = utils.db_get_item_by_id(task_data_or_id)
328
+
329
+ task_label = task_data['label']
330
+ dbg("starting "+ task_label,l=1)
331
+
332
+ if transfer_current_session_time:
333
+ start_time = self.session['start_time']
334
+ if(self.is_running == True):
335
+ self.stop_task(None,'cancel')
336
+
337
+ else:
338
+ start_time = now()
339
+
340
+ if(self.is_running == True):
341
+ self.stop_task()
342
+
343
+ self.is_running = True
344
+
345
+ s = {
346
+ "label": task_label,
347
+ "extended_label": utils.extended_label(task_data),
348
+ "start_time": start_time,
349
+ "duration":0,
350
+ "task":task_data,
351
+ "notes":'',
352
+ }
353
+
354
+ if s['task']['id'] in conf.user['time_targets']['tasks']:
355
+ s['target'] = copy.copy(conf.user['time_targets']['tasks'][s['task']['id']])
356
+ s['target']['scope'] = 'task'
357
+ target_start = datetime_minus_calendar_unit(s['target']['within_unit'],s['target']['within_value'])
358
+
359
+ s['target']['starting_value'] = utils.get_total_time(task_data['id'],'task', target_start,None,"get_minutes")
360
+ s['target']['percent'] = s['target']['starting_value'] / s['target']['value'] * 100
361
+
362
+ if s['task']['parent_id'] in conf.user['time_targets']['lists']:
363
+ s['target'] = copy.copy(conf.user['time_targets']['lists'][s['task']['parent_id']])
364
+ s['target']['scope'] = 'list'
365
+ target_start = datetime_minus_calendar_unit(s['target']['within_unit'],s['target']['within_value'])
366
+
367
+ s['target']['starting_value'] = utils.get_total_time(task_data['parent_id'],'list', target_start,None,"get_minutes")
368
+ s['target']['percent'] = s['target']['starting_value'] / s['target']['value'] * 100
369
+
370
+ dbg("session with list target started",s,s="targets",l=3)
371
+
372
+ self.session = s
373
+
374
+ utils.db_set_session_cache(self.session)
375
+
376
+ self.tick()
377
+
378
+ # print('total times for task ',task_label, utils.get_times(task_data))
379
+
380
+ self.task_running_menu_additions()
381
+
382
+ self.menu.show_all()
383
+ print("task data",task_data['data'])
384
+
385
+ if task_data['id'] in conf.user['task_commands']:
386
+ command_data = conf.user['task_commands'][task_data['id']]
387
+ if command_data['status']:
388
+ thread = threading.Thread(target=self.run_task_command,args=(command_data['command'],))
389
+ print("found task command:", command_data['command'])
390
+ thread.start()
391
+
392
+
393
+ def run_task_command(self,command):
394
+ print("running task command:", command)
395
+
396
+ self.running_command_task_label = copy.copy(self.session['label'])
397
+
398
+ # Check if already running
399
+ process = None
400
+ for proc in psutil.process_iter():
401
+ pinfo = proc.as_dict(attrs=['pid', 'name'])
402
+ print("Check process", pinfo['name'])
403
+ if pinfo['name'] == command:
404
+ process = proc
405
+ break
406
+ else:
407
+ continue
408
+
409
+ if process:
410
+ print("waiting for already running command with psutils", command)
411
+ process.wait()
412
+ else:
413
+ print("Launching command with subprocess.run", command)
414
+ subprocess.run(command)
415
+
416
+ print("running task command complete:", command)
417
+
418
+ if self.session['label'] == self.running_command_task_label:
419
+ GLib.idle_add(self.stop_task)
420
+ GLib.idle_add(self.open_task_window)
421
+
422
+
423
+ def mark_done(self, w=None, task = None):
424
+ ''' second (task) argument is required and must be a task object '''
425
+ print("Mark Task done")
426
+ print(task)
427
+
428
+ todolist_conf = conf.user['todolists'][task['todolist']]
429
+
430
+ try:
431
+ done_thread = threading.Thread(target=conf.todo_connectors[todolist_conf['type']].mark_task_done, args=(task,) )
432
+ done_thread.start()
433
+
434
+ db_query("UPDATE tasks set status = '0' WHERE id = ? ",(task['id'],) )
435
+ utils.reindex_one(task)
436
+
437
+ # print('remove menu item')
438
+ self.menu_tasks[task['id']].destroy()
439
+
440
+ playsound('sound/xylophone-chord.mp3',False)
441
+
442
+ except Exception as e:
443
+ error_notice('Error Marking Task Done'," Marking "+ task['label']+" as done in "+todolist_conf['label']+" had a serious failure",e )
444
+
445
+
446
+
447
+ def stop_task(self, w = '', action = 'save', custom_end_time=None):
448
+ ''' supported actions are save,cancel,mark_done '''
449
+
450
+ list_menus = self.list_menus
451
+ menu_tasks = self.menu_tasks
452
+ session = self.session
453
+
454
+ print("Stopping ", session['label'])
455
+ task = session['task']
456
+ if self.is_running == True:
457
+ session['duration'] = time_difference(session['start_time'],custom_end_time)
458
+ db_query("DELETE FROM system WHERE field = 'session' ")
459
+
460
+ if action != 'cancel':
461
+
462
+ # Get time tracker for this tasks todolist
463
+ todolist_conf = conf.user['todolists'][task['todolist']]
464
+ timetracker_conf = conf.user['timetrackers'][todolist_conf['timetracker']]
465
+
466
+ dbg("Save Session to "+ todolist_conf['timetracker'])
467
+ try:
468
+ save_thread = threading.Thread(target=conf.timetracker_connectors[timetracker_conf['type']].save_session, args=(session,timetracker_conf) )
469
+ save_thread.start()
470
+ except Exception as e:
471
+ error_notice('Error Saving Time Data'," Recording timetracking for "+ task['label']+" in "+timetracker_conf['label']+" had a serious failure",e )
472
+
473
+ session['timetracker'] = todolist_conf['timetracker']
474
+
475
+ utils.db_save_session(session)
476
+
477
+ if action == 'mark_done':
478
+ self.mark_done(None,task)
479
+
480
+ else:
481
+ print("add ",utils.extended_label(task)," to recent tasks")
482
+ i = Gtk.MenuItem.new_with_label(utils.extended_label(task))
483
+ i.connect('activate',self.start_task,task)
484
+ list_menus['recent'].prepend(i)
485
+ try:
486
+ list_menus['recent'].get_children()[11].destroy()
487
+ except Exception as e:
488
+ dbg("Exception trying to rotate recent tasks. probably are less than 11",l=2,s="recent")
489
+
490
+ # Check de-hoist completed time targets
491
+ ballance = utils.check_time_target(task)
492
+ dbg('time target ballance:', ballance,l=2,s='time_targets')
493
+
494
+ if ballance != False and ballance < 0:
495
+ dbg('un-hoist completed time target',l=2,s='time_targets')
496
+ try:
497
+ self.menu.remove(menu_tasks[task['id']]) # how to check that this actually worked/ it was hoisted?
498
+ list_menus[task['parent_id']].append(menu_tasks[task['id']])
499
+ except Exception as e:
500
+ # Boo Hoo?
501
+ dbg('de-hoisting failed',e)
502
+ playsound('sound/xylophone-chord.mp3',False)
503
+
504
+ self.menu.show_all()
505
+
506
+ self.is_running = False
507
+ # print(utils.get_times(task))
508
+
509
+ notify.Notification.new(action.capitalize()+" "+session['label']+" "+sec_to_time(session['duration']),utils.pretty_dict(utils.get_times(task)), None).show()
510
+
511
+ # Start randomness timer
512
+ self.session = {
513
+ "label": 'Randomness',
514
+ "extended_label": 'Randomness',
515
+ "start_time": now(),
516
+ "duration":0,
517
+ "task":{}
518
+ }
519
+
520
+ self.tick()
521
+
522
+ # Rm stop task menu item
523
+ self.menu.get_children()[0].destroy()
524
+ self.menu.get_children()[0].destroy()
525
+ self.do_not_disturb_menu_item.set_label("Do Not Disturb")
526
+
527
+ else:
528
+ print('no task running!')
529
+
530
+
531
+ def task_running_menu_additions(self):
532
+
533
+ i = Gtk.ImageMenuItem.new_with_label("Edit Session")
534
+ i.set_image(Gtk.Image.new_from_file(os.path.abspath('icon/edit.svg')))
535
+
536
+ # i.set_image(Gtk.Image.new_from_file('/usr/share/icons/Yaru/scalable/status/software-installed-symbolic.svg'))
537
+ i.set_always_show_image(True)
538
+ i.connect("activate", self.open_session_options_dialog,'from_menu')
539
+ self.menu.insert(i,0)
540
+
541
+ i = Gtk.ImageMenuItem.new_with_label("Pause" )
542
+ i.set_image(Gtk.Image.new_from_file(os.path.abspath('icon/pause.svg')))
543
+
544
+ # i.set_image(Gtk.Image.new_from_file('/usr/share/icons/Yaru/scalable/multimedia/pause-symbolic.svg'))
545
+ i.set_always_show_image(True)
546
+ i.connect("activate", self.stop_task)
547
+ self.menu.insert(i,0)
548
+
549
+
550
+ def async_refresh(self, w=None):
551
+
552
+ self.indicator.set_label("Refreshing Todolists", "Wide")
553
+ menu_item = Gtk.MenuItem.new_with_label("Refreshing Todolists")
554
+ self.menu.append(menu_item)
555
+ self.menu.show_all()
556
+
557
+ connectors_thread = threading.Thread(target=self.async_refresh_inner)
558
+ connectors_thread.start()
559
+
560
+
561
+ def async_refresh_inner(self):
562
+ # dbg("async refresh started",s="todoloading",l=3)
563
+ utils.get_todolists()
564
+ dbg("async refresh complete",s="todoloading",l=3)
565
+ GLib.idle_add(self.update_menu)
566
+
567
+ try:
568
+ GLib.idle_add(self.taskwindow.refresh_search_cache)
569
+ except Exception:
570
+ pass
571
+
572
+
573
+ def update_menu(self, w = ''):
574
+ # print("update_menu")
575
+ menu = self.menu
576
+
577
+ self.menu_tasks = {}
578
+ self.list_menus = {}
579
+
580
+ menu_tasks = self.menu_tasks
581
+ list_menus = self.list_menus
582
+
583
+
584
+ menu.foreach(lambda child: child.destroy())
585
+
586
+ utils.add_todos_to_menu(menu, menu_tasks, list_menus, self.start_task)
587
+
588
+ menu.append(Gtk.SeparatorMenuItem.new())
589
+
590
+ # Recent Tasks Menu
591
+ list_menus['recent'] = Gtk.Menu() # the sub_list that items get added to
592
+ recents = Gtk.MenuItem.new_with_mnemonic("Recent Tasks") # the "item" that gets added
593
+ recents.set_submenu(list_menus['recent'])
594
+ menu.append(recents)
595
+
596
+ for id, t in utils.get_recent_tasks().items():
597
+ # print('recent task',t)
598
+ try:
599
+ i = Gtk.MenuItem.new_with_label(utils.extended_label(t))
600
+ i.connect('activate',self.start_task,t)
601
+ list_menus['recent'].append(i)
602
+
603
+ except Exception as e:
604
+ # Because a key error if the task has was completed or todolist removed
605
+ print('recent_tasks error', e)
606
+ menu.append(Gtk.SeparatorMenuItem.new())
607
+
608
+
609
+ # Todolist openables
610
+ openables = []
611
+ for id, todolist in conf.user['todolists'].items():
612
+ if todolist['status']:
613
+ openable = get_connector_openable(None, todolist,False)
614
+ if openable not in openables:
615
+ openables.append(openable)
616
+
617
+ menu_aw = Gtk.MenuItem.new_with_label("Open "+todolist['label']+" ")
618
+ menu_aw.connect("activate", get_connector_openable, todolist)
619
+ menu.append(menu_aw)
620
+
621
+ # Timetrackers
622
+ for id, timetracker in conf.user['timetrackers'].items():
623
+ if timetracker['status']:
624
+ openable = get_connector_openable(None, timetracker,False)
625
+ if openable not in openables:
626
+ openables.append(openable)
627
+ menu_aw = Gtk.MenuItem.new_with_label("Open "+timetracker['label']+" ")
628
+ menu_aw.connect("activate", get_connector_openable, timetracker)
629
+ menu.append(menu_aw)
630
+
631
+ # Update List
632
+ # Lots of spaces to make menu wide-ish in preparation for opening wide sub menus
633
+ menu_update = Gtk.MenuItem.new_with_label("Refresh Lists ")
634
+ menu_update.connect("activate", self.async_refresh)
635
+ menu.append(menu_update)
636
+
637
+ setting_menu_item = Gtk.MenuItem.new_with_label("Settings")
638
+ setting_menu_item.connect("activate",self.open_settings_window)
639
+ menu.append(setting_menu_item)
640
+
641
+ task_window_menu_item = Gtk.MenuItem.new_with_label("Tasks")
642
+ task_window_menu_item.connect("activate",self.open_task_window)
643
+ menu.prepend(task_window_menu_item)
644
+ self.indicator.set_secondary_activate_target(task_window_menu_item)
645
+
646
+
647
+ new_task_menu_item = Gtk.MenuItem.new_with_label("New Task")
648
+ new_task_menu_item.connect("activate",self.open_new_task_dialog)
649
+ menu.append(new_task_menu_item)
650
+
651
+ self.do_not_disturb_menu_item = Gtk.MenuItem.new_with_label("Do Not Disturb")
652
+ if 'do-not-disturb' in self.session:
653
+ self.do_not_disturb_menu_item.set_label("Disable Do Not Disturb")
654
+ self.do_not_disturb_menu_item.connect("activate",self.toggle_do_not_disturb)
655
+ menu.append(self.do_not_disturb_menu_item)
656
+
657
+ quit_menu_item = Gtk.MenuItem.new_with_label("Quit")
658
+ quit_menu_item.connect("activate",self.quit)
659
+ menu.append(quit_menu_item)
660
+
661
+ if self.is_running:
662
+ self.task_running_menu_additions()
663
+
664
+ menu.show_all()
665
+
666
+
667
+ def open_session_options_dialog(self, w = None, params = None):
668
+
669
+ if hasattr(self, 'session_options_dialog'):
670
+ # print("present existing session_options_dialog",self.session_options_dialog)
671
+ self.session_options_dialog.present()
672
+ else:
673
+ self.session_options_dialog = SessionOptionsDialog(self,params)
674
+ self.session_options_dialog.show_all()
675
+
676
+
677
+ def open_settings_window(self, w = None, **kwargs):
678
+
679
+ if hasattr(self, 'settings_window'):
680
+ self.settings_window.present()
681
+ else:
682
+ self.settings_window = SettingsWindow(self,**kwargs)
683
+ self.settings_window.show_all()
684
+
685
+
686
+ def open_task_window(self, w = None, data = None):
687
+ self.taskwindow = TaskWindow(self, data)
688
+
689
+
690
+ def open_new_task_dialog(self, w = None, passed_data = None):
691
+
692
+ if hasattr(self, 'new_task_dialog'):
693
+ self.new_task_dialog.present()
694
+ else:
695
+ self.new_task_dialog = NewTaskWDialog(self,passed_data)
696
+ self.new_task_dialog.show_all()
697
+
698
+
699
+ def signal_handler(self, sig, frame):
700
+ dbg('Signal received',sig,s='signals')
701
+
702
+ if sig == signal.SIGUSR1:
703
+ # try:
704
+ # print('TaskWindow._instance',TaskWindow._instance)
705
+ # if TaskWindow._instance:
706
+ # self.taskwindow.destroy()
707
+ # else:
708
+ # self.open_task_window()
709
+
710
+ # except AttributeError:
711
+ self.open_task_window()
712
+
713
+ elif sig == signal.SIGUSR2:
714
+ self.open_session_options_dialog()
715
+
716
+ else:
717
+ dbg("no handler for received signal",s='signals',l=3)
718
+
719
+ def check_pipe(self):
720
+ print("Listening to pipe at ",pipe_file)
721
+ try:
722
+ with open(pipe_file, "r") as pipe:
723
+ data = pipe.read().strip()
724
+
725
+ print("check_pipe ")
726
+ print(data)
727
+
728
+ pipe.close()
729
+
730
+ # TODO: add registry of special commands
731
+ # if data in ['quit','open_task_window']:
732
+ # TODO: how to all dynamic function name here?
733
+ # GLib.idle_add(self.open_task_window)
734
+
735
+
736
+ if data == 'quit':
737
+ GLib.idle_add(self.quit)
738
+
739
+ elif data == 'open_task_window':
740
+ GLib.idle_add(self.open_task_window)
741
+
742
+ elif data == 'stop':
743
+ GLib.idle_add(self.stop_task)
744
+
745
+
746
+ else:
747
+ search_results = utils.taskindex_search('"'+str(data)+'"')
748
+ if len(search_results) > 0:
749
+ self.start_task(None, utils.first(search_results)['id'])
750
+
751
+ else:
752
+ error_notice('Commandline Task failed','Could not find a task matching argument '+str(data))
753
+
754
+ except FileNotFoundError as e:
755
+ print("Error starting pip listener, CLI will not work",e)
756
+
757
+ # keep listening
758
+ self.check_pipe()
759
+
760
+
761
+ def startup():
762
+ # print("startup ",sys.argv)
763
+
764
+ ''' if called with no arguments send shutdown to pipe, wait 2 seconds delete pipe file and launch application. if called with arguments send args to pipe if it exist otherwise '''
765
+
766
+ parser = argparse.ArgumentParser(
767
+ # prog='ProgramName',
768
+ # description='What the program does',
769
+ # epilog='Text at the bottom of help'
770
+ )
771
+
772
+ parser.add_argument('task',nargs='?') # optional positional argument
773
+ parser.add_argument('-s', '--debug_systems', nargs="*", default=[]) # option that takes a value
774
+ parser.add_argument('-l', '--debug_level', default=1) # option that takes a value
775
+ parser.add_argument('-f', '--force', action='store_true', help="Force restart by deleting existing named pipe") # on/off flag
776
+ # parser.add_argument('-v', '--verbose', action='store_true') # on/off flag
777
+
778
+ args = parser.parse_args()
779
+ dbg(args)
780
+
781
+ conf.debug_level = int(args.debug_level)
782
+ conf.debug_systems = args.debug_systems
783
+
784
+ if args.force:
785
+ print("Lanched with --force flag, forcibly deleting old pipe")
786
+ try:
787
+ os.remove(pipe_file)
788
+ except Exception as e:
789
+ print(e)
790
+
791
+
792
+ try:
793
+ os.mkfifo(pipe_file)
794
+ dbg("Named pipe created successfully!", s="cli")
795
+
796
+ signal.signal(signal.SIGUSR1, Application.signal_handler)
797
+ app = Application()
798
+
799
+ if args.task:
800
+ print("Writing args.task to pipe", args.task)
801
+ with open(pipe_file, "w") as pipeout:
802
+ pipeout.write(args.task)
803
+ pipeout.close()
804
+
805
+
806
+ except FileExistsError:
807
+ dbg("Named pipe exists, application must be running (or improperly shut down.) ",s="cli")
808
+
809
+ # if args: pass to pipe and exit
810
+ if args.task:
811
+ pipe_line = args.task
812
+ else:
813
+ #otherwise open task window
814
+ pipe_line = "open_task_window"
815
+
816
+
817
+ dbg("Writing arg to pipe ",pipe_line, s="cli")
818
+
819
+ with open(pipe_file, "w") as pipeout:
820
+ pipeout.write(pipe_line)
821
+ pipeout.close()
822
+
823
+ exit()
824
+
825
+
826
+
827
+ except Exception as e:
828
+ print(f"Named pipe creation failed: {e}")
829
+
830
+
831
+ if __name__ == "__main__":
832
+ startup()
833
+ # print("called with garv",sys.argv)
834
+ # signal.signal(signal.SIGUSR1, Application.signal_handler) # works
835
+ # app = Application()
836
+
837
+
838
+