diff --git a/include/ghostty.h b/include/ghostty.h index 246fb9ed3..6316133bb 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -597,6 +597,7 @@ typedef enum { GHOSTTY_ACTION_COLOR_CHANGE, GHOSTTY_ACTION_RELOAD_CONFIG, GHOSTTY_ACTION_CONFIG_CHANGE, + GHOSTTY_ACTION_REPORT_CURSOR_POSITION, } ghostty_action_tag_e; typedef union { diff --git a/src/Surface.zig b/src/Surface.zig index 5a1d8c01d..0feb7a891 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -71,6 +71,10 @@ renderer_thread: renderer.Thread, /// The actual thread renderer_thr: std.Thread, +/// Cursor state. Updated at most once per frame so it may not be accurate 100% +/// of the time. +cursor: Cursor, + /// Mouse state. mouse: Mouse, @@ -150,6 +154,37 @@ pub const InputEffect = enum { closed, }; +/// Cursor state for the surface. +const Cursor = struct { + x: terminal.size.CellCountInt = 0, + y: terminal.size.CellCountInt = 0, + + pub fn reportCursorPosition(self: *Cursor, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt) void { + const surface: *Surface = @alignCast(@fieldParentPtr("cursor", self)); + + // Defer updating the stored cursor position until after the apprt has been + // informed of the new position. + defer { + self.x = x; + self.y = y; + } + + surface.rt_app.performAction( + .{ .surface = surface }, + .report_cursor_position, + .{ + .x = x, + .y = y, + }, + ) catch |err| { + log.warn( + "failed to notify surface of cursor position err={}", + .{err}, + ); + }; + } +}; + /// Mouse state for the surface. const Mouse = struct { /// The last tracked mouse button state by button. @@ -496,6 +531,7 @@ pub fn init( .terminal = &self.io.terminal, }, .renderer_thr = undefined, + .cursor = .{}, .mouse = .{}, .keyboard = .{}, .io = undefined, @@ -943,6 +979,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), + + .report_cursor_position => |v| self.cursor.reportCursorPosition(v.x, v.y), } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index fe2039e52..5f8b2d2cc 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -226,6 +226,10 @@ pub const Action = union(Key) { /// for changes. config_change: ConfigChange, + /// Report the location of the cursor. This will happen at most once per + /// frame, and only if the cursor has moved since the last update. + report_cursor_position: CursorPositionReport, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -266,6 +270,7 @@ pub const Action = union(Key) { color_change, reload_config, config_change, + report_cursor_position, }; /// Sync with: ghostty_action_u @@ -549,3 +554,9 @@ pub const ConfigChange = struct { }; } }; + +/// Used to report the location of the cursor. +pub const CursorPositionReport = extern struct { + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, +}; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 686a70ddb..318cdc5a9 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -238,6 +238,7 @@ pub const App = struct { .pwd, .config_change, .toggle_maximize, + .report_cursor_position, => log.info("unimplemented action={}", .{action}), } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 63ba0a692..67f112712 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -535,6 +535,7 @@ pub fn performAction( .toggle_split_zoom => self.toggleSplitZoom(target), .toggle_window_decorations => self.toggleWindowDecorations(target), .quit_timer => self.quitTimer(value), + .report_cursor_position => self.reportCursorPosition(target, value), // Unimplemented .close_all_windows, @@ -1898,6 +1899,17 @@ pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) v c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0); } +fn reportCursorPosition( + _: *const App, + target: apprt.Target, + value: apprt.action.CursorPositionReport, +) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.reportCursorPosition(value), + } +} + fn isValidAppId(app_id: [:0]const u8) bool { if (app_id.len > 255 or app_id.len == 0) return false; if (app_id[0] == '.') return false; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 61866dcec..00257ac49 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2189,3 +2189,8 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { } return false; } + +pub fn reportCursorPosition(self: *Surface, position: apprt.action.CursorPositionReport) void { + _ = self; + log.debug("cursor position: {d}×{d}", .{ position.x, position.y }); +} diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index f3fd71432..7e6d6c3f4 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -81,6 +81,12 @@ pub const Message = union(enum) { /// The terminal has reported a change in the working directory. pwd_change: WriteReq, + /// Report cursor position + report_cursor_position: struct { + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + }, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 45d8f84c2..b439bbb53 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -25,6 +25,7 @@ const graphics = macos.graphics; const fgMode = @import("cell.zig").fgMode; const isCovering = @import("cell.zig").isCovering; const shadertoy = @import("shadertoy.zig"); +const CursorPosition = @import("cursor_position.zig").CursorPosition; const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; @@ -161,6 +162,9 @@ health: std.atomic.Value(Health) = .{ .raw = .healthy }, /// Our GPU state gpu_state: GPUState, +/// The cursor position as of the last frame update +cursor_position: CursorPosition(Metal) = .{}, + /// State we need for the GPU that is shared between all frames. pub const GPUState = struct { // The count of buffers we use for double/triple buffering. If @@ -991,6 +995,10 @@ pub fn updateFrame( cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, viewport_pin: terminal.Pin, + cursor_position: struct { + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + }, /// If true, rebuild the full screen. full_rebuild: bool, @@ -1154,6 +1162,10 @@ pub fn updateFrame( .color_palette = state.terminal.color_palette.colors, .viewport_pin = viewport_pin, .full_rebuild = full_rebuild, + .cursor_position = .{ + .x = state.terminal.screen.cursor.x, + .y = state.terminal.screen.cursor.y, + }, }; }; defer { @@ -1240,6 +1252,8 @@ pub fn updateFrame( } } } + + self.cursor_position.update(critical.cursor_position.x, critical.cursor_position.y); } /// Draw the frame to the screen. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e5dec6b2b..33abfe4fe 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -30,6 +30,7 @@ const custom = @import("opengl/custom.zig"); const Image = gl_image.Image; const ImageMap = gl_image.ImageMap; const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement); +const CursorPosition = @import("cursor_position.zig").CursorPosition; const log = std.log.scoped(.grid); @@ -146,6 +147,9 @@ image_bg_end: u32 = 0, image_text_end: u32 = 0, image_virtual: bool = false, +/// The cursor position as of the last frame update. +cursor_position: CursorPosition(OpenGL) = .{}, + /// Deferred OpenGL operation to update the screen size. const SetScreenSize = struct { size: renderer.Size, @@ -700,6 +704,10 @@ pub fn updateFrame( screen_type: terminal.ScreenType, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, + cursor_position: struct { + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + }, cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, }; @@ -861,6 +869,10 @@ pub fn updateFrame( .screen_type = state.terminal.active_screen, .mouse = state.mouse, .preedit = preedit, + .cursor_position = .{ + .x = state.terminal.screen.cursor.x, + .y = state.terminal.screen.cursor.y, + }, .cursor_style = cursor_style, .color_palette = state.terminal.color_palette.colors, }; @@ -893,6 +905,8 @@ pub fn updateFrame( // CoreText this triggers off-thread cleanup logic. self.font_shaper.endFrame(); } + + self.cursor_position.update(critical.cursor_position.x, critical.cursor_position.y); } /// This goes through the Kitty graphic placements and accumulates the diff --git a/src/renderer/cursor_position.zig b/src/renderer/cursor_position.zig new file mode 100644 index 000000000..88f84b848 --- /dev/null +++ b/src/renderer/cursor_position.zig @@ -0,0 +1,31 @@ +const terminal = @import("../terminal/main.zig"); + +pub fn CursorPosition(comptime T: type) type { + return struct { + x: terminal.size.CellCountInt = 0, + y: terminal.size.CellCountInt = 0, + + pub fn update( + self: *CursorPosition(T), + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + ) void { + const renderer: *T = @alignCast(@fieldParentPtr("cursor_position", self)); + + if (self.x != x or self.y != y) { + _ = renderer.surface_mailbox.push( + .{ + .report_cursor_position = .{ + .x = x, + .y = y, + }, + }, + .{ .instant = {} }, + ); + } + + self.x = x; + self.y = y; + } + }; +}