jott-cli 0.4.0__tar.gz → 0.5.1__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.
- {jott_cli-0.4.0 → jott_cli-0.5.1}/PKG-INFO +7 -1
- jott_cli-0.5.1/jot/__init__.py +53 -0
- jott_cli-0.5.1/jot/_app_navigation_mixin.py +148 -0
- jott_cli-0.5.1/jot/_dispatch_mixin.py +302 -0
- jott_cli-0.5.1/jot/app.py +277 -0
- jott_cli-0.5.1/jot/cli/__init__.py +207 -0
- jott_cli-0.5.1/jot/cli/archive.py +120 -0
- jott_cli-0.5.1/jot/cli/config.py +189 -0
- jott_cli-0.5.1/jot/cli/views.py +108 -0
- jott_cli-0.5.1/jot/commands/_ai_analysis_mixin.py +280 -0
- jott_cli-0.5.1/jot/commands/_ai_suggest_mixin.py +202 -0
- jott_cli-0.5.1/jot/commands/_audio_timer_mixin.py +113 -0
- jott_cli-0.5.1/jot/commands/_bulk_mixin.py +134 -0
- jott_cli-0.5.1/jot/commands/_context_mixin.py +203 -0
- jott_cli-0.5.1/jot/commands/_core_mixin.py +279 -0
- jott_cli-0.5.1/jot/commands/_gcal_mixin.py +387 -0
- jott_cli-0.5.1/jot/commands/_metadata_mixin.py +178 -0
- jott_cli-0.5.1/jot/commands/_notes_mixin.py +195 -0
- jott_cli-0.5.1/jot/commands/_transfer_mixin.py +288 -0
- jott_cli-0.5.1/jot/commands/_web_clipboard_mixin.py +140 -0
- jott_cli-0.5.1/jot/commands/handler.py +59 -0
- jott_cli-0.5.1/jot/core/_age_backlog_mixin.py +91 -0
- jott_cli-0.5.1/jot/core/_compress_mixin.py +104 -0
- jott_cli-0.5.1/jot/core/_crud_mixin.py +200 -0
- jott_cli-0.5.1/jot/core/_delete_mixin.py +132 -0
- jott_cli-0.5.1/jot/core/_export_mixin.py +130 -0
- jott_cli-0.5.1/jot/core/_id_migration_mixin.py +98 -0
- jott_cli-0.5.1/jot/core/_metadata_mixin.py +129 -0
- jott_cli-0.5.1/jot/core/_navigation_mixin.py +138 -0
- jott_cli-0.5.1/jot/core/_persistence_mixin.py +39 -0
- jott_cli-0.5.1/jot/core/_subtask_mixin.py +117 -0
- jott_cli-0.5.1/jot/core/archive_manager.py +186 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/core/constants.py +19 -5
- jott_cli-0.5.1/jot/core/task_manager.py +107 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/events.py +4 -2
- jott_cli-0.5.1/jot/integrations/keywords/_config_mixin.py +69 -0
- jott_cli-0.5.1/jot/integrations/keywords/_handlers_mixin.py +162 -0
- jott_cli-0.5.1/jot/integrations/keywords/handler.py +99 -0
- jott_cli-0.5.1/jot/mcp/__init__.py +1 -0
- jott_cli-0.5.1/jot/mcp/handlers.py +218 -0
- jott_cli-0.5.1/jot/mcp/schemas.py +185 -0
- jott_cli-0.5.1/jot/mcp/server.py +66 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/projects/registry.py +8 -1
- jott_cli-0.5.1/jot/ui/__init__.py +40 -0
- jott_cli-0.5.1/jot/ui/display.py +16 -0
- jott_cli-0.5.1/jot/ui/display_archive.py +117 -0
- jott_cli-0.5.1/jot/ui/display_footer.py +133 -0
- jott_cli-0.5.1/jot/ui/display_help.py +229 -0
- jott_cli-0.5.1/jot/ui/display_projects.py +151 -0
- jott_cli-0.5.1/jot/ui/display_tasks.py +287 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/ui/formatting.py +2 -8
- jott_cli-0.5.1/jot/ui/input.py +237 -0
- jott_cli-0.5.1/jot/ui/picker.py +238 -0
- jott_cli-0.5.1/jot/ui/styles.py +61 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/text_utils.py +53 -18
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/PKG-INFO +7 -1
- jott_cli-0.5.1/jott_cli.egg-info/SOURCES.txt +93 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/requires.txt +6 -0
- jott_cli-0.5.1/jott_cli.egg-info/top_level.txt +1 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/pyproject.toml +11 -4
- jott_cli-0.5.1/tests/test_command_handler.py +418 -0
- jott_cli-0.5.1/tests/test_dispatch.py +151 -0
- jott_cli-0.5.1/tests/test_edit_edge_cases.py +277 -0
- jott_cli-0.5.1/tests/test_fuzzy_search.py +151 -0
- jott_cli-0.5.1/tests/test_gcal_notes.py +155 -0
- jott_cli-0.5.1/tests/test_highlight.py +156 -0
- jott_cli-0.5.1/tests/test_input.py +548 -0
- jott_cli-0.5.1/tests/test_jot.py +3654 -0
- jott_cli-0.5.1/tests/test_picker.py +345 -0
- jott_cli-0.5.1/tests/test_styles.py +202 -0
- jott_cli-0.5.1/tests/test_subtask_notes.py +246 -0
- jott_cli-0.5.1/tests/test_today_filter.py +173 -0
- jott_cli-0.4.0/jot/__init__.py +0 -47
- jott_cli-0.4.0/jot/commands/handler.py +0 -3432
- jott_cli-0.4.0/jot/core/task_manager.py +0 -1159
- jott_cli-0.4.0/jot/integrations/keywords/handler.py +0 -443
- jott_cli-0.4.0/jot/ui/__init__.py +0 -22
- jott_cli-0.4.0/jot/ui/display.py +0 -820
- jott_cli-0.4.0/jot/ui/input.py +0 -74
- jott_cli-0.4.0/jott_cli.egg-info/SOURCES.txt +0 -40
- jott_cli-0.4.0/jott_cli.egg-info/top_level.txt +0 -2
- jott_cli-0.4.0/mcp_server.py +0 -615
- {jott_cli-0.4.0 → jott_cli-0.5.1}/LICENSE +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/README.md +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/config.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/manager.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/templates.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/commands/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/core/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/core/id_manager.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/account_manager.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/auth.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/keywords/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/projects/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/projects/backup.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/__init__.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/date_utils.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/validation.py +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/dependency_links.txt +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/entry_points.txt +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/setup.cfg +0 -0
- {jott_cli-0.4.0 → jott_cli-0.5.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jott-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Feature-rich interactive CLI task manager with AI integration, calendar sync, and keyword automation
|
|
5
5
|
Author-email: Scott Anderson <sonander@gmail.com>
|
|
6
6
|
Maintainer-email: Scott Anderson <sonander@gmail.com>
|
|
@@ -9,6 +9,7 @@ Project-URL: Homepage, https://github.com/son1112/jot
|
|
|
9
9
|
Project-URL: Repository, https://github.com/son1112/jot
|
|
10
10
|
Project-URL: Issues, https://github.com/son1112/jot/issues
|
|
11
11
|
Project-URL: Changelog, https://github.com/son1112/jot/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Documentation, https://son1112.github.io/jot/
|
|
12
13
|
Keywords: cli,task-management,productivity,todo,gtd,ai-integration
|
|
13
14
|
Classifier: Development Status :: 4 - Beta
|
|
14
15
|
Classifier: Environment :: Console
|
|
@@ -29,6 +30,7 @@ Requires-Dist: google-auth-oauthlib>=0.5.0
|
|
|
29
30
|
Requires-Dist: google-auth-httplib2>=0.1.0
|
|
30
31
|
Requires-Dist: google-api-python-client>=2.0.0
|
|
31
32
|
Requires-Dist: filelock>=3.0.0
|
|
33
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
32
34
|
Provides-Extra: dev
|
|
33
35
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
36
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -40,6 +42,10 @@ Provides-Extra: mcp
|
|
|
40
42
|
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
41
43
|
Provides-Extra: tts
|
|
42
44
|
Requires-Dist: pyttsx3>=2.90; extra == "tts"
|
|
45
|
+
Provides-Extra: docs
|
|
46
|
+
Requires-Dist: sphinx<8.0,>=5.0; extra == "docs"
|
|
47
|
+
Requires-Dist: furo>=2023.1; extra == "docs"
|
|
48
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.20; extra == "docs"
|
|
43
49
|
Dynamic: license-file
|
|
44
50
|
|
|
45
51
|
# jott - AI-Powered CLI Project Management
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""jott package - Simple, extensible interactive CLI task list
|
|
2
|
+
|
|
3
|
+
This package provides modular components for the jott task management system.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
__version__ = "0.5.1"
|
|
11
|
+
|
|
12
|
+
# When building Sphinx docs, skip the dynamic import bridge and heavy deps.
|
|
13
|
+
# Set JOT_SPHINX_BUILD=1 in docs/sphinx/conf.py before importing jot.
|
|
14
|
+
if os.environ.get('JOT_SPHINX_BUILD'):
|
|
15
|
+
main = None
|
|
16
|
+
else:
|
|
17
|
+
# Import extracted classes from their new modules
|
|
18
|
+
from jot.core.id_manager import IDManager
|
|
19
|
+
from jot.core.task_manager import TaskManager
|
|
20
|
+
from jot.projects.registry import ProjectRegistry
|
|
21
|
+
from jot.projects.backup import ProjectBackup
|
|
22
|
+
from jot.categories.manager import CategoryManager
|
|
23
|
+
from jot.categories.config import CategoryConfig
|
|
24
|
+
from jot.categories.templates import CategoryTemplates
|
|
25
|
+
from jot.integrations.gcal.account_manager import GoogleCalendarAccountManager
|
|
26
|
+
from jot.integrations.gcal.auth import GoogleCalendarAuth
|
|
27
|
+
from jot.integrations.gcal.events import create_gcal_event, fetch_gcal_events
|
|
28
|
+
from jot.integrations.keywords.handler import KeywordHandler
|
|
29
|
+
|
|
30
|
+
# Load jot.py for main() and other not-yet-extracted components
|
|
31
|
+
_jot_script_path = Path(__file__).parent.parent / 'jot.py'
|
|
32
|
+
_spec = importlib.util.spec_from_file_location("_jot_script", _jot_script_path)
|
|
33
|
+
_jot_script = importlib.util.module_from_spec(_spec)
|
|
34
|
+
_spec.loader.exec_module(_jot_script)
|
|
35
|
+
|
|
36
|
+
# Export main function from jot.py
|
|
37
|
+
main = _jot_script.main
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
'main',
|
|
41
|
+
'TaskManager',
|
|
42
|
+
'ProjectRegistry',
|
|
43
|
+
'ProjectBackup',
|
|
44
|
+
'IDManager',
|
|
45
|
+
'CategoryManager',
|
|
46
|
+
'CategoryConfig',
|
|
47
|
+
'CategoryTemplates',
|
|
48
|
+
'KeywordHandler',
|
|
49
|
+
'GoogleCalendarAccountManager',
|
|
50
|
+
'GoogleCalendarAuth',
|
|
51
|
+
'create_gcal_event',
|
|
52
|
+
'fetch_gcal_events',
|
|
53
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Navigation mixin — navigation, toggle, and switching helpers for App."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from jot.categories.manager import CategoryManager
|
|
7
|
+
from jot.commands import CommandHandler
|
|
8
|
+
from jot.core.constants import MODE_QUICK_ADD
|
|
9
|
+
from jot.core.task_manager import TaskManager
|
|
10
|
+
from jot.ui.styles import RESET, CYAN
|
|
11
|
+
from jot.utils.date_utils import get_today_day_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AppNavigationMixin:
|
|
15
|
+
"""Navigation, toggle, and category/project switching for App."""
|
|
16
|
+
|
|
17
|
+
def _handle_navigation(self, key, tasks_to_display):
|
|
18
|
+
"""Process UP/DOWN/SHIFT_UP/SHIFT_DOWN. Returns True if
|
|
19
|
+
the key was a navigation key."""
|
|
20
|
+
filtered = (
|
|
21
|
+
tasks_to_display if self.state.show_today_only else None)
|
|
22
|
+
|
|
23
|
+
if key == 'UP':
|
|
24
|
+
self.task_manager.set_prev_current(filtered)
|
|
25
|
+
return True
|
|
26
|
+
if key == 'DOWN':
|
|
27
|
+
self.task_manager.set_next_current(filtered)
|
|
28
|
+
return True
|
|
29
|
+
if key == 'SHIFT_UP':
|
|
30
|
+
self.task_manager.move_task_up()
|
|
31
|
+
return True
|
|
32
|
+
if key == 'SHIFT_DOWN':
|
|
33
|
+
self.task_manager.move_task_down()
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def _handle_toggle(self, key):
|
|
38
|
+
"""Process shared toggle keys A/O/Y/F/?. Returns True if
|
|
39
|
+
the key was a toggle key."""
|
|
40
|
+
st = self.state
|
|
41
|
+
|
|
42
|
+
toggle_map = {
|
|
43
|
+
'A': ('show_archived', 'archived tasks'),
|
|
44
|
+
'O': ('sort_by_day', None),
|
|
45
|
+
'Y': ('show_today_only', None),
|
|
46
|
+
'F': ('show_notes_inline', 'inline notes'),
|
|
47
|
+
'?': ('show_shortcuts', 'shortcuts'),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if key not in toggle_map:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
attr, label = toggle_map[key]
|
|
54
|
+
|
|
55
|
+
if key == 'O':
|
|
56
|
+
st.sort_by_day = not st.sort_by_day
|
|
57
|
+
msg = "Sorting by day" if st.sort_by_day else "Normal order"
|
|
58
|
+
elif key == 'Y':
|
|
59
|
+
st.show_today_only = not st.show_today_only
|
|
60
|
+
today = get_today_day_name()
|
|
61
|
+
msg = (f"Showing only {today} tasks"
|
|
62
|
+
if st.show_today_only else "Showing all tasks")
|
|
63
|
+
else:
|
|
64
|
+
current = getattr(st, attr)
|
|
65
|
+
setattr(st, attr, not current)
|
|
66
|
+
new_val = not current
|
|
67
|
+
status = "Showing" if new_val else "Hiding"
|
|
68
|
+
msg = f"{status} {label}"
|
|
69
|
+
|
|
70
|
+
print(f"\n{CYAN}\u2713 {msg}{RESET}")
|
|
71
|
+
time.sleep(0.3)
|
|
72
|
+
st.input_buffer = ""
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def _reload_task_manager(self, task_manager):
|
|
76
|
+
"""Replace the active TaskManager and CommandHandler."""
|
|
77
|
+
import os
|
|
78
|
+
self.task_manager = task_manager
|
|
79
|
+
self.command_handler = CommandHandler(
|
|
80
|
+
task_manager, registry=self.registry)
|
|
81
|
+
self._last_mtime = (
|
|
82
|
+
os.path.getmtime(task_manager.storage_file)
|
|
83
|
+
if task_manager.storage_file.exists() else 0)
|
|
84
|
+
|
|
85
|
+
def _handle_switch_category(self):
|
|
86
|
+
"""Shift+C — switch category via picker."""
|
|
87
|
+
result = self.command_handler.switch_category()
|
|
88
|
+
if isinstance(result, tuple) and result[0] == 'switch_category':
|
|
89
|
+
_, new_category, new_is_global = result
|
|
90
|
+
if new_is_global:
|
|
91
|
+
tm = TaskManager(
|
|
92
|
+
category=new_category, is_global=True,
|
|
93
|
+
project_registry=self.registry)
|
|
94
|
+
else:
|
|
95
|
+
tm = TaskManager(
|
|
96
|
+
directory=self.target_dir, category=new_category,
|
|
97
|
+
is_global=False, project_registry=self.registry)
|
|
98
|
+
self._reload_task_manager(tm)
|
|
99
|
+
print(f"\u2713 Switched to category: "
|
|
100
|
+
f"{new_category or 'default'}")
|
|
101
|
+
print("\nPress Enter to continue...", end='', flush=True)
|
|
102
|
+
input()
|
|
103
|
+
self.state.input_buffer = ""
|
|
104
|
+
|
|
105
|
+
def _handle_switch_project(self):
|
|
106
|
+
"""Shift+Z — switch project via picker."""
|
|
107
|
+
result = self.command_handler.switch_project()
|
|
108
|
+
if isinstance(result, tuple) and result[0] == 'switch_project':
|
|
109
|
+
_, proj_name, project_path = result
|
|
110
|
+
self.target_dir = project_path
|
|
111
|
+
self.project_name = proj_name
|
|
112
|
+
tm = TaskManager(
|
|
113
|
+
directory=self.target_dir, category=None,
|
|
114
|
+
is_global=False, project_registry=self.registry)
|
|
115
|
+
self._reload_task_manager(tm)
|
|
116
|
+
print(f"\u2713 Switched to project: {proj_name}")
|
|
117
|
+
print("\nPress Enter to continue...", end='', flush=True)
|
|
118
|
+
input()
|
|
119
|
+
self.state.input_buffer = ""
|
|
120
|
+
|
|
121
|
+
def _handle_tab_cycle(self):
|
|
122
|
+
"""Tab — cycle through categories in the project."""
|
|
123
|
+
cat_manager = CategoryManager(project_dir=self.target_dir)
|
|
124
|
+
categories = cat_manager.discover_categories()
|
|
125
|
+
if not categories:
|
|
126
|
+
self.state.input_buffer = ""
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
current_cat = self.task_manager.category
|
|
130
|
+
if current_cat is None:
|
|
131
|
+
next_cat = categories[0]
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
idx = categories.index(current_cat)
|
|
135
|
+
next_cat = (
|
|
136
|
+
categories[idx + 1]
|
|
137
|
+
if idx + 1 < len(categories) else None)
|
|
138
|
+
except ValueError:
|
|
139
|
+
next_cat = categories[0]
|
|
140
|
+
|
|
141
|
+
tm = TaskManager(
|
|
142
|
+
directory=self.target_dir, category=next_cat,
|
|
143
|
+
is_global=False, project_registry=self.registry)
|
|
144
|
+
self._reload_task_manager(tm)
|
|
145
|
+
label = next_cat if next_cat else "default"
|
|
146
|
+
print(f"\n{CYAN}\u2192 {label}{RESET}")
|
|
147
|
+
time.sleep(0.2)
|
|
148
|
+
self.state.input_buffer = ""
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Dispatch mixin — mode-specific key dispatch for App."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from jot.core.constants import (
|
|
6
|
+
MODE_QUICK_ADD, MODE_COMMAND, MODE_MULTISELECT,
|
|
7
|
+
MODE_FUZZY_SEARCH, MODE_ALL_CATEGORIES,
|
|
8
|
+
)
|
|
9
|
+
from jot.ui.styles import RESET, CYAN
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DispatchMixin:
|
|
13
|
+
"""Mode-specific key dispatch methods for App."""
|
|
14
|
+
|
|
15
|
+
def _dispatch_quick_add(self, key, tasks_to_display):
|
|
16
|
+
"""Handle a keypress in QUICK_ADD mode."""
|
|
17
|
+
st = self.state
|
|
18
|
+
|
|
19
|
+
if key == '\r' or key == '\n':
|
|
20
|
+
self._quick_add_enter()
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
if key == '\x1b':
|
|
24
|
+
st.mode = MODE_COMMAND
|
|
25
|
+
st.input_buffer = ""
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if key == '\x7f':
|
|
29
|
+
st.input_buffer = st.input_buffer[:-1]
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if key == 'C':
|
|
33
|
+
self._handle_switch_category()
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if key == 'Z':
|
|
37
|
+
self._handle_switch_project()
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if key == 'L':
|
|
41
|
+
should_toggle, new_mode = (
|
|
42
|
+
self.command_handler.toggle_all_categories_view(st.mode))
|
|
43
|
+
if should_toggle:
|
|
44
|
+
st.mode = new_mode
|
|
45
|
+
st.input_buffer = ""
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if key == '=':
|
|
49
|
+
self.task_manager.sort_by_priority()
|
|
50
|
+
st.input_buffer = ""
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
method_name = self._QUICK_ADD_SIMPLE.get(key)
|
|
54
|
+
if method_name:
|
|
55
|
+
getattr(self.command_handler, method_name)()
|
|
56
|
+
st.input_buffer = ""
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if self._handle_toggle(key):
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if key == 'V':
|
|
63
|
+
st.mode = MODE_MULTISELECT
|
|
64
|
+
st.selected_tasks.clear()
|
|
65
|
+
st.input_buffer = ""
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if key == '\x06': # Ctrl+F
|
|
69
|
+
st.mode = MODE_FUZZY_SEARCH
|
|
70
|
+
st.search_buffer = ""
|
|
71
|
+
st.filtered_tasks = []
|
|
72
|
+
st.input_buffer = ""
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if key == '\t':
|
|
76
|
+
self._handle_tab_cycle()
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if self._handle_navigation(key, tasks_to_display):
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if len(key) == 1 and key.isprintable():
|
|
83
|
+
st.input_buffer += key
|
|
84
|
+
|
|
85
|
+
def _quick_add_enter(self):
|
|
86
|
+
"""Handle Enter key in quick-add mode."""
|
|
87
|
+
st = self.state
|
|
88
|
+
if st.input_buffer.strip():
|
|
89
|
+
result = self.task_manager.add_task(st.input_buffer.strip())
|
|
90
|
+
if isinstance(result, dict):
|
|
91
|
+
if result.get('action') == 'broadcast':
|
|
92
|
+
self._confirm_broadcast(result)
|
|
93
|
+
elif result.get('action') == 'route':
|
|
94
|
+
self._execute_route(result)
|
|
95
|
+
st.input_buffer = ""
|
|
96
|
+
else:
|
|
97
|
+
tally_count = self.task_manager.increment_tally()
|
|
98
|
+
if tally_count:
|
|
99
|
+
print(f"\n{CYAN}\u2713 Tally: \u00d7{tally_count}{RESET}")
|
|
100
|
+
time.sleep(0.3)
|
|
101
|
+
|
|
102
|
+
def _confirm_broadcast(self, info):
|
|
103
|
+
"""Prompt user before broadcasting task to all projects."""
|
|
104
|
+
from jot.ui.input import restore_terminal
|
|
105
|
+
projects = (self.task_manager.keyword_handler
|
|
106
|
+
.project_registry.list_projects())
|
|
107
|
+
n = len(projects)
|
|
108
|
+
print(f"\n\u26a0 Broadcast \"{info['task_text']}\" "
|
|
109
|
+
f"to {n} projects? (y/N): ", end='', flush=True)
|
|
110
|
+
restore_terminal()
|
|
111
|
+
if input().strip().lower() == 'y':
|
|
112
|
+
count = self.task_manager._route_to_project(
|
|
113
|
+
info['task_text'], info['project_name'],
|
|
114
|
+
info['priority'], info['status'],
|
|
115
|
+
info['labels'], info['effort'])
|
|
116
|
+
print(f"{CYAN}\u2713 Added to {count} projects{RESET}")
|
|
117
|
+
else:
|
|
118
|
+
print("Cancelled")
|
|
119
|
+
time.sleep(0.5)
|
|
120
|
+
|
|
121
|
+
def _execute_route(self, info):
|
|
122
|
+
"""Route task to a single project with feedback."""
|
|
123
|
+
count = self.task_manager._route_to_project(
|
|
124
|
+
info['task_text'], info['project_name'],
|
|
125
|
+
info['priority'], info['status'],
|
|
126
|
+
info['labels'], info['effort'])
|
|
127
|
+
if count:
|
|
128
|
+
print(f"\n{CYAN}\u2713 Routed to "
|
|
129
|
+
f"{info['project_name']}{RESET}")
|
|
130
|
+
time.sleep(0.5)
|
|
131
|
+
|
|
132
|
+
def _dispatch_command(self, key, tasks_to_display):
|
|
133
|
+
"""Handle a keypress in COMMAND mode."""
|
|
134
|
+
st = self.state
|
|
135
|
+
|
|
136
|
+
if key == 'a':
|
|
137
|
+
st.mode = MODE_QUICK_ADD
|
|
138
|
+
st.input_buffer = ""
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if self._handle_navigation(key, tasks_to_display):
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if key == '\r' or key == '\n':
|
|
145
|
+
tally_count = self.task_manager.increment_tally()
|
|
146
|
+
if tally_count:
|
|
147
|
+
print(f"\n{CYAN}\u2713 Tally: \u00d7{tally_count}{RESET}")
|
|
148
|
+
time.sleep(0.3)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if key in ('A', 'O', 'Y'):
|
|
152
|
+
self._handle_toggle(key)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if key:
|
|
156
|
+
st.running = self.command_handler.handle(key)
|
|
157
|
+
|
|
158
|
+
def _dispatch_multiselect(self, key, tasks_to_display):
|
|
159
|
+
"""Handle a keypress in MULTISELECT mode."""
|
|
160
|
+
st = self.state
|
|
161
|
+
|
|
162
|
+
if key == '\x1b':
|
|
163
|
+
st.mode = MODE_QUICK_ADD
|
|
164
|
+
st.selected_tasks.clear()
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
if key == ' ':
|
|
168
|
+
current_task = self.task_manager.get_current_task()
|
|
169
|
+
if current_task:
|
|
170
|
+
task_id = current_task['id']
|
|
171
|
+
if task_id in st.selected_tasks:
|
|
172
|
+
st.selected_tasks.remove(task_id)
|
|
173
|
+
else:
|
|
174
|
+
st.selected_tasks.add(task_id)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if key == 'A':
|
|
178
|
+
for task in self.task_manager.tasks:
|
|
179
|
+
st.selected_tasks.add(task['id'])
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if key == 'N':
|
|
183
|
+
st.selected_tasks.clear()
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
filtered = (
|
|
187
|
+
tasks_to_display if st.show_today_only else None)
|
|
188
|
+
|
|
189
|
+
if key == 'UP':
|
|
190
|
+
self.task_manager.set_prev_current(filtered)
|
|
191
|
+
return
|
|
192
|
+
if key == 'DOWN':
|
|
193
|
+
self.task_manager.set_next_current(filtered)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if key == 'SHIFT_UP':
|
|
197
|
+
self.task_manager.set_prev_current(filtered)
|
|
198
|
+
current_task = self.task_manager.get_current_task()
|
|
199
|
+
if current_task:
|
|
200
|
+
st.selected_tasks.add(current_task['id'])
|
|
201
|
+
return
|
|
202
|
+
if key == 'SHIFT_DOWN':
|
|
203
|
+
self.task_manager.set_next_current(filtered)
|
|
204
|
+
current_task = self.task_manager.get_current_task()
|
|
205
|
+
if current_task:
|
|
206
|
+
st.selected_tasks.add(current_task['id'])
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if key == 'B':
|
|
210
|
+
self._multiselect_bulk_action()
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
def _multiselect_bulk_action(self):
|
|
214
|
+
"""Process bulk action menu in multiselect mode."""
|
|
215
|
+
st = self.state
|
|
216
|
+
if not st.selected_tasks:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
action = self.command_handler.bulk_actions_menu(st.selected_tasks)
|
|
220
|
+
action_map = {
|
|
221
|
+
'priority': self.command_handler.bulk_set_priority,
|
|
222
|
+
'status': self.command_handler.bulk_set_status,
|
|
223
|
+
'delete': self.command_handler.bulk_delete,
|
|
224
|
+
'archive': self.command_handler.bulk_archive,
|
|
225
|
+
}
|
|
226
|
+
handler = action_map.get(action)
|
|
227
|
+
if handler:
|
|
228
|
+
handler(st.selected_tasks)
|
|
229
|
+
st.selected_tasks.clear()
|
|
230
|
+
|
|
231
|
+
def _dispatch_fuzzy_search(self, key):
|
|
232
|
+
"""Handle a keypress in FUZZY_SEARCH mode."""
|
|
233
|
+
st = self.state
|
|
234
|
+
|
|
235
|
+
if key == '\x1b':
|
|
236
|
+
st.mode = MODE_QUICK_ADD
|
|
237
|
+
st.search_buffer = ""
|
|
238
|
+
st.match_positions = {}
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
if key == '\r' or key == '\n':
|
|
242
|
+
current_task = self.task_manager.get_current_task()
|
|
243
|
+
if current_task:
|
|
244
|
+
st.mode = MODE_QUICK_ADD
|
|
245
|
+
st.search_buffer = ""
|
|
246
|
+
st.match_positions = {}
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
if key == '\x7f':
|
|
250
|
+
if st.search_buffer:
|
|
251
|
+
st.search_buffer = st.search_buffer[:-1]
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
if key == '\x01': # Ctrl+A
|
|
255
|
+
st.include_archived_in_search = (
|
|
256
|
+
not st.include_archived_in_search)
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
if key in ('UP', 'DOWN'):
|
|
260
|
+
self._fuzzy_navigate(key)
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if len(key) == 1 and key.isprintable():
|
|
264
|
+
st.search_buffer += key
|
|
265
|
+
|
|
266
|
+
def _fuzzy_navigate(self, key):
|
|
267
|
+
"""Navigate within fuzzy search filtered results."""
|
|
268
|
+
st = self.state
|
|
269
|
+
if not st.filtered_tasks:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
current_task = self.task_manager.get_current_task()
|
|
273
|
+
if not current_task:
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
current_id = current_task['id']
|
|
277
|
+
filtered_ids = [t[0]['id'] for t in st.filtered_tasks]
|
|
278
|
+
if current_id not in filtered_ids:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
idx = filtered_ids.index(current_id)
|
|
282
|
+
if key == 'UP' and idx > 0:
|
|
283
|
+
self.task_manager.set_current(
|
|
284
|
+
st.filtered_tasks[idx - 1][0]['id'])
|
|
285
|
+
elif key == 'DOWN' and idx < len(st.filtered_tasks) - 1:
|
|
286
|
+
self.task_manager.set_current(
|
|
287
|
+
st.filtered_tasks[idx + 1][0]['id'])
|
|
288
|
+
|
|
289
|
+
def _dispatch_all_categories(self, key, tasks_to_display):
|
|
290
|
+
"""Handle a keypress in ALL_CATEGORIES mode."""
|
|
291
|
+
st = self.state
|
|
292
|
+
|
|
293
|
+
if key == '\x1b' or key == 'L':
|
|
294
|
+
st.mode = MODE_QUICK_ADD
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
filtered = (
|
|
298
|
+
tasks_to_display if st.show_today_only else None)
|
|
299
|
+
if key == 'UP':
|
|
300
|
+
self.task_manager.set_prev_current(filtered)
|
|
301
|
+
elif key == 'DOWN':
|
|
302
|
+
self.task_manager.set_next_current(filtered)
|