jott-cli 0.7.0__tar.gz → 0.8.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 (108) hide show
  1. {jott_cli-0.7.0/jott_cli.egg-info → jott_cli-0.8.1}/PKG-INFO +1 -1
  2. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/__init__.py +1 -1
  3. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/_dispatch_mixin.py +2 -1
  4. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/app.py +6 -1
  5. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_claude_mixin.py +10 -0
  6. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_core_mixin.py +3 -15
  7. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_metadata_mixin.py +21 -10
  8. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_notes_mixin.py +6 -1
  9. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_delete_mixin.py +51 -2
  10. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_metadata_mixin.py +15 -0
  11. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_footer.py +0 -3
  12. jott_cli-0.8.1/jot/ui/display_help.py +282 -0
  13. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/styles.py +43 -4
  14. {jott_cli-0.7.0 → jott_cli-0.8.1/jott_cli.egg-info}/PKG-INFO +1 -1
  15. {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/SOURCES.txt +4 -0
  16. {jott_cli-0.7.0 → jott_cli-0.8.1}/pyproject.toml +1 -1
  17. jott_cli-0.8.1/tests/test_claude_org_prompt.py +90 -0
  18. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_command_handler.py +5 -3
  19. jott_cli-0.8.1/tests/test_deleted_notes_dir.py +134 -0
  20. jott_cli-0.8.1/tests/test_notes_tempfile_location.py +73 -0
  21. jott_cli-0.8.1/tests/test_state_palette.py +139 -0
  22. jott_cli-0.7.0/jot/ui/display_help.py +0 -233
  23. {jott_cli-0.7.0 → jott_cli-0.8.1}/LICENSE +0 -0
  24. {jott_cli-0.7.0 → jott_cli-0.8.1}/README.md +0 -0
  25. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/_app_navigation_mixin.py +0 -0
  26. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/__init__.py +0 -0
  27. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/config.py +0 -0
  28. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/manager.py +0 -0
  29. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/templates.py +0 -0
  30. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/__init__.py +0 -0
  31. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/archive.py +0 -0
  32. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/config.py +0 -0
  33. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/views.py +0 -0
  34. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/__init__.py +0 -0
  35. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_ai_analysis_mixin.py +0 -0
  36. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_ai_suggest_mixin.py +0 -0
  37. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_audio_timer_mixin.py +0 -0
  38. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_bulk_mixin.py +0 -0
  39. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_context_mixin.py +0 -0
  40. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_gcal_mixin.py +0 -0
  41. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_github_mixin.py +0 -0
  42. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_transfer_mixin.py +0 -0
  43. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_web_clipboard_mixin.py +0 -0
  44. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/handler.py +0 -0
  45. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/__init__.py +0 -0
  46. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_age_backlog_mixin.py +0 -0
  47. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_compress_mixin.py +0 -0
  48. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_crud_mixin.py +0 -0
  49. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_export_mixin.py +0 -0
  50. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_id_migration_mixin.py +0 -0
  51. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_navigation_mixin.py +0 -0
  52. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_persistence_mixin.py +0 -0
  53. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_subtask_mixin.py +0 -0
  54. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/archive_manager.py +0 -0
  55. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/constants.py +0 -0
  56. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/id_manager.py +0 -0
  57. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/task_manager.py +0 -0
  58. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/__init__.py +0 -0
  59. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/__init__.py +0 -0
  60. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/account_manager.py +0 -0
  61. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/auth.py +0 -0
  62. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/events.py +0 -0
  63. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/github/__init__.py +0 -0
  64. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/github/issues.py +0 -0
  65. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/__init__.py +0 -0
  66. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/_config_mixin.py +0 -0
  67. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/_handlers_mixin.py +0 -0
  68. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/handler.py +0 -0
  69. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/__init__.py +0 -0
  70. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/handlers.py +0 -0
  71. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/schemas.py +0 -0
  72. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/server.py +0 -0
  73. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/projects/__init__.py +0 -0
  74. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/projects/backup.py +0 -0
  75. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/projects/registry.py +0 -0
  76. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/__init__.py +0 -0
  77. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display.py +0 -0
  78. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_archive.py +0 -0
  79. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_projects.py +0 -0
  80. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_tasks.py +0 -0
  81. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/formatting.py +0 -0
  82. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/input.py +0 -0
  83. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/picker.py +0 -0
  84. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/rendering.py +0 -0
  85. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/__init__.py +0 -0
  86. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/date_utils.py +0 -0
  87. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/text_utils.py +0 -0
  88. {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/validation.py +0 -0
  89. {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/dependency_links.txt +0 -0
  90. {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/entry_points.txt +0 -0
  91. {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/requires.txt +0 -0
  92. {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/top_level.txt +0 -0
  93. {jott_cli-0.7.0 → jott_cli-0.8.1}/setup.cfg +0 -0
  94. {jott_cli-0.7.0 → jott_cli-0.8.1}/setup.py +0 -0
  95. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_dispatch.py +0 -0
  96. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_edit_edge_cases.py +0 -0
  97. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_fuzzy_search.py +0 -0
  98. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_gcal_notes.py +0 -0
  99. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_github.py +0 -0
  100. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_highlight.py +0 -0
  101. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_input.py +0 -0
  102. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_jot.py +0 -0
  103. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_picker.py +0 -0
  104. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_styles.py +0 -0
  105. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_subtask_notes.py +0 -0
  106. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_terminal_wrap.py +0 -0
  107. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_today_filter.py +0 -0
  108. {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_transfer_subtasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jott-cli
3
- Version: 0.7.0
3
+ Version: 0.8.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>
@@ -7,7 +7,7 @@ import importlib.util
7
7
  import os
8
8
  from pathlib import Path
9
9
 
10
- __version__ = "0.7.0"
10
+ __version__ = "0.8.1"
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.
@@ -285,7 +285,8 @@ class DispatchMixin:
285
285
  st.input_buffer = ""
286
286
  return
287
287
  if second == '?':
288
- st.show_shortcuts = not st.show_shortcuts
288
+ from jot.ui.display_help import help_modal
289
+ help_modal()
289
290
  st.input_buffer = ""
290
291
  return
291
292
  if second == 'i':
@@ -37,7 +37,7 @@ class AppState:
37
37
  sort_by_day: bool = False
38
38
  show_today_only: bool = False
39
39
  include_archived_in_search: bool = True
40
- show_shortcuts: bool = False
40
+ show_shortcuts: bool = False # deprecated; kept for API compat
41
41
  show_notes_inline: bool = False
42
42
  running: bool = True
43
43
  selected_tasks: set = field(default_factory=set)
@@ -84,6 +84,11 @@ class App(DispatchMixin, AppNavigationMixin):
84
84
  os.path.getmtime(task_manager.storage_file)
85
85
  if task_manager.storage_file.exists() else 0)
86
86
 
87
+ try:
88
+ task_manager._migrate_legacy_deleted_notes()
89
+ except Exception as e:
90
+ print(f"Warning: deleted-notes migration failed: {e}")
91
+
87
92
  # ------------------------------------------------------------------
88
93
  # Public entry point
89
94
  # ------------------------------------------------------------------
@@ -8,6 +8,15 @@ from datetime import datetime
8
8
 
9
9
  from jot.ui.styles import RESET, CYAN, DIM
10
10
 
11
+ _ORG_OUTPUT_INSTRUCTION = (
12
+ "\n\nIMPORTANT: Format your entire response as org-mode markup:\n"
13
+ "- Code blocks: #+begin_src <lang>\n...\n#+end_src\n"
14
+ "- Inline code: ~symbol~ or =literal=\n"
15
+ "- File references: [[relative/path/to/file][filename.ext]]\n"
16
+ "- Headings: * ** *** etc.\n"
17
+ "Do NOT use markdown backticks or markdown headings."
18
+ )
19
+
11
20
 
12
21
  class ClaudeMixin:
13
22
  """Launch Claude Code in a new tmux window with current task."""
@@ -209,6 +218,7 @@ class ClaudeMixin:
209
218
  f"Existing notes:\n{existing_notes}")
210
219
  else:
211
220
  prompt = task_text
221
+ prompt += _ORG_OUTPUT_INSTRUCTION
212
222
 
213
223
  cmd = [
214
224
  'claude', '-p', '--model', 'sonnet',
@@ -277,21 +277,9 @@ class CoreMixin:
277
277
 
278
278
  @staticmethod
279
279
  def help():
280
- """Show help message"""
281
- print("""
282
- Commands: a Add c Current d Delete D Delete-current e Edit E Edit-current
283
- M Move r Refresh h Help q Quit
284
-
285
- Navigation: ↑/Ctrl+p Up ↓/Ctrl+n Down Shift+↑ Move up Shift+↓ Move down
286
-
287
- Quick-Add: .? Shortcuts .f Notes .n Edit notes .j Agent task
288
- .z Project .l Categories .w Day .x Status
289
- Ctrl+R Register Ctrl+T Transfer .m Move Ctrl+X Quit
290
-
291
- Command Mode (ESC): r Refresh
292
-
293
- Tip: Press '.' to start a leader chord. Press '.?' for full shortcuts.
294
- Full CLI docs: jott --help""")
280
+ """Show help modal with all keyboard shortcuts."""
281
+ from jot.ui.display_help import help_modal
282
+ help_modal()
295
283
  return True
296
284
 
297
285
  def toggle_caps(self):
@@ -7,7 +7,7 @@ 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
+ HIGHLIGHT_COLORS, STATE_ORDER, STATE_COLORS,
11
11
  )
12
12
  from jot.ui.input import restore_terminal
13
13
 
@@ -125,20 +125,29 @@ class MetadataMixin:
125
125
  return True
126
126
 
127
127
  def toggle_highlight(self):
128
- """Pick a highlight color for the current task, or clear it."""
128
+ """Pick a task state (color-coded) for the current task."""
129
129
  current_task = self.task_manager.get_current_task()
130
130
  if not current_task:
131
131
  return True
132
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
- ))
133
+ items = []
134
+ for state in STATE_ORDER:
135
+ color_key = STATE_COLORS[state]
136
+ if color_key is None:
137
+ items.append(PickerItem(
138
+ value="_clear", label="none", color=DIM,
139
+ annotation=state,
140
+ ))
141
+ else:
142
+ items.append(PickerItem(
143
+ value=color_key, label=color_key,
144
+ color=HIGHLIGHT_COLORS[color_key],
145
+ annotation=state,
146
+ ))
138
147
 
139
148
  result = quick_picker(
140
149
  items,
141
- prompt=f"Highlight color for: {current_task['text']}",
150
+ prompt=f"State for: {current_task['text']}",
142
151
  )
143
152
 
144
153
  if result.value is None:
@@ -146,11 +155,13 @@ class MetadataMixin:
146
155
 
147
156
  if result.value == "_clear":
148
157
  self.task_manager.set_highlight(color=None)
149
- print(f"\n{GREEN}✓{RESET} Highlight cleared")
158
+ print(f"\n{GREEN}✓{RESET} State cleared (backlog)")
150
159
  else:
151
160
  self.task_manager.set_highlight(color=result.value)
152
161
  fmt = HIGHLIGHT_COLORS[result.value]
153
- print(f"\n{GREEN}✓{RESET} Highlight set to {fmt} {result.value} {RESET}")
162
+ from jot.ui.styles import COLOR_TO_STATE
163
+ state_name = COLOR_TO_STATE.get(result.value, result.value)
164
+ print(f"\n{GREEN}✓{RESET} State: {fmt} {state_name} {RESET}")
154
165
 
155
166
  print("\nPress Enter to continue...", end='', flush=True)
156
167
  restore_terminal()
@@ -4,6 +4,7 @@ import os
4
4
  import shlex
5
5
  import subprocess
6
6
  import tempfile
7
+ from pathlib import Path
7
8
 
8
9
  from jot.ui.styles import RESET, DIM, CYAN, YELLOW
9
10
 
@@ -77,7 +78,11 @@ class NotesMixin:
77
78
  file_extension = self.get_notes_file_extension(editor)
78
79
  is_org_mode = file_extension == '.org'
79
80
 
80
- with tempfile.NamedTemporaryFile(mode='w+', suffix=file_extension, delete=False) as tf:
81
+ work_dir = getattr(self.task_manager, 'project_dir', None) or Path.cwd()
82
+ with tempfile.NamedTemporaryFile(
83
+ mode='w+', suffix=file_extension, delete=False,
84
+ dir=work_dir
85
+ ) as tf:
81
86
  temp_file = tf.name
82
87
  if is_org_mode:
83
88
  tf.write(f"#+TITLE: Notes for task: {task_text}\n")
@@ -1,12 +1,55 @@
1
- """Delete mixin archive, permanent delete, notes export."""
1
+ """Delete mixin, archive, permanent delete, notes export."""
2
2
 
3
3
  import os
4
+ import re
4
5
  from datetime import datetime
5
6
 
6
7
 
8
+ DELETED_NOTES_DIR = ".jot-deleted-notes"
9
+ _DELETED_NOTES_FILENAME_RE = re.compile(
10
+ r"^TASK-\d+-notes-\d{8}_\d{6}\.org$"
11
+ )
12
+
13
+
7
14
  class DeleteMixin:
8
15
  """Task removal, archival, and notes preservation."""
9
16
 
17
+ def _migrate_legacy_deleted_notes(self):
18
+ """Move stray TASK-<id>-notes-<ts>.org files from project_dir
19
+ into the DELETED_NOTES_DIR subdirectory. Idempotent: a no-op
20
+ when no matching files exist at the project root.
21
+ """
22
+ try:
23
+ entries = list(self.project_dir.iterdir())
24
+ except (OSError, AttributeError):
25
+ return 0
26
+
27
+ matches = [
28
+ p for p in entries
29
+ if p.is_file() and _DELETED_NOTES_FILENAME_RE.match(p.name)
30
+ ]
31
+ if not matches:
32
+ return 0
33
+
34
+ dest_dir = self.project_dir / DELETED_NOTES_DIR
35
+ try:
36
+ dest_dir.mkdir(exist_ok=True)
37
+ except OSError as e:
38
+ print(f"Warning: could not create {dest_dir}: {e}")
39
+ return 0
40
+
41
+ moved = 0
42
+ for src in matches:
43
+ dest = dest_dir / src.name
44
+ if dest.exists():
45
+ continue
46
+ try:
47
+ src.rename(dest)
48
+ moved += 1
49
+ except OSError as e:
50
+ print(f"Warning: could not move {src.name}: {e}")
51
+ return moved
52
+
10
53
  def _unlink_children(self, task_id):
11
54
  """Remove parent:{task_id} labels from all children."""
12
55
  parent_label = f"parent:{task_id}"
@@ -131,7 +174,13 @@ class DeleteMixin:
131
174
  )
132
175
 
133
176
  filename = f"TASK-{task_id}-notes-{now.strftime('%Y%m%d_%H%M%S')}.org"
134
- org_file_path = self.project_dir / filename
177
+ dest_dir = self.project_dir / DELETED_NOTES_DIR
178
+ try:
179
+ dest_dir.mkdir(exist_ok=True)
180
+ except OSError as e:
181
+ print(f"Warning: Could not create {dest_dir}: {e}")
182
+ return None
183
+ org_file_path = dest_dir / filename
135
184
 
136
185
  try:
137
186
  with open(org_file_path, 'w') as f:
@@ -3,6 +3,18 @@
3
3
  from datetime import datetime, timezone
4
4
 
5
5
 
6
+ # One-way mapping from legacy status values to highlight color keys.
7
+ # Setting status auto-paints the highlight; the reverse is NOT applied.
8
+ # Status is being deprecated in favor of the richer state palette;
9
+ # this exists so the legacy `* status` picker still does something useful.
10
+ _STATUS_TO_HIGHLIGHT = {
11
+ 'todo': 'red',
12
+ 'in-progress': 'orange',
13
+ 'blocked': 'burnt',
14
+ 'done': 'yellow',
15
+ }
16
+
17
+
6
18
  class MetadataMixin:
7
19
  """Set task metadata: current flag, agent, day, priority, status, tally."""
8
20
 
@@ -118,6 +130,9 @@ class MetadataMixin:
118
130
  task['done'] = True
119
131
  elif task.get('done', False):
120
132
  task['done'] = False
133
+ color = _STATUS_TO_HIGHLIGHT.get(status)
134
+ if color:
135
+ task['highlight'] = color
121
136
  self._save_tasks()
122
137
  return True
123
138
  return False
@@ -35,9 +35,6 @@ def _render_quick_add(input_buffer, show_shortcuts):
35
35
  f"commands | {BOLD}Shift+V{RESET}: multi-select | "
36
36
  f"{BOLD}.?{RESET}: shortcuts"
37
37
  )
38
- if show_shortcuts:
39
- from jot.ui.display_help import display_categorized_shortcuts
40
- display_categorized_shortcuts()
41
38
  print(f"→ {input_buffer}█", end='')
42
39
 
43
40
 
@@ -0,0 +1,282 @@
1
+ """Display functions for help and keyboard shortcuts."""
2
+
3
+ import shutil
4
+ import sys
5
+
6
+ from jot.ui.input import get_key
7
+ from jot.ui.rendering import buffered_output
8
+ from jot.ui.styles import RESET, BOLD, DIM, CYAN, GREEN, YELLOW, get_terminal_width
9
+
10
+
11
+ # ── Shortcut data ──────────────────────────────────────────────────────
12
+
13
+ SHORTCUTS = {
14
+ "Navigation": [
15
+ ("↑/↓", "Navigate tasks"),
16
+ ("Tab", "Cycle through categories"),
17
+ ("Ctrl+F", "Fuzzy search tasks"),
18
+ (".z", "Switch project"),
19
+ ("Ctrl+C", "Switch category"),
20
+ (".l", "Toggle all categories view"),
21
+ ],
22
+ "Task Management": [
23
+ ("Enter", "Add task"),
24
+ ("Ctrl+E", "Edit current task"),
25
+ ("c", "Mark current task"),
26
+ ("d", "Delete task"),
27
+ (".d", "Duplicate task"),
28
+ ("t", "Toggle task done"),
29
+ ("a", "Archive task"),
30
+ (".a", "Toggle archive view"),
31
+ ("m", "Move to different category"),
32
+ (".m", "Move task to another project"),
33
+ (".k", "Copy task to another project"),
34
+ ("Ctrl+T", "Transfer to category"),
35
+ ("Ctrl+↑/↓", "Reorder tasks"),
36
+ ("=", "Sort by priority"),
37
+ ],
38
+ "Task Details": [
39
+ ("n", "Add/edit notes"),
40
+ (".n", "Edit task notes"),
41
+ (".f", "Toggle inline notes display"),
42
+ ("p", "Toggle priority cycle"),
43
+ (".h", "Set priority to high"),
44
+ (".x", "Set task status"),
45
+ (".w", "Assign day to task"),
46
+ (".o", "Toggle day sorting"),
47
+ (".y", "Toggle today filter"),
48
+ (".u", "Toggle ALL CAPS"),
49
+ ("Ctrl+D", "Mark current as done"),
50
+ (".j", "Mark as agent task"),
51
+ ("*", "Highlight color picker"),
52
+ ("~", "Quick highlight toggle"),
53
+ ("Ctrl+L", "Assign parent (link subtask)"),
54
+ ],
55
+ "Claude Code": [
56
+ (".A", "Ask Claude (read-only, writes to notes)"),
57
+ (".X", "Execute Claude (edit permissions)"),
58
+ (".C", "Launch Claude Code with task (tmux)"),
59
+ (".v", "Send task to local Claude session"),
60
+ (".V", "Send task to any Claude session"),
61
+ ],
62
+ "Export & Import": [
63
+ (".e", "Execute keyword action"),
64
+ (".b", "Priority timer"),
65
+ (".G", "Export to Google Calendar"),
66
+ (".i", "Import from Google Calendar"),
67
+ (".c", "Copy task to clipboard"),
68
+ ("Ctrl+U", "Open URLs in task"),
69
+ ("Shift+1", "Fix duplicate task IDs"),
70
+ ("Shift+4", "AI task suggestion"),
71
+ ],
72
+ "GitHub": [
73
+ (".I", "Import issues from GitHub"),
74
+ (".H", "Export task as GitHub issue"),
75
+ (".D", "Close linked GitHub issue"),
76
+ ],
77
+ "System": [
78
+ (".?", "Toggle this help"),
79
+ ("r", "Refresh display"),
80
+ ("Ctrl+R", "Register current project"),
81
+ ("q/Ctrl+X", "Quit"),
82
+ ("Esc", "Exit mode / Quit"),
83
+ ],
84
+ }
85
+
86
+
87
+ def _get_terminal_height():
88
+ """Return current terminal height, defaulting to 24."""
89
+ try:
90
+ return shutil.get_terminal_size().lines
91
+ except (ValueError, OSError):
92
+ return 24
93
+
94
+
95
+ # ── Build lines for the modal ──────────────────────────────────────────
96
+
97
+ def _build_help_lines():
98
+ """Build a list of (line_string, is_header) tuples for the modal body."""
99
+ lines = []
100
+ first = True
101
+ for category, items in SHORTCUTS.items():
102
+ if not first:
103
+ lines.append(("", False))
104
+ first = False
105
+ lines.append((category, True))
106
+ for key, desc in items:
107
+ lines.append((f" {YELLOW}{key:<12}{RESET} {desc}", False))
108
+ return lines
109
+
110
+
111
+ # ── Modal rendering ────────────────────────────────────────────────────
112
+
113
+ def help_modal():
114
+ """Show a centered, scrollable help modal. Blocks until dismissed.
115
+
116
+ Controls:
117
+ ↑/↓ or j/k Scroll one line
118
+ PgUp/PgDn Scroll one page
119
+ Home/End Jump to top/bottom
120
+ ESC/q/? Dismiss
121
+ """
122
+ lines = _build_help_lines()
123
+ scroll = 0
124
+
125
+ while True:
126
+ term_w = get_terminal_width()
127
+ term_h = _get_terminal_height()
128
+
129
+ # Box dimensions: 70% width, 85% height, centered
130
+ box_w = max(40, min(int(term_w * 0.70), term_w - 4))
131
+ box_h = max(10, min(int(term_h * 0.85), term_h - 2))
132
+ inner_w = box_w - 4 # 2 border + 2 padding
133
+ visible_rows = box_h - 4 # top border + title + bottom border + footer
134
+
135
+ max_scroll = max(0, len(lines) - visible_rows)
136
+ scroll = max(0, min(scroll, max_scroll))
137
+
138
+ # Centering offsets
139
+ pad_left = (term_w - box_w) // 2
140
+ pad_top = (term_h - box_h) // 2
141
+ margin = " " * pad_left
142
+
143
+ with buffered_output():
144
+ sys.stdout.write('\033[?25l\033[H\033[J') # hide cursor, clear
145
+
146
+ # Top padding
147
+ for _ in range(pad_top):
148
+ sys.stdout.write('\n')
149
+
150
+ # Top border
151
+ title = " Keybindings "
152
+ side = box_w - 2 - len(title)
153
+ left_bar = side // 2
154
+ right_bar = side - left_bar
155
+ sys.stdout.write(
156
+ f"{margin}{CYAN}┌{'─' * left_bar}{BOLD}{title}"
157
+ f"{RESET}{CYAN}{'─' * right_bar}┐{RESET}\n"
158
+ )
159
+
160
+ # Scroll indicator
161
+ if max_scroll > 0:
162
+ pct = int(scroll / max_scroll * 100) if max_scroll else 0
163
+ scroll_info = f" {scroll + 1}-{min(scroll + visible_rows, len(lines))}/{len(lines)} "
164
+ else:
165
+ scroll_info = ""
166
+
167
+ # Visible content rows
168
+ window = lines[scroll:scroll + visible_rows]
169
+ for i, (line_text, is_header) in enumerate(window):
170
+ if is_header:
171
+ content = f"{CYAN}{BOLD}{line_text}{RESET}"
172
+ else:
173
+ content = line_text
174
+
175
+ # Pad content to fill the box width (strip ANSI for measuring)
176
+ from jot.ui.styles import visible_len
177
+ vis = visible_len(content)
178
+ pad = max(0, inner_w - vis)
179
+ sys.stdout.write(
180
+ f"{margin}{CYAN}│{RESET} {content}{' ' * pad} "
181
+ f"{CYAN}│{RESET}\n"
182
+ )
183
+
184
+ # Fill remaining rows if content is shorter than visible area
185
+ for _ in range(visible_rows - len(window)):
186
+ sys.stdout.write(
187
+ f"{margin}{CYAN}│{RESET} {' ' * inner_w} "
188
+ f"{CYAN}│{RESET}\n"
189
+ )
190
+
191
+ # Footer row (scroll hints)
192
+ if max_scroll > 0:
193
+ hint = f"{DIM}j/k scroll q/ESC close{RESET} {DIM}{scroll_info}{RESET}"
194
+ else:
195
+ hint = f"{DIM}q/ESC close{RESET}"
196
+ hint_vis = visible_len(hint)
197
+ hint_pad = max(0, inner_w - hint_vis)
198
+ sys.stdout.write(
199
+ f"{margin}{CYAN}│{RESET} {hint}{' ' * hint_pad} "
200
+ f"{CYAN}│{RESET}\n"
201
+ )
202
+
203
+ # Bottom border
204
+ sys.stdout.write(
205
+ f"{margin}{CYAN}└{'─' * (box_w - 2)}┘{RESET}\n"
206
+ )
207
+
208
+ # Wait for input
209
+ key = get_key()
210
+ if key is None:
211
+ continue
212
+
213
+ # Scroll keys first — must precede dismiss check so arrow
214
+ # keys scroll instead of being swallowed by ESC handling.
215
+ if key in ('UP', 'k'):
216
+ scroll = max(0, scroll - 1)
217
+ continue
218
+ if key in ('DOWN', 'j'):
219
+ scroll = min(max_scroll, scroll + 1)
220
+ continue
221
+
222
+ # Dismiss keys
223
+ if key in ('\x1b', 'q', '?', '\r', '\n'):
224
+ break
225
+ if key == 'SHIFT_UP':
226
+ scroll = max(0, scroll - visible_rows)
227
+ continue
228
+ if key == 'SHIFT_DOWN':
229
+ scroll = min(max_scroll, scroll + visible_rows)
230
+ continue
231
+ if key == 'g':
232
+ scroll = 0
233
+ continue
234
+ if key == 'G':
235
+ scroll = max_scroll
236
+ continue
237
+
238
+ sys.stdout.write('\033[?25h')
239
+ sys.stdout.flush()
240
+
241
+
242
+ # ── Legacy functions (kept for CLI --help and tests) ───────────────────
243
+
244
+ def display_categorized_shortcuts():
245
+ """Display keyboard shortcuts organized by category (non-modal)."""
246
+ w = get_terminal_width()
247
+ print(f"\n{BOLD}Keyboard Shortcuts{RESET}")
248
+ print("=" * w)
249
+
250
+ for category, items in SHORTCUTS.items():
251
+ print(f"\n{CYAN}{category}:{RESET}")
252
+ for key, description in items:
253
+ print(f" {YELLOW}{key:15}{RESET} {description}")
254
+
255
+ print("\n" + "=" * w + "\n")
256
+
257
+
258
+ def display_help():
259
+ """Display comprehensive help information."""
260
+ help_text = f"""
261
+ {BOLD}Jott - Simple Interactive Task List{RESET}
262
+ {DIM}Version 0.7.1{RESET}
263
+
264
+ {BOLD}BASIC USAGE:{RESET}
265
+ jott Start interactive task manager (current project)
266
+ jott -c <category> Start in specific category
267
+ jott --category <cat> Start in specific category (long form)
268
+ jott --global Start with global tasks
269
+ jott -h / --help Show this help message
270
+
271
+ {BOLD}PROJECT MANAGEMENT:{RESET}
272
+ jott --all-projects Show tasks from all registered projects
273
+ jott --list-projects List all registered projects
274
+ jott --register <name> <path> Register a new project
275
+ jott --unregister <name> Remove project from registry
276
+ jott --refresh Refresh project registry (auto-discover)
277
+ jott --fix-duplicates Fix duplicate task IDs in current directory
278
+
279
+ For keyboard shortcuts, press {CYAN}.?{RESET} inside the interactive TUI.
280
+
281
+ {DIM}For more info: https://github.com/son1112/jot{RESET}"""
282
+ print(help_text)
@@ -49,26 +49,65 @@ BLACK = '\033[30m'
49
49
  # Background colors
50
50
  BG_YELLOW = '\033[103m'
51
51
  BG_ORANGE = '\033[48;5;208m'
52
+ BG_BURNT_ORANGE = '\033[48;5;166m'
52
53
  BG_MAGENTA = '\033[105m'
53
54
  BG_GRAY = '\033[100m'
54
55
  BG_DARK_GRAY = '\033[48;5;236m'
55
56
  BG_CYAN = '\033[106m'
57
+ BG_BLUE = '\033[48;5;33m'
56
58
  BG_RED = '\033[101m'
57
59
  BG_GREEN = '\033[102m'
58
60
 
59
- # Semantic mappings highlight color palette
61
+ # Semantic mappings, highlight color palette.
62
+ # Each color maps to a task state in STATE_HIGHLIGHTS below; the legacy
63
+ # `status` field is being deprecated in favor of these state-bearing
64
+ # highlights.
60
65
  HIGHLIGHT_COLORS = {
61
66
  'gray': WHITE + BG_DARK_GRAY,
62
- 'yellow': BLACK + BG_YELLOW,
63
- 'orange': BLACK + BG_ORANGE,
64
67
  'magenta': WHITE + BG_MAGENTA,
65
68
  'cyan': BLACK + BG_CYAN,
66
69
  'red': WHITE + BG_RED,
70
+ 'orange': BLACK + BG_ORANGE,
71
+ 'burnt': WHITE + BG_BURNT_ORANGE,
72
+ 'yellow': BLACK + BG_YELLOW,
73
+ 'blue': WHITE + BG_BLUE,
67
74
  'green': BLACK + BG_GREEN,
68
75
  }
69
- HIGHLIGHT_DEFAULT = 'yellow'
76
+ HIGHLIGHT_DEFAULT = 'gray'
70
77
  HIGHLIGHT_FMT = HIGHLIGHT_COLORS[HIGHLIGHT_DEFAULT]
71
78
 
79
+ # Semantic mappings, task state pipeline.
80
+ # Ordered from earliest to latest; STATE_COLORS[state] is the
81
+ # highlight-color key (looks up into HIGHLIGHT_COLORS), or None for
82
+ # the unhighlighted "backlog" state.
83
+ STATE_ORDER = [
84
+ 'backlog',
85
+ 'needs-review',
86
+ 'in-review',
87
+ 'ready',
88
+ 'todo',
89
+ 'doing',
90
+ 'blocked',
91
+ 'done',
92
+ 'shipping',
93
+ 'complete',
94
+ ]
95
+ STATE_COLORS = {
96
+ 'backlog': None,
97
+ 'needs-review': 'gray',
98
+ 'in-review': 'magenta',
99
+ 'ready': 'cyan',
100
+ 'todo': 'red',
101
+ 'doing': 'orange',
102
+ 'blocked': 'burnt',
103
+ 'done': 'yellow',
104
+ 'shipping': 'blue',
105
+ 'complete': 'green',
106
+ }
107
+ # Reverse lookup: highlight-color key → state name (for one-way
108
+ # inference from the legacy color field if ever needed).
109
+ COLOR_TO_STATE = {v: k for k, v in STATE_COLORS.items() if v is not None}
110
+
72
111
  # Semantic mappings — priority
73
112
  PRIORITY_COLORS = {
74
113
  'none': DIM,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jott-cli
3
- Version: 0.7.0
3
+ Version: 0.8.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>
@@ -84,7 +84,9 @@ jott_cli.egg-info/dependency_links.txt
84
84
  jott_cli.egg-info/entry_points.txt
85
85
  jott_cli.egg-info/requires.txt
86
86
  jott_cli.egg-info/top_level.txt
87
+ tests/test_claude_org_prompt.py
87
88
  tests/test_command_handler.py
89
+ tests/test_deleted_notes_dir.py
88
90
  tests/test_dispatch.py
89
91
  tests/test_edit_edge_cases.py
90
92
  tests/test_fuzzy_search.py
@@ -93,7 +95,9 @@ tests/test_github.py
93
95
  tests/test_highlight.py
94
96
  tests/test_input.py
95
97
  tests/test_jot.py
98
+ tests/test_notes_tempfile_location.py
96
99
  tests/test_picker.py
100
+ tests/test_state_palette.py
97
101
  tests/test_styles.py
98
102
  tests/test_subtask_notes.py
99
103
  tests/test_terminal_wrap.py