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 +0 -67
- nowfocus/__main__.py +42 -24
- nowfocus/conf.py +12 -5
- nowfocus/connectors/todotxt.py +2 -19
- nowfocus/connectors/txt.py +31 -20
- nowfocus/connectors/vikunja.py +6 -4
- nowfocus/new_task_dialog.py +4 -1
- nowfocus/settings.py +55 -25
- nowfocus/task_window.py +4 -4
- nowfocus/utils.py +143 -21
- {nowfocus-0.2.8.dist-info → nowfocus-0.2.12.dist-info}/METADATA +9 -6
- {nowfocus-0.2.8.dist-info → nowfocus-0.2.12.dist-info}/RECORD +16 -16
- {nowfocus-0.2.8.dist-info → nowfocus-0.2.12.dist-info}/WHEEL +0 -0
- {nowfocus-0.2.8.dist-info → nowfocus-0.2.12.dist-info}/entry_points.txt +0 -0
- {nowfocus-0.2.8.dist-info → nowfocus-0.2.12.dist-info}/licenses/LICENSE +0 -0
- {nowfocus-0.2.8.dist-info → nowfocus-0.2.12.dist-info}/top_level.txt +0 -0
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(
|
|
167
|
-
os.remove(
|
|
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
|
|
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
|
-
|
|
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 ",
|
|
750
|
+
# print("Listening to pipe at ",conf.pipe)
|
|
731
751
|
|
|
732
752
|
try:
|
|
733
|
-
with open(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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':
|
|
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,
|
nowfocus/connectors/todotxt.py
CHANGED
|
@@ -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
|
|
nowfocus/connectors/txt.py
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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,
|
|
186
|
+
# prev item is a sub_list header, copy it to lists
|
|
177
187
|
lists[prev_id] = tasks[prev_id]
|
|
178
|
-
|
|
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)
|
nowfocus/connectors/vikunja.py
CHANGED
|
@@ -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 =
|
|
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
|
|
302
|
-
if category
|
|
303
|
+
if item:
|
|
304
|
+
if category in ('list','lists'):
|
|
303
305
|
url += 'projects/'+str(item['data']['id'])
|
|
304
|
-
|
|
306
|
+
else:
|
|
305
307
|
url += 'tasks/'+str(item['data']['id'])
|
|
306
308
|
|
|
307
309
|
utils.open_external(url)
|
nowfocus/new_task_dialog.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
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",
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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=
|
|
2
|
-
nowfocus/__main__.py,sha256=
|
|
3
|
-
nowfocus/conf.py,sha256=
|
|
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=
|
|
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=
|
|
9
|
+
nowfocus/settings.py,sha256=c6Z5FSVvSEx9fm8mQkdRo1TLfxFJEJdCMiqGyElv4bs,36873
|
|
10
10
|
nowfocus/styles.css,sha256=m_7YXkDD7wljw5sFrv-CEJVImRpe92ER6kMa5rCICVo,489
|
|
11
|
-
nowfocus/task_window.py,sha256=
|
|
12
|
-
nowfocus/utils.py,sha256=
|
|
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=
|
|
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=
|
|
23
|
-
nowfocus/connectors/vikunja.py,sha256=
|
|
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.
|
|
53
|
-
nowfocus-0.2.
|
|
54
|
-
nowfocus-0.2.
|
|
55
|
-
nowfocus-0.2.
|
|
56
|
-
nowfocus-0.2.
|
|
57
|
-
nowfocus-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|