| 1 | const DEFAULT_COLOR = 256; |
| 2 | const FLAG_BOLD = 0x01; |
| 3 | const FLAG_DIM = 0x02; |
| 4 | const FLAG_ITALIC = 0x04; |
| 5 | const FLAG_UNDERLINE = 0x08; |
| 6 | const FLAG_REVERSE = 0x20; |
| 7 | const FLAG_INVISIBLE = 0x40; |
| 8 | const FLAG_STRIKETHROUGH = 0x80; |
| 9 | const DEFAULT_SCROLLBACK_OVERSCAN_ROWS = 10; |
| 10 | function rgbToCSS(packed) { |
| 11 | const r = (packed >> 16) & 0xff; |
| 12 | const g = (packed >> 8) & 0xff; |
| 13 | const b = packed & 0xff; |
| 14 | return `rgb(${r},${g},${b})`; |
| 15 | } |
| 16 | function colorToCSS(index) { |
| 17 | if (index === DEFAULT_COLOR) |
| 18 | return null; |
| 19 | if (index < 16) |
| 20 | return `var(--term-color-${index})`; |
| 21 | if (index < 232) { |
| 22 | const n = index - 16; |
| 23 | const r = Math.floor(n / 36) * 51; |
| 24 | const g = (Math.floor(n / 6) % 6) * 51; |
| 25 | const b = (n % 6) * 51; |
| 26 | return `rgb(${r},${g},${b})`; |
| 27 | } |
| 28 | const level = (index - 232) * 10 + 8; |
| 29 | return `rgb(${level},${level},${level})`; |
| 30 | } |
| 31 | function cellFgCSS(fg, fgRgb) { |
| 32 | if (fgRgb !== undefined) |
| 33 | return rgbToCSS(fgRgb); |
| 34 | return colorToCSS(fg); |
| 35 | } |
| 36 | function cellBgCSS(bg, bgRgb) { |
| 37 | if (bgRgb !== undefined) |
| 38 | return rgbToCSS(bgRgb); |
| 39 | return colorToCSS(bg); |
| 40 | } |
| 41 | function buildCellStyle(fg, bg, flags, fgRgb, bgRgb) { |
| 42 | let fgIdx = fg, bgIdx = bg, fgR = fgRgb, bgR = bgRgb; |
| 43 | if (flags & FLAG_REVERSE) { |
| 44 | const tmpIdx = fgIdx; |
| 45 | fgIdx = bgIdx; |
| 46 | bgIdx = tmpIdx; |
| 47 | const tmpR = fgR; |
| 48 | fgR = bgR; |
| 49 | bgR = tmpR; |
| 50 | if (fgR === undefined && fgIdx === DEFAULT_COLOR) |
| 51 | fgIdx = 0; |
| 52 | if (bgR === undefined && bgIdx === DEFAULT_COLOR) |
| 53 | bgIdx = 7; |
| 54 | } |
| 55 | const fgCSS = cellFgCSS(fgIdx, fgR); |
| 56 | const bgCSS = cellBgCSS(bgIdx, bgR); |
| 57 | let style = ""; |
| 58 | if (fgCSS) |
| 59 | style += `color:${fgCSS};`; |
| 60 | if (bgCSS) |
| 61 | style += `background:${bgCSS};`; |
| 62 | if (flags & FLAG_BOLD) |
| 63 | style += "font-weight:bold;"; |
| 64 | if (flags & FLAG_DIM) |
| 65 | style += "opacity:0.5;"; |
| 66 | if (flags & FLAG_ITALIC) |
| 67 | style += "font-style:italic;"; |
| 68 | const decorations = []; |
| 69 | if (flags & FLAG_UNDERLINE) |
| 70 | decorations.push("underline"); |
| 71 | if (flags & FLAG_STRIKETHROUGH) |
| 72 | decorations.push("line-through"); |
| 73 | if (decorations.length) |
| 74 | style += `text-decoration:${decorations.join(" ")};`; |
| 75 | if (flags & FLAG_INVISIBLE) |
| 76 | style += "visibility:hidden;"; |
| 77 | return style; |
| 78 | } |
| 79 | function appendRun(parent, text, style) { |
| 80 | const span = document.createElement("span"); |
| 81 | if (style) |
| 82 | span.style.cssText = style; |
| 83 | span.textContent = text; |
| 84 | parent.appendChild(span); |
| 85 | } |
| 86 | function escapeHTML(text) { |
| 87 | return text |
| 88 | .replace(/&/g, "&") |
| 89 | .replace(/</g, "<") |
| 90 | .replace(/>/g, ">") |
| 91 | .replace(/"/g, """); |
| 92 | } |
| 93 | function safeLinkHref(uri) { |
| 94 | if (!uri) |
| 95 | return undefined; |
| 96 | try { |
| 97 | const url = new URL(uri); |
| 98 | return url.protocol === "http:" || url.protocol === "https:" |
| 99 | ? url.href |
| 100 | : undefined; |
| 101 | } |
| 102 | catch { |
| 103 | return undefined; |
| 104 | } |
| 105 | } |
| 106 | function linkIdentity(cell) { |
| 107 | if (!cell.linkUri) |
| 108 | return ""; |
| 109 | return cell.linkKey ?? `fallback\0${cell.linkId ?? ""}\0${cell.linkUri}`; |
| 110 | } |
| 111 | function resolveColors(fg, bg, flags, fgRgb, bgRgb) { |
| 112 | let fgIdx = fg, bgIdx = bg, fgR = fgRgb, bgR = bgRgb; |
| 113 | if (flags & FLAG_REVERSE) { |
| 114 | [fgIdx, bgIdx] = [bgIdx, fgIdx]; |
| 115 | [fgR, bgR] = [bgR, fgR]; |
| 116 | if (fgR === undefined && fgIdx === DEFAULT_COLOR) |
| 117 | fgIdx = 0; |
| 118 | if (bgR === undefined && bgIdx === DEFAULT_COLOR) |
| 119 | bgIdx = 7; |
| 120 | } |
| 121 | return { |
| 122 | fg: cellFgCSS(fgIdx, fgR) || "var(--term-fg)", |
| 123 | bg: cellBgCSS(bgIdx, bgR) || "var(--term-bg)", |
| 124 | }; |
| 125 | } |
| 126 | // Pixel-snapped vertical gradient stops keyed off `--term-row-height` so that |
| 127 | // every cell paints the eighth-block boundary on the same physical pixel — |
| 128 | // using raw percentages (e.g. `12.5%`) at the canonical 17px row-height |
| 129 | // resolves to 2.125px and the browser rounds it differently across cells, |
| 130 | // producing the per-cell jog Claude Code's horizontal-rule (`▔▔▔▔▔`) makes |
| 131 | // visible against the row immediately below. |
| 132 | const SNAP_1_8 = "round(calc(var(--term-row-height) * 0.125), 1px)"; |
| 133 | const SNAP_2_8 = "round(calc(var(--term-row-height) * 0.25), 1px)"; |
| 134 | const SNAP_3_8 = "round(calc(var(--term-row-height) * 0.375), 1px)"; |
| 135 | const SNAP_4_8 = "round(calc(var(--term-row-height) * 0.5), 1px)"; |
| 136 | const SNAP_5_8 = "round(calc(var(--term-row-height) * 0.625), 1px)"; |
| 137 | const SNAP_6_8 = "round(calc(var(--term-row-height) * 0.75), 1px)"; |
| 138 | const SNAP_7_8 = "round(calc(var(--term-row-height) * 0.875), 1px)"; |
| 139 | function getBlockBackground(cp, fg, bg) { |
| 140 | switch (cp) { |
| 141 | case 0x2580: |
| 142 | return `linear-gradient(${fg} ${SNAP_4_8},${bg} ${SNAP_4_8})`; |
| 143 | case 0x2581: |
| 144 | return `linear-gradient(${bg} ${SNAP_7_8},${fg} ${SNAP_7_8})`; |
| 145 | case 0x2582: |
| 146 | return `linear-gradient(${bg} ${SNAP_6_8},${fg} ${SNAP_6_8})`; |
| 147 | case 0x2583: |
| 148 | return `linear-gradient(${bg} ${SNAP_5_8},${fg} ${SNAP_5_8})`; |
| 149 | case 0x2584: |
| 150 | return `linear-gradient(${bg} ${SNAP_4_8},${fg} ${SNAP_4_8})`; |
| 151 | case 0x2585: |
| 152 | return `linear-gradient(${bg} ${SNAP_3_8},${fg} ${SNAP_3_8})`; |
| 153 | case 0x2586: |
| 154 | return `linear-gradient(${bg} ${SNAP_2_8},${fg} ${SNAP_2_8})`; |
| 155 | case 0x2587: |
| 156 | return `linear-gradient(${bg} ${SNAP_1_8},${fg} ${SNAP_1_8})`; |
| 157 | case 0x2588: |
| 158 | return fg; |
| 159 | case 0x2589: |
| 160 | return `linear-gradient(to right,${fg} 87.5%,${bg} 87.5%)`; |
| 161 | case 0x258a: |
| 162 | return `linear-gradient(to right,${fg} 75%,${bg} 75%)`; |
| 163 | case 0x258b: |
| 164 | return `linear-gradient(to right,${fg} 62.5%,${bg} 62.5%)`; |
| 165 | case 0x258c: |
| 166 | return `linear-gradient(to right,${fg} 50%,${bg} 50%)`; |
| 167 | case 0x258d: |
| 168 | return `linear-gradient(to right,${fg} 37.5%,${bg} 37.5%)`; |
| 169 | case 0x258e: |
| 170 | return `linear-gradient(to right,${fg} 25%,${bg} 25%)`; |
| 171 | case 0x258f: |
| 172 | return `linear-gradient(to right,${fg} 12.5%,${bg} 12.5%)`; |
| 173 | case 0x2590: |
| 174 | return `linear-gradient(to right,${bg} 50%,${fg} 50%)`; |
| 175 | case 0x2591: |
| 176 | return `color-mix(in srgb,${fg} 25%,${bg})`; |
| 177 | case 0x2592: |
| 178 | return `color-mix(in srgb,${fg} 50%,${bg})`; |
| 179 | case 0x2593: |
| 180 | return `color-mix(in srgb,${fg} 75%,${bg})`; |
| 181 | case 0x2594: |
| 182 | return `linear-gradient(${fg} ${SNAP_1_8},${bg} ${SNAP_1_8})`; |
| 183 | case 0x2595: |
| 184 | return `linear-gradient(to right,${bg} 87.5%,${fg} 87.5%)`; |
| 185 | default: { |
| 186 | const QUADRANTS = { |
| 187 | 0x2596: [false, false, true, false], |
| 188 | 0x2597: [false, false, false, true], |
| 189 | 0x2598: [true, false, false, false], |
| 190 | 0x2599: [true, false, true, true], |
| 191 | 0x259a: [true, false, false, true], |
| 192 | 0x259b: [true, true, true, false], |
| 193 | 0x259c: [true, true, false, true], |
| 194 | 0x259d: [false, true, false, false], |
| 195 | 0x259e: [false, true, true, false], |
| 196 | 0x259f: [false, true, true, true], |
| 197 | }; |
| 198 | const q = QUADRANTS[cp]; |
| 199 | if (!q) |
| 200 | return fg; |
| 201 | const [tl, tr, bl, br] = q; |
| 202 | if (tl && tr && bl && br) |
| 203 | return fg; |
| 204 | const layers = []; |
| 205 | const POS = ["0 0", "100% 0", "0 100%", "100% 100%"]; |
| 206 | q.forEach((filled, i) => { |
| 207 | if (filled) |
| 208 | layers.push(`linear-gradient(${fg},${fg}) ${POS[i]}/50% 50% no-repeat`); |
| 209 | }); |
| 210 | layers.push(bg); |
| 211 | return layers.join(","); |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | export class Renderer { |
| 216 | constructor(container) { |
| 217 | this.rows = 0; |
| 218 | this.cols = 0; |
| 219 | this.rowEls = []; |
| 220 | this.prevCursorRow = -1; |
| 221 | this.prevCursorCol = -1; |
| 222 | this.prevContainerBg = ""; |
| 223 | this.prevRowBg = []; |
| 224 | this._scrollbackRowEls = []; |
| 225 | this._scrollbackStartKey = 0; |
| 226 | this._renderedScrollbackCount = -1; |
| 227 | this._renderedDiscardedCount = -1; |
| 228 | this._scrollbackTopSpacer = null; |
| 229 | this._scrollbackBottomSpacer = null; |
| 230 | this.container = container; |
| 231 | } |
| 232 | setup(cols, rows) { |
| 233 | this.cols = cols; |
| 234 | this.rows = rows; |
| 235 | this.container.innerHTML = ""; |
| 236 | this.rowEls = []; |
| 237 | this.prevRowBg = []; |
| 238 | this._scrollbackRowEls = []; |
| 239 | this._scrollbackStartKey = 0; |
| 240 | this._renderedScrollbackCount = -1; |
| 241 | this._renderedDiscardedCount = -1; |
| 242 | const fragment = document.createDocumentFragment(); |
| 243 | this._scrollbackTopSpacer = document.createElement("div"); |
| 244 | this._scrollbackTopSpacer.className = "term-scrollback-spacer"; |
| 245 | fragment.appendChild(this._scrollbackTopSpacer); |
| 246 | this._scrollbackBottomSpacer = document.createElement("div"); |
| 247 | this._scrollbackBottomSpacer.className = "term-scrollback-spacer"; |
| 248 | fragment.appendChild(this._scrollbackBottomSpacer); |
| 249 | for (let r = 0; r < rows; r++) { |
| 250 | const rowEl = document.createElement("div"); |
| 251 | rowEl.className = "term-row"; |
| 252 | fragment.appendChild(rowEl); |
| 253 | this.rowEls.push(rowEl); |
| 254 | } |
| 255 | this.container.appendChild(fragment); |
| 256 | this.prevCursorRow = -1; |
| 257 | this.prevCursorCol = -1; |
| 258 | } |
| 259 | _buildRowContent(rowEl, getCell, lineLen, cursorCol, rowIndex) { |
| 260 | let html = ""; |
| 261 | let runStyle = ""; |
| 262 | let runText = ""; |
| 263 | let runCells = []; |
| 264 | let runStart = 0; |
| 265 | let runLinkKey = ""; |
| 266 | let runLinkUri; |
| 267 | let outputLinkKey = ""; |
| 268 | const appendContent = (content, linkKey, uri) => { |
| 269 | const href = safeLinkHref(uri); |
| 270 | const nextLinkKey = href ? linkKey : ""; |
| 271 | if (nextLinkKey !== outputLinkKey) { |
| 272 | if (outputLinkKey) |
| 273 | html += "</a>"; |
| 274 | if (nextLinkKey) { |
| 275 | html += `<a class="term-link" href="${escapeHTML(href)}" target="_blank" rel="noopener noreferrer">`; |
| 276 | } |
| 277 | outputLinkKey = nextLinkKey; |
| 278 | } |
| 279 | html += content; |
| 280 | }; |
| 281 | const flushRun = (endCol) => { |
| 282 | if (!runText) |
| 283 | return; |
| 284 | const escaped = escapeHTML(runText); |
| 285 | let content = ""; |
| 286 | if (cursorCol >= runStart && cursorCol < endCol) { |
| 287 | const offset = cursorCol - runStart; |
| 288 | const before = runCells.slice(0, offset).join(""); |
| 289 | const cursorChar = runCells[offset] || " "; |
| 290 | const after = runCells.slice(offset + 1).join(""); |
| 291 | if (before) { |
| 292 | content += runStyle |
| 293 | ? `<span style="${runStyle}">${escapeHTML(before)}</span>` |
| 294 | : `<span>${escapeHTML(before)}</span>`; |
| 295 | } |
| 296 | content += runStyle |
| 297 | ? `<span class="term-cursor" style="${runStyle}">${escapeHTML(cursorChar)}</span>` |
| 298 | : `<span class="term-cursor">${escapeHTML(cursorChar)}</span>`; |
| 299 | if (after) { |
| 300 | content += runStyle |
| 301 | ? `<span style="${runStyle}">${escapeHTML(after)}</span>` |
| 302 | : `<span>${escapeHTML(after)}</span>`; |
| 303 | } |
| 304 | } |
| 305 | else { |
| 306 | content += runStyle |
| 307 | ? `<span style="${runStyle}">${escaped}</span>` |
| 308 | : `<span>${escaped}</span>`; |
| 309 | } |
| 310 | appendContent(content, runLinkKey, runLinkUri); |
| 311 | runText = ""; |
| 312 | runCells = []; |
| 313 | }; |
| 314 | const appendStyledSpan = (className, style, text, linkKey, linkUri) => { |
| 315 | const classAttr = className ? ` class="${className}"` : ""; |
| 316 | const styleAttr = style ? ` style="${style}"` : ""; |
| 317 | appendContent(`<span${classAttr}${styleAttr}>${escapeHTML(text)}</span>`, linkKey, linkUri); |
| 318 | }; |
| 319 | for (let col = 0; col < this.cols; col++) { |
| 320 | const cell = getCell(col); |
| 321 | const inBounds = col < lineLen; |
| 322 | const cp = inBounds ? cell.char : 0; |
| 323 | const width = inBounds ? (cell.width ?? 1) : 1; |
| 324 | const cellLinkKey = inBounds ? linkIdentity(cell) : ""; |
| 325 | const cellLinkUri = inBounds ? cell.linkUri : undefined; |
| 326 | if (inBounds && width === 0) { |
| 327 | flushRun(col); |
| 328 | // Skipping is only right when this continues the wide cell to the |
| 329 | // left, which already covers both columns and its cursor. A width-0 |
| 330 | // cell with no wide cell before it owns its column, so dropping it |
| 331 | // would shorten the row. |
| 332 | const continuesWide = col > 0 && (getCell(col - 1).width ?? 1) === 2; |
| 333 | if (!continuesWide) { |
| 334 | appendStyledSpan(col === cursorCol ? "term-cursor" : "", "", " ", cellLinkKey, cellLinkUri); |
| 335 | } |
| 336 | runStyle = ""; |
| 337 | runLinkKey = ""; |
| 338 | runLinkUri = undefined; |
| 339 | runText = ""; |
| 340 | runCells = []; |
| 341 | runStart = col + 1; |
| 342 | continue; |
| 343 | } |
| 344 | if (inBounds && width === 2) { |
| 345 | flushRun(col); |
| 346 | // A scrollback row keeps the width it was stored at, so a narrower |
| 347 | // grid can put the last rendered column on a wide lead whose |
| 348 | // continuation is outside the row. Drawing the pair here would spill |
| 349 | // a second column past the row. |
| 350 | if (col + 1 >= this.cols) { |
| 351 | appendStyledSpan(col === cursorCol ? "term-cursor" : "", "", " ", cellLinkKey, cellLinkUri); |
| 352 | runStyle = ""; |
| 353 | runLinkKey = ""; |
| 354 | runLinkUri = undefined; |
| 355 | runText = ""; |
| 356 | runCells = []; |
| 357 | runStart = col + 1; |
| 358 | continue; |
| 359 | } |
| 360 | const ch = cell.chars ?? (cp >= 32 ? String.fromCodePoint(cp) : " "); |
| 361 | const style = buildCellStyle(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb); |
| 362 | const cls = cursorCol >= col && cursorCol < col + 2 |
| 363 | ? "term-wide term-cursor" |
| 364 | : "term-wide"; |
| 365 | appendStyledSpan(cls, style, ch, cellLinkKey, cellLinkUri); |
| 366 | runStyle = ""; |
| 367 | runLinkKey = ""; |
| 368 | runLinkUri = undefined; |
| 369 | runText = ""; |
| 370 | runCells = []; |
| 371 | runStart = col + 2; |
| 372 | continue; |
| 373 | } |
| 374 | if (inBounds && cp >= 0x2580 && cp <= 0x259f) { |
| 375 | flushRun(col); |
| 376 | const colors = resolveColors(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb); |
| 377 | const cls = col === cursorCol ? "term-block term-cursor" : "term-block"; |
| 378 | const bg = getBlockBackground(cp, colors.fg, colors.bg); |
| 379 | const dim = cell.flags & FLAG_DIM ? "opacity:0.5;" : ""; |
| 380 | appendContent(`<span class="${cls}" style="background:${bg};${dim}"></span>`, cellLinkKey, cellLinkUri); |
| 381 | runStyle = ""; |
| 382 | runLinkKey = ""; |
| 383 | runLinkUri = undefined; |
| 384 | runText = ""; |
| 385 | runCells = []; |
| 386 | runStart = col + 1; |
| 387 | } |
| 388 | else { |
| 389 | const ch = cell.chars ?? (inBounds && cp >= 32 ? String.fromCodePoint(cp) : " "); |
| 390 | const style = inBounds |
| 391 | ? buildCellStyle(cell.fg, cell.bg, cell.flags, cell.fgRgb, cell.bgRgb) |
| 392 | : ""; |
| 393 | if (style !== runStyle || cellLinkKey !== runLinkKey) { |
| 394 | flushRun(col); |
| 395 | runStyle = style; |
| 396 | runLinkKey = cellLinkKey; |
| 397 | runLinkUri = cellLinkUri; |
| 398 | runText = ch; |
| 399 | runCells = [ch]; |
| 400 | runStart = col; |
| 401 | } |
| 402 | else { |
| 403 | runText += ch; |
| 404 | runCells.push(ch); |
| 405 | } |
| 406 | } |
| 407 | } |
| 408 | flushRun(this.cols); |
| 409 | if (outputLinkKey) |
| 410 | html += "</a>"; |
| 411 | rowEl.innerHTML = html; |
| 412 | let bgCss = ""; |
| 413 | if (lineLen >= this.cols && this.cols > 0) { |
| 414 | const lastCell = getCell(this.cols - 1); |
| 415 | let bgIdx = lastCell.bg; |
| 416 | let bgR = lastCell.bgRgb; |
| 417 | if (lastCell.flags & FLAG_REVERSE) { |
| 418 | bgIdx = lastCell.fg; |
| 419 | bgR = lastCell.fgRgb; |
| 420 | if (bgR === undefined && bgIdx === DEFAULT_COLOR) |
| 421 | bgIdx = 7; |
| 422 | } |
| 423 | bgCss = cellBgCSS(bgIdx, bgR) || ""; |
| 424 | } |
| 425 | const boxShadow = bgCss ? `0 1px 0 ${bgCss}` : ""; |
| 426 | if (rowIndex >= 0) { |
| 427 | if (bgCss !== (this.prevRowBg[rowIndex] ?? "")) { |
| 428 | rowEl.style.background = bgCss; |
| 429 | rowEl.style.boxShadow = boxShadow; |
| 430 | this.prevRowBg[rowIndex] = bgCss; |
| 431 | } |
| 432 | } |
| 433 | else { |
| 434 | rowEl.style.background = bgCss; |
| 435 | rowEl.style.boxShadow = boxShadow; |
| 436 | } |
| 437 | } |
| 438 | _buildScrollbackRowEl(core, sbOffset) { |
| 439 | const rowEl = document.createElement("div"); |
| 440 | rowEl.className = "term-row term-scrollback-row"; |
| 441 | const lineLen = core.getScrollbackLineLen(sbOffset); |
| 442 | this._buildRowContent(rowEl, (col) => core.getScrollbackCell(sbOffset, col), lineLen, -1, -1); |
| 443 | return rowEl; |
| 444 | } |
| 445 | syncScrollback(core, viewport) { |
| 446 | const scrollbackCount = core.getScrollbackCount(); |
| 447 | const rowHeight = viewport?.rowHeight ?? 0; |
| 448 | const virtual = viewport !== undefined && rowHeight > 0; |
| 449 | const overscan = viewport?.overscanRows ?? DEFAULT_SCROLLBACK_OVERSCAN_ROWS; |
| 450 | const hasDiscardedCount = viewport?.scrollbackDiscardedCount !== undefined; |
| 451 | const discardedCount = viewport?.scrollbackDiscardedCount ?? 0; |
| 452 | const viewportHeight = viewport && viewport.clientHeight > 0 |
| 453 | ? viewport.clientHeight |
| 454 | : this.rows * rowHeight; |
| 455 | const firstVisible = virtual |
| 456 | ? Math.floor(viewport.scrollTop / rowHeight) |
| 457 | : 0; |
| 458 | const visibleRows = virtual |
| 459 | ? Math.ceil(viewportHeight / rowHeight) |
| 460 | : scrollbackCount; |
| 461 | let start = virtual |
| 462 | ? Math.max(0, Math.min(scrollbackCount, firstVisible - overscan)) |
| 463 | : 0; |
| 464 | let end = virtual |
| 465 | ? Math.max(start, Math.min(scrollbackCount, firstVisible + visibleRows + overscan)) |
| 466 | : scrollbackCount; |
| 467 | const selection = this.container.ownerDocument.getSelection(); |
| 468 | const selectionInContainer = selection !== null && |
| 469 | !selection.isCollapsed && |
| 470 | (this.container.contains(selection.anchorNode) || |
| 471 | this.container.contains(selection.focusNode)); |
| 472 | if (selectionInContainer && this._scrollbackRowEls.length > 0) { |
| 473 | start = Math.min(start, Math.max(0, this._scrollbackStartKey - discardedCount)); |
| 474 | end = Math.max(end, Math.min(scrollbackCount, this._scrollbackStartKey - |
| 475 | discardedCount + |
| 476 | this._scrollbackRowEls.length)); |
| 477 | } |
| 478 | const startKey = discardedCount + start; |
| 479 | if (hasDiscardedCount && |
| 480 | scrollbackCount === this._renderedScrollbackCount && |
| 481 | discardedCount === this._renderedDiscardedCount && |
| 482 | startKey === this._scrollbackStartKey && |
| 483 | end - start === this._scrollbackRowEls.length) { |
| 484 | return; |
| 485 | } |
| 486 | const previous = new Map(); |
| 487 | for (let i = 0; i < this._scrollbackRowEls.length; i++) { |
| 488 | previous.set(this._scrollbackStartKey + i, this._scrollbackRowEls[i]); |
| 489 | } |
| 490 | const endKey = discardedCount + end; |
| 491 | for (const [key, rowEl] of previous) { |
| 492 | if (key < startKey || key >= endKey) |
| 493 | rowEl.remove(); |
| 494 | } |
| 495 | const nextRows = []; |
| 496 | let nextSibling = this._scrollbackTopSpacer?.nextSibling ?? null; |
| 497 | for (let index = start; index < end; index++) { |
| 498 | const key = discardedCount + index; |
| 499 | const offset = scrollbackCount - 1 - index; |
| 500 | const candidate = this._buildScrollbackRowEl(core, offset); |
| 501 | const existing = previous.get(key); |
| 502 | let rowEl = candidate; |
| 503 | let positioned = false; |
| 504 | if (existing && |
| 505 | existing.innerHTML === candidate.innerHTML && |
| 506 | existing.style.cssText === candidate.style.cssText) { |
| 507 | rowEl = existing; |
| 508 | } |
| 509 | else if (existing) { |
| 510 | existing.replaceWith(candidate); |
| 511 | positioned = true; |
| 512 | } |
| 513 | if (!positioned && rowEl !== nextSibling) { |
| 514 | this.container.insertBefore(rowEl, nextSibling ?? this._scrollbackBottomSpacer); |
| 515 | } |
| 516 | nextSibling = rowEl.nextSibling; |
| 517 | nextRows.push(rowEl); |
| 518 | } |
| 519 | this._scrollbackRowEls = nextRows; |
| 520 | this._scrollbackStartKey = startKey; |
| 521 | this._renderedScrollbackCount = scrollbackCount; |
| 522 | this._renderedDiscardedCount = discardedCount; |
| 523 | if (this._scrollbackTopSpacer) { |
| 524 | this._scrollbackTopSpacer.style.height = `${start * rowHeight}px`; |
| 525 | } |
| 526 | if (this._scrollbackBottomSpacer) { |
| 527 | this._scrollbackBottomSpacer.style.height = `${(scrollbackCount - end) * rowHeight}px`; |
| 528 | } |
| 529 | } |
| 530 | render(core, viewport) { |
| 531 | const rows = core.getRows(); |
| 532 | const cols = core.getCols(); |
| 533 | let resized = false; |
| 534 | if (rows !== this.rows || cols !== this.cols) { |
| 535 | this.setup(cols, rows); |
| 536 | resized = true; |
| 537 | } |
| 538 | this.syncScrollback(core, viewport); |
| 539 | const cursor = core.getCursor(); |
| 540 | const cursorVisible = cursor.visible; |
| 541 | const needsCursorUpdate = cursor.row !== this.prevCursorRow || cursor.col !== this.prevCursorCol; |
| 542 | for (let r = 0; r < this.rows; r++) { |
| 543 | const isDirty = resized || core.isDirtyRow(r); |
| 544 | const hadCursor = r === this.prevCursorRow && needsCursorUpdate; |
| 545 | const hasCursor = r === cursor.row; |
| 546 | if (isDirty || hadCursor || (hasCursor && needsCursorUpdate)) { |
| 547 | const cCol = hasCursor && cursorVisible ? cursor.col : -1; |
| 548 | this._buildRowContent(this.rowEls[r], (col) => core.getCell(r, col), this.cols, cCol, r); |
| 549 | } |
| 550 | } |
| 551 | this.prevCursorRow = cursor.row; |
| 552 | this.prevCursorCol = cursor.col; |
| 553 | const lastRowDirty = resized || core.isDirtyRow(this.rows - 1); |
| 554 | if (lastRowDirty) { |
| 555 | const bottomRight = core.getCell(this.rows - 1, this.cols - 1); |
| 556 | let gridBgIdx = bottomRight.bg; |
| 557 | let gridBgRgb = bottomRight.bgRgb; |
| 558 | if (bottomRight.flags & FLAG_REVERSE) { |
| 559 | gridBgIdx = bottomRight.fg; |
| 560 | gridBgRgb = bottomRight.fgRgb; |
| 561 | if (gridBgRgb === undefined && gridBgIdx === DEFAULT_COLOR) |
| 562 | gridBgIdx = 7; |
| 563 | } |
| 564 | const containerBg = cellBgCSS(gridBgIdx, gridBgRgb) || ""; |
| 565 | if (containerBg !== this.prevContainerBg) { |
| 566 | this.container.style.background = containerBg; |
| 567 | this.prevContainerBg = containerBg; |
| 568 | } |
| 569 | } |
| 570 | core.clearDirty(); |
| 571 | } |
| 572 | } |