depth-first-thinking 2.1.1 → 2.1.4
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.
- package/README.md +7 -4
- package/package.json +2 -2
- package/src/data/types.ts +3 -0
- package/src/index.ts +1 -1
- package/src/tui/app.test.ts +109 -0
- package/src/tui/app.ts +41 -3
- package/src/tui/navigation.ts +8 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ bun add -g depth-first-thinking
|
|
|
22
22
|
|
|
23
23
|
### Options
|
|
24
24
|
|
|
25
|
-
- `dft delete <name> --yes` - Skip confirmation prompt when deleting
|
|
25
|
+
- `dft delete <name> -y, --yes` - Skip confirmation prompt when deleting
|
|
26
26
|
- `dft tree <name> --show-status` - Show status markers (default: true)
|
|
27
27
|
- `dft tree <name> --no-status` - Hide status markers
|
|
28
28
|
|
|
@@ -34,9 +34,12 @@ Running `dft` without any arguments will:
|
|
|
34
34
|
|
|
35
35
|
## Navigation
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
37
|
+
When you open a project, **Zen Mode** is enabled by default. In Zen Mode, only the currently selected task is displayed, while navigation still works across the full tree:
|
|
38
|
+
|
|
39
|
+
- `↑` `↓` - Move between sibling tasks at the current level
|
|
40
|
+
- `→` `Space` `Enter` - Enter task / view its subtasks (move deeper in the hierarchy)
|
|
41
|
+
- `←` - Go back to parent task (move up in the hierarchy)
|
|
42
|
+
- `m` - Toggle between **Zen Mode** (single-task view) and **List Mode** (full list at current level)
|
|
40
43
|
- `n` - New subtask
|
|
41
44
|
- `e` - Edit task title
|
|
42
45
|
- `d` - Toggle done status
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "depth-first-thinking",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "A terminal-based task manager with depth-first navigation.
|
|
3
|
+
"version": "2.1.4",
|
|
4
|
+
"description": "A terminal-based task manager with depth-first navigation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"tui",
|
package/src/data/types.ts
CHANGED
|
@@ -27,10 +27,13 @@ export interface ModalState {
|
|
|
27
27
|
selectedButton?: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export type ViewMode = "list" | "zen";
|
|
31
|
+
|
|
30
32
|
export interface AppState {
|
|
31
33
|
project: Project;
|
|
32
34
|
navigationStack: string[];
|
|
33
35
|
selectedIndex: number;
|
|
36
|
+
viewMode: ViewMode;
|
|
34
37
|
modalState: ModalState | null;
|
|
35
38
|
feedbackMessage: string | null;
|
|
36
39
|
feedbackTimeout?: ReturnType<typeof setTimeout>;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AppState, Project } from "../data/types";
|
|
3
|
+
import {
|
|
4
|
+
diveIn,
|
|
5
|
+
ensureValidSelection,
|
|
6
|
+
getCurrentList,
|
|
7
|
+
goBack,
|
|
8
|
+
initializeNavigation,
|
|
9
|
+
} from "./navigation";
|
|
10
|
+
|
|
11
|
+
function createTestProject(): Project {
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
return {
|
|
14
|
+
project_name: "Test Project",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
created_at: now,
|
|
17
|
+
modified_at: now,
|
|
18
|
+
root: {
|
|
19
|
+
id: "root",
|
|
20
|
+
title: "Root",
|
|
21
|
+
status: "open",
|
|
22
|
+
children: [
|
|
23
|
+
{
|
|
24
|
+
id: "a",
|
|
25
|
+
title: "Task A",
|
|
26
|
+
status: "open",
|
|
27
|
+
children: [],
|
|
28
|
+
created_at: now,
|
|
29
|
+
completed_at: null,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "b",
|
|
33
|
+
title: "Task B",
|
|
34
|
+
status: "open",
|
|
35
|
+
children: [],
|
|
36
|
+
created_at: now,
|
|
37
|
+
completed_at: null,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
created_at: now,
|
|
41
|
+
completed_at: null,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("viewMode and navigation state", () => {
|
|
47
|
+
test("initial state uses zen mode and selection is valid", () => {
|
|
48
|
+
const project = createTestProject();
|
|
49
|
+
const state: AppState = {
|
|
50
|
+
project,
|
|
51
|
+
navigationStack: [],
|
|
52
|
+
selectedIndex: 0,
|
|
53
|
+
viewMode: "zen",
|
|
54
|
+
modalState: null,
|
|
55
|
+
feedbackMessage: null,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
initializeNavigation(state);
|
|
59
|
+
ensureValidSelection(state);
|
|
60
|
+
|
|
61
|
+
expect(state.viewMode).toBe("zen");
|
|
62
|
+
expect(getCurrentList(state)).toHaveLength(2);
|
|
63
|
+
expect(state.selectedIndex).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("selection survives mode changes", () => {
|
|
67
|
+
const project = createTestProject();
|
|
68
|
+
const state: AppState = {
|
|
69
|
+
project,
|
|
70
|
+
navigationStack: [],
|
|
71
|
+
selectedIndex: 1,
|
|
72
|
+
viewMode: "zen",
|
|
73
|
+
modalState: null,
|
|
74
|
+
feedbackMessage: null,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// simulate toggling between modes
|
|
78
|
+
state.viewMode = "list";
|
|
79
|
+
expect(state.selectedIndex).toBe(1);
|
|
80
|
+
|
|
81
|
+
state.viewMode = "zen";
|
|
82
|
+
expect(state.selectedIndex).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("going back restores parent selection", () => {
|
|
86
|
+
const project = createTestProject();
|
|
87
|
+
const state: AppState = {
|
|
88
|
+
project,
|
|
89
|
+
navigationStack: [],
|
|
90
|
+
selectedIndex: 1,
|
|
91
|
+
viewMode: "zen",
|
|
92
|
+
modalState: null,
|
|
93
|
+
feedbackMessage: null,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Dive into Task B
|
|
97
|
+
const diveResult = diveIn(state);
|
|
98
|
+
expect(diveResult.success).toBe(true);
|
|
99
|
+
expect(state.navigationStack).toEqual(["b"]);
|
|
100
|
+
|
|
101
|
+
// Go back to parent list and expect Task B to be selected again
|
|
102
|
+
const backResult = goBack(state);
|
|
103
|
+
expect(backResult.success).toBe(true);
|
|
104
|
+
expect(state.navigationStack).toEqual([]);
|
|
105
|
+
|
|
106
|
+
const list = getCurrentList(state);
|
|
107
|
+
expect(list[state.selectedIndex]?.id).toBe("b");
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/tui/app.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
toggleNodeStatus,
|
|
17
17
|
} from "../data/operations";
|
|
18
18
|
import { saveProject } from "../data/storage";
|
|
19
|
-
import type { AppState, ModalState, Node, Project } from "../data/types";
|
|
19
|
+
import type { AppState, ModalState, Node, Project, ViewMode } from "../data/types";
|
|
20
20
|
import { truncate } from "../utils/formatting";
|
|
21
21
|
import { validateTitle } from "../utils/validation";
|
|
22
22
|
import {
|
|
@@ -62,6 +62,7 @@ export class TUIApp {
|
|
|
62
62
|
project,
|
|
63
63
|
navigationStack: [],
|
|
64
64
|
selectedIndex: 0,
|
|
65
|
+
viewMode: "zen",
|
|
65
66
|
modalState: null,
|
|
66
67
|
feedbackMessage: null,
|
|
67
68
|
};
|
|
@@ -110,7 +111,7 @@ export class TUIApp {
|
|
|
110
111
|
|
|
111
112
|
this.hintsText = new TextRenderable(this.renderer, {
|
|
112
113
|
id: "hints",
|
|
113
|
-
content: "↑↓ select →/space enter ← back n new e edit d done x del q quit",
|
|
114
|
+
content: "↑↓ select →/space enter ← back n new e edit d done x del m mode q quit",
|
|
114
115
|
fg: colors.keyHints,
|
|
115
116
|
position: "absolute",
|
|
116
117
|
left: 1,
|
|
@@ -145,7 +146,12 @@ export class TUIApp {
|
|
|
145
146
|
const width = process.stdout.columns || 80;
|
|
146
147
|
ensureValidSelection(this.state);
|
|
147
148
|
this.breadcrumbText.content = this.formatBreadcrumb(width);
|
|
148
|
-
|
|
149
|
+
const height = process.stdout.rows || 24;
|
|
150
|
+
if (this.state.viewMode === "zen") {
|
|
151
|
+
this.listText.content = this.formatZen(width, height);
|
|
152
|
+
} else {
|
|
153
|
+
this.listText.content = this.formatList(width);
|
|
154
|
+
}
|
|
149
155
|
this.feedbackText.content = this.state.feedbackMessage || "";
|
|
150
156
|
this.updateModal();
|
|
151
157
|
}
|
|
@@ -200,6 +206,28 @@ export class TUIApp {
|
|
|
200
206
|
return ` [${count}]`;
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
private formatZen(width: number, height: number): string {
|
|
210
|
+
const selected = getSelectedNode(this.state);
|
|
211
|
+
if (!selected) {
|
|
212
|
+
return "No items. Press 'n' to create one.";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const lines: string[] = [];
|
|
216
|
+
|
|
217
|
+
const title = truncate(selected.title, width - 4);
|
|
218
|
+
const status = this.getStatusDisplay(selected);
|
|
219
|
+
const childCount = this.getChildCountDisplay(selected);
|
|
220
|
+
|
|
221
|
+
lines.push(`> ${title}${childCount}${status}`);
|
|
222
|
+
|
|
223
|
+
const availableLines = (height || 24) - 6;
|
|
224
|
+
for (let i = 1; i < availableLines; i++) {
|
|
225
|
+
lines.push("");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return lines.join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
203
231
|
private formatList(width: number): string {
|
|
204
232
|
const list = getCurrentList(this.state);
|
|
205
233
|
|
|
@@ -388,9 +416,19 @@ export class TUIApp {
|
|
|
388
416
|
case "q":
|
|
389
417
|
this.quit();
|
|
390
418
|
break;
|
|
419
|
+
|
|
420
|
+
case "m":
|
|
421
|
+
this.toggleViewMode();
|
|
422
|
+
break;
|
|
391
423
|
}
|
|
392
424
|
}
|
|
393
425
|
|
|
426
|
+
private toggleViewMode(): void {
|
|
427
|
+
const currentMode: ViewMode = this.state.viewMode;
|
|
428
|
+
this.state.viewMode = currentMode === "zen" ? "list" : "zen";
|
|
429
|
+
this.showFeedback(this.state.viewMode === "zen" ? "Zen Mode" : "List Mode");
|
|
430
|
+
}
|
|
431
|
+
|
|
394
432
|
private handleNavigation(result: { success: boolean; feedbackMessage?: string }): void {
|
|
395
433
|
if (!result.success && result.feedbackMessage) {
|
|
396
434
|
this.showFeedback(result.feedbackMessage);
|
package/src/tui/navigation.ts
CHANGED
|
@@ -98,8 +98,15 @@ export function goBack(state: AppState): NavigationResult {
|
|
|
98
98
|
return { success: false, feedbackMessage: "At root" };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Remember the node we are leaving so we can re-select it
|
|
102
|
+
// when we return to its parent list.
|
|
103
|
+
const lastId = state.navigationStack[state.navigationStack.length - 1];
|
|
101
104
|
state.navigationStack.pop();
|
|
102
|
-
|
|
105
|
+
|
|
106
|
+
const list = getCurrentList(state);
|
|
107
|
+
const previousIndex = list.findIndex((node) => node.id === lastId);
|
|
108
|
+
|
|
109
|
+
state.selectedIndex = previousIndex >= 0 ? previousIndex : 0;
|
|
103
110
|
return { success: true };
|
|
104
111
|
}
|
|
105
112
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "2.1.
|
|
1
|
+
export const VERSION = "2.1.4";
|