320 lines
8.5 KiB
TypeScript
320 lines
8.5 KiB
TypeScript
/**
|
|
* CSI (Control Sequence Introducer) Types
|
|
*
|
|
* Enums and types for CSI command parameters.
|
|
*/
|
|
|
|
import { ESC, ESC_TYPE, SEP } from './ansi.js'
|
|
|
|
export const CSI_PREFIX = ESC + String.fromCharCode(ESC_TYPE.CSI)
|
|
|
|
/**
|
|
* CSI parameter byte ranges
|
|
*/
|
|
export const CSI_RANGE = {
|
|
PARAM_START: 0x30,
|
|
PARAM_END: 0x3f,
|
|
INTERMEDIATE_START: 0x20,
|
|
INTERMEDIATE_END: 0x2f,
|
|
FINAL_START: 0x40,
|
|
FINAL_END: 0x7e,
|
|
} as const
|
|
|
|
/** Check if a byte is a CSI parameter byte */
|
|
export function isCSIParam(byte: number): boolean {
|
|
return byte >= CSI_RANGE.PARAM_START && byte <= CSI_RANGE.PARAM_END
|
|
}
|
|
|
|
/** Check if a byte is a CSI intermediate byte */
|
|
export function isCSIIntermediate(byte: number): boolean {
|
|
return (
|
|
byte >= CSI_RANGE.INTERMEDIATE_START && byte <= CSI_RANGE.INTERMEDIATE_END
|
|
)
|
|
}
|
|
|
|
/** Check if a byte is a CSI final byte (@ through ~) */
|
|
export function isCSIFinal(byte: number): boolean {
|
|
return byte >= CSI_RANGE.FINAL_START && byte <= CSI_RANGE.FINAL_END
|
|
}
|
|
|
|
/**
|
|
* Generate a CSI sequence: ESC [ p1;p2;...;pN final
|
|
* Single arg: treated as raw body
|
|
* Multiple args: last is final byte, rest are params joined by ;
|
|
*/
|
|
export function csi(...args: (string | number)[]): string {
|
|
if (args.length === 0) return CSI_PREFIX
|
|
if (args.length === 1) return `${CSI_PREFIX}${args[0]}`
|
|
const params = args.slice(0, -1)
|
|
const final = args[args.length - 1]
|
|
return `${CSI_PREFIX}${params.join(SEP)}${final}`
|
|
}
|
|
|
|
/**
|
|
* CSI final bytes - the command identifier
|
|
*/
|
|
export const CSI = {
|
|
// Cursor movement
|
|
CUU: 0x41, // A - Cursor Up
|
|
CUD: 0x42, // B - Cursor Down
|
|
CUF: 0x43, // C - Cursor Forward
|
|
CUB: 0x44, // D - Cursor Back
|
|
CNL: 0x45, // E - Cursor Next Line
|
|
CPL: 0x46, // F - Cursor Previous Line
|
|
CHA: 0x47, // G - Cursor Horizontal Absolute
|
|
CUP: 0x48, // H - Cursor Position
|
|
CHT: 0x49, // I - Cursor Horizontal Tab
|
|
VPA: 0x64, // d - Vertical Position Absolute
|
|
HVP: 0x66, // f - Horizontal Vertical Position
|
|
|
|
// Erase
|
|
ED: 0x4a, // J - Erase in Display
|
|
EL: 0x4b, // K - Erase in Line
|
|
ECH: 0x58, // X - Erase Character
|
|
|
|
// Insert/Delete
|
|
IL: 0x4c, // L - Insert Lines
|
|
DL: 0x4d, // M - Delete Lines
|
|
ICH: 0x40, // @ - Insert Characters
|
|
DCH: 0x50, // P - Delete Characters
|
|
|
|
// Scroll
|
|
SU: 0x53, // S - Scroll Up
|
|
SD: 0x54, // T - Scroll Down
|
|
|
|
// Modes
|
|
SM: 0x68, // h - Set Mode
|
|
RM: 0x6c, // l - Reset Mode
|
|
|
|
// SGR
|
|
SGR: 0x6d, // m - Select Graphic Rendition
|
|
|
|
// Other
|
|
DSR: 0x6e, // n - Device Status Report
|
|
DECSCUSR: 0x71, // q - Set Cursor Style (with space intermediate)
|
|
DECSTBM: 0x72, // r - Set Top and Bottom Margins
|
|
SCOSC: 0x73, // s - Save Cursor Position
|
|
SCORC: 0x75, // u - Restore Cursor Position
|
|
CBT: 0x5a, // Z - Cursor Backward Tabulation
|
|
} as const
|
|
|
|
/**
|
|
* Erase in Display regions (ED command parameter)
|
|
*/
|
|
export const ERASE_DISPLAY = ['toEnd', 'toStart', 'all', 'scrollback'] as const
|
|
|
|
/**
|
|
* Erase in Line regions (EL command parameter)
|
|
*/
|
|
export const ERASE_LINE_REGION = ['toEnd', 'toStart', 'all'] as const
|
|
|
|
/**
|
|
* Cursor styles (DECSCUSR)
|
|
*/
|
|
export type CursorStyle = 'block' | 'underline' | 'bar'
|
|
|
|
export const CURSOR_STYLES: Array<{ style: CursorStyle; blinking: boolean }> = [
|
|
{ style: 'block', blinking: true }, // 0 - default
|
|
{ style: 'block', blinking: true }, // 1
|
|
{ style: 'block', blinking: false }, // 2
|
|
{ style: 'underline', blinking: true }, // 3
|
|
{ style: 'underline', blinking: false }, // 4
|
|
{ style: 'bar', blinking: true }, // 5
|
|
{ style: 'bar', blinking: false }, // 6
|
|
]
|
|
|
|
// Cursor movement generators
|
|
|
|
/** Move cursor up n lines (CSI n A) */
|
|
export function cursorUp(n = 1): string {
|
|
return n === 0 ? '' : csi(n, 'A')
|
|
}
|
|
|
|
/** Move cursor down n lines (CSI n B) */
|
|
export function cursorDown(n = 1): string {
|
|
return n === 0 ? '' : csi(n, 'B')
|
|
}
|
|
|
|
/** Move cursor forward n columns (CSI n C) */
|
|
export function cursorForward(n = 1): string {
|
|
return n === 0 ? '' : csi(n, 'C')
|
|
}
|
|
|
|
/** Move cursor back n columns (CSI n D) */
|
|
export function cursorBack(n = 1): string {
|
|
return n === 0 ? '' : csi(n, 'D')
|
|
}
|
|
|
|
/** Move cursor to column n (1-indexed) (CSI n G) */
|
|
export function cursorTo(col: number): string {
|
|
return csi(col, 'G')
|
|
}
|
|
|
|
/** Move cursor to column 1 (CSI G) */
|
|
export const CURSOR_LEFT = csi('G')
|
|
|
|
/** Move cursor to row, col (1-indexed) (CSI row ; col H) */
|
|
export function cursorPosition(row: number, col: number): string {
|
|
return csi(row, col, 'H')
|
|
}
|
|
|
|
/** Move cursor to home position (CSI H) */
|
|
export const CURSOR_HOME = csi('H')
|
|
|
|
/**
|
|
* Move cursor relative to current position
|
|
* Positive x = right, negative x = left
|
|
* Positive y = down, negative y = up
|
|
*/
|
|
export function cursorMove(x: number, y: number): string {
|
|
let result = ''
|
|
// Horizontal first (matches ansi-escapes behavior)
|
|
if (x < 0) {
|
|
result += cursorBack(-x)
|
|
} else if (x > 0) {
|
|
result += cursorForward(x)
|
|
}
|
|
// Then vertical
|
|
if (y < 0) {
|
|
result += cursorUp(-y)
|
|
} else if (y > 0) {
|
|
result += cursorDown(y)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Save/restore cursor position
|
|
|
|
/** Save cursor position (CSI s) */
|
|
export const CURSOR_SAVE = csi('s')
|
|
|
|
/** Restore cursor position (CSI u) */
|
|
export const CURSOR_RESTORE = csi('u')
|
|
|
|
// Erase generators
|
|
|
|
/** Erase from cursor to end of line (CSI K) */
|
|
export function eraseToEndOfLine(): string {
|
|
return csi('K')
|
|
}
|
|
|
|
/** Erase from cursor to start of line (CSI 1 K) */
|
|
export function eraseToStartOfLine(): string {
|
|
return csi(1, 'K')
|
|
}
|
|
|
|
/** Erase entire line (CSI 2 K) */
|
|
export function eraseLine(): string {
|
|
return csi(2, 'K')
|
|
}
|
|
|
|
/** Erase entire line - constant form */
|
|
export const ERASE_LINE = csi(2, 'K')
|
|
|
|
/** Erase from cursor to end of screen (CSI J) */
|
|
export function eraseToEndOfScreen(): string {
|
|
return csi('J')
|
|
}
|
|
|
|
/** Erase from cursor to start of screen (CSI 1 J) */
|
|
export function eraseToStartOfScreen(): string {
|
|
return csi(1, 'J')
|
|
}
|
|
|
|
/** Erase entire screen (CSI 2 J) */
|
|
export function eraseScreen(): string {
|
|
return csi(2, 'J')
|
|
}
|
|
|
|
/** Erase entire screen - constant form */
|
|
export const ERASE_SCREEN = csi(2, 'J')
|
|
|
|
/** Erase scrollback buffer (CSI 3 J) */
|
|
export const ERASE_SCROLLBACK = csi(3, 'J')
|
|
|
|
/**
|
|
* Erase n lines starting from cursor line, moving cursor up
|
|
* This erases each line and moves up, ending at column 1
|
|
*/
|
|
export function eraseLines(n: number): string {
|
|
if (n <= 0) return ''
|
|
let result = ''
|
|
for (let i = 0; i < n; i++) {
|
|
result += ERASE_LINE
|
|
if (i < n - 1) {
|
|
result += cursorUp(1)
|
|
}
|
|
}
|
|
result += CURSOR_LEFT
|
|
return result
|
|
}
|
|
|
|
// Scroll
|
|
|
|
/** Scroll up n lines (CSI n S) */
|
|
export function scrollUp(n = 1): string {
|
|
return n === 0 ? '' : csi(n, 'S')
|
|
}
|
|
|
|
/** Scroll down n lines (CSI n T) */
|
|
export function scrollDown(n = 1): string {
|
|
return n === 0 ? '' : csi(n, 'T')
|
|
}
|
|
|
|
/** Set scroll region (DECSTBM, CSI top;bottom r). 1-indexed, inclusive. */
|
|
export function setScrollRegion(top: number, bottom: number): string {
|
|
return csi(top, bottom, 'r')
|
|
}
|
|
|
|
/** Reset scroll region to full screen (DECSTBM, CSI r). Homes the cursor. */
|
|
export const RESET_SCROLL_REGION = csi('r')
|
|
|
|
// Bracketed paste markers (input from terminal, not output)
|
|
// These are sent by the terminal to delimit pasted content when
|
|
// bracketed paste mode is enabled (via DEC mode 2004)
|
|
|
|
/** Sent by terminal before pasted content (CSI 200 ~) */
|
|
export const PASTE_START = csi('200~')
|
|
|
|
/** Sent by terminal after pasted content (CSI 201 ~) */
|
|
export const PASTE_END = csi('201~')
|
|
|
|
// Focus event markers (input from terminal, not output)
|
|
// These are sent by the terminal when focus changes while
|
|
// focus events mode is enabled (via DEC mode 1004)
|
|
|
|
/** Sent by terminal when it gains focus (CSI I) */
|
|
export const FOCUS_IN = csi('I')
|
|
|
|
/** Sent by terminal when it loses focus (CSI O) */
|
|
export const FOCUS_OUT = csi('O')
|
|
|
|
// Kitty keyboard protocol (CSI u)
|
|
// Enables enhanced key reporting with modifier information
|
|
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
|
|
/**
|
|
* Enable Kitty keyboard protocol with basic modifier reporting
|
|
* CSI > 1 u - pushes mode with flags=1 (disambiguate escape codes)
|
|
* This makes Shift+Enter send CSI 13;2 u instead of just CR
|
|
*/
|
|
export const ENABLE_KITTY_KEYBOARD = csi('>1u')
|
|
|
|
/**
|
|
* Disable Kitty keyboard protocol
|
|
* CSI < u - pops the keyboard mode stack
|
|
*/
|
|
export const DISABLE_KITTY_KEYBOARD = csi('<u')
|
|
|
|
/**
|
|
* Enable xterm modifyOtherKeys level 2.
|
|
* tmux accepts this (not the kitty stack) to enable extended keys — when
|
|
* extended-keys-format is csi-u, tmux then emits keys in kitty format.
|
|
*/
|
|
export const ENABLE_MODIFY_OTHER_KEYS = csi('>4;2m')
|
|
|
|
/**
|
|
* Disable xterm modifyOtherKeys (reset to default).
|
|
*/
|
|
export const DISABLE_MODIFY_OTHER_KEYS = csi('>4m')
|