@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
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useApp, useInput } from "ink";
|
|
4
|
+
import { Frame } from "./Frame.js";
|
|
5
|
+
import { TodosPage } from "./TodosPage.js";
|
|
6
|
+
import { SettingsPage } from "./SettingsPage.js";
|
|
7
|
+
import { AddTaskInput } from "./AddTaskInput.js";
|
|
8
|
+
import { SnapshotsModal } from "./SnapshotsModal.js";
|
|
9
|
+
import { SnapshotDetailView } from "./SnapshotDetailView.js";
|
|
10
|
+
import { loadSettings, saveSettings } from "../settings.js";
|
|
11
|
+
import { computeProgress, createList, loadLists, loadTasks, renameList } from "../store.js";
|
|
12
|
+
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 SNAPSHOT_MODAL_HINT = "↑/k ↓/j select · enter/space view · d delete · esc close";
|
|
15
|
+
const SNAPSHOT_CONFIRM_DELETE_HINT = "y confirm delete · n/esc cancel";
|
|
16
|
+
const VIEW_SNAPSHOT_HINT = "esc/h back to live view";
|
|
17
|
+
function buildPages(lists) {
|
|
18
|
+
return [...lists.map((list) => ({ type: "list", listId: list.id })), { type: "settings" }];
|
|
19
|
+
}
|
|
20
|
+
function pagesEqual(a, b) {
|
|
21
|
+
if (a.type === "settings" || b.type === "settings") {
|
|
22
|
+
return a.type === b.type;
|
|
23
|
+
}
|
|
24
|
+
return a.listId === b.listId;
|
|
25
|
+
}
|
|
26
|
+
export function App() {
|
|
27
|
+
const { exit } = useApp();
|
|
28
|
+
const [lists, setLists] = useState(() => loadLists());
|
|
29
|
+
const [settings, setSettings] = useState(() => loadSettings());
|
|
30
|
+
const [page, setPage] = useState(() => {
|
|
31
|
+
const savedId = settings.lastActiveListId;
|
|
32
|
+
const match = savedId !== null && lists.some((list) => list.id === savedId);
|
|
33
|
+
return { type: "list", listId: match ? savedId : lists[0].id };
|
|
34
|
+
});
|
|
35
|
+
const [mode, setMode] = useState("normal");
|
|
36
|
+
const [focus, setFocus] = useState("content");
|
|
37
|
+
const [navLocked, setNavLocked] = useState(false);
|
|
38
|
+
const [message, setMessage] = useState(undefined);
|
|
39
|
+
const [hint, setHint] = useState("");
|
|
40
|
+
const [progress, setProgress] = useState(() => {
|
|
41
|
+
const activeListId = page.type === "list" ? page.listId : lists[0].id;
|
|
42
|
+
const { done, total } = computeProgress(loadTasks(activeListId));
|
|
43
|
+
return { done, total };
|
|
44
|
+
});
|
|
45
|
+
const [snapshotSummaries, setSnapshotSummaries] = useState([]);
|
|
46
|
+
const [snapshotSelectedIndex, setSnapshotSelectedIndex] = useState(0);
|
|
47
|
+
const [snapshotModalMode, setSnapshotModalMode] = useState("list");
|
|
48
|
+
const [pendingDeleteSnapshotId, setPendingDeleteSnapshotId] = useState(undefined);
|
|
49
|
+
const [viewingSnapshot, setViewingSnapshot] = useState(undefined);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (page.type === "list") {
|
|
52
|
+
setSettings(saveSettings({ lastActiveListId: page.listId }));
|
|
53
|
+
}
|
|
54
|
+
}, [page]);
|
|
55
|
+
function refreshLists() {
|
|
56
|
+
setLists(loadLists());
|
|
57
|
+
}
|
|
58
|
+
const renamingListId = mode === "nav-rename-list" && page.type === "list" ? page.listId : undefined;
|
|
59
|
+
function cyclePage(delta) {
|
|
60
|
+
const pages = buildPages(lists);
|
|
61
|
+
const currentIndex = pages.findIndex((candidate) => pagesEqual(candidate, page));
|
|
62
|
+
const nextIndex = (currentIndex + delta + pages.length) % pages.length;
|
|
63
|
+
setPage(pages[nextIndex]);
|
|
64
|
+
}
|
|
65
|
+
useInput((input, key) => {
|
|
66
|
+
if (key.ctrl && input === "c") {
|
|
67
|
+
exit();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (viewingSnapshot) {
|
|
71
|
+
if (key.escape || input === "h") {
|
|
72
|
+
setViewingSnapshot(undefined);
|
|
73
|
+
setHint(NAV_HINT);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (mode === "snapshots") {
|
|
78
|
+
if (snapshotModalMode === "confirm-delete") {
|
|
79
|
+
if (input === "y" || input === "d" || key.return) {
|
|
80
|
+
if (pendingDeleteSnapshotId !== undefined) {
|
|
81
|
+
deleteSnapshot(pendingDeleteSnapshotId);
|
|
82
|
+
const next = listSnapshots();
|
|
83
|
+
setSnapshotSummaries(next);
|
|
84
|
+
setSnapshotSelectedIndex((index) => Math.min(index, Math.max(next.length - 1, 0)));
|
|
85
|
+
}
|
|
86
|
+
setPendingDeleteSnapshotId(undefined);
|
|
87
|
+
setSnapshotModalMode("list");
|
|
88
|
+
setHint(SNAPSHOT_MODAL_HINT);
|
|
89
|
+
}
|
|
90
|
+
else if (input === "n" || key.escape) {
|
|
91
|
+
setPendingDeleteSnapshotId(undefined);
|
|
92
|
+
setSnapshotModalMode("list");
|
|
93
|
+
setHint(SNAPSHOT_MODAL_HINT);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (key.escape) {
|
|
98
|
+
setMode("normal");
|
|
99
|
+
setHint(NAV_HINT);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.upArrow || input === "k") {
|
|
103
|
+
setSnapshotSelectedIndex((index) => Math.max(index - 1, 0));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.downArrow || input === "j") {
|
|
107
|
+
setSnapshotSelectedIndex((index) => Math.min(index + 1, snapshotSummaries.length - 1));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (input === " " || key.return) {
|
|
111
|
+
const summary = snapshotSummaries[snapshotSelectedIndex];
|
|
112
|
+
if (summary) {
|
|
113
|
+
const detail = loadSnapshotDetail(summary.id);
|
|
114
|
+
if (detail) {
|
|
115
|
+
setViewingSnapshot(detail);
|
|
116
|
+
setMode("normal");
|
|
117
|
+
setHint(VIEW_SNAPSHOT_HINT);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (input === "d" || input === "x") {
|
|
123
|
+
const summary = snapshotSummaries[snapshotSelectedIndex];
|
|
124
|
+
if (summary) {
|
|
125
|
+
setPendingDeleteSnapshotId(summary.id);
|
|
126
|
+
setSnapshotModalMode("confirm-delete");
|
|
127
|
+
setHint(SNAPSHOT_CONFIRM_DELETE_HINT);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (mode === "new-list" || mode === "nav-rename-list") {
|
|
134
|
+
if (key.escape) {
|
|
135
|
+
setMode("normal");
|
|
136
|
+
setHint(NAV_HINT);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (navLocked) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (input === "h") {
|
|
144
|
+
setMessage(undefined);
|
|
145
|
+
setSnapshotSummaries(listSnapshots());
|
|
146
|
+
setSnapshotSelectedIndex(0);
|
|
147
|
+
setSnapshotModalMode("list");
|
|
148
|
+
setMode("snapshots");
|
|
149
|
+
setHint(SNAPSHOT_MODAL_HINT);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (key.ctrl && input === "n") {
|
|
153
|
+
setMessage(undefined);
|
|
154
|
+
setHint("enter save · esc cancel");
|
|
155
|
+
setMode("new-list");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key.tab) {
|
|
159
|
+
setMessage(undefined);
|
|
160
|
+
cyclePage(key.shift ? -1 : 1);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (key.leftArrow) {
|
|
164
|
+
setFocus("nav");
|
|
165
|
+
setHint(NAV_HINT);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (key.rightArrow) {
|
|
169
|
+
setFocus("content");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (focus === "nav") {
|
|
173
|
+
if (key.upArrow) {
|
|
174
|
+
setMessage(undefined);
|
|
175
|
+
cyclePage(-1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (key.downArrow) {
|
|
179
|
+
setMessage(undefined);
|
|
180
|
+
cyclePage(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (input === "r" && page.type === "list") {
|
|
184
|
+
setMessage(undefined);
|
|
185
|
+
setHint("enter save · esc cancel");
|
|
186
|
+
setMode("nav-rename-list");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
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) => {
|
|
192
|
+
const visible = !viewingSnapshot && mode === "normal" && page.type === "list" && page.listId === list.id;
|
|
193
|
+
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
|
+
}), _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) => {
|
|
195
|
+
const list = createList(name);
|
|
196
|
+
setLists(loadLists());
|
|
197
|
+
setPage({ type: "list", listId: list.id });
|
|
198
|
+
setMode("normal");
|
|
199
|
+
} })), renamingListId !== undefined && (_jsx(AddTaskInput, { label: "Rename list: ", initialValue: lists.find((l) => l.id === renamingListId)?.name ?? "", onSubmit: (text) => {
|
|
200
|
+
const result = renameList(renamingListId, text);
|
|
201
|
+
if (result.status === "blocked") {
|
|
202
|
+
setMessage(result.reason);
|
|
203
|
+
}
|
|
204
|
+
else if (result.status === "ok") {
|
|
205
|
+
refreshLists();
|
|
206
|
+
}
|
|
207
|
+
setMode("normal");
|
|
208
|
+
setHint(NAV_HINT);
|
|
209
|
+
} }))] }));
|
|
210
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { theme } from "./theme.js";
|
|
5
|
+
export function EditTaskModal({ value, onChange, onSubmit, focus, priority, }) {
|
|
6
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.accent, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: "Edit task:" }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, focus: focus === "input" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { inverse: focus === "priority", children: [" [", priority ? "★" : " ", "] Priority "] }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { inverse: focus === "save", children: " [Save] " }), _jsx(Text, { children: " " }), _jsx(Text, { inverse: focus === "cancel", children: " [Cancel] " })] })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { Divider } from "./Divider.js";
|
|
4
|
+
import { MessageArea } from "./MessageArea.js";
|
|
5
|
+
import { ProgressBar } from "./ProgressBar.js";
|
|
6
|
+
import { Sidebar } from "./Sidebar.js";
|
|
7
|
+
import { theme } from "./theme.js";
|
|
8
|
+
const BORDER_WIDTH = 1; // each side
|
|
9
|
+
const PADDING_X = 1; // each side
|
|
10
|
+
const SIDEBAR_WIDTH = 16;
|
|
11
|
+
const TITLE = "Taskfile";
|
|
12
|
+
const TITLE_SEPARATOR = " │ ";
|
|
13
|
+
export function Frame({ children, message, hint, width, height, lists, activePage, progress, progressAnimated, navFocused, snapshotDate, }) {
|
|
14
|
+
const innerWidth = width - BORDER_WIDTH * 2 - PADDING_X * 2;
|
|
15
|
+
const contentWidth = innerWidth - SIDEBAR_WIDTH;
|
|
16
|
+
const contentPaddingLeft = 1;
|
|
17
|
+
const messageAreaWidth = contentWidth - contentPaddingLeft;
|
|
18
|
+
const titleWidth = SIDEBAR_WIDTH;
|
|
19
|
+
const progressBarWidth = Math.max(innerWidth - titleWidth - TITLE_SEPARATOR.length, 0);
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, width: width, height: height, paddingX: PADDING_X, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: theme.accent, children: TITLE.padEnd(titleWidth, " ") }), _jsx(Text, { dimColor: true, children: TITLE_SEPARATOR }), snapshotDate ? (_jsxs(Text, { bold: true, color: theme.accent, children: ["Snapshot - ", snapshotDate] })) : (_jsx(ProgressBar, { done: progress.done, total: progress.total, width: progressBarWidth, animated: progressAnimated }))] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Sidebar, { width: SIDEBAR_WIDTH, lists: lists, activePage: activePage, focused: navFocused }), _jsxs(Box, { flexDirection: "column", width: contentWidth, paddingLeft: contentPaddingLeft, children: [_jsx(MessageArea, { message: message, width: messageAreaWidth }), children] })] }), _jsx(Divider, { width: innerWidth }), _jsx(Text, { dimColor: true, children: hint || " " })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { Divider } from "./Divider.js";
|
|
4
|
+
export function MessageArea({ message, width }) {
|
|
5
|
+
return (_jsxs(Box, { height: 4, width: width, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: message || " " }), _jsx(Text, { children: " " }), _jsx(Divider, { width: width, dotted: true }), _jsx(Text, { children: " " })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { theme } from "./theme.js";
|
|
5
|
+
const WAVE_INTERVAL_MS = 120;
|
|
6
|
+
const WAVE_WIDTH = 3;
|
|
7
|
+
const WAVE_COLOR = "cyanBright"; // bright counterpart of theme.accent ("cyan")
|
|
8
|
+
const FILLED_CHAR = "▓";
|
|
9
|
+
const EMPTY_CHAR = "░";
|
|
10
|
+
export function ProgressBar({ done, total, width, animated, }) {
|
|
11
|
+
const [tick, setTick] = useState(0);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!animated)
|
|
14
|
+
return;
|
|
15
|
+
const interval = setInterval(() => setTick((t) => t + 1), WAVE_INTERVAL_MS);
|
|
16
|
+
return () => clearInterval(interval);
|
|
17
|
+
}, [animated]);
|
|
18
|
+
const percent = total === 0 ? 0 : Math.round((done / total) * 100);
|
|
19
|
+
const label = `Progress ${percent} % `;
|
|
20
|
+
const barWidth = Math.max(width - label.length, 0);
|
|
21
|
+
const filledWidth = Math.min(barWidth, Math.round((percent / 100) * barWidth));
|
|
22
|
+
const emptyWidth = barWidth - filledWidth;
|
|
23
|
+
const segments = [];
|
|
24
|
+
if (animated && filledWidth > 0) {
|
|
25
|
+
const waveCenter = tick % filledWidth;
|
|
26
|
+
const waveStart = Math.max(0, waveCenter - Math.floor(WAVE_WIDTH / 2));
|
|
27
|
+
const waveEnd = Math.min(filledWidth, waveStart + WAVE_WIDTH);
|
|
28
|
+
if (waveStart > 0)
|
|
29
|
+
segments.push({ text: FILLED_CHAR.repeat(waveStart), color: theme.accent });
|
|
30
|
+
segments.push({ text: FILLED_CHAR.repeat(waveEnd - waveStart), color: WAVE_COLOR });
|
|
31
|
+
if (filledWidth > waveEnd)
|
|
32
|
+
segments.push({ text: FILLED_CHAR.repeat(filledWidth - waveEnd), color: theme.accent });
|
|
33
|
+
}
|
|
34
|
+
else if (filledWidth > 0) {
|
|
35
|
+
segments.push({ text: FILLED_CHAR.repeat(filledWidth), color: theme.accent });
|
|
36
|
+
}
|
|
37
|
+
if (emptyWidth > 0) {
|
|
38
|
+
segments.push({ text: EMPTY_CHAR.repeat(emptyWidth), dim: true });
|
|
39
|
+
}
|
|
40
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.accent, children: label }), segments.map((segment, index) => (_jsx(Text, { color: segment.color, dimColor: segment.dim, children: segment.text }, index)))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { pruneSnapshots } from "../snapshots.js";
|
|
6
|
+
import { saveSettings, SNAPSHOT_RETENTION_OPTIONS } from "../settings.js";
|
|
7
|
+
const VIEW_HINT = "↑/k ↓/j select · enter edit · space toggle · ←/→ nav focus · Tab lists · Ctrl+N new list · q quit";
|
|
8
|
+
const EDIT_HINT = "enter save · esc cancel";
|
|
9
|
+
const RETENTION_LABELS = {
|
|
10
|
+
"1-week": "1 week",
|
|
11
|
+
"1-month": "1 month",
|
|
12
|
+
"3-months": "3 months",
|
|
13
|
+
"6-months": "6 months",
|
|
14
|
+
"12-months": "12 months",
|
|
15
|
+
forever: "Forever",
|
|
16
|
+
};
|
|
17
|
+
const FIELDS = [
|
|
18
|
+
{ key: "width", label: "Window width", type: "number" },
|
|
19
|
+
{ key: "height", label: "Window height", type: "number" },
|
|
20
|
+
{ key: "progressAnimation", label: "Progress bar animation", type: "boolean" },
|
|
21
|
+
{ key: "dailySnapshotsEnabled", label: "Daily snapshots", type: "boolean" },
|
|
22
|
+
{
|
|
23
|
+
key: "dailySnapshotRetention",
|
|
24
|
+
label: "Snapshot retention",
|
|
25
|
+
type: "select",
|
|
26
|
+
options: SNAPSHOT_RETENTION_OPTIONS,
|
|
27
|
+
formatOption: (value) => RETENTION_LABELS[value],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
export function SettingsPage({ visible, active, settings, onSettingsChange, setMessage, onNavLockChange, onHintChange, }) {
|
|
31
|
+
const { exit } = useApp();
|
|
32
|
+
const [selectedField, setSelectedField] = useState(0);
|
|
33
|
+
const [mode, setMode] = useState("view");
|
|
34
|
+
const [editValue, setEditValue] = useState("");
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
onNavLockChange(mode === "edit");
|
|
37
|
+
if (active) {
|
|
38
|
+
onHintChange(mode === "view" ? VIEW_HINT : EDIT_HINT);
|
|
39
|
+
}
|
|
40
|
+
}, [mode, active, onNavLockChange, onHintChange]);
|
|
41
|
+
useInput((input, key) => {
|
|
42
|
+
if (mode === "edit") {
|
|
43
|
+
if (key.escape) {
|
|
44
|
+
setMode("view");
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// mode === "view"
|
|
49
|
+
setMessage(undefined);
|
|
50
|
+
if (input === "q") {
|
|
51
|
+
exit();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.upArrow || input === "k") {
|
|
55
|
+
setSelectedField((index) => Math.max(index - 1, 0));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.downArrow || input === "j") {
|
|
59
|
+
setSelectedField((index) => Math.min(index + 1, FIELDS.length - 1));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const field = FIELDS[selectedField];
|
|
63
|
+
if (field.type === "boolean" && (input === " " || key.return)) {
|
|
64
|
+
const next = saveSettings({ [field.key]: !settings[field.key] });
|
|
65
|
+
onSettingsChange(next);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (field.type === "select" && (input === " " || key.return)) {
|
|
69
|
+
const currentValue = settings[field.key];
|
|
70
|
+
const currentOptionIndex = field.options.indexOf(currentValue);
|
|
71
|
+
const nextValue = field.options[(currentOptionIndex + 1) % field.options.length];
|
|
72
|
+
const next = saveSettings({ [field.key]: nextValue });
|
|
73
|
+
onSettingsChange(next);
|
|
74
|
+
pruneSnapshots(nextValue);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (key.return) {
|
|
78
|
+
setEditValue("");
|
|
79
|
+
setMode("edit");
|
|
80
|
+
}
|
|
81
|
+
}, { isActive: active });
|
|
82
|
+
function submitEdit(text) {
|
|
83
|
+
const field = FIELDS[selectedField].key;
|
|
84
|
+
const parsed = Number.parseInt(text, 10);
|
|
85
|
+
if (Number.isNaN(parsed)) {
|
|
86
|
+
setMessage("Please enter a whole number.");
|
|
87
|
+
setMode("view");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const next = saveSettings({ [field]: parsed });
|
|
91
|
+
onSettingsChange(next);
|
|
92
|
+
setMessage(next[field] === parsed ? "Saved." : `Saved (clamped to ${next[field]}).`);
|
|
93
|
+
setMode("view");
|
|
94
|
+
}
|
|
95
|
+
if (!visible)
|
|
96
|
+
return _jsx(_Fragment, {});
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Settings" }), _jsx(Text, { children: " " }), FIELDS.map((field, index) => {
|
|
98
|
+
const isSelected = visible && mode === "view" && index === selectedField;
|
|
99
|
+
const isEditing = visible && mode === "edit" && index === selectedField;
|
|
100
|
+
if (field.type === "boolean") {
|
|
101
|
+
const value = settings[field.key];
|
|
102
|
+
return (_jsx(Box, { children: _jsxs(Text, { inverse: isSelected, children: ["[", value ? "x" : " ", "] ", field.label] }) }, field.key));
|
|
103
|
+
}
|
|
104
|
+
if (field.type === "select") {
|
|
105
|
+
const value = settings[field.key];
|
|
106
|
+
return (_jsx(Box, { children: _jsxs(Text, { inverse: isSelected, children: [field.label, ": ", field.formatOption(value)] }) }, field.key));
|
|
107
|
+
}
|
|
108
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { inverse: isSelected, children: [field.label, ": "] }), isEditing ? (_jsx(TextInput, { value: editValue, onChange: setEditValue, onSubmit: submitEdit, placeholder: String(settings[field.key]) })) : (_jsx(Text, { children: settings[field.key] }))] }, field.key));
|
|
109
|
+
})] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme } from "./theme.js";
|
|
4
|
+
function NavRow({ label, active, contentWidth }) {
|
|
5
|
+
const truncated = label.length > contentWidth ? `${label.slice(0, Math.max(contentWidth - 1, 0))}…` : label;
|
|
6
|
+
const row = ` ${truncated}`.padEnd(contentWidth, " ");
|
|
7
|
+
return (_jsx(Text, { backgroundColor: active ? theme.navHighlightBg : undefined, color: active ? theme.navHighlightFg : undefined, bold: active, children: row }));
|
|
8
|
+
}
|
|
9
|
+
export function Sidebar({ width, lists, activePage, focused, }) {
|
|
10
|
+
const contentWidth = width - 1; // 1 column reserved for the right border
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, flexGrow: 1, borderStyle: "single", borderColor: focused ? theme.accent : undefined, borderTop: false, borderBottom: false, borderLeft: false, children: [lists.map((list) => (_jsx(NavRow, { label: list.name, active: activePage.type === "list" && activePage.listId === list.id, contentWidth: contentWidth }, list.id))), _jsx(Box, { flexGrow: 1 }), _jsx(NavRow, { label: "Settings", active: activePage.type === "settings", contentWidth: contentWidth })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TaskList } from "./TaskList.js";
|
|
4
|
+
export function SnapshotDetailView({ detail }) {
|
|
5
|
+
return (_jsx(Box, { flexDirection: "column", children: detail.lists.map((list, index) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, underline: true, children: list.name }), _jsx(TaskList, { tasks: list.tasks, selectedIndex: -1 })] }, index))) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme } from "./theme.js";
|
|
4
|
+
export function SnapshotsModal({ summaries, selectedIndex, modalMode, pendingDeleteId, }) {
|
|
5
|
+
const pending = summaries.find((summary) => summary.id === pendingDeleteId);
|
|
6
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.accent, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Daily Snapshots" }), _jsx(Text, { children: " " }), summaries.length === 0 && _jsx(Text, { dimColor: true, children: "No snapshots yet." }), summaries.map((summary, index) => (_jsx(Text, { inverse: index === selectedIndex, children: summary.date }, summary.id))), modalMode === "confirm-delete" && pending && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Delete snapshot from ", pending.date, "? (y/n)"] }) }))] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
const TWINKLE_FRAMES = ["yellow", "yellowBright", "white", "yellowBright"];
|
|
5
|
+
const TWINKLE_INTERVAL_MS = 400;
|
|
6
|
+
export function TaskList({ tasks, selectedIndex }) {
|
|
7
|
+
const [tick, setTick] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const interval = setInterval(() => setTick((t) => (t + 1) % TWINKLE_FRAMES.length), TWINKLE_INTERVAL_MS);
|
|
10
|
+
return () => clearInterval(interval);
|
|
11
|
+
}, []);
|
|
12
|
+
if (tasks.length === 0) {
|
|
13
|
+
return _jsx(Text, { dimColor: true, children: "No tasks yet. Press a to add one." });
|
|
14
|
+
}
|
|
15
|
+
return (_jsx(Box, { flexDirection: "column", children: tasks.map((task, index) => {
|
|
16
|
+
const marker = task.done ? "[x]" : "[ ]";
|
|
17
|
+
const selected = index === selectedIndex;
|
|
18
|
+
const label = task.parentId !== null
|
|
19
|
+
? ` - ${marker} #${task.id} ${task.text}`
|
|
20
|
+
: `${marker} #${task.id} ${task.text}`;
|
|
21
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: task.priority ? TWINKLE_FRAMES[tick] : undefined, children: task.priority ? "*" : " " }), _jsx(Text, { inverse: selected, strikethrough: task.done, children: " " + label })] }, task.id));
|
|
22
|
+
}) }));
|
|
23
|
+
}
|