@suds-cli/filetree 0.1.0-alpha.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/dist/index.js ADDED
@@ -0,0 +1,461 @@
1
+ import { newBinding, matches } from '@suds-cli/key';
2
+ import { WindowSizeMsg, KeyMsg } from '@suds-cli/tea';
3
+ import { Style } from '@suds-cli/chapstick';
4
+
5
+ // src/model.ts
6
+ var defaultKeyMap = {
7
+ down: newBinding({ keys: ["j", "down", "ctrl+n"] }).withHelp("j/\u2193", "down"),
8
+ up: newBinding({ keys: ["k", "up", "ctrl+p"] }).withHelp("k/\u2191", "up")
9
+ };
10
+ function defaultStyles() {
11
+ return {
12
+ selectedItem: new Style().foreground("#00ff00").bold(true),
13
+ normalItem: new Style()
14
+ };
15
+ }
16
+ function mergeStyles(overrides) {
17
+ if (!overrides) {
18
+ return defaultStyles();
19
+ }
20
+ return { ...defaultStyles(), ...overrides };
21
+ }
22
+
23
+ // src/messages.ts
24
+ var GetDirectoryListingMsg = class {
25
+ constructor(items) {
26
+ this.items = items;
27
+ }
28
+ _tag = "filetree-get-directory-listing";
29
+ };
30
+ var ErrorMsg = class {
31
+ constructor(error) {
32
+ this.error = error;
33
+ }
34
+ _tag = "filetree-error";
35
+ };
36
+
37
+ // src/fs.ts
38
+ function convertBytesToSizeString(size) {
39
+ if (size < 1024) {
40
+ return `${size}B`;
41
+ }
42
+ const units = ["K", "M", "G", "T"];
43
+ let unitIndex = -1;
44
+ let adjustedSize = size;
45
+ while (adjustedSize >= 1024 && unitIndex < units.length - 1) {
46
+ adjustedSize /= 1024;
47
+ unitIndex++;
48
+ }
49
+ const unit = units[unitIndex];
50
+ if (!unit) {
51
+ return `${size}B`;
52
+ }
53
+ return `${adjustedSize.toFixed(1)}${unit}`;
54
+ }
55
+ function formatPermissions(mode, isDirectory) {
56
+ const type = isDirectory ? "d" : "-";
57
+ const owner = [
58
+ mode & 256 ? "r" : "-",
59
+ mode & 128 ? "w" : "-",
60
+ mode & 64 ? "x" : "-"
61
+ ].join("");
62
+ const group = [
63
+ mode & 32 ? "r" : "-",
64
+ mode & 16 ? "w" : "-",
65
+ mode & 8 ? "x" : "-"
66
+ ].join("");
67
+ const others = [
68
+ mode & 4 ? "r" : "-",
69
+ mode & 2 ? "w" : "-",
70
+ mode & 1 ? "x" : "-"
71
+ ].join("");
72
+ return `${type}${owner}${group}${others}`;
73
+ }
74
+ function formatDate(date) {
75
+ const year = date.getFullYear();
76
+ const month = String(date.getMonth() + 1).padStart(2, "0");
77
+ const day = String(date.getDate()).padStart(2, "0");
78
+ const hours = String(date.getHours()).padStart(2, "0");
79
+ const minutes = String(date.getMinutes()).padStart(2, "0");
80
+ const seconds = String(date.getSeconds()).padStart(2, "0");
81
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
82
+ }
83
+ function isPermissionError(error) {
84
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "EACCES" || error.code === "EPERM");
85
+ }
86
+ function getDirectoryListingCmd(filesystem, path, dir, showHidden) {
87
+ return async () => {
88
+ try {
89
+ const resolvedDir = path.resolve(dir);
90
+ const entries = await filesystem.readdir(resolvedDir, {
91
+ withFileTypes: true
92
+ });
93
+ if (entries.length > 0 && typeof entries[0] === "string") {
94
+ throw new Error(
95
+ "Filesystem adapter must support withFileTypes option"
96
+ );
97
+ }
98
+ const typedEntries = entries;
99
+ const items = [];
100
+ for (const entry of typedEntries) {
101
+ if (!showHidden && entry.name.startsWith(".")) {
102
+ continue;
103
+ }
104
+ const itemPath = path.join(resolvedDir, entry.name);
105
+ try {
106
+ const stats = await filesystem.stat(itemPath);
107
+ const extension = entry.isDirectory() ? "" : path.extname(entry.name);
108
+ const dateStr = formatDate(stats.mtime);
109
+ const perms = formatPermissions(stats.mode, entry.isDirectory());
110
+ const size = convertBytesToSizeString(stats.size);
111
+ const details = `${dateStr} ${perms} ${size}`;
112
+ items.push({
113
+ name: entry.name,
114
+ details,
115
+ path: itemPath,
116
+ extension,
117
+ isDirectory: entry.isDirectory(),
118
+ currentDirectory: resolvedDir,
119
+ mode: stats.mode
120
+ });
121
+ } catch (error) {
122
+ if (isPermissionError(error)) {
123
+ continue;
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ items.sort((a, b) => {
129
+ if (a.isDirectory !== b.isDirectory) {
130
+ return a.isDirectory ? -1 : 1;
131
+ }
132
+ return a.name.localeCompare(b.name);
133
+ });
134
+ return new GetDirectoryListingMsg(items);
135
+ } catch (error) {
136
+ return new ErrorMsg(
137
+ error instanceof Error ? error : new Error(String(error))
138
+ );
139
+ }
140
+ };
141
+ }
142
+
143
+ // src/model.ts
144
+ var FiletreeModel = class _FiletreeModel {
145
+ /** Current cursor position */
146
+ cursor;
147
+ /** Array of directory items */
148
+ files;
149
+ /** Whether component is active and receives input */
150
+ active;
151
+ /** Key bindings */
152
+ keyMap;
153
+ /** Minimum viewport scroll position */
154
+ min;
155
+ /** Maximum viewport scroll position */
156
+ max;
157
+ /** Component height */
158
+ height;
159
+ /** Component width */
160
+ width;
161
+ /** Styles */
162
+ styles;
163
+ /** Current directory */
164
+ currentDir;
165
+ /** Whether to show hidden files */
166
+ showHidden;
167
+ /** Last error, if any */
168
+ error;
169
+ /** Filesystem adapter */
170
+ filesystem;
171
+ /** Path adapter */
172
+ path;
173
+ constructor(cursor, files, active, keyMap, min, max, height, width, styles, currentDir, showHidden, error, filesystem, path) {
174
+ this.cursor = cursor;
175
+ this.files = files;
176
+ this.active = active;
177
+ this.keyMap = keyMap;
178
+ this.min = min;
179
+ this.max = max;
180
+ this.height = height;
181
+ this.width = width;
182
+ this.styles = styles;
183
+ this.currentDir = currentDir;
184
+ this.showHidden = showHidden;
185
+ this.error = error;
186
+ this.filesystem = filesystem;
187
+ this.path = path;
188
+ }
189
+ /**
190
+ * Creates a new filetree model.
191
+ * @param options - Configuration options
192
+ * @returns A new FiletreeModel instance
193
+ * @public
194
+ */
195
+ static new(options) {
196
+ const currentDir = options.currentDir ?? options.filesystem.cwd();
197
+ const showHidden = options.showHidden ?? false;
198
+ const keyMap = options.keyMap ?? defaultKeyMap;
199
+ const styles = mergeStyles(options.styles);
200
+ const width = options.width ?? 80;
201
+ const height = options.height ?? 24;
202
+ return new _FiletreeModel(
203
+ 0,
204
+ // cursor
205
+ [],
206
+ // files
207
+ true,
208
+ // active
209
+ keyMap,
210
+ 0,
211
+ // min
212
+ height - 1,
213
+ // max
214
+ height,
215
+ width,
216
+ styles,
217
+ currentDir,
218
+ showHidden,
219
+ null,
220
+ // error
221
+ options.filesystem,
222
+ options.path
223
+ );
224
+ }
225
+ /**
226
+ * Initializes the model and returns a command to load the directory.
227
+ * @returns Command to load directory listing
228
+ * @public
229
+ */
230
+ init() {
231
+ return getDirectoryListingCmd(
232
+ this.filesystem,
233
+ this.path,
234
+ this.currentDir,
235
+ this.showHidden
236
+ );
237
+ }
238
+ /**
239
+ * Sets whether the component is active and receives input.
240
+ * @param active - Whether component should be active
241
+ * @returns Updated model
242
+ * @public
243
+ */
244
+ setIsActive(active) {
245
+ if (this.active === active) {
246
+ return this;
247
+ }
248
+ return new _FiletreeModel(
249
+ this.cursor,
250
+ this.files,
251
+ active,
252
+ this.keyMap,
253
+ this.min,
254
+ this.max,
255
+ this.height,
256
+ this.width,
257
+ this.styles,
258
+ this.currentDir,
259
+ this.showHidden,
260
+ this.error,
261
+ this.filesystem,
262
+ this.path
263
+ );
264
+ }
265
+ /**
266
+ * Updates the model in response to a message.
267
+ * @param msg - The message to handle
268
+ * @returns Tuple of updated model and command
269
+ * @public
270
+ */
271
+ update(msg) {
272
+ if (msg instanceof GetDirectoryListingMsg) {
273
+ const newMax = Math.max(
274
+ 0,
275
+ Math.min(this.height - 1, msg.items.length - 1)
276
+ );
277
+ return [
278
+ new _FiletreeModel(
279
+ 0,
280
+ // reset cursor to top
281
+ msg.items,
282
+ this.active,
283
+ this.keyMap,
284
+ 0,
285
+ newMax,
286
+ this.height,
287
+ this.width,
288
+ this.styles,
289
+ this.currentDir,
290
+ this.showHidden,
291
+ null,
292
+ // clear error
293
+ this.filesystem,
294
+ this.path
295
+ ),
296
+ null
297
+ ];
298
+ }
299
+ if (msg instanceof ErrorMsg) {
300
+ return [
301
+ new _FiletreeModel(
302
+ this.cursor,
303
+ this.files,
304
+ this.active,
305
+ this.keyMap,
306
+ this.min,
307
+ this.max,
308
+ this.height,
309
+ this.width,
310
+ this.styles,
311
+ this.currentDir,
312
+ this.showHidden,
313
+ msg.error,
314
+ this.filesystem,
315
+ this.path
316
+ ),
317
+ null
318
+ ];
319
+ }
320
+ if (msg instanceof WindowSizeMsg) {
321
+ const newHeight = msg.height;
322
+ const newWidth = msg.width;
323
+ const newMax = Math.max(0, Math.min(newHeight - 1, this.files.length - 1));
324
+ const maxValidCursor = Math.max(0, this.files.length - 1);
325
+ const adjustedCursor = Math.min(
326
+ Math.min(this.cursor, maxValidCursor),
327
+ newMax
328
+ );
329
+ const newMin = Math.min(
330
+ Math.max(0, adjustedCursor - (newHeight - 1)),
331
+ adjustedCursor
332
+ );
333
+ return [
334
+ new _FiletreeModel(
335
+ adjustedCursor,
336
+ this.files,
337
+ this.active,
338
+ this.keyMap,
339
+ newMin,
340
+ newMax,
341
+ newHeight,
342
+ newWidth,
343
+ this.styles,
344
+ this.currentDir,
345
+ this.showHidden,
346
+ this.error,
347
+ this.filesystem,
348
+ this.path
349
+ ),
350
+ null
351
+ ];
352
+ }
353
+ if (!this.active) {
354
+ return [this, null];
355
+ }
356
+ if (msg instanceof KeyMsg) {
357
+ if (matches(msg, this.keyMap.down)) {
358
+ if (this.files.length === 0) {
359
+ return [this, null];
360
+ }
361
+ const nextCursor = Math.min(this.cursor + 1, this.files.length - 1);
362
+ let nextMin = this.min;
363
+ let nextMax = this.max;
364
+ if (nextCursor > this.max) {
365
+ nextMin = this.min + 1;
366
+ nextMax = this.max + 1;
367
+ }
368
+ return [
369
+ new _FiletreeModel(
370
+ nextCursor,
371
+ this.files,
372
+ this.active,
373
+ this.keyMap,
374
+ nextMin,
375
+ nextMax,
376
+ this.height,
377
+ this.width,
378
+ this.styles,
379
+ this.currentDir,
380
+ this.showHidden,
381
+ this.error,
382
+ this.filesystem,
383
+ this.path
384
+ ),
385
+ null
386
+ ];
387
+ }
388
+ if (matches(msg, this.keyMap.up)) {
389
+ if (this.files.length === 0) {
390
+ return [this, null];
391
+ }
392
+ const nextCursor = Math.max(this.cursor - 1, 0);
393
+ let nextMin = this.min;
394
+ let nextMax = this.max;
395
+ if (nextCursor < this.min && this.min > 0) {
396
+ nextMin = this.min - 1;
397
+ nextMax = this.max - 1;
398
+ }
399
+ return [
400
+ new _FiletreeModel(
401
+ nextCursor,
402
+ this.files,
403
+ this.active,
404
+ this.keyMap,
405
+ nextMin,
406
+ nextMax,
407
+ this.height,
408
+ this.width,
409
+ this.styles,
410
+ this.currentDir,
411
+ this.showHidden,
412
+ this.error,
413
+ this.filesystem,
414
+ this.path
415
+ ),
416
+ null
417
+ ];
418
+ }
419
+ }
420
+ return [this, null];
421
+ }
422
+ /**
423
+ * Renders the file tree view.
424
+ * @returns Rendered string
425
+ * @public
426
+ */
427
+ view() {
428
+ if (this.error) {
429
+ return `Error: ${this.error.message}`;
430
+ }
431
+ if (this.files.length === 0) {
432
+ return "(empty directory)";
433
+ }
434
+ const lines = [];
435
+ for (let i = this.min; i <= this.max && i < this.files.length; i++) {
436
+ const item = this.files[i];
437
+ if (!item) continue;
438
+ const isSelected = i === this.cursor;
439
+ const style = isSelected ? this.styles.selectedItem : this.styles.normalItem;
440
+ const line = `${item.name} ${item.details}`;
441
+ lines.push(style.render(line));
442
+ }
443
+ const remainingLines = this.height - lines.length;
444
+ for (let i = 0; i < remainingLines; i++) {
445
+ lines.push("");
446
+ }
447
+ return lines.join("\n");
448
+ }
449
+ /**
450
+ * Gets the currently selected file, if any.
451
+ * @returns The selected DirectoryItem or null
452
+ * @public
453
+ */
454
+ get selectedFile() {
455
+ return this.files[this.cursor] ?? null;
456
+ }
457
+ };
458
+
459
+ export { ErrorMsg, FiletreeModel, GetDirectoryListingMsg, convertBytesToSizeString, defaultKeyMap, defaultStyles, getDirectoryListingCmd, mergeStyles };
460
+ //# sourceMappingURL=index.js.map
461
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/keymap.ts","../src/styles.ts","../src/messages.ts","../src/fs.ts","../src/model.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Binding, newBinding } from '@suds-cli/key'\n\n/**\n * Keyboard bindings for the filetree component.\n * @public\n */\nexport interface FiletreeKeyMap {\n down: Binding\n up: Binding\n}\n\n/**\n * Default key bindings for the filetree.\n * @public\n */\nexport const defaultKeyMap: FiletreeKeyMap = {\n down: newBinding({ keys: ['j', 'down', 'ctrl+n'] }).withHelp('j/↓', 'down'),\n up: newBinding({ keys: ['k', 'up', 'ctrl+p'] }).withHelp('k/↑', 'up'),\n}\n","import { Style } from '@suds-cli/chapstick'\n\n/**\n * Style configuration for the filetree component.\n * @public\n */\nexport interface FiletreeStyles {\n /** Style for selected/highlighted item */\n selectedItem: Style\n /** Style for normal items */\n normalItem: Style\n}\n\n/**\n * Default styles for the filetree component.\n * @public\n */\nexport function defaultStyles(): FiletreeStyles {\n return {\n selectedItem: new Style().foreground('#00ff00').bold(true),\n normalItem: new Style(),\n }\n}\n\n/**\n * Merge user provided style overrides with defaults.\n * @public\n */\nexport function mergeStyles(\n overrides?: Partial<FiletreeStyles>,\n): FiletreeStyles {\n if (!overrides) {\n return defaultStyles()\n }\n return { ...defaultStyles(), ...overrides }\n}\n","import type { DirectoryItem } from './types.js'\n\n/**\n * Message containing directory listing results.\n * @public\n */\nexport class GetDirectoryListingMsg {\n readonly _tag = 'filetree-get-directory-listing'\n\n constructor(public readonly items: DirectoryItem[]) {}\n}\n\n/**\n * Message containing an error.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'filetree-error'\n\n constructor(public readonly error: Error) {}\n}\n","import type { Cmd } from '@suds-cli/tea'\nimport type {\n DirectoryEntry,\n FileSystemAdapter,\n PathAdapter,\n} from '@suds-cli/machine'\nimport type { DirectoryItem } from './types.js'\nimport { GetDirectoryListingMsg, ErrorMsg } from './messages.js'\n\n/**\n * Converts bytes to a human-readable size string.\n * @param size - Size in bytes\n * @returns Formatted string (e.g., \"1.2K\", \"5.4M\", \"2.3G\")\n * @public\n */\nexport function convertBytesToSizeString(size: number): string {\n if (size < 1024) {\n return `${size}B`\n }\n\n const units = ['K', 'M', 'G', 'T']\n let unitIndex = -1\n let adjustedSize = size\n\n while (adjustedSize >= 1024 && unitIndex < units.length - 1) {\n adjustedSize /= 1024\n unitIndex++\n }\n\n const unit = units[unitIndex]\n\n if (!unit) {\n return `${size}B`\n }\n\n // Format to 1 decimal place\n return `${adjustedSize.toFixed(1)}${unit}`\n}\n\n/**\n * Formats file permissions in Unix-style notation.\n * @param mode - File mode bits\n * @returns Permission string (e.g., \"-rw-r--r--\", \"drwxr-xr-x\")\n */\nfunction formatPermissions(mode: number, isDirectory: boolean): string {\n const type = isDirectory ? 'd' : '-'\n const owner = [\n mode & 0o400 ? 'r' : '-',\n mode & 0o200 ? 'w' : '-',\n mode & 0o100 ? 'x' : '-',\n ].join('')\n const group = [\n mode & 0o040 ? 'r' : '-',\n mode & 0o020 ? 'w' : '-',\n mode & 0o010 ? 'x' : '-',\n ].join('')\n const others = [\n mode & 0o004 ? 'r' : '-',\n mode & 0o002 ? 'w' : '-',\n mode & 0o001 ? 'x' : '-',\n ].join('')\n\n return `${type}${owner}${group}${others}`\n}\n\n/**\n * Formats a date for display.\n * @param date - Date to format\n * @returns Formatted date string\n */\nfunction formatDate(date: Date): string {\n const year = date.getFullYear()\n const month = String(date.getMonth() + 1).padStart(2, '0')\n const day = String(date.getDate()).padStart(2, '0')\n const hours = String(date.getHours()).padStart(2, '0')\n const minutes = String(date.getMinutes()).padStart(2, '0')\n const seconds = String(date.getSeconds()).padStart(2, '0')\n\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`\n}\n\n/**\n * Type guard to check if an error is a filesystem permission error.\n * @param error - The error to check\n * @returns True if the error is a permission-related filesystem error\n */\nfunction isPermissionError(\n error: unknown,\n): error is { code: 'EACCES' | 'EPERM' } {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n ((error as { code: unknown }).code === 'EACCES' ||\n (error as { code: unknown }).code === 'EPERM')\n )\n}\n\n/**\n * Creates a command to asynchronously fetch directory contents.\n * @param filesystem - Filesystem adapter for file operations\n * @param path - Path adapter for path operations\n * @param dir - Directory path to list\n * @param showHidden - Whether to show hidden files\n * @returns Command that will emit GetDirectoryListingMsg or ErrorMsg\n * @public\n */\nexport function getDirectoryListingCmd(\n filesystem: FileSystemAdapter,\n path: PathAdapter,\n dir: string,\n showHidden: boolean,\n): Cmd<GetDirectoryListingMsg | ErrorMsg> {\n return async () => {\n try {\n const resolvedDir = path.resolve(dir)\n const entries = await filesystem.readdir(resolvedDir, {\n withFileTypes: true,\n })\n\n // Type guard: entries should be DirectoryEntry[] when withFileTypes is true\n if (\n entries.length > 0 &&\n typeof entries[0] === 'string'\n ) {\n // Fallback if adapter returns string[] instead of DirectoryEntry[]\n throw new Error(\n 'Filesystem adapter must support withFileTypes option',\n )\n }\n\n const typedEntries = entries as DirectoryEntry[]\n const items: DirectoryItem[] = []\n\n for (const entry of typedEntries) {\n // Skip hidden files if showHidden is false\n if (!showHidden && entry.name.startsWith('.')) {\n continue\n }\n\n const itemPath = path.join(resolvedDir, entry.name)\n\n try {\n const stats = await filesystem.stat(itemPath)\n const extension = entry.isDirectory() ? '' : path.extname(entry.name)\n\n // Format details: \"2024-01-15 10:30:00 -rw-r--r-- 1.2K\"\n const dateStr = formatDate(stats.mtime)\n const perms = formatPermissions(stats.mode, entry.isDirectory())\n const size = convertBytesToSizeString(stats.size)\n const details = `${dateStr} ${perms} ${size}`\n\n items.push({\n name: entry.name,\n details,\n path: itemPath,\n extension,\n isDirectory: entry.isDirectory(),\n currentDirectory: resolvedDir,\n mode: stats.mode,\n })\n } catch (error: unknown) {\n // Only skip permission-related errors; re-throw unexpected errors\n if (isPermissionError(error)) {\n // Skip files we can't stat due to permission issues\n // EACCES: Permission denied, EPERM: Operation not permitted\n continue\n }\n // Re-throw unexpected errors to surface real bugs\n throw error\n }\n }\n\n // Sort: directories first, then by name\n items.sort((a, b) => {\n if (a.isDirectory !== b.isDirectory) {\n return a.isDirectory ? -1 : 1\n }\n return a.name.localeCompare(b.name)\n })\n\n return new GetDirectoryListingMsg(items)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n","import { matches } from '@suds-cli/key'\nimport { type Cmd, type Msg, KeyMsg, WindowSizeMsg } from '@suds-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@suds-cli/machine'\nimport type { DirectoryItem } from './types.js'\nimport { defaultKeyMap, type FiletreeKeyMap } from './keymap.js'\nimport { mergeStyles, type FiletreeStyles } from './styles.js'\nimport { getDirectoryListingCmd } from './fs.js'\nimport { GetDirectoryListingMsg, ErrorMsg } from './messages.js'\n\n/**\n * Options for creating a new FiletreeModel.\n * @public\n */\nexport interface FiletreeOptions {\n /** Filesystem adapter for file operations */\n filesystem: FileSystemAdapter\n /** Path adapter for path operations */\n path: PathAdapter\n /** Initial directory to display */\n currentDir?: string\n /** Whether to show hidden files */\n showHidden?: boolean\n /** Custom key bindings */\n keyMap?: FiletreeKeyMap\n /** Custom styles */\n styles?: Partial<FiletreeStyles>\n /** Initial width */\n width?: number\n /** Initial height */\n height?: number\n}\n\n/**\n * Model for the filetree component.\n * @public\n */\nexport class FiletreeModel {\n /** Current cursor position */\n readonly cursor: number\n\n /** Array of directory items */\n readonly files: DirectoryItem[]\n\n /** Whether component is active and receives input */\n readonly active: boolean\n\n /** Key bindings */\n readonly keyMap: FiletreeKeyMap\n\n /** Minimum viewport scroll position */\n readonly min: number\n\n /** Maximum viewport scroll position */\n readonly max: number\n\n /** Component height */\n readonly height: number\n\n /** Component width */\n readonly width: number\n\n /** Styles */\n readonly styles: FiletreeStyles\n\n /** Current directory */\n readonly currentDir: string\n\n /** Whether to show hidden files */\n readonly showHidden: boolean\n\n /** Last error, if any */\n readonly error: Error | null\n\n /** Filesystem adapter */\n readonly filesystem: FileSystemAdapter\n\n /** Path adapter */\n readonly path: PathAdapter\n\n private constructor(\n cursor: number,\n files: DirectoryItem[],\n active: boolean,\n keyMap: FiletreeKeyMap,\n min: number,\n max: number,\n height: number,\n width: number,\n styles: FiletreeStyles,\n currentDir: string,\n showHidden: boolean,\n error: Error | null,\n filesystem: FileSystemAdapter,\n path: PathAdapter,\n ) {\n this.cursor = cursor\n this.files = files\n this.active = active\n this.keyMap = keyMap\n this.min = min\n this.max = max\n this.height = height\n this.width = width\n this.styles = styles\n this.currentDir = currentDir\n this.showHidden = showHidden\n this.error = error\n this.filesystem = filesystem\n this.path = path\n }\n\n /**\n * Creates a new filetree model.\n * @param options - Configuration options\n * @returns A new FiletreeModel instance\n * @public\n */\n static new(options: FiletreeOptions): FiletreeModel {\n const currentDir = options.currentDir ?? options.filesystem.cwd()\n const showHidden = options.showHidden ?? false\n const keyMap = options.keyMap ?? defaultKeyMap\n const styles = mergeStyles(options.styles)\n const width = options.width ?? 80\n const height = options.height ?? 24\n\n return new FiletreeModel(\n 0, // cursor\n [], // files\n true, // active\n keyMap,\n 0, // min\n height - 1, // max\n height,\n width,\n styles,\n currentDir,\n showHidden,\n null, // error\n options.filesystem,\n options.path,\n )\n }\n\n /**\n * Initializes the model and returns a command to load the directory.\n * @returns Command to load directory listing\n * @public\n */\n init(): Cmd<Msg> {\n return getDirectoryListingCmd(\n this.filesystem,\n this.path,\n this.currentDir,\n this.showHidden,\n )\n }\n\n /**\n * Sets whether the component is active and receives input.\n * @param active - Whether component should be active\n * @returns Updated model\n * @public\n */\n setIsActive(active: boolean): FiletreeModel {\n if (this.active === active) {\n return this\n }\n\n return new FiletreeModel(\n this.cursor,\n this.files,\n active,\n this.keyMap,\n this.min,\n this.max,\n this.height,\n this.width,\n this.styles,\n this.currentDir,\n this.showHidden,\n this.error,\n this.filesystem,\n this.path,\n )\n }\n\n /**\n * Updates the model in response to a message.\n * @param msg - The message to handle\n * @returns Tuple of updated model and command\n * @public\n */\n update(msg: Msg): [FiletreeModel, Cmd<Msg> | null] {\n // Handle directory listing message\n if (msg instanceof GetDirectoryListingMsg) {\n const newMax = Math.max(\n 0,\n Math.min(this.height - 1, msg.items.length - 1),\n )\n return [\n new FiletreeModel(\n 0, // reset cursor to top\n msg.items,\n this.active,\n this.keyMap,\n 0,\n newMax,\n this.height,\n this.width,\n this.styles,\n this.currentDir,\n this.showHidden,\n null, // clear error\n this.filesystem,\n this.path,\n ),\n null,\n ]\n }\n\n // Handle error message\n if (msg instanceof ErrorMsg) {\n return [\n new FiletreeModel(\n this.cursor,\n this.files,\n this.active,\n this.keyMap,\n this.min,\n this.max,\n this.height,\n this.width,\n this.styles,\n this.currentDir,\n this.showHidden,\n msg.error,\n this.filesystem,\n this.path,\n ),\n null,\n ]\n }\n\n // Handle window size message\n if (msg instanceof WindowSizeMsg) {\n const newHeight = msg.height\n const newWidth = msg.width\n const newMax = Math.max(0, Math.min(newHeight - 1, this.files.length - 1))\n \n // Clamp cursor position to valid range and viewport bounds\n const maxValidCursor = Math.max(0, this.files.length - 1)\n const adjustedCursor = Math.min(\n Math.min(this.cursor, maxValidCursor),\n newMax,\n )\n \n // Adjust viewport min to keep cursor visible after resize\n // If cursor is at the bottom of the viewport, min should be adjusted\n const newMin = Math.min(\n Math.max(0, adjustedCursor - (newHeight - 1)),\n adjustedCursor,\n )\n\n return [\n new FiletreeModel(\n adjustedCursor,\n this.files,\n this.active,\n this.keyMap,\n newMin,\n newMax,\n newHeight,\n newWidth,\n this.styles,\n this.currentDir,\n this.showHidden,\n this.error,\n this.filesystem,\n this.path,\n ),\n null,\n ]\n }\n\n // Only handle keyboard input if active\n if (!this.active) {\n return [this, null]\n }\n\n // Handle keyboard input\n if (msg instanceof KeyMsg) {\n // Move down\n if (matches(msg, this.keyMap.down)) {\n // Don't navigate if files list is empty\n if (this.files.length === 0) {\n return [this, null]\n }\n const nextCursor = Math.min(this.cursor + 1, this.files.length - 1)\n\n // Adjust viewport if needed\n let nextMin = this.min\n let nextMax = this.max\n\n if (nextCursor > this.max) {\n nextMin = this.min + 1\n nextMax = this.max + 1\n }\n\n return [\n new FiletreeModel(\n nextCursor,\n this.files,\n this.active,\n this.keyMap,\n nextMin,\n nextMax,\n this.height,\n this.width,\n this.styles,\n this.currentDir,\n this.showHidden,\n this.error,\n this.filesystem,\n this.path,\n ),\n null,\n ]\n }\n\n // Move up\n if (matches(msg, this.keyMap.up)) {\n // Don't navigate if files list is empty\n if (this.files.length === 0) {\n return [this, null]\n }\n const nextCursor = Math.max(this.cursor - 1, 0)\n\n // Adjust viewport if needed\n let nextMin = this.min\n let nextMax = this.max\n\n if (nextCursor < this.min && this.min > 0) {\n nextMin = this.min - 1\n nextMax = this.max - 1\n }\n\n return [\n new FiletreeModel(\n nextCursor,\n this.files,\n this.active,\n this.keyMap,\n nextMin,\n nextMax,\n this.height,\n this.width,\n this.styles,\n this.currentDir,\n this.showHidden,\n this.error,\n this.filesystem,\n this.path,\n ),\n null,\n ]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Renders the file tree view.\n * @returns Rendered string\n * @public\n */\n view(): string {\n if (this.error) {\n return `Error: ${this.error.message}`\n }\n\n if (this.files.length === 0) {\n return '(empty directory)'\n }\n\n const lines: string[] = []\n\n // Render visible items in viewport\n for (let i = this.min; i <= this.max && i < this.files.length; i++) {\n const item = this.files[i]\n if (!item) continue\n\n const isSelected = i === this.cursor\n const style = isSelected\n ? this.styles.selectedItem\n : this.styles.normalItem\n\n // Format: \"name details\"\n const line = `${item.name} ${item.details}`\n lines.push(style.render(line))\n }\n\n // Fill remaining height with empty lines\n const remainingLines = this.height - lines.length\n for (let i = 0; i < remainingLines; i++) {\n lines.push('')\n }\n\n return lines.join('\\n')\n }\n\n /**\n * Gets the currently selected file, if any.\n * @returns The selected DirectoryItem or null\n * @public\n */\n get selectedFile(): DirectoryItem | null {\n return this.files[this.cursor] ?? null\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@suds-cli/filetree",
3
+ "description": "Navigable file tree browser component for Suds terminal UIs",
4
+ "version": "0.1.0-alpha.0",
5
+ "dependencies": {
6
+ "@suds-cli/chapstick": "0.1.0-alpha.1",
7
+ "@suds-cli/key": "0.1.0-alpha.0",
8
+ "@suds-cli/machine": "0.1.0-alpha.0",
9
+ "@suds-cli/tea": "0.1.0-alpha.0"
10
+ },
11
+ "devDependencies": {
12
+ "@types/node": "^24.10.2",
13
+ "typescript": "5.8.2",
14
+ "vitest": "^4.0.15"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.0.0"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "import": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "require": {
26
+ "types": "./dist/index.d.cts",
27
+ "default": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "main": "./dist/index.cjs",
36
+ "module": "./dist/index.js",
37
+ "type": "module",
38
+ "types": "./dist/index.d.ts",
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "check:api-report": "pnpm run generate:api-report",
42
+ "check:eslint": "pnpm run lint",
43
+ "generate:api-report": "api-extractor run --local",
44
+ "lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
45
+ "test": "vitest run"
46
+ }
47
+ }