@rayhanadev/katto 0.1.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 +22 -0
- package/README.md +186 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +518 -0
- package/dist/index.mjs.map +1 -0
- package/dist/sdk-JYaqBNlb.mjs +422 -0
- package/dist/sdk-JYaqBNlb.mjs.map +1 -0
- package/dist/sdk.d.mts +71 -0
- package/dist/sdk.d.mts.map +1 -0
- package/dist/sdk.mjs +2 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { a as DEFAULT_TARGETS, i as createOptions, n as freshStats, o as VERSION, r as sortedEntries, t as Katto } from "./sdk-JYaqBNlb.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { BoxRenderable, StyledText, TextAttributes, TextRenderable, bg, createCliRenderer, fg } from "@opentui/core";
|
|
6
|
+
//#region src/cli/json.ts
|
|
7
|
+
async function runJson(options) {
|
|
8
|
+
const katto = new Katto(options);
|
|
9
|
+
for await (const progress of katto.scanWithProgress()) if (options.jsonStream && progress.phase === "found") console.log(JSON.stringify({
|
|
10
|
+
type: "result",
|
|
11
|
+
result: katto.serialize(progress.entry)
|
|
12
|
+
}));
|
|
13
|
+
const entries = katto.entries;
|
|
14
|
+
if (options.deleteAll) await katto.deleteAll(entries);
|
|
15
|
+
if (options.json) console.log(JSON.stringify({
|
|
16
|
+
stats: {
|
|
17
|
+
...katto.stats,
|
|
18
|
+
elapsedMs: Date.now() - katto.stats.startedAt
|
|
19
|
+
},
|
|
20
|
+
results: katto.sort(entries).map((entry) => katto.serialize(entry))
|
|
21
|
+
}, null, 2));
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/cli/options.ts
|
|
25
|
+
const sade = createRequire(import.meta.url)("sade");
|
|
26
|
+
async function parseOptions(argv) {
|
|
27
|
+
const program = createProgram();
|
|
28
|
+
if (argv.includes("--help") || argv.includes("-h") || argv.includes("?")) {
|
|
29
|
+
program.help();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
33
|
+
console.log(`katto, ${VERSION}`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
return await new Promise((resolveOptions, rejectOptions) => {
|
|
37
|
+
program.action((directory, opts) => {
|
|
38
|
+
try {
|
|
39
|
+
resolveOptions(normalizeOptions(directory, opts));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
rejectOptions(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
program.parse([
|
|
45
|
+
"bun",
|
|
46
|
+
"katto",
|
|
47
|
+
...argv
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function createProgram() {
|
|
52
|
+
return sade("katto [directory]", true).version(VERSION).describe("Fast interactive cleanup for generated dependency/build folders.").option("-d, --directory", "Root directory to scan. Defaults to cwd.").option("-f, --full", "Scan from your home directory.", false).option("-t, --targets", "Comma-separated folder names.", DEFAULT_TARGETS.join(",")).option("-E, --exclude", "Comma-separated names or paths to prune.", "").option("-x, --exclude-sensitive", "Skip common sensitive/cache roots.", false).option("-D, --delete-all", "Delete every match after scanning.", false).option("-y, --yes", "Skip delete-all confirmation.", false).option("--dry-run", "Simulate deletion.", false).option("--json", "Print final JSON instead of the TUI.", false).option("--json-stream", "Print one JSON object per found folder.", false).option("--no-size", "Skip size calculation for maximum scan speed.", false).option("--no-stats", "Alias for --no-size.", false).option("-s, --sort", "Sort by found, size, path, or age.", "size").option("--size-strategy", "Size calculation: auto, native, js, or none.", "auto").option("--size-unit", "Display unit: auto, mb, gb, or bytes.", "auto").example("-d ~/Projects -t node_modules,.next,dist").example("--json --no-size");
|
|
53
|
+
}
|
|
54
|
+
function normalizeOptions(directory, opts) {
|
|
55
|
+
const sort = validateSort(opts.sort ?? "size");
|
|
56
|
+
const sizeStrategy = validateSizeStrategy(opts["size-strategy"] ?? opts.sizeStrategy ?? "auto");
|
|
57
|
+
const sizeUnit = validateSizeUnit(opts["size-unit"] ?? opts.sizeUnit ?? "auto");
|
|
58
|
+
const root = opts.full ? homedir() : opts.directory ?? directory ?? process.cwd();
|
|
59
|
+
if (opts.json && opts["json-stream"]) throw new Error("Cannot use both --json and --json-stream.");
|
|
60
|
+
const targets = uniqueList(splitList(opts.targets ?? DEFAULT_TARGETS.join(",")));
|
|
61
|
+
if (targets.length === 0) throw new Error("At least one target is required.");
|
|
62
|
+
return {
|
|
63
|
+
...createOptions({
|
|
64
|
+
root,
|
|
65
|
+
targets,
|
|
66
|
+
exclude: uniqueList(splitList(opts.exclude ?? "")),
|
|
67
|
+
excludeSensitive: Boolean(opts["exclude-sensitive"]),
|
|
68
|
+
dryRun: Boolean(opts["dry-run"] || opts.dryRun),
|
|
69
|
+
noSize: Boolean(opts["no-size"] || opts.noSize || opts["no-stats"] || opts.noStats || opts.size !== void 0 && !opts.size || opts.stats !== void 0 && !opts.stats),
|
|
70
|
+
sizeStrategy,
|
|
71
|
+
sort,
|
|
72
|
+
sizeUnit
|
|
73
|
+
}),
|
|
74
|
+
deleteAll: Boolean(opts["delete-all"]),
|
|
75
|
+
yes: Boolean(opts.yes),
|
|
76
|
+
json: Boolean(opts.json),
|
|
77
|
+
jsonStream: Boolean(opts["json-stream"] || opts.jsonStream)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function validateSort(value) {
|
|
81
|
+
if (value === "found" || value === "size" || value === "path" || value === "age") return value;
|
|
82
|
+
throw new Error("--sort must be one of: found, size, path, age");
|
|
83
|
+
}
|
|
84
|
+
function validateSizeUnit(value) {
|
|
85
|
+
if (value === "auto" || value === "mb" || value === "gb" || value === "bytes") return value;
|
|
86
|
+
throw new Error("--size-unit must be one of: auto, mb, gb, bytes");
|
|
87
|
+
}
|
|
88
|
+
function validateSizeStrategy(value) {
|
|
89
|
+
if (value === "auto" || value === "native" || value === "js" || value === "none") return value;
|
|
90
|
+
throw new Error("--size-strategy must be one of: auto, native, js, none");
|
|
91
|
+
}
|
|
92
|
+
function splitList(value) {
|
|
93
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
function uniqueList(values) {
|
|
96
|
+
return [...new Set(values)];
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/utils/format.ts
|
|
100
|
+
function formatSize(bytes, unit) {
|
|
101
|
+
if (bytes === null) return "pending";
|
|
102
|
+
if (unit === "bytes") return `${bytes} B`;
|
|
103
|
+
const mb = bytes / 1024 / 1024;
|
|
104
|
+
if (unit === "mb" || unit === "auto" && mb < 1024) return `${Math.round(mb)} MB`;
|
|
105
|
+
return `${(mb / 1024).toFixed(1)} GB`;
|
|
106
|
+
}
|
|
107
|
+
function formatAge(mtime) {
|
|
108
|
+
if (mtime === null) return "--";
|
|
109
|
+
const days = Math.max(0, Math.floor((Date.now() - mtime) / 864e5));
|
|
110
|
+
if (days === 0) return "today";
|
|
111
|
+
if (days === 1) return "1d";
|
|
112
|
+
return `${days}d`;
|
|
113
|
+
}
|
|
114
|
+
function truncate(value, width) {
|
|
115
|
+
if (width <= 0) return "";
|
|
116
|
+
if (value.length <= width) return value.padEnd(width);
|
|
117
|
+
if (width === 1) return value.slice(0, 1);
|
|
118
|
+
return `${value.slice(0, width - 1)}…`;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/utils/math.ts
|
|
122
|
+
function clamp(value, min, max) {
|
|
123
|
+
return value < min ? min : value > max ? max : value;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/tui/view.ts
|
|
127
|
+
function createView(renderer) {
|
|
128
|
+
const root = new BoxRenderable(renderer, {
|
|
129
|
+
width: "100%",
|
|
130
|
+
height: "100%",
|
|
131
|
+
flexDirection: "column",
|
|
132
|
+
backgroundColor: "#0F1115",
|
|
133
|
+
paddingX: 1,
|
|
134
|
+
paddingY: 1
|
|
135
|
+
});
|
|
136
|
+
const header = new TextRenderable(renderer, {
|
|
137
|
+
height: 2,
|
|
138
|
+
content: "",
|
|
139
|
+
fg: "#DDE6F2",
|
|
140
|
+
attributes: TextAttributes.BOLD
|
|
141
|
+
});
|
|
142
|
+
const summary = new TextRenderable(renderer, {
|
|
143
|
+
height: 1,
|
|
144
|
+
content: "",
|
|
145
|
+
fg: "#8FA3B8"
|
|
146
|
+
});
|
|
147
|
+
const tableHead = new TextRenderable(renderer, {
|
|
148
|
+
height: 3,
|
|
149
|
+
content: "",
|
|
150
|
+
fg: "#B8C5D6",
|
|
151
|
+
attributes: TextAttributes.BOLD
|
|
152
|
+
});
|
|
153
|
+
const body = new TextRenderable(renderer, {
|
|
154
|
+
flexGrow: 1,
|
|
155
|
+
content: "",
|
|
156
|
+
fg: "#D7DEE8"
|
|
157
|
+
});
|
|
158
|
+
const footer = new TextRenderable(renderer, {
|
|
159
|
+
height: 2,
|
|
160
|
+
content: "",
|
|
161
|
+
fg: "#8FA3B8"
|
|
162
|
+
});
|
|
163
|
+
root.add(header);
|
|
164
|
+
root.add(summary);
|
|
165
|
+
root.add(tableHead);
|
|
166
|
+
root.add(body);
|
|
167
|
+
root.add(footer);
|
|
168
|
+
renderer.root.add(root);
|
|
169
|
+
return {
|
|
170
|
+
header,
|
|
171
|
+
summary,
|
|
172
|
+
tableHead,
|
|
173
|
+
body,
|
|
174
|
+
footer
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function paint(renderer, parts, state, options) {
|
|
178
|
+
const width = Math.max(52, renderer.width - 2);
|
|
179
|
+
const height = Math.max(4, renderer.height - 11);
|
|
180
|
+
const visible = sortedEntries(state.entries, state.sort);
|
|
181
|
+
const ready = isReady(state);
|
|
182
|
+
state.selected = clamp(state.selected, 0, Math.max(0, visible.length - 1));
|
|
183
|
+
if (state.selected < state.offset) state.offset = state.selected;
|
|
184
|
+
if (state.selected >= state.offset + height) state.offset = state.selected - height + 1;
|
|
185
|
+
parts.header.content = renderHeader(options);
|
|
186
|
+
parts.summary.content = renderSummary(state, options);
|
|
187
|
+
parts.tableHead.content = renderTableHead(width);
|
|
188
|
+
parts.body.content = renderRows({
|
|
189
|
+
entries: visible,
|
|
190
|
+
state,
|
|
191
|
+
options,
|
|
192
|
+
width,
|
|
193
|
+
height,
|
|
194
|
+
ready
|
|
195
|
+
});
|
|
196
|
+
parts.footer.content = renderFooter(state, ready);
|
|
197
|
+
renderer.requestRender();
|
|
198
|
+
}
|
|
199
|
+
function isReady(state) {
|
|
200
|
+
return state.stats.done && state.entries.every((entry) => entry.status !== "sizing");
|
|
201
|
+
}
|
|
202
|
+
function renderHeader(options) {
|
|
203
|
+
const root = truncate(formatRoot(options.root), 72).trim();
|
|
204
|
+
return new StyledText([
|
|
205
|
+
text("[k] katto "),
|
|
206
|
+
fg("#7F8A99")(`(${root})`),
|
|
207
|
+
text("\nFast cleanup for generated dependency and build folders")
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
function renderSummary(state, options) {
|
|
211
|
+
const elapsed = ((Date.now() - state.stats.startedAt) / 1e3).toFixed(1);
|
|
212
|
+
const total = state.entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0);
|
|
213
|
+
return [
|
|
214
|
+
statCell("status", isReady(state) ? "ready" : "scanning"),
|
|
215
|
+
statCell("found", String(state.entries.length)),
|
|
216
|
+
statCell("sized", String(state.stats.sized)),
|
|
217
|
+
statCell("total", formatSize(total, options.sizeUnit)),
|
|
218
|
+
statCell("deleted", String(state.stats.deleted)),
|
|
219
|
+
statCell("reclaimed", formatSize(state.stats.reclaimed, options.sizeUnit)),
|
|
220
|
+
statCell("elapsed", `${elapsed}s`)
|
|
221
|
+
].join(" ");
|
|
222
|
+
}
|
|
223
|
+
function renderTableHead(width) {
|
|
224
|
+
const cols = columns(width);
|
|
225
|
+
return [
|
|
226
|
+
border("top", cols),
|
|
227
|
+
row([
|
|
228
|
+
"size",
|
|
229
|
+
"age",
|
|
230
|
+
"status",
|
|
231
|
+
"path"
|
|
232
|
+
], cols),
|
|
233
|
+
border("middle", cols)
|
|
234
|
+
].join("\n");
|
|
235
|
+
}
|
|
236
|
+
function renderRows(input) {
|
|
237
|
+
const { entries, state, options, width, height, ready } = input;
|
|
238
|
+
const cols = columns(width);
|
|
239
|
+
const rowHeight = Math.max(1, height - 1);
|
|
240
|
+
const chunks = [];
|
|
241
|
+
entries.slice(state.offset, state.offset + rowHeight).forEach((entry, index) => {
|
|
242
|
+
const absolute = state.offset + index;
|
|
243
|
+
const size = formatSize(entry.size, options.sizeUnit);
|
|
244
|
+
const age = formatAge(entry.mtime);
|
|
245
|
+
const status = entry.status;
|
|
246
|
+
const line = row([
|
|
247
|
+
size,
|
|
248
|
+
age,
|
|
249
|
+
status,
|
|
250
|
+
entry.path.replace(`${options.root}/`, "")
|
|
251
|
+
], cols);
|
|
252
|
+
chunks.push(ready && absolute === state.selected ? highlight(line) : text(line));
|
|
253
|
+
chunks.push(text("\n"));
|
|
254
|
+
});
|
|
255
|
+
if (chunks.length === 0) {
|
|
256
|
+
const message = state.stats.done ? "No target folders found." : "Waiting for matches...";
|
|
257
|
+
chunks.push(text(row([
|
|
258
|
+
"",
|
|
259
|
+
"",
|
|
260
|
+
"",
|
|
261
|
+
message
|
|
262
|
+
], cols)), text("\n"));
|
|
263
|
+
}
|
|
264
|
+
chunks.push(text(border("bottom", cols)));
|
|
265
|
+
return new StyledText(chunks);
|
|
266
|
+
}
|
|
267
|
+
function renderFooter(state, ready) {
|
|
268
|
+
if (state.confirmDeleteAll) return `Confirm delete all ${state.entries.length} matches: y confirm n cancel\n${state.message}`;
|
|
269
|
+
if (!ready) return `q quit r rescan sort ${state.sort}\nSelection unlocks after scan and metadata finish.`;
|
|
270
|
+
return `j/k move d delete D delete all s sort:${state.sort} r rescan q quit\n${state.message}`;
|
|
271
|
+
}
|
|
272
|
+
function statCell(label, value) {
|
|
273
|
+
return `${label} ${value}`;
|
|
274
|
+
}
|
|
275
|
+
function columns(width) {
|
|
276
|
+
return {
|
|
277
|
+
size: 10,
|
|
278
|
+
age: 7,
|
|
279
|
+
status: 9,
|
|
280
|
+
path: Math.max(8, width - 39)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function row(values, cols) {
|
|
284
|
+
return [
|
|
285
|
+
"│",
|
|
286
|
+
cell(values[0], cols.size),
|
|
287
|
+
"│",
|
|
288
|
+
cell(values[1], cols.age),
|
|
289
|
+
"│",
|
|
290
|
+
cell(values[2], cols.status),
|
|
291
|
+
"│",
|
|
292
|
+
cell(values[3], cols.path),
|
|
293
|
+
"│"
|
|
294
|
+
].join("");
|
|
295
|
+
}
|
|
296
|
+
function border(kind, cols) {
|
|
297
|
+
const chars = kind === "top" ? [
|
|
298
|
+
"┌",
|
|
299
|
+
"┬",
|
|
300
|
+
"┐"
|
|
301
|
+
] : kind === "middle" ? [
|
|
302
|
+
"├",
|
|
303
|
+
"┼",
|
|
304
|
+
"┤"
|
|
305
|
+
] : [
|
|
306
|
+
"└",
|
|
307
|
+
"┴",
|
|
308
|
+
"┘"
|
|
309
|
+
];
|
|
310
|
+
const segment = (width) => "─".repeat(width + 2);
|
|
311
|
+
return [
|
|
312
|
+
chars[0],
|
|
313
|
+
segment(cols.size),
|
|
314
|
+
chars[1],
|
|
315
|
+
segment(cols.age),
|
|
316
|
+
chars[1],
|
|
317
|
+
segment(cols.status),
|
|
318
|
+
chars[1],
|
|
319
|
+
segment(cols.path),
|
|
320
|
+
chars[2]
|
|
321
|
+
].join("");
|
|
322
|
+
}
|
|
323
|
+
function cell(value, width) {
|
|
324
|
+
return ` ${truncate(value, width)} `;
|
|
325
|
+
}
|
|
326
|
+
function text(value) {
|
|
327
|
+
return {
|
|
328
|
+
__isChunk: true,
|
|
329
|
+
text: value
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function highlight(value) {
|
|
333
|
+
return bg("#243447")(fg("#F8FAFC")(value));
|
|
334
|
+
}
|
|
335
|
+
function formatRoot(path) {
|
|
336
|
+
const home = homedir();
|
|
337
|
+
return path === home ? "~" : path.startsWith(`${home}/`) ? `~/${path.slice(home.length + 1)}` : path;
|
|
338
|
+
}
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/tui/app.ts
|
|
341
|
+
async function runTui(options) {
|
|
342
|
+
const renderer = await createCliRenderer({
|
|
343
|
+
exitOnCtrlC: true,
|
|
344
|
+
targetFps: 30,
|
|
345
|
+
maxFps: 30,
|
|
346
|
+
backgroundColor: "#101216"
|
|
347
|
+
});
|
|
348
|
+
const state = {
|
|
349
|
+
entries: [],
|
|
350
|
+
stats: freshStats(),
|
|
351
|
+
selected: 0,
|
|
352
|
+
offset: 0,
|
|
353
|
+
sort: options.sort,
|
|
354
|
+
message: "Scanning...",
|
|
355
|
+
confirmDeleteAll: false
|
|
356
|
+
};
|
|
357
|
+
const view = createView(renderer);
|
|
358
|
+
let renderQueued = false;
|
|
359
|
+
let scanRun = 0;
|
|
360
|
+
let katto = new Katto(options);
|
|
361
|
+
const requestPaint = () => {
|
|
362
|
+
if (renderQueued) return;
|
|
363
|
+
renderQueued = true;
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
renderQueued = false;
|
|
366
|
+
paint(renderer, view, state, options);
|
|
367
|
+
}, 66);
|
|
368
|
+
};
|
|
369
|
+
const startScan = async () => {
|
|
370
|
+
const currentRun = ++scanRun;
|
|
371
|
+
katto = new Katto(options);
|
|
372
|
+
resetForScan(state);
|
|
373
|
+
state.stats = katto.stats;
|
|
374
|
+
requestPaint();
|
|
375
|
+
try {
|
|
376
|
+
for await (const progress of katto.scanWithProgress()) onScanEntry(state, currentRun, scanRun, progress.entries, requestPaint);
|
|
377
|
+
if (currentRun !== scanRun) return;
|
|
378
|
+
state.entries = katto.entries;
|
|
379
|
+
state.stats = katto.stats;
|
|
380
|
+
state.message = `Scan complete in ${((Date.now() - state.stats.startedAt) / 1e3).toFixed(1)}s.`;
|
|
381
|
+
requestPaint();
|
|
382
|
+
await maybeDeleteAllAfterScan(katto, options, state, requestPaint);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
state.message = error instanceof Error ? error.message : String(error);
|
|
385
|
+
state.stats.done = true;
|
|
386
|
+
requestPaint();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const deleteSelected = async () => {
|
|
390
|
+
const entry = katto.sort(state.entries, state.sort)[state.selected];
|
|
391
|
+
if (!entry || entry.status === "deleted" || entry.status === "deleting") return;
|
|
392
|
+
state.message = `Deleting ${entry.path}`;
|
|
393
|
+
requestPaint();
|
|
394
|
+
if (await katto.deleteEntry(entry)) state.message = `Deleted ${entry.path}`;
|
|
395
|
+
else state.message = entry.error ?? `Failed to delete ${entry.path}`;
|
|
396
|
+
requestPaint();
|
|
397
|
+
};
|
|
398
|
+
const deleteAllEntries = async () => {
|
|
399
|
+
state.message = "Deleting all matches...";
|
|
400
|
+
await katto.deleteAll(state.entries, requestPaint);
|
|
401
|
+
state.message = "Delete-all complete.";
|
|
402
|
+
requestPaint();
|
|
403
|
+
};
|
|
404
|
+
renderer.keyInput.on("keypress", (key) => {
|
|
405
|
+
handleKey({
|
|
406
|
+
key,
|
|
407
|
+
state,
|
|
408
|
+
options,
|
|
409
|
+
rendererDestroy: () => renderer.destroy(),
|
|
410
|
+
requestPaint,
|
|
411
|
+
startScan,
|
|
412
|
+
deleteSelected,
|
|
413
|
+
deleteAllEntries
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
renderer.on("resize", requestPaint);
|
|
417
|
+
paint(renderer, view, state, options);
|
|
418
|
+
await startScan();
|
|
419
|
+
}
|
|
420
|
+
function resetForScan(state) {
|
|
421
|
+
state.entries.splice(0);
|
|
422
|
+
state.stats = freshStats();
|
|
423
|
+
state.selected = 0;
|
|
424
|
+
state.offset = 0;
|
|
425
|
+
state.message = "Scanning...";
|
|
426
|
+
state.confirmDeleteAll = false;
|
|
427
|
+
}
|
|
428
|
+
function onScanEntry(state, currentRun, scanRun, entries, requestPaint) {
|
|
429
|
+
if (currentRun !== scanRun) return;
|
|
430
|
+
state.entries = entries;
|
|
431
|
+
state.stats.found = state.entries.length;
|
|
432
|
+
state.stats.sized = state.entries.filter((item) => item.size !== null).length;
|
|
433
|
+
requestPaint();
|
|
434
|
+
}
|
|
435
|
+
async function maybeDeleteAllAfterScan(katto, options, state, requestPaint) {
|
|
436
|
+
if (!options.deleteAll) return;
|
|
437
|
+
if (!options.yes) {
|
|
438
|
+
state.confirmDeleteAll = true;
|
|
439
|
+
requestPaint();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
state.message = "Deleting all matches...";
|
|
443
|
+
await katto.deleteAll(state.entries, requestPaint);
|
|
444
|
+
state.message = "Delete-all complete.";
|
|
445
|
+
requestPaint();
|
|
446
|
+
}
|
|
447
|
+
function handleKey(input) {
|
|
448
|
+
const { key, state, options, rendererDestroy, requestPaint, startScan, deleteSelected, deleteAllEntries } = input;
|
|
449
|
+
if (key.name === "q" || key.name === "escape") {
|
|
450
|
+
rendererDestroy();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (state.confirmDeleteAll) {
|
|
454
|
+
handleDeleteAllConfirm(key, state, options, requestPaint, deleteAllEntries);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (key.name === "r") {
|
|
458
|
+
startScan();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!isReady(state)) return;
|
|
462
|
+
switch (key.name) {
|
|
463
|
+
case "down":
|
|
464
|
+
case "j":
|
|
465
|
+
state.selected++;
|
|
466
|
+
requestPaint();
|
|
467
|
+
break;
|
|
468
|
+
case "up":
|
|
469
|
+
case "k":
|
|
470
|
+
state.selected--;
|
|
471
|
+
requestPaint();
|
|
472
|
+
break;
|
|
473
|
+
case "d":
|
|
474
|
+
deleteSelected();
|
|
475
|
+
break;
|
|
476
|
+
case "D":
|
|
477
|
+
state.confirmDeleteAll = true;
|
|
478
|
+
requestPaint();
|
|
479
|
+
break;
|
|
480
|
+
case "s":
|
|
481
|
+
state.sort = state.sort === "found" ? "size" : state.sort === "size" ? "path" : state.sort === "path" ? "age" : "found";
|
|
482
|
+
requestPaint();
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function handleDeleteAllConfirm(key, state, options, requestPaint, deleteAllEntries) {
|
|
487
|
+
if (key.name === "y") {
|
|
488
|
+
state.confirmDeleteAll = false;
|
|
489
|
+
deleteAllEntries();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (key.name === "n") {
|
|
493
|
+
state.confirmDeleteAll = false;
|
|
494
|
+
options.deleteAll = false;
|
|
495
|
+
state.message = "Delete-all cancelled.";
|
|
496
|
+
requestPaint();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/index.ts
|
|
501
|
+
async function main() {
|
|
502
|
+
try {
|
|
503
|
+
const options = await parseOptions(Bun.argv.slice(2));
|
|
504
|
+
if (options.json || options.jsonStream) {
|
|
505
|
+
await runJson(options);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
await runTui(options);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
await main();
|
|
515
|
+
//#endregion
|
|
516
|
+
export {};
|
|
517
|
+
|
|
518
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/cli/json.ts","../src/cli/options.ts","../src/utils/format.ts","../src/utils/math.ts","../src/tui/view.ts","../src/tui/app.ts","../src/index.ts"],"sourcesContent":["import type { CliOptions } from \"../types\";\n\nimport { Katto } from \"../sdk\";\n\nexport async function runJson(options: CliOptions): Promise<void> {\n const katto = new Katto(options);\n for await (const progress of katto.scanWithProgress()) {\n if (options.jsonStream && progress.phase === \"found\") {\n console.log(JSON.stringify({ type: \"result\", result: katto.serialize(progress.entry) }));\n }\n }\n\n const entries = katto.entries;\n if (options.deleteAll) await katto.deleteAll(entries);\n\n if (options.json) {\n console.log(\n JSON.stringify(\n {\n stats: { ...katto.stats, elapsedMs: Date.now() - katto.stats.startedAt },\n results: katto.sort(entries).map((entry) => katto.serialize(entry)),\n },\n null,\n 2,\n ),\n );\n }\n}\n","import { createRequire } from \"node:module\";\nimport { homedir } from \"node:os\";\n\nimport type { CliOptions, SizeStrategy, SizeUnit, SortMode } from \"../types\";\n\nimport { DEFAULT_TARGETS, VERSION } from \"../constants\";\nimport { createOptions } from \"../core/options\";\n\nconst require = createRequire(import.meta.url);\nconst sade = require(\"sade\") as typeof import(\"sade\");\n\ninterface SadeOptions {\n directory?: string;\n full?: boolean;\n targets?: string;\n exclude?: string;\n \"exclude-sensitive\"?: boolean;\n \"delete-all\"?: boolean;\n yes?: boolean;\n size?: boolean;\n stats?: boolean;\n \"dry-run\"?: boolean;\n dryRun?: boolean;\n json?: boolean;\n \"json-stream\"?: boolean;\n jsonStream?: boolean;\n \"no-size\"?: boolean;\n noSize?: boolean;\n \"no-stats\"?: boolean;\n noStats?: boolean;\n sort?: string;\n \"size-strategy\"?: string;\n sizeStrategy?: string;\n \"size-unit\"?: string;\n sizeUnit?: string;\n}\n\nexport async function parseOptions(argv: string[]): Promise<CliOptions> {\n const program = createProgram();\n\n if (argv.includes(\"--help\") || argv.includes(\"-h\") || argv.includes(\"?\")) {\n program.help();\n process.exit(0);\n }\n\n if (argv.includes(\"--version\") || argv.includes(\"-v\")) {\n console.log(`katto, ${VERSION}`);\n process.exit(0);\n }\n\n return await new Promise((resolveOptions, rejectOptions) => {\n program.action((directory: string | undefined, opts: SadeOptions) => {\n try {\n resolveOptions(normalizeOptions(directory, opts));\n } catch (error) {\n rejectOptions(error);\n }\n });\n\n program.parse([\"bun\", \"katto\", ...argv]);\n });\n}\n\nfunction createProgram(): import(\"sade\").Sade {\n return sade(\"katto [directory]\", true)\n .version(VERSION)\n .describe(\"Fast interactive cleanup for generated dependency/build folders.\")\n .option(\"-d, --directory\", \"Root directory to scan. Defaults to cwd.\")\n .option(\"-f, --full\", \"Scan from your home directory.\", false)\n .option(\"-t, --targets\", \"Comma-separated folder names.\", DEFAULT_TARGETS.join(\",\"))\n .option(\"-E, --exclude\", \"Comma-separated names or paths to prune.\", \"\")\n .option(\"-x, --exclude-sensitive\", \"Skip common sensitive/cache roots.\", false)\n .option(\"-D, --delete-all\", \"Delete every match after scanning.\", false)\n .option(\"-y, --yes\", \"Skip delete-all confirmation.\", false)\n .option(\"--dry-run\", \"Simulate deletion.\", false)\n .option(\"--json\", \"Print final JSON instead of the TUI.\", false)\n .option(\"--json-stream\", \"Print one JSON object per found folder.\", false)\n .option(\"--no-size\", \"Skip size calculation for maximum scan speed.\", false)\n .option(\"--no-stats\", \"Alias for --no-size.\", false)\n .option(\"-s, --sort\", \"Sort by found, size, path, or age.\", \"size\")\n .option(\"--size-strategy\", \"Size calculation: auto, native, js, or none.\", \"auto\")\n .option(\"--size-unit\", \"Display unit: auto, mb, gb, or bytes.\", \"auto\")\n .example(\"-d ~/Projects -t node_modules,.next,dist\")\n .example(\"--json --no-size\");\n}\n\nfunction normalizeOptions(directory: string | undefined, opts: SadeOptions): CliOptions {\n const sort = validateSort(opts.sort ?? \"size\");\n const sizeStrategy = validateSizeStrategy(opts[\"size-strategy\"] ?? opts.sizeStrategy ?? \"auto\");\n const sizeUnit = validateSizeUnit(opts[\"size-unit\"] ?? opts.sizeUnit ?? \"auto\");\n const root = opts.full ? homedir() : (opts.directory ?? directory ?? process.cwd());\n\n if (opts.json && opts[\"json-stream\"]) {\n throw new Error(\"Cannot use both --json and --json-stream.\");\n }\n\n const targets = uniqueList(splitList(opts.targets ?? DEFAULT_TARGETS.join(\",\")));\n if (targets.length === 0) throw new Error(\"At least one target is required.\");\n\n const options = createOptions({\n root,\n targets,\n exclude: uniqueList(splitList(opts.exclude ?? \"\")),\n excludeSensitive: Boolean(opts[\"exclude-sensitive\"]),\n dryRun: Boolean(opts[\"dry-run\"] || opts.dryRun),\n noSize: Boolean(\n opts[\"no-size\"] ||\n opts.noSize ||\n opts[\"no-stats\"] ||\n opts.noStats ||\n (opts.size !== undefined && !opts.size) ||\n (opts.stats !== undefined && !opts.stats),\n ),\n sizeStrategy,\n sort,\n sizeUnit,\n });\n\n return {\n ...options,\n deleteAll: Boolean(opts[\"delete-all\"]),\n yes: Boolean(opts.yes),\n json: Boolean(opts.json),\n jsonStream: Boolean(opts[\"json-stream\"] || opts.jsonStream),\n };\n}\n\nfunction validateSort(value: string): SortMode {\n if (value === \"found\" || value === \"size\" || value === \"path\" || value === \"age\") return value;\n throw new Error(\"--sort must be one of: found, size, path, age\");\n}\n\nfunction validateSizeUnit(value: string): SizeUnit {\n if (value === \"auto\" || value === \"mb\" || value === \"gb\" || value === \"bytes\") return value;\n throw new Error(\"--size-unit must be one of: auto, mb, gb, bytes\");\n}\n\nfunction validateSizeStrategy(value: string): SizeStrategy {\n if (value === \"auto\" || value === \"native\" || value === \"js\" || value === \"none\") return value;\n throw new Error(\"--size-strategy must be one of: auto, native, js, none\");\n}\n\nfunction splitList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction uniqueList(values: string[]): string[] {\n return [...new Set(values)];\n}\n","import type { SizeUnit } from \"../types\";\n\nexport function formatSize(bytes: number | null, unit: SizeUnit): string {\n if (bytes === null) return \"pending\";\n if (unit === \"bytes\") return `${bytes} B`;\n\n const mb = bytes / 1024 / 1024;\n if (unit === \"mb\" || (unit === \"auto\" && mb < 1024)) return `${Math.round(mb)} MB`;\n\n return `${(mb / 1024).toFixed(1)} GB`;\n}\n\nexport function formatAge(mtime: number | null): string {\n if (mtime === null) return \"--\";\n\n const days = Math.max(0, Math.floor((Date.now() - mtime) / 86_400_000));\n if (days === 0) return \"today\";\n if (days === 1) return \"1d\";\n\n return `${days}d`;\n}\n\nexport function truncate(value: string, width: number): string {\n if (width <= 0) return \"\";\n if (value.length <= width) return value.padEnd(width);\n if (width === 1) return value.slice(0, 1);\n\n return `${value.slice(0, width - 1)}…`;\n}\n","export function clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n","import {\n BoxRenderable,\n StyledText,\n TextAttributes,\n TextRenderable,\n bg,\n fg,\n type CliRenderer,\n type TextChunk,\n} from \"@opentui/core\";\nimport { homedir } from \"node:os\";\n\nimport type { Entry, Options, TuiState } from \"../types\";\n\nimport { sortedEntries } from \"../core/results\";\nimport { formatAge, formatSize, truncate } from \"../utils/format\";\nimport { clamp } from \"../utils/math\";\n\ninterface ViewParts {\n header: TextRenderable;\n summary: TextRenderable;\n tableHead: TextRenderable;\n body: TextRenderable;\n footer: TextRenderable;\n}\n\nexport function createView(renderer: CliRenderer): ViewParts {\n const root = new BoxRenderable(renderer, {\n width: \"100%\",\n height: \"100%\",\n flexDirection: \"column\",\n backgroundColor: \"#0F1115\",\n paddingX: 1,\n paddingY: 1,\n });\n\n const header = new TextRenderable(renderer, {\n height: 2,\n content: \"\",\n fg: \"#DDE6F2\",\n attributes: TextAttributes.BOLD,\n });\n const summary = new TextRenderable(renderer, {\n height: 1,\n content: \"\",\n fg: \"#8FA3B8\",\n });\n const tableHead = new TextRenderable(renderer, {\n height: 3,\n content: \"\",\n fg: \"#B8C5D6\",\n attributes: TextAttributes.BOLD,\n });\n const body = new TextRenderable(renderer, {\n flexGrow: 1,\n content: \"\",\n fg: \"#D7DEE8\",\n });\n const footer = new TextRenderable(renderer, {\n height: 2,\n content: \"\",\n fg: \"#8FA3B8\",\n });\n\n root.add(header);\n root.add(summary);\n root.add(tableHead);\n root.add(body);\n root.add(footer);\n renderer.root.add(root);\n\n return { header, summary, tableHead, body, footer };\n}\n\nexport function paint(\n renderer: CliRenderer,\n parts: ViewParts,\n state: TuiState,\n options: Options,\n): void {\n const width = Math.max(52, renderer.width - 2);\n const height = Math.max(4, renderer.height - 11);\n const visible = sortedEntries(state.entries, state.sort);\n const ready = isReady(state);\n\n state.selected = clamp(state.selected, 0, Math.max(0, visible.length - 1));\n if (state.selected < state.offset) state.offset = state.selected;\n if (state.selected >= state.offset + height) state.offset = state.selected - height + 1;\n\n parts.header.content = renderHeader(options);\n parts.summary.content = renderSummary(state, options);\n parts.tableHead.content = renderTableHead(width);\n parts.body.content = renderRows({\n entries: visible,\n state,\n options,\n width,\n height,\n ready,\n });\n parts.footer.content = renderFooter(state, ready);\n renderer.requestRender();\n}\n\nexport function isReady(state: TuiState): boolean {\n return state.stats.done && state.entries.every((entry) => entry.status !== \"sizing\");\n}\n\nfunction renderHeader(options: Options): StyledText {\n const root = truncate(formatRoot(options.root), 72).trim();\n return new StyledText([\n text(\"[k] katto \"),\n fg(\"#7F8A99\")(`(${root})`),\n text(\"\\nFast cleanup for generated dependency and build folders\"),\n ]);\n}\n\nfunction renderSummary(state: TuiState, options: Options): string {\n const elapsed = ((Date.now() - state.stats.startedAt) / 1000).toFixed(1);\n const total = state.entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0);\n const status = isReady(state) ? \"ready\" : \"scanning\";\n\n return [\n statCell(\"status\", status),\n statCell(\"found\", String(state.entries.length)),\n statCell(\"sized\", String(state.stats.sized)),\n statCell(\"total\", formatSize(total, options.sizeUnit)),\n statCell(\"deleted\", String(state.stats.deleted)),\n statCell(\"reclaimed\", formatSize(state.stats.reclaimed, options.sizeUnit)),\n statCell(\"elapsed\", `${elapsed}s`),\n ].join(\" \");\n}\n\nfunction renderTableHead(width: number): string {\n const cols = columns(width);\n return [\n border(\"top\", cols),\n row([\"size\", \"age\", \"status\", \"path\"], cols),\n border(\"middle\", cols),\n ].join(\"\\n\");\n}\n\nfunction renderRows(input: {\n entries: Entry[];\n state: TuiState;\n options: Options;\n width: number;\n height: number;\n ready: boolean;\n}): StyledText {\n const { entries, state, options, width, height, ready } = input;\n const cols = columns(width);\n const rowHeight = Math.max(1, height - 1);\n const chunks: TextChunk[] = [];\n const rows = entries.slice(state.offset, state.offset + rowHeight);\n\n rows.forEach((entry, index) => {\n const absolute = state.offset + index;\n const size = formatSize(entry.size, options.sizeUnit);\n const age = formatAge(entry.mtime);\n const status = entry.status;\n const relativePath = entry.path.replace(`${options.root}/`, \"\");\n const line = row([size, age, status, relativePath], cols);\n\n chunks.push(ready && absolute === state.selected ? highlight(line) : text(line));\n chunks.push(text(\"\\n\"));\n });\n\n if (chunks.length === 0) {\n const message = state.stats.done ? \"No target folders found.\" : \"Waiting for matches...\";\n chunks.push(text(row([\"\", \"\", \"\", message], cols)), text(\"\\n\"));\n }\n\n chunks.push(text(border(\"bottom\", cols)));\n return new StyledText(chunks);\n}\n\nfunction renderFooter(state: TuiState, ready: boolean): string {\n if (state.confirmDeleteAll) {\n return `Confirm delete all ${state.entries.length} matches: y confirm n cancel\\n${state.message}`;\n }\n\n if (!ready) {\n return `q quit r rescan sort ${state.sort}\\nSelection unlocks after scan and metadata finish.`;\n }\n\n return `j/k move d delete D delete all s sort:${state.sort} r rescan q quit\\n${state.message}`;\n}\n\nfunction statCell(label: string, value: string): string {\n return `${label} ${value}`;\n}\n\nfunction columns(width: number) {\n const size = 10;\n const age = 7;\n const status = 9;\n const separators = 5;\n const cellPadding = 8;\n const fixed = size + age + status + separators + cellPadding;\n return {\n size,\n age,\n status,\n path: Math.max(8, width - fixed),\n };\n}\n\nfunction row(values: [string, string, string, string], cols: ReturnType<typeof columns>): string {\n return [\n \"│\",\n cell(values[0], cols.size),\n \"│\",\n cell(values[1], cols.age),\n \"│\",\n cell(values[2], cols.status),\n \"│\",\n cell(values[3], cols.path),\n \"│\",\n ].join(\"\");\n}\n\nfunction border(kind: \"top\" | \"middle\" | \"bottom\", cols: ReturnType<typeof columns>): string {\n const chars =\n kind === \"top\" ? [\"┌\", \"┬\", \"┐\"] : kind === \"middle\" ? [\"├\", \"┼\", \"┤\"] : [\"└\", \"┴\", \"┘\"];\n const segment = (width: number): string => \"─\".repeat(width + 2);\n\n return [\n chars[0],\n segment(cols.size),\n chars[1],\n segment(cols.age),\n chars[1],\n segment(cols.status),\n chars[1],\n segment(cols.path),\n chars[2],\n ].join(\"\");\n}\n\nfunction cell(value: string, width: number): string {\n return ` ${truncate(value, width)} `;\n}\n\nfunction text(value: string): TextChunk {\n return {\n __isChunk: true,\n text: value,\n };\n}\n\nfunction highlight(value: string): TextChunk {\n return bg(\"#243447\")(fg(\"#F8FAFC\")(value));\n}\n\nfunction formatRoot(path: string): string {\n const home = homedir();\n return path === home\n ? \"~\"\n : path.startsWith(`${home}/`)\n ? `~/${path.slice(home.length + 1)}`\n : path;\n}\n","import { createCliRenderer, type KeyEvent } from \"@opentui/core\";\n\nimport type { CliOptions, TuiState } from \"../types\";\n\nimport { RENDER_INTERVAL_MS } from \"../constants\";\nimport { freshStats } from \"../core/stats\";\nimport { Katto } from \"../sdk\";\nimport { createView, isReady, paint } from \"./view\";\n\nexport async function runTui(options: CliOptions): Promise<void> {\n const renderer = await createCliRenderer({\n exitOnCtrlC: true,\n targetFps: 30,\n maxFps: 30,\n backgroundColor: \"#101216\",\n });\n\n const state: TuiState = {\n entries: [],\n stats: freshStats(),\n selected: 0,\n offset: 0,\n sort: options.sort,\n message: \"Scanning...\",\n confirmDeleteAll: false,\n };\n\n const view = createView(renderer);\n let renderQueued = false;\n let scanRun = 0;\n let katto = new Katto(options);\n\n const requestPaint = (): void => {\n if (renderQueued) return;\n renderQueued = true;\n setTimeout(() => {\n renderQueued = false;\n paint(renderer, view, state, options);\n }, RENDER_INTERVAL_MS);\n };\n\n const startScan = async (): Promise<void> => {\n const currentRun = ++scanRun;\n katto = new Katto(options);\n resetForScan(state);\n state.stats = katto.stats;\n requestPaint();\n\n try {\n for await (const progress of katto.scanWithProgress()) {\n onScanEntry(state, currentRun, scanRun, progress.entries, requestPaint);\n }\n if (currentRun !== scanRun) return;\n\n state.entries = katto.entries;\n state.stats = katto.stats;\n state.message = `Scan complete in ${((Date.now() - state.stats.startedAt) / 1000).toFixed(1)}s.`;\n requestPaint();\n\n await maybeDeleteAllAfterScan(katto, options, state, requestPaint);\n } catch (error) {\n state.message = error instanceof Error ? error.message : String(error);\n state.stats.done = true;\n requestPaint();\n }\n };\n\n const deleteSelected = async (): Promise<void> => {\n const entry = katto.sort(state.entries, state.sort)[state.selected];\n if (!entry || entry.status === \"deleted\" || entry.status === \"deleting\") return;\n state.message = `Deleting ${entry.path}`;\n requestPaint();\n\n const ok = await katto.deleteEntry(entry);\n if (ok) {\n state.message = `Deleted ${entry.path}`;\n } else {\n state.message = entry.error ?? `Failed to delete ${entry.path}`;\n }\n\n requestPaint();\n };\n\n const deleteAllEntries = async (): Promise<void> => {\n state.message = \"Deleting all matches...\";\n await katto.deleteAll(state.entries, requestPaint);\n state.message = \"Delete-all complete.\";\n requestPaint();\n };\n\n renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n handleKey({\n key,\n state,\n options,\n rendererDestroy: () => renderer.destroy(),\n requestPaint,\n startScan,\n deleteSelected,\n deleteAllEntries,\n });\n });\n\n renderer.on(\"resize\", requestPaint);\n paint(renderer, view, state, options);\n await startScan();\n}\n\nfunction resetForScan(state: TuiState): void {\n state.entries.splice(0);\n state.stats = freshStats();\n state.selected = 0;\n state.offset = 0;\n state.message = \"Scanning...\";\n state.confirmDeleteAll = false;\n}\n\nfunction onScanEntry(\n state: TuiState,\n currentRun: number,\n scanRun: number,\n entries: TuiState[\"entries\"],\n requestPaint: () => void,\n): void {\n if (currentRun !== scanRun) return;\n state.entries = entries;\n state.stats.found = state.entries.length;\n state.stats.sized = state.entries.filter((item) => item.size !== null).length;\n requestPaint();\n}\n\nasync function maybeDeleteAllAfterScan(\n katto: Katto,\n options: CliOptions,\n state: TuiState,\n requestPaint: () => void,\n): Promise<void> {\n if (!options.deleteAll) return;\n\n if (!options.yes) {\n state.confirmDeleteAll = true;\n requestPaint();\n return;\n }\n\n state.message = \"Deleting all matches...\";\n await katto.deleteAll(state.entries, requestPaint);\n state.message = \"Delete-all complete.\";\n requestPaint();\n}\n\nfunction handleKey(input: {\n key: KeyEvent;\n state: TuiState;\n options: CliOptions;\n rendererDestroy: () => void;\n requestPaint: () => void;\n startScan: () => Promise<void>;\n deleteSelected: () => Promise<void>;\n deleteAllEntries: () => Promise<void>;\n}): void {\n const {\n key,\n state,\n options,\n rendererDestroy,\n requestPaint,\n startScan,\n deleteSelected,\n deleteAllEntries,\n } = input;\n\n if (key.name === \"q\" || key.name === \"escape\") {\n rendererDestroy();\n return;\n }\n\n if (state.confirmDeleteAll) {\n handleDeleteAllConfirm(key, state, options, requestPaint, deleteAllEntries);\n return;\n }\n\n if (key.name === \"r\") {\n void startScan();\n return;\n }\n\n if (!isReady(state)) return;\n\n switch (key.name) {\n case \"down\":\n case \"j\":\n state.selected++;\n requestPaint();\n break;\n case \"up\":\n case \"k\":\n state.selected--;\n requestPaint();\n break;\n case \"d\":\n void deleteSelected();\n break;\n case \"D\":\n state.confirmDeleteAll = true;\n requestPaint();\n break;\n case \"s\":\n state.sort =\n state.sort === \"found\"\n ? \"size\"\n : state.sort === \"size\"\n ? \"path\"\n : state.sort === \"path\"\n ? \"age\"\n : \"found\";\n requestPaint();\n break;\n }\n}\n\nfunction handleDeleteAllConfirm(\n key: KeyEvent,\n state: TuiState,\n options: CliOptions,\n requestPaint: () => void,\n deleteAllEntries: () => Promise<void>,\n): void {\n if (key.name === \"y\") {\n state.confirmDeleteAll = false;\n void deleteAllEntries();\n return;\n }\n\n if (key.name === \"n\") {\n state.confirmDeleteAll = false;\n options.deleteAll = false;\n state.message = \"Delete-all cancelled.\";\n requestPaint();\n }\n}\n","#!/usr/bin/env bun\nimport { runJson } from \"./cli/json\";\nimport { parseOptions } from \"./cli/options\";\nimport { runTui } from \"./tui/app\";\n\nasync function main(): Promise<void> {\n try {\n const options = await parseOptions(Bun.argv.slice(2));\n if (options.json || options.jsonStream) {\n await runJson(options);\n return;\n }\n\n await runTui(options);\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n }\n}\n\nawait main();\n"],"mappings":"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"}
|