depth-first-thinking 2.1.0 → 2.1.3
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 +3 -2
- package/src/commands/update.ts +2 -27
- package/src/data/types.ts +3 -0
- package/src/index.ts +3 -2
- 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 -0
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.3",
|
|
4
|
+
"description": "A terminal-based task manager with depth-first navigation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"tui",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"check:fix": "bunx biome check --write src",
|
|
47
47
|
"check-types": "bunx tsc --noEmit",
|
|
48
48
|
"test": "bun test",
|
|
49
|
+
"sync-version": "bun run scripts/sync-version.ts",
|
|
49
50
|
"prepublishOnly": "bun run check && bun run check-types",
|
|
50
51
|
"prepare": "husky",
|
|
51
52
|
"ci": "bun run format && bun run lint && bun run check-types"
|
package/src/commands/update.ts
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
import { ExitCodes } from "../data/types";
|
|
2
|
-
|
|
3
|
-
async function getCurrentVersion(): Promise<string> {
|
|
4
|
-
try {
|
|
5
|
-
// Resolve the package.json that belongs to this installed package,
|
|
6
|
-
// regardless of the current working directory.
|
|
7
|
-
const packageUrl = new URL("../../package.json", import.meta.url);
|
|
8
|
-
const packageJsonFile = Bun.file(packageUrl);
|
|
9
|
-
|
|
10
|
-
if (await packageJsonFile.exists()) {
|
|
11
|
-
const packageJson = (await packageJsonFile.json()) as { version?: string };
|
|
12
|
-
return packageJson.version ?? "unknown";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return "unknown";
|
|
16
|
-
} catch {
|
|
17
|
-
return "unknown";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
2
|
+
import { VERSION } from "../version";
|
|
20
3
|
|
|
21
4
|
async function fetchLatestVersion(): Promise<string | null> {
|
|
22
5
|
try {
|
|
@@ -47,17 +30,9 @@ function compareVersions(current: string, latest: string): number {
|
|
|
47
30
|
}
|
|
48
31
|
|
|
49
32
|
export async function updateCommand(): Promise<void> {
|
|
50
|
-
const currentVersion =
|
|
33
|
+
const currentVersion = VERSION;
|
|
51
34
|
console.log(`Current version: ${currentVersion}`);
|
|
52
35
|
|
|
53
|
-
if (currentVersion === "unknown") {
|
|
54
|
-
console.log(
|
|
55
|
-
"Could not determine the current version. Please make sure dft is installed correctly.",
|
|
56
|
-
);
|
|
57
|
-
process.exit(ExitCodes.FILESYSTEM_ERROR);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
const latestVersion = await fetchLatestVersion();
|
|
62
37
|
|
|
63
38
|
if (!latestVersion) {
|
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
|
@@ -9,13 +9,14 @@ import { treeCommand } from "./commands/tree";
|
|
|
9
9
|
import { updateCommand } from "./commands/update";
|
|
10
10
|
import { getMostOpenedProject } from "./data/storage";
|
|
11
11
|
import { isValidProjectName } from "./utils/validation";
|
|
12
|
+
import { VERSION } from "./version";
|
|
12
13
|
|
|
13
14
|
const program = new Command();
|
|
14
15
|
|
|
15
16
|
program
|
|
16
17
|
.name("dft")
|
|
17
|
-
.description("
|
|
18
|
-
.version(
|
|
18
|
+
.description("A terminal-based task manager with depth-first navigation.")
|
|
19
|
+
.version(VERSION);
|
|
19
20
|
|
|
20
21
|
program
|
|
21
22
|
.command("new <project_name>")
|
|
@@ -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
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "2.1.3";
|