gh-here 1.0.5 → 1.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/.claude/settings.local.json +2 -1
- package/README.md +11 -44
- package/lib/file-utils.js +45 -1
- package/lib/renderers.js +121 -41
- package/lib/server.js +24 -4
- package/package.json +1 -1
- package/public/styles.css +306 -69
- package/tests/fileTypeDetection.test.js +111 -0
package/README.md
CHANGED
|
@@ -29,11 +29,12 @@ Navigate to any directory and run:
|
|
|
29
29
|
```bash
|
|
30
30
|
gh-here # Start server on available port
|
|
31
31
|
gh-here --open # Start server and open browser
|
|
32
|
+
gh-here --port=8080 # Start server on port 8080
|
|
32
33
|
gh-here --open --browser=safari # Start server and open in Safari
|
|
33
34
|
gh-here --open --browser=arc # Start server and open in Arc
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
The app will automatically find an available port starting from
|
|
37
|
+
The app will automatically find an available port starting from 5555 and serve your current directory with a GitHub-like interface.
|
|
37
38
|
|
|
38
39
|
## Features
|
|
39
40
|
|
|
@@ -46,12 +47,13 @@ The app will automatically find an available port starting from 3000 and serve y
|
|
|
46
47
|
- File and folder creation, editing, renaming, and deletion
|
|
47
48
|
|
|
48
49
|
### 🎨 Code Viewing & Editing
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
50
|
+
- VS Code-quality Monaco Editor with advanced syntax highlighting for 30+ languages
|
|
51
|
+
- GitHub-style line numbers with selection (click, shift-click, ctrl-click)
|
|
52
|
+
- Professional in-browser file editing with auto-save to localStorage
|
|
53
|
+
- Draft management with persistence across sessions
|
|
52
54
|
- Raw and rendered markdown views
|
|
53
55
|
- Shareable URLs with line selections (`#L10-L20`)
|
|
54
|
-
-
|
|
56
|
+
- Monaco Editor features: IntelliSense, bracket matching, folding
|
|
55
57
|
|
|
56
58
|
### 🔀 Git Integration
|
|
57
59
|
- Automatic git repository detection
|
|
@@ -86,44 +88,9 @@ The app will automatically find an available port starting from 3000 and serve y
|
|
|
86
88
|
- Error handling and loading states
|
|
87
89
|
- Notification system for user feedback
|
|
88
90
|
|
|
89
|
-
##
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- JavaScript (.js, .mjs, .jsx) - React, Node.js
|
|
93
|
-
- TypeScript (.ts, .tsx)
|
|
94
|
-
- Python (.py)
|
|
95
|
-
- Java (.java)
|
|
96
|
-
- Go (.go)
|
|
97
|
-
- Rust (.rs)
|
|
98
|
-
- PHP (.php)
|
|
99
|
-
- Ruby (.rb)
|
|
100
|
-
- Swift (.swift)
|
|
101
|
-
- Kotlin (.kt)
|
|
102
|
-
- Dart (.dart)
|
|
103
|
-
|
|
104
|
-
### Web Technologies
|
|
105
|
-
- HTML (.html)
|
|
106
|
-
- CSS (.css, .scss, .sass, .less)
|
|
107
|
-
- JSON (.json)
|
|
108
|
-
- XML (.xml)
|
|
109
|
-
- YAML (.yml, .yaml)
|
|
110
|
-
|
|
111
|
-
### Documentation & Config
|
|
112
|
-
- Markdown (.md) - with beautiful rendering
|
|
113
|
-
- Text files (.txt)
|
|
114
|
-
- Configuration files (ESLint, Prettier, Webpack, etc.)
|
|
115
|
-
- Docker files (Dockerfile, docker-compose.yml)
|
|
116
|
-
- Environment files (.env)
|
|
117
|
-
|
|
118
|
-
### Media & Archives
|
|
119
|
-
- Images (.png, .jpg, .gif, .svg, .webp)
|
|
120
|
-
- Videos (.mp4, .mov, .avi)
|
|
121
|
-
- Audio (.mp3, .wav, .flac)
|
|
122
|
-
- Archives (.zip, .tar, .gz, .rar)
|
|
123
|
-
|
|
124
|
-
### Shell & Database
|
|
125
|
-
- Shell scripts (.sh, .bash, .zsh)
|
|
126
|
-
- SQL (.sql)
|
|
91
|
+
## Language Support
|
|
92
|
+
|
|
93
|
+
Supports syntax highlighting for 30+ languages including JavaScript, TypeScript, Python, Go, Rust, Java, C/C++, and many more through Monaco Editor integration.
|
|
127
94
|
|
|
128
95
|
## Keyboard Shortcuts
|
|
129
96
|
|
|
@@ -161,7 +128,7 @@ npm install
|
|
|
161
128
|
npm start
|
|
162
129
|
```
|
|
163
130
|
|
|
164
|
-
Navigate to `http://localhost:
|
|
131
|
+
Navigate to `http://localhost:5555` to view the interface.
|
|
165
132
|
|
|
166
133
|
## Dependencies
|
|
167
134
|
|
package/lib/file-utils.js
CHANGED
|
@@ -256,9 +256,53 @@ function formatBytes(bytes) {
|
|
|
256
256
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* File type detection functions
|
|
261
|
+
*/
|
|
262
|
+
function isImageFile(filePathOrExt) {
|
|
263
|
+
const ext = getExtension(filePathOrExt);
|
|
264
|
+
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico'];
|
|
265
|
+
return imageExtensions.includes(ext);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isBinaryFile(filePathOrExt) {
|
|
269
|
+
const ext = getExtension(filePathOrExt);
|
|
270
|
+
const binaryExtensions = [
|
|
271
|
+
// Images (handled separately)
|
|
272
|
+
'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico',
|
|
273
|
+
// Archives
|
|
274
|
+
'zip', 'tar', 'gz', 'rar', '7z',
|
|
275
|
+
// Executables
|
|
276
|
+
'exe', 'bin', 'app', 'deb', 'rpm',
|
|
277
|
+
// Documents
|
|
278
|
+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
|
|
279
|
+
// Media
|
|
280
|
+
'mp4', 'mov', 'avi', 'mkv', 'mp3', 'wav', 'flac',
|
|
281
|
+
// Other
|
|
282
|
+
'class', 'so', 'dll', 'dylib'
|
|
283
|
+
];
|
|
284
|
+
return binaryExtensions.includes(ext);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isTextFile(filePathOrExt) {
|
|
288
|
+
return !isBinaryFile(filePathOrExt);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getExtension(filePathOrExt) {
|
|
292
|
+
// If it's already just an extension (no dots), return as-is
|
|
293
|
+
if (!filePathOrExt.includes('.') && !filePathOrExt.includes('/') && !filePathOrExt.includes('\\')) {
|
|
294
|
+
return filePathOrExt.toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
// Extract extension from file path
|
|
297
|
+
return path.extname(filePathOrExt).toLowerCase().slice(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
259
300
|
module.exports = {
|
|
260
301
|
getFileIcon,
|
|
261
302
|
getLanguageFromExtension,
|
|
262
303
|
getLanguageColor,
|
|
263
|
-
formatBytes
|
|
304
|
+
formatBytes,
|
|
305
|
+
isImageFile,
|
|
306
|
+
isBinaryFile,
|
|
307
|
+
isTextFile
|
|
264
308
|
};
|
package/lib/renderers.js
CHANGED
|
@@ -5,7 +5,7 @@ const marked = require('marked');
|
|
|
5
5
|
const octicons = require('@primer/octicons');
|
|
6
6
|
const { exec } = require('child_process');
|
|
7
7
|
|
|
8
|
-
const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes } = require('./file-utils');
|
|
8
|
+
const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes, isImageFile, isBinaryFile, isTextFile } = require('./file-utils');
|
|
9
9
|
const { getGitStatusIcon, getGitStatusDescription } = require('./git');
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -13,8 +13,9 @@ const { getGitStatusIcon, getGitStatusDescription } = require('./git');
|
|
|
13
13
|
* Handles all HTML template generation for different views
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
function renderDirectory(currentPath, items, showGitignored = false, gitBranch = null, gitStatus = {}) {
|
|
17
|
-
const
|
|
16
|
+
function renderDirectory(currentPath, items, showGitignored = false, gitBranch = null, gitStatus = {}, workingDir = null) {
|
|
17
|
+
const workingDirName = workingDir ? path.basename(workingDir) : null;
|
|
18
|
+
const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch, workingDirName);
|
|
18
19
|
const readmeFile = findReadmeFile(items);
|
|
19
20
|
const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
|
|
20
21
|
const languageStats = generateLanguageStats(items);
|
|
@@ -22,7 +23,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
22
23
|
const itemsHtml = items.map(item => `
|
|
23
24
|
<tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
|
|
24
25
|
<td class="icon">
|
|
25
|
-
${item.isDirectory ? octicons['file-directory'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
|
|
26
|
+
${item.isDirectory ? octicons['file-directory-fill'].toSVG({ class: 'octicon-directory' }) : getFileIcon(item.name)}
|
|
26
27
|
</td>
|
|
27
28
|
<td class="git-status-col">
|
|
28
29
|
${item.gitStatus ? (item.gitStatus.status === '??' ? `<span class="git-status git-status-untracked" title="Untracked file">${require('./git').getGitStatusIcon('??')}</span>` : `<span class="git-status git-status-${item.gitStatus.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(item.gitStatus.status)}">${getGitStatusIcon(item.gitStatus.status)}</span>`) : ''}
|
|
@@ -78,13 +79,9 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
78
79
|
<header>
|
|
79
80
|
<div class="header-content">
|
|
80
81
|
<div class="header-left">
|
|
81
|
-
<h1
|
|
82
|
+
<h1>gh-here</h1>
|
|
82
83
|
</div>
|
|
83
84
|
<div class="header-right">
|
|
84
|
-
<div class="search-container">
|
|
85
|
-
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
86
|
-
<input type="text" id="file-search" placeholder="Find files..." class="search-input">
|
|
87
|
-
</div>
|
|
88
85
|
${Object.keys(gitStatus).length > 0 ? `
|
|
89
86
|
<button id="commit-btn" class="commit-btn" title="Commit changes">
|
|
90
87
|
${octicons['git-commit'].toSVG({ class: 'commit-icon' })}
|
|
@@ -100,15 +97,42 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
100
97
|
</div>
|
|
101
98
|
</div>
|
|
102
99
|
</header>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
|
|
101
|
+
<!-- White canvas section like GitHub -->
|
|
102
|
+
<div class="repo-canvas">
|
|
103
|
+
<div class="repo-canvas-content">
|
|
104
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
105
|
+
<hr class="repo-divider">
|
|
106
|
+
|
|
107
|
+
<div class="repo-controls">
|
|
108
|
+
<div class="repo-controls-left">
|
|
109
|
+
${gitBranch ? `
|
|
110
|
+
<button class="branch-button">
|
|
111
|
+
${octicons['git-branch'].toSVG({ class: 'octicon-branch' })}
|
|
112
|
+
<span class="branch-name">${gitBranch}</span>
|
|
113
|
+
${octicons['chevron-down'].toSVG({ class: 'octicon-chevron' })}
|
|
114
|
+
</button>
|
|
115
|
+
` : ''}
|
|
116
|
+
</div>
|
|
117
|
+
<div class="repo-controls-right">
|
|
118
|
+
<div class="search-container">
|
|
119
|
+
${octicons.search.toSVG({ class: 'search-icon' })}
|
|
120
|
+
<input type="text" id="file-search" placeholder="Go to file" class="search-input">
|
|
121
|
+
<kbd class="search-hotkey">t</kbd>
|
|
122
|
+
</div>
|
|
123
|
+
<button id="new-file-btn" class="btn btn-outline">
|
|
124
|
+
<span class="btn-text">New file</span>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
109
128
|
</div>
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
</div>
|
|
130
|
+
<main>
|
|
131
|
+
<div class="repo-canvas">
|
|
132
|
+
<div class="repo-canvas-content">
|
|
133
|
+
${languageStats}
|
|
134
|
+
<div class="file-table-container">
|
|
135
|
+
<table class="file-table" id="file-table">
|
|
112
136
|
<thead>
|
|
113
137
|
<tr>
|
|
114
138
|
<th></th>
|
|
@@ -133,8 +157,10 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
|
|
|
133
157
|
${itemsHtml}
|
|
134
158
|
</tbody>
|
|
135
159
|
</table>
|
|
160
|
+
</div>
|
|
161
|
+
${readmePreview}
|
|
162
|
+
</div>
|
|
136
163
|
</div>
|
|
137
|
-
${readmePreview}
|
|
138
164
|
</main>
|
|
139
165
|
</body>
|
|
140
166
|
</html>
|
|
@@ -265,7 +291,7 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot) {
|
|
|
265
291
|
<header>
|
|
266
292
|
<div class="header-content">
|
|
267
293
|
<div class="header-left">
|
|
268
|
-
<h1
|
|
294
|
+
<h1>gh-here</h1>
|
|
269
295
|
</div>
|
|
270
296
|
<div class="header-right">
|
|
271
297
|
${viewToggle}
|
|
@@ -276,9 +302,12 @@ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot) {
|
|
|
276
302
|
</div>
|
|
277
303
|
</header>
|
|
278
304
|
<main>
|
|
279
|
-
<div class="
|
|
280
|
-
<div class="
|
|
281
|
-
|
|
305
|
+
<div class="main-content">
|
|
306
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
307
|
+
<div class="diff-container">
|
|
308
|
+
<div class="diff-content">
|
|
309
|
+
${diffContent}
|
|
310
|
+
</div>
|
|
282
311
|
</div>
|
|
283
312
|
</div>
|
|
284
313
|
</main>
|
|
@@ -340,6 +369,7 @@ function renderRawDiff(diffOutput, ext) {
|
|
|
340
369
|
return `<div class="raw-diff-container">${linesHtml}</div>`;
|
|
341
370
|
}
|
|
342
371
|
|
|
372
|
+
|
|
343
373
|
async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir) {
|
|
344
374
|
const breadcrumbs = generateBreadcrumbs(filePath);
|
|
345
375
|
let displayContent;
|
|
@@ -349,7 +379,49 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
349
379
|
const absolutePath = path.resolve(path.join(workingDir, filePath));
|
|
350
380
|
const hasGitChanges = gitStatus && gitStatus[absolutePath];
|
|
351
381
|
|
|
352
|
-
|
|
382
|
+
// Determine file category and handle accordingly
|
|
383
|
+
if (isImageFile(ext)) {
|
|
384
|
+
// Handle image files
|
|
385
|
+
const imageUrl = `/download?path=${encodeURIComponent(filePath)}`;
|
|
386
|
+
displayContent = `
|
|
387
|
+
<div class="image-container">
|
|
388
|
+
<img src="${imageUrl}" alt="${path.basename(filePath)}" class="image-display">
|
|
389
|
+
<div class="image-info">
|
|
390
|
+
<span class="image-filename">${path.basename(filePath)}</span>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
`;
|
|
394
|
+
|
|
395
|
+
// Add diff view for images with git changes
|
|
396
|
+
if (hasGitChanges) {
|
|
397
|
+
const currentParams = new URLSearchParams({ path: filePath });
|
|
398
|
+
const diffUrl = `/?${currentParams.toString()}&view=diff`;
|
|
399
|
+
|
|
400
|
+
viewToggle = `
|
|
401
|
+
<div class="view-toggle">
|
|
402
|
+
<a href="/?path=${encodeURIComponent(filePath)}" class="view-btn active">
|
|
403
|
+
${octicons.eye.toSVG({ class: 'view-icon' })} View
|
|
404
|
+
</a>
|
|
405
|
+
<a href="${diffUrl}" class="view-btn">
|
|
406
|
+
${octicons.diff.toSVG({ class: 'view-icon' })} Diff
|
|
407
|
+
</a>
|
|
408
|
+
</div>
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
} else if (isBinaryFile(ext) && !isImageFile(ext)) {
|
|
412
|
+
// Handle other binary files - download only
|
|
413
|
+
displayContent = `
|
|
414
|
+
<div class="binary-file-container">
|
|
415
|
+
<div class="binary-file-info">
|
|
416
|
+
<h3>Binary File</h3>
|
|
417
|
+
<p>This is a binary file that cannot be displayed in the browser.</p>
|
|
418
|
+
<a href="/download?path=${encodeURIComponent(filePath)}" class="btn btn-primary" download="${path.basename(filePath)}">
|
|
419
|
+
${octicons.download.toSVG({ class: 'download-icon' })} Download File
|
|
420
|
+
</a>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
`;
|
|
424
|
+
} else if (ext === 'md') {
|
|
353
425
|
if (viewMode === 'raw') {
|
|
354
426
|
const highlighted = hljs.highlight(content, { language: 'markdown' }).value;
|
|
355
427
|
|
|
@@ -433,18 +505,17 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
433
505
|
<header>
|
|
434
506
|
<div class="header-content">
|
|
435
507
|
<div class="header-left">
|
|
436
|
-
<h1
|
|
437
|
-
${breadcrumbs}
|
|
438
|
-
${hasGitChanges ? (hasGitChanges.status === '??' ? `<span class="git-status git-status-untracked" title="Untracked file">${getGitStatusIcon('??')}</span>` : `<span class="git-status git-status-${hasGitChanges.status.replace(' ', '')}" title="Git Status: ${getGitStatusDescription(hasGitChanges.status)}">${getGitStatusIcon(hasGitChanges.status)}</span>`) : ''}
|
|
439
|
-
</h1>
|
|
508
|
+
<h1>gh-here</h1>
|
|
440
509
|
</div>
|
|
441
510
|
<div class="header-right">
|
|
442
511
|
<div id="filename-input-container" class="filename-input-container" style="display: none;">
|
|
443
512
|
<input type="text" id="filename-input" class="filename-input" placeholder="Name your file...">
|
|
444
513
|
</div>
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
514
|
+
${isTextFile(ext) ? `
|
|
515
|
+
<button id="edit-btn" class="edit-btn" aria-label="Edit file">
|
|
516
|
+
${octicons.pencil.toSVG({ class: 'edit-icon' })}
|
|
517
|
+
</button>
|
|
518
|
+
` : ''}
|
|
448
519
|
${viewToggle}
|
|
449
520
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
450
521
|
${octicons.moon.toSVG({ class: 'theme-icon' })}
|
|
@@ -453,9 +524,12 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
453
524
|
</div>
|
|
454
525
|
</header>
|
|
455
526
|
<main>
|
|
456
|
-
<div class="
|
|
457
|
-
|
|
458
|
-
|
|
527
|
+
<div class="main-content">
|
|
528
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
529
|
+
<div class="file-content">
|
|
530
|
+
${displayContent}
|
|
531
|
+
</div>
|
|
532
|
+
${isTextFile(ext) ? `
|
|
459
533
|
<div id="editor-container" class="editor-container" style="display: none;">
|
|
460
534
|
<div class="editor-header">
|
|
461
535
|
<div class="editor-title">Edit ${path.basename(filePath)}</div>
|
|
@@ -469,6 +543,8 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
|
|
|
469
543
|
<div id="file-editor" class="monaco-editor"></div>
|
|
470
544
|
</div>
|
|
471
545
|
</div>
|
|
546
|
+
` : ''}
|
|
547
|
+
</div>
|
|
472
548
|
</main>
|
|
473
549
|
</body>
|
|
474
550
|
</html>
|
|
@@ -492,7 +568,7 @@ function renderNewFile(currentPath) {
|
|
|
492
568
|
<header>
|
|
493
569
|
<div class="header-content">
|
|
494
570
|
<div class="header-left">
|
|
495
|
-
<h1
|
|
571
|
+
<h1>gh-here</h1>
|
|
496
572
|
</div>
|
|
497
573
|
<div class="header-right">
|
|
498
574
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme">
|
|
@@ -502,8 +578,10 @@ function renderNewFile(currentPath) {
|
|
|
502
578
|
</div>
|
|
503
579
|
</header>
|
|
504
580
|
<main>
|
|
505
|
-
<div class="
|
|
506
|
-
<div class="
|
|
581
|
+
<div class="main-content">
|
|
582
|
+
<div class="breadcrumb-section">${breadcrumbs}</div>
|
|
583
|
+
<div class="new-file-container">
|
|
584
|
+
<div class="new-file-header">
|
|
507
585
|
<div class="filename-section">
|
|
508
586
|
<span class="filename-label">Name your file...</span>
|
|
509
587
|
<input type="text" id="new-filename-input" class="new-filename-input" placeholder="README.md" autofocus>
|
|
@@ -519,24 +597,26 @@ function renderNewFile(currentPath) {
|
|
|
519
597
|
</div>
|
|
520
598
|
</div>
|
|
521
599
|
</div>
|
|
600
|
+
</div>
|
|
522
601
|
</main>
|
|
523
602
|
</body>
|
|
524
603
|
</html>
|
|
525
604
|
`;
|
|
526
605
|
}
|
|
527
606
|
|
|
528
|
-
function generateBreadcrumbs(currentPath, gitBranch = null) {
|
|
529
|
-
|
|
607
|
+
function generateBreadcrumbs(currentPath, gitBranch = null, workingDirName = null) {
|
|
608
|
+
const rootDisplayName = workingDirName || 'repository';
|
|
609
|
+
|
|
610
|
+
// At root, show working directory name
|
|
530
611
|
if (!currentPath || currentPath === '.') {
|
|
531
|
-
|
|
532
|
-
return `${octicons.home.toSVG({ class: 'octicon-home' })} gh-here ${gitBranchDisplay}`;
|
|
612
|
+
return `${octicons.home.toSVG({ class: 'octicon-home' })} ${rootDisplayName}`;
|
|
533
613
|
}
|
|
534
614
|
|
|
535
615
|
// In subdirectories, show clickable path
|
|
536
616
|
const parts = currentPath.split('/').filter(p => p && p !== '.');
|
|
537
617
|
let breadcrumbs = `
|
|
538
618
|
<div class="breadcrumb-item">
|
|
539
|
-
<a href="/">${octicons.home.toSVG({ class: 'octicon-home' })}</a>
|
|
619
|
+
<a href="/">${octicons.home.toSVG({ class: 'octicon-home' })} ${rootDisplayName}</a>
|
|
540
620
|
</div>
|
|
541
621
|
`;
|
|
542
622
|
let buildPath = '';
|
package/lib/server.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { getGitStatus, getGitBranch, commitAllChanges, commitSelectedFiles, getGitDiff } = require('./git');
|
|
6
6
|
const { isIgnoredByGitignore, getGitignoreRules } = require('./gitignore');
|
|
7
7
|
const { renderDirectory, renderFileDiff, renderFile, renderNewFile } = require('./renderers');
|
|
8
|
+
const { isImageFile, isBinaryFile } = require('./file-utils');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Express server setup and route handlers
|
|
@@ -16,6 +17,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
16
17
|
app.use('/static', express.static(path.join(__dirname, '..', 'public')));
|
|
17
18
|
app.use('/octicons', express.static(path.join(__dirname, '..', 'node_modules', '@primer', 'octicons', 'build')));
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
// Download route
|
|
20
22
|
app.get('/download', (req, res) => {
|
|
21
23
|
const filePath = req.query.path || '';
|
|
@@ -25,8 +27,16 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
25
27
|
const stats = fs.statSync(fullPath);
|
|
26
28
|
if (stats.isFile()) {
|
|
27
29
|
const fileName = path.basename(fullPath);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
// For images, serve inline for viewing, otherwise force download
|
|
32
|
+
if (isImageFile(fullPath)) {
|
|
33
|
+
// Serve image inline
|
|
34
|
+
res.sendFile(fullPath);
|
|
35
|
+
} else {
|
|
36
|
+
// Force download for non-images
|
|
37
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
38
|
+
res.sendFile(fullPath);
|
|
39
|
+
}
|
|
30
40
|
} else {
|
|
31
41
|
res.status(400).send('Cannot download directories');
|
|
32
42
|
}
|
|
@@ -82,12 +92,17 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
82
92
|
return a.name.localeCompare(b.name);
|
|
83
93
|
});
|
|
84
94
|
|
|
85
|
-
res.send(renderDirectory(currentPath, items, showGitignored, gitBranch, gitStatus));
|
|
95
|
+
res.send(renderDirectory(currentPath, items, showGitignored, gitBranch, gitStatus, workingDir));
|
|
86
96
|
} else {
|
|
87
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
88
97
|
const ext = path.extname(fullPath).slice(1);
|
|
89
98
|
const viewMode = req.query.view || 'rendered';
|
|
90
99
|
|
|
100
|
+
// For image files, don't try to read as text
|
|
101
|
+
let content = '';
|
|
102
|
+
if (!isImageFile(fullPath)) {
|
|
103
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
if (viewMode === 'diff' && isGitRepo) {
|
|
92
107
|
// Check if file has git status
|
|
93
108
|
const absolutePath = path.resolve(fullPath);
|
|
@@ -122,6 +137,11 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
|
|
|
122
137
|
return res.status(403).send('Access denied');
|
|
123
138
|
}
|
|
124
139
|
|
|
140
|
+
// Check if it's a binary file - prevent editing
|
|
141
|
+
if (isBinaryFile(fullPath)) {
|
|
142
|
+
return res.status(400).json({ error: 'Cannot edit binary files' });
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
126
146
|
res.send(content);
|
|
127
147
|
} catch (error) {
|
package/package.json
CHANGED
package/public/styles.css
CHANGED
|
@@ -52,15 +52,15 @@ body {
|
|
|
52
52
|
header {
|
|
53
53
|
background-color: var(--bg-secondary);
|
|
54
54
|
border-bottom: 1px solid var(--border-primary);
|
|
55
|
-
padding: 16px 24px;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
.header-content {
|
|
59
58
|
display: flex;
|
|
60
59
|
justify-content: space-between;
|
|
61
60
|
align-items: flex-start;
|
|
62
|
-
max-width:
|
|
61
|
+
max-width: 900px;
|
|
63
62
|
margin: 0 auto;
|
|
63
|
+
padding: 16px 24px;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
.header-left h1 {
|
|
@@ -82,6 +82,203 @@ header {
|
|
|
82
82
|
gap: 12px;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/* GitHub-style White Canvas Section */
|
|
86
|
+
.repo-canvas {
|
|
87
|
+
background-color: var(--bg-primary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.repo-canvas-content {
|
|
91
|
+
max-width: 900px;
|
|
92
|
+
margin: 0 auto;
|
|
93
|
+
padding: 16px 24px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.breadcrumb-section {
|
|
97
|
+
font-size: 20px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
color: var(--text-primary);
|
|
100
|
+
margin: 0 0 12px 0;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.repo-name {
|
|
107
|
+
font-size: 20px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
color: var(--text-primary);
|
|
110
|
+
margin: 0 0 12px 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.repo-divider {
|
|
114
|
+
border: none;
|
|
115
|
+
border-top: 1px solid var(--border-primary);
|
|
116
|
+
margin: 0 0 12px 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.repo-controls {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: space-between;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.repo-controls-left {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.repo-controls-right {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 12px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.branch-button {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 6px;
|
|
140
|
+
padding: 5px 12px;
|
|
141
|
+
background: var(--bg-primary);
|
|
142
|
+
border: 1px solid var(--border-primary);
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
color: var(--text-primary);
|
|
145
|
+
font-size: 14px;
|
|
146
|
+
font-weight: 500;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
transition: all 0.2s ease;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.branch-button:hover {
|
|
152
|
+
background: var(--bg-secondary);
|
|
153
|
+
border-color: var(--text-secondary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.branch-count,
|
|
157
|
+
.tag-count {
|
|
158
|
+
color: var(--text-secondary);
|
|
159
|
+
font-size: 14px;
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 6px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.octicon-branch {
|
|
166
|
+
width: 16px;
|
|
167
|
+
height: 16px;
|
|
168
|
+
fill: var(--text-secondary);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.octicon-small {
|
|
172
|
+
width: 14px;
|
|
173
|
+
height: 14px;
|
|
174
|
+
fill: var(--text-secondary);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.branch-name {
|
|
178
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.octicon-chevron {
|
|
183
|
+
width: 12px;
|
|
184
|
+
height: 12px;
|
|
185
|
+
fill: var(--text-secondary);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.search-container {
|
|
189
|
+
position: relative;
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.search-input {
|
|
195
|
+
background: var(--bg-primary);
|
|
196
|
+
border: 1px solid var(--border-primary);
|
|
197
|
+
border-radius: 6px;
|
|
198
|
+
padding: 6px 40px 6px 32px;
|
|
199
|
+
color: var(--text-primary);
|
|
200
|
+
font-size: 14px;
|
|
201
|
+
width: 300px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.search-input:focus {
|
|
205
|
+
outline: none;
|
|
206
|
+
border-color: #0969da;
|
|
207
|
+
box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.3);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.search-icon {
|
|
211
|
+
position: absolute;
|
|
212
|
+
left: 8px;
|
|
213
|
+
width: 16px;
|
|
214
|
+
height: 16px;
|
|
215
|
+
fill: var(--text-secondary);
|
|
216
|
+
pointer-events: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.search-hotkey {
|
|
220
|
+
position: absolute;
|
|
221
|
+
right: 8px;
|
|
222
|
+
background: var(--bg-tertiary);
|
|
223
|
+
border: 1px solid var(--border-primary);
|
|
224
|
+
border-radius: 3px;
|
|
225
|
+
padding: 2px 6px;
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
color: var(--text-secondary);
|
|
228
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* GitHub-style Buttons */
|
|
232
|
+
.btn {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 6px;
|
|
236
|
+
padding: 6px 12px;
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
font-size: 14px;
|
|
239
|
+
font-weight: 500;
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
transition: all 0.2s ease;
|
|
242
|
+
border: 1px solid;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.btn-primary {
|
|
246
|
+
background: var(--bg-primary);
|
|
247
|
+
border-color: var(--border-primary);
|
|
248
|
+
color: var(--text-primary);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.btn-primary:hover {
|
|
252
|
+
background: var(--bg-secondary);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.btn-outline {
|
|
256
|
+
background: var(--bg-primary);
|
|
257
|
+
border-color: var(--border-primary);
|
|
258
|
+
color: var(--text-primary);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.btn-outline:hover {
|
|
262
|
+
background: var(--bg-secondary);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.btn-success {
|
|
266
|
+
background: #238636;
|
|
267
|
+
border-color: rgba(240, 246, 252, 0.1);
|
|
268
|
+
color: #ffffff;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.btn-success:hover {
|
|
272
|
+
background: #2ea043;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.btn-icon,
|
|
276
|
+
.btn-chevron {
|
|
277
|
+
width: 16px;
|
|
278
|
+
height: 16px;
|
|
279
|
+
fill: currentColor;
|
|
280
|
+
}
|
|
281
|
+
|
|
85
282
|
.theme-toggle,
|
|
86
283
|
.gitignore-toggle,
|
|
87
284
|
.edit-btn {
|
|
@@ -158,7 +355,7 @@ header {
|
|
|
158
355
|
}
|
|
159
356
|
|
|
160
357
|
.breadcrumb-item {
|
|
161
|
-
display: flex;
|
|
358
|
+
display: inline-flex;
|
|
162
359
|
align-items: center;
|
|
163
360
|
}
|
|
164
361
|
|
|
@@ -184,9 +381,13 @@ header {
|
|
|
184
381
|
}
|
|
185
382
|
|
|
186
383
|
main {
|
|
187
|
-
padding:
|
|
188
|
-
|
|
384
|
+
padding: 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.main-content {
|
|
388
|
+
max-width: 900px;
|
|
189
389
|
margin: 0 auto;
|
|
390
|
+
padding: 16px 24px;
|
|
190
391
|
}
|
|
191
392
|
|
|
192
393
|
.directory-actions {
|
|
@@ -261,13 +462,13 @@ main {
|
|
|
261
462
|
vertical-align: text-bottom;
|
|
262
463
|
}
|
|
263
464
|
|
|
264
|
-
/* GitHub-style blue folder icons */
|
|
465
|
+
/* GitHub-style baby blue folder icons */
|
|
265
466
|
.octicon-directory {
|
|
266
|
-
fill: #
|
|
467
|
+
fill: #7dd3fc;
|
|
267
468
|
}
|
|
268
469
|
|
|
269
470
|
[data-theme="light"] .octicon-directory {
|
|
270
|
-
fill: #
|
|
471
|
+
fill: #54adf5;
|
|
271
472
|
}
|
|
272
473
|
|
|
273
474
|
.octicon-home {
|
|
@@ -296,43 +497,7 @@ main {
|
|
|
296
497
|
[data-theme="light"] .text-orange { color: #bc4c00; }
|
|
297
498
|
[data-theme="light"] .text-purple { color: #6f42c1; }
|
|
298
499
|
|
|
299
|
-
/*
|
|
300
|
-
.search-container {
|
|
301
|
-
position: relative;
|
|
302
|
-
display: flex;
|
|
303
|
-
align-items: center;
|
|
304
|
-
background: var(--bg-primary);
|
|
305
|
-
border: 1px solid var(--border-primary);
|
|
306
|
-
border-radius: 6px;
|
|
307
|
-
padding: 6px 8px;
|
|
308
|
-
transition: all 0.2s ease;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.search-container:focus-within {
|
|
312
|
-
border-color: var(--link-color);
|
|
313
|
-
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
.search-icon {
|
|
317
|
-
width: 16px;
|
|
318
|
-
height: 16px;
|
|
319
|
-
fill: var(--text-secondary);
|
|
320
|
-
margin-right: 6px;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.search-input {
|
|
324
|
-
background: none;
|
|
325
|
-
border: none;
|
|
326
|
-
outline: none;
|
|
327
|
-
color: var(--text-primary);
|
|
328
|
-
font-size: 14px;
|
|
329
|
-
width: 200px;
|
|
330
|
-
transition: all 0.2s ease;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
.search-input::placeholder {
|
|
334
|
-
color: var(--text-secondary);
|
|
335
|
-
}
|
|
500
|
+
/* Old search styles removed - using new GitHub-style search */
|
|
336
501
|
|
|
337
502
|
/* Language stats */
|
|
338
503
|
.language-stats {
|
|
@@ -451,9 +616,6 @@ main {
|
|
|
451
616
|
|
|
452
617
|
|
|
453
618
|
/* Loading states */
|
|
454
|
-
.search-input:focus {
|
|
455
|
-
width: 250px;
|
|
456
|
-
}
|
|
457
619
|
|
|
458
620
|
/* Quick actions */
|
|
459
621
|
.name {
|
|
@@ -560,6 +722,83 @@ main {
|
|
|
560
722
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
561
723
|
}
|
|
562
724
|
|
|
725
|
+
/* Image viewing styles */
|
|
726
|
+
.image-container {
|
|
727
|
+
padding: 20px;
|
|
728
|
+
text-align: center;
|
|
729
|
+
background-color: var(--bg-primary);
|
|
730
|
+
border-radius: 6px;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.image-display {
|
|
734
|
+
max-width: 100%;
|
|
735
|
+
max-height: 80vh;
|
|
736
|
+
height: auto;
|
|
737
|
+
border-radius: 6px;
|
|
738
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
|
739
|
+
background: var(--bg-secondary);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
[data-theme="light"] .image-display {
|
|
743
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.image-info {
|
|
747
|
+
margin-top: 16px;
|
|
748
|
+
padding-top: 16px;
|
|
749
|
+
border-top: 1px solid var(--border-primary);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.image-filename {
|
|
753
|
+
font-size: 14px;
|
|
754
|
+
font-weight: 600;
|
|
755
|
+
color: var(--text-primary);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* Binary file viewing styles */
|
|
759
|
+
.binary-file-container {
|
|
760
|
+
padding: 40px 20px;
|
|
761
|
+
text-align: center;
|
|
762
|
+
background-color: var(--bg-primary);
|
|
763
|
+
border-radius: 6px;
|
|
764
|
+
border: 1px solid var(--border-primary);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.binary-file-info h3 {
|
|
768
|
+
font-size: 18px;
|
|
769
|
+
font-weight: 600;
|
|
770
|
+
color: var(--text-primary);
|
|
771
|
+
margin-bottom: 12px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.binary-file-info p {
|
|
775
|
+
font-size: 14px;
|
|
776
|
+
color: var(--text-secondary);
|
|
777
|
+
margin-bottom: 20px;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.binary-file-info .btn {
|
|
781
|
+
display: inline-flex;
|
|
782
|
+
align-items: center;
|
|
783
|
+
gap: 8px;
|
|
784
|
+
padding: 8px 16px;
|
|
785
|
+
border-radius: 6px;
|
|
786
|
+
font-size: 14px;
|
|
787
|
+
font-weight: 500;
|
|
788
|
+
text-decoration: none;
|
|
789
|
+
border: 1px solid transparent;
|
|
790
|
+
cursor: pointer;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.binary-file-info .btn-primary {
|
|
794
|
+
background-color: var(--link-color);
|
|
795
|
+
color: white;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.binary-file-info .btn-primary:hover {
|
|
799
|
+
opacity: 0.9;
|
|
800
|
+
}
|
|
801
|
+
|
|
563
802
|
/* Line numbers for code viewing */
|
|
564
803
|
.with-line-numbers {
|
|
565
804
|
counter-reset: line;
|
|
@@ -1109,8 +1348,18 @@ main {
|
|
|
1109
1348
|
|
|
1110
1349
|
|
|
1111
1350
|
@media (max-width: 768px) {
|
|
1112
|
-
main {
|
|
1113
|
-
padding: 16px;
|
|
1351
|
+
.main-content {
|
|
1352
|
+
padding: 12px 16px;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
.repo-canvas-content {
|
|
1356
|
+
padding: 12px 16px;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.header-content {
|
|
1360
|
+
padding: 12px 16px;
|
|
1361
|
+
flex-direction: column;
|
|
1362
|
+
gap: 12px;
|
|
1114
1363
|
}
|
|
1115
1364
|
|
|
1116
1365
|
.file-table .size,
|
|
@@ -1118,13 +1367,15 @@ main {
|
|
|
1118
1367
|
display: none;
|
|
1119
1368
|
}
|
|
1120
1369
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1370
|
+
.repo-controls {
|
|
1371
|
+
flex-direction: column;
|
|
1372
|
+
gap: 12px;
|
|
1373
|
+
align-items: stretch;
|
|
1123
1374
|
}
|
|
1124
1375
|
|
|
1125
|
-
.
|
|
1376
|
+
.repo-controls-right {
|
|
1126
1377
|
flex-direction: column;
|
|
1127
|
-
gap:
|
|
1378
|
+
gap: 8px;
|
|
1128
1379
|
}
|
|
1129
1380
|
|
|
1130
1381
|
.header-right {
|
|
@@ -1331,22 +1582,8 @@ main {
|
|
|
1331
1582
|
transform: translateY(0);
|
|
1332
1583
|
}
|
|
1333
1584
|
}
|
|
1334
|
-
}
|
|
1335
|
-
/* Git Integration */
|
|
1336
|
-
.git-branch {
|
|
1337
|
-
display: inline-flex;
|
|
1338
|
-
align-items: center;
|
|
1339
|
-
margin-left: 12px;
|
|
1340
|
-
color: var(--text-secondary);
|
|
1341
|
-
font-size: 14px;
|
|
1342
|
-
font-weight: 500;
|
|
1343
|
-
gap: 6px;
|
|
1344
|
-
}
|
|
1345
1585
|
|
|
1346
|
-
|
|
1347
|
-
width: 16px;
|
|
1348
|
-
height: 16px;
|
|
1349
|
-
}
|
|
1586
|
+
/* Git Integration */
|
|
1350
1587
|
|
|
1351
1588
|
.git-status {
|
|
1352
1589
|
display: inline;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Simple unit tests for file type detection
|
|
2
|
+
// Run with: node tests/fileTypeDetection.test.js
|
|
3
|
+
|
|
4
|
+
// Import the functions from our file-utils module
|
|
5
|
+
const { isImageFile, isBinaryFile, isTextFile } = require('../lib/file-utils');
|
|
6
|
+
|
|
7
|
+
function test(description, testFn) {
|
|
8
|
+
try {
|
|
9
|
+
testFn();
|
|
10
|
+
console.log(`✅ ${description}`);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.log(`❌ ${description}`);
|
|
13
|
+
console.log(` Error: ${error.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assert(condition, message) {
|
|
18
|
+
if (!condition) {
|
|
19
|
+
throw new Error(message || 'Assertion failed');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Test isImageFile function
|
|
24
|
+
test('should detect PNG files as images', () => {
|
|
25
|
+
assert(isImageFile('photo.png'), 'PNG should be detected as image');
|
|
26
|
+
assert(isImageFile('PNG'), 'Extension-only PNG should work');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should detect common image formats', () => {
|
|
30
|
+
const imageFiles = ['test.jpg', 'test.jpeg', 'test.gif', 'test.svg', 'test.webp', 'test.bmp', 'test.tiff', 'test.ico'];
|
|
31
|
+
imageFiles.forEach(file => {
|
|
32
|
+
assert(isImageFile(file), `${file} should be detected as image`);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should not detect text files as images', () => {
|
|
37
|
+
const textFiles = ['readme.md', 'script.js', 'style.css', 'config.json'];
|
|
38
|
+
textFiles.forEach(file => {
|
|
39
|
+
assert(!isImageFile(file), `${file} should not be detected as image`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Test isBinaryFile function
|
|
44
|
+
test('should detect images as binary files', () => {
|
|
45
|
+
assert(isBinaryFile('photo.png'), 'Images should be binary');
|
|
46
|
+
assert(isBinaryFile('animation.gif'), 'GIFs should be binary');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should detect archives as binary files', () => {
|
|
50
|
+
const archives = ['file.zip', 'backup.tar', 'compressed.gz', 'archive.rar', 'package.7z'];
|
|
51
|
+
archives.forEach(file => {
|
|
52
|
+
assert(isBinaryFile(file), `${file} should be detected as binary`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should detect executables as binary files', () => {
|
|
57
|
+
const executables = ['program.exe', 'binary.bin', 'application.app'];
|
|
58
|
+
executables.forEach(file => {
|
|
59
|
+
assert(isBinaryFile(file), `${file} should be detected as binary`);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should detect documents as binary files', () => {
|
|
64
|
+
const docs = ['document.pdf', 'spreadsheet.xlsx', 'presentation.pptx'];
|
|
65
|
+
docs.forEach(file => {
|
|
66
|
+
assert(isBinaryFile(file), `${file} should be detected as binary`);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should not detect text files as binary', () => {
|
|
71
|
+
const textFiles = ['readme.md', 'script.js', 'style.css', 'data.json', 'config.yml', 'index.html'];
|
|
72
|
+
textFiles.forEach(file => {
|
|
73
|
+
assert(!isBinaryFile(file), `${file} should not be detected as binary`);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Test isTextFile function
|
|
78
|
+
test('should detect common text files', () => {
|
|
79
|
+
const textFiles = ['readme.md', 'script.js', 'style.css', 'data.json', 'config.yml', 'index.html', 'app.py', 'main.go'];
|
|
80
|
+
textFiles.forEach(file => {
|
|
81
|
+
assert(isTextFile(file), `${file} should be detected as text`);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should not detect binary files as text', () => {
|
|
86
|
+
const binaryFiles = ['photo.png', 'archive.zip', 'program.exe', 'document.pdf'];
|
|
87
|
+
binaryFiles.forEach(file => {
|
|
88
|
+
assert(!isTextFile(file), `${file} should not be detected as text`);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Test edge cases
|
|
93
|
+
test('should handle files without extensions', () => {
|
|
94
|
+
assert(isTextFile('README'), 'Files without extensions should default to text');
|
|
95
|
+
assert(isTextFile('Makefile'), 'Common text files without extensions should be text');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should handle extension-only input', () => {
|
|
99
|
+
assert(isImageFile('png'), 'Should handle bare extensions');
|
|
100
|
+
assert(isBinaryFile('exe'), 'Should handle bare extensions for binary');
|
|
101
|
+
assert(isTextFile('js'), 'Should handle bare extensions for text');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should be case insensitive', () => {
|
|
105
|
+
assert(isImageFile('PHOTO.PNG'), 'Should handle uppercase extensions');
|
|
106
|
+
assert(isBinaryFile('ARCHIVE.ZIP'), 'Should handle uppercase extensions');
|
|
107
|
+
assert(isTextFile('SCRIPT.JS'), 'Should handle uppercase extensions');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Run all tests
|
|
111
|
+
console.log('Running file type detection tests...\n');
|