ggltasks 0.1.7__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.
- ggltasks/__init__.py +0 -0
- ggltasks/auth.py +53 -0
- ggltasks/local_storage.py +37 -0
- ggltasks/main.py +444 -0
- ggltasks/task_service.py +514 -0
- ggltasks/ui_manager.py +321 -0
- ggltasks-0.1.7.dist-info/METADATA +102 -0
- ggltasks-0.1.7.dist-info/RECORD +12 -0
- ggltasks-0.1.7.dist-info/WHEEL +5 -0
- ggltasks-0.1.7.dist-info/entry_points.txt +2 -0
- ggltasks-0.1.7.dist-info/licenses/LICENSE +0 -0
- ggltasks-0.1.7.dist-info/top_level.txt +1 -0
ggltasks/__init__.py
ADDED
|
File without changes
|
ggltasks/auth.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import google.oauth2.credentials
|
|
3
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
4
|
+
from google.auth.transport.requests import Request
|
|
5
|
+
|
|
6
|
+
SCOPES = ['https://www.googleapis.com/auth/tasks']
|
|
7
|
+
HOME_DIR = os.path.expanduser('~')
|
|
8
|
+
GGLTASKS_DIR = os.path.join(HOME_DIR, '.ggltasks')
|
|
9
|
+
TOKEN_PATH = os.path.join(GGLTASKS_DIR, 'token.json')
|
|
10
|
+
CLIENT_SECRETS_PATH = os.path.join(GGLTASKS_DIR, 'client_secrets.json')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ensure_dir_exists():
|
|
14
|
+
os.makedirs(GGLTASKS_DIR, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_credentials():
|
|
18
|
+
_ensure_dir_exists()
|
|
19
|
+
|
|
20
|
+
if not os.path.exists(CLIENT_SECRETS_PATH):
|
|
21
|
+
raise FileNotFoundError(
|
|
22
|
+
f"\n\n Google API credentials not found!\n"
|
|
23
|
+
f" Expected location: {CLIENT_SECRETS_PATH}\n\n"
|
|
24
|
+
f" Steps to fix:\n"
|
|
25
|
+
f" 1. Go to https://console.cloud.google.com/\n"
|
|
26
|
+
f" 2. APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID\n"
|
|
27
|
+
f" 3. Application type: Desktop App\n"
|
|
28
|
+
f" 4. Download the JSON file, rename it to 'client_secrets.json'\n"
|
|
29
|
+
f" 5. Move it to: {CLIENT_SECRETS_PATH}\n"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
creds = None
|
|
33
|
+
if os.path.exists(TOKEN_PATH):
|
|
34
|
+
try:
|
|
35
|
+
creds = google.oauth2.credentials.Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error loading token.json: {e}. Initiating full re-authentication.")
|
|
38
|
+
creds = None
|
|
39
|
+
|
|
40
|
+
if not creds or not creds.valid:
|
|
41
|
+
if creds and creds.expired and creds.refresh_token:
|
|
42
|
+
try:
|
|
43
|
+
creds.refresh(Request())
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"Token refresh failed: {e}. Initiating full re-authentication.")
|
|
46
|
+
creds = None
|
|
47
|
+
if not creds:
|
|
48
|
+
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_PATH, SCOPES)
|
|
49
|
+
creds = flow.run_local_server(port=0)
|
|
50
|
+
with open(TOKEN_PATH, 'w') as token:
|
|
51
|
+
token.write(creds.to_json())
|
|
52
|
+
|
|
53
|
+
return creds
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
HOME_DIR = os.path.expanduser('~')
|
|
5
|
+
GGLTASKS_DIR = os.path.join(HOME_DIR, '.ggltasks')
|
|
6
|
+
STORAGE_FILE = os.path.join(GGLTASKS_DIR, 'local_tasks.json')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _ensure_dir_exists():
|
|
10
|
+
os.makedirs(GGLTASKS_DIR, exist_ok=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_data():
|
|
14
|
+
_ensure_dir_exists()
|
|
15
|
+
if not os.path.exists(STORAGE_FILE):
|
|
16
|
+
return {'task_lists': [], 'tasks': {}}
|
|
17
|
+
try:
|
|
18
|
+
with open(STORAGE_FILE, 'r') as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
except (json.JSONDecodeError, IOError):
|
|
21
|
+
return {'task_lists': [], 'tasks': {}}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def save_data(data):
|
|
25
|
+
_ensure_dir_exists()
|
|
26
|
+
tmp_path = STORAGE_FILE + '.tmp'
|
|
27
|
+
try:
|
|
28
|
+
with open(tmp_path, 'w') as f:
|
|
29
|
+
json.dump(data, f, indent=4)
|
|
30
|
+
os.replace(tmp_path, STORAGE_FILE)
|
|
31
|
+
return True
|
|
32
|
+
except IOError:
|
|
33
|
+
try:
|
|
34
|
+
os.remove(tmp_path)
|
|
35
|
+
except OSError:
|
|
36
|
+
pass
|
|
37
|
+
return False
|
ggltasks/main.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
from unicurses import *
|
|
2
|
+
from .task_service import TaskService
|
|
3
|
+
from .ui_manager import UIManager
|
|
4
|
+
import sys
|
|
5
|
+
import curses
|
|
6
|
+
from dateutil.parser import ParserError, isoparse
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
import queue
|
|
12
|
+
from unicurses import wrapper
|
|
13
|
+
|
|
14
|
+
_sync_result_queue = queue.Queue()
|
|
15
|
+
_sync_thread = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run_sync(service, result_queue):
|
|
19
|
+
try:
|
|
20
|
+
service.sync_to_google()
|
|
21
|
+
result_queue.put(('ok', None))
|
|
22
|
+
except Exception as exc:
|
|
23
|
+
result_queue.put(('err', str(exc)))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _trigger_background_sync(service, ui_manager):
|
|
27
|
+
global _sync_thread
|
|
28
|
+
if _sync_thread is not None and _sync_thread.is_alive():
|
|
29
|
+
return
|
|
30
|
+
ui_manager.start_sync_animation()
|
|
31
|
+
_sync_thread = threading.Thread(
|
|
32
|
+
target=_run_sync,
|
|
33
|
+
args=(service, _sync_result_queue),
|
|
34
|
+
daemon=True,
|
|
35
|
+
)
|
|
36
|
+
_sync_thread.start()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def open_editor_for_task_notes(stdscr, app_state, ui_manager):
|
|
40
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
41
|
+
initial_content = selected_task.get('notes', '')
|
|
42
|
+
editor = os.environ.get('EDITOR', 'vim')
|
|
43
|
+
|
|
44
|
+
with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False, mode='w+', encoding='utf-8') as tf:
|
|
45
|
+
tf.write(initial_content)
|
|
46
|
+
temp_path = tf.name
|
|
47
|
+
|
|
48
|
+
def_prog_mode()
|
|
49
|
+
endwin()
|
|
50
|
+
subprocess.call([editor, temp_path])
|
|
51
|
+
reset_prog_mode()
|
|
52
|
+
doupdate()
|
|
53
|
+
|
|
54
|
+
with open(temp_path, 'r', encoding='utf-8') as tf:
|
|
55
|
+
new_note = tf.read()
|
|
56
|
+
|
|
57
|
+
os.remove(temp_path)
|
|
58
|
+
|
|
59
|
+
if new_note != initial_content:
|
|
60
|
+
app_state.service.change_detail_task(app_state.active_list_id, selected_task['id'], new_note)
|
|
61
|
+
app_state.refresh_data()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_valid_date(date_str):
|
|
65
|
+
try:
|
|
66
|
+
isoparse(date_str)
|
|
67
|
+
return True
|
|
68
|
+
except (ParserError, ValueError):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AppState:
|
|
73
|
+
def __init__(self, task_service):
|
|
74
|
+
self.service = task_service
|
|
75
|
+
self.task_lists = self.service.get_task_lists()
|
|
76
|
+
self.active_list_id = self.service.active_list_id
|
|
77
|
+
self.current_parent_task_id = None
|
|
78
|
+
self.viewing_completed_tasks = False
|
|
79
|
+
self.filtered_tasks_cache = {}
|
|
80
|
+
self.task_counts = {}
|
|
81
|
+
self.list_buffer = ""
|
|
82
|
+
self.task_buffer = ""
|
|
83
|
+
self.parent_task_id_stack = []
|
|
84
|
+
self.parent_task_idx_stack = []
|
|
85
|
+
self.tasks = self.get_tasks_for_active_list()
|
|
86
|
+
self.tasks_due_today = self.service.get_tasks_due_today()
|
|
87
|
+
self.calculate_task_counts()
|
|
88
|
+
self.show_help = False
|
|
89
|
+
|
|
90
|
+
def calculate_task_counts(self):
|
|
91
|
+
for task_list in self.task_lists:
|
|
92
|
+
list_id = task_list['id']
|
|
93
|
+
self.task_counts[list_id] = len([
|
|
94
|
+
t for t in self.service.get_tasks_for_list(list_id)
|
|
95
|
+
if t.get('status') != 'completed'
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
def get_tasks_for_active_list(self):
|
|
99
|
+
if self.current_parent_task_id:
|
|
100
|
+
all_tasks = self.service.get_subtasks(self.active_list_id, self.current_parent_task_id)
|
|
101
|
+
else:
|
|
102
|
+
if self.active_list_id not in self.filtered_tasks_cache or self.service.dirty:
|
|
103
|
+
fetched_tasks = self.service.get_tasks_for_list(self.active_list_id)
|
|
104
|
+
self.filtered_tasks_cache[self.active_list_id] = fetched_tasks
|
|
105
|
+
all_tasks = self.filtered_tasks_cache[self.active_list_id]
|
|
106
|
+
|
|
107
|
+
if self.viewing_completed_tasks:
|
|
108
|
+
return [t for t in all_tasks if t.get('status') == 'completed']
|
|
109
|
+
else:
|
|
110
|
+
filtered = [t for t in all_tasks if t.get('status') != 'completed']
|
|
111
|
+
has_completed = any(t.get('status') == 'completed' for t in all_tasks)
|
|
112
|
+
if has_completed:
|
|
113
|
+
filtered.append({"id": "COMPLETED_SEPARATOR", "title": "", "status": "separator", "is_button": True})
|
|
114
|
+
filtered.append({"id": "SHOW_COMPLETED_BTN", "title": "--- Show Completed Tasks ---", "status": "button", "is_button": True})
|
|
115
|
+
return filtered
|
|
116
|
+
|
|
117
|
+
def refresh_data(self):
|
|
118
|
+
self.task_lists = self.service.get_task_lists()
|
|
119
|
+
if not self.active_list_id and self.task_lists:
|
|
120
|
+
self.active_list_id = self.task_lists[0]['id']
|
|
121
|
+
self.service.active_list_id = self.active_list_id
|
|
122
|
+
self.filtered_tasks_cache.clear()
|
|
123
|
+
self.tasks = self.get_tasks_for_active_list()
|
|
124
|
+
self.tasks_due_today = self.service.get_tasks_due_today()
|
|
125
|
+
self.calculate_task_counts()
|
|
126
|
+
|
|
127
|
+
def change_active_list(self, list_id):
|
|
128
|
+
if self.service.set_active_list(list_id):
|
|
129
|
+
self.active_list_id = list_id
|
|
130
|
+
self.current_parent_task_id = None
|
|
131
|
+
self.viewing_completed_tasks = False
|
|
132
|
+
self.tasks = self.get_tasks_for_active_list()
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def handle_input(stdscr, app_state, ui_manager):
|
|
138
|
+
try:
|
|
139
|
+
key = getch()
|
|
140
|
+
except curses.error:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
if key == -1:
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
if key in [ord('q'), ord('Q')]:
|
|
147
|
+
if _sync_thread is not None and _sync_thread.is_alive():
|
|
148
|
+
ui_manager.show_temporary_message("Waiting for sync to finish...")
|
|
149
|
+
_sync_thread.join(timeout=10.0)
|
|
150
|
+
if app_state.service.dirty:
|
|
151
|
+
ui_manager.start_sync_animation()
|
|
152
|
+
app_state.service.sync_to_google()
|
|
153
|
+
ui_manager.stop_sync_animation()
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
if key == KEY_RESIZE:
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
elif key == KEY_UP or key == ord('k'):
|
|
160
|
+
if ui_manager.active_panel == 'tasks':
|
|
161
|
+
ui_manager.update_task_selection(app_state.tasks, -1)
|
|
162
|
+
elif ui_manager.active_panel == 'due_today':
|
|
163
|
+
if ui_manager.selected_due_today_idx == 0:
|
|
164
|
+
ui_manager.active_panel = 'lists'
|
|
165
|
+
ui_manager.selected_list_idx = len(app_state.task_lists) - 1
|
|
166
|
+
else:
|
|
167
|
+
ui_manager.update_due_today_selection(app_state.tasks_due_today, -1)
|
|
168
|
+
elif ui_manager.active_panel == 'lists':
|
|
169
|
+
ui_manager.update_list_selection(app_state.task_lists, -1)
|
|
170
|
+
elif key == KEY_DOWN or key == ord('j'):
|
|
171
|
+
if ui_manager.active_panel == 'tasks':
|
|
172
|
+
ui_manager.update_task_selection(app_state.tasks, 1)
|
|
173
|
+
elif ui_manager.active_panel == 'lists':
|
|
174
|
+
if ui_manager.selected_list_idx == len(app_state.task_lists) - 1 and app_state.tasks_due_today:
|
|
175
|
+
ui_manager.active_panel = 'due_today'
|
|
176
|
+
ui_manager.selected_due_today_idx = 0
|
|
177
|
+
else:
|
|
178
|
+
ui_manager.update_list_selection(app_state.task_lists, 1)
|
|
179
|
+
elif ui_manager.active_panel == 'due_today':
|
|
180
|
+
ui_manager.update_due_today_selection(app_state.tasks_due_today, 1)
|
|
181
|
+
elif key == KEY_LEFT or key == ord('h'):
|
|
182
|
+
if app_state.viewing_completed_tasks:
|
|
183
|
+
app_state.viewing_completed_tasks = False
|
|
184
|
+
app_state.refresh_data()
|
|
185
|
+
ui_manager.reset_task_scroll()
|
|
186
|
+
elif app_state.current_parent_task_id:
|
|
187
|
+
app_state.current_parent_task_id = app_state.parent_task_id_stack.pop()
|
|
188
|
+
app_state.refresh_data()
|
|
189
|
+
if app_state.parent_task_idx_stack:
|
|
190
|
+
ui_manager.selected_task_idx = app_state.parent_task_idx_stack.pop()
|
|
191
|
+
elif ui_manager.active_panel == 'tasks':
|
|
192
|
+
ui_manager.toggle_panel()
|
|
193
|
+
elif key == KEY_RIGHT or key == ord('l'):
|
|
194
|
+
if ui_manager.active_panel == 'due_today' and app_state.tasks_due_today:
|
|
195
|
+
selected_task = app_state.tasks_due_today[ui_manager.selected_due_today_idx]
|
|
196
|
+
target_list_id = selected_task.get('_list_id')
|
|
197
|
+
if target_list_id:
|
|
198
|
+
if app_state.active_list_id != target_list_id:
|
|
199
|
+
app_state.change_active_list(target_list_id)
|
|
200
|
+
# Find the index of the task in the active list
|
|
201
|
+
task_idx = 0
|
|
202
|
+
for i, t in enumerate(app_state.tasks):
|
|
203
|
+
if t['id'] == selected_task['id']:
|
|
204
|
+
task_idx = i
|
|
205
|
+
break
|
|
206
|
+
ui_manager.selected_task_idx = task_idx
|
|
207
|
+
ui_manager.active_panel = 'tasks'
|
|
208
|
+
elif ui_manager.active_panel == 'lists':
|
|
209
|
+
selected_list = app_state.task_lists[ui_manager.selected_list_idx]
|
|
210
|
+
if app_state.active_list_id != selected_list['id']:
|
|
211
|
+
app_state.change_active_list(selected_list["id"])
|
|
212
|
+
ui_manager.reset_task_scroll()
|
|
213
|
+
ui_manager.toggle_panel()
|
|
214
|
+
elif ui_manager.active_panel == 'tasks' and app_state.tasks:
|
|
215
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
216
|
+
if selected_task.get("is_button"):
|
|
217
|
+
app_state.viewing_completed_tasks = True
|
|
218
|
+
app_state.refresh_data()
|
|
219
|
+
ui_manager.reset_task_scroll()
|
|
220
|
+
else:
|
|
221
|
+
app_state.parent_task_id_stack.append(app_state.current_parent_task_id)
|
|
222
|
+
app_state.parent_task_idx_stack.append(ui_manager.selected_task_idx)
|
|
223
|
+
app_state.current_parent_task_id = selected_task['id']
|
|
224
|
+
app_state.refresh_data()
|
|
225
|
+
ui_manager.reset_task_scroll()
|
|
226
|
+
|
|
227
|
+
elif key == ord('c'):
|
|
228
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
229
|
+
if not selected_task.get("is_button"):
|
|
230
|
+
app_state.service.toggle_task_status(app_state.active_list_id, selected_task["id"])
|
|
231
|
+
app_state.refresh_data()
|
|
232
|
+
|
|
233
|
+
elif key == ord('w'):
|
|
234
|
+
_trigger_background_sync(app_state.service, ui_manager)
|
|
235
|
+
|
|
236
|
+
elif key == ord('r'):
|
|
237
|
+
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
|
238
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
239
|
+
if not selected_task.get("is_button"):
|
|
240
|
+
new_title = ui_manager.get_user_input("New Task Title: ")
|
|
241
|
+
app_state.service.rename_task(app_state.active_list_id, selected_task["id"], new_title)
|
|
242
|
+
app_state.refresh_data()
|
|
243
|
+
elif ui_manager.active_panel == 'lists' and app_state.task_lists:
|
|
244
|
+
new_title = ui_manager.get_user_input("New List Title: ")
|
|
245
|
+
if new_title:
|
|
246
|
+
selected_list = app_state.task_lists[ui_manager.selected_list_idx]
|
|
247
|
+
app_state.service.rename_list(selected_list['id'], new_title)
|
|
248
|
+
app_state.refresh_data()
|
|
249
|
+
|
|
250
|
+
elif key == ord('a'):
|
|
251
|
+
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
|
252
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
253
|
+
if not selected_task.get("is_button"):
|
|
254
|
+
new_date = ui_manager.get_user_input("Due Date: ", example="2026-12-25 14:30")
|
|
255
|
+
if is_valid_date(new_date):
|
|
256
|
+
app_state.service.change_date_task(app_state.active_list_id, selected_task['id'], new_date)
|
|
257
|
+
app_state.refresh_data()
|
|
258
|
+
else:
|
|
259
|
+
ui_manager.show_temporary_message(f"Invalid date format: '{new_date}'")
|
|
260
|
+
|
|
261
|
+
elif key == ord('i'):
|
|
262
|
+
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
|
263
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
264
|
+
if not selected_task.get("is_button"):
|
|
265
|
+
open_editor_for_task_notes(stdscr, app_state, ui_manager)
|
|
266
|
+
|
|
267
|
+
elif key == ord('K'):
|
|
268
|
+
if ui_manager.active_panel == 'tasks' and app_state.tasks and ui_manager.selected_task_idx > 0:
|
|
269
|
+
task = app_state.tasks[ui_manager.selected_task_idx]
|
|
270
|
+
if not task.get('is_button'):
|
|
271
|
+
if ui_manager.selected_task_idx == 1:
|
|
272
|
+
prev_id = None
|
|
273
|
+
else:
|
|
274
|
+
prev_id = app_state.tasks[ui_manager.selected_task_idx - 2]['id']
|
|
275
|
+
app_state.service.move_task(app_state.active_list_id, task['id'], previous_id=prev_id, parent_id=task.get('parent'))
|
|
276
|
+
app_state.refresh_data()
|
|
277
|
+
ui_manager.selected_task_idx -= 1
|
|
278
|
+
|
|
279
|
+
elif key == ord('J'):
|
|
280
|
+
if ui_manager.active_panel == 'tasks' and app_state.tasks and ui_manager.selected_task_idx < len(app_state.tasks) - 1:
|
|
281
|
+
task = app_state.tasks[ui_manager.selected_task_idx]
|
|
282
|
+
if not task.get('is_button'):
|
|
283
|
+
next_task = app_state.tasks[ui_manager.selected_task_idx + 1]
|
|
284
|
+
if not next_task.get('is_button'):
|
|
285
|
+
app_state.service.move_task(app_state.active_list_id, task['id'], previous_id=next_task['id'], parent_id=task.get('parent'))
|
|
286
|
+
app_state.refresh_data()
|
|
287
|
+
ui_manager.selected_task_idx += 1
|
|
288
|
+
|
|
289
|
+
elif key == ord('X'):
|
|
290
|
+
if ui_manager.active_panel == 'tasks':
|
|
291
|
+
confirm = ui_manager.get_confirm_keypress("Clear all completed tasks?")
|
|
292
|
+
if confirm.lower() == 'y':
|
|
293
|
+
app_state.service.clear_completed_tasks(app_state.active_list_id)
|
|
294
|
+
app_state.refresh_data()
|
|
295
|
+
ui_manager.selected_task_idx = 0
|
|
296
|
+
|
|
297
|
+
elif key == ord('d'):
|
|
298
|
+
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
|
299
|
+
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
300
|
+
if not selected_task.get("is_button"):
|
|
301
|
+
confirm = ui_manager.get_confirm_keypress(f"Delete '{selected_task['title'][:30]}'?")
|
|
302
|
+
if confirm.lower() == 'y':
|
|
303
|
+
app_state.task_buffer = app_state.service.get_task(app_state.active_list_id, selected_task['id'])
|
|
304
|
+
app_state.service.delete_task(app_state.active_list_id, selected_task["id"])
|
|
305
|
+
app_state.refresh_data()
|
|
306
|
+
if ui_manager.selected_task_idx >= len(app_state.tasks) and len(app_state.tasks) > 0:
|
|
307
|
+
ui_manager.selected_task_idx = len(app_state.tasks) - 1
|
|
308
|
+
elif ui_manager.active_panel == 'lists' and app_state.task_lists:
|
|
309
|
+
selected_list = app_state.task_lists[ui_manager.selected_list_idx]
|
|
310
|
+
confirm = ui_manager.get_confirm_keypress(f"Delete list '{selected_list['title']}'?")
|
|
311
|
+
if confirm.lower() == 'y':
|
|
312
|
+
app_state.list_buffer = selected_list['title']
|
|
313
|
+
app_state.service.delete_list(selected_list["id"])
|
|
314
|
+
app_state.task_lists = app_state.service.get_task_lists()
|
|
315
|
+
if app_state.task_lists:
|
|
316
|
+
app_state.change_active_list(app_state.task_lists[0]['id'])
|
|
317
|
+
else:
|
|
318
|
+
app_state.active_list_id = None
|
|
319
|
+
app_state.refresh_data()
|
|
320
|
+
|
|
321
|
+
elif key == ord('p'):
|
|
322
|
+
if ui_manager.active_panel == 'tasks':
|
|
323
|
+
if app_state.tasks:
|
|
324
|
+
current_task = app_state.tasks[ui_manager.selected_task_idx]
|
|
325
|
+
if current_task.get("is_button"):
|
|
326
|
+
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer)
|
|
327
|
+
else:
|
|
328
|
+
unfiltered_tasks = app_state.service.data['tasks'][app_state.active_list_id]
|
|
329
|
+
unfiltered_index = -1
|
|
330
|
+
for i, task in enumerate(unfiltered_tasks):
|
|
331
|
+
if task['id'] == current_task['id']:
|
|
332
|
+
unfiltered_index = i
|
|
333
|
+
break
|
|
334
|
+
if unfiltered_index != -1:
|
|
335
|
+
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer, unfiltered_index)
|
|
336
|
+
else:
|
|
337
|
+
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer)
|
|
338
|
+
else:
|
|
339
|
+
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer)
|
|
340
|
+
app_state.refresh_data()
|
|
341
|
+
else:
|
|
342
|
+
app_state.service.add_list(app_state.list_buffer)
|
|
343
|
+
app_state.refresh_data()
|
|
344
|
+
|
|
345
|
+
elif key == ord('o'):
|
|
346
|
+
if ui_manager.active_panel == 'tasks':
|
|
347
|
+
new_title = ui_manager.get_user_input("New Task Title: ")
|
|
348
|
+
if new_title:
|
|
349
|
+
if app_state.current_parent_task_id:
|
|
350
|
+
app_state.service.add_task(app_state.active_list_id, new_title, parent=app_state.current_parent_task_id)
|
|
351
|
+
else:
|
|
352
|
+
app_state.service.add_task(app_state.active_list_id, new_title)
|
|
353
|
+
app_state.refresh_data()
|
|
354
|
+
else:
|
|
355
|
+
new_title = ui_manager.get_user_input("New List Title: ")
|
|
356
|
+
if new_title:
|
|
357
|
+
app_state.service.add_list(new_title)
|
|
358
|
+
app_state.refresh_data()
|
|
359
|
+
|
|
360
|
+
elif key == ord('?'):
|
|
361
|
+
ui_manager.toggle_help()
|
|
362
|
+
|
|
363
|
+
if app_state.service.dirty:
|
|
364
|
+
_trigger_background_sync(app_state.service, ui_manager)
|
|
365
|
+
|
|
366
|
+
return True
|
|
367
|
+
def _run_initial_sync(service, result_queue):
|
|
368
|
+
try:
|
|
369
|
+
service.sync_from_google()
|
|
370
|
+
result_queue.put(('ok', None))
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
result_queue.put(('err', str(exc)))
|
|
373
|
+
|
|
374
|
+
def _trigger_initial_sync(service, ui_manager):
|
|
375
|
+
ui_manager.start_sync_animation()
|
|
376
|
+
threading.Thread(
|
|
377
|
+
target=_run_initial_sync,
|
|
378
|
+
args=(service, _sync_result_queue),
|
|
379
|
+
daemon=True,
|
|
380
|
+
).start()
|
|
381
|
+
|
|
382
|
+
def main_loop(stdscr):
|
|
383
|
+
task_service = TaskService()
|
|
384
|
+
ui_manager = UIManager(stdscr)
|
|
385
|
+
app_state = AppState(task_service)
|
|
386
|
+
|
|
387
|
+
curs_set(0)
|
|
388
|
+
noecho()
|
|
389
|
+
halfdelay(2)
|
|
390
|
+
keypad(stdscr, True)
|
|
391
|
+
|
|
392
|
+
_trigger_initial_sync(app_state.service, ui_manager)
|
|
393
|
+
app_state.refresh_data()
|
|
394
|
+
|
|
395
|
+
running = True
|
|
396
|
+
while running:
|
|
397
|
+
try:
|
|
398
|
+
result_kind, result_msg = _sync_result_queue.get_nowait()
|
|
399
|
+
ui_manager.stop_sync_animation()
|
|
400
|
+
if result_kind == 'ok':
|
|
401
|
+
app_state.refresh_data()
|
|
402
|
+
if app_state.service.dirty:
|
|
403
|
+
_trigger_background_sync(app_state.service, ui_manager)
|
|
404
|
+
else:
|
|
405
|
+
app_state.service.sync_from_google()
|
|
406
|
+
app_state.refresh_data()
|
|
407
|
+
ui_manager.show_temporary_message(f"Sync failed (reverted): {result_msg}")
|
|
408
|
+
except queue.Empty:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
parent_task = None
|
|
413
|
+
if app_state.current_parent_task_id:
|
|
414
|
+
parent_task = app_state.service.get_task(app_state.active_list_id, app_state.current_parent_task_id)
|
|
415
|
+
|
|
416
|
+
parent_ids = app_state.service.get_parent_task_ids(app_state.active_list_id)
|
|
417
|
+
children_counts = app_state.service.get_children_counts(app_state.active_list_id)
|
|
418
|
+
|
|
419
|
+
ui_manager.draw_layout(
|
|
420
|
+
app_state.task_lists,
|
|
421
|
+
app_state.tasks,
|
|
422
|
+
app_state.active_list_id,
|
|
423
|
+
app_state.task_counts,
|
|
424
|
+
parent_task=parent_task,
|
|
425
|
+
parent_ids=parent_ids,
|
|
426
|
+
children_counts=children_counts,
|
|
427
|
+
viewing_completed_tasks=app_state.viewing_completed_tasks,
|
|
428
|
+
tasks_due_today=app_state.tasks_due_today
|
|
429
|
+
)
|
|
430
|
+
except Exception as e:
|
|
431
|
+
ui_manager.show_temporary_message(f"Error: {e}")
|
|
432
|
+
|
|
433
|
+
running = handle_input(stdscr, app_state, ui_manager)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def cli():
|
|
437
|
+
try:
|
|
438
|
+
wrapper(main_loop)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"An error occurred: {e}", file=sys.stderr)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
cli()
|