nowfocus 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {nowfocus-0.4.2/src/nowfocus.egg-info → nowfocus-0.4.4}/PKG-INFO +2 -10
  2. {nowfocus-0.4.2 → nowfocus-0.4.4}/README.md +1 -9
  3. {nowfocus-0.4.2 → nowfocus-0.4.4}/pyproject.toml +1 -8
  4. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/__main__.py +157 -145
  5. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/conf.py +41 -48
  6. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/desktop-extras/nowfocus.desktop +1 -1
  7. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/example-todo.txt +1 -2
  8. nowfocus-0.4.4/src/nowfocus/install.py +92 -0
  9. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/session_options.py +1 -1
  10. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/task_window.py +5 -5
  11. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/utils.py +2 -22
  12. nowfocus-0.4.4/src/nowfocus/version_migrator.py +20 -0
  13. {nowfocus-0.4.2 → nowfocus-0.4.4/src/nowfocus.egg-info}/PKG-INFO +2 -10
  14. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus.egg-info/SOURCES.txt +2 -0
  15. {nowfocus-0.4.2 → nowfocus-0.4.4}/LICENSE +0 -0
  16. {nowfocus-0.4.2 → nowfocus-0.4.4}/setup.cfg +0 -0
  17. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/__init__.py +0 -0
  18. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/activitywatch.py +0 -0
  19. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/caldav.py +0 -0
  20. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/csv.py +0 -0
  21. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/psc_timetracker.py +0 -0
  22. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/taskwarrior.py +0 -0
  23. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/timewarrior.py +0 -0
  24. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/todo_template.py +0 -0
  25. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/todotxt.py +0 -0
  26. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/trello.py +0 -0
  27. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/txt.py +0 -0
  28. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/connectors/vikunja.py +0 -0
  29. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/desktop-extras/nowfocus.png +0 -0
  30. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/desktop-extras/nowfocus.svg +0 -0
  31. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/cancel.png +0 -0
  32. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/cancel.svg +0 -0
  33. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/edit.png +0 -0
  34. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/edit.svg +0 -0
  35. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-0.svg +0 -0
  36. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-1.svg +0 -0
  37. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-2.svg +0 -0
  38. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-3.svg +0 -0
  39. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-4.svg +0 -0
  40. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-5.svg +0 -0
  41. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-6.svg +0 -0
  42. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-7.svg +0 -0
  43. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-8.svg +0 -0
  44. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-9.svg +0 -0
  45. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon-red.svg +0 -0
  46. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/icon.svg +0 -0
  47. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/mark-done.png +0 -0
  48. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/mark-done.svg +0 -0
  49. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/pause.png +0 -0
  50. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/pause.svg +0 -0
  51. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/settings.png +0 -0
  52. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/icon/settings.svg +0 -0
  53. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/new_task_dialog.py +0 -0
  54. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/session_edit_dialog.py +0 -0
  55. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/sessions.csv +0 -0
  56. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/settings.py +0 -0
  57. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/sound/bell-xylophone-g.mp3 +0 -0
  58. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/sound/dinner-bell.mp3 +0 -0
  59. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/sound/xylophone-chord.mp3 +0 -0
  60. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/styles.css +0 -0
  61. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus/user_idle_time.py +0 -0
  62. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus.egg-info/dependency_links.txt +0 -0
  63. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus.egg-info/entry_points.txt +0 -0
  64. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus.egg-info/requires.txt +0 -0
  65. {nowfocus-0.4.2 → nowfocus-0.4.4}/src/nowfocus.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nowfocus
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: nowfocus: the open source task-tracking self-control panel.
5
5
  Author: AltruistEnterprises
6
6
  Project-URL: Homepage, https://www.nowfocus.org
@@ -112,13 +112,6 @@ nowfocus is a clean, keyboard-driven time management dashboard that flexibly con
112
112
  # Install nowfocus
113
113
  pipx install nowfocus
114
114
 
115
- # Enter application directory
116
- cd ~/.local/share/pipx/venvs/nowfocus/lib/python3.12/site-packages/nowfocus/desktop-extras
117
-
118
- # Copy desktop file and icon to /usr/share
119
- sudo cp nowfocus.desktop /usr/share/applications/nowfocus.desktop
120
- sudo cp nowfocus.svg /usr/share/icons/hicolor/scalable/apps/nowfocus.svg
121
-
122
115
  # and now Focus!
123
116
  nowfocus
124
117
 
@@ -130,7 +123,6 @@ nowfocus is a clean, keyboard-driven time management dashboard that flexibly con
130
123
  ```
131
124
 
132
125
 
133
-
134
126
  3. Add the following command to your startup applications:
135
127
  ```
136
128
  nowfocus --force
@@ -178,7 +170,7 @@ Open nowfocus **Settings** from the indicator menu or tasks window and connect y
178
170
 
179
171
  ### Install From Source
180
172
  ```
181
- git clone https://codeberg.org/AltruistEnterprises/nowfocus/nowfocus.git
173
+ git clone https://codeberg.org/AltruistEnterprises/nowfocus.git
182
174
  cd nowfocus
183
175
  python3 -m venv .venv/nowfocus-build
184
176
  source .venv/nowfocus-build/bin/activate
@@ -69,13 +69,6 @@ nowfocus is a clean, keyboard-driven time management dashboard that flexibly con
69
69
  # Install nowfocus
70
70
  pipx install nowfocus
71
71
 
72
- # Enter application directory
73
- cd ~/.local/share/pipx/venvs/nowfocus/lib/python3.12/site-packages/nowfocus/desktop-extras
74
-
75
- # Copy desktop file and icon to /usr/share
76
- sudo cp nowfocus.desktop /usr/share/applications/nowfocus.desktop
77
- sudo cp nowfocus.svg /usr/share/icons/hicolor/scalable/apps/nowfocus.svg
78
-
79
72
  # and now Focus!
80
73
  nowfocus
81
74
 
@@ -87,7 +80,6 @@ nowfocus is a clean, keyboard-driven time management dashboard that flexibly con
87
80
  ```
88
81
 
89
82
 
90
-
91
83
  3. Add the following command to your startup applications:
92
84
  ```
93
85
  nowfocus --force
@@ -135,7 +127,7 @@ Open nowfocus **Settings** from the indicator menu or tasks window and connect y
135
127
 
136
128
  ### Install From Source
137
129
  ```
138
- git clone https://codeberg.org/AltruistEnterprises/nowfocus/nowfocus.git
130
+ git clone https://codeberg.org/AltruistEnterprises/nowfocus.git
139
131
  cd nowfocus
140
132
  python3 -m venv .venv/nowfocus-build
141
133
  source .venv/nowfocus-build/bin/activate
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nowfocus"
3
- version = "0.4.2"
3
+ version = "0.4.4"
4
4
  authors = [
5
5
  { name="AltruistEnterprises" },
6
6
  ]
@@ -22,15 +22,10 @@ dependencies = [
22
22
  "psutil",
23
23
  "pygobject == 3.50.0",
24
24
  "pycairo == 1.27",
25
- # "meson",
26
- # "meson-python",
27
25
  "attrs",
28
26
  "caldav",
29
27
  "certifi",
30
28
  "charset-normalizer",
31
- # "dbus-idle",
32
- # "dbus-python",
33
- # "jeepney",
34
29
  "icalendar",
35
30
  "idna",
36
31
  "pywin32>=221 ; platform_system == 'Windows'",
@@ -56,12 +51,10 @@ dependencies = [
56
51
  [project.urls]
57
52
  Homepage = "https://www.nowfocus.org"
58
53
  repository = "https://codeberg.org/AltruistEnterprises/nowfocus"
59
- # Homepage = "https://gitlab.com/GitFr33/nowfocus"
60
54
 
61
55
  [project.gui-scripts]
62
56
  nowfocus = "nowfocus.__main__:startup"
63
57
 
64
-
65
58
  # include non .py files in the dist
66
59
  [tool.setuptools.packages.find]
67
60
  where = ["src"]
@@ -29,8 +29,6 @@ except Exception as e:
29
29
  gi.require_version('AppIndicator3', '0.1')
30
30
  from gi.repository import AppIndicator3 as appindicator
31
31
 
32
- # from dbus_idle import IdleMonitor
33
-
34
32
  # Set working dir to file location
35
33
  os.chdir(os.path.dirname(os.path.realpath(__file__)))
36
34
 
@@ -62,81 +60,59 @@ class Application(Gtk.Application):
62
60
  def __init__(self, *args, **kwargs):
63
61
  super().__init__(*args, application_id="org.nowfocus.nowfocus", **kwargs)
64
62
 
65
- # try:
66
- # # To put everything here...
67
- # # this doesn't work because as soon as an exception occurs ii jumps to the handler and breaks all the following code.
63
+ try:
68
64
 
69
- # except Exception as e:
70
- # print(conf.app_name +' had a pretty bad error. Please submit the following trace as an issue on the git repo or email it to the developers ')
71
- # traceback.print_tb(e.__traceback__)
72
- # return None
73
-
74
- self.window = None
65
+
66
+
67
+ self.window = None
75
68
 
76
- self.is_running = False
77
- self.session = default_session()
69
+ self.is_running = False
70
+ self.device_in_use = True
71
+ self.UserIdleTime = UserIdleTime()
72
+ self.last_user_idle_check_time = None
73
+ self.session = default_session()
78
74
 
79
- self.menu_tasks = {}
80
- self.list_menus = {}
75
+ self.menu = Gtk.Menu()
76
+ self.menu_tasks = {}
77
+ self.list_menus = {}
81
78
 
82
- self.indicator = appindicator.Indicator.new(conf.app_name, os.path.abspath('icon/icon-1.svg'), appindicator.IndicatorCategory.APPLICATION_STATUS)
79
+ self.indicator = appindicator.Indicator.new(conf.app_name, os.path.abspath('icon/icon-1.svg'), appindicator.IndicatorCategory.APPLICATION_STATUS)
80
+ self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
81
+ self.indicator.set_menu(self.menu)
83
82
 
84
- self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
83
+ if conf.is_first_load:
84
+ import install
85
+ install.run_first_load_actions()
85
86
 
86
- self.menu = Gtk.Menu()
87
- # menu.set_reserve_toggle_size(False) # skip menu left padding, doesn't work
87
+ utils.db_schema_update()
88
88
 
89
- utils.db_init()
90
- utils.db_schema_update()
89
+ self.async_refresh()
91
90
 
91
+ # utils.db_cleanup()
92
92
 
93
- # self.update_menu()
94
- self.async_refresh()
93
+ main_tick_timer = GLib.timeout_add_seconds(1, self.tick)
95
94
 
96
- # utils.db_cleanup()
95
+ self.resume_session_from_db()
97
96
 
98
- self.indicator.set_menu(self.menu)
97
+ self.start_pipe()
98
+ self.open_task_window()
99
99
 
100
- main_tick_timer = GLib.timeout_add_seconds(1, self.tick)
100
+ # Testing
101
+ # time.sleep(3)
102
+ # self.open_session_options_dialog('test_param')
103
+ # self.open_settings_window() #for testing
104
+ # self.open_new_task_dialog() #for testing
105
+ # self.print_time_totals()
101
106
 
102
- try:
103
- db_session = db_query("SELECT value FROM system WHERE field = 'session'")
107
+ signal.signal(signal.SIGINT, self.quit)
108
+ signal.signal(signal.SIGUSR1, self.signal_handler)
109
+ signal.signal(signal.SIGUSR2, self.signal_handler)
104
110
 
105
- if db_session:
106
- s = json.loads(db_session[0]['value'])
107
- s['start_time'] = datetime.strptime(s['start_time'],'%Y-%m-%d %H:%M:%S.%f')
108
- self.session = s
109
- self.is_running = True
110
- dbg("resuming session",s['label'],l=2, s='session')
111
+ Gtk.main()
111
112
  except Exception as e:
112
- dbg("Error resuming session",e,l=1)
113
-
114
- self.UserIdleTime = UserIdleTime()
115
-
116
- self.pipethread = threading.Thread(target=self.check_pipe)
117
- self.pipethread.daemon = True
118
- self.pipethread.start()
119
-
120
- # Testing
121
- # time.sleep(3)
122
- # self.open_session_options_dialog('test_param')
123
-
124
- # self.open_settings_window() #for testing
125
- # self.open_task_window() #for testing
126
-
127
- # time.sleep(2)
128
- # self.open_new_task_dialog() #for testing
129
-
130
- # time.sleep(2)
131
- # self.print_time_totals()
132
-
133
- # signal.signal(signal.SIGINT, signal.SIG_DFL)
134
- signal.signal(signal.SIGINT, self.quit)
135
- signal.signal(signal.SIGUSR1, self.signal_handler)
136
- signal.signal(signal.SIGUSR2, self.signal_handler)
137
-
138
- Gtk.main()
139
-
113
+ print(conf.app_name +' had a pretty bad error. Please submit the following trace as an issue on the git repo or email it to the developers ')
114
+ traceback.print_tb(e.__traceback__)
115
+ self.quit()
140
116
 
141
117
 
142
118
  def print_time_totals(self = None, widget = None):
@@ -177,25 +153,13 @@ class Application(Gtk.Application):
177
153
  exit()
178
154
 
179
155
 
180
- def seconds_since_user_active(self):
181
- # returns seconds of inactivity
182
- # See https://stackoverflow.com/questions/67083083/how-to-get-idle-time-in-linux or
156
+ def get_seconds_since_user_active(self):
157
+
158
+ if self.last_user_idle_check_time != now().replace(microsecond=0):
159
+ self.last_user_idle_check_time = now().replace(microsecond=0)
160
+ self.seconds_since_user_active = self.UserIdleTime.get()
183
161
 
184
- # Works on x11 but not wayland
185
- # Requires xprintidle (sudo apt install xprintidle)
186
- # idle_time = int(int(subprocess.getoutput('xprintidle')) / 1000)
187
-
188
- # Version that works, using: https://github.com/bkbilly/dbus_idle
189
- # but has many deps
190
- # Requires:
191
- # sudo apt install meson libdbus-glib-1-dev patchelf
192
- # pip install dbus-idle
193
-
194
- # idle_time = int(int(IdleMonitor().get_dbus_idle()) / 1000)
195
- # return idle_time
196
-
197
- return self.UserIdleTime.get()
198
-
162
+ return self.seconds_since_user_active
199
163
 
200
164
 
201
165
  def toggle_do_not_disturb(self, widget):
@@ -213,81 +177,106 @@ class Application(Gtk.Application):
213
177
 
214
178
  def tock(self):
215
179
 
216
- if 'do-not-disturb' in self.session:
217
- return None
218
-
219
- afk_time = self.seconds_since_user_active()
220
- # print("Idle time: "+str(afk_time))
180
+ user_inactive_time = self.get_seconds_since_user_active()
181
+ # print("user_inactive_time: "+str(user_inactive_time))
221
182
 
222
183
  dbg('Last todo todo_sync_time', conf.todo_sync_time, 'Time diff',int(time_difference(conf.todo_sync_time)),'Auto refresh interval * 60', (conf.user['todolist_refresh_interval'] * 60), s="todoloading", l=3 )
223
184
 
224
- if (int(time_difference(conf.todo_sync_time)) / 60) > conf.user['todolist_refresh_interval'] * 60 :
185
+ if (int(time_difference(conf.todo_sync_time)) / 60) > conf.user['todolist_refresh_interval'] * 60:
186
+ # TODO: use individual todo_refresh_times
225
187
  self.async_refresh()
226
188
 
227
- # TODO: use individual todo_refresh_times
228
-
229
189
  minutes = (int(self.session['duration']) / 60)
230
190
 
231
- if(self.is_running == False):
191
+ if self.check_device_became_inactive():
192
+ self.handle_device_became_inactive()
193
+
194
+ elif self.device_in_use == False or 'do-not-disturb' in self.session:
195
+ # Do nothing
196
+ dbg("Do not disturb is active")
197
+
198
+ elif self.is_running:
199
+
200
+ if self.session['label'] in conf.user['custom_pomodoro_intervals']:
201
+ check = conf.user['custom_pomodoro_intervals'][self.session['label']]
202
+ else:
203
+ check = conf.user['pomodoro_interval']
204
+
205
+ if float(minutes / check ).is_integer():
206
+ notify.Notification.new("Time for a Break?","You've been working on "+self.session['label']+" for "+str(minutes)+" minutes. ", None).show()
207
+
208
+ playsound('sound/bell-xylophone-g.mp3',False)
209
+
232
210
 
233
- if num_is_multiple_of(minutes,conf.user['randomness_interrupt_interval']):
234
- if afk_time < 30:
211
+ if 'target' in self.session:
212
+ t = self.session['target']
213
+
214
+ t['percent'] = round(( (t['starting_value'] + minutes) / t['value'] ) * 100,1)
215
+ print("At", t['percent'], "% of ", t['scope'], " target")
216
+
217
+ if t['type'] == 'max':
218
+ if t['percent'] >= 100:
219
+
220
+ notify.Notification.new("Time is up for this "+t['scope'],"You'r at "+str(round(t['percent']))+"% of your "+str(t['value'])+" minutes in the last "+str(t['within_value'])+" "+ t['within_unit'], None).show()
221
+
222
+ playsound('sound/dinner-bell.mp3',False)
223
+
224
+ elif t['type'] == 'min' and round(t['starting_value'] + minutes) == t['value']:
225
+ notify.Notification.new("Good job on doing "+self.session['label'],"You've reached your target of "+str(t['value'])+" minutes "+str(t['within_value'])+" "+ t['within_unit'], None).show()
226
+
227
+ playsound('sound/xylophone-chord.mp3',False)
228
+
229
+ elif num_is_multiple_of(minutes,conf.user['randomness_interrupt_interval']):
235
230
 
236
- notify.Notification.new("What Am I doing?","Your randomness timer is at "+str(minutes)+" minutes. ", None).show()
231
+ notify.Notification.new("What Am I doing?","Your randomness timer is at "+str(minutes)+" minutes. ", None).show()
237
232
 
238
- self.open_task_window()
239
-
240
- playsound('sound/dinner-bell.mp3',False)
233
+ self.open_task_window()
234
+
235
+ playsound('sound/dinner-bell.mp3',False)
241
236
 
242
- elif afk_time > 120:
243
- self.session['duration'] = 0
244
- self.session['start_time'] = now()
245
- print("Idle time reset. afk:", afk_time, self.session)
246
237
 
238
+ def check_device_became_inactive(self):
239
+ if self.get_seconds_since_user_active() > (conf.user['device_not_in_use_threshold'] * 60):
240
+ if self.device_in_use == True:
241
+ self.device_in_use = False
242
+ return True
247
243
  else:
248
- if afk_time > 120:
249
- # only show this once
250
- if afk_time < 181:
251
- self.open_task_window(None,{'afk_time':afk_time})
252
- else:
253
- if self.session['label'] in conf.user['custom_pomodoro_intervals']:
254
- check = conf.user['custom_pomodoro_intervals'][self.session['label']]
255
- else:
256
- check = conf.user['pomodoro_interval']
257
-
258
- if float(minutes / check ).is_integer():
259
- notify.Notification.new("Time for a Break?","You've been working on "+self.session['label']+" for "+str(minutes)+" minutes. ", None).show()
244
+ self.device_in_use = True
245
+ return False
260
246
 
261
- playsound('sound/bell-xylophone-g.mp3',False)
262
247
 
248
+ def handle_device_became_inactive(self):
249
+ self.user_last_inactive = now()
263
250
 
264
- if 'target' in self.session:
265
- t = self.session['target']
251
+ user_inactive_time = self.get_seconds_since_user_active()
252
+ dbg('device_became_inactive ',l=-1)
266
253
 
267
- t['percent'] = round(( (t['starting_value'] + minutes) / t['value'] ) * 100,1)
268
- print("At", t['percent'], "% of ", t['scope'], " target")
254
+ if self.is_running and 'do-not-disturb' not in self.session:
269
255
 
270
- if t['type'] == 'max':
271
- if t['percent'] >= 100:
256
+ self.open_task_window(None,{'user_inactive_time':user_inactive_time})
272
257
 
273
- notify.Notification.new("Time is up for this "+t['scope'],"You'r at "+str(round(t['percent']))+"% of your "+str(t['value'])+" minutes in the last "+str(t['within_value'])+" "+ t['within_unit'], None).show()
258
+ else:
259
+ self.session['duration'] = self.session['duration'] - user_inactive_time
260
+ if self.session['duration'] > 60:
261
+ utils.db_save_session(self.session)
262
+
263
+ self.session['duration'] = 0
264
+ self.session['start_time'] = now()
265
+ dbg("Recording:", user_inactive_time, self.session)
266
+
267
+ if 'do-not-disturb' not in self.session and conf.user['randomness_interrupt_interval'] > 0:
268
+ self.open_task_window(None)
274
269
 
275
- playsound('sound/dinner-bell.mp3',False)
276
270
 
277
- elif t['type'] == 'min' and round(t['starting_value'] + minutes) == t['value']:
278
- notify.Notification.new("Good job on doing "+self.session['label'],"You've reached your target of "+str(t['value'])+" minutes "+str(t['within_value'])+" "+ t['within_unit'], None).show()
279
-
280
- playsound('sound/xylophone-chord.mp3',False)
281
-
282
-
283
271
  def tick(self):
284
272
 
285
273
  # check for suspend indicated by gap in tick intervals
286
274
  time_since_last_tick = round(time_difference(self.session['start_time']) - self.session['duration'])
287
275
  if time_since_last_tick > 10:
276
+
288
277
  dbg(time_since_last_tick, " seconds since last tick. Probably just woke from suspend. ")
289
278
  if self.is_running:
290
- self.open_task_window(None,{'afk_time':time_since_last_tick})
279
+ self.open_task_window(None,{'user_inactive_time':time_since_last_tick})
291
280
  else:
292
281
  self.session['start_time'] = now()
293
282
 
@@ -312,7 +301,6 @@ class Application(Gtk.Application):
312
301
 
313
302
  if duration > 60 and num_is_multiple_of(duration, 2):
314
303
  icon = 'icon-red.svg'
315
-
316
304
  else:
317
305
  icon = 'icon-1.svg'
318
306
 
@@ -321,13 +309,28 @@ class Application(Gtk.Application):
321
309
  self.indicator.set_icon_full(os.path.abspath('icon/'+icon),label)
322
310
  self.indicator.set_label(label,label)
323
311
 
312
+ if num_is_multiple_of(duration,5): # check every 5 seconds
313
+ if self.device_in_use:
314
+ self.refresh_all_changed_todo_files()
315
+ elif self.get_seconds_since_user_active() < 60:
316
+ self.device_in_use = True
317
+ self.handle_device_became_active()
318
+ else:
319
+ # TODO: incriment a user away timer
320
+ dbg("User is still away")
324
321
 
325
- if num_is_multiple_of(self.icon_tick_number,3):
326
- self.refresh_all_changed_todo_files()
327
-
328
322
  return True
329
323
 
330
324
 
325
+ def handle_device_became_active(self):
326
+ dbg('device_became_active! ',l=-1)
327
+ self.user_last_active = now()
328
+
329
+ if self.is_running == False:
330
+ self.session['start_time'] = now()
331
+ self.session['duration'] = 0
332
+
333
+
331
334
  def refresh_all_changed_todo_files(self):
332
335
  for id, todo_config in conf.user['todolists'].items():
333
336
  if todo_config['status'] and 'watch_file' in todo_config:
@@ -352,6 +355,20 @@ class Application(Gtk.Application):
352
355
  handle_todo_read_error(todo_config,e)
353
356
 
354
357
 
358
+ def resume_session_from_db(self):
359
+ try:
360
+ db_session = db_query("SELECT value FROM system WHERE field = 'session'")
361
+
362
+ if db_session:
363
+ s = json.loads(db_session[0]['value'])
364
+ s['start_time'] = datetime.strptime(s['start_time'],'%Y-%m-%d %H:%M:%S.%f')
365
+ self.session = s
366
+ self.is_running = True
367
+ dbg("resuming session",s['label'],l=2, s='session')
368
+ except Exception as e:
369
+ dbg("Error resuming session",e,l=1)
370
+
371
+
355
372
  def start_task(self, w = None, task_data_or_id = None, transfer_current_session_time = False):
356
373
 
357
374
  if isinstance(task_data_or_id, dict):
@@ -431,7 +448,7 @@ class Application(Gtk.Application):
431
448
 
432
449
 
433
450
  def run_task_command(self,command):
434
- print("running task command:", command)
451
+ dbg("running task command:", command,l=-1)
435
452
 
436
453
  self.running_command_task_label = copy.copy(self.session['label'])
437
454
 
@@ -736,24 +753,20 @@ class Application(Gtk.Application):
736
753
  dbg('Signal received',sig,s='signals')
737
754
 
738
755
  if sig == signal.SIGUSR1:
739
- # try:
740
- # print('TaskWindow._instance',TaskWindow._instance)
741
- # if TaskWindow._instance:
742
- # self.taskwindow.destroy()
743
- # else:
744
- # self.open_task_window()
745
-
746
- # except AttributeError:
747
756
  self.open_task_window()
748
-
749
757
  elif sig == signal.SIGUSR2:
750
758
  self.open_session_options_dialog()
751
-
752
759
  else:
753
760
  dbg("no handler for received signal",s='signals',l=3)
754
761
 
762
+
763
+ def start_pipe(self):
764
+ self.pipethread = threading.Thread(target=self.check_pipe)
765
+ self.pipethread.daemon = True
766
+ self.pipethread.start()
767
+
768
+
755
769
  def check_pipe(self):
756
- # print("Listening to pipe at ",conf.pipe)
757
770
 
758
771
  try:
759
772
  with open(conf.pipe, "r") as pipe:
@@ -823,7 +836,6 @@ def startup():
823
836
  except Exception as e:
824
837
  print(e)
825
838
 
826
-
827
839
  try:
828
840
  os.mkfifo(conf.pipe)
829
841
  dbg("Named pipe created successfully!", s="cli")
@@ -839,7 +851,7 @@ def startup():
839
851
 
840
852
 
841
853
  except FileExistsError:
842
- dbg("Named pipe exists, application must be running (or improperly shut down.) ",s="cli")
854
+ print("Named pipe exists, application must be running (or improperly shut down.) ")
843
855
 
844
856
  # if args: pass to pipe and exit
845
857
  if args.task:
@@ -18,11 +18,14 @@ user_data_dir = GLib.get_user_data_dir()+"/"+app_id
18
18
  Path(user_data_dir).mkdir(parents=True, exist_ok=True)
19
19
 
20
20
  user_settings_dir = GLib.get_user_config_dir()+"/"+app_id
21
- Path(user_settings_dir).mkdir(parents=True, exist_ok=True)
21
+ ''' Depreciated. use user_data_dir instead '''
22
22
 
23
+ db_file = user_data_dir+"/data.db"
24
+ settings_file = user_data_dir+"/nowfocus-settings.json"
23
25
  debug_level = 1 # dev value
24
26
  debug_systems = []
25
- pipe = "/tmp/"+app_id+"-pipe" # Will that work?
27
+ pipe = "/tmp/"+app_id+"-pipe"
28
+ is_first_load = False
26
29
 
27
30
  # key and type must be the same, (Seems redundant but it's is quite helpful)
28
31
  connectors = {
@@ -59,9 +62,10 @@ prototype_settings = {
59
62
  "pomodoro_interval": 40,
60
63
  "open_task_window_fullscreen": True,
61
64
  "show_task_window_sidebars": False,
62
- "randomness_interrupt_interval":5,
65
+ "randomness_interrupt_interval":7, #minutes
66
+ "device_not_in_use_threshold":3.5, #minutes
63
67
  "default_text": "What am I doing?",
64
- "todolist_refresh_interval":1,
68
+ "todolist_refresh_interval":1, #hours
65
69
  "version":0.4,
66
70
  "display_todolist_as_top_level_list":'auto',
67
71
  'max_top_level_menu_items':10,
@@ -69,7 +73,7 @@ prototype_settings = {
69
73
  'invoice_hourly_rate':0,
70
74
 
71
75
  "custom_pomodoro_intervals": {
72
- "email":7
76
+ "email":7 #minutes
73
77
  },
74
78
 
75
79
  'prompts':'What am I doing?\nWhy am I here?\nWhat could I do?\nWhat do I wish to accomplish?\nWhat is my aim?\nWhat\'s next',
@@ -84,11 +88,11 @@ prototype_settings = {
84
88
  },
85
89
 
86
90
  "todolists": {
87
- "Example Todo":{
88
- "id":"Example Todo",
91
+ "Nowfocus Todo":{
92
+ "id":"Nowfocus Todo",
89
93
  "type": "txt",
90
- "label": "Example Todo",
91
- "file": "example-todo.txt",
94
+ "label": "Nowfocus Todo",
95
+ "file": user_data_dir+'/nowfocus-todo.txt',
92
96
  "timetracker":"Example CSV",
93
97
  "status":True
94
98
  }
@@ -98,36 +102,43 @@ prototype_settings = {
98
102
  "id": "Example CSV",
99
103
  "label": "Example CSV",
100
104
  "type":"csv",
101
- "file":"sessions.csv",
105
+ "file":user_data_dir+'/nowfocus-timetracking-spreadsheet.csv',
102
106
  "status":True
103
107
  }
104
108
  }
105
109
  }
106
110
 
107
- if os.path.isfile(user_settings_dir+"/user_settings.json"):
111
+ if os.path.isfile(settings_file):
112
+ with open(settings_file, "r") as file:
113
+ settings = json.load(file)
114
+
115
+ # Backward compatibility, check in old settings dir
116
+ elif os.path.isfile(user_settings_dir+"/user_settings.json"):
108
117
  with open(user_settings_dir+"/user_settings.json", "r") as file:
109
- settings_file = json.load(file)
118
+ settings = json.load(file)
110
119
  else:
120
+
111
121
  print("Setting up initial settings file")
112
- settings_file = {}
122
+ is_first_load = True
123
+ settings = {}
113
124
 
114
125
 
115
126
  user = {}
116
127
 
117
- # Merge user_settings.json with prototype settings
128
+ # Merge settings with prototype settings
118
129
  for key, val in prototype_settings.items():
119
130
 
120
- if settings_file and key in connectors:
131
+ if settings and key in connectors:
121
132
  user[key] = {}
122
133
 
123
134
  # merge and validate todolist and timetracker settings against connector prototypes
124
- for c_key, c_val in settings_file[key].items():
135
+ for c_key, c_val in settings[key].items():
125
136
 
126
137
  if c_val['type'] in connectors[key]:
127
138
  proto = copy.copy(connectors[key][c_val['type']])
128
139
  user[key][c_key] = proto
129
140
  else:
130
- print('ERROR: no connector for type '+c_val['type']+' in user_settings.json','skipping that connector',c_val)
141
+ print('ERROR: no connector for type '+c_val['type']+' in settings_file','skipping that connector',c_val)
131
142
  continue
132
143
 
133
144
  # print('validate user_settings connector against prototype.','user',c_val,'proto',proto)
@@ -142,46 +153,28 @@ for key, val in prototype_settings.items():
142
153
  else:
143
154
  print("Adding missing: connection field "+p_field+")")
144
155
 
145
- elif key in settings_file and type(settings_file[key]) == type(val):
146
- user[key] = settings_file[key]
156
+ elif key in settings and type(settings[key]) == type(val):
157
+ user[key] = settings[key]
147
158
  else:
148
159
  user[key] = val
149
160
 
150
161
  # print(json.dumps(user, indent=4))
151
162
  # print(json.dumps(connectors['todolists'], indent=4))
152
163
 
153
- # update time_target format
154
- for id, tt in user['time_targets']['lists'].items():
155
- if 'within_value' not in tt:
156
- print("Updating time target to new format ",tt)
157
- tt['within_value'] = tt['num_days']
158
- tt['within_unit'] = 'days'
159
- print(tt)
160
- if 'status' not in tt:
161
- tt['status'] = True
162
164
 
163
-
164
- for id, tt in user['time_targets']['tasks'].items():
165
- tt
166
- if 'within_value' not in tt:
167
- print("Updating time target to new format ",tt)
168
- tt['within_value'] = tt['num_days']
169
- tt['within_unit'] = 'days'
170
- print(tt)
171
- if 'status' not in tt:
172
- tt['status'] = True
173
-
174
-
175
- with open(user_settings_dir+"/user_settings.json","w") as settings_file:
165
+ with open(settings_file,"w") as settings_file:
176
166
  json.dump(user, settings_file)
177
167
 
178
-
179
168
  todo_connectors = {}
180
169
  timetracker_connectors = {}
181
170
 
182
- # Load used connector modules
183
- for id, todolist in user['todolists'].items():
184
- todo_connectors[todolist['type']] = importlib.import_module('connectors.'+todolist['type'])
185
-
186
- for id, timetracker in user['timetrackers'].items():
187
- timetracker_connectors[timetracker['type']] = importlib.import_module('connectors.'+timetracker['type'])
171
+ try:
172
+
173
+ for id, todolist in user['todolists'].items():
174
+ todo_connectors[todolist['type']] = importlib.import_module('connectors.'+todolist['type'])
175
+
176
+ for id, timetracker in user['timetrackers'].items():
177
+ timetracker_connectors[timetracker['type']] = importlib.import_module('connectors.'+timetracker['type'])
178
+ except Exception as e:
179
+ print("error loading connector")
180
+ print(e)
@@ -3,7 +3,7 @@ Name=nowfocus
3
3
  GenericName=Task, Todo, Timer, timertacker
4
4
  Version=1.0
5
5
  Comment=Accomplish one worthwhile thing at a time.
6
- Exec=nowfocus
6
+ Exec=nowfocus -f
7
7
  Icon=nowfocus
8
8
  Terminal=false
9
9
  Type=Application
@@ -5,9 +5,8 @@ Exercise
5
5
  Running
6
6
  Push-ups
7
7
  Yoga
8
- Set Up nowfocus
8
+ Set Up Nowfocus
9
9
  1 Open Settings
10
10
  3 Add your real Todo List(s)
11
11
  2 Set up your Time Tracker(s)
12
12
  4 Notice how numbering tasks "hoists" them
13
- 5 Refresh List
@@ -0,0 +1,92 @@
1
+ import os.path
2
+ import json
3
+ import importlib
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ import copy
7
+ import subprocess, sys
8
+ import shutil
9
+
10
+ import gi
11
+ gi.require_version('Gtk', '3.0')
12
+ from gi.repository import Gtk, GLib, Gdk, Gio
13
+
14
+ # Set working dir to file location
15
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
16
+
17
+ # Add working dir to path
18
+ sys.path.append(os.path.dirname(__file__))
19
+
20
+ import conf
21
+ from utils import *
22
+
23
+ def run_first_load_actions():
24
+ db_init()
25
+ create_default_timetracking_csv()
26
+ create_default_todo()
27
+ copy_desktop_integration_files()
28
+
29
+
30
+ def create_default_timetracking_csv():
31
+ target_file = conf.user_data_dir+'/nowfocus-timetracking-spreadsheet.csv'
32
+
33
+ data = 'date,duration,project,task,start time'
34
+
35
+ if not os.path.isfile(target_file):
36
+ with open(target_file, 'w') as file:
37
+ file.writelines(data)
38
+ print("Created default timetracker at "+target_file)
39
+ else:
40
+ print("Default timetracker already exists at "+target_file)
41
+
42
+
43
+ def create_default_todo():
44
+ source_file = 'example-todo.txt'
45
+ target_file = conf.user_data_dir+'/nowfocus-todo.txt'
46
+
47
+ if not os.path.isfile(target_file):
48
+ with open(source_file, 'r') as file:
49
+ data = file.readlines()
50
+
51
+ with open(target_file, 'w') as file:
52
+ file.writelines(data)
53
+
54
+
55
+ def db_init():
56
+ if not os.path.isfile(conf.db_file):
57
+ print('initializing database')
58
+
59
+ db_query("CREATE TABLE lists (id TEXT, label TEXT DEFAULT '', parent_id TEXT DEFAULT '', parent_label TEXT DEFAULT '', todolist TEXT DEFAULT '', priority INTEGER DEFAULT 0, status INTEGER DEFAULT 1, extended_label TEXT DEFAULT '', data TEXT DEFAULT '{}');")
60
+
61
+ db_query("CREATE TABLE tasks (id TEXT, label TEXT DEFAULT '', parent_id TEXT DEFAULT '', parent_label TEXT DEFAULT '', todolist TEXT DEFAULT '', priority INTEGER DEFAULT 0, status INTEGER DEFAULT 1, extended_label TEXT, data TEXT DEFAULT '{}');")
62
+
63
+ # , tags TEXT
64
+
65
+ db_query("CREATE TABLE sessions (start_time TEXT, duration INTEGER, task_id TEXT, parent_id TEXT, todolist TEXT, extended_label TEXT, notes TEXT, timetracker TEXT);")
66
+
67
+ db_query("CREATE TABLE system (field TEXT PRIMARY KEY NOT NULL, value TEXT);")
68
+
69
+ db_query("INSERT INTO system(field, value) VALUES('db_schema_version', '0.4')")
70
+
71
+
72
+ def copy_desktop_integration_files():
73
+
74
+ home = GLib.get_home_dir()
75
+
76
+ files = (
77
+ (home+'/.local/share/icons/hicolor/scalable/apps','nowfocus.svg'),
78
+ (home+'/.local/share/icons','nowfocus.png'),
79
+ (home+'/.local/share/applications/','nowfocus.desktop'),
80
+ (home+'/.config/autostart/','nowfocus.desktop')
81
+ )
82
+
83
+
84
+ for file in files:
85
+ try:
86
+ Path(file[0]).mkdir(parents=True, exist_ok=True)
87
+ shutil.copy("desktop-extras/"+file[1],file[0])
88
+ print("Copied "+file[1]+" to "+file[0])
89
+
90
+ except Exception as e:
91
+ print("Error Copying "+file[1]+" to "+file[0],e)
92
+
@@ -139,7 +139,7 @@ class SessionOptionsDialog(Gtk.Dialog):
139
139
 
140
140
  print(' start_time + ', session['start_time'])
141
141
 
142
- # Or change session[duration] value to reflect dropped / afk session
142
+ # Or change session[duration] value to reflect dropped session
143
143
  if response == 3:
144
144
  # Create custom end time
145
145
  custom_end_time = session['start_time'] + timedelta(seconds = (self.duration_input.get_value_as_int() * 60))
@@ -117,15 +117,15 @@ class TaskWindow(Gtk.Window):
117
117
 
118
118
  if passed_data:
119
119
  dbg('taskwindow passed_data',passed_data,s='taskwindow')
120
- if 'afk_time' in passed_data:
120
+ if 'user_inactive_time' in passed_data:
121
121
 
122
- last_active_time = datetime.now() - timedelta(seconds=passed_data['afk_time'])
122
+ last_active_time = datetime.now() - timedelta(seconds=passed_data['user_inactive_time'])
123
123
 
124
124
  last_active_str = time.strftime('%H:%M', last_active_time.timetuple())
125
125
 
126
- afk_label = Gtk.Label()
127
- afk_label.set_markup("<b>Inactive Since "+" "+str(last_active_str) +"</b>")
128
- self.header.add(afk_label)
126
+ user_inactive_label = Gtk.Label()
127
+ user_inactive_label.set_markup("<b>Inactive Since "+" "+str(last_active_str) +"</b>")
128
+ self.header.add(user_inactive_label)
129
129
 
130
130
  pause_then_button = Gtk.Button(label="Finish Then")
131
131
 
@@ -29,8 +29,6 @@ notify.init(conf.app_name)
29
29
 
30
30
  lists = {}
31
31
 
32
- db_file = conf.user_data_dir+"/data.db"
33
-
34
32
 
35
33
  def dbg(*data, **kwargs):
36
34
  ''' Any number of positional args then kwargs
@@ -318,7 +316,7 @@ def first(i, default = None):
318
316
  def save_user_settings():
319
317
  print("Save updated user_settings")
320
318
  dbg(conf.user)
321
- with open(conf.user_settings_dir+"/user_settings.json","w") as settings_file:
319
+ with open(conf.settings_file,"w") as settings_file:
322
320
  json.dump(conf.user, settings_file)
323
321
 
324
322
 
@@ -326,8 +324,7 @@ def db_query(sql,parameters=None,key=None,error_handling=1):
326
324
  '''error_handling: 2 = raise error, 1 = notify error, 0 = ignore errors '''
327
325
  # print('db_query parameters type: ',type(parameters))
328
326
  # print('first(parameters)',first(parameters))
329
- global db_file
330
- c = sqlite3.connect(db_file)
327
+ c = sqlite3.connect(conf.db_file)
331
328
  c.row_factory = sqlite3.Row
332
329
  try:
333
330
  with c:
@@ -368,23 +365,6 @@ def db_query(sql,parameters=None,key=None,error_handling=1):
368
365
  return []
369
366
 
370
367
 
371
- def db_init():
372
- if not os.path.isfile(db_file):
373
- print('initializing database')
374
-
375
- db_query("CREATE TABLE lists (id TEXT, label TEXT DEFAULT '', parent_id TEXT DEFAULT '', parent_label TEXT DEFAULT '', todolist TEXT DEFAULT '', priority INTEGER DEFAULT 0, status INTEGER DEFAULT 1, extended_label TEXT DEFAULT '', data TEXT DEFAULT '{}');")
376
-
377
- db_query("CREATE TABLE tasks (id TEXT, label TEXT DEFAULT '', parent_id TEXT DEFAULT '', parent_label TEXT DEFAULT '', todolist TEXT DEFAULT '', priority INTEGER DEFAULT 0, status INTEGER DEFAULT 1, extended_label TEXT, data TEXT DEFAULT '{}');")
378
-
379
- # , tags TEXT
380
-
381
- db_query("CREATE TABLE sessions (start_time TEXT, duration INTEGER, task_id TEXT, parent_id TEXT, todolist TEXT, extended_label TEXT, notes TEXT, timetracker TEXT);")
382
-
383
- db_query("CREATE TABLE system (field TEXT PRIMARY KEY NOT NULL, value TEXT);")
384
-
385
- db_query("INSERT INTO system(field, value) VALUES('db_schema_version', '0.4')")
386
-
387
-
388
368
  def db_schema_update():
389
369
 
390
370
  try:
@@ -0,0 +1,20 @@
1
+
2
+ # update time_target format
3
+ for id, tt in user['time_targets']['lists'].items():
4
+ if 'within_value' not in tt:
5
+ print("Updating time target to new format ",tt)
6
+ tt['within_value'] = tt['num_days']
7
+ tt['within_unit'] = 'days'
8
+ print(tt)
9
+ if 'status' not in tt:
10
+ tt['status'] = True
11
+
12
+
13
+ for id, tt in user['time_targets']['tasks'].items():
14
+ if 'within_value' not in tt:
15
+ print("Updating time target to new format ",tt)
16
+ tt['within_value'] = tt['num_days']
17
+ tt['within_unit'] = 'days'
18
+ print(tt)
19
+ if 'status' not in tt:
20
+ tt['status'] = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nowfocus
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: nowfocus: the open source task-tracking self-control panel.
5
5
  Author: AltruistEnterprises
6
6
  Project-URL: Homepage, https://www.nowfocus.org
@@ -112,13 +112,6 @@ nowfocus is a clean, keyboard-driven time management dashboard that flexibly con
112
112
  # Install nowfocus
113
113
  pipx install nowfocus
114
114
 
115
- # Enter application directory
116
- cd ~/.local/share/pipx/venvs/nowfocus/lib/python3.12/site-packages/nowfocus/desktop-extras
117
-
118
- # Copy desktop file and icon to /usr/share
119
- sudo cp nowfocus.desktop /usr/share/applications/nowfocus.desktop
120
- sudo cp nowfocus.svg /usr/share/icons/hicolor/scalable/apps/nowfocus.svg
121
-
122
115
  # and now Focus!
123
116
  nowfocus
124
117
 
@@ -130,7 +123,6 @@ nowfocus is a clean, keyboard-driven time management dashboard that flexibly con
130
123
  ```
131
124
 
132
125
 
133
-
134
126
  3. Add the following command to your startup applications:
135
127
  ```
136
128
  nowfocus --force
@@ -178,7 +170,7 @@ Open nowfocus **Settings** from the indicator menu or tasks window and connect y
178
170
 
179
171
  ### Install From Source
180
172
  ```
181
- git clone https://codeberg.org/AltruistEnterprises/nowfocus/nowfocus.git
173
+ git clone https://codeberg.org/AltruistEnterprises/nowfocus.git
182
174
  cd nowfocus
183
175
  python3 -m venv .venv/nowfocus-build
184
176
  source .venv/nowfocus-build/bin/activate
@@ -5,6 +5,7 @@ src/nowfocus/__init__.py
5
5
  src/nowfocus/__main__.py
6
6
  src/nowfocus/conf.py
7
7
  src/nowfocus/example-todo.txt
8
+ src/nowfocus/install.py
8
9
  src/nowfocus/new_task_dialog.py
9
10
  src/nowfocus/session_edit_dialog.py
10
11
  src/nowfocus/session_options.py
@@ -14,6 +15,7 @@ src/nowfocus/styles.css
14
15
  src/nowfocus/task_window.py
15
16
  src/nowfocus/user_idle_time.py
16
17
  src/nowfocus/utils.py
18
+ src/nowfocus/version_migrator.py
17
19
  src/nowfocus.egg-info/PKG-INFO
18
20
  src/nowfocus.egg-info/SOURCES.txt
19
21
  src/nowfocus.egg-info/dependency_links.txt
File without changes
File without changes