|
12 | 12 | var history = JSON.parse(localStorage.getItem('ss_history') || '[]'); |
13 | 13 | var historyIndex = -1; |
14 | 14 | var initialContent = output.innerHTML; |
| 15 | + var findIndex = null; |
| 16 | + var suppressScroll = false; |
15 | 17 |
|
16 | 18 | function scrollToBottom() { |
17 | 19 | output.scrollTop = output.scrollHeight; |
|
157 | 159 | return '/' + parts.join('/') || '/'; |
158 | 160 | } |
159 | 161 |
|
| 162 | + if (target === '.') return currentPath; |
| 163 | + |
160 | 164 | if (target === '~' || target === '/') return '/'; |
161 | 165 |
|
162 | 166 | if (target.charAt(0) === '/') return target; |
|
217 | 221 | var lines = [ |
218 | 222 | '<span style="color:var(--ss-accent)">Available commands:</span>', |
219 | 223 | '', |
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> <page> 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> <page> Display page content', |
224 | | - ' <span style="color:var(--ss-green)">open</span> <page> 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> <name> 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> <page> Navigate to a page', |
| 226 | + ' <span style="color:var(--ss-green)">open</span> <page> Navigate to and display a page', |
| 227 | + ' <span style="color:var(--ss-green)">find</span> <query> 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> <name> Switch theme (system, light, dark)', |
228 | 235 | '', |
229 | 236 | 'You can also click any link to navigate.', |
230 | 237 | ]; |
|
299 | 306 | } |
300 | 307 | }, |
301 | 308 |
|
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 | | - |
323 | 309 | open: function (args) { |
324 | 310 | if (!args || args.length === 0) { |
325 | 311 | appendOutput('open: missing file operand', 'error'); |
|
382 | 368 | appendOutput('theme: unknown theme: ' + escapeHtml(name), 'error'); |
383 | 369 | appendOutput('Available: system, light, dark', 'system'); |
384 | 370 | } |
| 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 <query>', '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, ''); |
385 | 480 | } |
386 | 481 | }; |
387 | 482 |
|
388 | 483 | function showPageContent(pagePath) { |
389 | | - var page = findPage(pagePath || currentPath); |
| 484 | + var resolvedPath = pagePath || currentPath; |
| 485 | + var page = findPage(resolvedPath); |
390 | 486 | if (page) { |
391 | 487 | var content = document.createElement('div'); |
392 | 488 | content.className = 'content'; |
|
397 | 493 | output.innerHTML = ''; |
398 | 494 | commands.ls(); |
399 | 495 | } |
400 | | - showWelcome(); |
| 496 | + if (resolvedPath === '/') showWelcome(); |
401 | 497 | output.scrollTop = 0; |
| 498 | + suppressScroll = true; |
402 | 499 | } |
403 | 500 |
|
404 | 501 | function executeCommand(cmdStr) { |
|
421 | 518 | appendOutput('command not found: ' + escapeHtml(cmd) + '. Type <span style="color:var(--ss-green)">help</span> for available commands.', 'error'); |
422 | 519 | } |
423 | 520 |
|
424 | | - scrollToBottom(); |
| 521 | + if (suppressScroll) { |
| 522 | + suppressScroll = false; |
| 523 | + } else { |
| 524 | + scrollToBottom(); |
| 525 | + } |
425 | 526 | } |
426 | 527 |
|
427 | 528 | // ── Tab completion ── |
|
0 commit comments