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.
- {jott_cli-0.7.0/jott_cli.egg-info → jott_cli-0.8.1}/PKG-INFO +1 -1
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/__init__.py +1 -1
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/_dispatch_mixin.py +2 -1
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/app.py +6 -1
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_claude_mixin.py +10 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_core_mixin.py +3 -15
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_metadata_mixin.py +21 -10
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_notes_mixin.py +6 -1
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_delete_mixin.py +51 -2
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_metadata_mixin.py +15 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_footer.py +0 -3
- jott_cli-0.8.1/jot/ui/display_help.py +282 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/styles.py +43 -4
- {jott_cli-0.7.0 → jott_cli-0.8.1/jott_cli.egg-info}/PKG-INFO +1 -1
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/SOURCES.txt +4 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/pyproject.toml +1 -1
- jott_cli-0.8.1/tests/test_claude_org_prompt.py +90 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_command_handler.py +5 -3
- jott_cli-0.8.1/tests/test_deleted_notes_dir.py +134 -0
- jott_cli-0.8.1/tests/test_notes_tempfile_location.py +73 -0
- jott_cli-0.8.1/tests/test_state_palette.py +139 -0
- jott_cli-0.7.0/jot/ui/display_help.py +0 -233
- {jott_cli-0.7.0 → jott_cli-0.8.1}/LICENSE +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/README.md +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/_app_navigation_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/config.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/manager.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/categories/templates.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/archive.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/config.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/cli/views.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_ai_analysis_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_ai_suggest_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_audio_timer_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_bulk_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_context_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_gcal_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_github_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_transfer_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/_web_clipboard_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/commands/handler.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_age_backlog_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_compress_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_crud_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_export_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_id_migration_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_navigation_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_persistence_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/_subtask_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/archive_manager.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/constants.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/id_manager.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/core/task_manager.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/account_manager.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/auth.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/gcal/events.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/github/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/github/issues.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/_config_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/_handlers_mixin.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/integrations/keywords/handler.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/handlers.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/schemas.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/mcp/server.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/projects/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/projects/backup.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/projects/registry.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_archive.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_projects.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/display_tasks.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/formatting.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/input.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/picker.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/ui/rendering.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/__init__.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/date_utils.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/text_utils.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jot/utils/validation.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/dependency_links.txt +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/entry_points.txt +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/requires.txt +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/jott_cli.egg-info/top_level.txt +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/setup.cfg +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/setup.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_dispatch.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_edit_edge_cases.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_fuzzy_search.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_gcal_notes.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_github.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_highlight.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_input.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_jot.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_picker.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_styles.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_subtask_notes.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_terminal_wrap.py +0 -0
- {jott_cli-0.7.0 → jott_cli-0.8.1}/tests/test_today_filter.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
@@ -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
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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 = [
|
|
134
|
-
for
|
|
135
|
-
|
|
136
|
-
|
|
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"
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = '
|
|
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.
|
|
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
|