From 7dd589824d4c9bda8265355718800cccaf7189a0 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:13:44 -0800 Subject: [PATCH] Add C API for keyboard copy mode selection --- src/Surface.zig | 40 ++++++++++++++++++++++++++++++++++++++++ src/apprt/embedded.zig | 16 ++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index d1adbbf7d8b..69ab672c853 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2010,6 +2010,46 @@ pub fn hasSelection(self: *const Surface) bool { return self.io.terminal.screens.active.selection != null; } +/// Start a selection anchored at the active cursor position. +pub fn selectCursorCell(self: *Surface) !bool { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + const screen: *terminal.Screen = self.io.terminal.screens.active; + const pin = pin: { + // pointFromPin(.viewport, ...) can return points beyond the visible rows + // when the viewport includes additional written history; only anchor to + // the live cursor if it is truly visible in the viewport. + if (screen.pages.pointFromPin(.viewport, screen.cursor.page_pin.*)) |pt| { + if (pt.viewport.y < @as(u32, screen.pages.rows)) { + break :pin screen.cursor.page_pin.*; + } + } + + // If we've scrolled away from the live cursor, start at the viewport origin + // so copy mode can select from the currently visible history. + break :pin screen.pages.pin(.{ .viewport = .{} }) orelse return false; + }; + // Entering keyboard copy mode should not clobber clipboard contents. + try screen.select(terminal.Selection.init(pin, pin, false)); + screen.dirty.selection = true; + try self.queueRender(); + return true; +} + +/// Clear the active selection, if any. +pub fn clearSelection(self: *Surface) !bool { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + const screen: *terminal.Screen = self.io.terminal.screens.active; + if (screen.selection == null) return false; + try self.setSelection(null); + screen.dirty.selection = true; + try self.queueRender(); + return true; +} + /// Returns the selected text. This is allocated. pub fn selectionString(self: *Surface, alloc: Allocator) !?[:0]const u8 { self.renderer_state.mutex.lock(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index e73e5ad127a..0c220ef1f26 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1584,6 +1584,22 @@ pub const CAPI = struct { return surface.core_surface.hasSelection(); } + /// Start a selection at the active cursor cell. + export fn ghostty_surface_select_cursor_cell(surface: *Surface) bool { + return surface.core_surface.selectCursorCell() catch |err| { + log.warn("error selecting cursor cell err={}", .{err}); + return false; + }; + } + + /// Clear the active selection. + export fn ghostty_surface_clear_selection(surface: *Surface) bool { + return surface.core_surface.clearSelection() catch |err| { + log.warn("error clearing selection err={}", .{err}); + return false; + }; + } + /// Same as ghostty_surface_read_text but reads from the user selection, /// if any. export fn ghostty_surface_read_selection(