@xxkeefer/mrkl 0.4.0 → 0.4.1
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/dist/chunks/list-tui.mjs +252 -0
- package/dist/cli.mjs +55 -7
- package/package.json +2 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Fzf } from 'fzf';
|
|
2
|
+
|
|
3
|
+
const ESC = "\x1B";
|
|
4
|
+
const CSI = `${ESC}[`;
|
|
5
|
+
const ALT_SCREEN_ON = `${CSI}?1049h`;
|
|
6
|
+
const ALT_SCREEN_OFF = `${CSI}?1049l`;
|
|
7
|
+
const CURSOR_HIDE = `${CSI}?25l`;
|
|
8
|
+
const CURSOR_SHOW = `${CSI}?25h`;
|
|
9
|
+
const CLEAR_SCREEN = `${CSI}2J${CSI}H`;
|
|
10
|
+
const BOLD = `${CSI}1m`;
|
|
11
|
+
const UNDERLINE = `${CSI}4m`;
|
|
12
|
+
const RESET = `${CSI}0m`;
|
|
13
|
+
const INVERSE = `${CSI}7m`;
|
|
14
|
+
const FG_CYAN = `${CSI}36m`;
|
|
15
|
+
const FG_YELLOW = `${CSI}33m`;
|
|
16
|
+
const FG_GREEN = `${CSI}32m`;
|
|
17
|
+
const FG_RED = `${CSI}31m`;
|
|
18
|
+
const FG_GRAY = `${CSI}90m`;
|
|
19
|
+
function statusColor(status) {
|
|
20
|
+
switch (status) {
|
|
21
|
+
case "todo":
|
|
22
|
+
return FG_YELLOW;
|
|
23
|
+
case "in-progress":
|
|
24
|
+
return FG_CYAN;
|
|
25
|
+
case "done":
|
|
26
|
+
return FG_GREEN;
|
|
27
|
+
case "closed":
|
|
28
|
+
return FG_RED;
|
|
29
|
+
default:
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function buildEntries(tasks) {
|
|
34
|
+
return tasks.map((task) => ({
|
|
35
|
+
task,
|
|
36
|
+
searchText: `${task.id} ${task.type} ${task.status} ${task.title} ${task.description}`
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
async function interactiveList(tasks, archivedTasks) {
|
|
40
|
+
const { stdin, stdout } = process;
|
|
41
|
+
const datasets = [
|
|
42
|
+
{ label: "Tasks", entries: buildEntries(tasks) },
|
|
43
|
+
{ label: "Archive", entries: buildEntries(archivedTasks) }
|
|
44
|
+
];
|
|
45
|
+
let activeTab = 0;
|
|
46
|
+
let query = "";
|
|
47
|
+
let selectedIndex = 0;
|
|
48
|
+
let scrollOffset = 0;
|
|
49
|
+
function getFiltered() {
|
|
50
|
+
const entries = datasets[activeTab].entries;
|
|
51
|
+
if (!query) return entries;
|
|
52
|
+
const fzf = new Fzf(entries, { selector: (e) => e.searchText });
|
|
53
|
+
return fzf.find(query).map((r) => r.item);
|
|
54
|
+
}
|
|
55
|
+
function getTermSize() {
|
|
56
|
+
return { cols: stdout.columns || 80, rows: stdout.rows || 24 };
|
|
57
|
+
}
|
|
58
|
+
function render() {
|
|
59
|
+
const { cols, rows } = getTermSize();
|
|
60
|
+
const filtered = getFiltered();
|
|
61
|
+
const buf = [];
|
|
62
|
+
const tabParts = datasets.map((ds, i) => {
|
|
63
|
+
if (i === activeTab) {
|
|
64
|
+
return `${FG_CYAN}${BOLD}[${ds.label}]${RESET}`;
|
|
65
|
+
}
|
|
66
|
+
return `${FG_GRAY} ${ds.label} ${RESET}`;
|
|
67
|
+
});
|
|
68
|
+
buf.push(tabParts.join(" "));
|
|
69
|
+
buf.push("");
|
|
70
|
+
buf.push(`${FG_CYAN}>${RESET} ${query}${UNDERLINE} ${RESET}`);
|
|
71
|
+
const listWidth = Math.floor(cols * 0.55);
|
|
72
|
+
const previewWidth = cols - listWidth - 3;
|
|
73
|
+
buf.push(`${FG_GRAY}${"\u2500".repeat(listWidth)}\u252C${"\u2500".repeat(previewWidth + 2)}${RESET}`);
|
|
74
|
+
const headerLine = formatRow("ID", "TYPE", "STATUS", "TITLE", listWidth);
|
|
75
|
+
buf.push(`${BOLD}${headerLine}${RESET}${FG_GRAY}\u2502${RESET}${BOLD} Preview${RESET}`);
|
|
76
|
+
buf.push(`${FG_GRAY}${"\u2500".repeat(listWidth)}\u253C${"\u2500".repeat(previewWidth + 2)}${RESET}`);
|
|
77
|
+
const contentRows = rows - 9;
|
|
78
|
+
const maxVisible = Math.max(1, contentRows);
|
|
79
|
+
if (filtered.length === 0) {
|
|
80
|
+
selectedIndex = 0;
|
|
81
|
+
} else {
|
|
82
|
+
selectedIndex = Math.min(selectedIndex, filtered.length - 1);
|
|
83
|
+
selectedIndex = Math.max(selectedIndex, 0);
|
|
84
|
+
}
|
|
85
|
+
if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
|
|
86
|
+
if (selectedIndex >= scrollOffset + maxVisible) scrollOffset = selectedIndex - maxVisible + 1;
|
|
87
|
+
const selectedTask = filtered[selectedIndex]?.task;
|
|
88
|
+
const previewLines = buildPreviewLines(selectedTask, previewWidth);
|
|
89
|
+
for (let i = 0; i < maxVisible; i++) {
|
|
90
|
+
const taskIdx = scrollOffset + i;
|
|
91
|
+
const entry = filtered[taskIdx];
|
|
92
|
+
let leftPart;
|
|
93
|
+
if (!entry) {
|
|
94
|
+
leftPart = " ".repeat(listWidth);
|
|
95
|
+
} else {
|
|
96
|
+
const isSelected = taskIdx === selectedIndex;
|
|
97
|
+
const row = formatRow(entry.task.id, entry.task.type, entry.task.status, entry.task.title, listWidth);
|
|
98
|
+
if (isSelected) {
|
|
99
|
+
leftPart = `${INVERSE}${row}${RESET}`;
|
|
100
|
+
} else {
|
|
101
|
+
const sc = statusColor(entry.task.status);
|
|
102
|
+
leftPart = colorizeRow(entry.task.id, entry.task.type, entry.task.status, entry.task.title, listWidth, sc);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const rightPart = previewLines[i] ?? "";
|
|
106
|
+
buf.push(`${leftPart}${FG_GRAY}\u2502${RESET} ${rightPart}`);
|
|
107
|
+
}
|
|
108
|
+
buf.push(`${FG_GRAY}${"\u2500".repeat(listWidth)}\u2534${"\u2500".repeat(previewWidth + 2)}${RESET}`);
|
|
109
|
+
const countInfo = `${filtered.length}/${datasets[activeTab].entries.length}`;
|
|
110
|
+
buf.push(`${FG_GRAY}${countInfo} tasks \u2191\u2193: navigate Tab: switch Enter: select Esc: quit Type to search${RESET}`);
|
|
111
|
+
stdout.write(CLEAR_SCREEN + buf.join("\n"));
|
|
112
|
+
}
|
|
113
|
+
function formatRow(id, type, status, title, width) {
|
|
114
|
+
const idCol = id.padEnd(14);
|
|
115
|
+
const typeCol = type.padEnd(12);
|
|
116
|
+
const statusCol = status.padEnd(14);
|
|
117
|
+
const usedWidth = 14 + 12 + 14;
|
|
118
|
+
const titleWidth = Math.max(1, width - usedWidth);
|
|
119
|
+
const titleCol = title.length > titleWidth ? title.slice(0, titleWidth - 1) + "\u2026" : title.padEnd(titleWidth);
|
|
120
|
+
return `${idCol}${typeCol}${statusCol}${titleCol}`;
|
|
121
|
+
}
|
|
122
|
+
function colorizeRow(id, type, status, title, width, sc) {
|
|
123
|
+
const idCol = id.padEnd(14);
|
|
124
|
+
const typeCol = type.padEnd(12);
|
|
125
|
+
const statusCol = status.padEnd(14);
|
|
126
|
+
const usedWidth = 14 + 12 + 14;
|
|
127
|
+
const titleWidth = Math.max(1, width - usedWidth);
|
|
128
|
+
const titleCol = title.length > titleWidth ? title.slice(0, titleWidth - 1) + "\u2026" : title.padEnd(titleWidth);
|
|
129
|
+
return `${FG_CYAN}${idCol}${RESET}${typeCol}${sc}${statusCol}${RESET}${titleCol}`;
|
|
130
|
+
}
|
|
131
|
+
function buildPreviewLines(task, width) {
|
|
132
|
+
if (!task) return [];
|
|
133
|
+
const lines = [];
|
|
134
|
+
lines.push(`${BOLD}${task.id}${RESET} ${FG_GRAY}${task.type}${RESET} ${statusColor(task.status)}${task.status}${RESET}`);
|
|
135
|
+
lines.push(`${BOLD}${task.title}${RESET}`);
|
|
136
|
+
lines.push("");
|
|
137
|
+
if (task.description) {
|
|
138
|
+
lines.push(`${UNDERLINE}Description${RESET}`);
|
|
139
|
+
for (const line of wrapText(task.description, width)) {
|
|
140
|
+
lines.push(line);
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
if (task.acceptance_criteria.length > 0) {
|
|
145
|
+
lines.push(`${UNDERLINE}Acceptance Criteria${RESET}`);
|
|
146
|
+
for (const ac of task.acceptance_criteria) {
|
|
147
|
+
for (const line of wrapText(`- [ ] ${ac}`, width)) {
|
|
148
|
+
lines.push(line);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return lines;
|
|
153
|
+
}
|
|
154
|
+
function wrapText(text, width) {
|
|
155
|
+
if (width <= 0) return [text];
|
|
156
|
+
const result = [];
|
|
157
|
+
for (const rawLine of text.split("\n")) {
|
|
158
|
+
const words = rawLine.split(" ");
|
|
159
|
+
let current = "";
|
|
160
|
+
for (const word of words) {
|
|
161
|
+
if (current.length + word.length + 1 > width && current.length > 0) {
|
|
162
|
+
result.push(current);
|
|
163
|
+
current = word;
|
|
164
|
+
} else {
|
|
165
|
+
current = current ? `${current} ${word}` : word;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (current) result.push(current);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
173
|
+
stdin.resume();
|
|
174
|
+
stdin.setEncoding("utf-8");
|
|
175
|
+
stdout.write(ALT_SCREEN_ON + CURSOR_HIDE);
|
|
176
|
+
render();
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
function cleanup() {
|
|
179
|
+
stdout.write(CURSOR_SHOW + ALT_SCREEN_OFF);
|
|
180
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
181
|
+
stdin.pause();
|
|
182
|
+
stdin.removeListener("data", onData);
|
|
183
|
+
}
|
|
184
|
+
function onData(data) {
|
|
185
|
+
const filtered = getFiltered();
|
|
186
|
+
for (let i = 0; i < data.length; i++) {
|
|
187
|
+
const ch = data[i];
|
|
188
|
+
if (ch === ESC) {
|
|
189
|
+
if (data[i + 1] === "[") {
|
|
190
|
+
const arrow = data[i + 2];
|
|
191
|
+
if (arrow === "A") {
|
|
192
|
+
if (selectedIndex > 0) selectedIndex--;
|
|
193
|
+
i += 2;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (arrow === "B") {
|
|
197
|
+
if (selectedIndex < filtered.length - 1) selectedIndex++;
|
|
198
|
+
i += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
cleanup();
|
|
205
|
+
resolve();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (ch === "") {
|
|
209
|
+
cleanup();
|
|
210
|
+
resolve();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (ch === " ") {
|
|
214
|
+
activeTab = (activeTab + 1) % datasets.length;
|
|
215
|
+
query = "";
|
|
216
|
+
selectedIndex = 0;
|
|
217
|
+
scrollOffset = 0;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (ch === "\r" || ch === "\n") {
|
|
221
|
+
cleanup();
|
|
222
|
+
const selected = getFiltered()[selectedIndex];
|
|
223
|
+
if (selected) {
|
|
224
|
+
process.stdout.write(`
|
|
225
|
+
Selected: ${selected.task.id} - ${selected.task.title}
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
resolve();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
232
|
+
if (query.length > 0) {
|
|
233
|
+
query = query.slice(0, -1);
|
|
234
|
+
selectedIndex = 0;
|
|
235
|
+
scrollOffset = 0;
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (ch >= " " && ch <= "~") {
|
|
240
|
+
query += ch;
|
|
241
|
+
selectedIndex = 0;
|
|
242
|
+
scrollOffset = 0;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
render();
|
|
246
|
+
}
|
|
247
|
+
stdin.on("data", onData);
|
|
248
|
+
stdout.on("resize", () => render());
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { interactiveList };
|
package/dist/cli.mjs
CHANGED
|
@@ -231,6 +231,25 @@ function executePrune(dir, filenames) {
|
|
|
231
231
|
unlinkSync(join(archiveDir, f));
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
function listArchivedTasks(filter) {
|
|
235
|
+
const config = loadConfig(filter.dir);
|
|
236
|
+
const archiveDir = join(filter.dir, config.tasks_dir, ".archive");
|
|
237
|
+
if (!existsSync(archiveDir)) return [];
|
|
238
|
+
const files = readdirSync(archiveDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
239
|
+
let tasks = files.flatMap((f) => {
|
|
240
|
+
try {
|
|
241
|
+
const content = readFileSync(join(archiveDir, f), "utf-8");
|
|
242
|
+
const task = parse(content, f);
|
|
243
|
+
if (!task.id || !task.type || !task.status) return [];
|
|
244
|
+
return [task];
|
|
245
|
+
} catch {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
if (filter.type) tasks = tasks.filter((t) => t.type === filter.type);
|
|
250
|
+
if (filter.status) tasks = tasks.filter((t) => t.status === filter.status);
|
|
251
|
+
return tasks;
|
|
252
|
+
}
|
|
234
253
|
function closeTask(dir, id) {
|
|
235
254
|
const config = loadConfig(dir);
|
|
236
255
|
const tasksDir = join(dir, config.tasks_dir);
|
|
@@ -357,6 +376,12 @@ const createCommand = defineCommand({
|
|
|
357
376
|
}
|
|
358
377
|
});
|
|
359
378
|
|
|
379
|
+
const COL_ID = 14;
|
|
380
|
+
const COL_TYPE = 12;
|
|
381
|
+
const COL_STATUS = 14;
|
|
382
|
+
function formatRow(id, type, status, title) {
|
|
383
|
+
return `${id.padEnd(COL_ID)}${type.padEnd(COL_TYPE)}${status.padEnd(COL_STATUS)}${title}`;
|
|
384
|
+
}
|
|
360
385
|
const listCommand = defineCommand({
|
|
361
386
|
meta: {
|
|
362
387
|
name: "list",
|
|
@@ -372,23 +397,46 @@ const listCommand = defineCommand({
|
|
|
372
397
|
type: "string",
|
|
373
398
|
alias: "s",
|
|
374
399
|
description: "Filter by status (todo, in-progress, done)"
|
|
400
|
+
},
|
|
401
|
+
plain: {
|
|
402
|
+
type: "boolean",
|
|
403
|
+
alias: "p",
|
|
404
|
+
description: "Plain text output (no interactive TUI)"
|
|
375
405
|
}
|
|
376
406
|
},
|
|
377
|
-
run({ args }) {
|
|
407
|
+
async run({ args }) {
|
|
378
408
|
const dir = process.cwd();
|
|
379
409
|
try {
|
|
380
|
-
const
|
|
410
|
+
const filter = {
|
|
381
411
|
dir,
|
|
382
412
|
type: args.type,
|
|
383
413
|
status: args.status
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
414
|
+
};
|
|
415
|
+
const tasks = listTasks(filter);
|
|
416
|
+
const archivedTasks = listArchivedTasks(filter);
|
|
417
|
+
if (tasks.length === 0 && archivedTasks.length === 0) {
|
|
418
|
+
consola.info("No tasks found");
|
|
387
419
|
return;
|
|
388
420
|
}
|
|
389
|
-
|
|
390
|
-
|
|
421
|
+
const usePlain = args.plain || !process.stdout.isTTY;
|
|
422
|
+
if (usePlain) {
|
|
423
|
+
consola.log(formatRow("ID", "TYPE", "STATUS", "TITLE"));
|
|
424
|
+
consola.log("\u2500".repeat(60));
|
|
425
|
+
for (const task of tasks) {
|
|
426
|
+
consola.log(formatRow(task.id, task.type, task.status, task.title));
|
|
427
|
+
}
|
|
428
|
+
if (archivedTasks.length > 0) {
|
|
429
|
+
consola.log("");
|
|
430
|
+
consola.log(`Archive (${archivedTasks.length}):`);
|
|
431
|
+
consola.log("\u2500".repeat(60));
|
|
432
|
+
for (const task of archivedTasks) {
|
|
433
|
+
consola.log(formatRow(task.id, task.type, task.status, task.title));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
391
437
|
}
|
|
438
|
+
const { interactiveList } = await import('./chunks/list-tui.mjs');
|
|
439
|
+
await interactiveList(tasks, archivedTasks);
|
|
392
440
|
} catch (err) {
|
|
393
441
|
consola.error(String(err.message));
|
|
394
442
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xxkeefer/mrkl",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightweight CLI tool for structured markdown task tracking",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"citty": "^0.1.6",
|
|
30
30
|
"consola": "^3.4.0",
|
|
31
|
+
"fzf": "^0.5.2",
|
|
31
32
|
"gray-matter": "^4.0.3",
|
|
32
33
|
"smol-toml": "^1.3.1"
|
|
33
34
|
},
|