@boba-cli/filepicker 0.1.0-alpha.2
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/README.md +16 -0
- package/dist/index.cjs +317 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @boba-cli/filepicker
|
|
2
|
+
|
|
3
|
+
File system browser component for Boba terminal UIs. Ported from the Charm `bubbles/filepicker` component.
|
|
4
|
+
|
|
5
|
+
<img src="../../examples/filepicker-demo.gif" width="950" alt="Filepicker component demo" />
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { FilepickerModel } from '@boba-cli/filepicker'
|
|
11
|
+
|
|
12
|
+
const [picker, cmd] = FilepickerModel.new({
|
|
13
|
+
currentDir: process.cwd(),
|
|
14
|
+
showHidden: false,
|
|
15
|
+
})
|
|
16
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var key = require('@boba-cli/key');
|
|
4
|
+
var chapstick = require('@boba-cli/chapstick');
|
|
5
|
+
var tea = require('@boba-cli/tea');
|
|
6
|
+
|
|
7
|
+
// src/fs.ts
|
|
8
|
+
async function readDirectory(filesystem, pathAdapter, path, showHidden, dirFirst = true) {
|
|
9
|
+
const result = await filesystem.readdir(path, { withFileTypes: true });
|
|
10
|
+
if (typeof result[0] === "string") {
|
|
11
|
+
throw new Error("Expected DirectoryEntry array but got string array");
|
|
12
|
+
}
|
|
13
|
+
const entries = result;
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const isHidden = isHiddenUnix(entry.name);
|
|
17
|
+
if (!showHidden && isHidden) continue;
|
|
18
|
+
const fullPath = pathAdapter.join(path, entry.name);
|
|
19
|
+
const stats = await filesystem.stat(fullPath);
|
|
20
|
+
files.push({
|
|
21
|
+
name: entry.name,
|
|
22
|
+
path: fullPath,
|
|
23
|
+
isDir: entry.isDirectory(),
|
|
24
|
+
isHidden,
|
|
25
|
+
size: stats.size,
|
|
26
|
+
mode: stats.mode
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return files.sort((a, b) => sortFiles(a, b, dirFirst));
|
|
30
|
+
}
|
|
31
|
+
function sortFiles(a, b, dirFirst = true) {
|
|
32
|
+
if (dirFirst) {
|
|
33
|
+
if (a.isDir && !b.isDir) return -1;
|
|
34
|
+
if (!a.isDir && b.isDir) return 1;
|
|
35
|
+
}
|
|
36
|
+
return a.name.localeCompare(b.name);
|
|
37
|
+
}
|
|
38
|
+
function isHiddenUnix(name) {
|
|
39
|
+
return name.startsWith(".");
|
|
40
|
+
}
|
|
41
|
+
var defaultKeyMap = {
|
|
42
|
+
up: key.newBinding({ keys: ["up", "k"] }),
|
|
43
|
+
down: key.newBinding({ keys: ["down", "j"] }),
|
|
44
|
+
select: key.newBinding({ keys: ["enter"] }),
|
|
45
|
+
back: key.newBinding({ keys: ["backspace", "h", "left"] }),
|
|
46
|
+
open: key.newBinding({ keys: ["right", "l"] }),
|
|
47
|
+
toggleHidden: key.newBinding({ keys: ["."] }),
|
|
48
|
+
pageUp: key.newBinding({ keys: ["pgup", "u"] }),
|
|
49
|
+
pageDown: key.newBinding({ keys: ["pgdown", "d"] }),
|
|
50
|
+
gotoTop: key.newBinding({ keys: ["home", "g"] }),
|
|
51
|
+
gotoBottom: key.newBinding({ keys: ["end", "G"] }),
|
|
52
|
+
shortHelp() {
|
|
53
|
+
return [this.up, this.down, this.select, this.back];
|
|
54
|
+
},
|
|
55
|
+
fullHelp() {
|
|
56
|
+
return [
|
|
57
|
+
[this.up, this.down, this.pageUp, this.pageDown],
|
|
58
|
+
[this.select, this.open, this.back, this.toggleHidden],
|
|
59
|
+
[this.gotoTop, this.gotoBottom]
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/messages.ts
|
|
65
|
+
var DirReadMsg = class {
|
|
66
|
+
constructor(path, files, error) {
|
|
67
|
+
this.path = path;
|
|
68
|
+
this.files = files;
|
|
69
|
+
this.error = error;
|
|
70
|
+
}
|
|
71
|
+
_tag = "filepicker-dir-read";
|
|
72
|
+
};
|
|
73
|
+
var FileSelectedMsg = class {
|
|
74
|
+
constructor(file) {
|
|
75
|
+
this.file = file;
|
|
76
|
+
}
|
|
77
|
+
_tag = "filepicker-file-selected";
|
|
78
|
+
};
|
|
79
|
+
function defaultStyles() {
|
|
80
|
+
return {
|
|
81
|
+
directory: new chapstick.Style().bold(true),
|
|
82
|
+
file: new chapstick.Style(),
|
|
83
|
+
hidden: new chapstick.Style().italic(true),
|
|
84
|
+
selected: new chapstick.Style().background("#303030").foreground("#ffffff"),
|
|
85
|
+
cursor: new chapstick.Style().bold(true),
|
|
86
|
+
status: new chapstick.Style().italic(true)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function filterByType(files, allowed) {
|
|
90
|
+
if (!allowed || allowed.length === 0) return files;
|
|
91
|
+
return files.filter((f) => {
|
|
92
|
+
if (f.isDir) return true;
|
|
93
|
+
return allowed.some((ext) => f.name.endsWith(ext));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
var FilepickerModel = class _FilepickerModel {
|
|
97
|
+
currentDir;
|
|
98
|
+
files;
|
|
99
|
+
cursor;
|
|
100
|
+
selectedFile;
|
|
101
|
+
showHidden;
|
|
102
|
+
showPermissions;
|
|
103
|
+
showSize;
|
|
104
|
+
dirFirst;
|
|
105
|
+
height;
|
|
106
|
+
allowedTypes;
|
|
107
|
+
styles;
|
|
108
|
+
keyMap;
|
|
109
|
+
filesystem;
|
|
110
|
+
path;
|
|
111
|
+
constructor(state) {
|
|
112
|
+
this.currentDir = state.currentDir;
|
|
113
|
+
this.files = state.files;
|
|
114
|
+
this.cursor = state.cursor;
|
|
115
|
+
this.selectedFile = state.selectedFile;
|
|
116
|
+
this.showHidden = state.showHidden;
|
|
117
|
+
this.showPermissions = state.showPermissions;
|
|
118
|
+
this.showSize = state.showSize;
|
|
119
|
+
this.dirFirst = state.dirFirst;
|
|
120
|
+
this.height = state.height;
|
|
121
|
+
this.allowedTypes = state.allowedTypes;
|
|
122
|
+
this.styles = state.styles;
|
|
123
|
+
this.keyMap = state.keyMap;
|
|
124
|
+
this.filesystem = state.filesystem;
|
|
125
|
+
this.path = state.path;
|
|
126
|
+
}
|
|
127
|
+
/** Create a new model and command to read the directory. */
|
|
128
|
+
static new(options) {
|
|
129
|
+
const styles = { ...defaultStyles(), ...options.styles ?? {} };
|
|
130
|
+
const model = new _FilepickerModel({
|
|
131
|
+
currentDir: options.currentDir ?? options.filesystem.cwd(),
|
|
132
|
+
files: [],
|
|
133
|
+
cursor: 0,
|
|
134
|
+
selectedFile: null,
|
|
135
|
+
showHidden: options.showHidden ?? false,
|
|
136
|
+
showPermissions: options.showPermissions ?? false,
|
|
137
|
+
showSize: options.showSize ?? false,
|
|
138
|
+
dirFirst: options.dirFirst ?? true,
|
|
139
|
+
height: options.height ?? 0,
|
|
140
|
+
allowedTypes: options.allowedTypes ?? [],
|
|
141
|
+
styles,
|
|
142
|
+
keyMap: options.keyMap ?? defaultKeyMap,
|
|
143
|
+
filesystem: options.filesystem,
|
|
144
|
+
path: options.path
|
|
145
|
+
});
|
|
146
|
+
return model.refresh();
|
|
147
|
+
}
|
|
148
|
+
/** Current selected file (if any). */
|
|
149
|
+
selected() {
|
|
150
|
+
return this.files[this.cursor];
|
|
151
|
+
}
|
|
152
|
+
/** Go to the parent directory. */
|
|
153
|
+
back() {
|
|
154
|
+
const parent = this.path.dirname(this.currentDir);
|
|
155
|
+
const model = this.with({ currentDir: parent, cursor: 0 });
|
|
156
|
+
return model.refresh();
|
|
157
|
+
}
|
|
158
|
+
/** Enter the highlighted directory, or select a file. */
|
|
159
|
+
enter() {
|
|
160
|
+
const file = this.selected();
|
|
161
|
+
if (!file) return [this, null];
|
|
162
|
+
if (file.isDir) {
|
|
163
|
+
const model = this.with({
|
|
164
|
+
currentDir: file.path,
|
|
165
|
+
cursor: 0,
|
|
166
|
+
selectedFile: null
|
|
167
|
+
});
|
|
168
|
+
return model.refresh();
|
|
169
|
+
}
|
|
170
|
+
return this.select();
|
|
171
|
+
}
|
|
172
|
+
/** Refresh the current directory listing. */
|
|
173
|
+
refresh() {
|
|
174
|
+
const cmd = async () => {
|
|
175
|
+
try {
|
|
176
|
+
const files = await readDirectory(
|
|
177
|
+
this.filesystem,
|
|
178
|
+
this.path,
|
|
179
|
+
this.currentDir,
|
|
180
|
+
this.showHidden,
|
|
181
|
+
this.dirFirst
|
|
182
|
+
);
|
|
183
|
+
const filtered = filterByType(files, this.allowedTypes);
|
|
184
|
+
return new DirReadMsg(this.currentDir, filtered);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return new DirReadMsg(this.currentDir, [], error);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return [this, cmd];
|
|
190
|
+
}
|
|
191
|
+
/** Select the current file. */
|
|
192
|
+
select() {
|
|
193
|
+
const file = this.selected();
|
|
194
|
+
if (!file) return [this, null];
|
|
195
|
+
const model = this.with({ selectedFile: file });
|
|
196
|
+
return [model, tea.msg(new FileSelectedMsg(file))];
|
|
197
|
+
}
|
|
198
|
+
/** Move cursor up. */
|
|
199
|
+
cursorUp() {
|
|
200
|
+
if (this.files.length === 0) return this;
|
|
201
|
+
const next = Math.max(0, this.cursor - 1);
|
|
202
|
+
return this.with({ cursor: next });
|
|
203
|
+
}
|
|
204
|
+
/** Move cursor down. */
|
|
205
|
+
cursorDown() {
|
|
206
|
+
if (this.files.length === 0) return this;
|
|
207
|
+
const next = Math.min(this.files.length - 1, this.cursor + 1);
|
|
208
|
+
return this.with({ cursor: next });
|
|
209
|
+
}
|
|
210
|
+
/** Jump to first entry. */
|
|
211
|
+
gotoTop() {
|
|
212
|
+
if (this.files.length === 0) return this;
|
|
213
|
+
return this.with({ cursor: 0 });
|
|
214
|
+
}
|
|
215
|
+
/** Jump to last entry. */
|
|
216
|
+
gotoBottom() {
|
|
217
|
+
if (this.files.length === 0) return this;
|
|
218
|
+
return this.with({ cursor: this.files.length - 1 });
|
|
219
|
+
}
|
|
220
|
+
/** Toggle hidden file visibility. */
|
|
221
|
+
toggleHidden() {
|
|
222
|
+
const model = this.with({ showHidden: !this.showHidden, cursor: 0 });
|
|
223
|
+
return model.refresh();
|
|
224
|
+
}
|
|
225
|
+
/** Tea init hook; triggers directory read. */
|
|
226
|
+
init() {
|
|
227
|
+
const [, cmd] = this.refresh();
|
|
228
|
+
return cmd;
|
|
229
|
+
}
|
|
230
|
+
/** Tea update handler. */
|
|
231
|
+
update(msgObj) {
|
|
232
|
+
if (msgObj instanceof DirReadMsg) {
|
|
233
|
+
if (msgObj.error) {
|
|
234
|
+
return [
|
|
235
|
+
this.with({
|
|
236
|
+
files: [],
|
|
237
|
+
cursor: 0,
|
|
238
|
+
selectedFile: null
|
|
239
|
+
}),
|
|
240
|
+
null
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
const files = msgObj.files;
|
|
244
|
+
const cursor = Math.min(this.cursor, Math.max(0, files.length - 1));
|
|
245
|
+
return [
|
|
246
|
+
this.with({
|
|
247
|
+
currentDir: msgObj.path,
|
|
248
|
+
files,
|
|
249
|
+
cursor,
|
|
250
|
+
selectedFile: files[cursor] ?? null
|
|
251
|
+
}),
|
|
252
|
+
null
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
if (msgObj instanceof tea.KeyMsg) {
|
|
256
|
+
if (key.matches(msgObj, this.keyMap.up)) return [this.cursorUp(), null];
|
|
257
|
+
if (key.matches(msgObj, this.keyMap.down)) return [this.cursorDown(), null];
|
|
258
|
+
if (key.matches(msgObj, this.keyMap.gotoTop)) return [this.gotoTop(), null];
|
|
259
|
+
if (key.matches(msgObj, this.keyMap.gotoBottom))
|
|
260
|
+
return [this.gotoBottom(), null];
|
|
261
|
+
if (key.matches(msgObj, this.keyMap.toggleHidden)) return this.toggleHidden();
|
|
262
|
+
if (key.matches(msgObj, this.keyMap.back)) return this.back();
|
|
263
|
+
if (key.matches(msgObj, this.keyMap.open)) return this.enter();
|
|
264
|
+
if (key.matches(msgObj, this.keyMap.select)) return this.select();
|
|
265
|
+
}
|
|
266
|
+
return [this, null];
|
|
267
|
+
}
|
|
268
|
+
/** Render the file list. */
|
|
269
|
+
view() {
|
|
270
|
+
const lines = [];
|
|
271
|
+
const header = this.styles.status.render(this.currentDir);
|
|
272
|
+
lines.push(header);
|
|
273
|
+
if (this.files.length === 0) {
|
|
274
|
+
lines.push(this.styles.status.render("(empty)"));
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
for (const [index, file] of this.files.entries()) {
|
|
278
|
+
const isSelected = index === this.cursor;
|
|
279
|
+
const style = file.isDir ? this.styles.directory : this.styles.file;
|
|
280
|
+
const base = file.isHidden ? this.styles.hidden : style;
|
|
281
|
+
const name = base.render(file.name);
|
|
282
|
+
const cursor = isSelected ? this.styles.cursor.render("\u27A4 ") : " ";
|
|
283
|
+
const line = isSelected ? this.styles.selected.render(cursor + name) : cursor + name;
|
|
284
|
+
lines.push(line);
|
|
285
|
+
}
|
|
286
|
+
return lines.join("\n");
|
|
287
|
+
}
|
|
288
|
+
with(patch) {
|
|
289
|
+
return new _FilepickerModel({
|
|
290
|
+
currentDir: this.currentDir,
|
|
291
|
+
files: this.files,
|
|
292
|
+
cursor: this.cursor,
|
|
293
|
+
selectedFile: this.selectedFile,
|
|
294
|
+
showHidden: this.showHidden,
|
|
295
|
+
showPermissions: this.showPermissions,
|
|
296
|
+
showSize: this.showSize,
|
|
297
|
+
dirFirst: this.dirFirst,
|
|
298
|
+
height: this.height,
|
|
299
|
+
allowedTypes: this.allowedTypes,
|
|
300
|
+
styles: this.styles,
|
|
301
|
+
keyMap: this.keyMap,
|
|
302
|
+
filesystem: this.filesystem,
|
|
303
|
+
path: this.path,
|
|
304
|
+
...patch
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
exports.DirReadMsg = DirReadMsg;
|
|
310
|
+
exports.FileSelectedMsg = FileSelectedMsg;
|
|
311
|
+
exports.FilepickerModel = FilepickerModel;
|
|
312
|
+
exports.defaultKeyMap = defaultKeyMap;
|
|
313
|
+
exports.isHiddenUnix = isHiddenUnix;
|
|
314
|
+
exports.readDirectory = readDirectory;
|
|
315
|
+
exports.sortFiles = sortFiles;
|
|
316
|
+
//# sourceMappingURL=index.cjs.map
|
|
317
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fs.ts","../src/keymap.ts","../src/messages.ts","../src/model.ts"],"names":["newBinding","Style","msg","KeyMsg","matches"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport type { FileInfo } from './types.js'\n\n/**\n * Read a directory and return file info entries.\n * @public\n */\nexport async function readDirectory(\n filesystem: FileSystemAdapter,\n pathAdapter: PathAdapter,\n path: string,\n showHidden: boolean,\n dirFirst = true,\n): Promise<FileInfo[]> {\n const result = await filesystem.readdir(path, { withFileTypes: true })\n\n // Type guard to ensure we got DirectoryEntry[]\n if (typeof result[0] === 'string') {\n throw new Error('Expected DirectoryEntry array but got string array')\n }\n\n const entries = result as Array<{\n readonly name: string\n isDirectory(): boolean\n isFile(): boolean\n isSymbolicLink(): boolean\n }>\n\n const files: FileInfo[] = []\n\n for (const entry of entries) {\n const isHidden = isHiddenUnix(entry.name)\n if (!showHidden && isHidden) continue\n\n const fullPath = pathAdapter.join(path, entry.name)\n const stats = await filesystem.stat(fullPath)\n\n files.push({\n name: entry.name,\n path: fullPath,\n isDir: entry.isDirectory(),\n isHidden,\n size: stats.size,\n mode: stats.mode,\n })\n }\n\n return files.sort((a, b) => sortFiles(a, b, dirFirst))\n}\n\n/**\n * Sort directories first then alphabetical.\n * @public\n */\nexport function sortFiles(a: FileInfo, b: FileInfo, dirFirst = true): number {\n if (dirFirst) {\n if (a.isDir && !b.isDir) return -1\n if (!a.isDir && b.isDir) return 1\n }\n return a.name.localeCompare(b.name)\n}\n\n/**\n * Hidden file detection (Unix-style).\n * @public\n */\nexport function isHiddenUnix(name: string): boolean {\n return name.startsWith('.')\n}\n","import { newBinding } from '@boba-cli/key'\nimport type { FilepickerKeyMap } from './types.js'\n\n/** Default filepicker keymap. @public */\nexport const defaultKeyMap: FilepickerKeyMap = {\n up: newBinding({ keys: ['up', 'k'] }),\n down: newBinding({ keys: ['down', 'j'] }),\n select: newBinding({ keys: ['enter'] }),\n back: newBinding({ keys: ['backspace', 'h', 'left'] }),\n open: newBinding({ keys: ['right', 'l'] }),\n toggleHidden: newBinding({ keys: ['.'] }),\n pageUp: newBinding({ keys: ['pgup', 'u'] }),\n pageDown: newBinding({ keys: ['pgdown', 'd'] }),\n gotoTop: newBinding({ keys: ['home', 'g'] }),\n gotoBottom: newBinding({ keys: ['end', 'G'] }),\n shortHelp() {\n return [this.up, this.down, this.select, this.back]\n },\n fullHelp() {\n return [\n [this.up, this.down, this.pageUp, this.pageDown],\n [this.select, this.open, this.back, this.toggleHidden],\n [this.gotoTop, this.gotoBottom],\n ]\n },\n}\n","import type { FileInfo } from './types.js'\n\n/** Directory listing finished (success or failure). @public */\nexport class DirReadMsg {\n readonly _tag = 'filepicker-dir-read'\n\n constructor(\n public readonly path: string,\n public readonly files: FileInfo[],\n public readonly error?: Error,\n ) {}\n}\n\n/** A file or directory was selected. @public */\nexport class FileSelectedMsg {\n readonly _tag = 'filepicker-file-selected'\n\n constructor(public readonly file: FileInfo) {}\n}\n","import { Style } from '@boba-cli/chapstick'\nimport { matches } from '@boba-cli/key'\nimport { msg, type Cmd, type Msg, KeyMsg } from '@boba-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport { readDirectory } from './fs.js'\nimport { defaultKeyMap } from './keymap.js'\nimport { DirReadMsg, FileSelectedMsg } from './messages.js'\nimport type {\n FileInfo,\n FilepickerKeyMap,\n FilepickerOptions,\n FilepickerStyles,\n} from './types.js'\n\ntype FilepickerState = {\n currentDir: string\n files: FileInfo[]\n cursor: number\n selectedFile: FileInfo | null\n showHidden: boolean\n showPermissions: boolean\n showSize: boolean\n dirFirst: boolean\n height: number\n allowedTypes: string[]\n styles: FilepickerStyles\n keyMap: FilepickerKeyMap\n filesystem: FileSystemAdapter\n path: PathAdapter\n}\n\nfunction defaultStyles(): FilepickerStyles {\n return {\n directory: new Style().bold(true),\n file: new Style(),\n hidden: new Style().italic(true),\n selected: new Style().background('#303030').foreground('#ffffff'),\n cursor: new Style().bold(true),\n status: new Style().italic(true),\n }\n}\n\nfunction filterByType(files: FileInfo[], allowed: string[]): FileInfo[] {\n if (!allowed || allowed.length === 0) return files\n return files.filter((f) => {\n if (f.isDir) return true\n return allowed.some((ext) => f.name.endsWith(ext))\n })\n}\n\n/**\n * File system picker with navigation and selection.\n * @public\n */\nexport class FilepickerModel {\n readonly currentDir: string\n readonly files: FileInfo[]\n readonly cursor: number\n readonly selectedFile: FileInfo | null\n readonly showHidden: boolean\n readonly showPermissions: boolean\n readonly showSize: boolean\n readonly dirFirst: boolean\n readonly height: number\n readonly allowedTypes: string[]\n readonly styles: FilepickerStyles\n readonly keyMap: FilepickerKeyMap\n readonly filesystem: FileSystemAdapter\n readonly path: PathAdapter\n\n private constructor(state: FilepickerState) {\n this.currentDir = state.currentDir\n this.files = state.files\n this.cursor = state.cursor\n this.selectedFile = state.selectedFile\n this.showHidden = state.showHidden\n this.showPermissions = state.showPermissions\n this.showSize = state.showSize\n this.dirFirst = state.dirFirst\n this.height = state.height\n this.allowedTypes = state.allowedTypes\n this.styles = state.styles\n this.keyMap = state.keyMap\n this.filesystem = state.filesystem\n this.path = state.path\n }\n\n /** Create a new model and command to read the directory. */\n static new(options: FilepickerOptions): [FilepickerModel, Cmd<Msg>] {\n const styles = { ...defaultStyles(), ...(options.styles ?? {}) }\n const model = new FilepickerModel({\n currentDir: options.currentDir ?? options.filesystem.cwd(),\n files: [],\n cursor: 0,\n selectedFile: null,\n showHidden: options.showHidden ?? false,\n showPermissions: options.showPermissions ?? false,\n showSize: options.showSize ?? false,\n dirFirst: options.dirFirst ?? true,\n height: options.height ?? 0,\n allowedTypes: options.allowedTypes ?? [],\n styles,\n keyMap: options.keyMap ?? defaultKeyMap,\n filesystem: options.filesystem,\n path: options.path,\n })\n return model.refresh()\n }\n\n /** Current selected file (if any). */\n selected(): FileInfo | undefined {\n return this.files[this.cursor]\n }\n\n /** Go to the parent directory. */\n back(): [FilepickerModel, Cmd<Msg>] {\n const parent = this.path.dirname(this.currentDir)\n const model = this.with({ currentDir: parent, cursor: 0 })\n return model.refresh()\n }\n\n /** Enter the highlighted directory, or select a file. */\n enter(): [FilepickerModel, Cmd<Msg>] {\n const file = this.selected()\n if (!file) return [this, null]\n if (file.isDir) {\n const model = this.with({\n currentDir: file.path,\n cursor: 0,\n selectedFile: null,\n })\n return model.refresh()\n }\n return this.select()\n }\n\n /** Refresh the current directory listing. */\n refresh(): [FilepickerModel, Cmd<Msg>] {\n const cmd: Cmd<Msg> = async () => {\n try {\n const files = await readDirectory(\n this.filesystem,\n this.path,\n this.currentDir,\n this.showHidden,\n this.dirFirst,\n )\n const filtered = filterByType(files, this.allowedTypes)\n return new DirReadMsg(this.currentDir, filtered)\n } catch (error) {\n return new DirReadMsg(this.currentDir, [], error as Error)\n }\n }\n return [this, cmd]\n }\n\n /** Select the current file. */\n select(): [FilepickerModel, Cmd<Msg>] {\n const file = this.selected()\n if (!file) return [this, null]\n const model = this.with({ selectedFile: file })\n return [model, msg(new FileSelectedMsg(file))]\n }\n\n /** Move cursor up. */\n cursorUp(): FilepickerModel {\n if (this.files.length === 0) return this\n const next = Math.max(0, this.cursor - 1)\n return this.with({ cursor: next })\n }\n\n /** Move cursor down. */\n cursorDown(): FilepickerModel {\n if (this.files.length === 0) return this\n const next = Math.min(this.files.length - 1, this.cursor + 1)\n return this.with({ cursor: next })\n }\n\n /** Jump to first entry. */\n gotoTop(): FilepickerModel {\n if (this.files.length === 0) return this\n return this.with({ cursor: 0 })\n }\n\n /** Jump to last entry. */\n gotoBottom(): FilepickerModel {\n if (this.files.length === 0) return this\n return this.with({ cursor: this.files.length - 1 })\n }\n\n /** Toggle hidden file visibility. */\n toggleHidden(): [FilepickerModel, Cmd<Msg>] {\n const model = this.with({ showHidden: !this.showHidden, cursor: 0 })\n return model.refresh()\n }\n\n /** Tea init hook; triggers directory read. */\n init(): Cmd<Msg> {\n const [, cmd] = this.refresh()\n return cmd\n }\n\n /** Tea update handler. */\n update(msgObj: Msg): [FilepickerModel, Cmd<Msg>] {\n if (msgObj instanceof DirReadMsg) {\n if (msgObj.error) {\n // Keep state but surface the error via status text\n return [\n this.with({\n files: [],\n cursor: 0,\n selectedFile: null,\n }),\n null,\n ]\n }\n const files = msgObj.files\n const cursor = Math.min(this.cursor, Math.max(0, files.length - 1))\n return [\n this.with({\n currentDir: msgObj.path,\n files,\n cursor,\n selectedFile: files[cursor] ?? null,\n }),\n null,\n ]\n }\n\n if (msgObj instanceof KeyMsg) {\n if (matches(msgObj, this.keyMap.up)) return [this.cursorUp(), null]\n if (matches(msgObj, this.keyMap.down)) return [this.cursorDown(), null]\n if (matches(msgObj, this.keyMap.gotoTop)) return [this.gotoTop(), null]\n if (matches(msgObj, this.keyMap.gotoBottom))\n return [this.gotoBottom(), null]\n if (matches(msgObj, this.keyMap.toggleHidden)) return this.toggleHidden()\n if (matches(msgObj, this.keyMap.back)) return this.back()\n if (matches(msgObj, this.keyMap.open)) return this.enter()\n if (matches(msgObj, this.keyMap.select)) return this.select()\n }\n\n return [this, null]\n }\n\n /** Render the file list. */\n view(): string {\n const lines: string[] = []\n const header = this.styles.status.render(this.currentDir)\n lines.push(header)\n\n if (this.files.length === 0) {\n lines.push(this.styles.status.render('(empty)'))\n return lines.join('\\n')\n }\n\n for (const [index, file] of this.files.entries()) {\n const isSelected = index === this.cursor\n const style = file.isDir ? this.styles.directory : this.styles.file\n const base = file.isHidden ? this.styles.hidden : style\n const name = base.render(file.name)\n const cursor = isSelected ? this.styles.cursor.render('➤ ') : ' '\n const line = isSelected\n ? this.styles.selected.render(cursor + name)\n : cursor + name\n lines.push(line)\n }\n\n return lines.join('\\n')\n }\n\n private with(patch: Partial<FilepickerState>): FilepickerModel {\n return new FilepickerModel({\n currentDir: this.currentDir,\n files: this.files,\n cursor: this.cursor,\n selectedFile: this.selectedFile,\n showHidden: this.showHidden,\n showPermissions: this.showPermissions,\n showSize: this.showSize,\n dirFirst: this.dirFirst,\n height: this.height,\n allowedTypes: this.allowedTypes,\n styles: this.styles,\n keyMap: this.keyMap,\n filesystem: this.filesystem,\n path: this.path,\n ...patch,\n })\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { FileSystemAdapter, PathAdapter } from '@boba-cli/machine';
|
|
2
|
+
import { Style } from '@boba-cli/chapstick';
|
|
3
|
+
import { KeyMap } from '@boba-cli/help';
|
|
4
|
+
import { Binding } from '@boba-cli/key';
|
|
5
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
6
|
+
|
|
7
|
+
/** Metadata for a file system entry. @public */
|
|
8
|
+
interface FileInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
isDir: boolean;
|
|
12
|
+
isHidden: boolean;
|
|
13
|
+
size: number;
|
|
14
|
+
mode: number;
|
|
15
|
+
}
|
|
16
|
+
/** Keyboard bindings for the filepicker. @public */
|
|
17
|
+
interface FilepickerKeyMap extends KeyMap {
|
|
18
|
+
up: Binding;
|
|
19
|
+
down: Binding;
|
|
20
|
+
select: Binding;
|
|
21
|
+
back: Binding;
|
|
22
|
+
open: Binding;
|
|
23
|
+
toggleHidden: Binding;
|
|
24
|
+
pageUp: Binding;
|
|
25
|
+
pageDown: Binding;
|
|
26
|
+
gotoTop: Binding;
|
|
27
|
+
gotoBottom: Binding;
|
|
28
|
+
shortHelp(): Binding[];
|
|
29
|
+
fullHelp(): Binding[][];
|
|
30
|
+
}
|
|
31
|
+
/** Style hooks for rendering the file list. @public */
|
|
32
|
+
interface FilepickerStyles {
|
|
33
|
+
directory: Style;
|
|
34
|
+
file: Style;
|
|
35
|
+
hidden: Style;
|
|
36
|
+
selected: Style;
|
|
37
|
+
cursor: Style;
|
|
38
|
+
status: Style;
|
|
39
|
+
}
|
|
40
|
+
/** Options for constructing a {@link FilepickerModel}. @public */
|
|
41
|
+
interface FilepickerOptions {
|
|
42
|
+
/** FileSystem adapter for file operations */
|
|
43
|
+
filesystem: FileSystemAdapter;
|
|
44
|
+
/** Path adapter for path operations */
|
|
45
|
+
path: PathAdapter;
|
|
46
|
+
currentDir?: string;
|
|
47
|
+
allowedTypes?: string[];
|
|
48
|
+
showHidden?: boolean;
|
|
49
|
+
showPermissions?: boolean;
|
|
50
|
+
showSize?: boolean;
|
|
51
|
+
dirFirst?: boolean;
|
|
52
|
+
height?: number;
|
|
53
|
+
styles?: Partial<FilepickerStyles>;
|
|
54
|
+
keyMap?: FilepickerKeyMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read a directory and return file info entries.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
declare function readDirectory(filesystem: FileSystemAdapter, pathAdapter: PathAdapter, path: string, showHidden: boolean, dirFirst?: boolean): Promise<FileInfo[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Sort directories first then alphabetical.
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
declare function sortFiles(a: FileInfo, b: FileInfo, dirFirst?: boolean): number;
|
|
67
|
+
/**
|
|
68
|
+
* Hidden file detection (Unix-style).
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
declare function isHiddenUnix(name: string): boolean;
|
|
72
|
+
|
|
73
|
+
/** Default filepicker keymap. @public */
|
|
74
|
+
declare const defaultKeyMap: FilepickerKeyMap;
|
|
75
|
+
|
|
76
|
+
/** Directory listing finished (success or failure). @public */
|
|
77
|
+
declare class DirReadMsg {
|
|
78
|
+
readonly path: string;
|
|
79
|
+
readonly files: FileInfo[];
|
|
80
|
+
readonly error?: Error | undefined;
|
|
81
|
+
readonly _tag = "filepicker-dir-read";
|
|
82
|
+
constructor(path: string, files: FileInfo[], error?: Error | undefined);
|
|
83
|
+
}
|
|
84
|
+
/** A file or directory was selected. @public */
|
|
85
|
+
declare class FileSelectedMsg {
|
|
86
|
+
readonly file: FileInfo;
|
|
87
|
+
readonly _tag = "filepicker-file-selected";
|
|
88
|
+
constructor(file: FileInfo);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* File system picker with navigation and selection.
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
declare class FilepickerModel {
|
|
96
|
+
readonly currentDir: string;
|
|
97
|
+
readonly files: FileInfo[];
|
|
98
|
+
readonly cursor: number;
|
|
99
|
+
readonly selectedFile: FileInfo | null;
|
|
100
|
+
readonly showHidden: boolean;
|
|
101
|
+
readonly showPermissions: boolean;
|
|
102
|
+
readonly showSize: boolean;
|
|
103
|
+
readonly dirFirst: boolean;
|
|
104
|
+
readonly height: number;
|
|
105
|
+
readonly allowedTypes: string[];
|
|
106
|
+
readonly styles: FilepickerStyles;
|
|
107
|
+
readonly keyMap: FilepickerKeyMap;
|
|
108
|
+
readonly filesystem: FileSystemAdapter;
|
|
109
|
+
readonly path: PathAdapter;
|
|
110
|
+
private constructor();
|
|
111
|
+
/** Create a new model and command to read the directory. */
|
|
112
|
+
static new(options: FilepickerOptions): [FilepickerModel, Cmd<Msg>];
|
|
113
|
+
/** Current selected file (if any). */
|
|
114
|
+
selected(): FileInfo | undefined;
|
|
115
|
+
/** Go to the parent directory. */
|
|
116
|
+
back(): [FilepickerModel, Cmd<Msg>];
|
|
117
|
+
/** Enter the highlighted directory, or select a file. */
|
|
118
|
+
enter(): [FilepickerModel, Cmd<Msg>];
|
|
119
|
+
/** Refresh the current directory listing. */
|
|
120
|
+
refresh(): [FilepickerModel, Cmd<Msg>];
|
|
121
|
+
/** Select the current file. */
|
|
122
|
+
select(): [FilepickerModel, Cmd<Msg>];
|
|
123
|
+
/** Move cursor up. */
|
|
124
|
+
cursorUp(): FilepickerModel;
|
|
125
|
+
/** Move cursor down. */
|
|
126
|
+
cursorDown(): FilepickerModel;
|
|
127
|
+
/** Jump to first entry. */
|
|
128
|
+
gotoTop(): FilepickerModel;
|
|
129
|
+
/** Jump to last entry. */
|
|
130
|
+
gotoBottom(): FilepickerModel;
|
|
131
|
+
/** Toggle hidden file visibility. */
|
|
132
|
+
toggleHidden(): [FilepickerModel, Cmd<Msg>];
|
|
133
|
+
/** Tea init hook; triggers directory read. */
|
|
134
|
+
init(): Cmd<Msg>;
|
|
135
|
+
/** Tea update handler. */
|
|
136
|
+
update(msgObj: Msg): [FilepickerModel, Cmd<Msg>];
|
|
137
|
+
/** Render the file list. */
|
|
138
|
+
view(): string;
|
|
139
|
+
private with;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { DirReadMsg, type FileInfo, FileSelectedMsg, type FilepickerKeyMap, FilepickerModel, type FilepickerOptions, type FilepickerStyles, defaultKeyMap, isHiddenUnix, readDirectory, sortFiles };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { FileSystemAdapter, PathAdapter } from '@boba-cli/machine';
|
|
2
|
+
import { Style } from '@boba-cli/chapstick';
|
|
3
|
+
import { KeyMap } from '@boba-cli/help';
|
|
4
|
+
import { Binding } from '@boba-cli/key';
|
|
5
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
6
|
+
|
|
7
|
+
/** Metadata for a file system entry. @public */
|
|
8
|
+
interface FileInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
isDir: boolean;
|
|
12
|
+
isHidden: boolean;
|
|
13
|
+
size: number;
|
|
14
|
+
mode: number;
|
|
15
|
+
}
|
|
16
|
+
/** Keyboard bindings for the filepicker. @public */
|
|
17
|
+
interface FilepickerKeyMap extends KeyMap {
|
|
18
|
+
up: Binding;
|
|
19
|
+
down: Binding;
|
|
20
|
+
select: Binding;
|
|
21
|
+
back: Binding;
|
|
22
|
+
open: Binding;
|
|
23
|
+
toggleHidden: Binding;
|
|
24
|
+
pageUp: Binding;
|
|
25
|
+
pageDown: Binding;
|
|
26
|
+
gotoTop: Binding;
|
|
27
|
+
gotoBottom: Binding;
|
|
28
|
+
shortHelp(): Binding[];
|
|
29
|
+
fullHelp(): Binding[][];
|
|
30
|
+
}
|
|
31
|
+
/** Style hooks for rendering the file list. @public */
|
|
32
|
+
interface FilepickerStyles {
|
|
33
|
+
directory: Style;
|
|
34
|
+
file: Style;
|
|
35
|
+
hidden: Style;
|
|
36
|
+
selected: Style;
|
|
37
|
+
cursor: Style;
|
|
38
|
+
status: Style;
|
|
39
|
+
}
|
|
40
|
+
/** Options for constructing a {@link FilepickerModel}. @public */
|
|
41
|
+
interface FilepickerOptions {
|
|
42
|
+
/** FileSystem adapter for file operations */
|
|
43
|
+
filesystem: FileSystemAdapter;
|
|
44
|
+
/** Path adapter for path operations */
|
|
45
|
+
path: PathAdapter;
|
|
46
|
+
currentDir?: string;
|
|
47
|
+
allowedTypes?: string[];
|
|
48
|
+
showHidden?: boolean;
|
|
49
|
+
showPermissions?: boolean;
|
|
50
|
+
showSize?: boolean;
|
|
51
|
+
dirFirst?: boolean;
|
|
52
|
+
height?: number;
|
|
53
|
+
styles?: Partial<FilepickerStyles>;
|
|
54
|
+
keyMap?: FilepickerKeyMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read a directory and return file info entries.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
declare function readDirectory(filesystem: FileSystemAdapter, pathAdapter: PathAdapter, path: string, showHidden: boolean, dirFirst?: boolean): Promise<FileInfo[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Sort directories first then alphabetical.
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
declare function sortFiles(a: FileInfo, b: FileInfo, dirFirst?: boolean): number;
|
|
67
|
+
/**
|
|
68
|
+
* Hidden file detection (Unix-style).
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
declare function isHiddenUnix(name: string): boolean;
|
|
72
|
+
|
|
73
|
+
/** Default filepicker keymap. @public */
|
|
74
|
+
declare const defaultKeyMap: FilepickerKeyMap;
|
|
75
|
+
|
|
76
|
+
/** Directory listing finished (success or failure). @public */
|
|
77
|
+
declare class DirReadMsg {
|
|
78
|
+
readonly path: string;
|
|
79
|
+
readonly files: FileInfo[];
|
|
80
|
+
readonly error?: Error | undefined;
|
|
81
|
+
readonly _tag = "filepicker-dir-read";
|
|
82
|
+
constructor(path: string, files: FileInfo[], error?: Error | undefined);
|
|
83
|
+
}
|
|
84
|
+
/** A file or directory was selected. @public */
|
|
85
|
+
declare class FileSelectedMsg {
|
|
86
|
+
readonly file: FileInfo;
|
|
87
|
+
readonly _tag = "filepicker-file-selected";
|
|
88
|
+
constructor(file: FileInfo);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* File system picker with navigation and selection.
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
declare class FilepickerModel {
|
|
96
|
+
readonly currentDir: string;
|
|
97
|
+
readonly files: FileInfo[];
|
|
98
|
+
readonly cursor: number;
|
|
99
|
+
readonly selectedFile: FileInfo | null;
|
|
100
|
+
readonly showHidden: boolean;
|
|
101
|
+
readonly showPermissions: boolean;
|
|
102
|
+
readonly showSize: boolean;
|
|
103
|
+
readonly dirFirst: boolean;
|
|
104
|
+
readonly height: number;
|
|
105
|
+
readonly allowedTypes: string[];
|
|
106
|
+
readonly styles: FilepickerStyles;
|
|
107
|
+
readonly keyMap: FilepickerKeyMap;
|
|
108
|
+
readonly filesystem: FileSystemAdapter;
|
|
109
|
+
readonly path: PathAdapter;
|
|
110
|
+
private constructor();
|
|
111
|
+
/** Create a new model and command to read the directory. */
|
|
112
|
+
static new(options: FilepickerOptions): [FilepickerModel, Cmd<Msg>];
|
|
113
|
+
/** Current selected file (if any). */
|
|
114
|
+
selected(): FileInfo | undefined;
|
|
115
|
+
/** Go to the parent directory. */
|
|
116
|
+
back(): [FilepickerModel, Cmd<Msg>];
|
|
117
|
+
/** Enter the highlighted directory, or select a file. */
|
|
118
|
+
enter(): [FilepickerModel, Cmd<Msg>];
|
|
119
|
+
/** Refresh the current directory listing. */
|
|
120
|
+
refresh(): [FilepickerModel, Cmd<Msg>];
|
|
121
|
+
/** Select the current file. */
|
|
122
|
+
select(): [FilepickerModel, Cmd<Msg>];
|
|
123
|
+
/** Move cursor up. */
|
|
124
|
+
cursorUp(): FilepickerModel;
|
|
125
|
+
/** Move cursor down. */
|
|
126
|
+
cursorDown(): FilepickerModel;
|
|
127
|
+
/** Jump to first entry. */
|
|
128
|
+
gotoTop(): FilepickerModel;
|
|
129
|
+
/** Jump to last entry. */
|
|
130
|
+
gotoBottom(): FilepickerModel;
|
|
131
|
+
/** Toggle hidden file visibility. */
|
|
132
|
+
toggleHidden(): [FilepickerModel, Cmd<Msg>];
|
|
133
|
+
/** Tea init hook; triggers directory read. */
|
|
134
|
+
init(): Cmd<Msg>;
|
|
135
|
+
/** Tea update handler. */
|
|
136
|
+
update(msgObj: Msg): [FilepickerModel, Cmd<Msg>];
|
|
137
|
+
/** Render the file list. */
|
|
138
|
+
view(): string;
|
|
139
|
+
private with;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { DirReadMsg, type FileInfo, FileSelectedMsg, type FilepickerKeyMap, FilepickerModel, type FilepickerOptions, type FilepickerStyles, defaultKeyMap, isHiddenUnix, readDirectory, sortFiles };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { newBinding, matches } from '@boba-cli/key';
|
|
2
|
+
import { Style } from '@boba-cli/chapstick';
|
|
3
|
+
import { msg, KeyMsg } from '@boba-cli/tea';
|
|
4
|
+
|
|
5
|
+
// src/fs.ts
|
|
6
|
+
async function readDirectory(filesystem, pathAdapter, path, showHidden, dirFirst = true) {
|
|
7
|
+
const result = await filesystem.readdir(path, { withFileTypes: true });
|
|
8
|
+
if (typeof result[0] === "string") {
|
|
9
|
+
throw new Error("Expected DirectoryEntry array but got string array");
|
|
10
|
+
}
|
|
11
|
+
const entries = result;
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
const isHidden = isHiddenUnix(entry.name);
|
|
15
|
+
if (!showHidden && isHidden) continue;
|
|
16
|
+
const fullPath = pathAdapter.join(path, entry.name);
|
|
17
|
+
const stats = await filesystem.stat(fullPath);
|
|
18
|
+
files.push({
|
|
19
|
+
name: entry.name,
|
|
20
|
+
path: fullPath,
|
|
21
|
+
isDir: entry.isDirectory(),
|
|
22
|
+
isHidden,
|
|
23
|
+
size: stats.size,
|
|
24
|
+
mode: stats.mode
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return files.sort((a, b) => sortFiles(a, b, dirFirst));
|
|
28
|
+
}
|
|
29
|
+
function sortFiles(a, b, dirFirst = true) {
|
|
30
|
+
if (dirFirst) {
|
|
31
|
+
if (a.isDir && !b.isDir) return -1;
|
|
32
|
+
if (!a.isDir && b.isDir) return 1;
|
|
33
|
+
}
|
|
34
|
+
return a.name.localeCompare(b.name);
|
|
35
|
+
}
|
|
36
|
+
function isHiddenUnix(name) {
|
|
37
|
+
return name.startsWith(".");
|
|
38
|
+
}
|
|
39
|
+
var defaultKeyMap = {
|
|
40
|
+
up: newBinding({ keys: ["up", "k"] }),
|
|
41
|
+
down: newBinding({ keys: ["down", "j"] }),
|
|
42
|
+
select: newBinding({ keys: ["enter"] }),
|
|
43
|
+
back: newBinding({ keys: ["backspace", "h", "left"] }),
|
|
44
|
+
open: newBinding({ keys: ["right", "l"] }),
|
|
45
|
+
toggleHidden: newBinding({ keys: ["."] }),
|
|
46
|
+
pageUp: newBinding({ keys: ["pgup", "u"] }),
|
|
47
|
+
pageDown: newBinding({ keys: ["pgdown", "d"] }),
|
|
48
|
+
gotoTop: newBinding({ keys: ["home", "g"] }),
|
|
49
|
+
gotoBottom: newBinding({ keys: ["end", "G"] }),
|
|
50
|
+
shortHelp() {
|
|
51
|
+
return [this.up, this.down, this.select, this.back];
|
|
52
|
+
},
|
|
53
|
+
fullHelp() {
|
|
54
|
+
return [
|
|
55
|
+
[this.up, this.down, this.pageUp, this.pageDown],
|
|
56
|
+
[this.select, this.open, this.back, this.toggleHidden],
|
|
57
|
+
[this.gotoTop, this.gotoBottom]
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/messages.ts
|
|
63
|
+
var DirReadMsg = class {
|
|
64
|
+
constructor(path, files, error) {
|
|
65
|
+
this.path = path;
|
|
66
|
+
this.files = files;
|
|
67
|
+
this.error = error;
|
|
68
|
+
}
|
|
69
|
+
_tag = "filepicker-dir-read";
|
|
70
|
+
};
|
|
71
|
+
var FileSelectedMsg = class {
|
|
72
|
+
constructor(file) {
|
|
73
|
+
this.file = file;
|
|
74
|
+
}
|
|
75
|
+
_tag = "filepicker-file-selected";
|
|
76
|
+
};
|
|
77
|
+
function defaultStyles() {
|
|
78
|
+
return {
|
|
79
|
+
directory: new Style().bold(true),
|
|
80
|
+
file: new Style(),
|
|
81
|
+
hidden: new Style().italic(true),
|
|
82
|
+
selected: new Style().background("#303030").foreground("#ffffff"),
|
|
83
|
+
cursor: new Style().bold(true),
|
|
84
|
+
status: new Style().italic(true)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function filterByType(files, allowed) {
|
|
88
|
+
if (!allowed || allowed.length === 0) return files;
|
|
89
|
+
return files.filter((f) => {
|
|
90
|
+
if (f.isDir) return true;
|
|
91
|
+
return allowed.some((ext) => f.name.endsWith(ext));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
var FilepickerModel = class _FilepickerModel {
|
|
95
|
+
currentDir;
|
|
96
|
+
files;
|
|
97
|
+
cursor;
|
|
98
|
+
selectedFile;
|
|
99
|
+
showHidden;
|
|
100
|
+
showPermissions;
|
|
101
|
+
showSize;
|
|
102
|
+
dirFirst;
|
|
103
|
+
height;
|
|
104
|
+
allowedTypes;
|
|
105
|
+
styles;
|
|
106
|
+
keyMap;
|
|
107
|
+
filesystem;
|
|
108
|
+
path;
|
|
109
|
+
constructor(state) {
|
|
110
|
+
this.currentDir = state.currentDir;
|
|
111
|
+
this.files = state.files;
|
|
112
|
+
this.cursor = state.cursor;
|
|
113
|
+
this.selectedFile = state.selectedFile;
|
|
114
|
+
this.showHidden = state.showHidden;
|
|
115
|
+
this.showPermissions = state.showPermissions;
|
|
116
|
+
this.showSize = state.showSize;
|
|
117
|
+
this.dirFirst = state.dirFirst;
|
|
118
|
+
this.height = state.height;
|
|
119
|
+
this.allowedTypes = state.allowedTypes;
|
|
120
|
+
this.styles = state.styles;
|
|
121
|
+
this.keyMap = state.keyMap;
|
|
122
|
+
this.filesystem = state.filesystem;
|
|
123
|
+
this.path = state.path;
|
|
124
|
+
}
|
|
125
|
+
/** Create a new model and command to read the directory. */
|
|
126
|
+
static new(options) {
|
|
127
|
+
const styles = { ...defaultStyles(), ...options.styles ?? {} };
|
|
128
|
+
const model = new _FilepickerModel({
|
|
129
|
+
currentDir: options.currentDir ?? options.filesystem.cwd(),
|
|
130
|
+
files: [],
|
|
131
|
+
cursor: 0,
|
|
132
|
+
selectedFile: null,
|
|
133
|
+
showHidden: options.showHidden ?? false,
|
|
134
|
+
showPermissions: options.showPermissions ?? false,
|
|
135
|
+
showSize: options.showSize ?? false,
|
|
136
|
+
dirFirst: options.dirFirst ?? true,
|
|
137
|
+
height: options.height ?? 0,
|
|
138
|
+
allowedTypes: options.allowedTypes ?? [],
|
|
139
|
+
styles,
|
|
140
|
+
keyMap: options.keyMap ?? defaultKeyMap,
|
|
141
|
+
filesystem: options.filesystem,
|
|
142
|
+
path: options.path
|
|
143
|
+
});
|
|
144
|
+
return model.refresh();
|
|
145
|
+
}
|
|
146
|
+
/** Current selected file (if any). */
|
|
147
|
+
selected() {
|
|
148
|
+
return this.files[this.cursor];
|
|
149
|
+
}
|
|
150
|
+
/** Go to the parent directory. */
|
|
151
|
+
back() {
|
|
152
|
+
const parent = this.path.dirname(this.currentDir);
|
|
153
|
+
const model = this.with({ currentDir: parent, cursor: 0 });
|
|
154
|
+
return model.refresh();
|
|
155
|
+
}
|
|
156
|
+
/** Enter the highlighted directory, or select a file. */
|
|
157
|
+
enter() {
|
|
158
|
+
const file = this.selected();
|
|
159
|
+
if (!file) return [this, null];
|
|
160
|
+
if (file.isDir) {
|
|
161
|
+
const model = this.with({
|
|
162
|
+
currentDir: file.path,
|
|
163
|
+
cursor: 0,
|
|
164
|
+
selectedFile: null
|
|
165
|
+
});
|
|
166
|
+
return model.refresh();
|
|
167
|
+
}
|
|
168
|
+
return this.select();
|
|
169
|
+
}
|
|
170
|
+
/** Refresh the current directory listing. */
|
|
171
|
+
refresh() {
|
|
172
|
+
const cmd = async () => {
|
|
173
|
+
try {
|
|
174
|
+
const files = await readDirectory(
|
|
175
|
+
this.filesystem,
|
|
176
|
+
this.path,
|
|
177
|
+
this.currentDir,
|
|
178
|
+
this.showHidden,
|
|
179
|
+
this.dirFirst
|
|
180
|
+
);
|
|
181
|
+
const filtered = filterByType(files, this.allowedTypes);
|
|
182
|
+
return new DirReadMsg(this.currentDir, filtered);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return new DirReadMsg(this.currentDir, [], error);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return [this, cmd];
|
|
188
|
+
}
|
|
189
|
+
/** Select the current file. */
|
|
190
|
+
select() {
|
|
191
|
+
const file = this.selected();
|
|
192
|
+
if (!file) return [this, null];
|
|
193
|
+
const model = this.with({ selectedFile: file });
|
|
194
|
+
return [model, msg(new FileSelectedMsg(file))];
|
|
195
|
+
}
|
|
196
|
+
/** Move cursor up. */
|
|
197
|
+
cursorUp() {
|
|
198
|
+
if (this.files.length === 0) return this;
|
|
199
|
+
const next = Math.max(0, this.cursor - 1);
|
|
200
|
+
return this.with({ cursor: next });
|
|
201
|
+
}
|
|
202
|
+
/** Move cursor down. */
|
|
203
|
+
cursorDown() {
|
|
204
|
+
if (this.files.length === 0) return this;
|
|
205
|
+
const next = Math.min(this.files.length - 1, this.cursor + 1);
|
|
206
|
+
return this.with({ cursor: next });
|
|
207
|
+
}
|
|
208
|
+
/** Jump to first entry. */
|
|
209
|
+
gotoTop() {
|
|
210
|
+
if (this.files.length === 0) return this;
|
|
211
|
+
return this.with({ cursor: 0 });
|
|
212
|
+
}
|
|
213
|
+
/** Jump to last entry. */
|
|
214
|
+
gotoBottom() {
|
|
215
|
+
if (this.files.length === 0) return this;
|
|
216
|
+
return this.with({ cursor: this.files.length - 1 });
|
|
217
|
+
}
|
|
218
|
+
/** Toggle hidden file visibility. */
|
|
219
|
+
toggleHidden() {
|
|
220
|
+
const model = this.with({ showHidden: !this.showHidden, cursor: 0 });
|
|
221
|
+
return model.refresh();
|
|
222
|
+
}
|
|
223
|
+
/** Tea init hook; triggers directory read. */
|
|
224
|
+
init() {
|
|
225
|
+
const [, cmd] = this.refresh();
|
|
226
|
+
return cmd;
|
|
227
|
+
}
|
|
228
|
+
/** Tea update handler. */
|
|
229
|
+
update(msgObj) {
|
|
230
|
+
if (msgObj instanceof DirReadMsg) {
|
|
231
|
+
if (msgObj.error) {
|
|
232
|
+
return [
|
|
233
|
+
this.with({
|
|
234
|
+
files: [],
|
|
235
|
+
cursor: 0,
|
|
236
|
+
selectedFile: null
|
|
237
|
+
}),
|
|
238
|
+
null
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
const files = msgObj.files;
|
|
242
|
+
const cursor = Math.min(this.cursor, Math.max(0, files.length - 1));
|
|
243
|
+
return [
|
|
244
|
+
this.with({
|
|
245
|
+
currentDir: msgObj.path,
|
|
246
|
+
files,
|
|
247
|
+
cursor,
|
|
248
|
+
selectedFile: files[cursor] ?? null
|
|
249
|
+
}),
|
|
250
|
+
null
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
if (msgObj instanceof KeyMsg) {
|
|
254
|
+
if (matches(msgObj, this.keyMap.up)) return [this.cursorUp(), null];
|
|
255
|
+
if (matches(msgObj, this.keyMap.down)) return [this.cursorDown(), null];
|
|
256
|
+
if (matches(msgObj, this.keyMap.gotoTop)) return [this.gotoTop(), null];
|
|
257
|
+
if (matches(msgObj, this.keyMap.gotoBottom))
|
|
258
|
+
return [this.gotoBottom(), null];
|
|
259
|
+
if (matches(msgObj, this.keyMap.toggleHidden)) return this.toggleHidden();
|
|
260
|
+
if (matches(msgObj, this.keyMap.back)) return this.back();
|
|
261
|
+
if (matches(msgObj, this.keyMap.open)) return this.enter();
|
|
262
|
+
if (matches(msgObj, this.keyMap.select)) return this.select();
|
|
263
|
+
}
|
|
264
|
+
return [this, null];
|
|
265
|
+
}
|
|
266
|
+
/** Render the file list. */
|
|
267
|
+
view() {
|
|
268
|
+
const lines = [];
|
|
269
|
+
const header = this.styles.status.render(this.currentDir);
|
|
270
|
+
lines.push(header);
|
|
271
|
+
if (this.files.length === 0) {
|
|
272
|
+
lines.push(this.styles.status.render("(empty)"));
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
for (const [index, file] of this.files.entries()) {
|
|
276
|
+
const isSelected = index === this.cursor;
|
|
277
|
+
const style = file.isDir ? this.styles.directory : this.styles.file;
|
|
278
|
+
const base = file.isHidden ? this.styles.hidden : style;
|
|
279
|
+
const name = base.render(file.name);
|
|
280
|
+
const cursor = isSelected ? this.styles.cursor.render("\u27A4 ") : " ";
|
|
281
|
+
const line = isSelected ? this.styles.selected.render(cursor + name) : cursor + name;
|
|
282
|
+
lines.push(line);
|
|
283
|
+
}
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
286
|
+
with(patch) {
|
|
287
|
+
return new _FilepickerModel({
|
|
288
|
+
currentDir: this.currentDir,
|
|
289
|
+
files: this.files,
|
|
290
|
+
cursor: this.cursor,
|
|
291
|
+
selectedFile: this.selectedFile,
|
|
292
|
+
showHidden: this.showHidden,
|
|
293
|
+
showPermissions: this.showPermissions,
|
|
294
|
+
showSize: this.showSize,
|
|
295
|
+
dirFirst: this.dirFirst,
|
|
296
|
+
height: this.height,
|
|
297
|
+
allowedTypes: this.allowedTypes,
|
|
298
|
+
styles: this.styles,
|
|
299
|
+
keyMap: this.keyMap,
|
|
300
|
+
filesystem: this.filesystem,
|
|
301
|
+
path: this.path,
|
|
302
|
+
...patch
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export { DirReadMsg, FileSelectedMsg, FilepickerModel, defaultKeyMap, isHiddenUnix, readDirectory, sortFiles };
|
|
308
|
+
//# sourceMappingURL=index.js.map
|
|
309
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fs.ts","../src/keymap.ts","../src/messages.ts","../src/model.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport type { FileInfo } from './types.js'\n\n/**\n * Read a directory and return file info entries.\n * @public\n */\nexport async function readDirectory(\n filesystem: FileSystemAdapter,\n pathAdapter: PathAdapter,\n path: string,\n showHidden: boolean,\n dirFirst = true,\n): Promise<FileInfo[]> {\n const result = await filesystem.readdir(path, { withFileTypes: true })\n\n // Type guard to ensure we got DirectoryEntry[]\n if (typeof result[0] === 'string') {\n throw new Error('Expected DirectoryEntry array but got string array')\n }\n\n const entries = result as Array<{\n readonly name: string\n isDirectory(): boolean\n isFile(): boolean\n isSymbolicLink(): boolean\n }>\n\n const files: FileInfo[] = []\n\n for (const entry of entries) {\n const isHidden = isHiddenUnix(entry.name)\n if (!showHidden && isHidden) continue\n\n const fullPath = pathAdapter.join(path, entry.name)\n const stats = await filesystem.stat(fullPath)\n\n files.push({\n name: entry.name,\n path: fullPath,\n isDir: entry.isDirectory(),\n isHidden,\n size: stats.size,\n mode: stats.mode,\n })\n }\n\n return files.sort((a, b) => sortFiles(a, b, dirFirst))\n}\n\n/**\n * Sort directories first then alphabetical.\n * @public\n */\nexport function sortFiles(a: FileInfo, b: FileInfo, dirFirst = true): number {\n if (dirFirst) {\n if (a.isDir && !b.isDir) return -1\n if (!a.isDir && b.isDir) return 1\n }\n return a.name.localeCompare(b.name)\n}\n\n/**\n * Hidden file detection (Unix-style).\n * @public\n */\nexport function isHiddenUnix(name: string): boolean {\n return name.startsWith('.')\n}\n","import { newBinding } from '@boba-cli/key'\nimport type { FilepickerKeyMap } from './types.js'\n\n/** Default filepicker keymap. @public */\nexport const defaultKeyMap: FilepickerKeyMap = {\n up: newBinding({ keys: ['up', 'k'] }),\n down: newBinding({ keys: ['down', 'j'] }),\n select: newBinding({ keys: ['enter'] }),\n back: newBinding({ keys: ['backspace', 'h', 'left'] }),\n open: newBinding({ keys: ['right', 'l'] }),\n toggleHidden: newBinding({ keys: ['.'] }),\n pageUp: newBinding({ keys: ['pgup', 'u'] }),\n pageDown: newBinding({ keys: ['pgdown', 'd'] }),\n gotoTop: newBinding({ keys: ['home', 'g'] }),\n gotoBottom: newBinding({ keys: ['end', 'G'] }),\n shortHelp() {\n return [this.up, this.down, this.select, this.back]\n },\n fullHelp() {\n return [\n [this.up, this.down, this.pageUp, this.pageDown],\n [this.select, this.open, this.back, this.toggleHidden],\n [this.gotoTop, this.gotoBottom],\n ]\n },\n}\n","import type { FileInfo } from './types.js'\n\n/** Directory listing finished (success or failure). @public */\nexport class DirReadMsg {\n readonly _tag = 'filepicker-dir-read'\n\n constructor(\n public readonly path: string,\n public readonly files: FileInfo[],\n public readonly error?: Error,\n ) {}\n}\n\n/** A file or directory was selected. @public */\nexport class FileSelectedMsg {\n readonly _tag = 'filepicker-file-selected'\n\n constructor(public readonly file: FileInfo) {}\n}\n","import { Style } from '@boba-cli/chapstick'\nimport { matches } from '@boba-cli/key'\nimport { msg, type Cmd, type Msg, KeyMsg } from '@boba-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport { readDirectory } from './fs.js'\nimport { defaultKeyMap } from './keymap.js'\nimport { DirReadMsg, FileSelectedMsg } from './messages.js'\nimport type {\n FileInfo,\n FilepickerKeyMap,\n FilepickerOptions,\n FilepickerStyles,\n} from './types.js'\n\ntype FilepickerState = {\n currentDir: string\n files: FileInfo[]\n cursor: number\n selectedFile: FileInfo | null\n showHidden: boolean\n showPermissions: boolean\n showSize: boolean\n dirFirst: boolean\n height: number\n allowedTypes: string[]\n styles: FilepickerStyles\n keyMap: FilepickerKeyMap\n filesystem: FileSystemAdapter\n path: PathAdapter\n}\n\nfunction defaultStyles(): FilepickerStyles {\n return {\n directory: new Style().bold(true),\n file: new Style(),\n hidden: new Style().italic(true),\n selected: new Style().background('#303030').foreground('#ffffff'),\n cursor: new Style().bold(true),\n status: new Style().italic(true),\n }\n}\n\nfunction filterByType(files: FileInfo[], allowed: string[]): FileInfo[] {\n if (!allowed || allowed.length === 0) return files\n return files.filter((f) => {\n if (f.isDir) return true\n return allowed.some((ext) => f.name.endsWith(ext))\n })\n}\n\n/**\n * File system picker with navigation and selection.\n * @public\n */\nexport class FilepickerModel {\n readonly currentDir: string\n readonly files: FileInfo[]\n readonly cursor: number\n readonly selectedFile: FileInfo | null\n readonly showHidden: boolean\n readonly showPermissions: boolean\n readonly showSize: boolean\n readonly dirFirst: boolean\n readonly height: number\n readonly allowedTypes: string[]\n readonly styles: FilepickerStyles\n readonly keyMap: FilepickerKeyMap\n readonly filesystem: FileSystemAdapter\n readonly path: PathAdapter\n\n private constructor(state: FilepickerState) {\n this.currentDir = state.currentDir\n this.files = state.files\n this.cursor = state.cursor\n this.selectedFile = state.selectedFile\n this.showHidden = state.showHidden\n this.showPermissions = state.showPermissions\n this.showSize = state.showSize\n this.dirFirst = state.dirFirst\n this.height = state.height\n this.allowedTypes = state.allowedTypes\n this.styles = state.styles\n this.keyMap = state.keyMap\n this.filesystem = state.filesystem\n this.path = state.path\n }\n\n /** Create a new model and command to read the directory. */\n static new(options: FilepickerOptions): [FilepickerModel, Cmd<Msg>] {\n const styles = { ...defaultStyles(), ...(options.styles ?? {}) }\n const model = new FilepickerModel({\n currentDir: options.currentDir ?? options.filesystem.cwd(),\n files: [],\n cursor: 0,\n selectedFile: null,\n showHidden: options.showHidden ?? false,\n showPermissions: options.showPermissions ?? false,\n showSize: options.showSize ?? false,\n dirFirst: options.dirFirst ?? true,\n height: options.height ?? 0,\n allowedTypes: options.allowedTypes ?? [],\n styles,\n keyMap: options.keyMap ?? defaultKeyMap,\n filesystem: options.filesystem,\n path: options.path,\n })\n return model.refresh()\n }\n\n /** Current selected file (if any). */\n selected(): FileInfo | undefined {\n return this.files[this.cursor]\n }\n\n /** Go to the parent directory. */\n back(): [FilepickerModel, Cmd<Msg>] {\n const parent = this.path.dirname(this.currentDir)\n const model = this.with({ currentDir: parent, cursor: 0 })\n return model.refresh()\n }\n\n /** Enter the highlighted directory, or select a file. */\n enter(): [FilepickerModel, Cmd<Msg>] {\n const file = this.selected()\n if (!file) return [this, null]\n if (file.isDir) {\n const model = this.with({\n currentDir: file.path,\n cursor: 0,\n selectedFile: null,\n })\n return model.refresh()\n }\n return this.select()\n }\n\n /** Refresh the current directory listing. */\n refresh(): [FilepickerModel, Cmd<Msg>] {\n const cmd: Cmd<Msg> = async () => {\n try {\n const files = await readDirectory(\n this.filesystem,\n this.path,\n this.currentDir,\n this.showHidden,\n this.dirFirst,\n )\n const filtered = filterByType(files, this.allowedTypes)\n return new DirReadMsg(this.currentDir, filtered)\n } catch (error) {\n return new DirReadMsg(this.currentDir, [], error as Error)\n }\n }\n return [this, cmd]\n }\n\n /** Select the current file. */\n select(): [FilepickerModel, Cmd<Msg>] {\n const file = this.selected()\n if (!file) return [this, null]\n const model = this.with({ selectedFile: file })\n return [model, msg(new FileSelectedMsg(file))]\n }\n\n /** Move cursor up. */\n cursorUp(): FilepickerModel {\n if (this.files.length === 0) return this\n const next = Math.max(0, this.cursor - 1)\n return this.with({ cursor: next })\n }\n\n /** Move cursor down. */\n cursorDown(): FilepickerModel {\n if (this.files.length === 0) return this\n const next = Math.min(this.files.length - 1, this.cursor + 1)\n return this.with({ cursor: next })\n }\n\n /** Jump to first entry. */\n gotoTop(): FilepickerModel {\n if (this.files.length === 0) return this\n return this.with({ cursor: 0 })\n }\n\n /** Jump to last entry. */\n gotoBottom(): FilepickerModel {\n if (this.files.length === 0) return this\n return this.with({ cursor: this.files.length - 1 })\n }\n\n /** Toggle hidden file visibility. */\n toggleHidden(): [FilepickerModel, Cmd<Msg>] {\n const model = this.with({ showHidden: !this.showHidden, cursor: 0 })\n return model.refresh()\n }\n\n /** Tea init hook; triggers directory read. */\n init(): Cmd<Msg> {\n const [, cmd] = this.refresh()\n return cmd\n }\n\n /** Tea update handler. */\n update(msgObj: Msg): [FilepickerModel, Cmd<Msg>] {\n if (msgObj instanceof DirReadMsg) {\n if (msgObj.error) {\n // Keep state but surface the error via status text\n return [\n this.with({\n files: [],\n cursor: 0,\n selectedFile: null,\n }),\n null,\n ]\n }\n const files = msgObj.files\n const cursor = Math.min(this.cursor, Math.max(0, files.length - 1))\n return [\n this.with({\n currentDir: msgObj.path,\n files,\n cursor,\n selectedFile: files[cursor] ?? null,\n }),\n null,\n ]\n }\n\n if (msgObj instanceof KeyMsg) {\n if (matches(msgObj, this.keyMap.up)) return [this.cursorUp(), null]\n if (matches(msgObj, this.keyMap.down)) return [this.cursorDown(), null]\n if (matches(msgObj, this.keyMap.gotoTop)) return [this.gotoTop(), null]\n if (matches(msgObj, this.keyMap.gotoBottom))\n return [this.gotoBottom(), null]\n if (matches(msgObj, this.keyMap.toggleHidden)) return this.toggleHidden()\n if (matches(msgObj, this.keyMap.back)) return this.back()\n if (matches(msgObj, this.keyMap.open)) return this.enter()\n if (matches(msgObj, this.keyMap.select)) return this.select()\n }\n\n return [this, null]\n }\n\n /** Render the file list. */\n view(): string {\n const lines: string[] = []\n const header = this.styles.status.render(this.currentDir)\n lines.push(header)\n\n if (this.files.length === 0) {\n lines.push(this.styles.status.render('(empty)'))\n return lines.join('\\n')\n }\n\n for (const [index, file] of this.files.entries()) {\n const isSelected = index === this.cursor\n const style = file.isDir ? this.styles.directory : this.styles.file\n const base = file.isHidden ? this.styles.hidden : style\n const name = base.render(file.name)\n const cursor = isSelected ? this.styles.cursor.render('➤ ') : ' '\n const line = isSelected\n ? this.styles.selected.render(cursor + name)\n : cursor + name\n lines.push(line)\n }\n\n return lines.join('\\n')\n }\n\n private with(patch: Partial<FilepickerState>): FilepickerModel {\n return new FilepickerModel({\n currentDir: this.currentDir,\n files: this.files,\n cursor: this.cursor,\n selectedFile: this.selectedFile,\n showHidden: this.showHidden,\n showPermissions: this.showPermissions,\n showSize: this.showSize,\n dirFirst: this.dirFirst,\n height: this.height,\n allowedTypes: this.allowedTypes,\n styles: this.styles,\n keyMap: this.keyMap,\n filesystem: this.filesystem,\n path: this.path,\n ...patch,\n })\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boba-cli/filepicker",
|
|
3
|
+
"description": "File system browser component for Boba terminal UIs.",
|
|
4
|
+
"version": "0.1.0-alpha.2",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@boba-cli/chapstick": "0.1.0-alpha.2",
|
|
7
|
+
"@boba-cli/help": "0.1.0-alpha.2",
|
|
8
|
+
"@boba-cli/key": "0.1.0-alpha.1",
|
|
9
|
+
"@boba-cli/machine": "0.1.0-alpha.1",
|
|
10
|
+
"@boba-cli/tea": "0.1.0-alpha.1"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^24.10.2",
|
|
14
|
+
"typescript": "5.8.2",
|
|
15
|
+
"vitest": "^4.0.16"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.0.0"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"default": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"main": "./dist/index.cjs",
|
|
37
|
+
"module": "./dist/index.js",
|
|
38
|
+
"type": "module",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"check:api-report": "pnpm run generate:api-report",
|
|
43
|
+
"check:eslint": "pnpm run lint",
|
|
44
|
+
"generate:api-report": "api-extractor run --local",
|
|
45
|
+
"lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
}
|
|
48
|
+
}
|