nowfocus 0.2.8__py3-none-any.whl → 0.2.13__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 '''
434
- print("Mark Task done")
435
- print(task)
444
+
445
+ print("Mark Task done",task['label'])
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
@@ -490,8 +510,8 @@ class Application(Gtk.Application):
490
510
  # print("add ",utils.extended_label(task)," to recent tasks")
491
511
  i = Gtk.MenuItem.new_with_label(utils.extended_label(task))
492
512
  i.connect('activate',self.start_task,task)
493
- list_menus['recent'].prepend(i)
494
513
  try:
514
+ list_menus['recent'].prepend(i)
495
515
  list_menus['recent'].get_children()[11].destroy()
496
516
  except Exception as e:
497
517
  dbg("Exception trying to rotate recent tasks. probably are less than 11",l=2,s="recent")
@@ -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
 
@@ -8,27 +8,36 @@ def add_new_task(user_conf,list,task_label):
8
8
 
9
9
 
10
10
  with open(file_uri, 'r') as file:
11
- # read a list of lines into data
12
11
  data = file.readlines()
13
12
 
14
13
  if list['id'] == user_conf['id']:
15
- # Top level insert
16
- new_line = task_label+"\n"
14
+ # Top level list, append to bottom
17
15
  line_no = len(data) + 1
18
16
  else:
19
- # Sub list insert
20
- # TODO: Add it at the end of the list rather than the start ...
21
- # Yup. rjust(indent + len)
17
+ line_no = get_line_no(list,data) + 1
18
+
19
+ new_line = list['data']['indent']+task_label+"\n"
22
20
 
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
21
+ # # Old version without recorded indent
22
+ # if list['id'] == user_conf['id']:
23
+ # # Top level insert
24
+ # new_line = task_label+"\n"
25
+ # line_no = len(data) + 1
26
+ # else:
27
+ # Sub list insert
28
+ # TODO: Add it at the end of the list rather than the start ...
29
+ # Yup. rjust(indent + len)
30
+
31
+
32
+ # 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
33
 
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
34
+ # new_line = task_label+"\n"
35
+ # # I'm sure there's a better way to add a bunch of spaces to to a string
36
+ # while indent > 0:
37
+ # new_line = " " + new_line
38
+ # indent = indent - 1
30
39
 
31
- line_no = get_line_no(list,data) + 1
40
+ # line_no = get_line_no(list,data) + 1
32
41
 
33
42
  data.insert(line_no,new_line)
34
43
 
@@ -64,17 +73,15 @@ def get_line_no(i,lines):
64
73
  else:
65
74
 
66
75
  line_no = 0 #TODO: Improve, (better foreach with key)
67
- in_list = False
68
76
 
69
77
  for line in lines:
70
78
 
71
- # TODO: Find the list
79
+ # TODO: Find the list
72
80
  if line == i['data']['original_line']:
73
81
  return line_no
74
82
 
75
- line_no = line_no + 1
76
-
77
- throw("Line not found error Ha!")
83
+
84
+ utils.dbg("Line not found error",i,lines,s='txt',l=0)
78
85
 
79
86
  def task_id(task):
80
87
  ''' The task parameter must have at least "label", "todolist" and "parent_id" '''
@@ -120,17 +127,18 @@ def get_todos(user_conf):
120
127
  tasks = {}
121
128
  lists = {}
122
129
  lists[user_conf['id']] = {
123
- 'id':user_conf['id'],
124
- 'label':user_conf['label'],
125
- 'parent_id':'',
126
- 'parent_label':'',
127
- 'todolist':user_conf['id'],
128
- 'data':{
129
- 'line_no':0,
130
- 'original_line':'',
131
- 'accepts_tasks':True
132
- }
133
- }
130
+ 'id':user_conf['id'],
131
+ 'label':user_conf['label'],
132
+ 'parent_id':'',
133
+ 'parent_label':'',
134
+ 'todolist':user_conf['id'],
135
+ 'data':{
136
+ 'line_no':0,
137
+ 'original_line':'',
138
+ 'accepts_tasks':True,
139
+ 'indent':'',
140
+ }
141
+ }
134
142
 
135
143
  current_list = []
136
144
  current_list_label = []
@@ -149,7 +157,7 @@ def get_todos(user_conf):
149
157
  # file_uri = "todo.txt"
150
158
  line_no = -1
151
159
 
152
- with open(file_uri) as file:
160
+ with open(file_uri,"r") as file:
153
161
  for line in file:
154
162
 
155
163
  line_no = line_no + 1
@@ -171,11 +179,13 @@ def get_todos(user_conf):
171
179
  #TODO: use markdown title syntax as list name in addition to indentation
172
180
 
173
181
  indent = len(line) - len(line.lstrip())
182
+ indent_str = line[0:indent]
174
183
 
175
184
  if indent > prev_indent:
176
- # prev item is a sub_list header, move it to lists
185
+ # prev item is a sub_list header, copy it to lists
177
186
  lists[prev_id] = tasks[prev_id]
178
- del tasks[prev_id]
187
+ lists[prev_id]['data']['indent'] = indent_str
188
+ # del tasks[prev_id]
179
189
  current_list.append(prev_id)
180
190
  current_list_label.append(prev_label)
181
191
  current_indent.append(indent)
@@ -194,6 +204,13 @@ def get_todos(user_conf):
194
204
 
195
205
 
196
206
  utils.dbg(line.rstrip(),s='txt',l=3)
207
+
208
+ # Get prioritized tasks (starting with a number)
209
+ if label.split()[0].isnumeric():
210
+ priority = float(label.split()[0])
211
+ label = label.removeprefix(label.split()[0]).strip()
212
+ else:
213
+ priority = '0'
197
214
 
198
215
  task_id = file_uri+":"+"/".join(current_list_label)+":"+label
199
216
 
@@ -211,6 +228,7 @@ def get_todos(user_conf):
211
228
  'parent_label':str(parent_label),
212
229
  'todolist':user_conf['id'],
213
230
  'status': status,
231
+ 'priority': priority,
214
232
  'data':{
215
233
  'line_no':line_no,
216
234
  'original_line':line,
@@ -218,11 +236,7 @@ def get_todos(user_conf):
218
236
  }
219
237
  }
220
238
 
221
- # Get prioritized tasks (starting with a number)
222
- # if label[0].isdigit()
223
- if label.split()[0].isnumeric():
224
- tasks[task_id]['priority'] = float(label.split()[0])
225
- tasks[task_id]['label'] = label.removeprefix(label.split()[0]).strip()
239
+
226
240
 
227
241
  prev_indent = indent
228
242
  prev_label = label
@@ -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