nowfocus 0.2.8__py3-none-any.whl → 0.2.12__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.
nowfocus/__init__.py CHANGED
@@ -1,68 +1 @@
1
- import os
2
-
3
- def startup():
4
- # print("startup ",sys.argv)
5
-
6
- ''' 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 '''
7
-
8
- parser = argparse.ArgumentParser(
9
- # prog='ProgramName',
10
- # description='What the program does',
11
- # epilog='Text at the bottom of help'
12
- )
13
-
14
- parser.add_argument('task',nargs='?') # optional positional argument
15
- parser.add_argument('-s', '--debug_systems', nargs="*", default=[]) # option that takes a value
16
- parser.add_argument('-l', '--debug_level', default=1) # option that takes a value
17
- parser.add_argument('-f', '--force', action='store_true', help="Force restart by deleting existing named pipe") # on/off flag
18
- # parser.add_argument('-v', '--verbose', action='store_true') # on/off flag
19
-
20
- args = parser.parse_args()
21
- print(args)
22
-
23
- conf.debug_level = int(args.debug_level)
24
- conf.debug_systems = args.debug_systems
25
-
26
- if args.force:
27
- print("Lanched with --force flag, forcibly deleting old pipe")
28
- try:
29
- os.remove(pipe_file)
30
- except Exception as e:
31
- print(e)
32
-
33
- try:
34
- os.mkfifo(pipe_file)
35
-
36
- signal.signal(signal.SIGUSR1, Application.signal_handler)
37
- app = Application()
38
-
39
- if args.task:
40
- print("Writing args.task to pipe", args.task)
41
- with open(pipe_file, "w") as pipeout:
42
- pipeout.write(args.task)
43
- pipeout.close()
44
-
45
-
46
- except FileExistsError:
47
- print("Named pipe exists, application must be running (or improperly shut down.) ")
48
-
49
- # if args: pass to pipe and exit
50
- if args.task:
51
- pipe_line = args.task
52
- else:
53
- pipe_line = "open_task_window"
54
-
55
-
56
- print("Writing arg ",pipe_line," to pipe")
57
-
58
- with open(pipe_file, "w") as pipeout:
59
- pipeout.write(pipe_line)
60
- pipeout.close()
61
-
62
- exit()
63
-
64
-
65
-
66
- except Exception as e:
67
- print(f"Named pipe creation failed: {e}")
68
1
 
nowfocus/__main__.py CHANGED
@@ -50,7 +50,6 @@ from session_options import SessionOptionsDialog
50
50
  dbg(conf.user,l=3,s='user_settings')
51
51
 
52
52
  setproctitle.setproctitle(conf.app_name)
53
- pipe_file = os.path.dirname(os.path.realpath(__file__))+"/"+conf.app_id+"_pipe"
54
53
 
55
54
  print(conf.app_name +" running from " + os.path.dirname(os.path.realpath(__file__)))
56
55
 
@@ -106,6 +105,8 @@ class Application(Gtk.Application):
106
105
  except Exception as e:
107
106
  dbg("Error resuming session",e,l=1)
108
107
 
108
+ utils.start_todo_file_watchers()
109
+
109
110
  self.pipethread = threading.Thread(target=self.check_pipe)
110
111
  self.pipethread.daemon = True
111
112
  self.pipethread.start()
@@ -163,11 +164,11 @@ class Application(Gtk.Application):
163
164
  print('Caching active session', self.session['label'])
164
165
  db_set_session_cache(self.session)
165
166
  try:
166
- # print("before os.remove(pipe_file)")
167
- os.remove(pipe_file)
167
+ # print("before os.remove(conf.pipe)")
168
+ os.remove(conf.pipe)
168
169
  print("Pipe removed")
169
170
  except Exception as e:
170
- print("Error removing pipe_file in quit",e)
171
+ print("Error removing conf.pipe in quit",e)
171
172
 
172
173
  notify.uninit()
173
174
  Gtk.main_quit()
@@ -217,6 +218,8 @@ class Application(Gtk.Application):
217
218
  if (int(time_difference(conf.todo_sync_time)) / 60) > conf.user['todolist_refresh_interval'] * 60 :
218
219
  self.async_refresh()
219
220
 
221
+ # TODO: use individual todo_refresh_times
222
+
220
223
  minutes = (int(self.session['duration']) / 60)
221
224
 
222
225
  if(self.is_running == False):
@@ -320,6 +323,13 @@ class Application(Gtk.Application):
320
323
  # https://lazka.github.io/pgi-docs/#AyatanaAppIndicator3-0.1/classes/Indicator.html#AyatanaAppIndicator3.Indicator.set_label
321
324
  indicator.set_label(label, "Wide")
322
325
 
326
+ for todo in conf.todo_sync_required:
327
+ # print('tick noticed a todo needing refreshment, time since refresh: ',time_difference(conf.todo_sync_times[todo]))
328
+
329
+ if time_difference(conf.todo_sync_times[todo]) > 4:
330
+ # print('tick noticed a todo needing refreshment')
331
+ self.async_refresh(None,conf.user['todolists'][todo])
332
+ conf.todo_sync_required = {}
323
333
  return True
324
334
 
325
335
 
@@ -431,13 +441,23 @@ class Application(Gtk.Application):
431
441
 
432
442
  def mark_done(self, w=None, task = None):
433
443
  ''' second (task) argument is required and must be a task object '''
444
+
434
445
  print("Mark Task done")
435
446
  print(task)
436
447
 
437
448
  todolist_conf = conf.user['todolists'][task['todolist']]
438
449
 
439
450
  try:
451
+
440
452
  done_thread = threading.Thread(target=conf.todo_connectors[todolist_conf['type']].mark_task_done, args=(task,) )
453
+ conf.todo_sync_times[todolist_conf['id']] = now() # this is to avoid causing a refresh, perhaps not the best though
454
+
455
+ # Other Options:
456
+ # make a custom class extending Thread with callback method that runs del conf.file_watch_ignores[todolist_conf['id']]
457
+ # Complicated
458
+ # deal with file_watch_ignores in the connector
459
+ # poor seperation
460
+ #
441
461
  done_thread.start()
442
462
 
443
463
  db_query("UPDATE tasks set status = '0' WHERE id = ? ",(task['id'],) )
@@ -454,7 +474,7 @@ class Application(Gtk.Application):
454
474
 
455
475
 
456
476
  def stop_task(self, w = '', action = 'save', custom_end_time=None):
457
- ''' supported actions are save,cancel,mark_done '''
477
+ ''' supported actions are save, cancel, mark_done '''
458
478
 
459
479
  list_menus = self.list_menus
460
480
  menu_tasks = self.menu_tasks
@@ -543,7 +563,6 @@ class Application(Gtk.Application):
543
563
  i = Gtk.ImageMenuItem.new_with_label("Edit Session")
544
564
  i.set_image(Gtk.Image.new_from_file(os.path.abspath('icon/edit.svg')))
545
565
 
546
- # i.set_image(Gtk.Image.new_from_file('/usr/share/icons/Yaru/scalable/status/software-installed-symbolic.svg'))
547
566
  i.set_always_show_image(True)
548
567
  i.connect("activate", self.open_session_options_dialog,'from_menu')
549
568
  self.menu.insert(i,0)
@@ -551,33 +570,34 @@ class Application(Gtk.Application):
551
570
  i = Gtk.ImageMenuItem.new_with_label("Pause" )
552
571
  i.set_image(Gtk.Image.new_from_file(os.path.abspath('icon/pause.svg')))
553
572
 
554
- # i.set_image(Gtk.Image.new_from_file('/usr/share/icons/Yaru/scalable/multimedia/pause-symbolic.svg'))
555
573
  i.set_always_show_image(True)
556
574
  i.connect("activate", self.stop_task)
557
575
  self.menu.insert(i,0)
558
576
 
559
577
 
560
- def async_refresh(self, w=None):
578
+ def async_refresh(self, w=None, single_todo = None):
561
579
 
562
580
  self.indicator.set_label("Refreshing Todolists", "Wide")
563
581
  menu_item = Gtk.MenuItem.new_with_label("Refreshing Todolists")
564
582
  self.menu.append(menu_item)
565
583
  self.menu.show_all()
566
584
 
567
- connectors_thread = threading.Thread(target=self.async_refresh_inner)
585
+ connectors_thread = threading.Thread(target=self.async_refresh_inner,args=(single_todo,))
568
586
  connectors_thread.start()
569
587
 
570
588
 
571
- def async_refresh_inner(self):
589
+ def async_refresh_inner(self, single_todo = None):
572
590
  # dbg("async refresh started",s="todoloading",l=3)
573
- utils.get_todolists()
591
+ if single_todo:
592
+ utils.refresh_todolist(single_todo)
593
+ utils.reindex()
594
+
595
+ else:
596
+ utils.get_todolists()
597
+
574
598
  dbg("async refresh complete",s="todoloading",l=3)
575
599
  GLib.idle_add(self.update_menu)
576
600
 
577
- try:
578
- GLib.idle_add(self.taskwindow.refresh_search_cache)
579
- except Exception:
580
- pass
581
601
 
582
602
 
583
603
  def update_menu(self, w = ''):
@@ -727,10 +747,10 @@ class Application(Gtk.Application):
727
747
  dbg("no handler for received signal",s='signals',l=3)
728
748
 
729
749
  def check_pipe(self):
730
- # print("Listening to pipe at ",pipe_file)
750
+ # print("Listening to pipe at ",conf.pipe)
731
751
 
732
752
  try:
733
- with open(pipe_file, "r") as pipe:
753
+ with open(conf.pipe, "r") as pipe:
734
754
  data = pipe.read().strip()
735
755
 
736
756
  print("check_pipe ")
@@ -739,10 +759,8 @@ class Application(Gtk.Application):
739
759
  pipe.close()
740
760
 
741
761
  # TODO: add registry of special commands
762
+ # How to handle function arguments? (for example refresh a todolist with it's id)
742
763
  # if data in ['quit','open_task_window']:
743
- # TODO: how to all dynamic function name here?
744
- # GLib.idle_add(self.open_task_window)
745
-
746
764
 
747
765
  if data == 'quit':
748
766
  GLib.idle_add(self.quit)
@@ -795,13 +813,13 @@ def startup():
795
813
  if args.force:
796
814
  print("Lanched with --force flag, forcibly deleting old pipe")
797
815
  try:
798
- os.remove(pipe_file)
816
+ os.remove(conf.pipe)
799
817
  except Exception as e:
800
818
  print(e)
801
819
 
802
820
 
803
821
  try:
804
- os.mkfifo(pipe_file)
822
+ os.mkfifo(conf.pipe)
805
823
  dbg("Named pipe created successfully!", s="cli")
806
824
 
807
825
  signal.signal(signal.SIGUSR1, Application.signal_handler)
@@ -809,7 +827,7 @@ def startup():
809
827
 
810
828
  if args.task:
811
829
  print("Writing args.task to pipe", args.task)
812
- with open(pipe_file, "w") as pipeout:
830
+ with open(conf.pipe, "w") as pipeout:
813
831
  pipeout.write(args.task)
814
832
  pipeout.close()
815
833
 
@@ -827,7 +845,7 @@ def startup():
827
845
 
828
846
  dbg("Writing arg to pipe ",pipe_line, s="cli")
829
847
 
830
- with open(pipe_file, "w") as pipeout:
848
+ with open(conf.pipe, "w") as pipeout:
831
849
  pipeout.write(pipe_line)
832
850
  pipeout.close()
833
851
 
nowfocus/conf.py CHANGED
@@ -22,20 +22,24 @@ Path(user_settings_dir).mkdir(parents=True, exist_ok=True)
22
22
 
23
23
  debug_level = 1 # dev value
24
24
  debug_systems = []
25
+ pipe = "/tmp/"+app_id+"-pipe" # Will that work?
25
26
 
26
27
  # key and type must be the same, (Seems redundant but it's is quite helpful)
27
28
  connectors = {
28
29
  "todolists":{
29
- 'txt':{'id':'txt','type':'txt','label':'','file':'',"status":True,'timetracker':''},
30
+ 'txt':{'id':'txt','type':'txt','label':'','file':'', "watch_file":True, "status":True,'timetracker':''},
30
31
 
31
32
  'trello':{'id':'trello','type':'trello','label':'Trello','APIKey':'aecf1d7791b4f1a4bb7d6ca5827ba0d3', 'token':'',"status":True,'timetracker':''},
32
33
 
33
34
  'vikunja':{'id':'vikunja','type':'vikunja','label':'Vikunja','url':'http://localhost:3456/', 'token':'',"username":"","status":True,'timetracker':''},
34
35
 
35
36
  'caldav':{'id':'caldav','type':'caldav','label':'CalDav Todo','url':'http://localhost:3456/','password': "",'username': '',"status":True,'timetracker':''},
37
+
36
38
  'psc_timetracker':{'id':'psc_timetracker','type':'psc_timetracker','label':'Timetracker','url':'https://photosynth.ca/timetracker/','key':'',"status":True,'timetracker':'Timetracker'},
39
+
37
40
  'taskwarrior':{'id':'taskwarrior','type':'taskwarrior','label':'TaskWarrior',"status":True,'timetracker':''},
38
- 'todotxt':{'id':'todotxt','type':'todotxt','label':'','file':"","status":True,'timetracker':''},
41
+
42
+ 'todotxt':{'id':'todotxt','type':'todotxt','label':'','file':"", "watch_file":True, "status":True,'timetracker':''},
39
43
  },
40
44
  "timetrackers":{
41
45
  'csv':{'id':'csv','type':'csv','label':'CSV file','file':'sessions.csv',"status":True},
@@ -46,18 +50,21 @@ connectors = {
46
50
  }
47
51
 
48
52
  todo_sync_time = datetime.now()
53
+ todo_sync_times = {}
49
54
 
50
55
  todo_sync_required = {}
56
+ file_watchers = {}
57
+ file_watch_ignores = {}
51
58
 
52
59
  prototype_settings = {
53
- "pomodoro_interval": 26,
60
+ "pomodoro_interval": 40,
54
61
  "open_task_window_fullscreen": True,
55
62
  "randomness_interrupt_interval":5,
56
63
  "default_text": "What am I doing?",
57
- "todolist_refresh_interval":6,
64
+ "todolist_refresh_interval":1,
58
65
  "version":0.2,
59
66
  "display_todolist_as_top_level_list":'auto',
60
- 'max_top_level_menu_items':18,
67
+ 'max_top_level_menu_items':10,
61
68
  # 'tick_interval':1,
62
69
  'hours_search_timeframe':'this year',
63
70
  'invoice_hourly_rate':0,
@@ -20,24 +20,6 @@ import utils
20
20
  # }
21
21
  # }
22
22
 
23
- # def watch(user_conf):
24
- # import pyinotify
25
-
26
- # """Watch for modifications of the todo file with pyinotify."""
27
- # wm = pyinotify.WatchManager()
28
- # notifier = pyinotify.ThreadedNotifier(wm, mark_changed, user_conf)
29
- # notifier.start()
30
-
31
- # # wm.add_watch(os.path.dirname(user_conf['file']), pyinotify.IN_MODIFY | pyinotify.IN_MOVED_TO) # Watch whole folder. Perhaps more reliable
32
- # wm.add_watch(user_conf['file'], pyinotify.IN_MODIFY | pyinotify.IN_MOVED_TO) # just the file. Perhaps better
33
-
34
-
35
- # def mark_changed(event, user_conf):
36
- # ''' This is in a thread '''
37
- # if event.pathname == user_conf['file']:
38
- # conf.todo_sync_required[user_conf['id']] = True
39
-
40
-
41
23
 
42
24
  def task_id(user_conf, t):
43
25
  ''' The task parameter must have at least "label", "todolist" and "parent_id" '''
@@ -71,7 +53,8 @@ def add_new_task(user_conf,list,task_label):
71
53
  }
72
54
  }
73
55
  return t
74
-
56
+
57
+
75
58
  def mark_task_done(task):
76
59
  '''Return True on success False on error'''
77
60
 
@@ -11,24 +11,32 @@ def add_new_task(user_conf,list,task_label):
11
11
  # read a list of lines into data
12
12
  data = file.readlines()
13
13
 
14
- if list['id'] == user_conf['id']:
15
- # Top level insert
16
- new_line = task_label+"\n"
17
- line_no = len(data) + 1
18
- else:
19
- # Sub list insert
20
- # TODO: Add it at the end of the list rather than the start ...
21
- # Yup. rjust(indent + len)
22
-
23
- indent = len(list['data']['original_line']) - len(list['data']['original_line'].lstrip()) + 4 #BUG: check indent of next line instead of arbitrarily using 4 spaces
14
+ # New Version
15
+
16
+ line_no = get_line_no(list,data) + 1
17
+ new_line = list['data']['indent']+task_label+"\n"
18
+
19
+ # # Old version without recorded indent
20
+
21
+ # if list['id'] == user_conf['id']:
22
+ # # Top level insert
23
+ # new_line = task_label+"\n"
24
+ # line_no = len(data) + 1
25
+ # else:
26
+ # Sub list insert
27
+ # TODO: Add it at the end of the list rather than the start ...
28
+ # Yup. rjust(indent + len)
29
+
30
+
31
+ # indent = len(list['data']['original_line']) - len(list['data']['original_line'].lstrip()) + 4 #BUG: check indent of next line instead of arbitrarily using 4 spaces
24
32
 
25
- new_line = task_label+"\n"
26
- # I'm sure there's a better way to add a bunch of spaces to to a string
27
- while indent > 0:
28
- new_line = " " + new_line
29
- indent = indent - 1
33
+ # new_line = task_label+"\n"
34
+ # # I'm sure there's a better way to add a bunch of spaces to to a string
35
+ # while indent > 0:
36
+ # new_line = " " + new_line
37
+ # indent = indent - 1
30
38
 
31
- line_no = get_line_no(list,data) + 1
39
+ # line_no = get_line_no(list,data) + 1
32
40
 
33
41
  data.insert(line_no,new_line)
34
42
 
@@ -128,7 +136,8 @@ def get_todos(user_conf):
128
136
  'data':{
129
137
  'line_no':0,
130
138
  'original_line':'',
131
- 'accepts_tasks':True
139
+ 'accepts_tasks':True,
140
+ 'indent':'',
132
141
  }
133
142
  }
134
143
 
@@ -149,7 +158,7 @@ def get_todos(user_conf):
149
158
  # file_uri = "todo.txt"
150
159
  line_no = -1
151
160
 
152
- with open(file_uri) as file:
161
+ with open(file_uri,"r") as file:
153
162
  for line in file:
154
163
 
155
164
  line_no = line_no + 1
@@ -171,11 +180,13 @@ def get_todos(user_conf):
171
180
  #TODO: use markdown title syntax as list name in addition to indentation
172
181
 
173
182
  indent = len(line) - len(line.lstrip())
183
+ indent_str = line[0:indent]
174
184
 
175
185
  if indent > prev_indent:
176
- # prev item is a sub_list header, move it to lists
186
+ # prev item is a sub_list header, copy it to lists
177
187
  lists[prev_id] = tasks[prev_id]
178
- del tasks[prev_id]
188
+ lists[prev_id]['data']['indent'] = indent_str
189
+ # del tasks[prev_id]
179
190
  current_list.append(prev_id)
180
191
  current_list_label.append(prev_label)
181
192
  current_indent.append(indent)
@@ -294,14 +294,16 @@ def get_todos(user_conf):
294
294
  return todos
295
295
 
296
296
 
297
- def launch(user_conf, item = None, category = None):
297
+ def launch(user_conf, item = None, category = 'task'):
298
298
  ''' Open Vikunja '''
299
+ utils.dbg("vikunja.py launch","user_conf", user_conf,"item",item,'category',category,l=3, s='vikunja')
300
+
299
301
  url = user_conf['url']
300
302
 
301
- if item and category:
302
- if category == 'list':
303
+ if item:
304
+ if category in ('list','lists'):
303
305
  url += 'projects/'+str(item['data']['id'])
304
- elif category == 'task':
306
+ else:
305
307
  url += 'tasks/'+str(item['data']['id'])
306
308
 
307
309
  utils.open_external(url)
@@ -77,7 +77,7 @@ class NewTaskWDialog(Gtk.Dialog):
77
77
 
78
78
  if response == 1 or response == 2:
79
79
 
80
- task_label = self.task_label_entry.get_text()
80
+ task_label = self.task_label_entry.get_text().strip()
81
81
 
82
82
  if not task_label:
83
83
  error_notice("Please Enter a name before saving new task")
@@ -86,7 +86,10 @@ class NewTaskWDialog(Gtk.Dialog):
86
86
  parent_list = self.selected_list
87
87
  todolist_conf = conf.user['todolists'][parent_list['todolist']]
88
88
  try:
89
+
90
+ conf.file_watch_ignores[todolist_conf['id']] = True
89
91
  task = conf.todo_connectors[todolist_conf['type']].add_new_task(todolist_conf,parent_list,task_label)
92
+ del conf.file_watch_ignores[todolist_conf['id']]
90
93
 
91
94
  dbg('connector add task response',task)
92
95
 
nowfocus/settings.py CHANGED
@@ -17,7 +17,7 @@ from utils import *
17
17
  from session_edit_dialog import SessionEditDialog
18
18
 
19
19
  def update_user_setting(widget = None, conf_ref = None, key = None, val = None, callback = None):
20
- ''' Update conf.user by passing an conf_ref user.conf pointer (exe. ' conf.user['task_commands']['task_id_email']) plus key for the value to be updated and a new value for it
20
+ ''' Update conf.user by passing a conf_ref user.conf pointer (exe. ' conf.user['task_commands']['task_id_email']) plus key for the value to be updated and a new value for it
21
21
  val can be a callable (with no required parameters), (so a widgets get_active can be used) in which case it's return value will be used '''
22
22
 
23
23
  print("update user_setting")
@@ -37,13 +37,6 @@ def update_user_setting(widget = None, conf_ref = None, key = None, val = None,
37
37
  if conf_ref and key:
38
38
  conf_ref[key] = val
39
39
 
40
- # print('object with updated '+key)
41
- # print(conf_ref)
42
-
43
- # print('(hopefully) updated conf.user before saving')
44
- # pretty_print(conf.user)
45
-
46
- # Disabled just for test
47
40
  save_user_settings()
48
41
 
49
42
  if callback:
@@ -57,13 +50,35 @@ def update_user_settings_value(widget = None, key_val = None):
57
50
 
58
51
  if key_val:
59
52
  conf.user[key_val['key']] = key_val['value']
60
-
61
53
  save_user_settings()
62
54
 
63
- def update_connector_setting(widget, app, connector_category, id, key, widget_getter = "Add this feature! :)"):
55
+
56
+ def update_connector_status(widget, app, connector_category, id, key, widget_getter = "Add this feature! :)"):
64
57
  conf.user[connector_category][id][key] = widget.get_active()
65
58
  save_user_settings()
66
- app.async_refresh()
59
+ after_todo_settings_change(app,id)
60
+ # app.async_refresh()
61
+
62
+
63
+ def after_todo_settings_change(app, todo = None):
64
+ if todo:
65
+ todo = utils.get_todo_by_id(todo)
66
+ if todo['status']:
67
+
68
+ app.async_refresh(todo)
69
+ utils.stop_todo_file_watchers(todo)
70
+ utils.start_todo_file_watchers(todo)
71
+
72
+ else:
73
+ utils.db_deactivate_todo(todo['id'])
74
+ utils.reindex()
75
+ utils.stop_todo_file_watchers(todo)
76
+ app.update_menu()
77
+
78
+ else:
79
+ app.async_refresh()
80
+ utils.stop_todo_file_watchers()
81
+ utils.start_todo_file_watchers()
67
82
 
68
83
 
69
84
  class SettingsWindow(Gtk.Window):
@@ -344,9 +359,12 @@ class SettingsWindow(Gtk.Window):
344
359
  print_time_totals_button.connect("clicked", self.app.print_time_totals)
345
360
  self.sessions_page.add(print_time_totals_button)
346
361
 
347
- show_long_sessions_button = Gtk.Button(label="Review long sessions")
348
- show_long_sessions_button.connect("clicked", self.show_sessions, 'Longest Sessions', 'duration')
362
+ show_recent_sessions_button = Gtk.Button(label="Show recent sessions")
363
+ show_recent_sessions_button.connect("clicked", self.show_sessions, 'Recent Sessions', 'start_time')
364
+ self.sessions_page.add(show_recent_sessions_button)
349
365
 
366
+ show_long_sessions_button = Gtk.Button(label="Show long sessions")
367
+ show_long_sessions_button.connect("clicked", self.show_sessions, 'Longest Sessions', 'duration')
350
368
  self.sessions_page.add(show_long_sessions_button)
351
369
 
352
370
  self.sessions_scroller = self.scroll_box(self.sessions_page)
@@ -539,7 +557,7 @@ class SettingsWindow(Gtk.Window):
539
557
 
540
558
  checkbutton = Gtk.CheckButton(label=data['label'])
541
559
  checkbutton.set_active(data['status'])
542
- checkbutton.connect("toggled", update_connector_setting, self.app, connector_category, id, 'status')
560
+ checkbutton.connect("toggled", update_connector_status, self.app, connector_category, id, 'status')
543
561
  box.pack_start(checkbutton,False,False,5)
544
562
 
545
563
  edit_button = Gtk.Button(label="Edit")
@@ -566,9 +584,13 @@ class SettingsWindow(Gtk.Window):
566
584
  if connector_category == 'todolists':
567
585
  db_query("DELETE FROM tasks WHERE todolist = ? AND id NOT IN (SELECT task_id FROM sessions)",(id,))
568
586
  db_query("DELETE FROM lists WHERE todolist = ?",(id,))
587
+ utils.reindex()
588
+ utils.stop_todo_file_watchers(connector)
589
+ self.app.update_menu()
569
590
 
570
591
  del conf.user[connector_category][id]
571
592
  save_user_settings()
593
+
572
594
  self.show_connectors(connector_category)
573
595
 
574
596
 
@@ -596,7 +618,7 @@ class SettingsWindow(Gtk.Window):
596
618
  else:
597
619
  connector_type = self.inputs['new_'+connector_category+'_type'].get_active_text()
598
620
 
599
- print("type in edit_add_connector %s" % (connector_type))
621
+ # print("type in edit_add_connector %s" % (connector_type))
600
622
 
601
623
  dialog = EditAddConnectorDialog(self, connector_category, connector_type, id)
602
624
  response = dialog.run()
@@ -606,11 +628,6 @@ class SettingsWindow(Gtk.Window):
606
628
  print("The OK button was clicked")
607
629
  self.show_connectors(connector_category)
608
630
 
609
- # TODO: Show connector errors
610
-
611
- self.app.async_refresh()
612
-
613
-
614
631
  elif response == Gtk.ResponseType.CANCEL:
615
632
  print("The Cancel button was clicked")
616
633
 
@@ -912,6 +929,10 @@ class EditAddConnectorDialog(Gtk.Dialog):
912
929
 
913
930
  self.entries = {}
914
931
 
932
+ self.app = parent.app
933
+ self.set_default_size(400,400)
934
+
935
+
915
936
  self.set_border_width(15)
916
937
 
917
938
  box = self.get_content_area()
@@ -932,7 +953,7 @@ class EditAddConnectorDialog(Gtk.Dialog):
932
953
 
933
954
  for key in fields:
934
955
 
935
- if key in ['id','type','timetracker','status']:
956
+ if key in ['id','type','timetracker','status','watch_file']:
936
957
  continue
937
958
 
938
959
  label = Gtk.Label(label=key)
@@ -979,13 +1000,13 @@ class EditAddConnectorDialog(Gtk.Dialog):
979
1000
  )
980
1001
 
981
1002
  self.show_all()
982
- self.connect("response", self.on_response, connector_category, connector_type, id)
1003
+ self.connect("response", self.on_response, connector_category, connector_type, id, data)
983
1004
 
984
1005
 
985
- def on_response(self, widget, response, connector_category, connector_type, id):
1006
+ def on_response(self, widget, response, connector_category, connector_type, id, initial_data):
986
1007
  if response == Gtk.ResponseType.OK:
987
1008
 
988
- new = {'type':connector_type,'status':True}
1009
+ new = initial_data
989
1010
 
990
1011
  for key in self.entries:
991
1012
 
@@ -1013,7 +1034,16 @@ class EditAddConnectorDialog(Gtk.Dialog):
1013
1034
  save_user_settings()
1014
1035
 
1015
1036
  # load the connector module
1016
- conf.todo_connectors[new['type']] = importlib.import_module('connectors.'+new['type'])
1037
+ if connector_category == 'timetrackers':
1038
+ conf.timetracker_connectors[new['type']] = importlib.import_module('connectors.'+new['type'])
1039
+
1040
+ if connector_category == 'todolists':
1041
+
1042
+ conf.todo_connectors[new['type']] = importlib.import_module('connectors.'+new['type'])
1043
+
1044
+ utils.stop_todo_file_watchers(new)
1045
+ utils.start_todo_file_watchers(new)
1046
+ self.app.async_refresh(new)
1017
1047
 
1018
1048
 
1019
1049
  elif response == Gtk.ResponseType.CANCEL:
nowfocus/task_window.py CHANGED
@@ -231,12 +231,12 @@ class TaskWindow(Gtk.Window):
231
231
  self.search_cache = {}
232
232
  self.search_cache_refresh_time = now().strftime("%Y-%m-%d %H:%M:%S")
233
233
  self.task_search(self.taskEntry)
234
-
234
+
235
235
 
236
236
  def open_new_task_dialog(self,w = None):
237
237
  passed_data = {"label":self.taskEntry.get_text()}
238
238
  self.app.open_new_task_dialog(self, passed_data)
239
-
239
+
240
240
 
241
241
  def tick(self):
242
242
 
@@ -245,7 +245,7 @@ class TaskWindow(Gtk.Window):
245
245
  self.recreate_header()
246
246
 
247
247
  if utils.taskindex_updated_time() > self.search_cache_refresh_time:
248
- print("taskindex was updated, utils.taskindex_updated_time()", utils.taskindex_updated_time(), 'self.search_cache_refresh_time',self.search_cache_refresh_time)
248
+ # print("taskindex was updated, utils.taskindex_updated_time()", utils.taskindex_updated_time(), 'self.search_cache_refresh_time',self.search_cache_refresh_time)
249
249
  self.refresh_search_cache()
250
250
  # else:
251
251
  # print("taskindex was not updated",current_data_version)
@@ -462,7 +462,7 @@ class TaskWindow(Gtk.Window):
462
462
  print("select_task task id",tid)
463
463
  t = utils.db_get_item_by_id(tid)
464
464
  # print('event',event)
465
- print("select_task self.modifyer_keys", self.modifyer_keys)
465
+ # print("select_task self.modifyer_keys", self.modifyer_keys)
466
466
 
467
467
  if self.modifyer_keys['control'] or event and event.button == 3: # Right-click (button 3) or control click context menu popover
468
468
 
nowfocus/utils.py CHANGED
@@ -8,7 +8,6 @@ from dateutil.relativedelta import relativedelta
8
8
  import copy
9
9
  import threading
10
10
 
11
-
12
11
  import sqlite3
13
12
  from contextlib import closing
14
13
  from pathlib import Path
@@ -19,6 +18,9 @@ from gi.repository import Gtk, GLib, Gdk
19
18
  gi.require_version('Notify', '0.7')
20
19
  from gi.repository import Notify as notify
21
20
 
21
+ from watchdog.events import FileSystemEventHandler, FileSystemEvent
22
+ from watchdog.observers import Observer
23
+
22
24
  import conf
23
25
  notify.init(conf.app_name)
24
26
 
@@ -32,7 +34,7 @@ def dbg(*data, **kwargs):
32
34
  l=level {0:Error, 1:Warning, 2:Info, 3:Details}
33
35
  s=system (Debuggable systems: 'taskwindow','signals','todoloading','user_settings', 'targets')'''
34
36
 
35
- levels = {0:'Error', 1:'Warning', 2:'Info', 3:'Details'}
37
+ levels = {-1: 'CLI output', 0:'Error', 1:'Warning', 2:'Info', 3:'Details'}
36
38
 
37
39
  # default system and level
38
40
  system = ''
@@ -104,6 +106,7 @@ def time_to_sec(time=None):
104
106
  sec += int(v) * 60 ** k
105
107
  return sec
106
108
 
109
+
107
110
  def sec_to_time(sec):
108
111
 
109
112
  try:
@@ -115,14 +118,16 @@ def sec_to_time(sec):
115
118
  time = str("{:02d}".format(int(sec // 3600))) + ':' + str("{:02d}".format(int((sec % 3600) // 60))) + ':' + str("{:02d}".format(int(sec % 60)))
116
119
  return time.lstrip('0').lstrip(':')
117
120
 
121
+
118
122
  def now():
119
123
  return datetime.now()
120
- # return datetime.now(timezone.utc)
121
124
  # return datetime.now(timezone.utc).astimezone().tzinfo
122
125
 
126
+
123
127
  def start_of_day():
124
128
  return now().replace(hour=0,minute=0,second=0,microsecond=0)
125
129
 
130
+
126
131
  def time_difference(earlier, later = None):
127
132
  ''' Return seconds (float), later defaults to now'''
128
133
  # date_format = "%Y-%m-%dT%H:%M:%S.%f%z" # This can probably be updated / tossed (ActivityWatch format )
@@ -136,6 +141,7 @@ def time_difference(earlier, later = None):
136
141
 
137
142
  return difference.total_seconds()
138
143
 
144
+
139
145
  def validate_start_time_str(start_time_string):
140
146
  try:
141
147
  return datetime.strptime(start_time_string,'%Y-%m-%d %H:%M:%S')
@@ -144,6 +150,7 @@ def validate_start_time_str(start_time_string):
144
150
  error_notice('Incorrect Start Time Format', start_time_string+" does no match %Y-%m-%d %H:%M:%S", e)
145
151
  return False
146
152
 
153
+
147
154
  def open_todo(w=None, i=None, item_type = 'tasks'):
148
155
  ''' run the launch() function from the todo connector for the provided i(tem) '''
149
156
  if not isinstance(i, dict):
@@ -156,15 +163,13 @@ def open_todo(w=None, i=None, item_type = 'tasks'):
156
163
 
157
164
  except Exception as e:
158
165
  # error_notice('Bonk', "error with "+ c['type']+ " open function ")
159
- dbg(('open_todo excption, trying get_connector_openable ',e))
166
+ dbg('open_todo excption, falling back to get_connector_openable ',e,s=todo['type'],l=1)
160
167
 
161
168
  get_connector_openable(None,todo)
162
-
169
+
163
170
 
164
171
  def get_connector_openable(widget = None, connector_dict = None, open_it = True):
165
172
 
166
- # if conf.todo_connectors[connector_dict['type']].launch(todo,i,item_type)
167
-
168
173
  for option in ['file','front_end_url','url','uri','open_command']:
169
174
 
170
175
  if option in connector_dict:
@@ -174,8 +179,8 @@ def get_connector_openable(widget = None, connector_dict = None, open_it = True)
174
179
  else:
175
180
  return connector_dict[option]
176
181
 
177
- print('ERROR: get_connector_openable failed to find an openable')
178
- print(connector_dict)
182
+ dbg('get_connector_openable failed for',l=1,s=connector_dict['type'])
183
+ dbg('connector_dict passed to get_connector_openable',connector_dict,l=3,s=connector_dict['type'])
179
184
  return False
180
185
 
181
186
 
@@ -274,6 +279,15 @@ def save_user_settings():
274
279
  json.dump(conf.user, settings_file)
275
280
 
276
281
 
282
+ def get_todo_by_file(file):
283
+ for id, todo in conf.user['todolists'].items():
284
+ if 'file' in todo and todo['file'] == file:
285
+ return todo
286
+
287
+ print('get_todo_by_file failed for', file)
288
+
289
+
290
+
277
291
  def db_query(sql,parameters=None,key=None,error_handling=1):
278
292
  '''error_handling: 2 = raise error, 1 = notify error, 0 = ignore errors '''
279
293
  # print('db_query parameters type: ',type(parameters))
@@ -356,7 +370,6 @@ def db_update():
356
370
  db_query("ALTER TABLE sessions ADD COLUMN extended_label TEXT")
357
371
 
358
372
  # add default values for priority and data
359
-
360
373
  # Since these column are just a cache, replace instead of copying columns
361
374
  db_query("ALTER TABLE tasks DROP COLUMN priority")
362
375
  db_query("ALTER TABLE tasks DROP COLUMN data")
@@ -422,6 +435,7 @@ def reindex_all():
422
435
  db_query("UPDATE taskindex set priority = ? WHERE id = ?",(t['priority'],t['id']))
423
436
 
424
437
  db_query("REPLACE INTO system(field, value) VALUES(?,?)",('taskindex_update_time',now().strftime("%Y-%m-%d %H:%M:%S")))
438
+ return True
425
439
 
426
440
 
427
441
  def reindex_one(t):
@@ -437,10 +451,11 @@ def reindex_one(t):
437
451
  print("reindexed",t['label'])
438
452
  # print(db_query("SELECT * FROM taskindex where id = ?",(t['id'],)))
439
453
  db_query("REPLACE INTO system(field, value) VALUES(?,?)",('taskindex_update_time',now().strftime("%Y-%m-%d %H:%M:%S")))
454
+ return True
440
455
 
441
456
 
442
457
  def taskindex_updated_time():
443
- ''' returns String (no datetime obj '''
458
+ ''' returns system db taskindex_update_time string formatted as %Y-%m-%d %H:%M:%S '''
444
459
  update_time = db_query("SELECT value FROM system WHERE field = 'taskindex_update_time'")[0]['value']
445
460
 
446
461
  # print('taskindex_updated_time',update_time)
@@ -673,10 +688,6 @@ def db_cleanup(widget = None):
673
688
 
674
689
  db_query("UPDATE sessions SET extended_label = (SELECT extended_label FROM tasks WHERE id = task_id), parent_id = (SELECT parent_id FROM tasks WHERE id = task_id) WHERE task_id IN (SELECT id FROM tasks) AND extended_label != (SELECT extended_label FROM tasks WHERE id = task_id)")
675
690
 
676
-
677
- # SELECT * FROM sessions WHERE extended_label IS NULL;
678
- # UPDATE sessions SET extended_label = (SELECT extended_label FROM tasks WHERE id = task_id) WHERE extended_label IS NULL
679
-
680
691
  # db_query("SELECT label, todolist FROM tasks WHERE todolist NOT IN ?",(ids_sql,))
681
692
  # db_query("DELETE FROM tasks WHERE todolist NOT IN "+ids_sql)
682
693
  # db_query("DELETE FROM lists WHERE todolist NOT IN "+ids_sql)
@@ -704,6 +715,17 @@ def db_get_item_by_id(id,table = 'tasks'):
704
715
  dbg('db_get_item_by_id failed',e,l=0)
705
716
 
706
717
 
718
+ def get_todo_by_id(todo_or_todo_id = None):
719
+ ''' get user_conf todo connector data, does not validate '''
720
+ if todo_or_todo_id and isinstance(todo_or_todo_id, str):
721
+ if todo_or_todo_id in conf.user['todolists']:
722
+ return conf.user['todolists'][todo_or_todo_id]
723
+ else:
724
+ return None
725
+
726
+ return todo_or_todo_id
727
+
728
+
707
729
  def proc_db_item(i,table='tasks'):
708
730
  ''' Parse task or list data json and apply time target priority '''
709
731
  i['data'] = json.loads(i['data'])
@@ -867,6 +889,77 @@ def get_times(task):
867
889
  return o
868
890
 
869
891
 
892
+ class TodoModifiedEventHandler(FileSystemEventHandler):
893
+ def __init__(self,todo = None):
894
+ # print('todo in TodoModifiedEventHandler',todo)
895
+ self.todo = todo
896
+
897
+ # Can't use on_modified because of swap-in save method used by editors
898
+ # def on_modified(self, event: FileModifiedEvent) -> None:
899
+
900
+ def on_any_event(self, event: FileSystemEvent) -> None:
901
+
902
+ # print(event)
903
+ # src_path=PATH/todo.txt', dest_path='', event_type='modified', is_directory=False, is_synthetic=False
904
+ # print(event.src_path,event.event_type)
905
+
906
+ if event.src_path == self.todo['file'] and event.event_type not in ('closed','opened') and not event.is_directory:
907
+ if self.todo['id'] not in conf.file_watch_ignores:
908
+ # print("Refresh", event.src_path, event.event_type)
909
+ conf.todo_sync_required[self.todo['id']] = self.todo['id']
910
+ # else:
911
+ # print('currently ignoring changes to '+self.todo['label'])
912
+
913
+ return True
914
+
915
+
916
+ def watch_todo_file(todo):
917
+
918
+ dbg("Setting up file modification observer for", todo['file'],s=todo['type'])
919
+
920
+ event_handler = TodoModifiedEventHandler(todo)
921
+
922
+ observer = Observer()
923
+ observer.daemon = True
924
+ path,filename=todo['file'].rsplit('/',1)
925
+ observer.schedule(event_handler, path+'/', recursive=False)
926
+ observer.start()
927
+
928
+ return observer
929
+
930
+
931
+ def start_todo_file_watchers(todo = None):
932
+ print("starting todo watchers. todo",todo)
933
+ if todo:
934
+ if todo['status'] and 'watch_file' in todo:
935
+ conf.file_watchers[todo['id']] = watch_todo_file(todo)
936
+ else:
937
+ print('todo passed to start_todo_file_watchers is ineligable', todo)
938
+ else:
939
+ for id, todo in conf.user['todolists'].items():
940
+
941
+ if todo['status'] and 'watch_file' in todo:
942
+
943
+ conf.file_watchers[id] = watch_todo_file(todo)
944
+
945
+
946
+ def stop_todo_file_watchers(todo = None):
947
+
948
+ if todo:
949
+ if todo['id'] in conf.file_watchers:
950
+ conf.file_watchers[todo['id']].stop()
951
+ conf.file_watchers[todo['id']].join()
952
+ del conf.file_watchers[todo['id']]
953
+ else:
954
+ for id, watcher in conf.file_watchers.items():
955
+ watcher.stop()
956
+ watcher.join()
957
+ del conf.file_watchers[id]
958
+
959
+ print('file_watchers after stop_todo_file_watchers')
960
+ print(conf.file_watchers.keys())
961
+
962
+
870
963
  def db_deactivate_todo(id):
871
964
  dbg("deactivating todolist: "+id, s='todoloading')
872
965
  # Delete inactive todo items (except where there are sessions for them)
@@ -876,6 +969,35 @@ def db_deactivate_todo(id):
876
969
  db_query("UPDATE tasks SET status = -1 WHERE todolist = ?",(id,))
877
970
 
878
971
 
972
+ def refresh_todolist(todo, catch_errors = False):
973
+
974
+ ''' Refresh a single todo. runs db_set_todolist and returns todos. Exceptions must be handled my caller '''
975
+
976
+ try:
977
+ dbg('Refreshing '+todo['label'],l=-1)
978
+ conf.file_watch_ignores[todo['id']] = True # get_todos can cause the same event that saving a file cases
979
+ todos = conf.todo_connectors[todo['type']].get_todos(todo)
980
+ del conf.file_watch_ignores[todo['id']]
981
+
982
+ dbg('Done Refreshing '+todo['label'],l=-1)
983
+
984
+ db_set_todolist(todo['id'],todos['lists'],todos['tasks'])
985
+
986
+ except Exception as e:
987
+ if catch_errors:
988
+ error_notice('Error Loading '+todo['label']+' i refresh_todolist',e)
989
+ todos = {'lists': {}, 'tasks':{}}
990
+
991
+ else:
992
+ raise e
993
+
994
+ if todo['id'] in conf.todo_sync_required:
995
+ del conf.todo_sync_required[todo['id']]
996
+
997
+ conf.todo_sync_times[todo['id']] = now() # It may seem foolish to do this even on an error, but otherwise it will constantly try to update when offline
998
+ return todos
999
+
1000
+
879
1001
  def get_todolists(use_db_cache = False):
880
1002
  tasks = {}
881
1003
  lists = {}
@@ -897,17 +1019,15 @@ def get_todolists(use_db_cache = False):
897
1019
  dbg('Refreshing '+todo['type']+' todo '+id, s='todoloading')
898
1020
 
899
1021
  try:
900
- todos = conf.todo_connectors[todo['type']].get_todos(todo)
901
- conf.todo_sync_time = now()
902
- db_set_todolist(todo['id'],todos['lists'],todos['tasks'])
1022
+ todos = refresh_todolist(todo,False)
903
1023
  except Exception as e:
904
1024
  # This will happen if offline
905
1025
 
906
- # For testing
907
- if conf.debug_systems == 3 or 'todoloading' in conf.debug_systems or todo['type'] in conf.debug_systems :
1026
+ if conf.debug_level == 3 or 'todoloading' in conf.debug_systems or todo['type'] in conf.debug_systems :
908
1027
  raise e
909
1028
 
910
- error_notice('Error Loading '+todo['label'],e)
1029
+ error_notice('Error refreshing '+todo['label'],e)
1030
+
911
1031
  try:
912
1032
  todos = db_get_todolist(todo['id'])
913
1033
  except Exception as e:
@@ -919,6 +1039,8 @@ def get_todolists(use_db_cache = False):
919
1039
 
920
1040
  if not use_db_cache:
921
1041
  reindex()
1042
+ conf.todo_sync_time = now()
1043
+
922
1044
 
923
1045
  lists_cache(lists)
924
1046
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nowfocus
3
- Version: 0.2.8
3
+ Version: 0.2.12
4
4
  Summary: nowfocus: the open source task-tracking self-control panel.
5
5
  Author: GitFr33
6
6
  Project-URL: Homepage, https://gitlab.com/GitFr33/nowfocus
@@ -42,6 +42,7 @@ Requires-Dist: tzlocal
42
42
  Requires-Dist: urllib3
43
43
  Requires-Dist: vobject
44
44
  Requires-Dist: x-wr-timezone
45
+ Requires-Dist: watchdog
45
46
  Dynamic: license-file
46
47
 
47
48
  <div align="center"><img src="https://gitlab.com/GitFr33/nowfocus/-/raw/main/nowfocus.svg" width="60" align="center">
@@ -52,15 +53,15 @@ Dynamic: license-file
52
53
 
53
54
  </div>
54
55
 
55
- nowfocus is a clean, keyboard-driven project time tracker build with python + GTK that flexibly connects multiple to-do lists with multiple time trackers and displays your current task and time spent in the status bar.
56
+ nowfocus is a clean, keyboard-driven project time tracker built with python + GTK that flexibly connects multiple to-do lists with multiple time trackers and displays your current task and time spent in the status bar.
56
57
 
57
58
  ## Features
58
59
  - Unlimited flexible combinations of to-do lists and time tracking systems
59
60
  - Infinitely nestable lists
60
61
  - Inactivity detection that automatically pauses time tracking
61
62
  - Pomodoro timer
62
- - Task prioritization
63
- - Time targets: set a minimum or maximum time for any task or list of tasks and get reminded to follow though
63
+ - Task prioritization
64
+ - Time targets: set a minimum or maximum time for any task or list of tasks and get reminded to follow though
64
65
  - Randomness interrupt bell (optional) to keep you on track with tracking your time
65
66
  - Keyboard-driven interface
66
67
  - Offline to-do list cache
@@ -76,7 +77,7 @@ nowfocus is a clean, keyboard-driven project time tracker build with python + GT
76
77
 
77
78
  ### Currently Supported To-do List Backends
78
79
 
79
- - Simple text or markdown file with indentation based sub-lists
80
+ - Simple text file with indentation based sub-lists
80
81
  - Any to-do list that supports [CalDav todos](https://en.wikipedia.org/wiki/CalDAV)
81
82
  - [todotxt format](http://todotxt.org/)
82
83
  - [TaskWarrior](https://taskwarrior.org/)
@@ -99,7 +100,9 @@ nowfocus is a clean, keyboard-driven project time tracker build with python + GT
99
100
  # Install dependencies
100
101
  sudo apt install pipx gir1.2-appindicator3-0.1 meson libdbus-glib-1-dev patchelf libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev
101
102
 
102
- # Set up pipx
103
+ # note gir1.2-ayatanaappindicator3-0.1 can be substituted for gir1.2-appindicator3-0.1
104
+
105
+ # Set up pipx
103
106
  pipx ensurepath
104
107
 
105
108
  # At this point you may need to restart your terminal window
@@ -1,15 +1,15 @@
1
- nowfocus/__init__.py,sha256=3SHoF2gwL4sMAoUT_HCRPrnVNk3Cet1v_tc7_t9sc_4,2189
2
- nowfocus/__main__.py,sha256=S3zV5Nr1e1aEhJDjNYguvaB81K4qpEEjd5fT6IlRutg,31813
3
- nowfocus/conf.py,sha256=IqQ9x0-qgLFJxqBIZvdQhW9Z5Dryrxv1P6dHHsiEwAQ,6775
1
+ nowfocus/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ nowfocus/__main__.py,sha256=LMqVVjXaZD31F84U6Ie7B0lYZaz54z2afRFanoBIphY,32504
3
+ nowfocus/conf.py,sha256=MZx8gPREKsrzOsTdf_LAaFB8ysMF-jMC5YQZpH6AU3E,6954
4
4
  nowfocus/example-todo.txt,sha256=HRaNcPB1k8cksTtJS1GPqMjOdOY3gAUTWiSL0_ip0q8,265
5
- nowfocus/new_task_dialog.py,sha256=rDHPyFhvs_UNITcsAw5vCK_Gl839CQYFGk3fUi_D1qE,4195
5
+ nowfocus/new_task_dialog.py,sha256=3dEkQ4Ef7Gky4BGTx59U0qNfJCdMUrGrzx8HNTGbov8,4337
6
6
  nowfocus/session_edit_dialog.py,sha256=V2QWSdNaxsQHRcG28CJBQM2sa45m5RNcu_suQF26mkM,6912
7
7
  nowfocus/session_options.py,sha256=QVwJA53U7qZsbLe-OFr6UuFeaquco_yps_CRXsQ2_q4,5078
8
8
  nowfocus/sessions.csv,sha256=kYpr06yQg_J86NQ4AiYw4RnQchcw3ouPKVYa1lYDUNo,39
9
- nowfocus/settings.py,sha256=I2tWIrIyLmzDaxuPRN7ldbSJsP_rScr4jwtruAiJNBE,35731
9
+ nowfocus/settings.py,sha256=c6Z5FSVvSEx9fm8mQkdRo1TLfxFJEJdCMiqGyElv4bs,36873
10
10
  nowfocus/styles.css,sha256=m_7YXkDD7wljw5sFrv-CEJVImRpe92ER6kMa5rCICVo,489
11
- nowfocus/task_window.py,sha256=vdvz-BU09Q_TtBlXbWF7UJ7cYu6KbXhlWukeK9gTpuE,26089
12
- nowfocus/utils.py,sha256=RBjaNAYWcszS3iIG-yo8uZg24nDp2Mk16t_wb8p6fQk,43014
11
+ nowfocus/task_window.py,sha256=jVyjruCE2uhxumOccmKCjOG9PqGvoDnch_Ctbbf6-no,26081
12
+ nowfocus/utils.py,sha256=z4eQPKoiRunLnJyorSUCzNqbetvAjeQIZd3fHJmA3ko,46953
13
13
  nowfocus/connectors/activitywatch.py,sha256=QbkOmjIOiVwccWc2xhhePd0Abww5vEiVpCNjeqOyYGg,921
14
14
  nowfocus/connectors/caldav.py,sha256=PeM_9yJC8W17L8Y5AyS75o6GfzTrPoMYKIvetND8T78,5089
15
15
  nowfocus/connectors/csv.py,sha256=FwMpHM5lPIT90HKBCQUncpaW7zqFjlHjMwKR0-XWg-4,821
@@ -17,10 +17,10 @@ nowfocus/connectors/psc_timetracker.py,sha256=gyx0bQkOC467lkF7tTcoKD451u3WPEEBjA
17
17
  nowfocus/connectors/taskwarrior.py,sha256=bs1h5lxPxruNb6Pqf6HP-Do29i6NUdbBVDp_D5s06Ps,3172
18
18
  nowfocus/connectors/timewarrior.py,sha256=0Hra0GVPYdRqGtG_TbH3gzfUl192hH1DO2_WrDdrACM,698
19
19
  nowfocus/connectors/todo_template.py,sha256=R37fA2LXo8_LpWIgqozytI5RqIUjGggFHup25xTykII,1572
20
- nowfocus/connectors/todotxt.py,sha256=9vmue-QpeMh_3YbgCjFxiRpqWCtjvHxvaDWglnz6JAE,4377
20
+ nowfocus/connectors/todotxt.py,sha256=AKkHe_kv7iyk9rbXL1VwCN-ZyCluGlWP4wBcLn_Wnz4,3669
21
21
  nowfocus/connectors/trello.py,sha256=VqwnvHGXXcljmdf6kRZcE6sfeBQYhped_KVBEBOzWXM,6072
22
- nowfocus/connectors/txt.py,sha256=nFiy64oXBMV0UFELvW6tEsHTii2Fg6gq310HgDv50eI,7500
23
- nowfocus/connectors/vikunja.py,sha256=iIRB67jZScX5rzW9dhxA9gMm542n5J7DUFhroYOpI4Y,10768
22
+ nowfocus/connectors/txt.py,sha256=z_UAoJW-pZTRflgXeq5w9BAA0V8IMCGiRfYiOfLc7dQ,7847
23
+ nowfocus/connectors/vikunja.py,sha256=jOEQGGMu9tJJXlcJwtbsqLkCTqbsSlnja-QoyBrJ9eM,10856
24
24
  nowfocus/desktop-extras/nowfocus.desktop,sha256=0kWsx0ZfvPbubGG1uuFSHxxYUw2GV9Ly_rtlboM1mak,294
25
25
  nowfocus/desktop-extras/nowfocus.png,sha256=P5rn6-0EAJa2WXf4SJoaNtLRUfiV3LdsOroPKsR6GfA,15148
26
26
  nowfocus/desktop-extras/nowfocus.svg,sha256=nps7naZzuhWWuKzQbpvxr9wLyzjmzMPzNHSBQMVetOo,2137
@@ -49,9 +49,9 @@ nowfocus/icon/settings.svg,sha256=fgkGJouPPtZLxZn2nr_5pEp9MdhRSRaW9mtdxhJHDuQ,39
49
49
  nowfocus/sound/bell-xylophone-g.mp3,sha256=1OBcRWvD87AGNcq1uZFR8HqG0nanJykImERfVDVxHD4,53891
50
50
  nowfocus/sound/dinner-bell.mp3,sha256=hjjO0xqA4uXpYw9KLwwlBnrVfRhVq1K5OXzwlMXhRn4,113620
51
51
  nowfocus/sound/xylophone-chord.mp3,sha256=gwgBSqhMt5PMzT5N03Z6TvDgipQZfnkEz_o81Rq5Z1U,131806
52
- nowfocus-0.2.8.dist-info/licenses/LICENSE,sha256=fSJzoHs1EOCwEd7FIyokFeGEma7NKmTVEdHkCr5OIV4,35127
53
- nowfocus-0.2.8.dist-info/METADATA,sha256=gQOHCcWt6-veUgS7Z6HKY6cD0CHQg55Was66VeSp3gU,6031
54
- nowfocus-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
- nowfocus-0.2.8.dist-info/entry_points.txt,sha256=RbYY19-irSoNVglNeNnL9D36cHft7aKsaEGEYoSH3pA,51
56
- nowfocus-0.2.8.dist-info/top_level.txt,sha256=3uLd9BwmfarZwqVUxkSJuVwJ8qHzjThte8rt_UYG7tE,9
57
- nowfocus-0.2.8.dist-info/RECORD,,
52
+ nowfocus-0.2.12.dist-info/licenses/LICENSE,sha256=fSJzoHs1EOCwEd7FIyokFeGEma7NKmTVEdHkCr5OIV4,35127
53
+ nowfocus-0.2.12.dist-info/METADATA,sha256=UKr9IoBZagnBMTFo2McdL67DIUgPBWQz1OH2XmlZtlc,6139
54
+ nowfocus-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ nowfocus-0.2.12.dist-info/entry_points.txt,sha256=RbYY19-irSoNVglNeNnL9D36cHft7aKsaEGEYoSH3pA,51
56
+ nowfocus-0.2.12.dist-info/top_level.txt,sha256=3uLd9BwmfarZwqVUxkSJuVwJ8qHzjThte8rt_UYG7tE,9
57
+ nowfocus-0.2.12.dist-info/RECORD,,