jott-cli 0.7.1__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.1/jott_cli.egg-info → jott_cli-0.8.1}/PKG-INFO +1 -1
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/__init__.py +1 -1
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/app.py +5 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_metadata_mixin.py +21 -10
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_delete_mixin.py +51 -2
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_metadata_mixin.py +15 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/styles.py +43 -4
- {jott_cli-0.7.1 → jott_cli-0.8.1/jott_cli.egg-info}/PKG-INFO +1 -1
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jott_cli.egg-info/SOURCES.txt +4 -0
- {jott_cli-0.7.1 → 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.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.1 → jott_cli-0.8.1}/LICENSE +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/README.md +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/_app_navigation_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/_dispatch_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/categories/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/categories/config.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/categories/manager.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/categories/templates.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/cli/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/cli/archive.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/cli/config.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/cli/views.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_ai_analysis_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_ai_suggest_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_audio_timer_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_bulk_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_claude_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_context_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_core_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_gcal_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_github_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_notes_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_transfer_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/_web_clipboard_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/commands/handler.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_age_backlog_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_compress_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_crud_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_export_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_id_migration_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_navigation_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_persistence_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/_subtask_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/archive_manager.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/constants.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/id_manager.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/core/task_manager.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/gcal/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/gcal/account_manager.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/gcal/auth.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/gcal/events.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/github/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/github/issues.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/keywords/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/keywords/_config_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/keywords/_handlers_mixin.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/integrations/keywords/handler.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/mcp/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/mcp/handlers.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/mcp/schemas.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/mcp/server.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/projects/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/projects/backup.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/projects/registry.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/display.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/display_archive.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/display_footer.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/display_help.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/display_projects.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/display_tasks.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/formatting.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/input.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/picker.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/ui/rendering.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/utils/__init__.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/utils/date_utils.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/utils/text_utils.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jot/utils/validation.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jott_cli.egg-info/dependency_links.txt +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jott_cli.egg-info/entry_points.txt +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jott_cli.egg-info/requires.txt +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/jott_cli.egg-info/top_level.txt +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/setup.cfg +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/setup.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_command_handler.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_dispatch.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_edit_edge_cases.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_fuzzy_search.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_gcal_notes.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_github.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_highlight.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_input.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_jot.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_picker.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_styles.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_subtask_notes.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_terminal_wrap.py +0 -0
- {jott_cli-0.7.1 → jott_cli-0.8.1}/tests/test_today_filter.py +0 -0
- {jott_cli-0.7.1 → 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.
|
|
@@ -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
|
# ------------------------------------------------------------------
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -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
|
|
@@ -7,7 +7,7 @@ include = ["jot", "jot.*"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "jott-cli"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.8.1"
|
|
11
11
|
description = "Feature-rich interactive CLI task manager with AI integration, calendar sync, and keyword automation"
|
|
12
12
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
13
13
|
requires-python = ">=3.6"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for org-mode formatting instruction appended to Claude prompts."""
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from jot import TaskManager
|
|
10
|
+
from jot.commands.handler import CommandHandler
|
|
11
|
+
from jot.commands._claude_mixin import _ORG_OUTPUT_INSTRUCTION
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_handler(tmpdir: Path):
|
|
15
|
+
(tmpdir / ".jot-registry.json").write_text("{}")
|
|
16
|
+
tm = TaskManager(storage_file='.jot.json', directory=str(tmpdir))
|
|
17
|
+
return tm, CommandHandler(tm)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _fake_subprocess_result(stdout="* Result\nbody\n"):
|
|
21
|
+
return SimpleNamespace(stdout=stdout, stderr="", returncode=0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestOrgOutputInstruction:
|
|
25
|
+
def test_instruction_constant_mentions_org_markers(self):
|
|
26
|
+
# Sanity: ensure the constant actually directs to org-mode output.
|
|
27
|
+
assert '#+begin_src' in _ORG_OUTPUT_INSTRUCTION
|
|
28
|
+
assert 'Do NOT use markdown' in _ORG_OUTPUT_INSTRUCTION
|
|
29
|
+
assert '~symbol~' in _ORG_OUTPUT_INSTRUCTION or '=literal=' in _ORG_OUTPUT_INSTRUCTION
|
|
30
|
+
|
|
31
|
+
def test_instruction_appended_when_no_existing_notes(self):
|
|
32
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
33
|
+
tmpdir = Path(raw)
|
|
34
|
+
tm, handler = _make_handler(tmpdir)
|
|
35
|
+
tm.add_task("write a fizzbuzz")
|
|
36
|
+
task = tm.tasks[0]
|
|
37
|
+
with patch(
|
|
38
|
+
'jot.commands._claude_mixin.subprocess.run',
|
|
39
|
+
return_value=_fake_subprocess_result(),
|
|
40
|
+
) as mocked:
|
|
41
|
+
handler._run_claude_background(
|
|
42
|
+
task['id'], task['text'], existing_notes='',
|
|
43
|
+
allow_edits=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
assert mocked.called
|
|
47
|
+
prompt = mocked.call_args.kwargs['input']
|
|
48
|
+
assert prompt.startswith("write a fizzbuzz")
|
|
49
|
+
assert prompt.endswith(_ORG_OUTPUT_INSTRUCTION)
|
|
50
|
+
|
|
51
|
+
def test_instruction_appended_when_existing_notes_present(self):
|
|
52
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
53
|
+
tmpdir = Path(raw)
|
|
54
|
+
tm, handler = _make_handler(tmpdir)
|
|
55
|
+
tm.add_task("rewrite the parser")
|
|
56
|
+
task = tm.tasks[0]
|
|
57
|
+
with patch(
|
|
58
|
+
'jot.commands._claude_mixin.subprocess.run',
|
|
59
|
+
return_value=_fake_subprocess_result(),
|
|
60
|
+
) as mocked:
|
|
61
|
+
handler._run_claude_background(
|
|
62
|
+
task['id'], task['text'],
|
|
63
|
+
existing_notes='prior context here',
|
|
64
|
+
allow_edits=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
prompt = mocked.call_args.kwargs['input']
|
|
68
|
+
assert 'rewrite the parser' in prompt
|
|
69
|
+
assert 'prior context here' in prompt
|
|
70
|
+
assert prompt.endswith(_ORG_OUTPUT_INSTRUCTION)
|
|
71
|
+
|
|
72
|
+
def test_instruction_appended_in_edit_mode(self):
|
|
73
|
+
# allow_edits=True path uses a different cmd but same prompt assembly.
|
|
74
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
75
|
+
tmpdir = Path(raw)
|
|
76
|
+
tm, handler = _make_handler(tmpdir)
|
|
77
|
+
tm.add_task("apply diff")
|
|
78
|
+
task = tm.tasks[0]
|
|
79
|
+
with patch(
|
|
80
|
+
'jot.commands._claude_mixin.subprocess.run',
|
|
81
|
+
return_value=_fake_subprocess_result(),
|
|
82
|
+
) as mocked:
|
|
83
|
+
handler._run_claude_background(
|
|
84
|
+
task['id'], task['text'],
|
|
85
|
+
existing_notes='notes',
|
|
86
|
+
allow_edits=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
prompt = mocked.call_args.kwargs['input']
|
|
90
|
+
assert prompt.endswith(_ORG_OUTPUT_INSTRUCTION)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for deleted-task notes living in .jot-deleted-notes/ subdir
|
|
3
|
+
and the one-shot migration of legacy stray files at the project root.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from jot import TaskManager
|
|
10
|
+
from jot.core._delete_mixin import DELETED_NOTES_DIR
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_tm(tmpdir: Path) -> TaskManager:
|
|
14
|
+
# Pre-seed registry to avoid auto-discovery side effects.
|
|
15
|
+
(tmpdir / ".jot-registry.json").write_text("{}")
|
|
16
|
+
return TaskManager(storage_file='.jot.json', directory=str(tmpdir))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestDeletedNotesDirectory:
|
|
20
|
+
def test_save_notes_writes_into_subdir(self):
|
|
21
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
22
|
+
tmpdir = Path(raw)
|
|
23
|
+
tm = _make_tm(tmpdir)
|
|
24
|
+
tm.add_task("task with notes")
|
|
25
|
+
tid = tm.tasks[0]['id']
|
|
26
|
+
tm.tasks[0]['notes'] = "important context\nline two"
|
|
27
|
+
|
|
28
|
+
path = tm.save_notes_to_org_file(tid)
|
|
29
|
+
|
|
30
|
+
assert path is not None
|
|
31
|
+
p = Path(path)
|
|
32
|
+
assert p.parent.name == DELETED_NOTES_DIR
|
|
33
|
+
assert p.parent.parent == tmpdir
|
|
34
|
+
assert p.exists()
|
|
35
|
+
assert "important context" in p.read_text()
|
|
36
|
+
|
|
37
|
+
def test_remove_task_routes_notes_into_subdir(self):
|
|
38
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
39
|
+
tmpdir = Path(raw)
|
|
40
|
+
tm = _make_tm(tmpdir)
|
|
41
|
+
tm.add_task("doomed task")
|
|
42
|
+
tid = tm.tasks[0]['id']
|
|
43
|
+
tm.tasks[0]['notes'] = "save me"
|
|
44
|
+
|
|
45
|
+
tm.remove_task(tid)
|
|
46
|
+
|
|
47
|
+
root_strays = list(tmpdir.glob("TASK-*-notes-*.org"))
|
|
48
|
+
subdir_files = list(
|
|
49
|
+
(tmpdir / DELETED_NOTES_DIR).glob("TASK-*-notes-*.org")
|
|
50
|
+
)
|
|
51
|
+
assert root_strays == []
|
|
52
|
+
assert len(subdir_files) == 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestLegacyNotesMigration:
|
|
56
|
+
def test_migrate_moves_matching_files(self):
|
|
57
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
58
|
+
tmpdir = Path(raw)
|
|
59
|
+
tm = _make_tm(tmpdir)
|
|
60
|
+
|
|
61
|
+
(tmpdir / "TASK-1-notes-20260101_120000.org").write_text("a")
|
|
62
|
+
(tmpdir / "TASK-42-notes-20260518_235959.org").write_text("b")
|
|
63
|
+
|
|
64
|
+
moved = tm._migrate_legacy_deleted_notes()
|
|
65
|
+
|
|
66
|
+
assert moved == 2
|
|
67
|
+
assert list(tmpdir.glob("TASK-*-notes-*.org")) == []
|
|
68
|
+
dest = tmpdir / DELETED_NOTES_DIR
|
|
69
|
+
assert (dest / "TASK-1-notes-20260101_120000.org").exists()
|
|
70
|
+
assert (dest / "TASK-42-notes-20260518_235959.org").exists()
|
|
71
|
+
|
|
72
|
+
def test_migrate_is_noop_when_no_matches(self):
|
|
73
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
74
|
+
tmpdir = Path(raw)
|
|
75
|
+
tm = _make_tm(tmpdir)
|
|
76
|
+
|
|
77
|
+
moved = tm._migrate_legacy_deleted_notes()
|
|
78
|
+
|
|
79
|
+
assert moved == 0
|
|
80
|
+
assert not (tmpdir / DELETED_NOTES_DIR).exists()
|
|
81
|
+
|
|
82
|
+
def test_migrate_ignores_non_matching_filenames(self):
|
|
83
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
84
|
+
tmpdir = Path(raw)
|
|
85
|
+
tm = _make_tm(tmpdir)
|
|
86
|
+
|
|
87
|
+
keep = [
|
|
88
|
+
"TASK-rfc-notes-draft.org",
|
|
89
|
+
"TASK-1-notes-bad.org",
|
|
90
|
+
"task-1-notes-20260101_120000.org",
|
|
91
|
+
"TASK-1-notes-20260101_120000.txt",
|
|
92
|
+
"NOTES.org",
|
|
93
|
+
]
|
|
94
|
+
for name in keep:
|
|
95
|
+
(tmpdir / name).write_text("x")
|
|
96
|
+
|
|
97
|
+
moved = tm._migrate_legacy_deleted_notes()
|
|
98
|
+
|
|
99
|
+
assert moved == 0
|
|
100
|
+
for name in keep:
|
|
101
|
+
assert (tmpdir / name).exists()
|
|
102
|
+
|
|
103
|
+
def test_migrate_does_not_clobber_existing_dest(self):
|
|
104
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
105
|
+
tmpdir = Path(raw)
|
|
106
|
+
tm = _make_tm(tmpdir)
|
|
107
|
+
|
|
108
|
+
stray = tmpdir / "TASK-7-notes-20260101_120000.org"
|
|
109
|
+
stray.write_text("new")
|
|
110
|
+
dest_dir = tmpdir / DELETED_NOTES_DIR
|
|
111
|
+
dest_dir.mkdir()
|
|
112
|
+
existing = dest_dir / "TASK-7-notes-20260101_120000.org"
|
|
113
|
+
existing.write_text("original")
|
|
114
|
+
|
|
115
|
+
tm._migrate_legacy_deleted_notes()
|
|
116
|
+
|
|
117
|
+
assert stray.exists()
|
|
118
|
+
assert existing.read_text() == "original"
|
|
119
|
+
|
|
120
|
+
def test_migrate_idempotent(self):
|
|
121
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
122
|
+
tmpdir = Path(raw)
|
|
123
|
+
tm = _make_tm(tmpdir)
|
|
124
|
+
(tmpdir / "TASK-9-notes-20260202_020202.org").write_text("z")
|
|
125
|
+
|
|
126
|
+
first = tm._migrate_legacy_deleted_notes()
|
|
127
|
+
second = tm._migrate_legacy_deleted_notes()
|
|
128
|
+
|
|
129
|
+
assert first == 1
|
|
130
|
+
assert second == 0
|
|
131
|
+
assert (
|
|
132
|
+
tmpdir / DELETED_NOTES_DIR
|
|
133
|
+
/ "TASK-9-notes-20260202_020202.org"
|
|
134
|
+
).exists()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests that notes editor temp files are created inside the project dir
|
|
3
|
+
(not /tmp), so editor/LSP can resolve project-local config and tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
from jot import TaskManager
|
|
13
|
+
from jot.commands.handler import CommandHandler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_handler(tmpdir: Path):
|
|
17
|
+
(tmpdir / ".jot-registry.json").write_text("{}")
|
|
18
|
+
tm = TaskManager(storage_file='.jot.json', directory=str(tmpdir))
|
|
19
|
+
return tm, CommandHandler(tm)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def _captured_edit(handler, editor):
|
|
24
|
+
"""Run edit_task_notes() under stubs and yield the captured file_path."""
|
|
25
|
+
captured = {}
|
|
26
|
+
|
|
27
|
+
def fake_editor(file_path):
|
|
28
|
+
captured['path'] = file_path
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
with patch.object(handler, 'safe_launch_editor', side_effect=fake_editor), \
|
|
32
|
+
patch('builtins.input', return_value=''), \
|
|
33
|
+
patch.dict(os.environ, {'EDITOR': editor}, clear=False):
|
|
34
|
+
handler.edit_task_notes()
|
|
35
|
+
|
|
36
|
+
yield captured['path']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestNotesTempfileLocation:
|
|
40
|
+
def test_tempfile_created_inside_project_dir(self):
|
|
41
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
42
|
+
tmpdir = Path(raw).resolve()
|
|
43
|
+
tm, handler = _make_handler(tmpdir)
|
|
44
|
+
tm.add_task("a task")
|
|
45
|
+
tm.set_current(tm.tasks[0]['id'])
|
|
46
|
+
|
|
47
|
+
with _captured_edit(handler, 'nano') as path:
|
|
48
|
+
edited_path = Path(path).resolve()
|
|
49
|
+
|
|
50
|
+
# The temp file should live inside tmpdir, not in /tmp.
|
|
51
|
+
assert tmpdir in edited_path.parents, (
|
|
52
|
+
f"temp file {edited_path} not under project {tmpdir}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_tempfile_uses_org_suffix_for_emacs(self):
|
|
56
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
57
|
+
tmpdir = Path(raw).resolve()
|
|
58
|
+
tm, handler = _make_handler(tmpdir)
|
|
59
|
+
tm.add_task("emacs task")
|
|
60
|
+
tm.set_current(tm.tasks[0]['id'])
|
|
61
|
+
|
|
62
|
+
with _captured_edit(handler, 'emacs -nw') as path:
|
|
63
|
+
assert path.endswith('.org')
|
|
64
|
+
|
|
65
|
+
def test_tempfile_uses_txt_suffix_for_other_editors(self):
|
|
66
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
67
|
+
tmpdir = Path(raw).resolve()
|
|
68
|
+
tm, handler = _make_handler(tmpdir)
|
|
69
|
+
tm.add_task("vim task")
|
|
70
|
+
tm.set_current(tm.tasks[0]['id'])
|
|
71
|
+
|
|
72
|
+
with _captured_edit(handler, 'vim') as path:
|
|
73
|
+
assert path.endswith('.txt')
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for the state-color palette and one-way status->highlight."""
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from jot import TaskManager
|
|
8
|
+
from jot.ui import styles
|
|
9
|
+
from jot.ui.styles import (
|
|
10
|
+
HIGHLIGHT_COLORS,
|
|
11
|
+
HIGHLIGHT_DEFAULT,
|
|
12
|
+
STATE_ORDER,
|
|
13
|
+
STATE_COLORS,
|
|
14
|
+
COLOR_TO_STATE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_tm(tmpdir: Path) -> TaskManager:
|
|
19
|
+
(tmpdir / ".jot-registry.json").write_text("{}")
|
|
20
|
+
return TaskManager(storage_file='.jot.json', directory=str(tmpdir))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestStatePalette:
|
|
24
|
+
def test_state_order_has_ten_entries(self):
|
|
25
|
+
assert len(STATE_ORDER) == 10
|
|
26
|
+
|
|
27
|
+
def test_state_order_is_pipeline_sequence(self):
|
|
28
|
+
assert STATE_ORDER == [
|
|
29
|
+
'backlog', 'needs-review', 'in-review', 'ready',
|
|
30
|
+
'todo', 'doing', 'blocked', 'done', 'shipping', 'complete',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def test_every_state_has_a_color_entry(self):
|
|
34
|
+
for state in STATE_ORDER:
|
|
35
|
+
assert state in STATE_COLORS
|
|
36
|
+
|
|
37
|
+
def test_backlog_maps_to_no_color(self):
|
|
38
|
+
assert STATE_COLORS['backlog'] is None
|
|
39
|
+
|
|
40
|
+
def test_non_backlog_states_map_to_real_palette_keys(self):
|
|
41
|
+
for state in STATE_ORDER:
|
|
42
|
+
color = STATE_COLORS[state]
|
|
43
|
+
if color is None:
|
|
44
|
+
continue
|
|
45
|
+
assert color in HIGHLIGHT_COLORS, (
|
|
46
|
+
f"state {state} maps to unknown color {color}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def test_state_colors_are_unique(self):
|
|
50
|
+
used = [c for c in STATE_COLORS.values() if c is not None]
|
|
51
|
+
assert len(used) == len(set(used)), "two states share a color"
|
|
52
|
+
|
|
53
|
+
def test_color_to_state_is_reverse_of_state_colors(self):
|
|
54
|
+
for color, state in COLOR_TO_STATE.items():
|
|
55
|
+
assert STATE_COLORS[state] == color
|
|
56
|
+
|
|
57
|
+
def test_burnt_and_blue_added_to_palette(self):
|
|
58
|
+
assert 'burnt' in HIGHLIGHT_COLORS
|
|
59
|
+
assert 'blue' in HIGHLIGHT_COLORS
|
|
60
|
+
|
|
61
|
+
def test_default_highlight_is_gray(self):
|
|
62
|
+
assert HIGHLIGHT_DEFAULT == 'gray'
|
|
63
|
+
|
|
64
|
+
def test_burnt_uses_256_color_166(self):
|
|
65
|
+
assert '48;5;166' in HIGHLIGHT_COLORS['burnt']
|
|
66
|
+
|
|
67
|
+
def test_blue_uses_256_color_33(self):
|
|
68
|
+
assert '48;5;33' in HIGHLIGHT_COLORS['blue']
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestStatusAutoHighlights:
|
|
72
|
+
def test_setting_todo_paints_red(self):
|
|
73
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
74
|
+
tm = _make_tm(Path(raw))
|
|
75
|
+
tm.add_task("x")
|
|
76
|
+
tid = tm.tasks[0]['id']
|
|
77
|
+
|
|
78
|
+
tm.set_task_status(tid, 'todo')
|
|
79
|
+
|
|
80
|
+
assert tm.tasks[0]['highlight'] == 'red'
|
|
81
|
+
|
|
82
|
+
def test_setting_in_progress_paints_orange(self):
|
|
83
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
84
|
+
tm = _make_tm(Path(raw))
|
|
85
|
+
tm.add_task("x")
|
|
86
|
+
tid = tm.tasks[0]['id']
|
|
87
|
+
|
|
88
|
+
tm.set_task_status(tid, 'in-progress')
|
|
89
|
+
|
|
90
|
+
assert tm.tasks[0]['highlight'] == 'orange'
|
|
91
|
+
|
|
92
|
+
def test_setting_blocked_paints_burnt(self):
|
|
93
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
94
|
+
tm = _make_tm(Path(raw))
|
|
95
|
+
tm.add_task("x")
|
|
96
|
+
tid = tm.tasks[0]['id']
|
|
97
|
+
|
|
98
|
+
tm.set_task_status(tid, 'blocked')
|
|
99
|
+
|
|
100
|
+
assert tm.tasks[0]['highlight'] == 'burnt'
|
|
101
|
+
|
|
102
|
+
def test_setting_done_paints_yellow(self):
|
|
103
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
104
|
+
tm = _make_tm(Path(raw))
|
|
105
|
+
tm.add_task("x")
|
|
106
|
+
tid = tm.tasks[0]['id']
|
|
107
|
+
|
|
108
|
+
tm.set_task_status(tid, 'done')
|
|
109
|
+
|
|
110
|
+
assert tm.tasks[0]['highlight'] == 'yellow'
|
|
111
|
+
|
|
112
|
+
def test_status_overrides_prior_highlight(self):
|
|
113
|
+
# Status change *always* repaints, since status is the driver.
|
|
114
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
115
|
+
tm = _make_tm(Path(raw))
|
|
116
|
+
tm.add_task("x")
|
|
117
|
+
tid = tm.tasks[0]['id']
|
|
118
|
+
tm.set_current(tid)
|
|
119
|
+
tm.set_highlight(color='magenta')
|
|
120
|
+
assert tm.tasks[0]['highlight'] == 'magenta'
|
|
121
|
+
|
|
122
|
+
tm.set_task_status(tid, 'todo')
|
|
123
|
+
|
|
124
|
+
assert tm.tasks[0]['highlight'] == 'red'
|
|
125
|
+
|
|
126
|
+
def test_highlight_change_does_not_alter_status(self):
|
|
127
|
+
# One-way: highlight changes must not feed back into status.
|
|
128
|
+
with tempfile.TemporaryDirectory() as raw:
|
|
129
|
+
tm = _make_tm(Path(raw))
|
|
130
|
+
tm.add_task("x")
|
|
131
|
+
tid = tm.tasks[0]['id']
|
|
132
|
+
tm.set_current(tid)
|
|
133
|
+
tm.set_task_status(tid, 'todo')
|
|
134
|
+
assert tm.tasks[0]['status'] == 'todo'
|
|
135
|
+
|
|
136
|
+
tm.set_highlight(color='green')
|
|
137
|
+
|
|
138
|
+
assert tm.tasks[0]['status'] == 'todo'
|
|
139
|
+
assert tm.tasks[0]['highlight'] == 'green'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|