Current section

Files

Jump to
ghostty priv ts dom.ts
Raw

priv/ts/dom.ts

import { rgb } from './util'
import type { Color } from './types'
export function createScreen(): HTMLDivElement {
const screen = document.createElement('div')
screen.style.position = 'relative'
screen.style.display = 'block'
screen.style.width = '100%'
// Fill explicitly sized containers while retaining content-based height
// when the terminal container itself has no configured height.
screen.style.height = '100%'
screen.style.overflow = 'hidden'
return screen
}
export function createPre(): HTMLPreElement {
const pre = document.createElement('pre')
pre.style.margin = '0'
pre.style.padding = '8px'
pre.style.backgroundColor = '#1e1e2e'
pre.style.color = '#cdd6f4'
pre.style.overflow = 'hidden'
// Keep the rendered rows in normal flow so an auto-height terminal gets
// its height from its content. Fit mode measures the container directly.
pre.style.position = 'relative'
pre.style.width = '100%'
pre.style.minHeight = '100%'
pre.style.boxSizing = 'border-box'
pre.style.userSelect = 'none'
pre.style.webkitUserSelect = 'none'
pre.style.cursor = 'text'
// Lock advance width to font's nominal monospace metric. Without these,
// the browser can insert sub-pixel kerning / ligature shifts that drift
// the actual char position away from the cursor's `col * metrics.width`
// offset — by col 80 the cursor is visibly off by several characters.
pre.style.letterSpacing = '0'
pre.style.fontKerning = 'none'
pre.style.fontFeatureSettings = 'normal'
pre.style.fontVariantLigatures = 'none'
pre.style.textRendering = 'geometricPrecision'
return pre
}
export function applyTerminalColors(
pre: HTMLPreElement,
foreground: Color | null,
background: Color | null,
defaultForeground: string,
defaultBackground: string
): void {
pre.style.color = foreground ? rgb(foreground) : defaultForeground
pre.style.backgroundColor = background ? rgb(background) : defaultBackground
}
export function createSelectionLayer(): HTMLDivElement {
const layer = document.createElement('div')
layer.setAttribute('aria-hidden', 'true')
layer.setAttribute('data-ghostty-selection-layer', 'true')
layer.style.position = 'absolute'
layer.style.inset = '0'
layer.style.pointerEvents = 'none'
layer.style.zIndex = '0'
return layer
}
export function createMeasure(): HTMLSpanElement {
const span = document.createElement('span')
span.textContent = 'MMMMMMMMMM'
span.setAttribute('aria-hidden', 'true')
span.style.position = 'absolute'
span.style.visibility = 'hidden'
span.style.pointerEvents = 'none'
span.style.whiteSpace = 'pre'
span.style.font = 'inherit'
span.style.lineHeight = 'inherit'
return span
}
export function createCursorEl(): { cursorEl: HTMLDivElement; cursorText: HTMLSpanElement } {
const cursorEl = document.createElement('div')
cursorEl.setAttribute('aria-hidden', 'true')
cursorEl.style.position = 'absolute'
cursorEl.style.display = 'none'
cursorEl.style.pointerEvents = 'none'
cursorEl.style.boxSizing = 'border-box'
cursorEl.style.whiteSpace = 'pre'
cursorEl.style.zIndex = '1'
const cursorText = document.createElement('span')
cursorText.style.display = 'block'
cursorText.style.width = '100%'
cursorText.style.height = '100%'
cursorText.style.font = 'inherit'
cursorText.style.lineHeight = 'inherit'
cursorEl.appendChild(cursorText)
return { cursorEl, cursorText }
}
export function setupInput(input: HTMLTextAreaElement): void {
input.setAttribute('data-ghostty-input', 'true')
input.setAttribute('aria-label', 'Terminal input')
input.setAttribute('autocapitalize', 'off')
input.setAttribute('autocomplete', 'off')
input.setAttribute('autocorrect', 'off')
input.setAttribute('spellcheck', 'false')
input.style.position = 'absolute'
input.style.left = '0'
input.style.top = '0'
input.style.width = '1px'
input.style.height = '1em'
input.style.padding = '0'
input.style.margin = '0'
input.style.border = '0'
input.style.outline = 'none'
input.style.opacity = '0'
input.style.resize = 'none'
input.style.overflow = 'hidden'
input.style.background = 'transparent'
input.style.color = 'transparent'
input.style.caretColor = 'transparent'
input.style.whiteSpace = 'pre'
input.style.pointerEvents = 'none'
input.style.zIndex = '2'
}
export function measureCellMetrics(
pre: HTMLPreElement,
measure: HTMLSpanElement,
input: HTMLTextAreaElement,
cursorEl: HTMLDivElement
): {
width: number
height: number
paddingLeft: number
paddingRight: number
paddingTop: number
paddingBottom: number
} {
const styles = window.getComputedStyle(pre)
measure.style.fontFamily = styles.fontFamily
measure.style.fontSize = styles.fontSize
measure.style.fontWeight = styles.fontWeight
measure.style.fontStyle = styles.fontStyle
measure.style.lineHeight = styles.lineHeight
// Match the layout-locking styles set on createPre — the measure span
// otherwise reports a width subject to kerning/ligatures that the pre
// itself suppresses, drifting the cursor off the real glyph position.
measure.style.letterSpacing = '0'
measure.style.fontKerning = 'none'
measure.style.fontFeatureSettings = 'normal'
measure.style.fontVariantLigatures = 'none'
measure.style.textRendering = 'geometricPrecision'
input.style.fontFamily = styles.fontFamily
input.style.fontSize = styles.fontSize
input.style.lineHeight = styles.lineHeight
cursorEl.style.fontFamily = styles.fontFamily
cursorEl.style.fontSize = styles.fontSize
cursorEl.style.lineHeight = styles.lineHeight
const measureRect = measure.getBoundingClientRect()
const fontSize = parseFloat(styles.fontSize) || 16
const lineHeight = parseFloat(styles.lineHeight) || fontSize * 1.2
const width = measureRect.width > 0 ? measureRect.width / 10 : fontSize * 0.6
return {
width,
height: lineHeight,
paddingLeft: parseFloat(styles.paddingLeft) || 0,
paddingRight: parseFloat(styles.paddingRight) || 0,
paddingTop: parseFloat(styles.paddingTop) || 0,
paddingBottom: parseFloat(styles.paddingBottom) || 0
}
}