gh-here 1.1.1 → 2.1.0

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/SAMPLE.md ADDED
@@ -0,0 +1,287 @@
1
+ # Syntax Highlighting Test
2
+
3
+ This file contains code samples in various languages to test syntax highlighting in markdown rendering.
4
+
5
+ ## JavaScript
6
+
7
+ ```javascript
8
+ function greet(name) {
9
+ const message = `Hello, ${name}!`;
10
+ console.log(message);
11
+ return message;
12
+ }
13
+
14
+ class Person {
15
+ constructor(name, age) {
16
+ this.name = name;
17
+ this.age = age;
18
+ }
19
+
20
+ introduce() {
21
+ return `My name is ${this.name} and I'm ${this.age} years old.`;
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Python
27
+
28
+ ```python
29
+ def calculate_fibonacci(n):
30
+ """Calculate the nth Fibonacci number."""
31
+ if n <= 1:
32
+ return n
33
+ return calculate_fibonacci(n - 1) + calculate_fibonacci(n - 2)
34
+
35
+ class Animal:
36
+ def __init__(self, name, species):
37
+ self.name = name
38
+ self.species = species
39
+
40
+ def make_sound(self):
41
+ return f"{self.name} the {self.species} makes a sound"
42
+ ```
43
+
44
+ ## Go
45
+
46
+ ```go
47
+ package main
48
+
49
+ import (
50
+ "fmt"
51
+ "time"
52
+ )
53
+
54
+ type User struct {
55
+ Name string
56
+ Email string
57
+ CreatedAt time.Time
58
+ }
59
+
60
+ func (u *User) SendEmail(message string) error {
61
+ fmt.Printf("Sending email to %s: %s\n", u.Email, message)
62
+ return nil
63
+ }
64
+
65
+ func main() {
66
+ user := &User{
67
+ Name: "John Doe",
68
+ Email: "john@example.com",
69
+ CreatedAt: time.Now(),
70
+ }
71
+ user.SendEmail("Welcome!")
72
+ }
73
+ ```
74
+
75
+ ## Rust
76
+
77
+ ```rust
78
+ use std::collections::HashMap;
79
+
80
+ fn main() {
81
+ let mut scores = HashMap::new();
82
+ scores.insert(String::from("Blue"), 10);
83
+ scores.insert(String::from("Red"), 50);
84
+
85
+ for (key, value) in &scores {
86
+ println!("{}: {}", key, value);
87
+ }
88
+ }
89
+
90
+ struct Rectangle {
91
+ width: u32,
92
+ height: u32,
93
+ }
94
+
95
+ impl Rectangle {
96
+ fn area(&self) -> u32 {
97
+ self.width * self.height
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## TypeScript
103
+
104
+ ```typescript
105
+ interface User {
106
+ id: number;
107
+ name: string;
108
+ email: string;
109
+ }
110
+
111
+ async function fetchUser(id: number): Promise<User> {
112
+ const response = await fetch(`/api/users/${id}`);
113
+ const data: User = await response.json();
114
+ return data;
115
+ }
116
+
117
+ class UserService {
118
+ private users: Map<number, User> = new Map();
119
+
120
+ addUser(user: User): void {
121
+ this.users.set(user.id, user);
122
+ }
123
+
124
+ getUser(id: number): User | undefined {
125
+ return this.users.get(id);
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Bash
131
+
132
+ ```bash
133
+ #!/bin/bash
134
+
135
+ # Function to backup files
136
+ backup_files() {
137
+ local source_dir=$1
138
+ local backup_dir=$2
139
+
140
+ if [ ! -d "$backup_dir" ]; then
141
+ mkdir -p "$backup_dir"
142
+ fi
143
+
144
+ tar -czf "$backup_dir/backup-$(date +%Y%m%d).tar.gz" "$source_dir"
145
+ echo "Backup completed successfully!"
146
+ }
147
+
148
+ backup_files "/home/user/documents" "/home/user/backups"
149
+ ```
150
+
151
+ ## SQL
152
+
153
+ ```sql
154
+ -- Create users table
155
+ CREATE TABLE users (
156
+ id SERIAL PRIMARY KEY,
157
+ username VARCHAR(50) UNIQUE NOT NULL,
158
+ email VARCHAR(100) UNIQUE NOT NULL,
159
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
160
+ );
161
+
162
+ -- Insert some data
163
+ INSERT INTO users (username, email) VALUES
164
+ ('john_doe', 'john@example.com'),
165
+ ('jane_smith', 'jane@example.com');
166
+
167
+ -- Query with JOIN
168
+ SELECT u.username, o.order_date, o.total
169
+ FROM users u
170
+ INNER JOIN orders o ON u.id = o.user_id
171
+ WHERE o.total > 100
172
+ ORDER BY o.order_date DESC;
173
+ ```
174
+
175
+ ## JSON
176
+
177
+ ```json
178
+ {
179
+ "name": "gh-here",
180
+ "version": "2.0.0",
181
+ "description": "A local GitHub-like file browser",
182
+ "dependencies": {
183
+ "express": "^4.18.2",
184
+ "highlight.js": "^11.9.0",
185
+ "marked": "^12.0.0"
186
+ },
187
+ "scripts": {
188
+ "start": "node bin/gh-here.js",
189
+ "test": "jest"
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## CSS
195
+
196
+ ```css
197
+ :root {
198
+ --primary-color: #0366d6;
199
+ --background-color: #ffffff;
200
+ --text-color: #24292e;
201
+ }
202
+
203
+ .container {
204
+ max-width: 1200px;
205
+ margin: 0 auto;
206
+ padding: 20px;
207
+ }
208
+
209
+ .button {
210
+ display: inline-block;
211
+ padding: 10px 20px;
212
+ background-color: var(--primary-color);
213
+ color: white;
214
+ border-radius: 5px;
215
+ transition: background-color 0.3s ease;
216
+ }
217
+
218
+ .button:hover {
219
+ background-color: #0256c7;
220
+ cursor: pointer;
221
+ }
222
+ ```
223
+
224
+ ## HTML
225
+
226
+ ```html
227
+ <!DOCTYPE html>
228
+ <html lang="en">
229
+ <head>
230
+ <meta charset="UTF-8">
231
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
232
+ <title>Sample Page</title>
233
+ <link rel="stylesheet" href="styles.css">
234
+ </head>
235
+ <body>
236
+ <header>
237
+ <h1>Welcome to My Site</h1>
238
+ <nav>
239
+ <ul>
240
+ <li><a href="#home">Home</a></li>
241
+ <li><a href="#about">About</a></li>
242
+ <li><a href="#contact">Contact</a></li>
243
+ </ul>
244
+ </nav>
245
+ </header>
246
+ <main>
247
+ <p>This is the main content area.</p>
248
+ </main>
249
+ <script src="script.js"></script>
250
+ </body>
251
+ </html>
252
+ ```
253
+
254
+ ## Java
255
+
256
+ ```java
257
+ import java.util.ArrayList;
258
+ import java.util.List;
259
+
260
+ public class BankAccount {
261
+ private String accountNumber;
262
+ private double balance;
263
+ private List<Transaction> transactions;
264
+
265
+ public BankAccount(String accountNumber, double initialBalance) {
266
+ this.accountNumber = accountNumber;
267
+ this.balance = initialBalance;
268
+ this.transactions = new ArrayList<>();
269
+ }
270
+
271
+ public void deposit(double amount) {
272
+ if (amount > 0) {
273
+ balance += amount;
274
+ transactions.add(new Transaction("DEPOSIT", amount));
275
+ }
276
+ }
277
+
278
+ public boolean withdraw(double amount) {
279
+ if (amount > 0 && balance >= amount) {
280
+ balance -= amount;
281
+ transactions.add(new Transaction("WITHDRAWAL", amount));
282
+ return true;
283
+ }
284
+ return false;
285
+ }
286
+ }
287
+ ```
package/bin/gh-here.js CHANGED
@@ -9,7 +9,8 @@ const { setupRoutes } = require('../lib/server');
9
9
 
10
10
  // Parse command line arguments
11
11
  const args = process.argv.slice(2);
12
- const openBrowser = args.includes('--open') || args.includes('-o');
12
+ const noOpen = args.includes('--no-open');
13
+ const openBrowser = !noOpen; // Default is to open browser
13
14
  const helpRequested = args.includes('--help') || args.includes('-h');
14
15
 
15
16
  // Check for port specification
@@ -37,17 +38,17 @@ gh-here - GitHub-like local file browser
37
38
  Usage: npx gh-here [options]
38
39
 
39
40
  Options:
40
- --open, -o Open browser automatically
41
+ --no-open Do not open browser automatically
41
42
  --browser=<name> Specify browser (safari, chrome, firefox, arc)
42
43
  --port=<number> Specify port number (default: 5555)
43
44
  --help, -h Show this help message
44
45
 
45
46
  Examples:
46
- npx gh-here Start server on port 5555 (or next available)
47
- npx gh-here --open Start server and open browser
48
- npx gh-here --port=8080 Start server on port 8080
49
- npx gh-here --open --browser=safari Start server and open in Safari
50
- npx gh-here --open --browser=arc Start server and open in Arc
47
+ npx gh-here Start server and open browser
48
+ npx gh-here --no-open Start server without opening browser
49
+ npx gh-here --port=8080 Start server on port 8080 and open browser
50
+ npx gh-here --browser=safari Start server and open in Safari
51
+ npx gh-here --browser=arc Start server and open in Arc
51
52
  `);
52
53
  process.exit(0);
53
54
  }
@@ -181,8 +182,6 @@ async function startServer() {
181
182
  if (openBrowser) {
182
183
  console.log(`🌍 Opening browser...`);
183
184
  setTimeout(() => openBrowserToUrl(url), 1000);
184
- } else {
185
- console.log(`💡 Tip: Use --open flag to launch browser automatically`);
186
185
  }
187
186
  });
188
187
  } catch (error) {
package/blog-post.md ADDED
@@ -0,0 +1,100 @@
1
+ # Why gh-here is the Perfect Tool for Terminal-Heavy Developers
2
+
3
+ If you're like most developers in 2025, you probably spend a lot of time in the terminal. You're using Claude Code to generate and refactor code, running commands left and right, and iterating quickly on projects that haven't even been pushed to GitHub yet. But sometimes you need to step back and get a bird's-eye view of your codebase—and that's where `gh-here` shines.
4
+
5
+ ## The Gap Between Terminal and IDE
6
+
7
+ Picture this scenario: You're deep in a terminal session, using Claude Code to implement a new feature. You've got files scattered across multiple directories, you're testing things with curl, and you're in that flow state where opening a heavy IDE would just break your momentum. But you need to:
8
+
9
+ - Quickly browse through your project structure
10
+ - Check what files you've modified
11
+ - Preview a README or markdown file
12
+ - Navigate between related files without losing context
13
+ - See language distribution across your codebase
14
+
15
+ This is the exact gap that `gh-here` fills.
16
+
17
+ ## Built for Modern Development Workflows
18
+
19
+ ### 1. **Perfect for AI-Assisted Development**
20
+ When you're working with Claude Code or GitHub Copilot, you're often generating and modifying files rapidly. `gh-here` gives you an instant, GitHub-like interface to review what's been changed without committing to version control or opening a full IDE.
21
+
22
+ ### 2. **Ideal for Pre-Push Workflows**
23
+ Not everything needs to hit GitHub immediately. Whether you're experimenting with a proof of concept or working on a feature branch, `gh-here` lets you navigate and review your local changes in a familiar, web-based interface.
24
+
25
+ ### 3. **Terminal-Native but GUI-Friendly**
26
+ Launch it with a single command (`gh-here`) from any directory, and instantly get a clean, GitHub-inspired interface in your browser. No configuration, no setup—just point and browse.
27
+
28
+ ## Real-World Use Cases
29
+
30
+ **Scenario 1: Code Review Before Push**
31
+ ```bash
32
+ $ claude "implement user authentication system"
33
+ # ... lots of generated files and changes
34
+ $ gh-here
35
+ # Browse through generated files, check git status, review changes
36
+ $ git add . && git commit -m "Add user authentication"
37
+ ```
38
+
39
+ **Scenario 2: Project Exploration**
40
+ ```bash
41
+ $ git clone some-interesting-repo
42
+ $ cd some-interesting-repo
43
+ $ gh-here
44
+ # Instantly get an overview: languages used, file structure, README preview
45
+ ```
46
+
47
+ **Scenario 3: Documentation Review**
48
+ ```bash
49
+ $ claude "update all the documentation"
50
+ $ gh-here
51
+ # Preview all the markdown files in GitHub-style rendering
52
+ # Check if everything looks good before committing
53
+ ```
54
+
55
+ ## Why Not Just Use GitHub or an IDE?
56
+
57
+ **GitHub**: Your code isn't pushed yet, or you're working on experimental changes you don't want in version control.
58
+
59
+ **IDE**: Too heavy for quick browsing, breaks your terminal flow, takes time to open and index.
60
+
61
+ **File Explorer**: No syntax highlighting, no git integration, no markdown rendering, no language stats.
62
+
63
+ **Terminal**: Great for editing, terrible for browsing and getting visual context.
64
+
65
+ ## The Sweet Spot
66
+
67
+ `gh-here` hits the perfect sweet spot:
68
+ - **Lightweight**: Starts instantly, no heavy indexing
69
+ - **Familiar**: GitHub-style interface that every developer knows
70
+ - **Integrated**: Shows git status, handles various file types, renders markdown
71
+ - **Flexible**: Works with any directory, whether it's a git repo or not
72
+ - **Terminal-friendly**: Launch with one command, works alongside your existing workflow
73
+
74
+ ## Features That Just Make Sense
75
+
76
+ - **Language statistics**: Instantly see what technologies your project uses
77
+ - **Git integration**: Visual git status indicators, diff viewing
78
+ - **File type support**: Syntax highlighting, image viewing, markdown rendering
79
+ - **Search functionality**: Quick file search with keyboard shortcuts
80
+ - **Breadcrumb navigation**: Always know where you are in your project
81
+
82
+ ## Perfect for 2025 Development
83
+
84
+ As development workflows become more AI-assisted and terminal-centric, tools like `gh-here` become essential. You're not always in an IDE, you're not always ready to push to GitHub, but you always need to understand your codebase structure and changes.
85
+
86
+ It's the missing link between your terminal workflow and the visual context you need to stay productive.
87
+
88
+ ## Get Started
89
+
90
+ ```bash
91
+ npm install -g gh-here
92
+ cd your-project
93
+ gh-here
94
+ ```
95
+
96
+ That's it. Your local codebase is now browsable in a clean, GitHub-style interface at `localhost:5556`.
97
+
98
+ ---
99
+
100
+ *Built for developers who live in the terminal but occasionally need a better view of their code.*
package/lib/renderers.js CHANGED
@@ -8,6 +8,23 @@ const { exec } = require('child_process');
8
8
  const { getFileIcon, getLanguageFromExtension, getLanguageColor, formatBytes, isImageFile, isBinaryFile, isTextFile } = require('./file-utils');
9
9
  const { getGitStatusIcon, getGitStatusDescription } = require('./git');
10
10
 
11
+ // Configure marked to use highlight.js for syntax highlighting
12
+ marked.use({
13
+ renderer: {
14
+ code(code, language) {
15
+ if (language && hljs.getLanguage(language)) {
16
+ try {
17
+ return `<pre><code class="hljs language-${language}">${hljs.highlight(code, { language }).value}</code></pre>`;
18
+ } catch (err) {
19
+ // Fall back to auto-detection if language-specific highlighting fails
20
+ }
21
+ }
22
+ // Auto-detect language if not specified or language not found
23
+ return `<pre><code class="hljs">${hljs.highlightAuto(code).value}</code></pre>`;
24
+ }
25
+ }
26
+ });
27
+
11
28
  /**
12
29
  * HTML rendering module
13
30
  * Handles all HTML template generation for different views
@@ -18,7 +35,8 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
18
35
  const breadcrumbs = generateBreadcrumbs(currentPath, gitBranch, workingDirName);
19
36
  const readmeFile = findReadmeFile(items);
20
37
  const readmePreview = readmeFile ? generateReadmePreview(currentPath, readmeFile) : '';
21
- const languageStats = generateLanguageStats(items);
38
+ // Only show language stats on root/top-level directory
39
+ const languageStats = (!currentPath || currentPath === '.') ? generateLanguageStats(items) : '';
22
40
 
23
41
  const itemsHtml = items.map(item => `
24
42
  <tr class="file-row" data-name="${item.name.toLowerCase()}" data-type="${item.isDirectory ? 'dir' : 'file'}" data-path="${item.path}">
@@ -113,6 +131,7 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
113
131
  ${octicons['chevron-down'].toSVG({ class: 'octicon-chevron' })}
114
132
  </button>
115
133
  ` : ''}
134
+ ${languageStats}
116
135
  </div>
117
136
  <div class="repo-controls-right">
118
137
  <div class="search-container">
@@ -130,7 +149,6 @@ function renderDirectory(currentPath, items, showGitignored = false, gitBranch =
130
149
  <main>
131
150
  <div class="repo-canvas">
132
151
  <div class="repo-canvas-content">
133
- ${languageStats}
134
152
  <div class="file-table-container">
135
153
  <table class="file-table" id="file-table">
136
154
  <thead>
@@ -242,8 +260,9 @@ function generateLanguageStats(items) {
242
260
  `;
243
261
  }
244
262
 
245
- async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot) {
246
- const breadcrumbs = generateBreadcrumbs(filePath);
263
+ async function renderFileDiff(filePath, ext, gitInfo, gitRepoRoot, workingDir = null) {
264
+ const workingDirName = workingDir ? path.basename(workingDir) : null;
265
+ const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
247
266
 
248
267
  // Get git diff for the file
249
268
  return new Promise((resolve, reject) => {
@@ -370,8 +389,9 @@ function renderRawDiff(diffOutput, ext) {
370
389
  }
371
390
 
372
391
 
373
- async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir) {
374
- const breadcrumbs = generateBreadcrumbs(filePath);
392
+ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStatus = null, workingDir = null) {
393
+ const workingDirName = workingDir ? path.basename(workingDir) : null;
394
+ const breadcrumbs = generateBreadcrumbs(filePath, null, workingDirName);
375
395
  let displayContent;
376
396
  let viewToggle = '';
377
397
 
@@ -551,8 +571,9 @@ async function renderFile(filePath, content, ext, viewMode = 'rendered', gitStat
551
571
  `;
552
572
  }
553
573
 
554
- function renderNewFile(currentPath) {
555
- const breadcrumbs = generateBreadcrumbs(currentPath);
574
+ function renderNewFile(currentPath, workingDir = null) {
575
+ const workingDirName = workingDir ? path.basename(workingDir) : null;
576
+ const breadcrumbs = generateBreadcrumbs(currentPath, null, workingDirName);
556
577
 
557
578
  return `
558
579
  <!DOCTYPE html>
package/lib/server.js CHANGED
@@ -108,7 +108,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
108
108
  const absolutePath = path.resolve(fullPath);
109
109
  const gitInfo = gitStatus[absolutePath];
110
110
  if (gitInfo) {
111
- const diffHtml = await renderFileDiff(currentPath, ext, gitInfo, gitRepoRoot);
111
+ const diffHtml = await renderFileDiff(currentPath, ext, gitInfo, gitRepoRoot, workingDir);
112
112
  return res.send(diffHtml);
113
113
  }
114
114
  }
@@ -123,7 +123,7 @@ function setupRoutes(app, workingDir, isGitRepo, gitRepoRoot) {
123
123
  // Route for creating new files
124
124
  app.get('/new', (req, res) => {
125
125
  const currentPath = req.query.path || '';
126
- res.send(renderNewFile(currentPath));
126
+ res.send(renderNewFile(currentPath, workingDir));
127
127
  });
128
128
 
129
129
  // API endpoint to get file content for editing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-here",
3
- "version": "1.1.1",
3
+ "version": "2.1.0",
4
4
  "description": "A local GitHub-like file browser for viewing code",
5
5
  "repository": "https://github.com/corywilkerson/gh-here",
6
6
  "main": "index.js",
package/public/styles.css CHANGED
@@ -499,29 +499,54 @@ main {
499
499
 
500
500
  /* Old search styles removed - using new GitHub-style search */
501
501
 
502
- /* Language stats */
502
+ /* Language stats in control bar */
503
503
  .language-stats {
504
504
  display: flex;
505
- gap: 16px;
506
- margin-bottom: 16px;
507
- padding: 12px 0;
508
- border-bottom: 1px solid var(--border-primary);
509
- flex-wrap: wrap;
505
+ align-items: center;
506
+ gap: 8px;
507
+ margin-left: 16px;
508
+ max-width: 300px;
509
+ overflow: hidden;
510
+ position: relative;
511
+ }
512
+
513
+ .language-stats:hover {
514
+ overflow: visible;
515
+ z-index: 10;
516
+ }
517
+
518
+ .language-stats:hover::before {
519
+ content: '';
520
+ position: absolute;
521
+ top: -4px;
522
+ left: -8px;
523
+ right: -8px;
524
+ bottom: -4px;
525
+ background: var(--bg-secondary);
526
+ border: 1px solid var(--border-primary);
527
+ border-radius: 6px;
528
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
529
+ z-index: -1;
530
+ pointer-events: none;
510
531
  }
511
532
 
512
533
  .lang-stat {
513
534
  display: flex;
514
535
  align-items: center;
515
- gap: 6px;
516
- font-size: 12px;
536
+ gap: 4px;
537
+ font-size: 11px;
517
538
  color: var(--text-secondary);
539
+ white-space: nowrap;
540
+ flex-shrink: 0;
541
+ padding: 2px 0;
518
542
  }
519
543
 
520
544
  .lang-dot {
521
- width: 12px;
522
- height: 12px;
545
+ width: 8px;
546
+ height: 8px;
523
547
  border-radius: 50%;
524
548
  display: inline-block;
549
+ flex-shrink: 0;
525
550
  }
526
551
 
527
552
  .lang-name {
@@ -2010,6 +2035,31 @@ main {
2010
2035
  height: 14px;
2011
2036
  }
2012
2037
 
2038
+ /* General modal overlay */
2039
+ .modal-overlay {
2040
+ position: fixed;
2041
+ top: 0;
2042
+ left: 0;
2043
+ right: 0;
2044
+ bottom: 0;
2045
+ background: rgba(0, 0, 0, 0.7);
2046
+ display: flex;
2047
+ align-items: center;
2048
+ justify-content: center;
2049
+ z-index: 1000;
2050
+ }
2051
+
2052
+ .modal-content {
2053
+ background: var(--bg-primary);
2054
+ border: 1px solid var(--border-primary);
2055
+ border-radius: 8px;
2056
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
2057
+ padding: 20px;
2058
+ max-width: 90vw;
2059
+ max-height: 90vh;
2060
+ overflow-y: auto;
2061
+ }
2062
+
2013
2063
  /* Clean commit modal */
2014
2064
  .commit-modal-overlay {
2015
2065
  position: fixed;