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.
@@ -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
+ }