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.
Files changed (105) hide show
  1. {jott_cli-0.4.0 → jott_cli-0.5.1}/PKG-INFO +7 -1
  2. jott_cli-0.5.1/jot/__init__.py +53 -0
  3. jott_cli-0.5.1/jot/_app_navigation_mixin.py +148 -0
  4. jott_cli-0.5.1/jot/_dispatch_mixin.py +302 -0
  5. jott_cli-0.5.1/jot/app.py +277 -0
  6. jott_cli-0.5.1/jot/cli/__init__.py +207 -0
  7. jott_cli-0.5.1/jot/cli/archive.py +120 -0
  8. jott_cli-0.5.1/jot/cli/config.py +189 -0
  9. jott_cli-0.5.1/jot/cli/views.py +108 -0
  10. jott_cli-0.5.1/jot/commands/_ai_analysis_mixin.py +280 -0
  11. jott_cli-0.5.1/jot/commands/_ai_suggest_mixin.py +202 -0
  12. jott_cli-0.5.1/jot/commands/_audio_timer_mixin.py +113 -0
  13. jott_cli-0.5.1/jot/commands/_bulk_mixin.py +134 -0
  14. jott_cli-0.5.1/jot/commands/_context_mixin.py +203 -0
  15. jott_cli-0.5.1/jot/commands/_core_mixin.py +279 -0
  16. jott_cli-0.5.1/jot/commands/_gcal_mixin.py +387 -0
  17. jott_cli-0.5.1/jot/commands/_metadata_mixin.py +178 -0
  18. jott_cli-0.5.1/jot/commands/_notes_mixin.py +195 -0
  19. jott_cli-0.5.1/jot/commands/_transfer_mixin.py +288 -0
  20. jott_cli-0.5.1/jot/commands/_web_clipboard_mixin.py +140 -0
  21. jott_cli-0.5.1/jot/commands/handler.py +59 -0
  22. jott_cli-0.5.1/jot/core/_age_backlog_mixin.py +91 -0
  23. jott_cli-0.5.1/jot/core/_compress_mixin.py +104 -0
  24. jott_cli-0.5.1/jot/core/_crud_mixin.py +200 -0
  25. jott_cli-0.5.1/jot/core/_delete_mixin.py +132 -0
  26. jott_cli-0.5.1/jot/core/_export_mixin.py +130 -0
  27. jott_cli-0.5.1/jot/core/_id_migration_mixin.py +98 -0
  28. jott_cli-0.5.1/jot/core/_metadata_mixin.py +129 -0
  29. jott_cli-0.5.1/jot/core/_navigation_mixin.py +138 -0
  30. jott_cli-0.5.1/jot/core/_persistence_mixin.py +39 -0
  31. jott_cli-0.5.1/jot/core/_subtask_mixin.py +117 -0
  32. jott_cli-0.5.1/jot/core/archive_manager.py +186 -0
  33. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/core/constants.py +19 -5
  34. jott_cli-0.5.1/jot/core/task_manager.py +107 -0
  35. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/events.py +4 -2
  36. jott_cli-0.5.1/jot/integrations/keywords/_config_mixin.py +69 -0
  37. jott_cli-0.5.1/jot/integrations/keywords/_handlers_mixin.py +162 -0
  38. jott_cli-0.5.1/jot/integrations/keywords/handler.py +99 -0
  39. jott_cli-0.5.1/jot/mcp/__init__.py +1 -0
  40. jott_cli-0.5.1/jot/mcp/handlers.py +218 -0
  41. jott_cli-0.5.1/jot/mcp/schemas.py +185 -0
  42. jott_cli-0.5.1/jot/mcp/server.py +66 -0
  43. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/projects/registry.py +8 -1
  44. jott_cli-0.5.1/jot/ui/__init__.py +40 -0
  45. jott_cli-0.5.1/jot/ui/display.py +16 -0
  46. jott_cli-0.5.1/jot/ui/display_archive.py +117 -0
  47. jott_cli-0.5.1/jot/ui/display_footer.py +133 -0
  48. jott_cli-0.5.1/jot/ui/display_help.py +229 -0
  49. jott_cli-0.5.1/jot/ui/display_projects.py +151 -0
  50. jott_cli-0.5.1/jot/ui/display_tasks.py +287 -0
  51. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/ui/formatting.py +2 -8
  52. jott_cli-0.5.1/jot/ui/input.py +237 -0
  53. jott_cli-0.5.1/jot/ui/picker.py +238 -0
  54. jott_cli-0.5.1/jot/ui/styles.py +61 -0
  55. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/text_utils.py +53 -18
  56. {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/PKG-INFO +7 -1
  57. jott_cli-0.5.1/jott_cli.egg-info/SOURCES.txt +93 -0
  58. {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/requires.txt +6 -0
  59. jott_cli-0.5.1/jott_cli.egg-info/top_level.txt +1 -0
  60. {jott_cli-0.4.0 → jott_cli-0.5.1}/pyproject.toml +11 -4
  61. jott_cli-0.5.1/tests/test_command_handler.py +418 -0
  62. jott_cli-0.5.1/tests/test_dispatch.py +151 -0
  63. jott_cli-0.5.1/tests/test_edit_edge_cases.py +277 -0
  64. jott_cli-0.5.1/tests/test_fuzzy_search.py +151 -0
  65. jott_cli-0.5.1/tests/test_gcal_notes.py +155 -0
  66. jott_cli-0.5.1/tests/test_highlight.py +156 -0
  67. jott_cli-0.5.1/tests/test_input.py +548 -0
  68. jott_cli-0.5.1/tests/test_jot.py +3654 -0
  69. jott_cli-0.5.1/tests/test_picker.py +345 -0
  70. jott_cli-0.5.1/tests/test_styles.py +202 -0
  71. jott_cli-0.5.1/tests/test_subtask_notes.py +246 -0
  72. jott_cli-0.5.1/tests/test_today_filter.py +173 -0
  73. jott_cli-0.4.0/jot/__init__.py +0 -47
  74. jott_cli-0.4.0/jot/commands/handler.py +0 -3432
  75. jott_cli-0.4.0/jot/core/task_manager.py +0 -1159
  76. jott_cli-0.4.0/jot/integrations/keywords/handler.py +0 -443
  77. jott_cli-0.4.0/jot/ui/__init__.py +0 -22
  78. jott_cli-0.4.0/jot/ui/display.py +0 -820
  79. jott_cli-0.4.0/jot/ui/input.py +0 -74
  80. jott_cli-0.4.0/jott_cli.egg-info/SOURCES.txt +0 -40
  81. jott_cli-0.4.0/jott_cli.egg-info/top_level.txt +0 -2
  82. jott_cli-0.4.0/mcp_server.py +0 -615
  83. {jott_cli-0.4.0 → jott_cli-0.5.1}/LICENSE +0 -0
  84. {jott_cli-0.4.0 → jott_cli-0.5.1}/README.md +0 -0
  85. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/__init__.py +0 -0
  86. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/config.py +0 -0
  87. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/manager.py +0 -0
  88. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/categories/templates.py +0 -0
  89. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/commands/__init__.py +0 -0
  90. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/core/__init__.py +0 -0
  91. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/core/id_manager.py +0 -0
  92. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/__init__.py +0 -0
  93. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/__init__.py +0 -0
  94. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/account_manager.py +0 -0
  95. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/gcal/auth.py +0 -0
  96. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/integrations/keywords/__init__.py +0 -0
  97. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/projects/__init__.py +0 -0
  98. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/projects/backup.py +0 -0
  99. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/__init__.py +0 -0
  100. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/date_utils.py +0 -0
  101. {jott_cli-0.4.0 → jott_cli-0.5.1}/jot/utils/validation.py +0 -0
  102. {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/dependency_links.txt +0 -0
  103. {jott_cli-0.4.0 → jott_cli-0.5.1}/jott_cli.egg-info/entry_points.txt +0 -0
  104. {jott_cli-0.4.0 → jott_cli-0.5.1}/setup.cfg +0 -0
  105. {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.4.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)