@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 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: settings.width, height: settings.height, lists: lists, activePage: page, progress: progress, progressAnimated: settings.progressAnimation, navFocused: focus === "nav", snapshotDate: viewingSnapshot?.date, children: [lists.map((list) => {
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);
@@ -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
- const { waitUntilExit } = render(_jsx(App, {}));
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.1",
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": "^7.1.1",
35
+ "ink": "^8.0.0",
35
36
  "ink-text-input": "^6.0.0",
36
- "react": "^19.2.0"
37
+ "react": "^19.3.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/node": "^26.2.0",
40
- "@types/react": "^19.2.0",
41
+ "@types/react": "^19.3.0",
41
42
  "ink-testing-library": "^4.0.0",
42
43
  "tsx": "^4.23.12",
43
- "typescript": "^5.7.3",
44
- "vitest": "^4.1.11"
44
+ "typescript": "^7.0.2",
45
+ "vitest": "^5.0.0"
45
46
  }
46
47
  }