Keep colors and styles of lines moved into the page
Lines scrolling off the terminal screen are converted from xterm.js's cells to styled page text: palette, 256 and true colors, background, bold (bright), italic, dim, underline, strikethrough, inverse and invisible, without trailing spaces and keeping empty lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c61a3c419
commit
fc250b31b6
2 changed files with 50 additions and 2 deletions
|
|
@ -1,5 +1,5 @@
|
|||
// The real terminal: xterm.js connected to the shared shell server over SSH. Loaded only once the first command is sent.
|
||||
import { Terminal, type IMarker } from '@xterm/xterm';
|
||||
import { Terminal, type IBufferLine, type IMarker } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import css from '@xterm/xterm/css/xterm.css?inline';
|
||||
import { connect } from './ssh';
|
||||
|
|
@ -7,6 +7,10 @@ import { connect } from './ssh';
|
|||
const SERVER = 'wss://web.nowhere.wirs.ing';
|
||||
const HOST_KEY = 'SHA256:aLNZKHCfhvbGYnjghKsOLPrOWPVWwCsIPCuTb5f9J6A';
|
||||
const SCROLLBACK = 10000;
|
||||
// xterm.js's default colors, to color lines moved into the page like the terminal does.
|
||||
const ANSI = ['#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf',
|
||||
'#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'];
|
||||
const CUBE = [0, 95, 135, 175, 215, 255];
|
||||
const boringData = 'LS0tLS1PUlRWQSBCQ1JBRkZVIENFVklOR1IgWFJMLS0tLS0KbzNPeW9hQW1uUDFlTUt4Z3F3Uk5OTk5OT1Q1aW96SE5OTk5Sb3o5aE1ETk5OTk5OTk5OT05OTk5aak5OTk5nbXAydGdNSgpEbEFHSGtCRE5OTlBQRDhVdU8vVDRKRE8xSnRVYUxIMUQyOWIvNlFPVzk3NWlyOERmTmZDRmppRE5OTlh3eVh2UmU1RmJ1ClhqTk5OTmdtcDJ0Z01KRGxBR0hrQkROTk5QUEQ4VXVPL1Q0SkRPMUp0VWFMSDFEMjliLzZRT1c5NzVpcjhEZk5mQ0ZqaUQKTk5OUk44eXRHaFIrMTlMTkVoeHo3cmQyU00xQ2lIb1IrYmpsQk5SQjVWb1BhaWFXUWpyUlU4b3VNTlVJbk5ycXVHSVFvMgp3L2JaUmEzaXo5N2tQalBqOVlQOU5OTk5WS1dpbzNFTm96OTNuVElsTUY1M25LV21ZenloTWxPd3JKcTNuSjRnWndWbFp0ClJQTmpEPQotLS0tLVJBUSBCQ1JBRkZVIENFVklOR1IgWFJMLS0tLS0K';
|
||||
|
||||
/** Continues the page's `prompt` with a terminal in which `command` was just entered, and returns that terminal. */
|
||||
|
|
@ -47,7 +51,7 @@ export function start(prompt: HTMLElement, command: string) {
|
|||
used = buffer.baseY ? term.rows : Math.max(used, buffer.cursorY + 1);
|
||||
const last = moved && !moved.isDisposed ? moved.line : -1;
|
||||
for (let y = last; y >= buffer.baseY; y--) history.lastChild!.remove(); // pulled back onto a taller screen
|
||||
for (let y = last + 1; y < buffer.baseY; y++) history.append(`${buffer.getLine(y)!.translateToString(true)}\n`);
|
||||
for (let y = last + 1; y < buffer.baseY; y++) history.append(pageLine(buffer.getLine(y)!));
|
||||
while (history.childNodes.length > SCROLLBACK) history.firstChild!.remove();
|
||||
if (last !== buffer.baseY - 1) {
|
||||
moved?.dispose();
|
||||
|
|
@ -57,6 +61,49 @@ export function start(prompt: HTMLElement, command: string) {
|
|||
history.style.lineHeight = `${rowHeight}px`;
|
||||
screen.style.height = `${(buffer.type === 'normal' ? used : term.rows) * rowHeight}px`;
|
||||
}
|
||||
|
||||
// A line as page text, with the colors and styles the terminal shows it in.
|
||||
const palette = ANSI.map((c, i) => (i === 2 ? color : c)); // green is the page's, as in the theme
|
||||
const colorOf = (n: number, rgb: boolean) => {
|
||||
if (rgb) return `#${n.toString(16).padStart(6, '0')}`;
|
||||
if (n < 16) return palette[n];
|
||||
if (n < 232) return `rgb(${CUBE[Math.floor((n - 16) / 36)]},${CUBE[Math.floor((n - 16) / 6) % 6]},${CUBE[(n - 16) % 6]})`;
|
||||
const gray = 8 + (n - 232) * 10;
|
||||
return `rgb(${gray},${gray},${gray})`;
|
||||
};
|
||||
const cell = term.buffer.normal.getNullCell();
|
||||
function pageLine(line: IBufferLine) {
|
||||
const row = document.createElement('div');
|
||||
let end = line.length; // up to the last visible cell, so copied text has no trailing spaces
|
||||
while (end && !line.getCell(end - 1, cell)!.getChars().trim() && cell.isBgDefault() && !cell.isInverse()) end--;
|
||||
let span: HTMLElement | undefined;
|
||||
let current: string | undefined;
|
||||
for (let x = 0; x < end; x++) {
|
||||
line.getCell(x, cell);
|
||||
if (!cell.getWidth()) continue; // the second half of a wide character
|
||||
const fgIndex = cell.getFgColor() + (cell.isBold() && cell.isFgPalette() && cell.getFgColor() < 8 ? 8 : 0); // bold is bright
|
||||
let fg = cell.isFgDefault() ? '' : colorOf(fgIndex, cell.isFgRGB());
|
||||
let bg = cell.isBgDefault() ? '' : colorOf(cell.getBgColor(), cell.isBgRGB());
|
||||
if (cell.isInverse()) [fg, bg] = [bg || '#000', fg || color];
|
||||
if (cell.isDim()) fg = `color-mix(in srgb, ${fg || color} 50%, transparent)`;
|
||||
const decoration = [cell.isUnderline() && 'underline', cell.isStrikethrough() && 'line-through'].filter(Boolean).join(' ');
|
||||
const style = [
|
||||
fg && `color:${fg}`,
|
||||
bg && `background:${bg}`,
|
||||
cell.isBold() && 'font-weight:bold',
|
||||
cell.isItalic() && 'font-style:italic',
|
||||
decoration && `text-decoration:${decoration}`,
|
||||
cell.isInvisible() && 'visibility:hidden',
|
||||
].filter(Boolean).join(';');
|
||||
if (style !== current) {
|
||||
span = row.appendChild(document.createElement('span'));
|
||||
span.style.cssText = current = style;
|
||||
}
|
||||
span!.textContent += cell.getChars() || ' ';
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// "Erase scrollback" (sent by `clear`) erases it from the page too.
|
||||
term.parser.registerCsiHandler({ final: 'J' }, (params) => (params[0] === 3 && history.replaceChildren(), false));
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ a:hover, a:focus-visible {
|
|||
the screen shows the rows in use and xterm.js fits rows and columns to the window. */
|
||||
.prompt:has(+ .history) { margin-bottom: 0; }
|
||||
.history { white-space: pre-wrap; }
|
||||
.history > div { min-height: 1lh; } /* keeps empty lines */
|
||||
.screen { overflow: clip; }
|
||||
.screen > div { height: 100dvh; }
|
||||
.screen .xterm-viewport, .screen .scrollbar { display: none; } /* the page scrolls instead */
|
||||
|
|
|
|||
Loading…
Reference in a new issue