termido 1.0.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/package.json +19 -0
- package/termido.js +578 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "termido",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A terminal todo manager with priorities, timestamps and search",
|
|
5
|
+
"main": "termido.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["todo", "terminal", "tui", "cli", "task-manager", "blessed"],
|
|
10
|
+
"author": "Saifur Rahman",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"bin": {
|
|
14
|
+
"termido": "./termido.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"blessed": "^0.1.81"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/termido.js
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const blessed = require("blessed");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
// ---------------- FILE ----------------
|
|
9
|
+
const DATA_DIR = path.join(os.homedir(), ".termido");
|
|
10
|
+
const FILE = path.join(DATA_DIR, "todos.json");
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
13
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadTodos() {
|
|
17
|
+
try {
|
|
18
|
+
const data = fs.readFileSync(FILE, "utf-8");
|
|
19
|
+
const parsed = JSON.parse(data);
|
|
20
|
+
return parsed.map(item => {
|
|
21
|
+
if (typeof item === "string") return { text: item, done: false, priority: "none", createdAt: null };
|
|
22
|
+
return {
|
|
23
|
+
text: item.text || "",
|
|
24
|
+
done: item.done || false,
|
|
25
|
+
priority: item.priority || "none",
|
|
26
|
+
createdAt: item.createdAt || null,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveTodos(list) {
|
|
35
|
+
fs.writeFileSync(FILE, JSON.stringify(list, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------- SCREEN ----------------
|
|
39
|
+
const screen = blessed.screen({
|
|
40
|
+
smartCSR: true,
|
|
41
|
+
title: "TermiDo",
|
|
42
|
+
terminal: "xterm-256color"
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
screen.key(["q", "C-c"], () => {
|
|
46
|
+
screen.destroy();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ---------------- STATE ----------------
|
|
51
|
+
let todos = loadTodos();
|
|
52
|
+
let filteredTodos = [...todos];
|
|
53
|
+
let selectedIndex = 0;
|
|
54
|
+
let mode = "normal";
|
|
55
|
+
// modes: normal | add | edit | search | priority | filter
|
|
56
|
+
let inputBuffer = "";
|
|
57
|
+
let activeFilter = "all"; // all | high | medium | low | none | done | pending
|
|
58
|
+
let pendingPriorityFor = null; // "add" | "edit" — which flow triggered priority pick
|
|
59
|
+
|
|
60
|
+
// ---------------- PRIORITY HELPERS ----------------
|
|
61
|
+
const PRIORITIES = ["high", "medium", "low", "none"];
|
|
62
|
+
|
|
63
|
+
const PRIORITY_COLOR = {
|
|
64
|
+
high: "red",
|
|
65
|
+
medium: "yellow",
|
|
66
|
+
low: "green",
|
|
67
|
+
none: "gray",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const PRIORITY_ICON = {
|
|
71
|
+
high: "!!",
|
|
72
|
+
medium: "! ",
|
|
73
|
+
low: ". ",
|
|
74
|
+
none: " ",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function timeAgo(iso) {
|
|
78
|
+
if (!iso) return "no date";
|
|
79
|
+
const diff = Math.floor((Date.now() - new Date(iso)) / 1000); // seconds
|
|
80
|
+
if (diff < 60) return `${diff}s ago`;
|
|
81
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
82
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
83
|
+
if (diff < 7 * 86400) return `${Math.floor(diff / 86400)}d ago`;
|
|
84
|
+
if (diff < 30 * 86400) return `${Math.floor(diff / (7 * 86400))}w ago`;
|
|
85
|
+
if (diff < 365 * 86400) return `${Math.floor(diff / (30 * 86400))}mo ago`;
|
|
86
|
+
return `${Math.floor(diff / (365 * 86400))}y ago`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------- HEADER ----------------
|
|
90
|
+
const header = blessed.box({
|
|
91
|
+
top: 0,
|
|
92
|
+
height: 3,
|
|
93
|
+
width: "100%",
|
|
94
|
+
content: "TermiDo",
|
|
95
|
+
align: "center",
|
|
96
|
+
valign: "middle",
|
|
97
|
+
border: { type: "line" },
|
|
98
|
+
style: { bg: "#000", fg: "white" }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------- SIDEBAR ----------------
|
|
102
|
+
const sidebar = blessed.box({
|
|
103
|
+
top: 3,
|
|
104
|
+
left: 0,
|
|
105
|
+
width: "25%",
|
|
106
|
+
bottom: 0,
|
|
107
|
+
border: { type: "line" },
|
|
108
|
+
label: " Stats ",
|
|
109
|
+
style: { border: { fg: "yellow" } }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function renderSidebar() {
|
|
113
|
+
const total = todos.length;
|
|
114
|
+
const done = todos.filter(t => t.done).length;
|
|
115
|
+
const pending = total - done;
|
|
116
|
+
const high = todos.filter(t => t.priority === "high").length;
|
|
117
|
+
const medium = todos.filter(t => t.priority === "medium").length;
|
|
118
|
+
const low = todos.filter(t => t.priority === "low").length;
|
|
119
|
+
|
|
120
|
+
const filterLabel = activeFilter === "all" ? "all" : activeFilter;
|
|
121
|
+
|
|
122
|
+
sidebar.setContent(
|
|
123
|
+
`\n Total: ${total}\n` +
|
|
124
|
+
` ✔ Done: ${done}\n` +
|
|
125
|
+
` ☐ Pending: ${pending}\n` +
|
|
126
|
+
`\n── Priority ──\n` +
|
|
127
|
+
` !! High: ${high}\n` +
|
|
128
|
+
` ! Medium: ${medium}\n` +
|
|
129
|
+
` . Low: ${low}\n` +
|
|
130
|
+
`\n── Filter ──\n` +
|
|
131
|
+
` F [${filterLabel}]\n` +
|
|
132
|
+
`\n── Keys ──\n` +
|
|
133
|
+
` / Search\n` +
|
|
134
|
+
` A Add\n` +
|
|
135
|
+
` E Edit\n` +
|
|
136
|
+
` P Priority\n` +
|
|
137
|
+
` Spc Toggle\n` +
|
|
138
|
+
` D Delete\n` +
|
|
139
|
+
` Q Quit`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
// ---------------- MAIN LIST ----------------
|
|
146
|
+
const list = blessed.list({
|
|
147
|
+
top: 3,
|
|
148
|
+
left: "25%",
|
|
149
|
+
width: "75%",
|
|
150
|
+
bottom: 4,
|
|
151
|
+
border: { type: "line" },
|
|
152
|
+
label: " Todos ",
|
|
153
|
+
tags: true,
|
|
154
|
+
scrollable: true,
|
|
155
|
+
alwaysScroll: true,
|
|
156
|
+
wrap: false,
|
|
157
|
+
scrollbar: {
|
|
158
|
+
ch: "█",
|
|
159
|
+
style: { fg: "cyan" }
|
|
160
|
+
},
|
|
161
|
+
style: {
|
|
162
|
+
selected: { bg: "blue", fg: "white" },
|
|
163
|
+
border: { fg: "cyan" }
|
|
164
|
+
},
|
|
165
|
+
keys: true,
|
|
166
|
+
mouse: true,
|
|
167
|
+
items: []
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------- PRIORITY PICKER OVERLAY ----------------
|
|
171
|
+
const priorityBox = blessed.list({
|
|
172
|
+
top: "center",
|
|
173
|
+
left: "center",
|
|
174
|
+
width: 30,
|
|
175
|
+
height: 7,
|
|
176
|
+
border: { type: "line" },
|
|
177
|
+
label: " Set Priority ",
|
|
178
|
+
tags: true,
|
|
179
|
+
keys: true,
|
|
180
|
+
mouse: true,
|
|
181
|
+
hidden: true,
|
|
182
|
+
style: {
|
|
183
|
+
selected: { bg: "blue", fg: "white" },
|
|
184
|
+
border: { fg: "magenta" }
|
|
185
|
+
},
|
|
186
|
+
items: [
|
|
187
|
+
"{red-fg}!! High{/}",
|
|
188
|
+
"{yellow-fg} ! Medium{/}",
|
|
189
|
+
"{green-fg} . Low{/}",
|
|
190
|
+
"{gray-fg} None{/}",
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---------------- FILTER PICKER OVERLAY ----------------
|
|
195
|
+
const filterBox = blessed.list({
|
|
196
|
+
top: "center",
|
|
197
|
+
left: "center",
|
|
198
|
+
width: 30,
|
|
199
|
+
height: 13,
|
|
200
|
+
border: { type: "line" },
|
|
201
|
+
label: " Filter By ",
|
|
202
|
+
tags: true,
|
|
203
|
+
keys: true,
|
|
204
|
+
mouse: true,
|
|
205
|
+
hidden: true,
|
|
206
|
+
style: {
|
|
207
|
+
selected: { bg: "blue", fg: "white" },
|
|
208
|
+
border: { fg: "cyan" }
|
|
209
|
+
},
|
|
210
|
+
items: [
|
|
211
|
+
" All",
|
|
212
|
+
"{red-fg}!! High priority{/}",
|
|
213
|
+
"{yellow-fg} ! Medium priority{/}",
|
|
214
|
+
"{green-fg} . Low priority{/}",
|
|
215
|
+
" No priority",
|
|
216
|
+
" ✔ Done",
|
|
217
|
+
" ☐ Pending",
|
|
218
|
+
" Cancel",
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ---------------- BOTTOM INPUT PANEL ----------------
|
|
223
|
+
const inputPanel = blessed.box({
|
|
224
|
+
bottom: 0,
|
|
225
|
+
left: "25%",
|
|
226
|
+
width: "75%",
|
|
227
|
+
height: 4,
|
|
228
|
+
border: { type: "line" },
|
|
229
|
+
label: " Ready ",
|
|
230
|
+
style: { border: { fg: "gray" } }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const inputDisplay = blessed.text({
|
|
234
|
+
parent: inputPanel,
|
|
235
|
+
top: 0,
|
|
236
|
+
left: 1,
|
|
237
|
+
right: 1,
|
|
238
|
+
height: 1,
|
|
239
|
+
content: "",
|
|
240
|
+
style: { fg: "white" },
|
|
241
|
+
wrap: false,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
function setInputPanelMode(label, color, prompt) {
|
|
245
|
+
inputPanel.setLabel(` ${label} `);
|
|
246
|
+
inputPanel.style.border.fg = color;
|
|
247
|
+
inputDisplay.setContent(prompt);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getVisibleBuffer(buf, maxWidth) {
|
|
251
|
+
if (buf.length <= maxWidth) return buf;
|
|
252
|
+
return "…" + buf.slice(-(maxWidth - 1));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderInputPanel() {
|
|
256
|
+
const screenW = (screen.width && screen.width > 10) ? screen.width : 80;
|
|
257
|
+
const panelWidth = Math.max(10, Math.floor(screenW * 0.75) - 6);
|
|
258
|
+
|
|
259
|
+
if (mode === "normal") {
|
|
260
|
+
setInputPanelMode("Ready", "gray",
|
|
261
|
+
"↑↓ Nav | Spc Toggle | D Del | E Edit | P Priority | F Filter | / Search | A Add | Q Quit");
|
|
262
|
+
} else if (mode === "add") {
|
|
263
|
+
setInputPanelMode("Add Todo [Enter] confirm [Esc] cancel", "green",
|
|
264
|
+
"> " + getVisibleBuffer(inputBuffer, panelWidth) + "█");
|
|
265
|
+
} else if (mode === "edit") {
|
|
266
|
+
setInputPanelMode("Edit Todo [Enter] confirm [Esc] cancel", "cyan",
|
|
267
|
+
"> " + getVisibleBuffer(inputBuffer, panelWidth) + "█");
|
|
268
|
+
} else if (mode === "search") {
|
|
269
|
+
setInputPanelMode("Search [Enter] jump [Esc] cancel", "yellow",
|
|
270
|
+
"/ " + getVisibleBuffer(inputBuffer, panelWidth) + "█");
|
|
271
|
+
} else if (mode === "priority") {
|
|
272
|
+
setInputPanelMode("Priority", "magenta", "↑↓ pick priority [Enter] confirm [Esc] cancel");
|
|
273
|
+
} else if (mode === "filter") {
|
|
274
|
+
setInputPanelMode("Filter", "cyan", "↑↓ pick filter [Enter] apply [Esc] cancel");
|
|
275
|
+
}
|
|
276
|
+
screen.render();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------- DATA HELPERS ----------------
|
|
280
|
+
function getActiveData() {
|
|
281
|
+
const base = mode === "search" ? filteredTodos : todos;
|
|
282
|
+
if (activeFilter === "all") return base;
|
|
283
|
+
return base.filter(t => {
|
|
284
|
+
if (activeFilter === "done") return t.done;
|
|
285
|
+
if (activeFilter === "pending") return !t.done;
|
|
286
|
+
return t.priority === activeFilter;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------- RENDER TODOS ----------------
|
|
291
|
+
function wrapText(text, maxWidth) {
|
|
292
|
+
const words = text.split(" ");
|
|
293
|
+
const lines = [];
|
|
294
|
+
let current = "";
|
|
295
|
+
for (const word of words) {
|
|
296
|
+
if ((current + (current ? " " : "") + word).length > maxWidth) {
|
|
297
|
+
if (current) lines.push(current);
|
|
298
|
+
let w = word;
|
|
299
|
+
while (w.length > maxWidth) {
|
|
300
|
+
lines.push(w.slice(0, maxWidth));
|
|
301
|
+
w = w.slice(maxWidth);
|
|
302
|
+
}
|
|
303
|
+
current = w;
|
|
304
|
+
} else {
|
|
305
|
+
current = current ? current + " " + word : word;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (current) lines.push(current);
|
|
309
|
+
return lines;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let lineToTodo = [];
|
|
313
|
+
|
|
314
|
+
function renderTodos() {
|
|
315
|
+
const data = getActiveData();
|
|
316
|
+
const screenW = (screen.width && screen.width > 10) ? screen.width : 80;
|
|
317
|
+
// subtract: border(2) + scrollbar(1) + padding(2) + priority icon(3)
|
|
318
|
+
const availWidth = Math.floor(screenW * 0.75) - 8;
|
|
319
|
+
const textWidth = Math.max(10, availWidth);
|
|
320
|
+
|
|
321
|
+
const items = [];
|
|
322
|
+
lineToTodo = [];
|
|
323
|
+
|
|
324
|
+
const filterSuffix = activeFilter !== "all" ? ` [${activeFilter}]` : "";
|
|
325
|
+
list.setLabel(` Todos${filterSuffix} `);
|
|
326
|
+
|
|
327
|
+
if (!data.length) {
|
|
328
|
+
items.push(" (No todos)");
|
|
329
|
+
lineToTodo.push(0);
|
|
330
|
+
} else {
|
|
331
|
+
data.forEach((t, i) => {
|
|
332
|
+
const doneIcon = t.done ? "✔" : "☐";
|
|
333
|
+
const pri = t.priority || "none";
|
|
334
|
+
const priIcon = PRIORITY_ICON[pri];
|
|
335
|
+
const color = PRIORITY_COLOR[pri];
|
|
336
|
+
const lines = wrapText(t.text, textWidth);
|
|
337
|
+
|
|
338
|
+
const age = timeAgo(t.createdAt);
|
|
339
|
+
const ageStr = age.padEnd(8); // fixed width so text aligns
|
|
340
|
+
lines.forEach((line, li) => {
|
|
341
|
+
if (li === 0) {
|
|
342
|
+
items.push(`{${color}-fg}${priIcon}{/} {gray-fg}${ageStr}{/} ${doneIcon} ${line}`);
|
|
343
|
+
} else {
|
|
344
|
+
items.push(` ${line}`); // indent continuation lines
|
|
345
|
+
}
|
|
346
|
+
lineToTodo.push(i);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
list.setItems(items);
|
|
352
|
+
const displayLine = lineToTodo.indexOf(selectedIndex);
|
|
353
|
+
list.select(displayLine >= 0 ? displayLine : 0);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function renderAll() {
|
|
357
|
+
renderSidebar();
|
|
358
|
+
renderTodos();
|
|
359
|
+
renderInputPanel();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---------------- GLOBAL KEYPRESS ----------------
|
|
363
|
+
screen.on("keypress", (ch, key) => {
|
|
364
|
+
if (mode === "normal" || mode === "priority" || mode === "filter") return;
|
|
365
|
+
|
|
366
|
+
const k = key.name;
|
|
367
|
+
|
|
368
|
+
if (k === "escape") {
|
|
369
|
+
if (mode === "search") filteredTodos = [...todos];
|
|
370
|
+
mode = "normal";
|
|
371
|
+
inputBuffer = "";
|
|
372
|
+
pendingPriorityFor = null;
|
|
373
|
+
list.focus();
|
|
374
|
+
renderAll();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (k === "enter") {
|
|
379
|
+
if (mode === "add") {
|
|
380
|
+
const val = inputBuffer.trim();
|
|
381
|
+
if (val) {
|
|
382
|
+
const newTodo = {
|
|
383
|
+
text: val,
|
|
384
|
+
done: false,
|
|
385
|
+
priority: "none",
|
|
386
|
+
createdAt: new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
todos.push(newTodo);
|
|
389
|
+
selectedIndex = todos.length - 1;
|
|
390
|
+
saveTodos(todos);
|
|
391
|
+
// after adding, open priority picker
|
|
392
|
+
inputBuffer = "";
|
|
393
|
+
mode = "priority";
|
|
394
|
+
pendingPriorityFor = "add";
|
|
395
|
+
priorityBox.show();
|
|
396
|
+
priorityBox.select(3); // default to "none"
|
|
397
|
+
priorityBox.focus();
|
|
398
|
+
renderAll();
|
|
399
|
+
screen.render();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
} else if (mode === "edit") {
|
|
403
|
+
const val = inputBuffer.trim();
|
|
404
|
+
if (val) {
|
|
405
|
+
todos[selectedIndex].text = val;
|
|
406
|
+
saveTodos(todos);
|
|
407
|
+
}
|
|
408
|
+
} else if (mode === "search") {
|
|
409
|
+
const matched = filteredTodos[selectedIndex];
|
|
410
|
+
if (matched) {
|
|
411
|
+
const realIdx = todos.findIndex(t => t === matched);
|
|
412
|
+
selectedIndex = realIdx !== -1 ? realIdx : 0;
|
|
413
|
+
}
|
|
414
|
+
filteredTodos = [...todos];
|
|
415
|
+
}
|
|
416
|
+
mode = "normal";
|
|
417
|
+
inputBuffer = "";
|
|
418
|
+
list.focus();
|
|
419
|
+
renderAll();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (k === "backspace") {
|
|
424
|
+
inputBuffer = inputBuffer.slice(0, -1);
|
|
425
|
+
} else if (ch && !key.ctrl && !key.meta) {
|
|
426
|
+
inputBuffer += ch;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (mode === "search") {
|
|
430
|
+
filteredTodos = todos.filter(t =>
|
|
431
|
+
t.text.toLowerCase().includes(inputBuffer.toLowerCase())
|
|
432
|
+
);
|
|
433
|
+
selectedIndex = 0;
|
|
434
|
+
renderTodos();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
renderInputPanel();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ---------------- PRIORITY PICKER HANDLER ----------------
|
|
441
|
+
priorityBox.key("enter", () => {
|
|
442
|
+
const i = priorityBox.selected;
|
|
443
|
+
const picked = PRIORITIES[i]; // high=0 medium=1 low=2 none=3
|
|
444
|
+
if (picked !== undefined) {
|
|
445
|
+
todos[selectedIndex].priority = picked;
|
|
446
|
+
saveTodos(todos);
|
|
447
|
+
}
|
|
448
|
+
priorityBox.hide();
|
|
449
|
+
mode = "normal";
|
|
450
|
+
pendingPriorityFor = null;
|
|
451
|
+
list.focus();
|
|
452
|
+
renderAll();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
priorityBox.key("escape", () => {
|
|
456
|
+
priorityBox.hide();
|
|
457
|
+
mode = "normal";
|
|
458
|
+
pendingPriorityFor = null;
|
|
459
|
+
list.focus();
|
|
460
|
+
renderAll();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// ---------------- FILTER PICKER HANDLER ----------------
|
|
464
|
+
const FILTER_VALUES = ["all", "high", "medium", "low", "none", "done", "pending", null];
|
|
465
|
+
|
|
466
|
+
filterBox.key("enter", () => {
|
|
467
|
+
const picked = FILTER_VALUES[filterBox.selected];
|
|
468
|
+
if (picked !== null) {
|
|
469
|
+
activeFilter = picked;
|
|
470
|
+
selectedIndex = 0;
|
|
471
|
+
}
|
|
472
|
+
filterBox.hide();
|
|
473
|
+
mode = "normal";
|
|
474
|
+
list.focus();
|
|
475
|
+
renderAll();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
filterBox.key("escape", () => {
|
|
479
|
+
filterBox.hide();
|
|
480
|
+
mode = "normal";
|
|
481
|
+
list.focus();
|
|
482
|
+
renderAll();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ---------------- NORMAL MODE KEYS ----------------
|
|
486
|
+
screen.key("a", () => {
|
|
487
|
+
if (mode !== "normal") return;
|
|
488
|
+
mode = "add";
|
|
489
|
+
inputBuffer = "";
|
|
490
|
+
renderInputPanel();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
list.key("e", () => {
|
|
494
|
+
if (mode !== "normal" || !todos.length) return;
|
|
495
|
+
mode = "edit";
|
|
496
|
+
inputBuffer = todos[selectedIndex].text;
|
|
497
|
+
renderInputPanel();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
screen.key("/", () => {
|
|
501
|
+
if (mode !== "normal") return;
|
|
502
|
+
mode = "search";
|
|
503
|
+
inputBuffer = "";
|
|
504
|
+
filteredTodos = [...todos];
|
|
505
|
+
renderInputPanel();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
list.key("p", () => {
|
|
509
|
+
if (mode !== "normal" || !todos.length) return;
|
|
510
|
+
mode = "priority";
|
|
511
|
+
const cur = PRIORITIES.indexOf(todos[selectedIndex].priority || "none");
|
|
512
|
+
priorityBox.select(cur >= 0 ? cur : 3);
|
|
513
|
+
priorityBox.show();
|
|
514
|
+
priorityBox.focus();
|
|
515
|
+
renderInputPanel();
|
|
516
|
+
screen.render();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
screen.key("f", () => {
|
|
520
|
+
if (mode !== "normal") return;
|
|
521
|
+
mode = "filter";
|
|
522
|
+
const cur = FILTER_VALUES.indexOf(activeFilter);
|
|
523
|
+
filterBox.select(cur >= 0 ? cur : 0);
|
|
524
|
+
filterBox.show();
|
|
525
|
+
filterBox.focus();
|
|
526
|
+
renderInputPanel();
|
|
527
|
+
screen.render();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
list.key("space", () => {
|
|
531
|
+
if (mode !== "normal" || !todos.length) return;
|
|
532
|
+
const data = getActiveData();
|
|
533
|
+
const real = todos.findIndex(t => t === data[selectedIndex]);
|
|
534
|
+
if (real !== -1) {
|
|
535
|
+
todos[real].done = !todos[real].done;
|
|
536
|
+
saveTodos(todos);
|
|
537
|
+
}
|
|
538
|
+
renderAll();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
list.key("d", () => {
|
|
542
|
+
if (mode !== "normal" || !todos.length) return;
|
|
543
|
+
const data = getActiveData();
|
|
544
|
+
const real = todos.findIndex(t => t === data[selectedIndex]);
|
|
545
|
+
if (real !== -1) todos.splice(real, 1);
|
|
546
|
+
saveTodos(todos);
|
|
547
|
+
if (selectedIndex >= getActiveData().length) selectedIndex = Math.max(0, getActiveData().length - 1);
|
|
548
|
+
renderAll();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ---------------- NAV ----------------
|
|
552
|
+
list.key(["up", "k"], () => {
|
|
553
|
+
if (mode !== "normal" && mode !== "search") return;
|
|
554
|
+
if (selectedIndex > 0) selectedIndex--;
|
|
555
|
+
renderTodos();
|
|
556
|
+
screen.render();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
list.key(["down", "j"], () => {
|
|
560
|
+
if (mode !== "normal" && mode !== "search") return;
|
|
561
|
+
const data = getActiveData();
|
|
562
|
+
if (selectedIndex < data.length - 1) selectedIndex++;
|
|
563
|
+
renderTodos();
|
|
564
|
+
screen.render();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// ---------------- BUILD ----------------
|
|
568
|
+
screen.append(header);
|
|
569
|
+
screen.append(sidebar);
|
|
570
|
+
screen.append(list);
|
|
571
|
+
screen.append(inputPanel);
|
|
572
|
+
screen.append(priorityBox);
|
|
573
|
+
screen.append(filterBox);
|
|
574
|
+
|
|
575
|
+
setImmediate(() => {
|
|
576
|
+
renderAll();
|
|
577
|
+
list.focus();
|
|
578
|
+
});
|