@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.
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export const theme = {
2
+ accent: "cyan",
3
+ navHighlightBg: "blackBright",
4
+ navHighlightFg: "white",
5
+ priority: "yellow",
6
+ };
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
+ }