nowfocus 0.2.7__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 +49 -28
- nowfocus/conf.py +12 -5
- nowfocus/connectors/todotxt.py +2 -19
- nowfocus/connectors/txt.py +31 -20
- nowfocus/connectors/vikunja.py +6 -4
- nowfocus/icon/icon-red.svg +101 -0
- nowfocus/icon/icon.svg +12 -1
- nowfocus/new_task_dialog.py +4 -1
- nowfocus/settings.py +55 -25
- nowfocus/task_window.py +6 -18
- nowfocus/utils.py +143 -21
- {nowfocus-0.2.7.dist-info → nowfocus-0.2.12.dist-info}/METADATA +9 -6
- {nowfocus-0.2.7.dist-info → nowfocus-0.2.12.dist-info}/RECORD +18 -18
- nowfocus/desktop-extras/set_gsettings_keybinding.sh +0 -51
- {nowfocus-0.2.7.dist-info → nowfocus-0.2.12.dist-info}/WHEEL +0 -0
- {nowfocus-0.2.7.dist-info → nowfocus-0.2.12.dist-info}/entry_points.txt +0 -0
- {nowfocus-0.2.7.dist-info → nowfocus-0.2.12.dist-info}/licenses/LICENSE +0 -0
- {nowfocus-0.2.7.dist-info → nowfocus-0.2.12.dist-info}/top_level.txt +0 -0
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
|
@@ -126,7 +126,6 @@ class TaskWindow(Gtk.Window):
|
|
|
126
126
|
self.taskEntry.set_text(passed_data['task']['label'])
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
# box.add(Gtk.Box(border_width=10)) # Spacer
|
|
130
129
|
|
|
131
130
|
self.scrolled_window = Gtk.ScrolledWindow()
|
|
132
131
|
self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
|
@@ -134,7 +133,7 @@ class TaskWindow(Gtk.Window):
|
|
|
134
133
|
self.scrolled_window.set_size_request(-1, 350)
|
|
135
134
|
|
|
136
135
|
self.scrolled_window.connect('scroll-event', self.on_scroll) # This doesn't catch scrollbar moves
|
|
137
|
-
self.scrolled_window.connect('edge-reached', self.on_scroll) # This is a little late
|
|
136
|
+
self.scrolled_window.connect('edge-reached', self.on_scroll) # This is a little late and may not fire if the list is short?
|
|
138
137
|
|
|
139
138
|
self.tasks_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=7)
|
|
140
139
|
self.tasks_box.set_halign(Gtk.Align.START)
|
|
@@ -164,10 +163,6 @@ class TaskWindow(Gtk.Window):
|
|
|
164
163
|
self.buttons_box.set_halign(Gtk.Align.CENTER)
|
|
165
164
|
self.buttons_box.set_name("Footer")
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
# box.add(Gtk.Box(border_width=10)) # Spacer
|
|
169
|
-
|
|
170
|
-
# box.add(self.buttons_box)
|
|
171
166
|
box.pack_start(self.buttons_box,False, False, 0)
|
|
172
167
|
|
|
173
168
|
self.settings_button = Gtk.Button(label="Settings")
|
|
@@ -180,19 +175,12 @@ class TaskWindow(Gtk.Window):
|
|
|
180
175
|
self.buttons_box.add(self.refresh_button)
|
|
181
176
|
|
|
182
177
|
# Todolist openables
|
|
183
|
-
openables = []
|
|
184
178
|
for id, todo in conf.user['todolists'].items():
|
|
185
179
|
if todo['status']:
|
|
186
180
|
try:
|
|
187
181
|
openable = conf.todo_connectors[todo['type']].launch
|
|
188
|
-
# if openable not in openables:
|
|
189
|
-
|
|
190
|
-
openables.append(openable)
|
|
191
|
-
|
|
192
182
|
openable_button = Gtk.Button(label=GLib.markup_escape_text(todo['label']))
|
|
193
|
-
|
|
194
|
-
openable_button.connect("clicked", lambda todo: openable(todo))
|
|
195
|
-
# openable_button.connect("clicked", openable, todo) # Doesn't work because of passing widget as first argument
|
|
183
|
+
openable_button.connect("clicked", lambda button_widget,todo=todo, openable=openable: openable(todo))
|
|
196
184
|
openable_button.set_property("tooltip-text","Open (new) "+GLib.markup_escape_text(todo['label']))
|
|
197
185
|
self.buttons_box.add(openable_button)
|
|
198
186
|
|
|
@@ -243,12 +231,12 @@ class TaskWindow(Gtk.Window):
|
|
|
243
231
|
self.search_cache = {}
|
|
244
232
|
self.search_cache_refresh_time = now().strftime("%Y-%m-%d %H:%M:%S")
|
|
245
233
|
self.task_search(self.taskEntry)
|
|
246
|
-
|
|
234
|
+
|
|
247
235
|
|
|
248
236
|
def open_new_task_dialog(self,w = None):
|
|
249
237
|
passed_data = {"label":self.taskEntry.get_text()}
|
|
250
238
|
self.app.open_new_task_dialog(self, passed_data)
|
|
251
|
-
|
|
239
|
+
|
|
252
240
|
|
|
253
241
|
def tick(self):
|
|
254
242
|
|
|
@@ -257,7 +245,7 @@ class TaskWindow(Gtk.Window):
|
|
|
257
245
|
self.recreate_header()
|
|
258
246
|
|
|
259
247
|
if utils.taskindex_updated_time() > self.search_cache_refresh_time:
|
|
260
|
-
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)
|
|
261
249
|
self.refresh_search_cache()
|
|
262
250
|
# else:
|
|
263
251
|
# print("taskindex was not updated",current_data_version)
|
|
@@ -474,7 +462,7 @@ class TaskWindow(Gtk.Window):
|
|
|
474
462
|
print("select_task task id",tid)
|
|
475
463
|
t = utils.db_get_item_by_id(tid)
|
|
476
464
|
# print('event',event)
|
|
477
|
-
print("select_task self.modifyer_keys", self.modifyer_keys)
|
|
465
|
+
# print("select_task self.modifyer_keys", self.modifyer_keys)
|
|
478
466
|
|
|
479
467
|
if self.modifyer_keys['control'] or event and event.button == 3: # Right-click (button 3) or control click context menu popover
|
|
480
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
|