ftreeview 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/cli.js +1619 -0
- package/package.json +58 -0
- package/src/App.jsx +243 -0
- package/src/cli.js +228 -0
- package/src/components/StatusBar.jsx +270 -0
- package/src/components/TreeLine.jsx +190 -0
- package/src/components/TreeView.jsx +129 -0
- package/src/hooks/useChangedFiles.js +82 -0
- package/src/hooks/useGitStatus.js +347 -0
- package/src/hooks/useIgnore.js +182 -0
- package/src/hooks/useNavigation.js +247 -0
- package/src/hooks/useTree.js +508 -0
- package/src/hooks/useWatcher.js +129 -0
- package/src/index.js +22 -0
- package/src/lib/changeStatus.js +79 -0
- package/src/lib/connectors.js +233 -0
- package/src/lib/constants.js +64 -0
- package/src/lib/icons.js +658 -0
- package/src/lib/theme.js +102 -0
package/src/lib/icons.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon and color mapping for ftree file explorer
|
|
3
|
+
*
|
|
4
|
+
* Provides emoji icons and chalk styles based on file types,
|
|
5
|
+
* extensions, and attributes (executable, hidden, symlink).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ASCII fallback icons for --no-icons mode
|
|
12
|
+
*/
|
|
13
|
+
const ASCII_ICONS = {
|
|
14
|
+
/** Collapsed directory indicator */
|
|
15
|
+
COLLAPSED_DIR: '▸',
|
|
16
|
+
/** Expanded directory indicator */
|
|
17
|
+
EXPANDED_DIR: '▾',
|
|
18
|
+
/** File indicator */
|
|
19
|
+
FILE: '·',
|
|
20
|
+
/** Error indicator */
|
|
21
|
+
ERROR: '⛔',
|
|
22
|
+
/** Symlink indicator */
|
|
23
|
+
SYMLINK: '↗',
|
|
24
|
+
/** Socket indicator */
|
|
25
|
+
SOCKET: 's',
|
|
26
|
+
/** FIFO indicator */
|
|
27
|
+
FIFO: 'p',
|
|
28
|
+
/** Block device indicator */
|
|
29
|
+
BLOCK_DEVICE: 'b',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Nerd Font icon set (requires a Nerd Font in the terminal).
|
|
34
|
+
* We keep this intentionally small; unknown types fall back to a generic file icon.
|
|
35
|
+
*/
|
|
36
|
+
const NERD_ICONS = {
|
|
37
|
+
/** Collapsed directory */
|
|
38
|
+
COLLAPSED_DIR: '',
|
|
39
|
+
/** Expanded directory */
|
|
40
|
+
EXPANDED_DIR: '',
|
|
41
|
+
/** File */
|
|
42
|
+
FILE: '',
|
|
43
|
+
/** Error */
|
|
44
|
+
ERROR: '',
|
|
45
|
+
/** Symlink */
|
|
46
|
+
SYMLINK: '',
|
|
47
|
+
/** Socket */
|
|
48
|
+
SOCKET: '',
|
|
49
|
+
/** FIFO */
|
|
50
|
+
FIFO: '',
|
|
51
|
+
/** Block device */
|
|
52
|
+
BLOCK_DEVICE: '',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const NERD_EXTENSION_ICONS = {
|
|
56
|
+
// JS/TS
|
|
57
|
+
'js': '',
|
|
58
|
+
'mjs': '',
|
|
59
|
+
'cjs': '',
|
|
60
|
+
'ts': '',
|
|
61
|
+
'tsx': '',
|
|
62
|
+
'jsx': '',
|
|
63
|
+
|
|
64
|
+
// Web
|
|
65
|
+
'html': '',
|
|
66
|
+
'css': '',
|
|
67
|
+
'scss': '',
|
|
68
|
+
'sass': '',
|
|
69
|
+
|
|
70
|
+
// Data/config
|
|
71
|
+
'json': '',
|
|
72
|
+
'yml': '',
|
|
73
|
+
'yaml': '',
|
|
74
|
+
|
|
75
|
+
// Docs
|
|
76
|
+
'md': '',
|
|
77
|
+
|
|
78
|
+
// Languages
|
|
79
|
+
'py': '',
|
|
80
|
+
'go': '',
|
|
81
|
+
'rs': '',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const NERD_SPECIAL_FILE_ICONS = {
|
|
85
|
+
'package.json': '',
|
|
86
|
+
'package-lock.json': '',
|
|
87
|
+
'README.md': '',
|
|
88
|
+
'LICENSE': '',
|
|
89
|
+
'.gitignore': '',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extension to icon mapping
|
|
94
|
+
* Maps file extensions to their representative emoji icons
|
|
95
|
+
*/
|
|
96
|
+
const EXTENSION_ICONS = {
|
|
97
|
+
// JavaScript/Node files
|
|
98
|
+
'js': '📜',
|
|
99
|
+
'mjs': '📜',
|
|
100
|
+
'cjs': '📜',
|
|
101
|
+
|
|
102
|
+
// TypeScript files
|
|
103
|
+
'ts': '🔷',
|
|
104
|
+
'tsx': '🔷',
|
|
105
|
+
|
|
106
|
+
// React JSX
|
|
107
|
+
'jsx': '⚛️',
|
|
108
|
+
|
|
109
|
+
// Python
|
|
110
|
+
'py': '🐍',
|
|
111
|
+
'pyi': '🐍',
|
|
112
|
+
|
|
113
|
+
// Go
|
|
114
|
+
'go': '🐹',
|
|
115
|
+
|
|
116
|
+
// Rust
|
|
117
|
+
'rs': '🦀',
|
|
118
|
+
|
|
119
|
+
// Documentation
|
|
120
|
+
'md': '📝',
|
|
121
|
+
'markdown': '📝',
|
|
122
|
+
'rst': '📝',
|
|
123
|
+
|
|
124
|
+
// Configuration files
|
|
125
|
+
'json': '🔧',
|
|
126
|
+
'yaml': '⚙️',
|
|
127
|
+
'yml': '⚙️',
|
|
128
|
+
'toml': '⚙️',
|
|
129
|
+
'ini': '⚙️',
|
|
130
|
+
'conf': '⚙️',
|
|
131
|
+
|
|
132
|
+
// Web files
|
|
133
|
+
'html': '🌐',
|
|
134
|
+
'htm': '🌐',
|
|
135
|
+
'css': '🎨',
|
|
136
|
+
'scss': '🎨',
|
|
137
|
+
'sass': '🎨',
|
|
138
|
+
'less': '🎨',
|
|
139
|
+
|
|
140
|
+
// Shell scripts
|
|
141
|
+
'sh': '🐚',
|
|
142
|
+
'bash': '🐚',
|
|
143
|
+
'zsh': '🐚',
|
|
144
|
+
'fish': '🐚',
|
|
145
|
+
|
|
146
|
+
// Lock files
|
|
147
|
+
'lock': '🔒',
|
|
148
|
+
'lockfile': '🔒',
|
|
149
|
+
|
|
150
|
+
// Environment files
|
|
151
|
+
'env': '🔑',
|
|
152
|
+
|
|
153
|
+
// Text and data
|
|
154
|
+
'txt': '📄',
|
|
155
|
+
'csv': '📄',
|
|
156
|
+
'tsv': '📄',
|
|
157
|
+
|
|
158
|
+
// Database files
|
|
159
|
+
'sql': '🗃️',
|
|
160
|
+
'db': '🗃️',
|
|
161
|
+
'sqlite': '🗃️',
|
|
162
|
+
'sqlite3': '🗃️',
|
|
163
|
+
|
|
164
|
+
// Audio files
|
|
165
|
+
'mp3': '🎵',
|
|
166
|
+
'wav': '🎵',
|
|
167
|
+
'flac': '🎵',
|
|
168
|
+
'ogg': '🎵',
|
|
169
|
+
'm4a': '🎵',
|
|
170
|
+
|
|
171
|
+
// Video files
|
|
172
|
+
'mp4': '🎬',
|
|
173
|
+
'mov': '🎬',
|
|
174
|
+
'avi': '🎬',
|
|
175
|
+
'mkv': '🎬',
|
|
176
|
+
'webm': '🎬',
|
|
177
|
+
|
|
178
|
+
// Image files
|
|
179
|
+
'png': '🖼️',
|
|
180
|
+
'jpg': '🖼️',
|
|
181
|
+
'jpeg': '🖼️',
|
|
182
|
+
'gif': '🖼️',
|
|
183
|
+
'svg': '🖼️',
|
|
184
|
+
'webp': '🖼️',
|
|
185
|
+
'ico': '🖼️',
|
|
186
|
+
|
|
187
|
+
// Documents
|
|
188
|
+
'pdf': '📕',
|
|
189
|
+
|
|
190
|
+
// Archives
|
|
191
|
+
'zip': '📦',
|
|
192
|
+
'tar': '📦',
|
|
193
|
+
'gz': '📦',
|
|
194
|
+
'bz2': '📦',
|
|
195
|
+
'xz': '📦',
|
|
196
|
+
'7z': '📦',
|
|
197
|
+
'rar': '📦',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Special filename to icon mapping
|
|
202
|
+
* Used for files without standard extensions (e.g., Dockerfile, .gitignore)
|
|
203
|
+
*/
|
|
204
|
+
const SPECIAL_FILE_ICONS = {
|
|
205
|
+
'Dockerfile': '🐳',
|
|
206
|
+
'dockerignore': '🐳',
|
|
207
|
+
'Makefile': '🛠️',
|
|
208
|
+
'docker-compose.yml': '🐳',
|
|
209
|
+
'docker-compose.yaml': '🐳',
|
|
210
|
+
'.gitignore': '📋',
|
|
211
|
+
'.gitattributes': '📋',
|
|
212
|
+
'.gitmodules': '📋',
|
|
213
|
+
'.env': '🔑',
|
|
214
|
+
'.env.example': '🔑',
|
|
215
|
+
'.env.local': '🔑',
|
|
216
|
+
'.env.production': '🔑',
|
|
217
|
+
'.env.development': '🔑',
|
|
218
|
+
'.nvmrc': '🔢',
|
|
219
|
+
'.npmrc': '📦',
|
|
220
|
+
'.editorconfig': '⚙️',
|
|
221
|
+
'.prettierrc': '✨',
|
|
222
|
+
'.prettierrc.json': '✨',
|
|
223
|
+
'.prettierrc.yml': '✨',
|
|
224
|
+
'.prettierrc.yaml': '✨',
|
|
225
|
+
'.eslintrc': '📏',
|
|
226
|
+
'.eslintrc.json': '📏',
|
|
227
|
+
'.eslintrc.yml': '📏',
|
|
228
|
+
'.eslintrc.yaml': '📏',
|
|
229
|
+
'.eslintrc.js': '📏',
|
|
230
|
+
'package-lock.json': '🔒',
|
|
231
|
+
'yarn.lock': '🔒',
|
|
232
|
+
'pnpm-lock.yaml': '🔒',
|
|
233
|
+
'package.json': '📦',
|
|
234
|
+
'tsconfig.json': '🔷',
|
|
235
|
+
'tslint.json': '📏',
|
|
236
|
+
'webpack.config.js': '📦',
|
|
237
|
+
'vite.config.js': '⚡',
|
|
238
|
+
'next.config.js': '▲',
|
|
239
|
+
'nuxt.config.js': '🟢',
|
|
240
|
+
'tailwind.config.js': '🎨',
|
|
241
|
+
'.babelrc': '📜',
|
|
242
|
+
'.babelrc.json': '📜',
|
|
243
|
+
'.browserslistrc': '🌐',
|
|
244
|
+
'LICENSE': '⚖️',
|
|
245
|
+
'LICENSE-MIT': '⚖️',
|
|
246
|
+
'LICENSE-APACHE': '⚖️',
|
|
247
|
+
'README': '📖',
|
|
248
|
+
'README.md': '📖',
|
|
249
|
+
'README.txt': '📖',
|
|
250
|
+
'CONTRIBUTING': '🤝',
|
|
251
|
+
'CHANGELOG': '📜',
|
|
252
|
+
'AUTHORS': '👥',
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Icon colors mapping
|
|
257
|
+
* Maps file types/extensions to their chalk color styles
|
|
258
|
+
*/
|
|
259
|
+
const ICON_COLORS = {
|
|
260
|
+
// JavaScript
|
|
261
|
+
'js': chalk.yellow,
|
|
262
|
+
'mjs': chalk.yellow,
|
|
263
|
+
'cjs': chalk.yellow,
|
|
264
|
+
|
|
265
|
+
// TypeScript
|
|
266
|
+
'ts': chalk.blue,
|
|
267
|
+
'tsx': chalk.blue,
|
|
268
|
+
|
|
269
|
+
// React
|
|
270
|
+
'jsx': chalk.cyan,
|
|
271
|
+
|
|
272
|
+
// Python
|
|
273
|
+
'py': chalk.green,
|
|
274
|
+
'pyi': chalk.green,
|
|
275
|
+
|
|
276
|
+
// Go
|
|
277
|
+
'go': chalk.cyan,
|
|
278
|
+
|
|
279
|
+
// Rust
|
|
280
|
+
'rs': chalk.red,
|
|
281
|
+
|
|
282
|
+
// Documentation
|
|
283
|
+
'md': chalk.white,
|
|
284
|
+
'markdown': chalk.white,
|
|
285
|
+
'rst': chalk.white,
|
|
286
|
+
|
|
287
|
+
// Configuration
|
|
288
|
+
'json': chalk.yellow.dim,
|
|
289
|
+
'yaml': chalk.magenta,
|
|
290
|
+
'yml': chalk.magenta,
|
|
291
|
+
'toml': chalk.magenta,
|
|
292
|
+
|
|
293
|
+
// Web
|
|
294
|
+
'html': chalk.hex('#e44d26'), // Orange
|
|
295
|
+
'css': chalk.magenta,
|
|
296
|
+
'scss': chalk.magenta,
|
|
297
|
+
'sass': chalk.magenta,
|
|
298
|
+
'less': chalk.magenta,
|
|
299
|
+
|
|
300
|
+
// Shell
|
|
301
|
+
'sh': chalk.green,
|
|
302
|
+
'bash': chalk.green,
|
|
303
|
+
'zsh': chalk.green,
|
|
304
|
+
|
|
305
|
+
// Docker
|
|
306
|
+
'Dockerfile': chalk.blue,
|
|
307
|
+
'dockerignore': chalk.blue,
|
|
308
|
+
|
|
309
|
+
// Lock files
|
|
310
|
+
'lock': chalk.dim.gray,
|
|
311
|
+
'lockfile': chalk.dim.gray,
|
|
312
|
+
'package-lock.json': chalk.dim.gray,
|
|
313
|
+
'yarn.lock': chalk.dim.gray,
|
|
314
|
+
'pnpm-lock.yaml': chalk.dim.gray,
|
|
315
|
+
|
|
316
|
+
// Environment
|
|
317
|
+
'env': chalk.yellow,
|
|
318
|
+
'.env': chalk.yellow,
|
|
319
|
+
|
|
320
|
+
// Git
|
|
321
|
+
'gitignore': chalk.dim,
|
|
322
|
+
'gitattributes': chalk.dim,
|
|
323
|
+
|
|
324
|
+
// Text/data
|
|
325
|
+
'txt': chalk.white,
|
|
326
|
+
'csv': chalk.white,
|
|
327
|
+
|
|
328
|
+
// Database
|
|
329
|
+
'sql': chalk.blue,
|
|
330
|
+
'db': chalk.blue,
|
|
331
|
+
'sqlite': chalk.blue,
|
|
332
|
+
|
|
333
|
+
// Audio
|
|
334
|
+
'mp3': chalk.magenta,
|
|
335
|
+
'wav': chalk.magenta,
|
|
336
|
+
'flac': chalk.magenta,
|
|
337
|
+
|
|
338
|
+
// Video
|
|
339
|
+
'mp4': chalk.red,
|
|
340
|
+
'mov': chalk.red,
|
|
341
|
+
'avi': chalk.red,
|
|
342
|
+
|
|
343
|
+
// Images
|
|
344
|
+
'png': chalk.green,
|
|
345
|
+
'jpg': chalk.green,
|
|
346
|
+
'jpeg': chalk.green,
|
|
347
|
+
'svg': chalk.green,
|
|
348
|
+
|
|
349
|
+
// Documents
|
|
350
|
+
'pdf': chalk.red,
|
|
351
|
+
|
|
352
|
+
// Archives
|
|
353
|
+
'zip': chalk.yellow,
|
|
354
|
+
'tar': chalk.yellow,
|
|
355
|
+
'gz': chalk.yellow,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the icon for a file or directory
|
|
360
|
+
*
|
|
361
|
+
* @param {string} name - The filename or directory name
|
|
362
|
+
* @param {boolean} isDir - Whether this is a directory
|
|
363
|
+
* @param {boolean} expanded - Whether the directory is expanded (only applies when isDir=true)
|
|
364
|
+
* @param {boolean} isSymlink - Whether this is a symbolic link
|
|
365
|
+
* @param {boolean} hasError - Whether this node has an error
|
|
366
|
+
* @param {string|null} fileType - Special file type: 'socket', 'fifo', 'blockDevice', 'charDevice'
|
|
367
|
+
* @param {Object} options - Options object
|
|
368
|
+
* @param {boolean} options.noIcons - If true, use ASCII fallback icons
|
|
369
|
+
* @returns {string} The icon character (emoji or ASCII)
|
|
370
|
+
*/
|
|
371
|
+
export function getIcon(name, isDir = false, expanded = false, isSymlink = false, hasError = false, fileType = null, options = {}) {
|
|
372
|
+
const { noIcons = false, iconSet } = options;
|
|
373
|
+
const resolvedSet = (iconSet || '').toLowerCase() || (noIcons ? 'ascii' : 'emoji');
|
|
374
|
+
|
|
375
|
+
// Handle ASCII mode
|
|
376
|
+
if (resolvedSet === 'ascii') {
|
|
377
|
+
if (hasError) {
|
|
378
|
+
return ASCII_ICONS.ERROR;
|
|
379
|
+
}
|
|
380
|
+
if (isSymlink) {
|
|
381
|
+
return ASCII_ICONS.SYMLINK;
|
|
382
|
+
}
|
|
383
|
+
if (fileType === 'socket') {
|
|
384
|
+
return ASCII_ICONS.SOCKET;
|
|
385
|
+
}
|
|
386
|
+
if (fileType === 'fifo') {
|
|
387
|
+
return ASCII_ICONS.FIFO;
|
|
388
|
+
}
|
|
389
|
+
if (fileType === 'blockDevice') {
|
|
390
|
+
return ASCII_ICONS.BLOCK_DEVICE;
|
|
391
|
+
}
|
|
392
|
+
if (isDir) {
|
|
393
|
+
return expanded ? ASCII_ICONS.EXPANDED_DIR : ASCII_ICONS.COLLAPSED_DIR;
|
|
394
|
+
}
|
|
395
|
+
return ASCII_ICONS.FILE;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Handle Nerd Font mode
|
|
399
|
+
if (resolvedSet === 'nerd') {
|
|
400
|
+
if (hasError) {
|
|
401
|
+
return NERD_ICONS.ERROR;
|
|
402
|
+
}
|
|
403
|
+
if (isSymlink) {
|
|
404
|
+
return NERD_ICONS.SYMLINK;
|
|
405
|
+
}
|
|
406
|
+
if (fileType === 'socket') {
|
|
407
|
+
return NERD_ICONS.SOCKET;
|
|
408
|
+
}
|
|
409
|
+
if (fileType === 'fifo') {
|
|
410
|
+
return NERD_ICONS.FIFO;
|
|
411
|
+
}
|
|
412
|
+
if (fileType === 'blockDevice') {
|
|
413
|
+
return NERD_ICONS.BLOCK_DEVICE;
|
|
414
|
+
}
|
|
415
|
+
if (isDir) {
|
|
416
|
+
return expanded ? NERD_ICONS.EXPANDED_DIR : NERD_ICONS.COLLAPSED_DIR;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (NERD_SPECIAL_FILE_ICONS.hasOwnProperty(name)) {
|
|
420
|
+
return NERD_SPECIAL_FILE_ICONS[name];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const ext = getExtension(name);
|
|
424
|
+
if (ext && NERD_EXTENSION_ICONS.hasOwnProperty(ext)) {
|
|
425
|
+
return NERD_EXTENSION_ICONS[ext];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return NERD_ICONS.FILE;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Handle errors first (highest priority)
|
|
432
|
+
if (hasError) {
|
|
433
|
+
return '⛔';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Handle special file types
|
|
437
|
+
if (fileType === 'socket') {
|
|
438
|
+
return '🔌';
|
|
439
|
+
}
|
|
440
|
+
if (fileType === 'fifo') {
|
|
441
|
+
return '🔀';
|
|
442
|
+
}
|
|
443
|
+
if (fileType === 'blockDevice') {
|
|
444
|
+
return '💾';
|
|
445
|
+
}
|
|
446
|
+
if (fileType === 'charDevice') {
|
|
447
|
+
return '📟';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Handle symlinks (they take precedence over regular files/dirs)
|
|
451
|
+
if (isSymlink) {
|
|
452
|
+
return '🔗';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Handle directories
|
|
456
|
+
if (isDir) {
|
|
457
|
+
return expanded ? '📂' : '📁';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check special files first (full filename match)
|
|
461
|
+
if (SPECIAL_FILE_ICONS.hasOwnProperty(name)) {
|
|
462
|
+
return SPECIAL_FILE_ICONS[name];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Extract extension
|
|
466
|
+
const ext = getExtension(name);
|
|
467
|
+
|
|
468
|
+
// Check extension mapping
|
|
469
|
+
if (ext && EXTENSION_ICONS.hasOwnProperty(ext)) {
|
|
470
|
+
return EXTENSION_ICONS[ext];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Check for special files with extension (e.g., .gitignore, Dockerfile)
|
|
474
|
+
const baseName = name.toLowerCase();
|
|
475
|
+
for (const [specialFile, icon] of Object.entries(SPECIAL_FILE_ICONS)) {
|
|
476
|
+
if (baseName === specialFile.toLowerCase() || baseName.endsWith(`/${specialFile.toLowerCase()}`)) {
|
|
477
|
+
return icon;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Default file icon
|
|
482
|
+
return '📄';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get the chalk style for a file or directory
|
|
487
|
+
*
|
|
488
|
+
* @param {string} name - The filename or directory name
|
|
489
|
+
* @param {number|null} mode - The file mode from fs.stats (for detecting executables)
|
|
490
|
+
* @param {boolean} isDir - Whether this is a directory
|
|
491
|
+
* @param {boolean} isSymlink - Whether this is a symbolic link
|
|
492
|
+
* @param {boolean} hasError - Whether this node has an error
|
|
493
|
+
* @param {string|null} fileType - Special file type: 'socket', 'fifo', 'blockDevice', 'charDevice'
|
|
494
|
+
* @returns {Function} A chalk style function
|
|
495
|
+
*/
|
|
496
|
+
export function getStyle(name, mode = null, isDir = false, isSymlink = false, hasError = false, fileType = null) {
|
|
497
|
+
// Errors get red styling
|
|
498
|
+
if (hasError) {
|
|
499
|
+
return chalk.red;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Symlinks get special styling
|
|
503
|
+
if (isSymlink) {
|
|
504
|
+
return chalk.magenta.italic;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Special file types
|
|
508
|
+
if (fileType === 'socket') {
|
|
509
|
+
return chalk.cyan.dim;
|
|
510
|
+
}
|
|
511
|
+
if (fileType === 'fifo') {
|
|
512
|
+
return chalk.yellow.dim;
|
|
513
|
+
}
|
|
514
|
+
if (fileType === 'blockDevice' || fileType === 'charDevice') {
|
|
515
|
+
return chalk.gray.dim;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Directories get blue bold
|
|
519
|
+
if (isDir) {
|
|
520
|
+
return chalk.blue.bold;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Executable files get green
|
|
524
|
+
if (mode !== null && (mode & 0o111) !== 0) {
|
|
525
|
+
return chalk.green;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Hidden files get gray dim
|
|
529
|
+
if (name.startsWith('.')) {
|
|
530
|
+
return chalk.gray.dim;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Check special files first
|
|
534
|
+
if (SPECIAL_FILE_ICONS.hasOwnProperty(name)) {
|
|
535
|
+
const baseName = name;
|
|
536
|
+
if (ICON_COLORS.hasOwnProperty(baseName)) {
|
|
537
|
+
return ICON_COLORS[baseName];
|
|
538
|
+
}
|
|
539
|
+
// Default for known special files without explicit color
|
|
540
|
+
for (const key of Object.keys(SPECIAL_FILE_ICONS)) {
|
|
541
|
+
if (name.toLowerCase() === key.toLowerCase()) {
|
|
542
|
+
if (ICON_COLORS.hasOwnProperty(key)) {
|
|
543
|
+
return ICON_COLORS[key];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Check extension-based colors
|
|
550
|
+
const ext = getExtension(name);
|
|
551
|
+
if (ext && ICON_COLORS.hasOwnProperty(ext)) {
|
|
552
|
+
return ICON_COLORS[ext];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Default style (white)
|
|
556
|
+
return chalk.white;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Extract the file extension from a filename
|
|
561
|
+
*
|
|
562
|
+
* @param {string} name - The filename
|
|
563
|
+
* @returns {string|null} The extension without the dot, or null if no extension
|
|
564
|
+
*/
|
|
565
|
+
function getExtension(name) {
|
|
566
|
+
const lastDotIndex = name.lastIndexOf('.');
|
|
567
|
+
|
|
568
|
+
// No dot found, or dot is at the start (hidden file) or end (no extension)
|
|
569
|
+
if (lastDotIndex <= 0 || lastDotIndex === name.length - 1) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return name.slice(lastDotIndex + 1).toLowerCase();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Detect special file types from file mode
|
|
578
|
+
*
|
|
579
|
+
* @param {number|null} mode - The file mode from fs.stats
|
|
580
|
+
* @returns {string|null} The file type: 'socket', 'fifo', 'blockDevice', 'charDevice', or null
|
|
581
|
+
*/
|
|
582
|
+
export function detectFileType(mode = null) {
|
|
583
|
+
if (mode === null) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// File type is stored in the upper 4 bits of the mode (0o170000 mask)
|
|
588
|
+
const fileType = mode & 0o170000;
|
|
589
|
+
|
|
590
|
+
switch (fileType) {
|
|
591
|
+
case 0o140000: // Socket
|
|
592
|
+
return 'socket';
|
|
593
|
+
case 0o010000: // FIFO (named pipe)
|
|
594
|
+
return 'fifo';
|
|
595
|
+
case 0o060000: // Block device
|
|
596
|
+
return 'blockDevice';
|
|
597
|
+
case 0o020000: // Character device
|
|
598
|
+
return 'charDevice';
|
|
599
|
+
case 0o120000: // Symlink
|
|
600
|
+
return 'symlink';
|
|
601
|
+
default:
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Apply color and icon to a filename for display
|
|
608
|
+
*
|
|
609
|
+
* @param {string} name - The filename or directory name
|
|
610
|
+
* @param {Object} options - Display options
|
|
611
|
+
* @param {boolean} options.isDir - Whether this is a directory
|
|
612
|
+
* @param {boolean} options.expanded - Whether the directory is expanded
|
|
613
|
+
* @param {boolean} options.isSymlink - Whether this is a symbolic link
|
|
614
|
+
* @param {boolean} options.hasError - Whether this node has an error
|
|
615
|
+
* @param {number|null} options.mode - The file mode (for executables and special file types)
|
|
616
|
+
* @param {boolean} options.noIcons - If true, don't show emoji icons
|
|
617
|
+
* @returns {string} The formatted name with icon and color
|
|
618
|
+
*/
|
|
619
|
+
export function formatName(name, options = {}) {
|
|
620
|
+
const {
|
|
621
|
+
isDir = false,
|
|
622
|
+
expanded = false,
|
|
623
|
+
isSymlink = false,
|
|
624
|
+
hasError = false,
|
|
625
|
+
mode = null,
|
|
626
|
+
noIcons = false,
|
|
627
|
+
} = options;
|
|
628
|
+
|
|
629
|
+
// Detect special file types from mode
|
|
630
|
+
const fileType = detectFileType(mode);
|
|
631
|
+
|
|
632
|
+
// Don't double-detect symlinks (they may already be detected)
|
|
633
|
+
const finalFileType = fileType === 'symlink' ? null : fileType;
|
|
634
|
+
|
|
635
|
+
const icon = getIcon(name, isDir, expanded, isSymlink, hasError, finalFileType, { noIcons });
|
|
636
|
+
const style = getStyle(name, mode, isDir, isSymlink, hasError, finalFileType);
|
|
637
|
+
const styledName = style(name);
|
|
638
|
+
|
|
639
|
+
return `${icon} ${styledName}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get all supported file extensions
|
|
644
|
+
*
|
|
645
|
+
* @returns {string[]} Array of supported extensions
|
|
646
|
+
*/
|
|
647
|
+
export function getSupportedExtensions() {
|
|
648
|
+
return Object.keys(EXTENSION_ICONS);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Get all special filenames
|
|
653
|
+
*
|
|
654
|
+
* @returns {string[]} Array of special filenames with custom icons
|
|
655
|
+
*/
|
|
656
|
+
export function getSpecialFiles() {
|
|
657
|
+
return Object.keys(SPECIAL_FILE_ICONS);
|
|
658
|
+
}
|