diff options
Diffstat (limited to 'bundle/src')
| -rw-r--r-- | bundle/src/ansi.js | 36 | ||||
| -rw-r--r-- | bundle/src/editor.js | 5 | ||||
| -rw-r--r-- | bundle/src/main.js | 32 |
3 files changed, 63 insertions, 10 deletions
diff --git a/bundle/src/ansi.js b/bundle/src/ansi.js new file mode 100644 index 0000000..66173ea --- /dev/null +++ b/bundle/src/ansi.js @@ -0,0 +1,36 @@ +// Splits terminal text into runs styled by SGR escape sequences, keeping the +// style and any sequence cut off at a chunk boundary for the next chunk. +// Handles reset, bold and the 16 foreground colors; drops other sequences. +const names = ['Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White']; +const sequence = /\x1b\[([0-?]*)[ -/]*([@-~])/g; +const unfinished = /\x1b(\[[0-?]*[ -/]*)?$/; + +export function createAnsiParser() { + let bold = false; + let color = null; + let rest = ''; + return text => { + text = rest + text; + const cut = text.match(unfinished); + rest = cut ? cut[0] : ''; + if (cut) text = text.slice(0, cut.index); + const runs = []; + let start = 0; + for (const match of text.matchAll(sequence)) { + if (match.index > start) runs.push({ text: text.slice(start, match.index), bold, color }); + start = match.index + match[0].length; + if (match[2] !== 'm') continue; + for (const parameter of match[1].split(';')) { + const code = Number(parameter || 0); + if (code === 0) { bold = false; color = null; } + else if (code === 1) bold = true; + else if (code === 22) bold = false; + else if (code === 39) color = null; + else if (code >= 30 && code <= 37) color = `terminal.ansi${names[code - 30]}`; + else if (code >= 90 && code <= 97) color = `terminal.ansiBright${names[code - 90]}`; + } + } + if (start < text.length) runs.push({ text: text.slice(start), bold, color }); + return runs; + }; +} diff --git a/bundle/src/editor.js b/bundle/src/editor.js index 853fa82..58b0e19 100644 --- a/bundle/src/editor.js +++ b/bundle/src/editor.js @@ -33,7 +33,10 @@ export const theme = { tokenColors: [{ settings: { foreground: dark.colors['editor.foreground'], background: dark.colors['editor.background'] } }, ...dark.tokenColors] }; -for (const [key, value] of Object.entries(dark.colors)) document.documentElement.style.setProperty(`--vscode-${key.replaceAll('.', '-')}`, value); +export const themeVariable = key => `--vscode-${key.replaceAll('.', '-')}`; +// VS Code falls back to its terminal defaults when a theme leaves terminal.ansi* unset. +const colors = { ...parse(sources['../vendor/vscode/terminal-colors.json']).colors, ...dark.colors }; +for (const [key, value] of Object.entries(colors)) document.documentElement.style.setProperty(themeVariable(key), value); class TokenState { constructor(stack) { this.stack = stack; } diff --git a/bundle/src/main.js b/bundle/src/main.js index b71ed27..991ef06 100644 --- a/bundle/src/main.js +++ b/bundle/src/main.js @@ -3,7 +3,8 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { open, save, message } from '@tauri-apps/plugin-dialog'; import { KeyCode, KeyMod } from 'monaco-editor/editor/editor.api.js'; import { StandardKeyboardEvent } from 'monaco-editor/base/browser/keyboardEvent.js'; -import { createEditor } from './editor.js'; +import { createEditor, themeVariable } from './editor.js'; +import { createAnsiParser } from './ansi.js'; import './style.css'; const status = document.querySelector('#status'); @@ -34,26 +35,39 @@ async function runFile() { output.textContent = ''; status.textContent = 'Running'; const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; - let pending = ''; + // Only compiler diagnostics on stderr are colored; program output stays verbatim. + const diagnostics = createAnsiParser(); + const pending = document.createDocumentFragment(); let frame = null; function flush() { frame = null; - if (!pending) return; - output.append(document.createTextNode(pending)); - pending = ''; + if (!pending.hasChildNodes()) return; + output.append(pending); output.scrollTop = output.scrollHeight; } - function append(text) { - pending += text; + function append(stream, text) { + for (const run of stream === 'stderr' ? diagnostics(text) : [{ text }]) { + if (!run.text) continue; + if (!run.bold && !run.color) { + if (pending.lastChild?.nodeType === Node.TEXT_NODE) pending.lastChild.appendData(run.text); + else pending.append(run.text); + continue; + } + const span = document.createElement('span'); + if (run.bold) span.style.fontWeight = 'bold'; + if (run.color) span.style.color = `var(${themeVariable(run.color)})`; + span.textContent = run.text; + pending.append(span); + } if (frame === null) frame = requestAnimationFrame(flush); } let finish; const finished = new Promise(resolve => { finish = resolve; }); const channel = new Channel(event => { if (event.event === 'data') { - append(decoders[event.stream].decode(Uint8Array.from(event.bytes), { stream: true })); + append(event.stream, decoders[event.stream].decode(Uint8Array.from(event.bytes), { stream: true })); } else if (event.event === 'finished') { - for (const decoder of Object.values(decoders)) append(decoder.decode()); + for (const [stream, decoder] of Object.entries(decoders)) append(stream, decoder.decode()); if (frame !== null) cancelAnimationFrame(frame); flush(); status.textContent = event.stopped ? 'Stopped' : `Exit code: ${event.code ?? 'terminated'}`; |
