diff --git a/src/Surface.zig b/src/Surface.zig index ba657181f..3e957844b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -525,6 +525,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { try self.rt_surface.setTitle(slice); }, + .set_cursor_shape => |shape| { + log.debug("changing cursor shape: {}", .{shape}); + try self.rt_surface.setCursorShape(shape); + }, + .cell_size => |size| try self.setCellSize(size), .clipboard_read => |kind| try self.clipboardRead(kind), diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 47b37ec9a..36bafb54b 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -15,6 +15,7 @@ const objc = @import("objc"); const input = @import("../input.zig"); const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); +const terminal = @import("../terminal/main.zig"); const Renderer = renderer.Renderer; const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); @@ -508,6 +509,12 @@ pub const Surface = struct { self.window.setTitle(slice.ptr); } + /// Set the shape of the cursor. + pub fn setCursorShape(self: *Surface, shape: terminal.CursorShape) !void { + _ = self; + _ = shape; + } + /// Read the clipboard. The windowing system is responsible for allocating /// a buffer as necessary. This should be a stable pointer until the next /// time getClipboardString is called. diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 67b4247e8..a0ba1a54d 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -2,6 +2,7 @@ const App = @import("../App.zig"); const Surface = @import("../Surface.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); +const terminal = @import("../terminal/main.zig"); const Config = @import("../config.zig").Config; /// The message types that can be sent to a single surface. @@ -16,6 +17,9 @@ pub const Message = union(enum) { /// of any length set_title: [256]u8, + /// Set the cursor shape. + set_cursor_shape: terminal.CursorShape, + /// Change the cell size. cell_size: renderer.CellSize, diff --git a/src/terminal/cursor_shape.zig b/src/terminal/cursor_shape.zig new file mode 100644 index 000000000..021ee74d4 --- /dev/null +++ b/src/terminal/cursor_shape.zig @@ -0,0 +1,113 @@ +const std = @import("std"); + +/// The possible cursor shapes. Not all app runtimes support these shapes. +/// The shapes are always based on the W3C supported cursor styles so we +/// can have a cross platform list. +pub const CursorShape = enum(c_int) { + default, + context_menu, + help, + pointer, + progress, + wait, + cell, + crosshair, + text, + vertical_text, + alias, + copy, + move, + no_drop, + not_allowed, + grab, + grabbing, + all_scroll, + col_resize, + row_resize, + n_resize, + e_resize, + s_resize, + w_resize, + ne_resize, + nw_resize, + se_resize, + sw_resize, + ew_resize, + ns_resize, + nesw_resize, + nwse_resize, + zoom_in, + zoom_out, + + /// Build cursor shape from string or null if its unknown. + pub fn fromString(v: []const u8) ?CursorShape { + return string_map.get(v); + } +}; + +const string_map = std.ComptimeStringMap(CursorShape, .{ + // W3C + .{ "default", .default }, + .{ "context-menu", .context_menu }, + .{ "help", .help }, + .{ "pointer", .pointer }, + .{ "progress", .progress }, + .{ "wait", .wait }, + .{ "cell", .cell }, + .{ "crosshair", .crosshair }, + .{ "text", .text }, + .{ "vertical-text", .vertical_text }, + .{ "alias", .alias }, + .{ "copy", .copy }, + .{ "move", .move }, + .{ "no-drop", .no_drop }, + .{ "not-allowed", .not_allowed }, + .{ "grab", .grab }, + .{ "grabbing", .grabbing }, + .{ "all-scroll", .all_scroll }, + .{ "col-resize", .col_resize }, + .{ "row-resize", .row_resize }, + .{ "n-resize", .n_resize }, + .{ "e-resize", .e_resize }, + .{ "s-resize", .s_resize }, + .{ "w-resize", .w_resize }, + .{ "ne-resize", .ne_resize }, + .{ "nw-resize", .nw_resize }, + .{ "se-resize", .se_resize }, + .{ "sw-resize", .sw_resize }, + .{ "ew-resize", .ew_resize }, + .{ "ns-resize", .ns_resize }, + .{ "nesw-resize", .nesw_resize }, + .{ "nwse-resize", .nwse_resize }, + .{ "zoom-in", .zoom_in }, + .{ "zoom-out", .zoom_out }, + + // xterm/foot + .{ "left_ptr", .default }, + .{ "question_arrow", .help }, + .{ "hand", .pointer }, + .{ "left_ptr_watch", .progress }, + .{ "watch", .wait }, + .{ "cross", .crosshair }, + .{ "xterm", .text }, + .{ "dnd-link", .alias }, + .{ "dnd-copy", .copy }, + .{ "dnd-move", .move }, + .{ "dnd-no-drop", .no_drop }, + .{ "crossed_circle", .not_allowed }, + .{ "hand1", .grab }, + .{ "right_side", .e_resize }, + .{ "top_side", .n_resize }, + .{ "top_right_corner", .ne_resize }, + .{ "top_left_corner", .nw_resize }, + .{ "bottom_side", .s_resize }, + .{ "bottom_right_corner", .se_resize }, + .{ "bottom_left_corner", .sw_resize }, + .{ "left_side", .w_resize }, + .{ "fleur", .all_scroll }, +}); + +test "cursor shape from string" { + const testing = std.testing; + try testing.expectEqual(CursorShape.default, CursorShape.fromString("default").?); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index e1a6ee439..32b7d6898 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -15,6 +15,7 @@ pub const parse_table = @import("parse_table.zig"); pub const Charset = charsets.Charset; pub const CharsetSlot = charsets.Slots; pub const CharsetActiveSlot = charsets.ActiveSlot; +pub const CursorShape = @import("cursor_shape.zig").CursorShape; pub const Terminal = @import("Terminal.zig"); pub const Parser = @import("Parser.zig"); pub const Selection = @import("Selection.zig"); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 04d2b9aef..a7c4834da 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -9,6 +9,7 @@ const modes = @import("modes.zig"); const osc = @import("osc.zig"); const sgr = @import("sgr.zig"); const trace = @import("tracy").trace; +const CursorShape = @import("cursor_shape.zig").CursorShape; const log = std.log.scoped(.stream); @@ -849,6 +850,17 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .pointer_cursor => |v| { + if (@hasDecl(T, "setCursorShape")) { + const shape = CursorShape.fromString(v.value) orelse { + log.warn("unknown cursor shape: {s}", .{v.value}); + return; + }; + + try self.handler.setCursorShape(shape); + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + else => if (@hasDecl(T, "oscUnimplemented")) try self.handler.oscUnimplemented(cmd) else diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index aa4627f35..4365c3a6a 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1682,6 +1682,15 @@ const StreamHandler = struct { }, .{ .forever = {} }); } + pub fn setCursorShape( + self: *StreamHandler, + shape: terminal.CursorShape, + ) !void { + _ = self.ev.surface_mailbox.push(.{ + .set_cursor_shape = shape, + }, .{ .forever = {} }); + } + pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void { // Note: we ignore the "kind" field and always use the standard clipboard. // iTerm also appears to do this but other terminals seem to only allow