sync: sync wip changes
This commit is contained in:
@@ -5,7 +5,7 @@ import sqs from '../utils/sqs'
|
||||
import type { CursorPosition, CursorStyle } from './CursorProperties'
|
||||
|
||||
export class Terminal {
|
||||
public static readonly Version = "0.1.1"
|
||||
public static readonly Version = "0.2.1"
|
||||
|
||||
private terminal: HTMLElement
|
||||
private cursor: HTMLElement
|
||||
@@ -15,6 +15,7 @@ export class Terminal {
|
||||
private lines: string[] = []
|
||||
private lineElements: HTMLElement[] = []
|
||||
private lineWrapped: boolean[] = []
|
||||
private lineStyles: Map<number, Map<number, string>> = new Map()
|
||||
private scrollPending = false
|
||||
private cursorRange: Range | null = null
|
||||
|
||||
@@ -34,7 +35,7 @@ export class Terminal {
|
||||
|
||||
this.SetCursorStyle('bar')
|
||||
this.NewPage()
|
||||
this.SetupMetricsRefresh()
|
||||
this.CreateMetricsRefreshListener()
|
||||
}
|
||||
|
||||
async LoadShell(shell: Shell) {
|
||||
@@ -53,6 +54,7 @@ export class Terminal {
|
||||
this.lines = []
|
||||
this.lineElements = []
|
||||
this.lineWrapped = []
|
||||
this.lineStyles.clear()
|
||||
this.SetCursorPosition(0, 0)
|
||||
}
|
||||
|
||||
@@ -158,6 +160,46 @@ export class Terminal {
|
||||
this.UpdateLine(row)
|
||||
}
|
||||
|
||||
SetCellStyle(col: number, row: number, style: string | null) {
|
||||
if (!Number.isFinite(col) || !Number.isFinite(row))
|
||||
return
|
||||
|
||||
const targetRow = Math.max(0, Math.floor(row))
|
||||
const targetCol = Math.max(0, Math.floor(col))
|
||||
const width = this.GetWrapWidth()
|
||||
|
||||
if (targetCol >= width)
|
||||
return
|
||||
|
||||
this.EnsureLine(targetRow)
|
||||
|
||||
let line = this.lines[targetRow] ?? ''
|
||||
if (targetCol >= line.length) {
|
||||
line += ' '.repeat(targetCol - line.length + 1)
|
||||
this.lines[targetRow] = line
|
||||
}
|
||||
|
||||
const cssText = style?.trim()
|
||||
if (cssText) {
|
||||
let rowStyles = this.lineStyles.get(targetRow)
|
||||
if (!rowStyles) {
|
||||
rowStyles = new Map()
|
||||
this.lineStyles.set(targetRow, rowStyles)
|
||||
}
|
||||
rowStyles.set(targetCol, cssText)
|
||||
} else {
|
||||
const rowStyles = this.lineStyles.get(targetRow)
|
||||
if (rowStyles) {
|
||||
rowStyles.delete(targetCol)
|
||||
if (rowStyles.size === 0)
|
||||
this.lineStyles.delete(targetRow)
|
||||
}
|
||||
}
|
||||
|
||||
this.UpdateLine(targetRow)
|
||||
this.UpdateCursor()
|
||||
}
|
||||
|
||||
InsertCell(char: string) {
|
||||
const width = this.GetWrapWidth()
|
||||
let row = this.cursorPosition.row
|
||||
@@ -208,7 +250,7 @@ export class Terminal {
|
||||
}
|
||||
|
||||
ResetCellSize() {
|
||||
// dynamically determine cell size using dom
|
||||
// dynamically determine cell size with dom
|
||||
const cell = document.createElement('span')
|
||||
cell.textContent = 'A'
|
||||
cell.style.position = 'absolute'
|
||||
@@ -328,8 +370,61 @@ export class Terminal {
|
||||
|
||||
private UpdateLine(row: number) {
|
||||
const line = this.lineElements[row]
|
||||
if (line)
|
||||
line.textContent = this.lines[row]
|
||||
if (!line)
|
||||
return
|
||||
|
||||
const text = this.lines[row] ?? ''
|
||||
const styles = this.lineStyles.get(row)
|
||||
if (!styles || styles.size === 0) {
|
||||
line.textContent = text
|
||||
return
|
||||
}
|
||||
|
||||
for (const col of Array.from(styles.keys())) {
|
||||
if (col < 0 || col >= text.length)
|
||||
styles.delete(col)
|
||||
}
|
||||
|
||||
if (styles.size === 0) {
|
||||
this.lineStyles.delete(row)
|
||||
line.textContent = text
|
||||
return
|
||||
}
|
||||
|
||||
const sorted = Array.from(styles.entries()).sort((a, b) => a[0] - b[0])
|
||||
const fragment = document.createDocumentFragment()
|
||||
let cursor = 0
|
||||
|
||||
let index = 0
|
||||
while (index < sorted.length) {
|
||||
const [col, style] = sorted[index]
|
||||
if (col > cursor)
|
||||
fragment.append(document.createTextNode(text.slice(cursor, col)))
|
||||
|
||||
let runStart = col
|
||||
let runEnd = col
|
||||
while (index + 1 < sorted.length) {
|
||||
const [nextCol, nextStyle] = sorted[index + 1]
|
||||
if (nextCol !== runEnd + 1 || nextStyle !== style)
|
||||
break
|
||||
|
||||
index += 1
|
||||
runEnd = nextCol
|
||||
}
|
||||
|
||||
const span = document.createElement('span')
|
||||
span.textContent = text.slice(runStart, runEnd + 1)
|
||||
span.style.cssText = style
|
||||
fragment.append(span)
|
||||
|
||||
cursor = runEnd + 1
|
||||
index += 1
|
||||
}
|
||||
|
||||
if (cursor < text.length)
|
||||
fragment.append(document.createTextNode(text.slice(cursor)))
|
||||
|
||||
line.replaceChildren(fragment)
|
||||
}
|
||||
|
||||
private ReflowFromRow(startRow: number) {
|
||||
@@ -350,19 +445,35 @@ export class Terminal {
|
||||
text += this.lines[endRow] ?? ''
|
||||
}
|
||||
|
||||
const lineLengths: number[] = []
|
||||
for (let row = startRow; row <= endRow; row++)
|
||||
lineLengths.push((this.lines[row] ?? '').length)
|
||||
|
||||
const styledIndexes = new Map<number, string>()
|
||||
let styleOffset = 0
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const rowStyles = this.lineStyles.get(row)
|
||||
const rowLength = lineLengths[row - startRow]
|
||||
if (rowStyles && rowLength > 0) {
|
||||
for (const [col, style] of rowStyles) {
|
||||
if (col >= 0 && col < rowLength)
|
||||
styledIndexes.set(styleOffset + col, style)
|
||||
}
|
||||
}
|
||||
styleOffset += rowLength
|
||||
}
|
||||
|
||||
let row = startRow
|
||||
let index = 0
|
||||
|
||||
if (text.length === 0) {
|
||||
this.lines[row] = ''
|
||||
this.UpdateLine(row)
|
||||
this.lineWrapped[row] = false
|
||||
row += 1
|
||||
} else {
|
||||
while (index < text.length) {
|
||||
const chunk = text.slice(index, index + width)
|
||||
this.lines[row] = chunk
|
||||
this.UpdateLine(row)
|
||||
this.lineWrapped[row] = index + width < text.length
|
||||
|
||||
row += 1
|
||||
@@ -373,13 +484,34 @@ export class Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
const newLastRow = text.length > 0 ? startRow + Math.floor((text.length - 1) / width) : startRow
|
||||
const clearEnd = Math.max(endRow, newLastRow)
|
||||
for (let clearRow = startRow; clearRow <= clearEnd; clearRow++)
|
||||
this.lineStyles.delete(clearRow)
|
||||
|
||||
if (styledIndexes.size > 0) {
|
||||
for (const [styleIndex, style] of styledIndexes) {
|
||||
const targetRow = startRow + Math.floor(styleIndex / width)
|
||||
const targetCol = styleIndex % width
|
||||
let rowStyles = this.lineStyles.get(targetRow)
|
||||
if (!rowStyles) {
|
||||
rowStyles = new Map()
|
||||
this.lineStyles.set(targetRow, rowStyles)
|
||||
}
|
||||
rowStyles.set(targetCol, style)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = row; i <= endRow; i++) {
|
||||
if (this.lines[i] !== '' || this.lineWrapped[i]) {
|
||||
this.lines[i] = ''
|
||||
this.UpdateLine(i)
|
||||
this.lineWrapped[i] = false
|
||||
}
|
||||
}
|
||||
|
||||
const renderEnd = Math.max(endRow, newLastRow)
|
||||
for (let renderRow = startRow; renderRow <= renderEnd; renderRow++)
|
||||
this.UpdateLine(renderRow)
|
||||
}
|
||||
|
||||
private GetCursorXOffset(): number {
|
||||
@@ -395,23 +527,55 @@ export class Terminal {
|
||||
const clamped = Math.min(col, textLength)
|
||||
let width = 0
|
||||
|
||||
const textNode = line.firstChild
|
||||
if (textNode && textNode.nodeType === Node.TEXT_NODE && clamped > 0) {
|
||||
const hasSingleTextNode = line.childNodes.length === 1 && line.firstChild?.nodeType === Node.TEXT_NODE
|
||||
if (clamped > 0) {
|
||||
if (clamped === textLength) {
|
||||
width = line.getBoundingClientRect().width
|
||||
} else if (this.cursorRange) {
|
||||
this.cursorRange.setStart(textNode, 0)
|
||||
this.cursorRange.setEnd(textNode, clamped)
|
||||
width = this.cursorRange.getBoundingClientRect().width
|
||||
if (hasSingleTextNode) {
|
||||
const textNode = line.firstChild as Text
|
||||
this.cursorRange.setStart(textNode, 0)
|
||||
this.cursorRange.setEnd(textNode, clamped)
|
||||
width = this.cursorRange.getBoundingClientRect().width
|
||||
} else {
|
||||
const target = this.FindTextNodeAtOffset(line, clamped)
|
||||
if (target) {
|
||||
this.cursorRange.setStart(line, 0)
|
||||
this.cursorRange.setEnd(target.node, target.offset)
|
||||
width = this.cursorRange.getBoundingClientRect().width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clamped > 0 && width === 0)
|
||||
width = clamped * this.cellWidth
|
||||
|
||||
if (col > textLength)
|
||||
width += (col - textLength) * this.cellWidth
|
||||
|
||||
return width
|
||||
}
|
||||
|
||||
private FindTextNodeAtOffset(root: HTMLElement, offset: number): { node: Text; offset: number } | null {
|
||||
if (offset <= 0)
|
||||
return null
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
let remaining = offset
|
||||
let node = walker.nextNode() as Text | null
|
||||
while (node) {
|
||||
const length = node.data.length
|
||||
if (remaining <= length)
|
||||
return { node, offset: remaining }
|
||||
|
||||
remaining -= length
|
||||
node = walker.nextNode() as Text | null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private RefreshMetrics() {
|
||||
this.ResetCellSize()
|
||||
this.lineElements.forEach(line => {
|
||||
@@ -420,7 +584,7 @@ export class Terminal {
|
||||
this.UpdateCursor()
|
||||
}
|
||||
|
||||
private SetupMetricsRefresh() {
|
||||
private CreateMetricsRefreshListener() {
|
||||
if ('fonts' in document) {
|
||||
document.fonts.ready.then(() => {
|
||||
this.RefreshMetrics()
|
||||
|
||||
Reference in New Issue
Block a user