/ ink / termio / csi.ts
csi.ts
  1  /**
  2   * CSI (Control Sequence Introducer) Types
  3   *
  4   * Enums and types for CSI command parameters.
  5   */
  6  
  7  import { ESC, ESC_TYPE, SEP } from './ansi.js'
  8  
  9  export const CSI_PREFIX = ESC + String.fromCharCode(ESC_TYPE.CSI)
 10  
 11  /**
 12   * CSI parameter byte ranges
 13   */
 14  export const CSI_RANGE = {
 15    PARAM_START: 0x30,
 16    PARAM_END: 0x3f,
 17    INTERMEDIATE_START: 0x20,
 18    INTERMEDIATE_END: 0x2f,
 19    FINAL_START: 0x40,
 20    FINAL_END: 0x7e,
 21  } as const
 22  
 23  /** Check if a byte is a CSI parameter byte */
 24  export function isCSIParam(byte: number): boolean {
 25    return byte >= CSI_RANGE.PARAM_START && byte <= CSI_RANGE.PARAM_END
 26  }
 27  
 28  /** Check if a byte is a CSI intermediate byte */
 29  export function isCSIIntermediate(byte: number): boolean {
 30    return (
 31      byte >= CSI_RANGE.INTERMEDIATE_START && byte <= CSI_RANGE.INTERMEDIATE_END
 32    )
 33  }
 34  
 35  /** Check if a byte is a CSI final byte (@ through ~) */
 36  export function isCSIFinal(byte: number): boolean {
 37    return byte >= CSI_RANGE.FINAL_START && byte <= CSI_RANGE.FINAL_END
 38  }
 39  
 40  /**
 41   * Generate a CSI sequence: ESC [ p1;p2;...;pN final
 42   * Single arg: treated as raw body
 43   * Multiple args: last is final byte, rest are params joined by ;
 44   */
 45  export function csi(...args: (string | number)[]): string {
 46    if (args.length === 0) return CSI_PREFIX
 47    if (args.length === 1) return `${CSI_PREFIX}${args[0]}`
 48    const params = args.slice(0, -1)
 49    const final = args[args.length - 1]
 50    return `${CSI_PREFIX}${params.join(SEP)}${final}`
 51  }
 52  
 53  /**
 54   * CSI final bytes - the command identifier
 55   */
 56  export const CSI = {
 57    // Cursor movement
 58    CUU: 0x41, // A - Cursor Up
 59    CUD: 0x42, // B - Cursor Down
 60    CUF: 0x43, // C - Cursor Forward
 61    CUB: 0x44, // D - Cursor Back
 62    CNL: 0x45, // E - Cursor Next Line
 63    CPL: 0x46, // F - Cursor Previous Line
 64    CHA: 0x47, // G - Cursor Horizontal Absolute
 65    CUP: 0x48, // H - Cursor Position
 66    CHT: 0x49, // I - Cursor Horizontal Tab
 67    VPA: 0x64, // d - Vertical Position Absolute
 68    HVP: 0x66, // f - Horizontal Vertical Position
 69  
 70    // Erase
 71    ED: 0x4a, // J - Erase in Display
 72    EL: 0x4b, // K - Erase in Line
 73    ECH: 0x58, // X - Erase Character
 74  
 75    // Insert/Delete
 76    IL: 0x4c, // L - Insert Lines
 77    DL: 0x4d, // M - Delete Lines
 78    ICH: 0x40, // @ - Insert Characters
 79    DCH: 0x50, // P - Delete Characters
 80  
 81    // Scroll
 82    SU: 0x53, // S - Scroll Up
 83    SD: 0x54, // T - Scroll Down
 84  
 85    // Modes
 86    SM: 0x68, // h - Set Mode
 87    RM: 0x6c, // l - Reset Mode
 88  
 89    // SGR
 90    SGR: 0x6d, // m - Select Graphic Rendition
 91  
 92    // Other
 93    DSR: 0x6e, // n - Device Status Report
 94    DECSCUSR: 0x71, // q - Set Cursor Style (with space intermediate)
 95    DECSTBM: 0x72, // r - Set Top and Bottom Margins
 96    SCOSC: 0x73, // s - Save Cursor Position
 97    SCORC: 0x75, // u - Restore Cursor Position
 98    CBT: 0x5a, // Z - Cursor Backward Tabulation
 99  } as const
100  
101  /**
102   * Erase in Display regions (ED command parameter)
103   */
104  export const ERASE_DISPLAY = ['toEnd', 'toStart', 'all', 'scrollback'] as const
105  
106  /**
107   * Erase in Line regions (EL command parameter)
108   */
109  export const ERASE_LINE_REGION = ['toEnd', 'toStart', 'all'] as const
110  
111  /**
112   * Cursor styles (DECSCUSR)
113   */
114  export type CursorStyle = 'block' | 'underline' | 'bar'
115  
116  export const CURSOR_STYLES: Array<{ style: CursorStyle; blinking: boolean }> = [
117    { style: 'block', blinking: true }, // 0 - default
118    { style: 'block', blinking: true }, // 1
119    { style: 'block', blinking: false }, // 2
120    { style: 'underline', blinking: true }, // 3
121    { style: 'underline', blinking: false }, // 4
122    { style: 'bar', blinking: true }, // 5
123    { style: 'bar', blinking: false }, // 6
124  ]
125  
126  // Cursor movement generators
127  
128  /** Move cursor up n lines (CSI n A) */
129  export function cursorUp(n = 1): string {
130    return n === 0 ? '' : csi(n, 'A')
131  }
132  
133  /** Move cursor down n lines (CSI n B) */
134  export function cursorDown(n = 1): string {
135    return n === 0 ? '' : csi(n, 'B')
136  }
137  
138  /** Move cursor forward n columns (CSI n C) */
139  export function cursorForward(n = 1): string {
140    return n === 0 ? '' : csi(n, 'C')
141  }
142  
143  /** Move cursor back n columns (CSI n D) */
144  export function cursorBack(n = 1): string {
145    return n === 0 ? '' : csi(n, 'D')
146  }
147  
148  /** Move cursor to column n (1-indexed) (CSI n G) */
149  export function cursorTo(col: number): string {
150    return csi(col, 'G')
151  }
152  
153  /** Move cursor to column 1 (CSI G) */
154  export const CURSOR_LEFT = csi('G')
155  
156  /** Move cursor to row, col (1-indexed) (CSI row ; col H) */
157  export function cursorPosition(row: number, col: number): string {
158    return csi(row, col, 'H')
159  }
160  
161  /** Move cursor to home position (CSI H) */
162  export const CURSOR_HOME = csi('H')
163  
164  /**
165   * Move cursor relative to current position
166   * Positive x = right, negative x = left
167   * Positive y = down, negative y = up
168   */
169  export function cursorMove(x: number, y: number): string {
170    let result = ''
171    // Horizontal first (matches ansi-escapes behavior)
172    if (x < 0) {
173      result += cursorBack(-x)
174    } else if (x > 0) {
175      result += cursorForward(x)
176    }
177    // Then vertical
178    if (y < 0) {
179      result += cursorUp(-y)
180    } else if (y > 0) {
181      result += cursorDown(y)
182    }
183    return result
184  }
185  
186  // Save/restore cursor position
187  
188  /** Save cursor position (CSI s) */
189  export const CURSOR_SAVE = csi('s')
190  
191  /** Restore cursor position (CSI u) */
192  export const CURSOR_RESTORE = csi('u')
193  
194  // Erase generators
195  
196  /** Erase from cursor to end of line (CSI K) */
197  export function eraseToEndOfLine(): string {
198    return csi('K')
199  }
200  
201  /** Erase from cursor to start of line (CSI 1 K) */
202  export function eraseToStartOfLine(): string {
203    return csi(1, 'K')
204  }
205  
206  /** Erase entire line (CSI 2 K) */
207  export function eraseLine(): string {
208    return csi(2, 'K')
209  }
210  
211  /** Erase entire line - constant form */
212  export const ERASE_LINE = csi(2, 'K')
213  
214  /** Erase from cursor to end of screen (CSI J) */
215  export function eraseToEndOfScreen(): string {
216    return csi('J')
217  }
218  
219  /** Erase from cursor to start of screen (CSI 1 J) */
220  export function eraseToStartOfScreen(): string {
221    return csi(1, 'J')
222  }
223  
224  /** Erase entire screen (CSI 2 J) */
225  export function eraseScreen(): string {
226    return csi(2, 'J')
227  }
228  
229  /** Erase entire screen - constant form */
230  export const ERASE_SCREEN = csi(2, 'J')
231  
232  /** Erase scrollback buffer (CSI 3 J) */
233  export const ERASE_SCROLLBACK = csi(3, 'J')
234  
235  /**
236   * Erase n lines starting from cursor line, moving cursor up
237   * This erases each line and moves up, ending at column 1
238   */
239  export function eraseLines(n: number): string {
240    if (n <= 0) return ''
241    let result = ''
242    for (let i = 0; i < n; i++) {
243      result += ERASE_LINE
244      if (i < n - 1) {
245        result += cursorUp(1)
246      }
247    }
248    result += CURSOR_LEFT
249    return result
250  }
251  
252  // Scroll
253  
254  /** Scroll up n lines (CSI n S) */
255  export function scrollUp(n = 1): string {
256    return n === 0 ? '' : csi(n, 'S')
257  }
258  
259  /** Scroll down n lines (CSI n T) */
260  export function scrollDown(n = 1): string {
261    return n === 0 ? '' : csi(n, 'T')
262  }
263  
264  /** Set scroll region (DECSTBM, CSI top;bottom r). 1-indexed, inclusive. */
265  export function setScrollRegion(top: number, bottom: number): string {
266    return csi(top, bottom, 'r')
267  }
268  
269  /** Reset scroll region to full screen (DECSTBM, CSI r). Homes the cursor. */
270  export const RESET_SCROLL_REGION = csi('r')
271  
272  // Bracketed paste markers (input from terminal, not output)
273  // These are sent by the terminal to delimit pasted content when
274  // bracketed paste mode is enabled (via DEC mode 2004)
275  
276  /** Sent by terminal before pasted content (CSI 200 ~) */
277  export const PASTE_START = csi('200~')
278  
279  /** Sent by terminal after pasted content (CSI 201 ~) */
280  export const PASTE_END = csi('201~')
281  
282  // Focus event markers (input from terminal, not output)
283  // These are sent by the terminal when focus changes while
284  // focus events mode is enabled (via DEC mode 1004)
285  
286  /** Sent by terminal when it gains focus (CSI I) */
287  export const FOCUS_IN = csi('I')
288  
289  /** Sent by terminal when it loses focus (CSI O) */
290  export const FOCUS_OUT = csi('O')
291  
292  // Kitty keyboard protocol (CSI u)
293  // Enables enhanced key reporting with modifier information
294  // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
295  
296  /**
297   * Enable Kitty keyboard protocol with basic modifier reporting
298   * CSI > 1 u - pushes mode with flags=1 (disambiguate escape codes)
299   * This makes Shift+Enter send CSI 13;2 u instead of just CR
300   */
301  export const ENABLE_KITTY_KEYBOARD = csi('>1u')
302  
303  /**
304   * Disable Kitty keyboard protocol
305   * CSI < u - pops the keyboard mode stack
306   */
307  export const DISABLE_KITTY_KEYBOARD = csi('<u')
308  
309  /**
310   * Enable xterm modifyOtherKeys level 2.
311   * tmux accepts this (not the kitty stack) to enable extended keys — when
312   * extended-keys-format is csi-u, tmux then emits keys in kitty format.
313   */
314  export const ENABLE_MODIFY_OTHER_KEYS = csi('>4;2m')
315  
316  /**
317   * Disable xterm modifyOtherKeys (reset to default).
318   */
319  export const DISABLE_MODIFY_OTHER_KEYS = csi('>4m')