@rushinglabs/taskfile 0.2.0
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/LICENSE +15 -0
- package/README.md +72 -0
- package/dist/commands/add.js +5 -0
- package/dist/commands/list.js +12 -0
- package/dist/commands/remove.js +15 -0
- package/dist/index.js +49 -0
- package/dist/settings.js +74 -0
- package/dist/snapshots.js +97 -0
- package/dist/store.js +253 -0
- package/dist/tui/AddTaskInput.js +13 -0
- package/dist/tui/App.js +210 -0
- package/dist/tui/Divider.js +5 -0
- package/dist/tui/EditTaskModal.js +7 -0
- package/dist/tui/Frame.js +21 -0
- package/dist/tui/MessageArea.js +6 -0
- package/dist/tui/ProgressBar.js +41 -0
- package/dist/tui/SettingsPage.js +110 -0
- package/dist/tui/Sidebar.js +12 -0
- package/dist/tui/SnapshotDetailView.js +6 -0
- package/dist/tui/SnapshotsModal.js +7 -0
- package/dist/tui/TaskList.js +23 -0
- package/dist/tui/TodosPage.js +222 -0
- package/dist/tui/index.js +10 -0
- package/dist/tui/theme.js +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text, useApp, useInput } from "ink";
|
|
4
|
+
import { addTask, computeProgress, hasSubItems, loadTasks, removeTask, renameList, setPriority, toggleTask, updateTask, } from "../store.js";
|
|
5
|
+
import { TaskList } from "./TaskList.js";
|
|
6
|
+
import { AddTaskInput } from "./AddTaskInput.js";
|
|
7
|
+
import { EditTaskModal } from "./EditTaskModal.js";
|
|
8
|
+
const LIST_HINT = "↑/k ↓/j move · space/enter toggle · a add · s add sub-item · e edit · d delete · r rename list · ←/→ nav focus · Tab switch · Ctrl+N new list · q quit";
|
|
9
|
+
const EDIT_HINT = "tab cycle · space toggle priority · enter save · esc cancel";
|
|
10
|
+
const RENAME_LIST_HINT = "enter save · esc cancel";
|
|
11
|
+
function resolveGroupParentId(tasks, index) {
|
|
12
|
+
const task = tasks[index];
|
|
13
|
+
if (!task)
|
|
14
|
+
return undefined;
|
|
15
|
+
return task.parentId === null ? task.id : task.parentId;
|
|
16
|
+
}
|
|
17
|
+
export function TodosPage({ visible, active, listId, listName, onListRenamed, setMessage, onNavLockChange, onHintChange, onProgressChange, }) {
|
|
18
|
+
const { exit } = useApp();
|
|
19
|
+
const [tasks, setTasks] = useState(() => loadTasks(listId));
|
|
20
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
21
|
+
const [mode, setMode] = useState("list");
|
|
22
|
+
const [pendingDeleteId, setPendingDeleteId] = useState(undefined);
|
|
23
|
+
const [addParentId, setAddParentId] = useState(null);
|
|
24
|
+
const [editingTaskId, setEditingTaskId] = useState(undefined);
|
|
25
|
+
const [editValue, setEditValue] = useState("");
|
|
26
|
+
const [editPriority, setEditPriority] = useState(false);
|
|
27
|
+
const [editFocus, setEditFocus] = useState("input");
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
onNavLockChange(mode === "add" || mode === "edit" || mode === "rename-list");
|
|
30
|
+
if (active) {
|
|
31
|
+
onHintChange(mode === "list" ? LIST_HINT : mode === "edit" ? EDIT_HINT : mode === "rename-list" ? RENAME_LIST_HINT : "");
|
|
32
|
+
}
|
|
33
|
+
}, [mode, active, onNavLockChange, onHintChange]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!visible)
|
|
36
|
+
return;
|
|
37
|
+
const { done, total } = computeProgress(tasks);
|
|
38
|
+
onProgressChange({ done, total });
|
|
39
|
+
}, [tasks, visible, onProgressChange]);
|
|
40
|
+
function refresh() {
|
|
41
|
+
setTasks(loadTasks(listId));
|
|
42
|
+
}
|
|
43
|
+
function commitEdit() {
|
|
44
|
+
if (editingTaskId === undefined) {
|
|
45
|
+
setMode("list");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const result = updateTask(editingTaskId, editValue);
|
|
49
|
+
if (result.status === "blocked") {
|
|
50
|
+
setMessage(result.reason);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (result.status === "ok") {
|
|
54
|
+
setPriority(editingTaskId, editPriority);
|
|
55
|
+
refresh();
|
|
56
|
+
}
|
|
57
|
+
setEditingTaskId(undefined);
|
|
58
|
+
setMode("list");
|
|
59
|
+
}
|
|
60
|
+
function cancelEdit() {
|
|
61
|
+
setEditingTaskId(undefined);
|
|
62
|
+
setMode("list");
|
|
63
|
+
}
|
|
64
|
+
function clampSelection(nextTasks, index) {
|
|
65
|
+
if (nextTasks.length === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
return Math.min(Math.max(index, 0), nextTasks.length - 1);
|
|
68
|
+
}
|
|
69
|
+
useInput((input, key) => {
|
|
70
|
+
setMessage(undefined);
|
|
71
|
+
if (mode === "add") {
|
|
72
|
+
if (key.escape) {
|
|
73
|
+
setMode("list");
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (mode === "edit") {
|
|
78
|
+
if (key.escape) {
|
|
79
|
+
cancelEdit();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (key.tab) {
|
|
83
|
+
const order = ["input", "priority", "save", "cancel"];
|
|
84
|
+
const delta = key.shift ? -1 : 1;
|
|
85
|
+
const nextIndex = (order.indexOf(editFocus) + delta + order.length) % order.length;
|
|
86
|
+
setEditFocus(order[nextIndex]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (editFocus === "input") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (editFocus === "priority") {
|
|
93
|
+
if (input === " " || key.return) {
|
|
94
|
+
setEditPriority((p) => !p);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (key.return) {
|
|
99
|
+
if (editFocus === "save") {
|
|
100
|
+
commitEdit();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
cancelEdit();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (mode === "rename-list") {
|
|
109
|
+
if (key.escape) {
|
|
110
|
+
setMode("list");
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (mode === "confirm-delete") {
|
|
115
|
+
if (input === "y" || input === "d" || key.return) {
|
|
116
|
+
if (pendingDeleteId !== undefined) {
|
|
117
|
+
const result = removeTask(pendingDeleteId);
|
|
118
|
+
if (result.status === "blocked") {
|
|
119
|
+
setMessage(result.reason);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const nextTasks = loadTasks(listId);
|
|
123
|
+
setTasks(nextTasks);
|
|
124
|
+
setSelectedIndex((index) => clampSelection(nextTasks, index));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
setPendingDeleteId(undefined);
|
|
128
|
+
setMode("list");
|
|
129
|
+
}
|
|
130
|
+
else if (input === "n" || key.escape) {
|
|
131
|
+
setPendingDeleteId(undefined);
|
|
132
|
+
setMode("list");
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// mode === "list"
|
|
137
|
+
if (input === "q" || key.escape) {
|
|
138
|
+
exit();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.upArrow || input === "k") {
|
|
142
|
+
setSelectedIndex((index) => clampSelection(tasks, index - 1));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.downArrow || input === "j") {
|
|
146
|
+
setSelectedIndex((index) => clampSelection(tasks, index + 1));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (input === " " || key.return) {
|
|
150
|
+
const task = tasks[selectedIndex];
|
|
151
|
+
if (task) {
|
|
152
|
+
const result = toggleTask(task.id);
|
|
153
|
+
if (result.status === "blocked") {
|
|
154
|
+
setMessage(result.reason);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
refresh();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (input === "e") {
|
|
163
|
+
const task = tasks[selectedIndex];
|
|
164
|
+
if (task) {
|
|
165
|
+
setEditingTaskId(task.id);
|
|
166
|
+
setEditValue(task.text);
|
|
167
|
+
setEditPriority(task.priority);
|
|
168
|
+
setEditFocus("input");
|
|
169
|
+
setMode("edit");
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (input === "d" || input === "x") {
|
|
174
|
+
const task = tasks[selectedIndex];
|
|
175
|
+
if (task) {
|
|
176
|
+
if (hasSubItems(task.id)) {
|
|
177
|
+
setMessage("Delete all sub-items first.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
setPendingDeleteId(task.id);
|
|
181
|
+
setMode("confirm-delete");
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (input === "a") {
|
|
186
|
+
setAddParentId(null);
|
|
187
|
+
setMode("add");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (input === "s") {
|
|
191
|
+
const groupParentId = resolveGroupParentId(tasks, selectedIndex);
|
|
192
|
+
if (groupParentId === undefined) {
|
|
193
|
+
setMessage("Add a task first before adding sub-items.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setAddParentId(groupParentId);
|
|
197
|
+
setMode("add");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (input === "r") {
|
|
201
|
+
setMode("rename-list");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}, { isActive: active });
|
|
205
|
+
const pendingDeleteTask = tasks.find((task) => task.id === pendingDeleteId);
|
|
206
|
+
if (!visible)
|
|
207
|
+
return _jsx(_Fragment, {});
|
|
208
|
+
return (_jsxs(_Fragment, { children: [_jsx(TaskList, { tasks: tasks, selectedIndex: selectedIndex }), mode === "confirm-delete" && pendingDeleteTask && _jsxs(Text, { children: ["Delete \"", pendingDeleteTask.text, "\"? (y/n)"] }), mode === "add" && (_jsx(AddTaskInput, { label: addParentId === null ? "New task: " : "New sub-item: ", onSubmit: (text) => {
|
|
209
|
+
addTask(listId, text, addParentId);
|
|
210
|
+
refresh();
|
|
211
|
+
setMode("list");
|
|
212
|
+
} })), mode === "rename-list" && (_jsx(AddTaskInput, { label: "Rename list: ", initialValue: listName, onSubmit: (text) => {
|
|
213
|
+
const result = renameList(listId, text);
|
|
214
|
+
if (result.status === "blocked") {
|
|
215
|
+
setMessage(result.reason);
|
|
216
|
+
}
|
|
217
|
+
else if (result.status === "ok") {
|
|
218
|
+
onListRenamed();
|
|
219
|
+
}
|
|
220
|
+
setMode("list");
|
|
221
|
+
} })), mode === "edit" && (_jsx(EditTaskModal, { value: editValue, onChange: setEditValue, onSubmit: commitEdit, focus: editFocus, priority: editPriority }))] }));
|
|
222
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { App } from "./App.js";
|
|
4
|
+
import { loadSettings } from "../settings.js";
|
|
5
|
+
import { runStartupSnapshotMaintenance } from "../snapshots.js";
|
|
6
|
+
export async function runTui() {
|
|
7
|
+
runStartupSnapshotMaintenance(loadSettings());
|
|
8
|
+
const { waitUntilExit } = render(_jsx(App, {}));
|
|
9
|
+
await waitUntilExit();
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rushinglabs/taskfile",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A simple todo list CLI, runnable via npx",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"taskfile": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"todo",
|
|
23
|
+
"cli",
|
|
24
|
+
"task"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.5.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"ink": "^7.1.1",
|
|
35
|
+
"react": "^19.2.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.13.1",
|
|
39
|
+
"@types/react": "^19.2.0",
|
|
40
|
+
"ink-testing-library": "^4.0.0",
|
|
41
|
+
"ink-text-input": "^6.0.0",
|
|
42
|
+
"tsx": "^4.23.12",
|
|
43
|
+
"typescript": "^5.7.3",
|
|
44
|
+
"vitest": "^4.1.10"
|
|
45
|
+
}
|
|
46
|
+
}
|