Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions lib/ghostty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export {
type GhosttyCell,
type GhosttyTerminalConfig,
KeyEncoderOption,
type RGB,
type RenderStateColors,
type RenderStateCursor,
type RGB,
};

/**
Expand All @@ -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);
}
Expand Down Expand Up @@ -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));
},
Expand Down Expand Up @@ -215,7 +215,7 @@ export class KeyEncoder {
eventPtr,
bufPtr,
bufferSize,
writtenPtr
writtenPtr,
);

if (encodeResult !== 0) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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',
};
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -569,7 +572,7 @@ export class GhosttyTerminal {
this.handle,
offset,
this.viewportBufferPtr,
this._cols
this._cols,
);

if (count < 0) return null;
Expand Down Expand Up @@ -629,7 +632,7 @@ export class GhosttyTerminal {
row,
col,
bufPtr,
bufSize
bufSize,
);

// 0 means no hyperlink at this position
Expand Down Expand Up @@ -676,7 +679,7 @@ export class GhosttyTerminal {
offset,
col,
bufPtr,
bufSize
bufSize,
);

// 0 means no hyperlink at this position
Expand Down Expand Up @@ -811,7 +814,7 @@ export class GhosttyTerminal {
row,
col,
this.graphemeBufferPtr,
16
16,
);

if (count < 0) return null;
Expand Down Expand Up @@ -849,7 +852,7 @@ export class GhosttyTerminal {
offset,
col,
this.graphemeBufferPtr,
16
16,
);

if (count < 0) return null;
Expand Down
22 changes: 15 additions & 7 deletions lib/input-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -793,7 +801,7 @@ export class InputHandler {
col: number,
row: number,
isRelease: boolean,
event: MouseEvent
event: MouseEvent,
): void {
const modifiers = this.getMouseModifiers(event);

Expand Down
26 changes: 19 additions & 7 deletions lib/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,15 +477,15 @@ export class Terminal implements ITerminalCore {
return this.copySelection();
},
this.textarea,
mouseConfig
mouseConfig,
);

// Create selection manager (pass textarea for context menu positioning)
this.selectionManager = new SelectionManager(
this,
this.renderer,
this.wasmTerm,
this.textarea
this.textarea,
);

// Connect selection manager to renderer
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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

Expand Down
19 changes: 11 additions & 8 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -396,7 +396,7 @@ export interface GhosttyWasmExports extends WebAssembly.Exports {
eventPtr: number,
bufPtr: number,
bufLen: number,
writtenPtr: number
writtenPtr: number,
): number;

// Key event
Expand All @@ -421,21 +421,24 @@ 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;
ghostty_render_state_mark_clean(terminal: TerminalHandle): void;
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
Expand All @@ -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;

Expand All @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions patches/ghostty-wasm-api.patch
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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" });
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down