Skip to content

Commit 26fda23

Browse files
feat: Add more commands and full site search
1 parent a33b731 commit 26fda23

6 files changed

Lines changed: 185 additions & 32 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ For an example of each (using the text "Shellscape"), see [example fonts](./exam
230230

231231
## Todos
232232

233+
* All text to be copied (can't currently focus)
234+
233235
* Display directories in tree format:
234236
```
235237
blog/

docs/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ nav:
1717

1818
terminal:
1919
prompt: ""
20+
whoami: "Shellscape - by Rob Reid"
2021
banner:
2122
text: "s h e l l s c a p e"
2223
font: univers

embed/scaffold/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ nav:
1717

1818
terminal:
1919
prompt: ""
20+
# whoami: "Your Name — a short bio or description"
2021
banner:
2122
text: "my site"
2223
# font: "slant" # default; try: big, standard, small, banner3, 3-d, colossal, larry3d

embed/static/terminal.js

Lines changed: 133 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
var history = JSON.parse(localStorage.getItem('ss_history') || '[]');
1313
var historyIndex = -1;
1414
var initialContent = output.innerHTML;
15+
var findIndex = null;
16+
var suppressScroll = false;
1517

1618
function scrollToBottom() {
1719
output.scrollTop = output.scrollHeight;
@@ -157,6 +159,8 @@
157159
return '/' + parts.join('/') || '/';
158160
}
159161

162+
if (target === '.') return currentPath;
163+
160164
if (target === '~' || target === '/') return '/';
161165

162166
if (target.charAt(0) === '/') return target;
@@ -217,14 +221,17 @@
217221
var lines = [
218222
'<span style="color:var(--ss-accent)">Available commands:</span>',
219223
'',
220-
' <span style="color:var(--ss-green)">ls</span> [dir] List pages at current or given location',
221-
' <span style="color:var(--ss-green)">cd</span> &lt;page&gt; Navigate to a page',
222-
' <span style="color:var(--ss-green)">cd ..</span> Go up one level',
223-
' <span style="color:var(--ss-green)">cat</span> &lt;page&gt; Display page content',
224-
' <span style="color:var(--ss-green)">open</span> &lt;page&gt; Navigate to and display a page',
225-
' <span style="color:var(--ss-green)">clear</span> Clear the terminal',
226-
' <span style="color:var(--ss-green)">history</span> Show command history',
227-
' <span style="color:var(--ss-green)">theme</span> &lt;name&gt; Switch theme (terminal, light)',
224+
' <span style="color:var(--ss-green)">ls</span> [dir] List pages at current or given location',
225+
' <span style="color:var(--ss-green)">cd</span> &lt;page&gt; Navigate to a page',
226+
' <span style="color:var(--ss-green)">open</span> &lt;page&gt; Navigate to and display a page',
227+
' <span style="color:var(--ss-green)">find</span> &lt;query&gt; Search pages by title, path, or content',
228+
' <span style="color:var(--ss-green)">tree</span> [dir] Show site hierarchy',
229+
' <span style="color:var(--ss-green)">pwd</span> Print current path',
230+
' <span style="color:var(--ss-green)">whoami</span> Display site author info',
231+
' <span style="color:var(--ss-green)">date</span> Show current date and time',
232+
' <span style="color:var(--ss-green)">clear</span> Clear the terminal',
233+
' <span style="color:var(--ss-green)">history</span> Show command history',
234+
' <span style="color:var(--ss-green)">theme</span> &lt;name&gt; Switch theme (system, light, dark)',
228235
'',
229236
'You can also click any link to navigate.',
230237
];
@@ -299,27 +306,6 @@
299306
}
300307
},
301308

302-
cat: function (args) {
303-
if (!args || args.length === 0) {
304-
appendOutput('cat: missing file operand', 'error');
305-
return;
306-
}
307-
308-
var target = args[0];
309-
var resolved = resolvePath(target);
310-
var page = findPage(resolved);
311-
312-
if (!page) {
313-
appendOutput('cat: ' + escapeHtml(target) + ': No such file', 'error');
314-
return;
315-
}
316-
317-
var content = document.createElement('div');
318-
content.className = 'content';
319-
content.innerHTML = buildPageHTML(page);
320-
appendRaw(content);
321-
},
322-
323309
open: function (args) {
324310
if (!args || args.length === 0) {
325311
appendOutput('open: missing file operand', 'error');
@@ -382,11 +368,121 @@
382368
appendOutput('theme: unknown theme: ' + escapeHtml(name), 'error');
383369
appendOutput('Available: system, light, dark', 'system');
384370
}
371+
},
372+
373+
pwd: function () {
374+
appendOutput('~' + currentPath);
375+
},
376+
377+
date: function () {
378+
appendOutput(new Date().toString());
379+
},
380+
381+
whoami: function () {
382+
var info = SITE.terminal.whoami;
383+
if (info) {
384+
appendOutput(info);
385+
} else {
386+
appendOutput('whoami: not configured', 'system');
387+
}
388+
},
389+
390+
find: function (args) {
391+
var query = (args && args.length > 0) ? args.join(' ').toLowerCase() : '';
392+
if (!query) {
393+
appendOutput('Usage: find &lt;query&gt;', 'system');
394+
return;
395+
}
396+
397+
function doSearch(index) {
398+
var titleHits = [];
399+
var contentHits = [];
400+
for (var url in index) {
401+
if (!index.hasOwnProperty(url)) continue;
402+
var entry = index[url];
403+
var title = (entry.title || '').toLowerCase();
404+
var content = entry.content || '';
405+
if (title.indexOf(query) !== -1 || url.toLowerCase().indexOf(query) !== -1) {
406+
titleHits.push(entry);
407+
} else if (content.indexOf(query) !== -1) {
408+
var idx = content.indexOf(query);
409+
var start = Math.max(0, idx - 40);
410+
var end = Math.min(content.length, idx + query.length + 40);
411+
var snippet = (start > 0 ? '...' : '') +
412+
content.substring(start, end) +
413+
(end < content.length ? '...' : '');
414+
contentHits.push({ url: entry.url, title: entry.title, snippet: snippet });
415+
}
416+
}
417+
418+
if (titleHits.length === 0 && contentHits.length === 0) {
419+
appendOutput('No results for \'' + escapeHtml(args.join(' ')) + '\'', 'system');
420+
return;
421+
}
422+
423+
for (var i = 0; i < titleHits.length; i++) {
424+
var r = titleHits[i];
425+
var link = '<a href="' + r.url + '" data-nav="' + r.url + '" style="color:var(--ss-green);cursor:pointer">' + escapeHtml(r.url) + '</a>';
426+
appendOutput(' ' + link + ' ' + escapeHtml(r.title));
427+
}
428+
for (var j = 0; j < contentHits.length; j++) {
429+
var c = contentHits[j];
430+
var cLink = '<a href="' + c.url + '" data-nav="' + c.url + '" style="color:var(--ss-green);cursor:pointer">' + escapeHtml(c.url) + '</a>';
431+
appendOutput(' ' + cLink + ' ' + escapeHtml(c.title));
432+
appendOutput(' <span style="color:var(--ss-dim)">' + escapeHtml(c.snippet) + '</span>');
433+
}
434+
}
435+
436+
if (findIndex) {
437+
doSearch(findIndex);
438+
} else {
439+
fetch('/find.json')
440+
.then(function (r) { return r.json(); })
441+
.then(function (data) {
442+
findIndex = data;
443+
doSearch(data);
444+
})
445+
.catch(function () {
446+
appendOutput('find: failed to load search index', 'error');
447+
});
448+
}
449+
},
450+
451+
tree: function (args) {
452+
var root = currentPath;
453+
if (args && args.length > 0) {
454+
root = resolvePath(args[0]);
455+
var entries = getNavEntries(root);
456+
if (entries.length === 0 && !findPage(root)) {
457+
appendOutput('tree: \'' + escapeHtml(args[0]) + '\': No such directory', 'error');
458+
return;
459+
}
460+
}
461+
462+
function buildTree(path, prefix) {
463+
var entries = getNavEntries(path);
464+
for (var i = 0; i < entries.length; i++) {
465+
var entry = entries[i];
466+
var isLast = i === entries.length - 1;
467+
var connector = isLast ? '└── ' : '├── ';
468+
var color = entry.type === 'dir' ? 'var(--ss-blue)' : 'var(--ss-green)';
469+
var display = entry.type === 'dir' ? entry.name + '/' : entry.name;
470+
appendOutput(prefix + connector + '<span style="color:' + color + '">' + escapeHtml(display) + '</span>');
471+
if (entry.type === 'dir') {
472+
var childPrefix = prefix + (isLast ? ' ' : '│ ');
473+
buildTree(entry.url, childPrefix);
474+
}
475+
}
476+
}
477+
478+
appendOutput('<span style="color:var(--ss-blue)">.</span>');
479+
buildTree(root, '');
385480
}
386481
};
387482

388483
function showPageContent(pagePath) {
389-
var page = findPage(pagePath || currentPath);
484+
var resolvedPath = pagePath || currentPath;
485+
var page = findPage(resolvedPath);
390486
if (page) {
391487
var content = document.createElement('div');
392488
content.className = 'content';
@@ -397,8 +493,9 @@
397493
output.innerHTML = '';
398494
commands.ls();
399495
}
400-
showWelcome();
496+
if (resolvedPath === '/') showWelcome();
401497
output.scrollTop = 0;
498+
suppressScroll = true;
402499
}
403500

404501
function executeCommand(cmdStr) {
@@ -421,7 +518,11 @@
421518
appendOutput('command not found: ' + escapeHtml(cmd) + '. Type <span style="color:var(--ss-green)">help</span> for available commands.', 'error');
422519
}
423520

424-
scrollToBottom();
521+
if (suppressScroll) {
522+
suppressScroll = false;
523+
} else {
524+
scrollToBottom();
525+
}
425526
}
426527

427528
// ── Tab completion ──

internal/builder/builder.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ func (b *Builder) Build() (*BuildResult, error) {
105105
return nil, fmt.Errorf("building site data: %w", err)
106106
}
107107

108+
if err := b.writeFindIndex(pages, posts); err != nil {
109+
return nil, fmt.Errorf("writing find index: %w", err)
110+
}
111+
108112
for _, page := range pages {
109113
if err := b.renderPage(tmpls, page, posts, tags, siteDataJSON); err != nil {
110114
return nil, fmt.Errorf("rendering %s: %w", page.URL, err)
@@ -256,6 +260,48 @@ func (b *Builder) writeTheme() error {
256260
return nil
257261
}
258262

263+
func (b *Builder) writeFindIndex(pages, posts []*content.Page) error {
264+
type findEntry struct {
265+
Title string `json:"title"`
266+
URL string `json:"url"`
267+
Content string `json:"content"`
268+
}
269+
270+
index := make(map[string]findEntry)
271+
for _, p := range append(pages, posts...) {
272+
if p.URL == "/" {
273+
continue
274+
}
275+
276+
var buf strings.Builder
277+
for _, line := range strings.Split(p.RawContent, "\n") {
278+
line = strings.TrimSpace(line)
279+
if line == "" {
280+
continue
281+
}
282+
line = strings.TrimLeft(line, "#")
283+
line = strings.TrimSpace(line)
284+
if line != "" {
285+
buf.WriteString(line)
286+
buf.WriteString(" ")
287+
}
288+
}
289+
290+
index[p.URL] = findEntry{
291+
Title: p.Frontmatter.Title,
292+
URL: p.URL,
293+
Content: strings.ToLower(strings.TrimSpace(buf.String())),
294+
}
295+
}
296+
297+
data, err := json.Marshal(index)
298+
if err != nil {
299+
return fmt.Errorf("marshalling find index: %w", err)
300+
}
301+
302+
return os.WriteFile(filepath.Join(b.outDir, "find.json"), data, 0o644)
303+
}
304+
259305
func (b *Builder) buildSiteDataJSON(pages []*content.Page, posts []*content.Page, tags map[string][]*content.Page) (template.JS, error) {
260306
siteData := map[string]any{
261307
"site": map[string]any{
@@ -265,6 +311,7 @@ func (b *Builder) buildSiteDataJSON(pages []*content.Page, posts []*content.Page
265311
},
266312
"terminal": map[string]any{
267313
"prompt": b.cfg.Terminal.Prompt,
314+
"whoami": b.cfg.Terminal.WhoAmI,
268315
"asciiArt": b.cfg.Terminal.ASCIIArt,
269316
"bannerHTML": banner.Render(b.cfg.Terminal.Banner.Text, b.cfg.Terminal.Banner.Font, b.cfg.Terminal.Banner.Colors, b.cfg.Terminal.Banner.ColorType),
270317
},

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type NavItem struct {
3232

3333
type TerminalConfig struct {
3434
Prompt string `yaml:"prompt"`
35+
WhoAmI string `yaml:"whoami"`
3536
ASCIIArt string `yaml:"ascii_art"`
3637
Banner BannerConfig `yaml:"banner"`
3738
}

0 commit comments

Comments
 (0)