diff --git a/lib/ghostty.ts b/lib/ghostty.ts index f079885..1df918f 100644 --- a/lib/ghostty.ts +++ b/lib/ghostty.ts @@ -31,9 +31,9 @@ export { type GhosttyCell, type GhosttyTerminalConfig, KeyEncoderOption, - type RGB, type RenderStateColors, type RenderStateCursor, + type RGB, }; /** @@ -55,7 +55,7 @@ export class Ghostty { createTerminal( cols: number = 80, rows: number = 24, - config?: GhosttyTerminalConfig + config?: GhosttyTerminalConfig, ): GhosttyTerminal { return new GhosttyTerminal(this.exports, this.memory, cols, rows, config); } @@ -145,7 +145,7 @@ export class Ghostty { const bytes = new Uint8Array( (wasmInstance.exports as GhosttyWasmExports).memory.buffer, ptr, - len + len, ); console.log('[ghostty-vt]', new TextDecoder().decode(bytes)); }, @@ -215,7 +215,7 @@ export class KeyEncoder { eventPtr, bufPtr, bufferSize, - writtenPtr + writtenPtr, ); if (encodeResult !== 0) { @@ -273,7 +273,7 @@ export class GhosttyTerminal { memory: WebAssembly.Memory, cols: number = 80, rows: number = 24, - config?: GhosttyTerminalConfig + config?: GhosttyTerminalConfig, ) { this.exports = exports; this.memory = memory; @@ -399,8 +399,11 @@ export class GhosttyTerminal { viewportX: this.exports.ghostty_render_state_get_cursor_x(this.handle), viewportY: this.exports.ghostty_render_state_get_cursor_y(this.handle), visible: this.exports.ghostty_render_state_get_cursor_visible(this.handle), - blinking: false, // TODO: Add blinking support - style: 'block', // TODO: Add style support + blinking: this.exports.ghostty_render_state_get_cursor_blinking(this.handle), + style: + (['block', 'bar', 'underline'] as const)[ + this.exports.ghostty_render_state_get_cursor_style(this.handle) + ] ?? 'block', }; } @@ -460,7 +463,7 @@ export class GhosttyTerminal { const count = this.exports.ghostty_render_state_get_viewport( this.handle, this.viewportBufferPtr, - totalCells + totalCells, ); if (count < 0) return this.cellPool; @@ -569,7 +572,7 @@ export class GhosttyTerminal { this.handle, offset, this.viewportBufferPtr, - this._cols + this._cols, ); if (count < 0) return null; @@ -629,7 +632,7 @@ export class GhosttyTerminal { row, col, bufPtr, - bufSize + bufSize, ); // 0 means no hyperlink at this position @@ -676,7 +679,7 @@ export class GhosttyTerminal { offset, col, bufPtr, - bufSize + bufSize, ); // 0 means no hyperlink at this position @@ -811,7 +814,7 @@ export class GhosttyTerminal { row, col, this.graphemeBufferPtr, - 16 + 16, ); if (count < 0) return null; @@ -849,7 +852,7 @@ export class GhosttyTerminal { offset, col, this.graphemeBufferPtr, - 16 + 16, ); if (count < 0) return null; diff --git a/lib/input-handler.ts b/lib/input-handler.ts index 83d6f3f..751fc13 100644 --- a/lib/input-handler.ts +++ b/lib/input-handler.ts @@ -13,8 +13,7 @@ * - Captures all keyboard input (preventDefault on everything) */ -import type { Ghostty } from './ghostty'; -import type { KeyEncoder } from './ghostty'; +import type { Ghostty, KeyEncoder } from './ghostty'; import type { IKeyEvent } from './interfaces'; import { Key, KeyAction, KeyEncoderOption, Mods } from './types'; @@ -231,7 +230,7 @@ export class InputHandler { getMode?: (mode: number) => boolean, onCopy?: () => boolean, inputElement?: HTMLElement, - mouseConfig?: MouseTrackingConfig + mouseConfig?: MouseTrackingConfig, ) { this.encoder = ghostty.createKeyEncoder(); this.container = container; @@ -384,9 +383,18 @@ export class InputHandler { } } - // Allow Ctrl+V and Cmd+V to trigger paste event (don't preventDefault) + // Ctrl+V / Cmd+V: emit \x16 to the PTY so apps that read it natively + // (e.g. opencode image paste via osascript) receive the signal, then let + // the browser paste event fire so handlePaste covers text content. if ((event.ctrlKey || event.metaKey) && event.code === 'KeyV') { - // Let the browser's native paste event fire + const encoded = this.encoder.encode({ + key: Key.V, + mods: event.ctrlKey ? Mods.CTRL : Mods.SUPER, + action: KeyAction.PRESS, + }); + if (encoded.length > 0) { + this.onDataCallback(new TextDecoder().decode(encoded)); + } return; } @@ -765,7 +773,7 @@ export class InputHandler { col: number, row: number, isRelease: boolean, - modifiers: number + modifiers: number, ): string { const btn = button + modifiers; const suffix = isRelease ? 'm' : 'M'; @@ -793,7 +801,7 @@ export class InputHandler { col: number, row: number, isRelease: boolean, - event: MouseEvent + event: MouseEvent, ): void { const modifiers = this.getMouseModifiers(event); diff --git a/lib/terminal.ts b/lib/terminal.ts index eeb7acd..04e0ea5 100644 --- a/lib/terminal.ts +++ b/lib/terminal.ts @@ -477,7 +477,7 @@ export class Terminal implements ITerminalCore { return this.copySelection(); }, this.textarea, - mouseConfig + mouseConfig, ); // Create selection manager (pass textarea for context menu positioning) @@ -485,7 +485,7 @@ export class Terminal implements ITerminalCore { this, this.renderer, this.wasmTerm, - this.textarea + this.textarea, ); // Connect selection manager to renderer @@ -845,7 +845,7 @@ export class Terminal implements ITerminalCore { * Returns true to prevent default handling */ public attachCustomKeyEventHandler( - customKeyEventHandler: (event: KeyboardEvent) => boolean + customKeyEventHandler: (event: KeyboardEvent) => boolean, ): void { this.customKeyEventHandler = customKeyEventHandler; // Update input handler if already created @@ -859,7 +859,7 @@ export class Terminal implements ITerminalCore { * Returns true to prevent default handling */ public attachCustomWheelEventHandler( - customWheelEventHandler?: (event: WheelEvent) => boolean + customWheelEventHandler?: (event: WheelEvent) => boolean, ): void { this.customWheelEventHandler = customWheelEventHandler; } @@ -1556,9 +1556,21 @@ export class Terminal implements ITerminalCore { const isAltScreen = this.wasmTerm?.isAlternateScreen() ?? false; if (isAltScreen) { - // Alternate screen: send arrow keys to the application - // Applications like vim handle scrolling internally - // Standard: ~3 arrow presses per wheel "click" + if (this.wasmTerm?.hasMouseTracking()) { + // App negotiated mouse tracking (e.g. vim `set mouse=a`): send SGR + // scroll sequence so the app scrolls its buffer, not the cursor. + const metrics = this.renderer?.getMetrics(); + const canvas = this.canvas; + if (metrics && canvas) { + const rect = canvas.getBoundingClientRect(); + const col = Math.max(1, Math.floor((e.clientX - rect.left) / metrics.width) + 1); + const row = Math.max(1, Math.floor((e.clientY - rect.top) / metrics.height) + 1); + const btn = e.deltaY < 0 ? 64 : 65; + this.dataEmitter.fire(`\x1b[<${btn};${col};${row}M`); + } + return; + } + // No mouse tracking: arrow-key fallback for apps like `less`. const direction = e.deltaY > 0 ? 'down' : 'up'; const count = Math.min(Math.abs(Math.round(e.deltaY / 33)), 5); // Cap at 5 diff --git a/lib/types.ts b/lib/types.ts index 4d6eefa..04ed6a8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -379,7 +379,7 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { parser: number, paramsPtr: number, subsPtr: number, - paramsLen: number + paramsLen: number, ): number; ghostty_sgr_next(parser: number, attrPtr: number): boolean; ghostty_sgr_attribute_tag(attrPtr: number): number; @@ -396,7 +396,7 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { eventPtr: number, bufPtr: number, bufLen: number, - writtenPtr: number + writtenPtr: number, ): number; // Key event @@ -421,6 +421,9 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { ghostty_render_state_get_cursor_x(terminal: TerminalHandle): number; ghostty_render_state_get_cursor_y(terminal: TerminalHandle): number; ghostty_render_state_get_cursor_visible(terminal: TerminalHandle): boolean; + /** Returns 0=block, 1=bar, 2=underline */ + ghostty_render_state_get_cursor_style(terminal: TerminalHandle): number; + ghostty_render_state_get_cursor_blinking(terminal: TerminalHandle): boolean; ghostty_render_state_get_bg_color(terminal: TerminalHandle): number; // 0xRRGGBB ghostty_render_state_get_fg_color(terminal: TerminalHandle): number; // 0xRRGGBB ghostty_render_state_is_row_dirty(terminal: TerminalHandle, row: number): boolean; @@ -428,14 +431,14 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { ghostty_render_state_get_viewport( terminal: TerminalHandle, bufPtr: number, - bufLen: number + bufLen: number, ): number; // Returns total cells written or -1 on error ghostty_render_state_get_grapheme( terminal: TerminalHandle, row: number, col: number, bufPtr: number, - bufLen: number + bufLen: number, ): number; // Returns count of codepoints or -1 on error // Terminal modes @@ -449,14 +452,14 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { terminal: TerminalHandle, offset: number, bufPtr: number, - bufLen: number + bufLen: number, ): number; // Returns cells written or -1 on error ghostty_terminal_get_scrollback_grapheme( terminal: TerminalHandle, offset: number, col: number, bufPtr: number, - bufLen: number + bufLen: number, ): number; // Returns codepoint count or -1 on error ghostty_terminal_is_row_wrapped(terminal: TerminalHandle, row: number): number; @@ -466,14 +469,14 @@ export interface GhosttyWasmExports extends WebAssembly.Exports { row: number, col: number, bufPtr: number, - bufLen: number + bufLen: number, ): number; // Returns bytes written, 0 if no hyperlink, -1 on error ghostty_terminal_get_scrollback_hyperlink_uri( terminal: TerminalHandle, offset: number, col: number, bufPtr: number, - bufLen: number + bufLen: number, ): number; // Returns bytes written, 0 if no hyperlink, -1 on error // Response API (for DSR and other terminal queries) diff --git a/patches/ghostty-wasm-api.patch b/patches/ghostty-wasm-api.patch index bd3feb9..4263997 100644 --- a/patches/ghostty-wasm-api.patch +++ b/patches/ghostty-wasm-api.patch @@ -157,6 +157,10 @@ index 000000000..c467102c3 +int ghostty_render_state_get_cursor_x(GhosttyTerminal term); +int ghostty_render_state_get_cursor_y(GhosttyTerminal term); +bool ghostty_render_state_get_cursor_visible(GhosttyTerminal term); ++/** Get cursor style: 0=block, 1=bar, 2=underline */ ++int ghostty_render_state_get_cursor_style(GhosttyTerminal term); ++/** Check if cursor is blinking */ ++bool ghostty_render_state_get_cursor_blinking(GhosttyTerminal term); + +/** Get default colors as 0xRRGGBB */ +uint32_t ghostty_render_state_get_bg_color(GhosttyTerminal term); @@ -340,6 +344,8 @@ index 03a883e20..1336676d7 100644 + @export(&c.render_state_get_cursor_x, .{ .name = "ghostty_render_state_get_cursor_x" }); + @export(&c.render_state_get_cursor_y, .{ .name = "ghostty_render_state_get_cursor_y" }); + @export(&c.render_state_get_cursor_visible, .{ .name = "ghostty_render_state_get_cursor_visible" }); ++ @export(&c.render_state_get_cursor_style, .{ .name = "ghostty_render_state_get_cursor_style" }); ++ @export(&c.render_state_get_cursor_blinking, .{ .name = "ghostty_render_state_get_cursor_blinking" }); + @export(&c.render_state_get_bg_color, .{ .name = "ghostty_render_state_get_bg_color" }); + @export(&c.render_state_get_fg_color, .{ .name = "ghostty_render_state_get_fg_color" }); + @export(&c.render_state_is_row_dirty, .{ .name = "ghostty_render_state_is_row_dirty" }); @@ -398,6 +404,8 @@ index bc92597f5..d0ee49c1b 100644 +pub const render_state_get_cursor_x = terminal.renderStateGetCursorX; +pub const render_state_get_cursor_y = terminal.renderStateGetCursorY; +pub const render_state_get_cursor_visible = terminal.renderStateGetCursorVisible; ++pub const render_state_get_cursor_style = terminal.renderStateGetCursorStyle; ++pub const render_state_get_cursor_blinking = terminal.renderStateGetCursorBlinking; +pub const render_state_get_bg_color = terminal.renderStateGetBgColor; +pub const render_state_get_fg_color = terminal.renderStateGetFgColor; +pub const render_state_is_row_dirty = terminal.renderStateIsRowDirty; @@ -991,6 +999,22 @@ index 000000000..73ae2e6fa + return wrapper.render_state.cursor.visible; +} + ++/// Get cursor style: 0=block, 1=bar, 2=underline ++pub fn renderStateGetCursorStyle(ptr: ?*anyopaque) callconv(.c) c_int { ++ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0)); ++ return switch (wrapper.terminal.screens.active.cursor.cursor_style) { ++ .bar => 1, ++ .underline => 2, ++ else => 0, ++ }; ++} ++ ++/// Check if cursor is blinking ++pub fn renderStateGetCursorBlinking(ptr: ?*anyopaque) callconv(.c) bool { ++ const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return false)); ++ return wrapper.terminal.modes.get(.cursor_blinking); ++} ++ +/// Get default background color as 0xRRGGBB +pub fn renderStateGetBgColor(ptr: ?*anyopaque) callconv(.c) u32 { + const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));