gh-here 1.0.1 → 1.0.4

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/bin/gh-here.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const express = require('express');
4
- const path = require('path');
5
- const fs = require('fs');
6
- const hljs = require('highlight.js');
7
- const marked = require('marked');
8
- const octicons = require('@primer/octicons');
9
4
  const { exec } = require('child_process');
10
5
 
6
+ // Import our modularized components
7
+ const { findGitRepo } = require('../lib/git');
8
+ const { setupRoutes } = require('../lib/server');
9
+
11
10
  // Parse command line arguments
12
11
  const args = process.argv.slice(2);
13
12
  const openBrowser = args.includes('--open') || args.includes('-o');
@@ -44,184 +43,11 @@ const app = express();
44
43
  const workingDir = process.cwd();
45
44
 
46
45
  // Git repository detection
47
- let isGitRepo = false;
48
- let gitRepoRoot = '';
49
-
50
- // Check if current directory or any parent is a git repository
51
- function findGitRepo(dir) {
52
- if (fs.existsSync(path.join(dir, '.git'))) {
53
- return dir;
54
- }
55
- const parentDir = path.dirname(dir);
56
- if (parentDir === dir) {
57
- return null; // Reached root directory
58
- }
59
- return findGitRepo(parentDir);
60
- }
61
-
62
- // Initialize git detection
63
- gitRepoRoot = findGitRepo(workingDir);
64
- isGitRepo = !!gitRepoRoot;
65
-
66
- // Git status icon and description helpers
67
- function getGitStatusIcon(status) {
68
- switch (status.trim()) {
69
- case 'M': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
70
- case 'A': return octicons['plus'].toSVG({ class: 'git-status-icon' });
71
- case 'D': return octicons['dash'].toSVG({ class: 'git-status-icon' });
72
- case 'R': return octicons['arrow-right'].toSVG({ class: 'git-status-icon' });
73
- case '??': return octicons['question'].toSVG({ class: 'git-status-icon' });
74
- case 'MM':
75
- case 'AM':
76
- case 'AD': return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
77
- default: return octicons['dot-fill'].toSVG({ class: 'git-status-icon' });
78
- }
79
- }
80
-
81
- function getGitStatusDescription(status) {
82
- switch (status.trim()) {
83
- case 'M': return 'Modified';
84
- case 'A': return 'Added';
85
- case 'D': return 'Deleted';
86
- case 'R': return 'Renamed';
87
- case '??': return 'Untracked';
88
- case 'MM': return 'Modified (staged and unstaged)';
89
- case 'AM': return 'Added (modified)';
90
- case 'AD': return 'Added (deleted)';
91
- default: return `Git status: ${status}`;
92
- }
93
- }
94
-
95
- // Get git status for files
96
- function getGitStatus() {
97
- return new Promise((resolve) => {
98
- if (!isGitRepo) {
99
- resolve({});
100
- return;
101
- }
102
-
103
- exec('git status --porcelain', { cwd: gitRepoRoot }, (error, stdout) => {
104
- if (error) {
105
- resolve({});
106
- return;
107
- }
108
-
109
- const statusMap = {};
110
- const lines = stdout.trim().split('\n').filter(line => line);
111
-
112
- for (const line of lines) {
113
- const status = line.substring(0, 2);
114
- const filePath = line.substring(3);
115
- const absolutePath = path.resolve(gitRepoRoot, filePath);
116
- statusMap[absolutePath] = {
117
- status: status.trim(),
118
- staged: status[0] !== ' ' && status[0] !== '?',
119
- modified: status[1] !== ' ',
120
- untracked: status === '??'
121
- };
122
- }
123
-
124
- resolve(statusMap);
125
- });
126
- });
127
- }
46
+ const gitRepoRoot = findGitRepo(workingDir);
47
+ const isGitRepo = !!gitRepoRoot;
128
48
 
129
- // Get git branch info
130
- function getGitBranch() {
131
- return new Promise((resolve) => {
132
- if (!isGitRepo) {
133
- resolve(null);
134
- return;
135
- }
136
-
137
- exec('git branch --show-current', { cwd: gitRepoRoot }, (error, stdout) => {
138
- if (error) {
139
- resolve('main');
140
- return;
141
- }
142
- resolve(stdout.trim() || 'main');
143
- });
144
- });
145
- }
146
-
147
- // .gitignore parsing functionality
148
- function parseGitignore(gitignorePath) {
149
- try {
150
- if (!fs.existsSync(gitignorePath)) {
151
- return [];
152
- }
153
-
154
- const content = fs.readFileSync(gitignorePath, 'utf8');
155
- return content
156
- .split('\n')
157
- .map(line => line.trim())
158
- .filter(line => line && !line.startsWith('#'))
159
- .map(pattern => {
160
- // Convert gitignore patterns to regex-like matching
161
- if (pattern.endsWith('/')) {
162
- // Directory pattern
163
- return { pattern: pattern.slice(0, -1), isDirectory: true };
164
- }
165
- return { pattern, isDirectory: false };
166
- });
167
- } catch (error) {
168
- return [];
169
- }
170
- }
171
-
172
- function isIgnoredByGitignore(filePath, gitignoreRules, isDirectory = false) {
173
- if (!gitignoreRules || gitignoreRules.length === 0) {
174
- return false;
175
- }
176
-
177
- const relativePath = path.relative(workingDir, filePath).replace(/\\/g, '/');
178
- const pathParts = relativePath.split('/');
179
-
180
- for (const rule of gitignoreRules) {
181
- const { pattern, isDirectory: ruleIsDirectory } = rule;
182
-
183
- // Skip directory rules for files and vice versa (unless rule applies to both)
184
- if (ruleIsDirectory && !isDirectory) {
185
- continue;
186
- }
187
-
188
- // Simple pattern matching (this is a basic implementation)
189
- if (pattern.includes('*')) {
190
- // Wildcard matching
191
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
192
- if (regex.test(relativePath) || pathParts.some(part => regex.test(part))) {
193
- return true;
194
- }
195
- } else {
196
- // Exact matching
197
- if (relativePath === pattern ||
198
- relativePath.startsWith(pattern + '/') ||
199
- pathParts.includes(pattern)) {
200
- return true;
201
- }
202
- }
203
- }
204
-
205
- return false;
206
- }
207
-
208
- // Cache for gitignore rules
209
- let gitignoreCache = null;
210
- let gitignoreCacheTime = 0;
211
-
212
- function getGitignoreRules() {
213
- const gitignorePath = path.join(workingDir, '.gitignore');
214
- const now = Date.now();
215
-
216
- // Cache for 5 seconds to avoid excessive file reads
217
- if (gitignoreCache && (now - gitignoreCacheTime) < 5000) {
218
- return gitignoreCache;
219
- }
220
-
221
- gitignoreCache = parseGitignore(gitignorePath);
222
- gitignoreCacheTime = now;
223
- return gitignoreCache;
224
- }
49
+ // Setup all routes
50
+ setupRoutes(app, workingDir, isGitRepo, gitRepoRoot);
225
51
 
226
52
  // Function to find an available port
227
53
  async function findAvailablePort(startPort = 3000) {
@@ -246,1079 +72,6 @@ async function findAvailablePort(startPort = 3000) {
246
72
  });
247
73
  }
248
74
 
249
- app.use('/static', express.static(path.join(__dirname, '..', 'public')));
250
- app.use('/octicons', express.static(path.join(__dirname, '..', 'node_modules', '@primer', 'octicons', 'build')));
251
-
252
- // Download route
253
- app.get('/download', (req, res) => {
254
- const filePath = req.query.path || '';
255
- const fullPath = path.join(workingDir, filePath);
256
-
257
- try {
258
- const stats = fs.statSync(fullPath);
259
- if (stats.isFile()) {
260
- const fileName = path.basename(fullPath);
261
- res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
262
- res.sendFile(fullPath);
263
- } else {
264
- res.status(400).send('Cannot download directories');
265
- }
266
- } catch (error) {
267
- res.status(404).send('File not found');
268
- }
269
- });
270
-
271
- app.get('/', async (req, res) => {
272
- const currentPath = req.query.path || '';
273
- const showGitignored = req.query.gitignore === 'false'; // Default to hiding gitignored files
274
- const fullPath = path.join(workingDir, currentPath);
275
-
276
- // Get git status and branch info
277
- const gitStatus = await getGitStatus();
278
- const gitBranch = await getGitBranch();
279
-
280
- try {
281
- const stats = fs.statSync(fullPath);
282
-
283
- if (stats.isDirectory()) {
284
- const gitignoreRules = getGitignoreRules();
285
-
286
- let items = fs.readdirSync(fullPath).map(item => {
287
- const itemPath = path.join(fullPath, item);
288
- const itemStats = fs.statSync(itemPath);
289
- const absoluteItemPath = path.resolve(itemPath);
290
- const gitInfo = gitStatus[absoluteItemPath] || null;
291
-
292
- return {
293
- name: item,
294
- path: path.join(currentPath, item).replace(/\\/g, '/'),
295
- isDirectory: itemStats.isDirectory(),
296
- size: itemStats.size,
297
- modified: itemStats.mtime,
298
- gitStatus: gitInfo
299
- };
300
- });
301
-
302
- // Filter out gitignored files unless explicitly requested to show them
303
- if (!showGitignored) {
304
- items = items.filter(item => {
305
- const itemFullPath = path.join(fullPath, item.name);
306
- return !isIgnoredByGitignore(itemFullPath, gitignoreRules, item.isDirectory);
307
- });
308
- }
309
-
310
- // Sort items (directories first, then alphabetically)
311
- items.sort((a, b) => {
312
- if (a.isDirectory && !b.isDirectory) return -1;
313
- if (!a.isDirectory && b.isDirectory) return 1;
314
- return a.name.localeCompare(b.name);
315
- });
316
-
317
- res.send(renderDirectory(currentPath, items, showGitignored, gitBranch));
318
- } else {
319
- const content = fs.readFileSync(fullPath, 'utf8');
320
- const ext = path.extname(fullPath).slice(1);
321
- const viewMode = req.query.view || 'rendered';
322
-
323
- if (viewMode === 'diff' && isGitRepo) {
324
- // Check if file has git status
325
- const absolutePath = path.resolve(fullPath);
326
- const gitInfo = gitStatus[absolutePath];
327
- if (gitInfo) {
328
- const diffHtml = await renderFileDiff(currentPath, ext, gitInfo);
329
- return res.send(diffHtml);
330
- }
331
- }
332
-
333
- res.send(await renderFile(currentPath, content, ext, viewMode, gitStatus));
334
- }
335
- } catch (error) {
336
- res.status(404).send(`<h1>File not found</h1><p>${error.message}</p>`);
337
- }
338
- });
339
-
340
- // Route for creating new files
341
- app.get('/new', (req, res) => {
342
- const currentPath = req.query.path || '';
343
- res.send(renderNewFile(currentPath));
344
- });
345
-
346
- // API endpoint to get file content for editing
347
- app.get('/api/file-content', (req, res) => {
348
- try {
349
- const currentPath = req.query.path || '';
350
- const fullPath = path.join(process.cwd(), currentPath);
351
-
352
- // Security check - ensure we're not accessing files outside the current directory
353
- if (!fullPath.startsWith(process.cwd())) {
354
- return res.status(403).send('Access denied');
355
- }
356
-
357
- const content = fs.readFileSync(fullPath, 'utf-8');
358
- res.send(content);
359
- } catch (error) {
360
- res.status(404).send(`File not found: ${error.message}`);
361
- }
362
- });
363
-
364
- // API endpoint to save file changes
365
- app.post('/api/save-file', express.json(), (req, res) => {
366
- try {
367
- const { path: filePath, content } = req.body;
368
- const fullPath = path.join(process.cwd(), filePath || '');
369
-
370
- // Security check - ensure we're not accessing files outside the current directory
371
- if (!fullPath.startsWith(process.cwd())) {
372
- return res.status(403).json({ success: false, error: 'Access denied' });
373
- }
374
-
375
- fs.writeFileSync(fullPath, content, 'utf-8');
376
- res.json({ success: true });
377
- } catch (error) {
378
- res.status(500).json({ success: false, error: error.message });
379
- }
380
- });
381
-
382
- // API endpoint to create new file
383
- app.post('/api/create-file', express.json(), (req, res) => {
384
- try {
385
- const { path: dirPath, filename } = req.body;
386
- const fullDirPath = path.join(process.cwd(), dirPath || '');
387
- const fullFilePath = path.join(fullDirPath, filename);
388
-
389
- // Security checks
390
- if (!fullDirPath.startsWith(process.cwd()) || !fullFilePath.startsWith(process.cwd())) {
391
- return res.status(403).json({ success: false, error: 'Access denied' });
392
- }
393
-
394
- // Check if file already exists
395
- if (fs.existsSync(fullFilePath)) {
396
- return res.status(400).json({ success: false, error: 'File already exists' });
397
- }
398
-
399
- // Create the file with empty content
400
- fs.writeFileSync(fullFilePath, '', 'utf-8');
401
- res.json({ success: true });
402
- } catch (error) {
403
- res.status(500).json({ success: false, error: error.message });
404
- }
405
- });
406
-
407
- // API endpoint to create new folder
408
- app.post('/api/create-folder', express.json(), (req, res) => {
409
- try {
410
- const { path: dirPath, foldername } = req.body;
411
- const fullDirPath = path.join(process.cwd(), dirPath || '');
412
- const fullFolderPath = path.join(fullDirPath, foldername);
413
-
414
- // Security checks
415
- if (!fullDirPath.startsWith(process.cwd()) || !fullFolderPath.startsWith(process.cwd())) {
416
- return res.status(403).json({ success: false, error: 'Access denied' });
417
- }
418
-
419
- // Check if folder already exists
420
- if (fs.existsSync(fullFolderPath)) {
421
- return res.status(400).json({ success: false, error: 'Folder already exists' });
422
- }
423
-
424
- // Create the folder
425
- fs.mkdirSync(fullFolderPath);
426
- res.json({ success: true });
427
- } catch (error) {
428
- res.status(500).json({ success: false, error: error.message });
429
- }
430
- });
431
-
432
- // API endpoint to delete file or folder
433
- app.post('/api/delete', express.json(), (req, res) => {
434
- try {
435
- const { path: itemPath } = req.body;
436
- const fullPath = path.join(process.cwd(), itemPath);
437
-
438
- // Security check
439
- if (!fullPath.startsWith(process.cwd())) {
440
- return res.status(403).json({ success: false, error: 'Access denied' });
441
- }
442
-
443
- // Check if item exists
444
- if (!fs.existsSync(fullPath)) {
445
- return res.status(404).json({ success: false, error: 'Item not found' });
446
- }
447
-
448
- // Delete the item
449
- const stats = fs.statSync(fullPath);
450
- if (stats.isDirectory()) {
451
- fs.rmSync(fullPath, { recursive: true, force: true });
452
- } else {
453
- fs.unlinkSync(fullPath);
454
- }
455
-
456
- res.json({ success: true });
457
- } catch (error) {
458
- res.status(500).json({ success: false, error: error.message });
459
- }
460
- });
461
-
462
- // API endpoint to rename file or folder
463
- app.post('/api/rename', express.json(), (req, res) => {
464
- try {
465
- const { path: oldPath, newName } = req.body;
466
- const fullOldPath = path.join(process.cwd(), oldPath);
467
- const dirPath = path.dirname(fullOldPath);
468
- const fullNewPath = path.join(dirPath, newName);
469
-
470
- // Security checks
471
- if (!fullOldPath.startsWith(process.cwd()) || !fullNewPath.startsWith(process.cwd())) {
472
- return res.status(403).json({ success: false, error: 'Access denied' });
473
- }
474
-
475
- // Check if old item exists
476
- if (!fs.existsSync(fullOldPath)) {
477
- return res.status(404).json({ success: false, error: 'Item not found' });
478
- }
479
-
480
- // Check if new name already exists
481
- if (fs.existsSync(fullNewPath)) {
482
- return res.status(400).json({ success: false, error: 'Name already exists' });
483
- }
484
-
485
- // Rename the item
486
- fs.renameSync(fullOldPath, fullNewPath);
487
- res.json({ success: true });
488
- } catch (error) {
489
- res.status(500).json({ success: false, error: error.message });
490
- }
491
- });
492
-
493
- // Git diff endpoint
494
- app.get('/api/git-diff', async (req, res) => {
495
- try {
496
- if (!isGitRepo) {
497
- return res.status(404).json({ success: false, error: 'Not a git repository' });
498
- }
499
-
500
- const filePath = req.query.path;
501
- const staged = req.query.staged === 'true';
502
-
503
- if (!filePath) {
504
- return res.status(400).json({ success: false, error: 'File path is required' });
505
- }
506
-
507
- // Get the diff for the specific file
508
- const diffCommand = staged ?
509
- `git diff --cached "${filePath}"` :
510
- `git diff "${filePath}"`;
511
-
512
- exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout, stderr) => {
513
- if (error) {
514
- return res.status(500).json({ success: false, error: error.message });
515
- }
516
-
517
- res.json({
518
- success: true,
519
- diff: stdout,
520
- staged: staged,
521
- filePath: filePath
522
- });
523
- });
524
- } catch (error) {
525
- res.status(500).json({ success: false, error: error.message });
526
- }
527
- });
528
-
529
-
530
- function renderDirectory(currentPath, items, showGitignored = false, gitBranch = null) {
531
- const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch);
532
- const readmeFile = findReadmeFile(items);
533
- const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
534
- const languageStats = generateLanguageStats(items);
535
-
536
- const itemsHtml = items.map(item => `
537
- <tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
538
- <td class="icon">
539
- ${item.isDirectory ? octicons['file-directory'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
540
- </td>
541
- <td class="name">
542
- <a href="/?path=${encodeURIComponent(item.path)}">${item.name}</a>
543
- ${item.gitStatus ? `<span class="git-status git-status-${item.gitStatus.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(item.gitStatus.status)}">${getGitStatusIcon(item.gitStatus.status)}</span>` : ''}
544
- <div class="quick-actions">
545
- <button class="quick-btn copy-path-btn" title="Copy path" data-path="${item.path}">
546
- ${octicons.copy.toSVG({ class: 'quick-icon' })}
547
- </button>
548
- ${!item.isDirectory && item.gitStatus ? `
549
- <button class="quick-btn diff-btn" title="Show diff" data-path="${item.path}">
550
- ${octicons.diff.toSVG({ class: 'quick-icon' })}
551
- </button>
552
- ` : ''}
553
- ${!item.isDirectory ? `
554
- <a class="quick-btn download-btn" href="/download?path=${encodeURIComponent(item.path)}" title="Download" download="${item.name}">
555
- ${octicons.download.toSVG({ class: 'quick-icon' })}
556
- </a>
557
- ` : ''}
558
- ${!item.isDirectory ? `
559
- <button class="quick-btn edit-file-btn" title="Edit file" data-path="${item.path}">
560
- ${octicons.pencil.toSVG({ class: 'quick-icon' })}
561
- </button>
562
- ` : `
563
- <button class="quick-btn rename-btn" title="Rename" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
564
- ${octicons.pencil.toSVG({ class: 'quick-icon' })}
565
- </button>
566
- `}
567
- <button class="quick-btn delete-btn" title="Delete" data-path="${item.path}" data-name="${item.name}" data-is-directory="${item.isDirectory}">
568
- ${octicons.trash.toSVG({ class: 'quick-icon' })}
569
- </button>
570
- </div>
571
- </td>
572
- <td class="size">
573
- ${item.isDirectory ? '-' : formatBytes(item.size)}
574
- </td>
575
- <td class="modified">
576
- ${item.modified.toLocaleDateString()}
577
- </td>
578
- </tr>
579
- `).join('');
580
-
581
- return `
582
- <!DOCTYPE html>
583
- <html data-theme="dark">
584
- <head>
585
- <title>gh-here: ${currentPath || 'Root'}</title>
586
- <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
587
- <script src="/static/app.js"></script>
588
- </head>
589
- <body>
590
- <header>
591
- <div class="header-content">
592
- <div class="header-left">
593
- <h1 class="header-path">${breadcrumbs}</h1>
594
- </div>
595
- <div class="header-right">
596
- <div class="search-container">
597
- ${octicons.search.toSVG({ class: 'search-icon' })}
598
- <input type="text" id="file-search" placeholder="Find files..." class="search-input">
599
- </div>
600
- <button id="gitignore-toggle" class="gitignore-toggle ${showGitignored ? 'showing-ignored' : ''}" aria-label="Toggle .gitignore filtering" title="${showGitignored ? 'Hide' : 'Show'} gitignored files">
601
- ${octicons.eye.toSVG({ class: 'gitignore-icon' })}
602
- </button>
603
- <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
604
- ${octicons.moon.toSVG({ class: 'theme-icon' })}
605
- </button>
606
- </div>
607
- </div>
608
- </header>
609
- <main>
610
- ${languageStats}
611
- <div class="directory-actions">
612
- <button id="new-file-btn" class="btn btn-secondary">
613
- ${octicons['file-added'].toSVG({ class: 'btn-icon' })} New file
614
- </button>
615
- </div>
616
- <div class="file-table-container">
617
- <table class="file-table" id="file-table">
618
- <thead>
619
- <tr>
620
- <th></th>
621
- <th>Name</th>
622
- <th>Size</th>
623
- <th>Modified</th>
624
- </tr>
625
- </thead>
626
- <tbody>
627
- ${currentPath && currentPath !== '.' ? `
628
- <tr class="file-row" data-name=".." data-type="dir">
629
- <td class="icon">${octicons['arrow-up'].toSVG({ class: 'octicon-directory' })}</td>
630
- <td class="name">
631
- <a href="/?path=${encodeURIComponent(path.dirname(currentPath))}">..</a>
632
- </td>
633
- <td class="size">-</td>
634
- <td class="modified">-</td>
635
- </tr>
636
- ` : ''}
637
- ${itemsHtml}
638
- </tbody>
639
- </table>
640
- </div>
641
- ${readmePreview}
642
- </main>
643
- </body>
644
- </html>
645
- `;
646
- }
647
-
648
- function findReadmeFile(items) {
649
- const readmeNames = ['README.md', 'readme.md', 'README.rst', 'readme.rst', 'README.txt', 'readme.txt', 'README'];
650
- return items.find(item => !item.isDirectory && readmeNames.includes(item.name));
651
- }
652
-
653
- function generateReadmePreview(currentPath, readmeFile) {
654
- try {
655
- const readmePath = path.join(workingDir, currentPath, readmeFile.name);
656
- const content = fs.readFileSync(readmePath, 'utf8');
657
- const ext = path.extname(readmeFile.name).slice(1).toLowerCase();
658
-
659
- let renderedContent;
660
- if (ext === 'md' || ext === '') {
661
- renderedContent = `<div class="markdown">${marked.parse(content)}</div>`;
662
- } else {
663
- const highlighted = hljs.highlightAuto(content).value;
664
- renderedContent = `<pre><code class="hljs">${highlighted}</code></pre>`;
665
- }
666
-
667
- return `
668
- <div class="readme-section">
669
- <div class="readme-header">
670
- <h2>
671
- ${octicons.book.toSVG({ class: 'readme-icon' })}
672
- ${readmeFile.name}
673
- </h2>
674
- </div>
675
- <div class="readme-content">
676
- ${renderedContent}
677
- </div>
678
- </div>
679
- `;
680
- } catch (error) {
681
- return '';
682
- }
683
- }
684
-
685
- function generateLanguageStats(items) {
686
- const languages = {};
687
- let totalFiles = 0;
688
-
689
- items.forEach(item => {
690
- if (!item.isDirectory) {
691
- const ext = path.extname(item.name).slice(1).toLowerCase();
692
- const lang = getLanguageFromExtension(ext) || 'other';
693
- languages[lang] = (languages[lang] || 0) + 1;
694
- totalFiles++;
695
- }
696
- });
697
-
698
- if (totalFiles === 0) return '';
699
-
700
- const sortedLangs = Object.entries(languages)
701
- .sort(([,a], [,b]) => b - a)
702
- .slice(0, 5);
703
-
704
- const statsHtml = sortedLangs.map(([lang, count]) => {
705
- const percentage = ((count / totalFiles) * 100).toFixed(1);
706
- const color = getLanguageColor(lang);
707
- return `
708
- <div class="lang-stat">
709
- <span class="lang-dot" style="background-color: ${color}"></span>
710
- <span class="lang-name">${lang}</span>
711
- <span class="lang-percent">${percentage}%</span>
712
- </div>
713
- `;
714
- }).join('');
715
-
716
- return `
717
- <div class="language-stats">
718
- ${statsHtml}
719
- </div>
720
- `;
721
- }
722
-
723
- function getLanguageColor(language) {
724
- const colors = {
725
- javascript: '#f1e05a',
726
- typescript: '#2b7489',
727
- python: '#3572A5',
728
- java: '#b07219',
729
- html: '#e34c26',
730
- css: '#563d7c',
731
- json: '#292929',
732
- markdown: '#083fa1',
733
- go: '#00ADD8',
734
- rust: '#dea584',
735
- php: '#4F5D95',
736
- ruby: '#701516',
737
- other: '#cccccc'
738
- };
739
- return colors[language] || colors.other;
740
- }
741
-
742
- async function renderFileDiff(filePath, ext, gitInfo) {
743
- const breadcrumbs = generateBreadcrumbs(filePath);
744
-
745
- // Get git diff for the file
746
- return new Promise((resolve, reject) => {
747
- const diffCommand = gitInfo.staged ?
748
- `git diff --cached "${filePath}"` :
749
- `git diff "${filePath}"`;
750
-
751
- exec(diffCommand, { cwd: gitRepoRoot }, (error, stdout) => {
752
- if (error) {
753
- return reject(error);
754
- }
755
-
756
- const diffContent = renderRawDiff(stdout, ext);
757
- const currentParams = new URLSearchParams({ path: filePath });
758
- const viewUrl = `/?${currentParams.toString()}&view=rendered`;
759
- const rawUrl = `/?${currentParams.toString()}&view=raw`;
760
- const diffUrl = `/?${currentParams.toString()}&view=diff`;
761
-
762
- const viewToggle = `
763
- <div class="view-toggle">
764
- <a href="${viewUrl}" class="view-btn">
765
- ${octicons.eye.toSVG({ class: 'view-icon' })} View
766
- </a>
767
- ${ext === 'md' ? `
768
- <a href="${rawUrl}" class="view-btn">
769
- ${octicons['file-code'].toSVG({ class: 'view-icon' })} Raw
770
- </a>
771
- ` : ''}
772
- <a href="${diffUrl}" class="view-btn active">
773
- ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
774
- </a>
775
- </div>
776
- `;
777
-
778
- const html = `
779
- <!DOCTYPE html>
780
- <html data-theme="dark">
781
- <head>
782
- <title>gh-here: ${path.basename(filePath)} (diff)</title>
783
- <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
784
- <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
785
- <script src="/static/app.js"></script>
786
- </head>
787
- <body>
788
- <header>
789
- <div class="header-content">
790
- <div class="header-left">
791
- <h1 class="header-path">${breadcrumbs}</h1>
792
- </div>
793
- <div class="header-right">
794
- ${viewToggle}
795
- <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
796
- ${octicons.moon.toSVG({ class: 'theme-icon' })}
797
- </button>
798
- </div>
799
- </div>
800
- </header>
801
- <main>
802
- <div class="diff-container">
803
- <div class="diff-content">
804
- ${diffContent}
805
- </div>
806
- </div>
807
- </main>
808
- </body>
809
- </html>
810
- `;
811
-
812
- resolve(html);
813
- });
814
- });
815
- }
816
-
817
- function renderRawDiff(diffOutput, ext) {
818
- if (!diffOutput.trim()) {
819
- return '<div class="no-changes">No changes to display</div>';
820
- }
821
-
822
- const language = getLanguageFromExtension(ext);
823
-
824
- // Apply syntax highlighting to the entire diff
825
- let highlighted;
826
- try {
827
- // Use diff language for syntax highlighting if available, otherwise use the file's language
828
- highlighted = hljs.highlight(diffOutput, { language: 'diff' }).value;
829
- } catch {
830
- // Fallback to plain text if diff highlighting fails
831
- highlighted = diffOutput.replace(/&/g, '&amp;')
832
- .replace(/</g, '&lt;')
833
- .replace(/>/g, '&gt;');
834
- }
835
-
836
- // Split into lines and add line numbers
837
- const lines = highlighted.split('\n');
838
- let lineNumber = 1;
839
-
840
- const linesHtml = lines.map(line => {
841
- // Determine line type based on first character
842
- let lineType = 'context';
843
- let displayLine = line;
844
-
845
- if (line.startsWith('<span class="hljs-deletion">-') || line.startsWith('-')) {
846
- lineType = 'removed';
847
- } else if (line.startsWith('<span class="hljs-addition">+') || line.startsWith('+')) {
848
- lineType = 'added';
849
- } else if (line.startsWith('@@') || line.includes('hljs-meta')) {
850
- lineType = 'hunk';
851
- } else if (line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('---') || line.startsWith('+++')) {
852
- lineType = 'header';
853
- }
854
-
855
- const currentLineNumber = (lineType === 'context' || lineType === 'removed' || lineType === 'added') ? lineNumber++ : '';
856
-
857
- return `<div class="diff-line diff-line-${lineType}">
858
- <span class="diff-line-number">${currentLineNumber}</span>
859
- <span class="diff-line-content">${displayLine}</span>
860
- </div>`;
861
- }).join('');
862
-
863
- return `<div class="raw-diff-container">${linesHtml}</div>`;
864
- }
865
-
866
- async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null) {
867
- const breadcrumbs = generateBreadcrumbs(filePath);
868
- let displayContent;
869
- let viewToggle = '';
870
-
871
- // Check if file has git changes
872
- const absolutePath = path.resolve(path.join(workingDir, filePath));
873
- const hasGitChanges = gitStatus && gitStatus[absolutePath];
874
-
875
- if (ext === 'md') {
876
- if (viewMode === 'raw') {
877
- const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
878
-
879
- // Add line numbers for raw markdown view
880
- const lines = highlighted.split('\n');
881
- const numberedLines = lines.map((line, index) => {
882
- const lineNum = index + 1;
883
- return `<span class="line-container" data-line="${lineNum}"><a class="line-number" href="#L${lineNum}" id="L${lineNum}">${lineNum}</a><span class="line-content">${line}</span></span>`;
884
- }).join('');
885
-
886
- displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
887
- } else {
888
- displayContent = `<div class="markdown">${marked.parse(content)}</div>`;
889
- }
890
-
891
- const currentParams = new URLSearchParams({ path: filePath });
892
- const rawUrl = `/?${currentParams.toString()}&view=raw`;
893
- const renderedUrl = `/?${currentParams.toString()}&view=rendered`;
894
- const diffUrl = `/?${currentParams.toString()}&view=diff`;
895
-
896
- viewToggle = `
897
- <div class="view-toggle">
898
- <a href="${renderedUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
899
- ${octicons.eye.toSVG({ class: 'view-icon' })} View
900
- </a>
901
- <a href="${rawUrl}" class="view-btn ${viewMode === 'raw' ? 'active' : ''}">
902
- ${octicons['file-code'].toSVG({ class: 'view-icon' })} Raw
903
- </a>
904
- ${hasGitChanges ? `
905
- <a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
906
- ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
907
- </a>
908
- ` : ''}
909
- </div>
910
- `;
911
- } else {
912
- const language = getLanguageFromExtension(ext);
913
- const highlighted = language ?
914
- hljs.highlight(content, { language }).value :
915
- hljs.highlightAuto(content).value;
916
-
917
- // Add line numbers with clickable links
918
- const lines = highlighted.split('\n');
919
- const numberedLines = lines.map((line, index) => {
920
- const lineNum = index + 1;
921
- return `<span class="line-container" data-line="${lineNum}"><a class="line-number" href="#L${lineNum}" id="L${lineNum}">${lineNum}</a><span class="line-content">${line}</span></span>`;
922
- }).join('');
923
-
924
- displayContent = `<pre><code class="hljs with-line-numbers">${numberedLines}</code></pre>`;
925
-
926
- // Add view toggle for non-markdown files with git changes
927
- if (hasGitChanges) {
928
- const currentParams = new URLSearchParams({ path: filePath });
929
- const viewUrl = `/?${currentParams.toString()}&view=rendered`;
930
- const diffUrl = `/?${currentParams.toString()}&view=diff`;
931
-
932
- viewToggle = `
933
- <div class="view-toggle">
934
- <a href="${viewUrl}" class="view-btn ${viewMode === 'rendered' ? 'active' : ''}">
935
- ${octicons.eye.toSVG({ class: 'view-icon' })} View
936
- </a>
937
- <a href="${diffUrl}" class="view-btn ${viewMode === 'diff' ? 'active' : ''}">
938
- ${octicons.diff.toSVG({ class: 'view-icon' })} Diff
939
- </a>
940
- </div>
941
- `;
942
- }
943
- }
944
-
945
- return `
946
- <!DOCTYPE html>
947
- <html data-theme="dark">
948
- <head>
949
- <title>gh-here: ${path.basename(filePath)}</title>
950
- <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
951
- <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
952
- <script src="/static/app.js"></script>
953
- </head>
954
- <body>
955
- <header>
956
- <div class="header-content">
957
- <div class="header-left">
958
- <h1 class="header-path">
959
- ${breadcrumbs}
960
- ${hasGitChanges ? `<span class="git-status git-status-${hasGitChanges.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(hasGitChanges.status)}">${getGitStatusIcon(hasGitChanges.status)}</span>` : ''}
961
- </h1>
962
- </div>
963
- <div class="header-right">
964
- <div id="filename-input-container" class="filename-input-container" style="display: none;">
965
- <input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
966
- </div>
967
- <button id="edit-btn" class="edit-btn" aria-label="Edit file">
968
- ${octicons.pencil.toSVG({ class: 'edit-icon' })}
969
- </button>
970
- ${viewToggle}
971
- <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
972
- ${octicons.moon.toSVG({ class: 'theme-icon' })}
973
- </button>
974
- </div>
975
- </div>
976
- </header>
977
- <main>
978
- <div class="file-content">
979
- ${displayContent}
980
- </div>
981
- <div id="editor-container" class="editor-container" style="display: none;">
982
- <div class="editor-header">
983
- <div class="editor-title">Edit ${path.basename(filePath)}</div>
984
- <div class="editor-actions">
985
- <button id="cancel-btn" class="btn btn-secondary">Cancel</button>
986
- <button id="save-btn" class="btn btn-primary">Save</button>
987
- </div>
988
- </div>
989
- <div class="editor-with-line-numbers">
990
- <div class="editor-line-numbers" id="editor-line-numbers">1</div>
991
- <textarea id="file-editor" class="file-editor"></textarea>
992
- </div>
993
- </div>
994
- </main>
995
- </body>
996
- </html>
997
- `;
998
- }
999
-
1000
- function renderNewFile(currentPath) {
1001
- const breadcrumbs = generateBreadcrumbs(currentPath);
1002
-
1003
- return `
1004
- <!DOCTYPE html>
1005
- <html data-theme="dark">
1006
- <head>
1007
- <title>gh-here: Create new file</title>
1008
- <link rel="stylesheet" href="/static/styles.css?v=${Date.now()}">
1009
- <link rel="stylesheet" href="/static/highlight.css?v=${Date.now()}">
1010
- <script src="/static/app.js"></script>
1011
- </head>
1012
- <body>
1013
- <header>
1014
- <div class="header-content">
1015
- <div class="header-left">
1016
- <h1 class="header-path">${breadcrumbs}</h1>
1017
- </div>
1018
- <div class="header-right">
1019
- <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
1020
- ${octicons.moon.toSVG({ class: 'theme-icon' })}
1021
- </button>
1022
- </div>
1023
- </div>
1024
- </header>
1025
- <main>
1026
- <div class="new-file-container">
1027
- <div class="new-file-header">
1028
- <div class="filename-section">
1029
- <span class="filename-label">Name your file...</span>
1030
- <input type="text" id="new-filename-input" class="new-filename-input" placeholder="README.md" autofocus>
1031
- </div>
1032
- <div class="new-file-actions">
1033
- <button id="cancel-new-file" class="btn btn-secondary">Cancel</button>
1034
- <button id="create-new-file" class="btn btn-primary">Create file</button>
1035
- </div>
1036
- </div>
1037
- <div class="new-file-editor">
1038
- <div class="editor-with-line-numbers">
1039
- <div class="editor-line-numbers" id="new-file-line-numbers">1</div>
1040
- <textarea id="new-file-content" class="file-editor" placeholder="Enter file contents here..."></textarea>
1041
- </div>
1042
- </div>
1043
- </div>
1044
- </main>
1045
- </body>
1046
- </html>
1047
- `;
1048
- }
1049
-
1050
- function generateBreadcrumbs(currentPath, gitBranch = null) {
1051
- // At root, show gh-here branding with git branch if available
1052
- if (!currentPath || currentPath === '.') {
1053
- const gitBranchDisplay = gitBranch ? `<span class="git-branch">${octicons['git-branch'].toSVG({ class: 'octicon-branch' })} ${gitBranch}</span>` : '';
1054
- return `${octicons.home.toSVG({ class: 'octicon-home' })} gh-here ${gitBranchDisplay}`;
1055
- }
1056
-
1057
- // In subdirectories, show clickable path
1058
- const parts = currentPath.split('/').filter(p => p && p !== '.');
1059
- let breadcrumbs = `
1060
- <div class="breadcrumb-item">
1061
- <a href="/">${octicons.home.toSVG({ class: 'octicon-home' })}</a>
1062
- </div>
1063
- `;
1064
- let buildPath = '';
1065
-
1066
- parts.forEach((part, index) => {
1067
- buildPath += (buildPath ? '/' : '') + part;
1068
- breadcrumbs += `
1069
- <span class="breadcrumb-separator">/</span>
1070
- <div class="breadcrumb-item">
1071
- <a href="/?path=${encodeURIComponent(buildPath)}">
1072
- <span>${part}</span>
1073
- </a>
1074
- </div>
1075
- `;
1076
- });
1077
-
1078
- return breadcrumbs;
1079
- }
1080
-
1081
- function getFileIcon(filename) {
1082
- const ext = path.extname(filename).toLowerCase();
1083
- const name = filename.toLowerCase();
1084
-
1085
- try {
1086
- // Configuration files
1087
- if (name === 'package.json' || name === 'composer.json') {
1088
- return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
1089
- }
1090
- if (name === 'tsconfig.json' || name === 'jsconfig.json') {
1091
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1092
- }
1093
- if (name === '.eslintrc' || name === '.eslintrc.json' || name === '.eslintrc.js' || name === '.eslintrc.yml') {
1094
- return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
1095
- }
1096
- if (name === '.prettierrc' || name === 'prettier.config.js' || name === '.prettierrc.json') {
1097
- return octicons.gear?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
1098
- }
1099
- if (name === 'webpack.config.js' || name === 'vite.config.js' || name === 'rollup.config.js' || name === 'next.config.js' || name === 'nuxt.config.js' || name === 'svelte.config.js') {
1100
- return octicons.gear?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
1101
- }
1102
- if (name === 'tailwind.config.js' || name === 'postcss.config.js' || name === 'babel.config.js' || name === '.babelrc') {
1103
- return octicons.gear?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
1104
- }
1105
-
1106
- // Docker files
1107
- if (name === 'dockerfile' || name === 'dockerfile.dev' || name === '.dockerignore') {
1108
- return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
1109
- }
1110
- if (name === 'docker-compose.yml' || name === 'docker-compose.yaml') {
1111
- return octicons.container?.toSVG({ class: 'octicon-file text-blue' }) || octicons.file.toSVG({ class: 'octicon-file text-blue' });
1112
- }
1113
-
1114
- // Git files
1115
- if (name === '.gitignore' || name === '.gitattributes' || name === '.gitmodules') {
1116
- return octicons['git-branch']?.toSVG({ class: 'octicon-file text-orange' }) || octicons.file.toSVG({ class: 'octicon-file text-orange' });
1117
- }
1118
-
1119
- // Documentation
1120
- if (name.startsWith('readme') || name === 'changelog.md' || name === 'history.md') {
1121
- return octicons.book.toSVG({ class: 'octicon-file text-blue' });
1122
- }
1123
- if (name === 'license' || name === 'license.txt' || name === 'license.md') {
1124
- return octicons.law?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
1125
- }
1126
-
1127
- // Build files
1128
- if (name === 'makefile' || name === 'makefile.am' || name === 'cmakelists.txt') {
1129
- return octicons.tools?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
1130
- }
1131
- if (name.endsWith('.lock') || name === 'yarn.lock' || name === 'package-lock.json' || name === 'pipfile.lock') {
1132
- return octicons.lock?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
1133
- }
1134
-
1135
- // CI/CD files
1136
- if (name === '.travis.yml' || name === '.circleci' || name.startsWith('.github')) {
1137
- return octicons.gear?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
1138
- }
1139
-
1140
- // Environment files
1141
- if (name === '.env' || name === '.env.local' || name.startsWith('.env.')) {
1142
- return octicons.key?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
1143
- }
1144
-
1145
- // Extension-based icons
1146
- switch (ext) {
1147
- case '.js':
1148
- case '.mjs':
1149
- return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
1150
- case '.jsx':
1151
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1152
- case '.ts':
1153
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1154
- case '.tsx':
1155
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1156
- case '.vue':
1157
- return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
1158
- case '.svelte':
1159
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
1160
- case '.py':
1161
- case '.pyx':
1162
- case '.pyi':
1163
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1164
- case '.java':
1165
- case '.class':
1166
- return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
1167
- case '.c':
1168
- case '.h':
1169
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1170
- case '.cpp':
1171
- case '.cxx':
1172
- case '.cc':
1173
- case '.hpp':
1174
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1175
- case '.cs':
1176
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
1177
- case '.go':
1178
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1179
- case '.rs':
1180
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
1181
- case '.php':
1182
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
1183
- case '.rb':
1184
- return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
1185
- case '.swift':
1186
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
1187
- case '.kt':
1188
- case '.kts':
1189
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
1190
- case '.dart':
1191
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1192
- case '.scala':
1193
- return octicons['file-code'].toSVG({ class: 'octicon-file text-red' });
1194
- case '.clj':
1195
- case '.cljs':
1196
- return octicons['file-code'].toSVG({ class: 'octicon-file text-green' });
1197
- case '.hs':
1198
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
1199
- case '.elm':
1200
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1201
- case '.r':
1202
- return octicons['file-code'].toSVG({ class: 'octicon-file text-blue' });
1203
- case '.html':
1204
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
1205
- case '.css':
1206
- case '.scss':
1207
- case '.sass':
1208
- case '.less':
1209
- return octicons.paintbrush?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
1210
- case '.json':
1211
- return octicons['file-code'].toSVG({ class: 'octicon-file text-yellow' });
1212
- case '.xml':
1213
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
1214
- case '.yml':
1215
- case '.yaml':
1216
- return octicons['file-code'].toSVG({ class: 'octicon-file text-purple' });
1217
- case '.md':
1218
- case '.markdown':
1219
- return octicons.book.toSVG({ class: 'octicon-file text-blue' });
1220
- case '.txt':
1221
- return octicons['file-text']?.toSVG({ class: 'octicon-file text-gray' }) || octicons.file.toSVG({ class: 'octicon-file text-gray' });
1222
- case '.pdf':
1223
- return octicons['file-binary']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
1224
- case '.png':
1225
- case '.jpg':
1226
- case '.jpeg':
1227
- case '.gif':
1228
- case '.svg':
1229
- case '.webp':
1230
- return octicons['file-media']?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
1231
- case '.mp4':
1232
- case '.mov':
1233
- case '.avi':
1234
- case '.mkv':
1235
- return octicons['device-camera-video']?.toSVG({ class: 'octicon-file text-red' }) || octicons.file.toSVG({ class: 'octicon-file text-red' });
1236
- case '.mp3':
1237
- case '.wav':
1238
- case '.flac':
1239
- return octicons.unmute?.toSVG({ class: 'octicon-file text-purple' }) || octicons.file.toSVG({ class: 'octicon-file text-purple' });
1240
- case '.zip':
1241
- case '.tar':
1242
- case '.gz':
1243
- case '.rar':
1244
- case '.7z':
1245
- return octicons['file-zip']?.toSVG({ class: 'octicon-file text-yellow' }) || octicons.file.toSVG({ class: 'octicon-file text-yellow' });
1246
- case '.sh':
1247
- case '.bash':
1248
- case '.zsh':
1249
- case '.fish':
1250
- return octicons.terminal?.toSVG({ class: 'octicon-file text-green' }) || octicons.file.toSVG({ class: 'octicon-file text-green' });
1251
- case '.sql':
1252
- return octicons['file-code'].toSVG({ class: 'octicon-file text-orange' });
1253
- default:
1254
- return octicons.file.toSVG({ class: 'octicon-file text-gray' });
1255
- }
1256
- } catch (error) {
1257
- return octicons.file.toSVG({ class: 'octicon-file text-gray' });
1258
- }
1259
- }
1260
-
1261
- function getColorForExtension(ext) {
1262
- const colorMap = {
1263
- '.js': 'yellow', '.jsx': 'yellow', '.ts': 'blue', '.tsx': 'blue',
1264
- '.py': 'green', '.java': 'red', '.go': 'blue', '.rs': 'orange',
1265
- '.html': 'orange', '.css': 'purple', '.json': 'yellow',
1266
- '.md': 'blue', '.txt': 'gray', '.sh': 'green'
1267
- };
1268
- return colorMap[ext] || 'gray';
1269
- }
1270
-
1271
- function getLanguageFromExtension(ext) {
1272
- const langMap = {
1273
- 'js': 'javascript',
1274
- 'jsx': 'javascript',
1275
- 'ts': 'typescript',
1276
- 'tsx': 'typescript',
1277
- 'py': 'python',
1278
- 'java': 'java',
1279
- 'html': 'html',
1280
- 'css': 'css',
1281
- 'scss': 'scss',
1282
- 'sass': 'sass',
1283
- 'json': 'json',
1284
- 'xml': 'xml',
1285
- 'yaml': 'yaml',
1286
- 'yml': 'yaml',
1287
- 'sh': 'bash',
1288
- 'bash': 'bash',
1289
- 'zsh': 'bash',
1290
- 'go': 'go',
1291
- 'rs': 'rust',
1292
- 'cpp': 'cpp',
1293
- 'cxx': 'cpp',
1294
- 'cc': 'cpp',
1295
- 'c': 'c',
1296
- 'h': 'c',
1297
- 'hpp': 'cpp',
1298
- 'php': 'php',
1299
- 'rb': 'ruby',
1300
- 'swift': 'swift',
1301
- 'kt': 'kotlin',
1302
- 'dart': 'dart',
1303
- 'r': 'r',
1304
- 'sql': 'sql',
1305
- 'dockerfile': 'dockerfile',
1306
- 'md': 'markdown',
1307
- 'markdown': 'markdown',
1308
- 'vue': 'vue',
1309
- 'svelte': 'svelte'
1310
- };
1311
- return langMap[ext];
1312
- }
1313
-
1314
- function formatBytes(bytes) {
1315
- if (bytes === 0) return '0 B';
1316
- const k = 1024;
1317
- const sizes = ['B', 'KB', 'MB', 'GB'];
1318
- const i = Math.floor(Math.log(bytes) / Math.log(k));
1319
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1320
- }
1321
-
1322
75
  // Function to open browser
1323
76
  function openBrowserToUrl(url) {
1324
77
  let command;