jott-cli 0.5.1__tar.gz → 0.5.2__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 (97) hide show
  1. {jott_cli-0.5.1/jott_cli.egg-info → jott_cli-0.5.2}/PKG-INFO +1 -1
  2. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/__init__.py +1 -1
  3. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/app.py +2 -0
  4. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_metadata_mixin.py +115 -5
  5. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_transfer_mixin.py +48 -72
  6. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_metadata_mixin.py +8 -3
  7. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_subtask_mixin.py +1 -1
  8. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/__init__.py +5 -2
  9. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/display_archive.py +5 -4
  10. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/display_footer.py +5 -4
  11. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/display_help.py +11 -6
  12. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/display_projects.py +10 -6
  13. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/display_tasks.py +125 -56
  14. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/styles.py +43 -2
  15. {jott_cli-0.5.1 → jott_cli-0.5.2/jott_cli.egg-info}/PKG-INFO +1 -1
  16. {jott_cli-0.5.1 → jott_cli-0.5.2}/jott_cli.egg-info/SOURCES.txt +1 -0
  17. {jott_cli-0.5.1 → jott_cli-0.5.2}/pyproject.toml +1 -1
  18. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_highlight.py +64 -44
  19. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_jot.py +4 -4
  20. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_styles.py +14 -4
  21. jott_cli-0.5.2/tests/test_subtask_notes.py +545 -0
  22. jott_cli-0.5.2/tests/test_terminal_wrap.py +212 -0
  23. jott_cli-0.5.1/tests/test_subtask_notes.py +0 -246
  24. {jott_cli-0.5.1 → jott_cli-0.5.2}/LICENSE +0 -0
  25. {jott_cli-0.5.1 → jott_cli-0.5.2}/README.md +0 -0
  26. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/_app_navigation_mixin.py +0 -0
  27. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/_dispatch_mixin.py +0 -0
  28. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/categories/__init__.py +0 -0
  29. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/categories/config.py +0 -0
  30. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/categories/manager.py +0 -0
  31. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/categories/templates.py +0 -0
  32. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/cli/__init__.py +0 -0
  33. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/cli/archive.py +0 -0
  34. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/cli/config.py +0 -0
  35. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/cli/views.py +0 -0
  36. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/__init__.py +0 -0
  37. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_ai_analysis_mixin.py +0 -0
  38. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_ai_suggest_mixin.py +0 -0
  39. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_audio_timer_mixin.py +0 -0
  40. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_bulk_mixin.py +0 -0
  41. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_context_mixin.py +0 -0
  42. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_core_mixin.py +0 -0
  43. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_gcal_mixin.py +0 -0
  44. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_notes_mixin.py +0 -0
  45. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/_web_clipboard_mixin.py +0 -0
  46. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/commands/handler.py +0 -0
  47. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/__init__.py +0 -0
  48. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_age_backlog_mixin.py +0 -0
  49. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_compress_mixin.py +0 -0
  50. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_crud_mixin.py +0 -0
  51. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_delete_mixin.py +0 -0
  52. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_export_mixin.py +0 -0
  53. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_id_migration_mixin.py +0 -0
  54. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_navigation_mixin.py +0 -0
  55. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/_persistence_mixin.py +0 -0
  56. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/archive_manager.py +0 -0
  57. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/constants.py +0 -0
  58. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/id_manager.py +0 -0
  59. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/core/task_manager.py +0 -0
  60. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/__init__.py +0 -0
  61. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/gcal/__init__.py +0 -0
  62. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/gcal/account_manager.py +0 -0
  63. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/gcal/auth.py +0 -0
  64. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/gcal/events.py +0 -0
  65. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/keywords/__init__.py +0 -0
  66. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/keywords/_config_mixin.py +0 -0
  67. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/keywords/_handlers_mixin.py +0 -0
  68. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/integrations/keywords/handler.py +0 -0
  69. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/mcp/__init__.py +0 -0
  70. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/mcp/handlers.py +0 -0
  71. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/mcp/schemas.py +0 -0
  72. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/mcp/server.py +0 -0
  73. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/projects/__init__.py +0 -0
  74. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/projects/backup.py +0 -0
  75. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/projects/registry.py +0 -0
  76. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/display.py +0 -0
  77. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/formatting.py +0 -0
  78. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/input.py +0 -0
  79. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/ui/picker.py +0 -0
  80. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/utils/__init__.py +0 -0
  81. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/utils/date_utils.py +0 -0
  82. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/utils/text_utils.py +0 -0
  83. {jott_cli-0.5.1 → jott_cli-0.5.2}/jot/utils/validation.py +0 -0
  84. {jott_cli-0.5.1 → jott_cli-0.5.2}/jott_cli.egg-info/dependency_links.txt +0 -0
  85. {jott_cli-0.5.1 → jott_cli-0.5.2}/jott_cli.egg-info/entry_points.txt +0 -0
  86. {jott_cli-0.5.1 → jott_cli-0.5.2}/jott_cli.egg-info/requires.txt +0 -0
  87. {jott_cli-0.5.1 → jott_cli-0.5.2}/jott_cli.egg-info/top_level.txt +0 -0
  88. {jott_cli-0.5.1 → jott_cli-0.5.2}/setup.cfg +0 -0
  89. {jott_cli-0.5.1 → jott_cli-0.5.2}/setup.py +0 -0
  90. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_command_handler.py +0 -0
  91. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_dispatch.py +0 -0
  92. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_edit_edge_cases.py +0 -0
  93. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_fuzzy_search.py +0 -0
  94. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_gcal_notes.py +0 -0
  95. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_input.py +0 -0
  96. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_picker.py +0 -0
  97. {jott_cli-0.5.1 → jott_cli-0.5.2}/tests/test_today_filter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jott-cli
3
- Version: 0.5.1
3
+ Version: 0.5.2
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>
@@ -7,7 +7,7 @@ import importlib.util
7
7
  import os
8
8
  from pathlib import Path
9
9
 
10
- __version__ = "0.5.1"
10
+ __version__ = "0.5.2"
11
11
 
12
12
  # When building Sphinx docs, skip the dynamic import bridge and heavy deps.
13
13
  # Set JOT_SPHINX_BUILD=1 in docs/sphinx/conf.py before importing jot.
@@ -80,6 +80,8 @@ class App(DispatchMixin, AppNavigationMixin):
80
80
  '#': 'reauthenticate_google_calendar',
81
81
  '\x12': 'register_current_project', # Ctrl+R
82
82
  '*': 'toggle_highlight',
83
+ '~': 'quick_highlight',
84
+ '\x0c': 'assign_parent', # Ctrl+L
83
85
  }
84
86
 
85
87
  def __init__(self, task_manager, command_handler, registry,
@@ -3,10 +3,11 @@
3
3
  from datetime import datetime
4
4
 
5
5
  from jot.core.constants import DAY_COLORS, DAY_ABBREVIATIONS
6
- from jot.ui.picker import PickerItem, quick_picker
6
+ from jot.ui.picker import PickerItem, quick_picker, fuzzy_picker
7
7
  from jot.ui.styles import (
8
8
  RESET, BOLD, DIM, GREEN, RED,
9
9
  PRIORITY_COLORS, PRIORITY_SYMBOLS, STATUS_COLORS, STATUS_SYMBOLS,
10
+ HIGHLIGHT_COLORS,
10
11
  )
11
12
  from jot.ui.input import restore_terminal
12
13
 
@@ -124,14 +125,52 @@ class MetadataMixin:
124
125
  return True
125
126
 
126
127
  def toggle_highlight(self):
127
- """Toggle highlight on the current task."""
128
+ """Pick a highlight color for the current task, or clear it."""
128
129
  current_task = self.task_manager.get_current_task()
129
130
  if not current_task:
130
131
  return True
132
+
133
+ items = [PickerItem(value="_clear", label="none (clear)", color=DIM)]
134
+ for name, fmt in HIGHLIGHT_COLORS.items():
135
+ items.append(PickerItem(
136
+ value=name, label=name, color=fmt,
137
+ ))
138
+
139
+ result = quick_picker(
140
+ items,
141
+ prompt=f"Highlight color for: {current_task['text']}",
142
+ )
143
+
144
+ if result.value is None:
145
+ return True
146
+
147
+ if result.value == "_clear":
148
+ self.task_manager.set_highlight(color=None)
149
+ print(f"\n{GREEN}✓{RESET} Highlight cleared")
150
+ else:
151
+ self.task_manager.set_highlight(color=result.value)
152
+ fmt = HIGHLIGHT_COLORS[result.value]
153
+ print(f"\n{GREEN}✓{RESET} Highlight set to {fmt} {result.value} {RESET}")
154
+
155
+ print("\nPress Enter to continue...", end='', flush=True)
156
+ restore_terminal()
157
+ input()
158
+ return True
159
+
160
+ def quick_highlight(self):
161
+ """Toggle default highlight color on the current task."""
162
+ current_task = self.task_manager.get_current_task()
163
+ if not current_task:
164
+ return True
165
+ from jot.ui.styles import HIGHLIGHT_DEFAULT
131
166
  was_highlighted = current_task.get('highlight', False)
132
- self.task_manager.toggle_highlight()
133
- label = "unhighlighted" if was_highlighted else "highlighted"
134
- print(f"\n{GREEN}✓{RESET} Task {label}")
167
+ if was_highlighted:
168
+ self.task_manager.set_highlight(color=None)
169
+ print(f"\n{GREEN}✓{RESET} Highlight cleared")
170
+ else:
171
+ self.task_manager.set_highlight(color=HIGHLIGHT_DEFAULT)
172
+ fmt = HIGHLIGHT_COLORS[HIGHLIGHT_DEFAULT]
173
+ print(f"\n{GREEN}✓{RESET} Highlighted {fmt} {HIGHLIGHT_DEFAULT} {RESET}")
135
174
  return True
136
175
 
137
176
  def set_priority_high(self):
@@ -176,3 +215,74 @@ class MetadataMixin:
176
215
  input()
177
216
 
178
217
  return True
218
+
219
+ def assign_parent(self):
220
+ """Assign current task as a subtask of another task (Ctrl+L)."""
221
+ current_task = self.task_manager.get_current_task()
222
+ if not current_task:
223
+ print("\n✗ No current task selected")
224
+ return True
225
+
226
+ task_id = current_task['id']
227
+ labels = current_task.get('labels', [])
228
+
229
+ # Find existing parent label
230
+ current_parent = None
231
+ for lbl in labels:
232
+ if lbl.startswith('parent:'):
233
+ current_parent = lbl.split(':', 1)[1]
234
+ break
235
+
236
+ # Build picker items
237
+ items = []
238
+ if current_parent is not None:
239
+ items.append(PickerItem(
240
+ value="_clear", label="Remove parent link", color=DIM,
241
+ ))
242
+
243
+ for task in self.task_manager.get_tasks():
244
+ if task['id'] == task_id:
245
+ continue
246
+ if task.get('status') == 'done':
247
+ continue
248
+ annotation = "(current parent)" if str(task['id']) == str(current_parent) else ""
249
+ items.append(PickerItem(
250
+ value=str(task['id']),
251
+ label=f"#{task['id']} {task['text']}",
252
+ annotation=annotation,
253
+ ))
254
+
255
+ if not items:
256
+ print("\n✗ No candidate tasks found")
257
+ return True
258
+
259
+ result = fuzzy_picker(
260
+ items,
261
+ prompt=f"Assign parent for: {current_task['text']}",
262
+ )
263
+
264
+ if result.value is None:
265
+ return True
266
+
267
+ # Strip existing parent: label
268
+ new_labels = [l for l in labels if not l.startswith('parent:')]
269
+
270
+ if result.value != "_clear":
271
+ new_labels.append(f"parent:{result.value}")
272
+
273
+ # Save updated labels
274
+ for task in self.task_manager.tasks:
275
+ if task['id'] == task_id:
276
+ task['labels'] = new_labels
277
+ self.task_manager._save_tasks()
278
+ break
279
+
280
+ if result.value == "_clear":
281
+ print(f"\n{GREEN}✓{RESET} Parent link removed")
282
+ else:
283
+ print(f"\n{GREEN}✓{RESET} Assigned as subtask of #{result.value}")
284
+
285
+ print("\nPress Enter to continue...", end='', flush=True)
286
+ restore_terminal()
287
+ input()
288
+ return True
@@ -5,9 +5,8 @@ from pathlib import Path
5
5
  from jot.categories.config import CategoryConfig
6
6
  from jot.categories.manager import CategoryManager
7
7
  from jot.core.task_manager import TaskManager
8
- from jot.ui.picker import PickerItem, fuzzy_picker
8
+ from jot.ui.picker import PickerItem, fuzzy_picker, quick_picker
9
9
  from jot.ui.styles import RESET, RED
10
- from jot.ui.input import restore_terminal
11
10
 
12
11
 
13
12
  class TransferMixin:
@@ -211,78 +210,55 @@ class TransferMixin:
211
210
 
212
211
  cat_config = CategoryConfig(project_dir=project_dir)
213
212
 
214
- print(f"\nTransfer task '{task_text}' to:")
215
- for i, (display_name, cat_name, is_global) in enumerate(categories, 1):
213
+ items = []
214
+ for display_name, cat_name, is_global in categories:
216
215
  if display_name == "default":
217
- cat_color = cat_config.get_color('default')
218
- print(f" {i}. {cat_color}default (no category){RESET}")
216
+ items.append(PickerItem(
217
+ value=(None, False),
218
+ label="default",
219
+ annotation="(no category)",
220
+ color=cat_config.get_color('default'),
221
+ ))
219
222
  else:
220
- cat_color = cat_config.get_color(cat_name, for_global=is_global)
221
223
  scope = "[global]" if is_global else "[local]"
222
- print(f" {i}. {cat_color}{cat_name} {scope}{RESET}")
223
-
224
- print("\nEnter number or name (ESC to cancel): ", end='', flush=True)
225
- restore_terminal()
226
-
227
- try:
228
- user_input = input().strip()
229
- if not user_input or user_input == '\x1b':
230
- print("✗ Cancelled")
231
- return True
232
-
233
- selected_category = None
234
- selected_is_global = False
235
-
236
- try:
237
- choice_index = int(user_input) - 1
238
- if 0 <= choice_index < len(categories):
239
- _, cat_name, is_global = categories[choice_index]
240
- selected_category = cat_name
241
- selected_is_global = is_global
242
- else:
243
- print("✗ Invalid number")
244
- return True
245
- except ValueError:
246
- found = False
247
- for display_name, cat_name, is_global in categories:
248
- if cat_name == user_input or display_name == user_input:
249
- selected_category = cat_name
250
- selected_is_global = is_global
251
- found = True
252
- break
253
- if not found:
254
- print(f"✗ Category '{user_input}' not found")
255
- return True
256
-
257
- if selected_is_global:
258
- dest_tm = TaskManager(
259
- category=selected_category, is_global=True, project_registry=self.registry
260
- )
261
- else:
262
- dest_tm = TaskManager(
263
- directory=project_dir,
264
- category=selected_category,
265
- is_global=False,
266
- project_registry=self.registry,
267
- )
268
-
269
- dest_tm.add_task(task_text)
270
- if task_done:
271
- new_task_id = dest_tm.tasks[-1]['id']
272
- dest_tm.set_task_status(new_task_id, 'done')
273
-
274
- self.task_manager.remove_task(task_id)
275
-
276
- dest_name = selected_category or 'default'
277
- if selected_is_global:
278
- print(f"✓ Transferred task to global:{dest_name}")
279
- else:
280
- print(f"✓ Transferred task to {dest_name}")
281
-
282
- print("\nPress Enter to continue...", end='', flush=True)
283
- input()
224
+ items.append(PickerItem(
225
+ value=(cat_name, is_global),
226
+ label=cat_name,
227
+ annotation=scope,
228
+ color=cat_config.get_color(cat_name, for_global=is_global),
229
+ ))
230
+
231
+ result = quick_picker(items, prompt=f"Transfer '{task_text}' to")
232
+ if result.value is None:
284
233
  return True
285
234
 
286
- except KeyboardInterrupt:
287
- print("\n✗ Cancelled")
288
- return True
235
+ selected_category, selected_is_global = result.value
236
+
237
+ if selected_is_global:
238
+ dest_tm = TaskManager(
239
+ category=selected_category, is_global=True, project_registry=self.registry
240
+ )
241
+ else:
242
+ dest_tm = TaskManager(
243
+ directory=project_dir,
244
+ category=selected_category,
245
+ is_global=False,
246
+ project_registry=self.registry,
247
+ )
248
+
249
+ dest_tm.add_task(task_text)
250
+ if task_done:
251
+ new_task_id = dest_tm.tasks[-1]['id']
252
+ dest_tm.set_task_status(new_task_id, 'done')
253
+
254
+ self.task_manager.remove_task(task_id)
255
+
256
+ dest_name = selected_category or 'default'
257
+ if selected_is_global:
258
+ print(f"✓ Transferred task to global:{dest_name}")
259
+ else:
260
+ print(f"✓ Transferred task to {dest_name}")
261
+
262
+ print("\nPress Enter to continue...", end='', flush=True)
263
+ input()
264
+ return True
@@ -61,8 +61,13 @@ class MetadataMixin:
61
61
  self._save_tasks()
62
62
  return True
63
63
 
64
- def toggle_highlight(self, task_id=None):
65
- """Toggle highlight on a task. Multiple tasks can be highlighted."""
64
+ def set_highlight(self, task_id=None, color=None):
65
+ """Set highlight color on a task, or clear it.
66
+
67
+ Args:
68
+ task_id: Task to highlight (defaults to current task).
69
+ color: Color name from HIGHLIGHT_COLORS, or None to clear.
70
+ """
66
71
  if task_id is None:
67
72
  current = self.get_current_task()
68
73
  if not current:
@@ -71,7 +76,7 @@ class MetadataMixin:
71
76
 
72
77
  for task in self.tasks:
73
78
  if task['id'] == task_id:
74
- task['highlight'] = not task.get('highlight', False)
79
+ task['highlight'] = color if color else False
75
80
  self._save_tasks()
76
81
  return True
77
82
  return False
@@ -101,7 +101,7 @@ class SubtaskMixin:
101
101
  sub['updated_at'] = now
102
102
  if not status_changed:
103
103
  updated += 1
104
- else:
104
+ elif not done:
105
105
  new_id = self.add_task(
106
106
  text,
107
107
  status='done' if done else 'todo',
@@ -16,7 +16,9 @@ from jot.ui.styles import (
16
16
  RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, GRAY, ORANGE, BLACK,
17
17
  PRIORITY_COLORS, PRIORITY_SYMBOLS, STATUS_COLORS, STATUS_SYMBOLS,
18
18
  ARCHIVE_SYMBOL, ARCHIVE_COLOR,
19
- BG_YELLOW, HIGHLIGHT_FMT,
19
+ BG_YELLOW, BG_ORANGE, BG_MAGENTA, BG_GRAY,
20
+ HIGHLIGHT_COLORS, HIGHLIGHT_DEFAULT, HIGHLIGHT_FMT,
21
+ get_terminal_width, strip_ansi, visible_len,
20
22
  )
21
23
 
22
24
  __all__ = [
@@ -34,7 +36,8 @@ __all__ = [
34
36
  'RED', 'GREEN', 'YELLOW', 'BLUE', 'MAGENTA', 'CYAN', 'WHITE', 'GRAY', 'ORANGE', 'BLACK',
35
37
  'PRIORITY_COLORS', 'PRIORITY_SYMBOLS', 'STATUS_COLORS', 'STATUS_SYMBOLS',
36
38
  'ARCHIVE_SYMBOL', 'ARCHIVE_COLOR',
37
- 'BG_YELLOW', 'HIGHLIGHT_FMT',
39
+ 'BG_YELLOW', 'HIGHLIGHT_COLORS', 'HIGHLIGHT_DEFAULT', 'HIGHLIGHT_FMT',
40
+ 'get_terminal_width', 'strip_ansi', 'visible_len',
38
41
  # Picker
39
42
  'PickerItem', 'PickerResult', 'fuzzy_picker', 'quick_picker',
40
43
  ]
@@ -5,6 +5,7 @@ from jot.categories.manager import CategoryManager
5
5
  from jot.categories.config import CategoryConfig
6
6
  from jot.ui.styles import (
7
7
  RESET, BOLD, DIM, CYAN, YELLOW,
8
+ get_terminal_width,
8
9
  )
9
10
 
10
11
 
@@ -15,7 +16,7 @@ def display_archive(project_dir, filter_category=None):
15
16
  project_dir, cat_manager, filter_category)
16
17
 
17
18
  print(f"\n{BOLD}Archived Tasks{RESET}")
18
- print("=" * 60)
19
+ print("=" * get_terminal_width())
19
20
 
20
21
  total_archived = 0
21
22
  for cat_name, tm in managers_to_check:
@@ -32,7 +33,7 @@ def display_archive(project_dir, filter_category=None):
32
33
 
33
34
  if total_archived == 0:
34
35
  print(f"\n{DIM}No archived tasks{RESET}")
35
- print("=" * 60 + "\n")
36
+ print("=" * get_terminal_width() + "\n")
36
37
 
37
38
 
38
39
  def _build_archive_managers(project_dir, cat_manager, filter_category):
@@ -74,7 +75,7 @@ def display_all_categories_view(
74
75
  show_archived: Whether to include archived tasks in the view.
75
76
  """
76
77
  print(f"\n{CYAN}{BOLD}ALL CATEGORIES VIEW{RESET}")
77
- print("-" * 50)
78
+ print("-" * get_terminal_width())
78
79
 
79
80
  cat_config = CategoryConfig(project_dir=project_dir)
80
81
  task_num = 1
@@ -110,7 +111,7 @@ def display_all_categories_view(
110
111
  f"\n{DIM}Total: {total_tasks} tasks across "
111
112
  f"{len(all_cat_data)} categories{RESET}"
112
113
  )
113
- print("-" * 50)
114
+ print("-" * get_terminal_width())
114
115
  print(
115
116
  f"{CYAN}Press Shift+L or ESC to return to "
116
117
  f"single category view{RESET}"
@@ -6,6 +6,7 @@ from jot.core.constants import (
6
6
  )
7
7
  from jot.ui.styles import (
8
8
  RESET, BOLD, DIM, CYAN, GREEN, YELLOW,
9
+ get_terminal_width,
9
10
  )
10
11
 
11
12
 
@@ -53,7 +54,7 @@ def _render_multiselect(selected_tasks):
53
54
  f"{BOLD}Shift+↑/↓{RESET}: select while moving | "
54
55
  f"{BOLD}A{RESET}: select all | {BOLD}N{RESET}: select none"
55
56
  )
56
- print("-" * 50)
57
+ print("-" * get_terminal_width())
57
58
 
58
59
 
59
60
  def _render_fuzzy_search(
@@ -88,7 +89,7 @@ def _render_fuzzy_search(
88
89
  f"{BOLD}Ctrl+A{RESET}: toggle archives | "
89
90
  f"{BOLD}ESC{RESET}: cancel"
90
91
  )
91
- print("-" * 50)
92
+ print("-" * get_terminal_width())
92
93
 
93
94
 
94
95
  def _render_command():
@@ -104,7 +105,7 @@ def _render_command():
104
105
  f"{BOLD}(M){RESET}ove | {BOLD}(r){RESET}efresh | "
105
106
  f"{BOLD}(h){RESET}elp | {BOLD}(q){RESET}uit"
106
107
  )
107
- print("-" * 50)
108
+ print("-" * get_terminal_width())
108
109
  print("Command: ", end='', flush=True)
109
110
 
110
111
 
@@ -130,4 +131,4 @@ def render_archived_section(archived_tasks):
130
131
  f"{DIM} [{status}] {task['id']}. "
131
132
  f"{day_prefix}{task['text']}{tally_suffix}{RESET}"
132
133
  )
133
- print(f"{DIM}-" * 50 + RESET)
134
+ print(f"{DIM}" + "-" * get_terminal_width() + RESET)
@@ -1,6 +1,6 @@
1
1
  """Display functions for help and keyboard shortcuts."""
2
2
 
3
- from jot.ui.styles import RESET, BOLD, DIM, CYAN, GREEN, YELLOW
3
+ from jot.ui.styles import RESET, BOLD, DIM, CYAN, GREEN, YELLOW, get_terminal_width
4
4
 
5
5
 
6
6
  def display_categorized_shortcuts():
@@ -42,7 +42,9 @@ def display_categorized_shortcuts():
42
42
  ("Shift+Y", "Toggle today filter"),
43
43
  ("Shift+D", "Mark current as done"),
44
44
  ("Shift+J", "Mark as agent task"),
45
- ("*", "Toggle highlight (yellow)"),
45
+ ("*", "Highlight color picker"),
46
+ ("~", "Quick highlight toggle"),
47
+ ("Ctrl+L", "Assign parent (link subtask)"),
46
48
  ],
47
49
  "Multi-Select Mode": [
48
50
  ("Shift+V", "Enter multi-select mode"),
@@ -79,22 +81,23 @@ def display_categorized_shortcuts():
79
81
  ],
80
82
  }
81
83
 
84
+ w = get_terminal_width()
82
85
  print(f"\n{BOLD}Keyboard Shortcuts{RESET}")
83
- print("=" * 60)
86
+ print("=" * w)
84
87
 
85
88
  for category, items in shortcuts.items():
86
89
  print(f"\n{CYAN}{category}:{RESET}")
87
90
  for key, description in items:
88
91
  print(f" {YELLOW}{key:15}{RESET} {description}")
89
92
 
90
- print("\n" + "=" * 60 + "\n")
93
+ print("\n" + "=" * w + "\n")
91
94
 
92
95
 
93
96
  def display_help():
94
97
  """Display comprehensive help information."""
95
98
  help_text = f"""
96
99
  {BOLD}Jott - Simple Interactive Task List{RESET}
97
- {DIM}Version 0.5.0{RESET}
100
+ {DIM}Version 0.5.2{RESET}
98
101
 
99
102
  {BOLD}BASIC USAGE:{RESET}
100
103
  jott Start interactive task manager (current project)
@@ -146,7 +149,9 @@ def display_help():
146
149
  {CYAN}Shift+Y{RESET} Toggle today filter
147
150
  {CYAN}Shift+D{RESET} Mark current task as done
148
151
  {CYAN}Shift+J{RESET} Mark task for agent/AI assistance
149
- {CYAN}*{RESET} Toggle highlight (yellow background)
152
+ {CYAN}*{RESET} Highlight color picker (6 colors)
153
+ {CYAN}~{RESET} Quick highlight toggle (default color)
154
+ {CYAN}Ctrl+L{RESET} Assign parent task (subtask link)
150
155
  {CYAN}Shift+E{RESET} Execute keyword action for task
151
156
 
152
157
  {BOLD}MULTI-SELECT MODE:{RESET}
@@ -4,6 +4,7 @@ from jot.core.task_manager import TaskManager
4
4
  from jot.categories.manager import CategoryManager
5
5
  from jot.ui.styles import (
6
6
  RESET, BOLD, DIM, CYAN, GREEN, YELLOW,
7
+ get_terminal_width,
7
8
  )
8
9
 
9
10
 
@@ -16,12 +17,13 @@ def display_all_projects(registry, show_categories=False):
16
17
  "projects in ~/projects/")
17
18
  return
18
19
 
19
- print("\n" + "=" * 60)
20
+ w = get_terminal_width()
21
+ print("\n" + "=" * w)
20
22
  if show_categories:
21
23
  print(f"{BOLD}ALL PROJECTS OVERVIEW (with categories){RESET}")
22
24
  else:
23
25
  print(f"{BOLD}ALL PROJECTS OVERVIEW{RESET}")
24
- print("=" * 60)
26
+ print("=" * w)
25
27
 
26
28
  for project_name in sorted(projects.keys()):
27
29
  project_path = projects[project_name]
@@ -40,13 +42,14 @@ def display_all_projects(registry, show_categories=False):
40
42
  f"{DIM}(error loading: {e}){RESET}")
41
43
  print(f" {DIM}{project_path}{RESET}")
42
44
 
43
- print("\n" + "=" * 60)
45
+ w = get_terminal_width()
46
+ print("\n" + "=" * w)
44
47
  print(
45
48
  f"Total: {len(projects)} projects | "
46
49
  f"Use {CYAN}jot -p <project>{RESET} to switch | "
47
50
  f"{CYAN}Shift+P{RESET} in jot UI"
48
51
  )
49
- print("=" * 60 + "\n")
52
+ print("=" * w + "\n")
50
53
 
51
54
 
52
55
  def _display_project_with_categories(project_name, project_path):
@@ -116,8 +119,9 @@ def display_category_stats(project_dir):
116
119
  local_categories = cat_manager.discover_categories()
117
120
  global_categories = CategoryManager.discover_global_categories()
118
121
 
122
+ w = get_terminal_width()
119
123
  print(f"\n{BOLD}Category Statistics{RESET}")
120
- print("=" * 60)
124
+ print("=" * w)
121
125
 
122
126
  tm_default = TaskManager(directory=project_dir)
123
127
  print(f"\n{CYAN}DEFAULT (uncategorized){RESET}")
@@ -148,4 +152,4 @@ def display_category_stats(project_dir):
148
152
  f"{YELLOW}⚠ Global categories: "
149
153
  f"{len(global_categories)}/{CategoryManager.MAX_CATEGORIES}{RESET}"
150
154
  )
151
- print("=" * 60 + "\n")
155
+ print("=" * w + "\n")