1/** 2 * The terminal's welcome banner. 3 * 4 * A few lines written into the viewport before the websocket connects, so the 5 * shell's first prompt lands underneath them (see docs/archive/terminal/terminal-features-plan.md 6 * §6). It is client-side decoration: nothing here is ever sent to the pty, so 7 * none of it reaches the daemon's scrollback, and a reattach replays the 8 * previous session *below* the banner rather than around it. 9 * 10 * The content comes from the server as text plus a tone â never as ready-made 11 * escape sequences. That split is the point of this module: the server says 12 * what a line *is* (`head`, `body`, `muted`) and knows the account's username, 13 * the server's hostname and the user's language; this file owns the entire 14 * escape vocabulary, which is three SGR codes long and cannot grow by accident 15 * from the other side of the wire. 16 * 17 * It carries no strings of its own, for the same reason `notify.ts` and 18 * `progress-view.ts` carry none. 19 */ /** How a line is drawn. The server chooses one; this file maps it to SGR. */ /** 20 * Below this width the full variant's padded command column stops lining up 21 * and every line wraps, which reads as damage rather than as a greeting. 22 */ export var COMPACT_COLUMNS = 60; 23/** 24 * Below this height the banner would be most of what the terminal shows. A 25 * short pane gets no banner at all rather than a shorter one â there is 26 * nothing to greet in six rows. 27 */ export var MIN_ROWS = 10; 28var TONE_SGR = { 29 head: "\x1b[1m", 30 body: "", 31 muted: "\x1b[2m" 32}; 33/** 34 * Validate what the `banner` value carried. 35 * 36 * Stimulus gives us whatever `JSON.parse` made of the attribute, typed as 37 * `object`; a stale asset cache or a hand-edited attribute can therefore make 38 * it any shape at all. Anything unrecognised yields null and the banner is 39 * simply not written, which is the correct outcome for a decoration. 40 */ export function parseBanner(raw) { 41 if (!raw || typeof raw !== "object") { 42 return null; 43 } 44 var lines = parseLines(raw.lines); 45 var compact = parseLines(raw.compact); 46 return lines.length > 0 ? { 47 lines: lines, 48 compact: compact 49 } : null; 50} 51function parseLines(raw) { 52 if (!Array.isArray(raw)) { 53 return []; 54 } 55 return raw.filter(function(line) { 56 return !!line && typeof line === "object"; 57 }).map(function(line) { 58 return { 59 text: typeof line.text === "string" ? sanitize(line.text) : "", 60 tone: line.tone === "head" || line.tone === "muted" ? line.tone : "body" 61 }; 62 }); 63} 64/** 65 * Strip everything that could steer the terminal rather than fill it. 66 * 67 * The text is our own translated copy, so this is not a defence against a 68 * hostile server â it is what keeps the "text, not escape sequences" contract 69 * true by construction. A stray `\x1b` in a translation would otherwise be 70 * indistinguishable from the server having been handed the escape vocabulary 71 * after all. 72 */ function sanitize(text) { 73 return text.replace(/[\u0000-\u001f\u007f-\u009f]/g, " "); 74} 75/** 76 * The bytes to write, or null when this terminal should get no banner. 77 * 78 * Width picks the variant, height vetoes it outright. Every line resets SGR at 79 * its end, so nothing here can leak an attribute into the shell's first line â 80 * which is the failure mode of banners written with a bare `\x1b[1m`. 81 */ export function renderBanner(content, cols, rows) { 82 if (rows < MIN_ROWS) { 83 return null; 84 } 85 var lines = cols < COMPACT_COLUMNS && content.compact.length > 0 ? content.compact : content.lines; 86 if (lines.length === 0) { 87 return null; 88 } 89 return lines.map(function(param) { 90 var text = param.text, tone = param.tone; 91 return text ? "".concat(TONE_SGR[tone]).concat(text, "\x1b[0m\r\n") : "\r\n"; 92 }).join(""); 93}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.