char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
6334b24
main
1// libghostty-vt as wasm: terminals to write to, snapshot and restore, read back for drawing, and encode keys for. 2// Shared by the server and the browser, so it uses nothing but WebAssembly. 3 4/** A struct, union or enum as `ghostty_type_json()` describes it. */ 5type TypeInfo = { 6kind :string ; 7size :number ; 8fields :Record < string , { offset :number ; size :number }>; 9values :Record < string , number >; 10}; 11 12type Exports = Record < string , ( ...args :number []) => number > & { 13memory :WebAssembly . Memory ; 14__indirect_function_table :WebAssembly . Table ; 15}; 16 17const SUCCESS = 0 ; 18const RESULTS :Record < number , string > = { 19[ - 1 ] :"out of memory" , 20[ - 2 ] :"invalid value" , 21[ - 3 ] :"out of space" , 22[ - 4 ] :"no value" , 23}; 24 25const MODS = { shift :1 , ctrl :2 , alt :4 , super :8 }; 26const CONTINUATION_BYTES = 64 * 1024 ; 27const BRACKETED_PASTE = 2004 ; 28 29/** A host function as a funcref the module's table takes: what a callback option is set to. */ 30function hostFunction ( fn :( ...args :number []) => void , params :number ) :WebAssembly . ExportValue { 31const section = ( id :number , body :number []) => [ id , body . length , ...body ]; 32const f = 0x66 ; 33const module = new Uint8Array ([ 34 ...[ 0x00 , 0x61 , 0x73 , 0x6d , 0x01 , 0x00 , 0x00 , 0x00 ], 35 ...section ( 1 , [ 1 , 0x60 , params , ...Array ( params ). fill ( 0x7f ), 0x00 ]), 36 ...section ( 2 , [ 1 , 1 , f , 1 , f , 0x00 , 0x00 ]), 37 ...section ( 7 , [ 1 , 1 , f , 0x00 , 0x00 ]), 38]); 39return new WebAssembly . Instance ( new WebAssembly . Module ( module ), { f :{ f :fn } }). exports . f ; 40} 41 42/** One instance of the module, and the terminals made in it. */ 43export class Ghostty { 44readonly #x:Exports ; 45readonly #types:Record < string , TypeInfo >; 46/** Scratch space for small out-parameters, reused by every call. */ 47readonly #scratch:number ; 48 49private constructor ( exports :Exports ) { 50this . #x= exports ; 51const json = JSON . parse ( this . string ( exports . ghostty_type_json ())) as { 52types :Record < string , TypeInfo >; 53}; 54this . #types= json . types ; 55this . #scratch= exports . ghostty_wasm_alloc ( 1024 ); 56} 57 58static async load ( source :Response | BufferSource ) :Promise < Ghostty > { 59const { instance} = source instanceof Response 60 ?await WebAssembly . instantiateStreaming ( source , {}) 61 :await WebAssembly . instantiate ( source , {}); 62return new Ghostty ( instance . exports as unknown as Exports ); 63} 64 65get x () :Exports { 66return this . #x; 67} 68 69/** A view of the module's memory as it is now, since a call that allocates may grow it. */ 70view () :DataView { 71return new DataView ( this . #x. memory . buffer ); 72} 73 74bytes () :Uint8Array { 75return new Uint8Array ( this . #x. memory . buffer ); 76} 77 78string ( pointer :number , length ?:number ) :string { 79const bytes = this . bytes (); 80let end = pointer ; 81if ( length === undefined ) { while ( bytes [ end ]) end ++ ; } 82else end = pointer + length ; 83return new TextDecoder (). decode ( bytes . slice ( pointer , end )); 84} 85 86type ( name :string ) :TypeInfo { 87const type = this . #types[ name ]; 88if ( type === undefined ) throw new Error ( `libghostty-vt has no type ${ name } ` ); 89return type ; 90} 91 92value ( type :string , name :string ) :number { 93const value = this . type ( type ). values [ name ]; 94if ( value === undefined ) throw new Error ( ` ${ type } has no ${ name } ` ); 95return value ; 96} 97 98check ( result :number , what :string ) { 99if ( result !== SUCCESS ) throw new Error ( ` ${ what } : ${ RESULTS [ result ] ?? result } ` ); 100} 101 102/** Scratch space of `size` bytes at `at` within it, zeroed. */ 103scratch ( size :number , at = 0 ) :number { 104const pointer = this . #scratch+ at ; 105this . bytes (). fill ( 0 , pointer , pointer + size ); 106return pointer ; 107} 108 109/** A sized struct in scratch space: zeroed, with its `size` field set. */ 110sized ( name :string , at = 0 ) :number { 111const type = this . type ( name ); 112const pointer = this . scratch ( type . size , at ); 113this . view (). setUint32 ( pointer + type . fields . size . offset , type . size , true ); 114return pointer ; 115} 116 117field ( type :string , pointer :number , name :string ) :number { 118const { offset, size} = this . type ( type ). fields [ name ]; 119const view = this . view (); 120return size === 1 121 ?view . getUint8 ( pointer + offset ) 122 :size === 2 123 ?view . getUint16 ( pointer + offset , true ) 124 :view . getUint32 ( pointer + offset , true ); 125} 126 127/** Make something through an out-pointer, as the C API's constructors do. */ 128make ( construct :( out :number ) => number , what :string ) :number { 129const out = this . #x. ghostty_wasm_alloc_opaque (); 130try { 131this . check ( construct ( out ), what ); 132return this . #x. ghostty_wasm_take_opaque ( out ); 133} finally { 134this . #x. ghostty_wasm_free_opaque ( out ); 135} 136} 137 138/** Copy `data` into the module for the length of `use`. */ 139with < T > ( data :Uint8Array , use :( pointer :number , length :number ) => T ) :T { 140const pointer = this . #x. ghostty_wasm_alloc ( Math . max ( 1 , data . length )); 141try { 142this . bytes (). set ( data , pointer ); 143return use ( pointer , data . length ); 144} finally { 145this . #x. ghostty_wasm_free ( pointer , Math . max ( 1 , data . length )); 146} 147} 148 149/** What an allocating call returns through a pointer and a length, copied out and freed. */ 150allocated ( produce :( out :number , length :number ) => number , what :string ) :Uint8Array { 151const out = this . #x. ghostty_wasm_alloc_opaque (); 152const length = this . #x. ghostty_wasm_alloc ( 4 ); 153try { 154this . check ( produce ( out , length ), what ); 155const pointer = this . #x. ghostty_wasm_take_opaque ( out ); 156const size = this . view (). getUint32 ( length , true ); 157const copy = this . bytes (). slice ( pointer , pointer + size ); 158this . #x. ghostty_free ( 0 , pointer , size ); 159return copy ; 160} finally { 161this . #x. ghostty_wasm_free_opaque ( out ); 162this . #x. ghostty_wasm_free ( length , 4 ); 163} 164} 165 166/** A host function in the module's table, as a function pointer to set a callback option to. */ 167callback ( fn :( ...args :number []) => void , params :number ) :number { 168const table = this . #x. __indirect_function_table ; 169const index = table . grow ( 1 ); 170table . set ( index , hostFunction ( fn , params ) as never ); 171return index ; 172} 173 174terminal ( cols :number , rows :number ) :Terminal { 175const handle = this . make (( out ) => this . #x. ghostty_terminal_new ( 0 , out , cols , rows ), "terminal" ); 176// a snapshot carries unfinished sequences only if the terminal kept them from the start 177const limit = this . scratch ( 4 ); 178this . view (). setUint32 ( limit , CONTINUATION_BYTES , true ); 179this . check ( 180this . #x. ghostty_terminal_set ( 181handle , 182this . value ( "GhosttyTerminalOption" , "CONTINUATION_MAX_BYTES" ), 183limit , 184), 185"continuation" , 186); 187return new Terminal ( this , handle ); 188} 189 190/** A terminal as a snapshot left it. */ 191restore ( snapshot :Uint8Array ) :Terminal { 192// the decoder reads the snapshot where it lies, so it stays until decoding is done 193return this . with ( snapshot , ( pointer , length ) => { 194const decoder = this . make ( 195( out ) => this . #x. ghostty_snapshot_decoder_new_buf ( 0 , out , pointer , length ), 196"decoder" , 197); 198try { 199return new Terminal ( 200this , 201this . make (( out ) => this . #x. ghostty_snapshot_decoder_decode ( decoder , out ), "restore" ), 202); 203} finally { 204this . #x. ghostty_snapshot_decoder_free ( decoder ); 205} 206}); 207} 208} 209 210export type Rgb = readonly [ number , number , number ]; 211 212/** A run of cells in one style; colors are absent where the terminal's default applies. */ 213export type Run = { 214text :string ; 215fg ?:Rgb ; 216bg ?:Rgb ; 217bold ?:boolean ; 218italic ?:boolean ; 219faint ?:boolean ; 220/** 0 for none; otherwise single, double, curly, dotted or dashed. */ 221underline ?:number ; 222strikethrough ?:boolean ; 223invisible ?:boolean ; 224/** A double-width character, which takes two cells. */ 225wide ?:boolean ; 226}; 227 228export type Cursor = { x :number ; y :number ; style :"bar" | "block" | "underline" | "hollow" }; 229 230/** What changed since the last frame: the rows that did, by their place in the viewport, and the cursor. */ 231export type Frame = { 232cols :number ; 233rows :number ; 234changed :Map < number , Run []>; 235cursor :Cursor | null ; 236}; 237 238export type KeyInput = { 239code :string ; 240key :string ; 241shiftKey :boolean ; 242ctrlKey :boolean ; 243altKey :boolean ; 244metaKey :boolean ; 245repeat :boolean ; 246}; 247 248const CURSORS = [ "bar" , "block" , "underline" , "hollow" ] as const ; 249 250/** One terminal's state, in its module. */ 251export class Terminal { 252readonly #g:Ghostty ; 253readonly handle :number ; 254 #render:{ state :number ; rows :number ; cells :number } | null = null ; 255 #keys:{ encoder :number ; event :number } | null = null ; 256 257constructor ( ghostty :Ghostty , handle :number ) { 258this . #g= ghostty ; 259this . handle = handle ; 260} 261 262write ( data :Uint8Array ) { 263this . #g. with ( 264data , 265( pointer , length ) => this . #g. x . ghostty_terminal_vt_write ( this . handle , pointer , length ), 266); 267} 268 269resize ( cols :number , rows :number ) { 270this . #g. check ( this . #g. x . ghostty_terminal_resize ( this . handle , cols , rows , 0 , 0 ), "resize" ); 271} 272 273/** Send what the terminal answers the program with, such as its reply to a query, to `write`. */ 274answer ( write :( data :Uint8Array ) => void ) { 275const g = this . #g; 276const pointer = g . callback (( _terminal , _userdata , data , length ) => { 277write ( g . bytes (). slice ( data , data + length )); 278}, 4 ); 279g . check ( 280g . x . ghostty_terminal_set ( this . handle , g . value ( "GhosttyTerminalOption" , "WRITE_PTY" ), pointer ), 281"answer" , 282); 283} 284 285/** Everything needed to draw and carry on this terminal elsewhere, scrollback included. */ 286snapshot () :Uint8Array { 287return this . #g. allocated ( 288( out , length ) => this . #g. x . ghostty_snapshot_encode_alloc ( this . handle , 0 , out , length ), 289"snapshot" , 290); 291} 292 293/** The viewport as plain text, for tests and for the curious. */ 294text () :string { 295const g = this . #g; 296const options = g . sized ( "GhosttyFormatterTerminalOptions" ); 297const fields = g . type ( "GhosttyFormatterTerminalOptions" ). fields ; 298const view = g . view (); 299view . setUint32 ( options + fields . emit . offset , g . value ( "GhosttyFormatterFormat" , "PLAIN" ), true ); 300view . setUint8 ( options + fields . trim . offset , 1 ); 301const extra = options + fields . extra . offset ; 302view . setUint32 ( extra , g . type ( "GhosttyFormatterTerminalExtra" ). size , true ); 303const screen = extra + g . type ( "GhosttyFormatterTerminalExtra" ). fields . screen . offset ; 304view . setUint32 ( screen , g . type ( "GhosttyFormatterScreenExtra" ). size , true ); 305// passed by value, which wasm's C ABI passes as a pointer 306const formatter = g . make ( 307( out ) => g . x . ghostty_formatter_terminal_new ( 0 , out , this . handle , options ), 308"formatter" , 309); 310try { 311return new TextDecoder (). decode ( 312g . allocated ( 313( out , length ) => g . x . ghostty_formatter_format_alloc ( formatter , 0 , out , length ), 314"format" , 315), 316); 317} finally { 318g . x . ghostty_formatter_free ( formatter ); 319} 320} 321 322/** Scroll the viewport by `delta` rows, up when negative, or back to the bottom. */ 323scroll ( delta :number | "bottom" ) { 324const g = this . #g; 325const type = g . type ( "GhosttyTerminalScrollViewport" ); 326const behavior = g . scratch ( type . size ); 327const view = g . view (); 328view . setUint32 ( 329behavior + type . fields . tag . offset , 330g . value ( "GhosttyTerminalScrollViewportTag" , delta === "bottom" ?"BOTTOM" :"DELTA" ), 331true , 332); 333if ( delta !== "bottom" ) view . setInt32 ( behavior + type . fields . value . offset , delta , true ); 334g . x . ghostty_terminal_scroll_viewport ( this . handle , behavior ); 335} 336 337mode ( mode :number , ansi = false ) :boolean { 338const g = this . #g; 339const config = g . scratch ( 4 ); 340g . view (). setUint16 ( config , ( mode & 0x7fff ) | ( ansi ?0x8000 :0 ), true ); 341g . check ( 342g . x . ghostty_terminal_get ( this . handle , g . value ( "GhosttyTerminalData" , "MODE" ), config ), 343"mode" , 344); 345return g . field ( "GhosttyTerminalModeConfig" , config , "value" ) !== 0 ; 346} 347 348/** Set the colors the program sees as its defaults: foreground, background, cursor, and the 256-color palette. */ 349colors ({ foreground, background, cursor, palette} :{ 350foreground :Rgb ; 351background :Rgb ; 352cursor :Rgb ; 353palette :readonly Rgb []; 354}) { 355const g = this . #g; 356const set = ( option :string , colors :readonly Rgb []) => { 357const pointer = g . scratch ( colors . length * 3 ); 358g . bytes (). set ( colors . flat (), pointer ); 359g . check ( 360g . x . ghostty_terminal_set ( this . handle , g . value ( "GhosttyTerminalOption" , option ), pointer ), 361option , 362); 363}; 364set ( "COLOR_FOREGROUND" , [ foreground ]); 365set ( "COLOR_BACKGROUND" , [ background ]); 366set ( "COLOR_CURSOR" , [ cursor ]); 367const full = [ ...palette ]; 368const defaults = g . scratch ( 768 , 0 ); 369g . x . ghostty_color_palette_default ( defaults ); 370for ( let i = full . length ; i < 256 ; i ++ ) { 371const at = defaults + i * 3 ; 372const bytes = g . bytes (); 373full . push ([ bytes [ at ], bytes [ at + 1 ], bytes [ at + 2 ]]); 374} 375set ( "COLOR_PALETTE" , full ); 376} 377 378/** What has changed since the last frame, in runs of one style. */ 379frame () :Frame { 380const g = this . #g; 381const x = g . x ; 382this . #render??= { 383state :g . make (( out ) => x . ghostty_render_state_new ( 0 , out ), "render state" ), 384rows :g . make (( out ) => x . ghostty_render_state_row_iterator_new ( 0 , out ), "rows" ), 385cells :g . make (( out ) => x . ghostty_render_state_row_cells_new ( 0 , out ), "cells" ), 386}; 387const { state, rows, cells} = this . #render; 388g . check ( x . ghostty_render_state_update ( state , this . handle ), "render" ); 389const data = ( name :string ) => g . value ( "GhosttyRenderStateData" , name ); 390const rowData = ( name :string ) => g . value ( "GhosttyRenderStateRowData" , name ); 391const cellData = ( name :string ) => g . value ( "GhosttyRenderStateRowCellsData" , name ); 392 393const out = g . scratch ( 8 ); 394g . check ( x . ghostty_render_state_get ( state , data ( "COLS" ), out ), "cols" ); 395const cols = g . view (). getUint16 ( out , true ); 396g . check ( x . ghostty_render_state_get ( state , data ( "ROWS" ), out ), "rows" ); 397const height = g . view (). getUint16 ( out , true ); 398 399const colors = g . sized ( "GhosttyRenderStateColors" , 64 ); 400g . check ( x . ghostty_render_state_get ( state , data ( "COLORS" ), colors ), "colors" ); 401const rgbAt = ( pointer :number ) :Rgb => { 402const bytes = g . bytes (); 403return [ bytes [ pointer ], bytes [ pointer + 1 ], bytes [ pointer + 2 ]]; 404}; 405const colorFields = g . type ( "GhosttyRenderStateColors" ). fields ; 406const foreground = rgbAt ( colors + colorFields . foreground . offset ); 407const background = rgbAt ( colors + colorFields . background . offset ); 408 409const cursorStruct = g . sized ( "GhosttyRenderStateCursor" , 900 ); 410g . check ( x . ghostty_render_state_get ( state , data ( "CURSOR" ), cursorStruct ), "cursor" ); 411const cursorField = ( name :string ) => g . field ( "GhosttyRenderStateCursor" , cursorStruct , name ); 412const cursor :Cursor | null = cursorField ( "visible" ) && cursorField ( "viewport_has_value" ) 413 ?{ 414x :cursorField ( "viewport_x" ), 415y :cursorField ( "viewport_y" ), 416style :CURSORS [ cursorField ( "visual_style" )] ?? "block" , 417} 418 :null ; 419 420const changed = new Map < number , Run []>(); 421const handlePointer = g . scratch ( 4 , 16 ); 422g . view (). setUint32 ( handlePointer , rows , true ); 423g . check ( x . ghostty_render_state_get ( state , data ( "ROW_ITERATOR" ), handlePointer ), "row iterator" ); 424const cellsPointer = g . scratch ( 4 , 20 ); 425g . view (). setUint32 ( cellsPointer , cells , true ); 426const styleType = g . type ( "GhosttyStyle" ); 427const style = g . scratch ( styleType . size , 120 ); 428const length = g . scratch ( 4 , 24 ); 429const graphemes = x . ghostty_wasm_alloc ( 4 * 64 ); 430const rawCell = g . scratch ( 8 , 32 ); 431const wideOut = g . scratch ( 4 , 40 ); 432const rgb = g . scratch ( 4 , 44 ); 433const y = g . scratch ( 2 , 48 ); 434const wideTag = g . value ( "GhosttyCellData" , "WIDE" ); 435const WIDE = g . value ( "GhosttyCellWide" , "WIDE" ); 436const SPACER = [ 437g . value ( "GhosttyCellWide" , "SPACER_TAIL" ), 438g . value ( "GhosttyCellWide" , "SPACER_HEAD" ), 439]; 440const flag = ( name :string ) => g . view (). getUint8 ( style + styleType . fields [ name ]. offset ) !== 0 ; 441try { 442while ( x . ghostty_render_state_row_iterator_next_dirty ( rows , y )) { 443const row = g . view (). getUint16 ( y , true ); 444g . check ( x . ghostty_render_state_row_get ( rows , rowData ( "CELLS" ), cellsPointer ), "row cells" ); 445const runs :Run [] = []; 446let blank = 0 ; 447while ( x . ghostty_render_state_row_cells_next ( cells )) { 448x . ghostty_render_state_row_cells_get ( cells , cellData ( "RAW" ), rawCell ); 449// a cell is 64 bits, which a wasm32 call takes as a 64-bit integer 450const cell = g . view (). getBigUint64 ( rawCell , true ); 451( x . ghostty_cell_get as unknown as ( cell :bigint , data :number , out :number ) => number )( 452cell , 453wideTag , 454wideOut , 455); 456const wide = g . view (). getUint32 ( wideOut , true ); 457if ( SPACER . includes ( wide )) continue ; 458g . check ( 459x . ghostty_render_state_row_cells_get ( cells , cellData ( "GRAPHEMES_LEN" ), length ), 460"graphemes" , 461); 462const count = g . view (). getUint32 ( length , true ); 463let text = " " ; 464if ( count > 0 ) { 465if ( count <= 64 ) { 466x . ghostty_render_state_row_cells_get ( cells , cellData ( "GRAPHEMES_BUF" ), graphemes ); 467text = "" ; 468for ( let i = 0 ; i < count ; i ++ ) { 469text += String . fromCodePoint ( g . view (). getUint32 ( graphemes + i * 4 , true )); 470} 471} else text = "\uFFFD" ; 472} 473const run :Run = { text}; 474if ( wide === WIDE ) run . wide = true ; 475x . ghostty_render_state_row_cells_get ( cells , cellData ( "HAS_STYLING" ), length ); 476if ( g . view (). getUint8 ( length )) { 477g . view (). setUint32 ( style + styleType . fields . size . offset , styleType . size , true ); 478x . ghostty_render_state_row_cells_get ( cells , cellData ( "STYLE" ), style ); 479if ( 480x . ghostty_render_state_row_cells_get ( cells , cellData ( "FG_COLOR" ), rgb ) === SUCCESS 481) { 482run . fg = rgbAt ( rgb ); 483} 484if ( 485x . ghostty_render_state_row_cells_get ( cells , cellData ( "BG_COLOR" ), rgb ) === SUCCESS 486) { 487run . bg = rgbAt ( rgb ); 488} 489if ( flag ( "inverse" )) [ run . fg , run . bg ] = [ run . bg ?? background , run . fg ?? foreground ]; 490if ( flag ( "bold" )) run . bold = true ; 491if ( flag ( "italic" )) run . italic = true ; 492if ( flag ( "faint" )) run . faint = true ; 493if ( flag ( "strikethrough" )) run . strikethrough = true ; 494if ( flag ( "invisible" )) run . invisible = true ; 495const underline = g . view (). getInt32 ( style + styleType . fields . underline . offset , true ); 496if ( underline ) run . underline = underline ; 497} 498const plain = Object . keys ( run ). length === 1 ; 499const last = runs . at ( - 1 ); 500if ( last && ! run . wide && ! last . wide && sameStyle ( last , run )) last . text += run . text ; 501else runs . push ( run ); 502// trailing blanks in the default style are dropped, so copied text has no trailing spaces 503blank = plain && text === " " ?blank + 1 :0 ; 504} 505if ( blank > 0 ) { 506const last = runs . at ( - 1 ) ! ; 507last . text = last . text . slice ( 0 , last . text . length - blank ); 508if ( last . text === "" ) runs . pop (); 509} 510changed . set ( row , runs ); 511g . view (). setUint8 ( length , 0 ); 512x . ghostty_render_state_row_set ( 513rows , 514g . value ( "GhosttyRenderStateRowOption" , "DIRTY" ), 515length , 516); 517} 518} finally { 519x . ghostty_wasm_free ( graphemes , 4 * 64 ); 520} 521g . view (). setUint32 ( out , g . value ( "GhosttyRenderStateDirty" , "FALSE" ), true ); 522x . ghostty_render_state_set ( state , g . value ( "GhosttyRenderStateOption" , "DIRTY" ), out ); 523return { cols, rows :height , changed, cursor}; 524} 525 526/** The bytes a key press sends the program, as the terminal's modes say to encode it; `null` for none. */ 527key ( input :KeyInput , action :"press" | "release" = "press" ) :Uint8Array | null { 528const g = this . #g; 529const x = g . x ; 530this . #keys??= { 531encoder :g . make (( out ) => x . ghostty_key_encoder_new ( 0 , out ), "key encoder" ), 532event :g . make (( out ) => x . ghostty_key_event_new ( 0 , out ), "key event" ), 533}; 534const { encoder, event} = this . #keys; 535const name = input . code . replace ( / ^Key(?=[A-Z]$) / , "" ). replace ( / ([a-z])([A-Z0-9]) / g , "$1_$2" ) 536. toUpperCase (); 537const key = g . type ( "GhosttyKey" ). values [ name ] ?? g . value ( "GhosttyKey" , "UNIDENTIFIED" ); 538x . ghostty_key_event_set_action ( 539event , 540g . value ( 541"GhosttyKeyAction" , 542action === "release" ?"RELEASE" :input . repeat ?"REPEAT" :"PRESS" , 543), 544); 545x . ghostty_key_event_set_key ( event , key ); 546x . ghostty_key_event_set_mods ( 547event , 548( input . shiftKey ?MODS . shift :0 ) | ( input . ctrlKey ?MODS . ctrl :0 ) | 549( input . altKey ?MODS . alt :0 ) | 550( input . metaKey ?MODS . super :0 ), 551); 552// a key that types something says what, such as "a"; named keys, such as "Enter", type nothing of their own 553const typed = [ ...input . key ]. length === 1 ?input . key :"" ; 554const text = new TextEncoder (). encode ( typed ); 555const textPointer = x . ghostty_wasm_alloc ( Math . max ( 1 , text . length )); 556g . bytes (). set ( text , textPointer ); 557x . ghostty_key_event_set_utf8 ( event , textPointer , text . length ); 558x . ghostty_key_event_set_unshifted_codepoint ( 559event , 560typed ?typed . toLowerCase (). codePointAt ( 0 ) ! :0 , 561); 562x . ghostty_key_encoder_setopt_from_terminal ( encoder , this . handle ); 563const output = x . ghostty_wasm_alloc ( 128 ); 564const length = g . scratch ( 4 ); 565try { 566const result = x . ghostty_key_encoder_encode ( encoder , event , output , 128 , length ); 567const size = g . view (). getUint32 ( length , true ); 568return result === SUCCESS && size > 0 ?g . bytes (). slice ( output , output + size ) :null ; 569} finally { 570x . ghostty_wasm_free ( output , 128 ); 571x . ghostty_wasm_free ( textPointer , Math . max ( 1 , text . length )); 572} 573} 574 575/** Pasted text as the program should get it: bracketed if it asked for that. */ 576paste ( text :string ) :Uint8Array { 577const g = this . #g; 578const data = new TextEncoder (). encode ( text ); 579const capacity = data . length + 16 ; 580const output = g . x . ghostty_wasm_alloc ( capacity ); 581const length = g . scratch ( 4 ); 582try { 583return g . with ( data , ( pointer , size ) => { 584g . check ( 585g . x . ghostty_paste_encode ( 586pointer , 587size , 588this . mode ( BRACKETED_PASTE ) ?1 :0 , 589output , 590capacity , 591length , 592), 593"paste" , 594); 595return g . bytes (). slice ( output , output + g . view (). getUint32 ( length , true )); 596}); 597} finally { 598g . x . ghostty_wasm_free ( output , capacity ); 599} 600} 601 602free () { 603const x = this . #g. x ; 604if ( this . #render) { 605x . ghostty_render_state_row_cells_free ( this . #render. cells ); 606x . ghostty_render_state_row_iterator_free ( this . #render. rows ); 607x . ghostty_render_state_free ( this . #render. state ); 608} 609if ( this . #keys) { 610x . ghostty_key_event_free ( this . #keys. event ); 611x . ghostty_key_encoder_free ( this . #keys. encoder ); 612} 613x . ghostty_terminal_free ( this . handle ); 614} 615} 616 617const STYLE_KEYS = [ 618"fg" , 619"bg" , 620"bold" , 621"italic" , 622"faint" , 623"underline" , 624"strikethrough" , 625"invisible" , 626] as const ; 627 628function sameStyle ( a :Run , b :Run ) :boolean { 629return STYLE_KEYS . every (( key ) => { 630const [ p , q ] = [ a [ key ], b [ key ]]; 631return Array . isArray ( p ) && Array . isArray ( q ) ?p . every (( v , i ) => v === q [ i ]) :p === q ; 632}); 633}