@rushinglabs/taskfile 0.2.1 → 0.2.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 +12 -0
- package/dist/production-env.js +7 -0
- package/dist/store.js +16 -0
- package/dist/tui/App.js +50 -5
- package/dist/tui/TodosPage.js +1 -1
- package/dist/tui/frameSize.js +11 -0
- package/dist/tui/index.js +3 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
A simple todo list CLI, runnable via `npx`.
|
|
4
4
|
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Download the installer for your platform from the
|
|
8
|
+
[latest release](https://github.com/meddlin/taskfile/releases/latest) — no Node.js required:
|
|
9
|
+
|
|
10
|
+
- **macOS**: `taskfile-<version>-macos-arm64.pkg` (Apple Silicon) or `…-macos-x64.pkg` (Intel)
|
|
11
|
+
- **Debian/Ubuntu**: `sudo apt install ./taskfile_<version>_amd64.deb` (or `_arm64.deb`)
|
|
12
|
+
- **Windows**: `taskfile-<version>-windows-x64-setup.exe`
|
|
13
|
+
|
|
14
|
+
Then run `taskfile`. Details, uninstall steps, and how to build installers yourself are in
|
|
15
|
+
[docs/installers.md](docs/installers.md). Or use npm, below.
|
|
16
|
+
|
|
5
17
|
## Usage
|
|
6
18
|
|
|
7
19
|
```bash
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// React picks its development or production build the first time it is imported, based on
|
|
2
|
+
// NODE_ENV. The development build records ~10 `performance.measure` entries per render, and Node
|
|
3
|
+
// buffers those forever. The TUI re-renders ~10 times a second for its animations, so a
|
|
4
|
+
// long-running session leaked ~3 KB per render until the process hit the V8 heap limit (~28 hours
|
|
5
|
+
// at 4 GB). This module must be imported before anything that imports react or ink.
|
|
6
|
+
process.env.NODE_ENV = "production";
|
|
7
|
+
export {};
|
package/dist/store.js
CHANGED
|
@@ -156,6 +156,22 @@ export function renameList(id, name) {
|
|
|
156
156
|
getDb().prepare("UPDATE lists SET name = ? WHERE id = ?").run(trimmed, id);
|
|
157
157
|
return { status: "ok", list: { ...rowToList(row), name: trimmed } };
|
|
158
158
|
}
|
|
159
|
+
// Deletes a list and every task in it. The last remaining list can't be deleted: the app always
|
|
160
|
+
// needs at least one list to land on.
|
|
161
|
+
export function deleteList(id) {
|
|
162
|
+
const row = getDb().prepare("SELECT id, name, createdAt FROM lists WHERE id = ?").get(id);
|
|
163
|
+
if (!row) {
|
|
164
|
+
return { status: "not-found" };
|
|
165
|
+
}
|
|
166
|
+
if (loadLists().length <= 1) {
|
|
167
|
+
return { status: "blocked", reason: "Can't delete the only list." };
|
|
168
|
+
}
|
|
169
|
+
// Sub-items reference their parent task, so they go first to keep the foreign key satisfied.
|
|
170
|
+
getDb().prepare("DELETE FROM tasks WHERE listId = ? AND parentId IS NOT NULL").run(id);
|
|
171
|
+
getDb().prepare("DELETE FROM tasks WHERE listId = ?").run(id);
|
|
172
|
+
getDb().prepare("DELETE FROM lists WHERE id = ?").run(id);
|
|
173
|
+
return { status: "ok", list: rowToList(row) };
|
|
174
|
+
}
|
|
159
175
|
export function getDefaultListId() {
|
|
160
176
|
return loadLists()[0].id;
|
|
161
177
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { useApp, useInput } from "ink";
|
|
3
|
+
import { Text, useApp, useInput, useWindowSize } from "ink";
|
|
4
4
|
import { Frame } from "./Frame.js";
|
|
5
5
|
import { TodosPage } from "./TodosPage.js";
|
|
6
6
|
import { SettingsPage } from "./SettingsPage.js";
|
|
7
7
|
import { AddTaskInput } from "./AddTaskInput.js";
|
|
8
8
|
import { SnapshotsModal } from "./SnapshotsModal.js";
|
|
9
9
|
import { SnapshotDetailView } from "./SnapshotDetailView.js";
|
|
10
|
+
import { fitFrameToWindow } from "./frameSize.js";
|
|
10
11
|
import { loadSettings, saveSettings } from "../settings.js";
|
|
11
|
-
import { computeProgress, createList, loadLists, loadTasks, renameList } from "../store.js";
|
|
12
|
+
import { computeProgress, createList, deleteList, loadLists, loadTasks, renameList } from "../store.js";
|
|
12
13
|
import { deleteSnapshot, listSnapshots, loadSnapshotDetail, } from "../snapshots.js";
|
|
13
|
-
const NAV_HINT = "↑/↓ cycle pages · ←/→ switch focus · r rename list · Tab switch · Ctrl+N new list · h snapshots · q quit";
|
|
14
|
+
const NAV_HINT = "↑/↓ cycle pages · ←/→ switch focus · r rename list · D delete list · Tab switch · Ctrl+N new list · h snapshots · q quit";
|
|
15
|
+
const CONFIRM_DELETE_LIST_HINT = "y/enter confirm delete · n/esc cancel";
|
|
14
16
|
const SNAPSHOT_MODAL_HINT = "↑/k ↓/j select · enter/space view · d delete · esc close";
|
|
15
17
|
const SNAPSHOT_CONFIRM_DELETE_HINT = "y confirm delete · n/esc cancel";
|
|
16
18
|
const VIEW_SNAPSHOT_HINT = "esc/h back to live view";
|
|
@@ -27,6 +29,7 @@ export function App() {
|
|
|
27
29
|
const { exit } = useApp();
|
|
28
30
|
const [lists, setLists] = useState(() => loadLists());
|
|
29
31
|
const [settings, setSettings] = useState(() => loadSettings());
|
|
32
|
+
const frameSize = fitFrameToWindow(settings, useWindowSize());
|
|
30
33
|
const [page, setPage] = useState(() => {
|
|
31
34
|
const savedId = settings.lastActiveListId;
|
|
32
35
|
const match = savedId !== null && lists.some((list) => list.id === savedId);
|
|
@@ -47,6 +50,7 @@ export function App() {
|
|
|
47
50
|
const [snapshotModalMode, setSnapshotModalMode] = useState("list");
|
|
48
51
|
const [pendingDeleteSnapshotId, setPendingDeleteSnapshotId] = useState(undefined);
|
|
49
52
|
const [viewingSnapshot, setViewingSnapshot] = useState(undefined);
|
|
53
|
+
const [pendingDeleteListId, setPendingDeleteListId] = useState(undefined);
|
|
50
54
|
useEffect(() => {
|
|
51
55
|
if (page.type === "list") {
|
|
52
56
|
setSettings(saveSettings({ lastActiveListId: page.listId }));
|
|
@@ -56,6 +60,22 @@ export function App() {
|
|
|
56
60
|
setLists(loadLists());
|
|
57
61
|
}
|
|
58
62
|
const renamingListId = mode === "nav-rename-list" && page.type === "list" ? page.listId : undefined;
|
|
63
|
+
const pendingDeleteList = mode === "confirm-delete-list" ? lists.find((list) => list.id === pendingDeleteListId) : undefined;
|
|
64
|
+
const pendingDeleteTaskCount = pendingDeleteList ? loadTasks(pendingDeleteList.id).length : 0;
|
|
65
|
+
function confirmDeleteList(listId) {
|
|
66
|
+
const index = lists.findIndex((list) => list.id === listId);
|
|
67
|
+
const result = deleteList(listId);
|
|
68
|
+
if (result.status === "blocked") {
|
|
69
|
+
setMessage(result.reason);
|
|
70
|
+
}
|
|
71
|
+
else if (result.status === "ok") {
|
|
72
|
+
const nextLists = loadLists();
|
|
73
|
+
setLists(nextLists);
|
|
74
|
+
// Land on the list that took the deleted one's place (or the new last list).
|
|
75
|
+
const neighbor = nextLists[Math.min(Math.max(index, 0), nextLists.length - 1)];
|
|
76
|
+
setPage({ type: "list", listId: neighbor.id });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
59
79
|
function cyclePage(delta) {
|
|
60
80
|
const pages = buildPages(lists);
|
|
61
81
|
const currentIndex = pages.findIndex((candidate) => pagesEqual(candidate, page));
|
|
@@ -130,6 +150,20 @@ export function App() {
|
|
|
130
150
|
}
|
|
131
151
|
return;
|
|
132
152
|
}
|
|
153
|
+
if (mode === "confirm-delete-list") {
|
|
154
|
+
if (input === "y" || key.return) {
|
|
155
|
+
if (pendingDeleteListId !== undefined) {
|
|
156
|
+
confirmDeleteList(pendingDeleteListId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (!(input === "n" || key.escape)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setPendingDeleteListId(undefined);
|
|
163
|
+
setMode("normal");
|
|
164
|
+
setHint(NAV_HINT);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
133
167
|
if (mode === "new-list" || mode === "nav-rename-list") {
|
|
134
168
|
if (key.escape) {
|
|
135
169
|
setMode("normal");
|
|
@@ -149,6 +183,17 @@ export function App() {
|
|
|
149
183
|
setHint(SNAPSHOT_MODAL_HINT);
|
|
150
184
|
return;
|
|
151
185
|
}
|
|
186
|
+
if (input === "D" && page.type === "list") {
|
|
187
|
+
setMessage(undefined);
|
|
188
|
+
if (lists.length <= 1) {
|
|
189
|
+
setMessage("Can't delete the only list.");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setPendingDeleteListId(page.listId);
|
|
193
|
+
setMode("confirm-delete-list");
|
|
194
|
+
setHint(CONFIRM_DELETE_LIST_HINT);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
152
197
|
if (key.ctrl && input === "n") {
|
|
153
198
|
setMessage(undefined);
|
|
154
199
|
setHint("enter save · esc cancel");
|
|
@@ -188,7 +233,7 @@ export function App() {
|
|
|
188
233
|
}
|
|
189
234
|
}
|
|
190
235
|
});
|
|
191
|
-
return (_jsxs(Frame, { message: message, hint: hint, width:
|
|
236
|
+
return (_jsxs(Frame, { message: message, hint: hint, width: frameSize.width, height: frameSize.height, lists: lists, activePage: page, progress: progress, progressAnimated: settings.progressAnimation, navFocused: focus === "nav", snapshotDate: viewingSnapshot?.date, children: [lists.map((list) => {
|
|
192
237
|
const visible = !viewingSnapshot && mode === "normal" && page.type === "list" && page.listId === list.id;
|
|
193
238
|
return (_jsx(TodosPage, { listId: list.id, listName: list.name, onListRenamed: refreshLists, visible: visible, active: visible && focus === "content", setMessage: setMessage, onNavLockChange: setNavLocked, onHintChange: setHint, onProgressChange: setProgress }, list.id));
|
|
194
239
|
}), _jsx(SettingsPage, { visible: !viewingSnapshot && mode === "normal" && page.type === "settings", active: !viewingSnapshot && mode === "normal" && page.type === "settings" && focus === "content", settings: settings, onSettingsChange: setSettings, setMessage: setMessage, onNavLockChange: setNavLocked, onHintChange: setHint }), viewingSnapshot && _jsx(SnapshotDetailView, { detail: viewingSnapshot }), mode === "snapshots" && (_jsx(SnapshotsModal, { summaries: snapshotSummaries, selectedIndex: snapshotSelectedIndex, modalMode: snapshotModalMode, pendingDeleteId: pendingDeleteSnapshotId })), mode === "new-list" && (_jsx(AddTaskInput, { label: "New list: ", onSubmit: (name) => {
|
|
@@ -196,7 +241,7 @@ export function App() {
|
|
|
196
241
|
setLists(loadLists());
|
|
197
242
|
setPage({ type: "list", listId: list.id });
|
|
198
243
|
setMode("normal");
|
|
199
|
-
} })), renamingListId !== undefined && (_jsx(AddTaskInput, { label: "Rename list: ", initialValue: lists.find((l) => l.id === renamingListId)?.name ?? "", onSubmit: (text) => {
|
|
244
|
+
} })), pendingDeleteList && (_jsxs(Text, { children: ["Delete list \"", pendingDeleteList.name, "\" and its ", pendingDeleteTaskCount, " task(s)? (y/n)"] })), renamingListId !== undefined && (_jsx(AddTaskInput, { label: "Rename list: ", initialValue: lists.find((l) => l.id === renamingListId)?.name ?? "", onSubmit: (text) => {
|
|
200
245
|
const result = renameList(renamingListId, text);
|
|
201
246
|
if (result.status === "blocked") {
|
|
202
247
|
setMessage(result.reason);
|
package/dist/tui/TodosPage.js
CHANGED
|
@@ -5,7 +5,7 @@ import { addTask, computeProgress, hasSubItems, loadTasks, removeTask, renameLis
|
|
|
5
5
|
import { TaskList } from "./TaskList.js";
|
|
6
6
|
import { AddTaskInput } from "./AddTaskInput.js";
|
|
7
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";
|
|
8
|
+
const LIST_HINT = "↑/k ↓/j move · space/enter toggle · a add · s add sub-item · e edit · d delete · r rename list · D delete list · ←/→ nav focus · Tab switch · Ctrl+N new list · q quit";
|
|
9
9
|
const EDIT_HINT = "tab cycle · space toggle priority · enter save · esc cancel";
|
|
10
10
|
const RENAME_LIST_HINT = "enter save · esc cancel";
|
|
11
11
|
function resolveGroupParentId(tasks, index) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Ink switches to a clear-and-redraw mode once the output is as tall as the
|
|
2
|
+
// terminal, so leave the last row free to keep rendering flicker-free.
|
|
3
|
+
const RESERVED_ROWS = 1;
|
|
4
|
+
// The width/height settings are maximums: the frame shrinks to fit a smaller
|
|
5
|
+
// terminal instead of overflowing it, and follows the terminal as it resizes.
|
|
6
|
+
export function fitFrameToWindow(configured, window) {
|
|
7
|
+
return {
|
|
8
|
+
width: Math.max(Math.min(configured.width, window.columns), 1),
|
|
9
|
+
height: Math.max(Math.min(configured.height, window.rows - RESERVED_ROWS), 1),
|
|
10
|
+
};
|
|
11
|
+
}
|
package/dist/tui/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { loadSettings } from "../settings.js";
|
|
|
5
5
|
import { runStartupSnapshotMaintenance } from "../snapshots.js";
|
|
6
6
|
export async function runTui() {
|
|
7
7
|
runStartupSnapshotMaintenance(loadSettings());
|
|
8
|
-
|
|
8
|
+
// Use the terminal's alternate screen (like vim/htop) so earlier shell output
|
|
9
|
+
// isn't visible around the app, and is restored when the TUI exits.
|
|
10
|
+
const { waitUntilExit } = render(_jsx(App, {}), { alternateScreen: true });
|
|
9
11
|
await waitUntilExit();
|
|
10
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rushinglabs/taskfile",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A simple todo list CLI, runnable via npx",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"dev": "tsx src/index.ts",
|
|
18
|
+
"installers": "tsx scripts/installers/build.ts",
|
|
18
19
|
"test": "vitest run",
|
|
19
20
|
"prepublishOnly": "npm run build"
|
|
20
21
|
},
|
|
@@ -31,16 +32,16 @@
|
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"commander": "^15.0.0",
|
|
34
|
-
"ink": "^
|
|
35
|
+
"ink": "^8.0.0",
|
|
35
36
|
"ink-text-input": "^6.0.0",
|
|
36
|
-
"react": "^19.
|
|
37
|
+
"react": "^19.3.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/node": "^26.2.0",
|
|
40
|
-
"@types/react": "^19.
|
|
41
|
+
"@types/react": "^19.3.0",
|
|
41
42
|
"ink-testing-library": "^4.0.0",
|
|
42
43
|
"tsx": "^4.23.12",
|
|
43
|
-
"typescript": "^
|
|
44
|
-
"vitest": "^
|
|
44
|
+
"typescript": "^7.0.2",
|
|
45
|
+
"vitest": "^5.0.0"
|
|
45
46
|
}
|
|
46
47
|
}
|