Download app/src/shell-ui.mjs from Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP: direct link, hf CLI and curl.
- Browser
- Download file 6.51 kB
-
https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/src/shell-ui.mjs
- Command line
-
hf download hf://spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/app/src/shell-ui.mjs
-
curl -L -o shell-ui.mjs https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/src/shell-ui.mjs
6.51 kB
| import Prism from 'prismjs/components/prism-core.js'; | |
| import 'prismjs/components/prism-bash.js'; | |
| import { stripTerminalSequences } from '@earendil-works/pi-tui/dist/utils.js'; | |
| Prism.manual = true; | |
| const plain = value => stripTerminalSequences(String(value ?? '')).replace(/\r\n?/g, '\n').replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, ''); | |
| const outputLimit = 24_000; | |
| function displayText(text) { | |
| text = plain(text); | |
| return text.length > outputLimit ? text.slice(0, outputLimit) + '\n… output truncated; use head or grep to narrow it down.' : text; | |
| } | |
| // Tokens become DOM text nodes, never HTML supplied by a command or file. | |
| function syntax(text) { | |
| const fragment = document.createDocumentFragment(); | |
| function append(parent, token) { | |
| if (typeof token === 'string') { parent.append(document.createTextNode(token)); return; } | |
| if (Array.isArray(token)) { token.forEach(item => append(parent, item)); return; } | |
| const span = document.createElement('span'); | |
| span.className = ['token', token.type, ...[token.alias ?? []].flat()].join(' '); | |
| append(span, token.content); parent.append(span); | |
| } | |
| // Very large pastes remain editable without synchronous grammar work. | |
| append(fragment, text.length > 8192 ? text : Prism.tokenize(text, Prism.languages.bash)); | |
| return fragment; | |
| } | |
| export function createShellUI(element, { run }) { | |
| const find = selector => element.querySelector(selector); | |
| const input = find('#command'), highlight = find('#command-highlight'); | |
| const output = find('#terminal-output'), form = find('#shell-form'); | |
| const runButton = find('#shell-run'), clearButton = find('#shell-clear'); | |
| const empty = output.firstElementChild.cloneNode(true); | |
| const history = [], entries = []; | |
| let historyIndex = 0, draft = '', running = false, busy = false; | |
| const atBottom = () => output.scrollHeight - output.scrollTop - output.clientHeight < 24; | |
| const scrollEnd = () => { output.scrollTop = output.scrollHeight; }; | |
| function controls() { | |
| runButton.disabled = running || busy || !input.value.trim(); | |
| runButton.textContent = running ? 'Running…' : 'Run'; | |
| clearButton.disabled = running || entries.length === 0; | |
| } | |
| function renderInput() { | |
| highlight.replaceChildren(syntax(input.value), document.createTextNode('\n')); | |
| input.style.height = 'auto'; input.style.height = Math.min(104, Math.max(32, input.scrollHeight)) + 'px'; | |
| highlight.scrollTop = input.scrollTop; highlight.scrollLeft = input.scrollLeft; | |
| controls(); | |
| } | |
| function setInput(value) { input.value = value; renderInput(); input.setSelectionRange(value.length, value.length); } | |
| function clear() { | |
| if (running) return; | |
| entries.length = 0; output.replaceChildren(empty.cloneNode(true)); controls(); | |
| } | |
| function appendOutput(entry, value, className) { | |
| if (!value) return; | |
| const pre = document.createElement('pre'); pre.className = className; | |
| pre.textContent = displayText(value); entry.append(pre); | |
| } | |
| form.onsubmit = async event => { | |
| event.preventDefault(); | |
| const command = input.value; | |
| if (running || busy || !command.trim()) return; | |
| running = true; | |
| if (history.at(-1) !== command) history.push(command); | |
| if (history.length > 50) history.shift(); | |
| historyIndex = history.length; draft = ''; setInput(''); input.focus(); | |
| if (!entries.length) output.replaceChildren(); | |
| const entry = document.createElement('section'); entry.className = 'shell-entry'; | |
| const header = document.createElement('div'); header.className = 'shell-entry-heading'; | |
| const prompt = document.createElement('span'); prompt.className = 'shell-prompt'; prompt.textContent = '$'; | |
| const code = document.createElement('pre'); code.className = 'shell-code'; code.append(syntax(displayText(command))); | |
| const status = document.createElement('span'); status.className = 'shell-status'; status.textContent = 'Running…'; | |
| header.append(prompt, code, status); entry.append(header); output.append(entry); entries.push(entry); scrollEnd(); | |
| try { | |
| const result = await run(command); | |
| const follow = atBottom(); | |
| appendOutput(entry, result.stdout, 'shell-stdout'); | |
| appendOutput(entry, result.stderr, 'shell-stderr'); | |
| status.classList.add(result.exitCode === 0 ? 'success' : 'error'); status.textContent = `[exit ${result.exitCode}]`; | |
| if (follow) scrollEnd(); | |
| } catch (error) { | |
| const follow = atBottom(); | |
| appendOutput(entry, error.message, 'shell-stderr'); status.classList.add('error'); status.textContent = '[error]'; | |
| if (!input.value) setInput(command); | |
| if (follow) scrollEnd(); | |
| } finally { | |
| // Keep complete recent command blocks, bounded independently of the FS. | |
| while (entries.length > 1 && (entries.length > 40 || output.textContent.length > 48_000)) entries.shift().remove(); | |
| running = false; controls(); | |
| } | |
| }; | |
| input.addEventListener('input', () => { historyIndex = history.length; draft = input.value; renderInput(); }); | |
| input.addEventListener('scroll', () => { highlight.scrollTop = input.scrollTop; highlight.scrollLeft = input.scrollLeft; }); | |
| input.addEventListener('keydown', event => { | |
| if (event.isComposing || event.keyCode === 229) return; | |
| if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); form.requestSubmit(); return; } | |
| if (event.ctrlKey && event.key.toLowerCase() === 'l') { event.preventDefault(); clear(); return; } | |
| if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey || input.selectionStart !== input.selectionEnd) return; | |
| const firstLine = !input.value.slice(0, input.selectionStart).includes('\n'); | |
| const lastLine = !input.value.slice(input.selectionEnd).includes('\n'); | |
| if (event.key === 'ArrowUp' && firstLine && historyIndex > 0) { | |
| event.preventDefault(); if (historyIndex === history.length) draft = input.value; | |
| setInput(history[--historyIndex]); | |
| } else if (event.key === 'ArrowDown' && lastLine && historyIndex < history.length) { | |
| event.preventDefault(); setInput(++historyIndex === history.length ? draft : history[historyIndex]); | |
| } | |
| }); | |
| clearButton.onclick = () => { clear(); input.focus(); }; | |
| // Wrapping and textarea height change with the workspace width. | |
| let width = 0; | |
| new ResizeObserver(([entry]) => { if (entry.contentRect.width !== width) { width = entry.contentRect.width; renderInput(); } }).observe(input.parentElement); | |
| renderInput(); | |
| return { setBusy(value) { busy = value; controls(); } }; | |
| } | |