mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #454 from mitchellh/pointer-cursor
OSC 22: Set Mouse Shape
This commit is contained in:
@ -70,6 +70,43 @@ typedef enum {
|
||||
GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN,
|
||||
} ghostty_input_mouse_momentum_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_SHAPE_DEFAULT,
|
||||
GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU,
|
||||
GHOSTTY_MOUSE_SHAPE_HELP,
|
||||
GHOSTTY_MOUSE_SHAPE_POINTER,
|
||||
GHOSTTY_MOUSE_SHAPE_PROGRESS,
|
||||
GHOSTTY_MOUSE_SHAPE_WAIT,
|
||||
GHOSTTY_MOUSE_SHAPE_CELL,
|
||||
GHOSTTY_MOUSE_SHAPE_CROSSHAIR,
|
||||
GHOSTTY_MOUSE_SHAPE_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT,
|
||||
GHOSTTY_MOUSE_SHAPE_ALIAS,
|
||||
GHOSTTY_MOUSE_SHAPE_COPY,
|
||||
GHOSTTY_MOUSE_SHAPE_MOVE,
|
||||
GHOSTTY_MOUSE_SHAPE_NO_DROP,
|
||||
GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED,
|
||||
GHOSTTY_MOUSE_SHAPE_GRAB,
|
||||
GHOSTTY_MOUSE_SHAPE_GRABBING,
|
||||
GHOSTTY_MOUSE_SHAPE_ALL_SCROLL,
|
||||
GHOSTTY_MOUSE_SHAPE_COL_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ROW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_N_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_E_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_S_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_W_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_SW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_EW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NS_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NESW_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_IN,
|
||||
GHOSTTY_MOUSE_SHAPE_ZOOM_OUT,
|
||||
} ghostty_mouse_shape_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE,
|
||||
GHOSTTY_NON_NATIVE_FULLSCREEN_TRUE,
|
||||
@ -264,6 +301,7 @@ typedef struct {
|
||||
typedef void (*ghostty_runtime_wakeup_cb)(void *);
|
||||
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *);
|
||||
typedef void (*ghostty_runtime_set_title_cb)(void *, const char *);
|
||||
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void *, ghostty_mouse_shape_e);
|
||||
typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e);
|
||||
typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s);
|
||||
@ -281,6 +319,7 @@ typedef struct {
|
||||
ghostty_runtime_wakeup_cb wakeup_cb;
|
||||
ghostty_runtime_reload_config_cb reload_config_cb;
|
||||
ghostty_runtime_set_title_cb set_title_cb;
|
||||
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
|
||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||
ghostty_runtime_write_clipboard_cb write_clipboard_cb;
|
||||
ghostty_runtime_new_split_cb new_split_cb;
|
||||
|
@ -72,6 +72,7 @@ extension Ghostty {
|
||||
wakeup_cb: { userdata in AppState.wakeup(userdata) },
|
||||
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
|
||||
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
|
||||
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
|
||||
read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) },
|
||||
write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) },
|
||||
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
|
||||
@ -333,6 +334,11 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {
|
||||
let surfaceView = Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
|
||||
surfaceView.setCursorShape(shape)
|
||||
}
|
||||
|
||||
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {
|
||||
guard let surface = self.surfaceUserdata(from: userdata) else { return }
|
||||
NotificationCenter.default.post(
|
||||
|
@ -195,7 +195,9 @@ extension Ghostty {
|
||||
private(set) var surface: ghostty_surface_t?
|
||||
var error: Error? = nil
|
||||
|
||||
private var markedText: NSMutableAttributedString;
|
||||
private var markedText: NSMutableAttributedString
|
||||
private var mouseEntered: Bool = false
|
||||
private var cursor: NSCursor = .arrow
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
@ -265,6 +267,64 @@ extension Ghostty {
|
||||
ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height))
|
||||
}
|
||||
|
||||
func setCursorShape(_ shape: ghostty_mouse_shape_e) {
|
||||
switch (shape) {
|
||||
case GHOSTTY_MOUSE_SHAPE_DEFAULT:
|
||||
cursor = .arrow
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||
cursor = .contextualMenu
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_TEXT:
|
||||
cursor = .iBeam
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||
cursor = .crosshair
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_GRAB:
|
||||
cursor = .openHand
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_GRABBING:
|
||||
cursor = .closedHand
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_POINTER:
|
||||
cursor = .pointingHand
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_W_RESIZE:
|
||||
cursor = .resizeLeft
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_E_RESIZE:
|
||||
cursor = .resizeRight
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_N_RESIZE:
|
||||
cursor = .resizeUp
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_S_RESIZE:
|
||||
cursor = .resizeDown
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_NS_RESIZE:
|
||||
cursor = .resizeUpDown
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_EW_RESIZE:
|
||||
cursor = .resizeLeftRight
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT:
|
||||
cursor = .iBeamCursorForVerticalLayout
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED:
|
||||
cursor = .operationNotAllowed
|
||||
|
||||
default:
|
||||
// We ignore unknown shapes.
|
||||
return
|
||||
}
|
||||
|
||||
// Set our cursor immediately if our mouse is over our window
|
||||
if (mouseEntered) {
|
||||
cursor.set()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
guard let window = self.window else { return }
|
||||
guard let surface = self.surface else { return }
|
||||
@ -370,6 +430,14 @@ extension Ghostty {
|
||||
self.mouseMoved(with: event)
|
||||
}
|
||||
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
mouseEntered = true
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
mouseEntered = false
|
||||
}
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
@ -413,6 +481,10 @@ extension Ghostty {
|
||||
ghostty_surface_mouse_scroll(surface, x, y, mods)
|
||||
}
|
||||
|
||||
override func cursorUpdate(with event: NSEvent) {
|
||||
cursor.set()
|
||||
}
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
||||
keyAction(action, event: event)
|
||||
|
@ -525,6 +525,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
try self.rt_surface.setTitle(slice);
|
||||
},
|
||||
|
||||
.set_mouse_shape => |shape| {
|
||||
log.debug("changing mouse shape: {}", .{shape});
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
},
|
||||
|
||||
.cell_size => |size| try self.setCellSize(size),
|
||||
|
||||
.clipboard_read => |kind| try self.clipboardRead(kind),
|
||||
|
@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
|
||||
const objc = @import("objc");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const input = @import("../input.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
@ -48,6 +49,9 @@ pub const App = struct {
|
||||
/// Called to set the title of the window.
|
||||
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
|
||||
|
||||
/// Called to set the cursor shape.
|
||||
set_mouse_shape: *const fn (SurfaceUD, terminal.MouseShape) callconv(.C) void,
|
||||
|
||||
/// Read the clipboard value. The return value must be preserved
|
||||
/// by the host until the next call. If there is no valid clipboard
|
||||
/// value then this should return null.
|
||||
@ -310,6 +314,13 @@ pub const Surface = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
self.app.opts.set_mouse_shape(
|
||||
self.opts.userdata,
|
||||
shape,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
|
@ -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");
|
||||
@ -275,7 +276,7 @@ pub const Surface = struct {
|
||||
window: glfw.Window,
|
||||
|
||||
/// The glfw mouse cursor handle.
|
||||
cursor: glfw.Cursor,
|
||||
cursor: ?glfw.Cursor,
|
||||
|
||||
/// The app we're part of
|
||||
app: *App,
|
||||
@ -335,16 +336,6 @@ pub const Surface = struct {
|
||||
nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id);
|
||||
}
|
||||
|
||||
// Create the cursor
|
||||
const cursor = glfw.Cursor.createStandard(.ibeam) orelse return glfw.mustGetErrorCode();
|
||||
errdefer cursor.destroy();
|
||||
if ((comptime !builtin.target.isDarwin()) or internal_os.macosVersionAtLeast(13, 0, 0)) {
|
||||
// We only set our cursor if we're NOT on Mac, or if we are then the
|
||||
// macOS version is >= 13 (Ventura). On prior versions, glfw crashes
|
||||
// since we use a tab group.
|
||||
win.setCursor(cursor);
|
||||
}
|
||||
|
||||
// Set our callbacks
|
||||
win.setUserPointer(&self.core_surface);
|
||||
win.setSizeCallback(sizeCallback);
|
||||
@ -360,11 +351,14 @@ pub const Surface = struct {
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.window = win,
|
||||
.cursor = cursor,
|
||||
.cursor = null,
|
||||
.core_surface = undefined,
|
||||
};
|
||||
errdefer self.* = undefined;
|
||||
|
||||
// Initialize our cursor
|
||||
try self.setMouseShape(.text);
|
||||
|
||||
// Add ourselves to the list of surfaces on the app.
|
||||
try app.app.addSurface(self);
|
||||
errdefer app.app.deleteSurface(self);
|
||||
@ -425,7 +419,10 @@ pub const Surface = struct {
|
||||
// We can now safely destroy our windows. We have to do this BEFORE
|
||||
// setting up the new focused window below.
|
||||
self.window.destroy();
|
||||
self.cursor.destroy();
|
||||
if (self.cursor) |c| {
|
||||
c.destroy();
|
||||
self.cursor = null;
|
||||
}
|
||||
|
||||
// If we have a tabgroup set, we want to manually focus the next window.
|
||||
// We should NOT have to do this usually, see the comments above.
|
||||
@ -508,6 +505,43 @@ pub const Surface = struct {
|
||||
self.window.setTitle(slice.ptr);
|
||||
}
|
||||
|
||||
/// Set the shape of the cursor.
|
||||
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
if ((comptime builtin.target.isDarwin()) and
|
||||
!internal_os.macosVersionAtLeast(13, 0, 0))
|
||||
{
|
||||
// We only set our cursor if we're NOT on Mac, or if we are then the
|
||||
// macOS version is >= 13 (Ventura). On prior versions, glfw crashes
|
||||
// since we use a tab group.
|
||||
return;
|
||||
}
|
||||
|
||||
const new = glfw.Cursor.createStandard(switch (shape) {
|
||||
.default => .arrow,
|
||||
.text => .ibeam,
|
||||
.crosshair => .crosshair,
|
||||
.pointer => .pointing_hand,
|
||||
.ew_resize => .resize_ew,
|
||||
.ns_resize => .resize_ns,
|
||||
.nwse_resize => .resize_nwse,
|
||||
.nesw_resize => .resize_nesw,
|
||||
.all_scroll => .resize_all,
|
||||
.not_allowed => .not_allowed,
|
||||
else => return, // unsupported, ignore
|
||||
}) orelse {
|
||||
const err = glfw.mustGetErrorCode();
|
||||
log.warn("error creating cursor: {}", .{err});
|
||||
return;
|
||||
};
|
||||
errdefer new.destroy();
|
||||
|
||||
// Set our cursor before we destroy the old one
|
||||
self.window.setCursor(new);
|
||||
|
||||
if (self.cursor) |c| c.destroy();
|
||||
self.cursor = new;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -8,6 +8,7 @@ const glfw = @import("glfw");
|
||||
const apprt = @import("../apprt.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const input = @import("../input.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
@ -44,9 +45,6 @@ pub const App = struct {
|
||||
app: *c.GtkApplication,
|
||||
ctx: *c.GMainContext,
|
||||
|
||||
cursor_default: *c.GdkCursor,
|
||||
cursor_ibeam: *c.GdkCursor,
|
||||
|
||||
/// This is set to false when the main loop should exit.
|
||||
running: bool = true,
|
||||
|
||||
@ -125,19 +123,11 @@ pub const App = struct {
|
||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||
c.g_application_activate(gapp);
|
||||
|
||||
// Get our cursors
|
||||
const cursor_default = c.gdk_cursor_new_from_name("default", null).?;
|
||||
errdefer c.g_object_unref(cursor_default);
|
||||
const cursor_ibeam = c.gdk_cursor_new_from_name("text", cursor_default).?;
|
||||
errdefer c.g_object_unref(cursor_ibeam);
|
||||
|
||||
return .{
|
||||
.core_app = core_app,
|
||||
.app = app,
|
||||
.config = config,
|
||||
.ctx = ctx,
|
||||
.cursor_default = cursor_default,
|
||||
.cursor_ibeam = cursor_ibeam,
|
||||
|
||||
// If we are NOT the primary instance, then we never want to run.
|
||||
// This means that another instance of the GTK app is running and
|
||||
@ -154,9 +144,6 @@ pub const App = struct {
|
||||
c.g_main_context_release(self.ctx);
|
||||
c.g_object_unref(self.app);
|
||||
|
||||
c.g_object_unref(self.cursor_ibeam);
|
||||
c.g_object_unref(self.cursor_default);
|
||||
|
||||
self.config.deinit();
|
||||
|
||||
glfw.terminate();
|
||||
@ -722,6 +709,9 @@ pub const Surface = struct {
|
||||
/// Our GTK area
|
||||
gl_area: *c.GtkGLArea,
|
||||
|
||||
/// Any active cursor we may have
|
||||
cursor: ?*c.GdkCursor = null,
|
||||
|
||||
/// Our title label (if there is one).
|
||||
title: Title,
|
||||
|
||||
@ -798,9 +788,6 @@ pub const Surface = struct {
|
||||
c.gtk_widget_set_focusable(widget, 1);
|
||||
c.gtk_widget_set_focus_on_click(widget, 1);
|
||||
|
||||
// When we're over the widget, set the cursor to the ibeam
|
||||
c.gtk_widget_set_cursor(widget, app.cursor_ibeam);
|
||||
|
||||
// Build our result
|
||||
self.* = .{
|
||||
.app = app,
|
||||
@ -880,6 +867,8 @@ pub const Surface = struct {
|
||||
// Free all our GTK stuff
|
||||
c.g_object_unref(self.im_context);
|
||||
c.g_value_unset(&self.clipboard);
|
||||
|
||||
if (self.cursor) |cursor| c.g_object_unref(cursor);
|
||||
}
|
||||
|
||||
fn render(self: *Surface) !void {
|
||||
@ -998,6 +987,61 @@ pub const Surface = struct {
|
||||
// ));
|
||||
}
|
||||
|
||||
pub fn setMouseShape(
|
||||
self: *Surface,
|
||||
shape: terminal.MouseShape,
|
||||
) !void {
|
||||
const name: [:0]const u8 = switch (shape) {
|
||||
.default => "default",
|
||||
.help => "help",
|
||||
.pointer => "pointer",
|
||||
.context_menu => "context-menu",
|
||||
.progress => "progress",
|
||||
.wait => "wait",
|
||||
.cell => "cell",
|
||||
.crosshair => "crosshair",
|
||||
.text => "text",
|
||||
.vertical_text => "vertical-text",
|
||||
.alias => "alias",
|
||||
.copy => "copy",
|
||||
.no_drop => "no-drop",
|
||||
.move => "move",
|
||||
.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",
|
||||
};
|
||||
|
||||
const cursor = c.gdk_cursor_new_from_name(name.ptr, null) orelse {
|
||||
log.warn("unsupported cursor name={s}", .{name});
|
||||
return;
|
||||
};
|
||||
errdefer c.g_object_unref(cursor);
|
||||
|
||||
// Set our new cursor
|
||||
c.gtk_widget_set_cursor(@ptrCast(self.gl_area), cursor);
|
||||
|
||||
// Free our existing cursor
|
||||
if (self.cursor) |old| c.g_object_unref(old);
|
||||
self.cursor = cursor;
|
||||
}
|
||||
|
||||
pub fn getClipboardString(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
|
@ -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 mouse shape.
|
||||
set_mouse_shape: terminal.MouseShape,
|
||||
|
||||
/// Change the cell size.
|
||||
cell_size: renderer.CellSize,
|
||||
|
||||
|
@ -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 MouseShape = @import("mouse_shape.zig").MouseShape;
|
||||
pub const Terminal = @import("Terminal.zig");
|
||||
pub const Parser = @import("Parser.zig");
|
||||
pub const Selection = @import("Selection.zig");
|
||||
|
115
src/terminal/mouse_shape.zig
Normal file
115
src/terminal/mouse_shape.zig
Normal file
@ -0,0 +1,115 @@
|
||||
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.
|
||||
//
|
||||
// Must be kept in sync with ghostty_cursor_shape_e
|
||||
pub const MouseShape = 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) ?MouseShape {
|
||||
return string_map.get(v);
|
||||
}
|
||||
};
|
||||
|
||||
const string_map = std.ComptimeStringMap(MouseShape, .{
|
||||
// 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(MouseShape.default, MouseShape.fromString("default").?);
|
||||
}
|
@ -83,6 +83,14 @@ pub const Command = union(enum) {
|
||||
/// be a file URL but it is up to the caller to utilize this value.
|
||||
value: []const u8,
|
||||
},
|
||||
|
||||
/// OSC 22. Set the mouse shape. There doesn't seem to be a standard
|
||||
/// naming scheme for cursors but it looks like terminals such as Foot
|
||||
/// are moving towards using the W3C CSS cursor names. For OSC parsing,
|
||||
/// we just parse whatever string is given.
|
||||
mouse_shape: struct {
|
||||
value: []const u8,
|
||||
},
|
||||
};
|
||||
|
||||
pub const Parser = struct {
|
||||
@ -130,6 +138,7 @@ pub const Parser = struct {
|
||||
@"13",
|
||||
@"133",
|
||||
@"2",
|
||||
@"22",
|
||||
@"5",
|
||||
@"52",
|
||||
@"7",
|
||||
@ -226,6 +235,7 @@ pub const Parser = struct {
|
||||
},
|
||||
|
||||
.@"2" => switch (c) {
|
||||
'2' => self.state = .@"22",
|
||||
';' => {
|
||||
self.command = .{ .change_window_title = undefined };
|
||||
|
||||
@ -236,6 +246,17 @@ pub const Parser = struct {
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"22" => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .mouse_shape = undefined };
|
||||
|
||||
self.state = .string;
|
||||
self.temp_state = .{ .str = &self.command.mouse_shape.value };
|
||||
self.buf_start = self.buf_idx;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.@"5" => switch (c) {
|
||||
'2' => self.state = .@"52",
|
||||
else => self.state = .invalid,
|
||||
@ -642,6 +663,19 @@ test "OSC: report pwd" {
|
||||
try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value));
|
||||
}
|
||||
|
||||
test "OSC: pointer cursor" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "22;pointer";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end().?;
|
||||
try testing.expect(cmd == .mouse_shape);
|
||||
try testing.expect(std.mem.eql(u8, "pointer", cmd.mouse_shape.value));
|
||||
}
|
||||
|
||||
test "OSC: report pwd empty" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -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 MouseShape = @import("mouse_shape.zig").MouseShape;
|
||||
|
||||
const log = std.log.scoped(.stream);
|
||||
|
||||
@ -849,6 +850,17 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.mouse_shape => |v| {
|
||||
if (@hasDecl(T, "setMouseShape")) {
|
||||
const shape = MouseShape.fromString(v.value) orelse {
|
||||
log.warn("unknown cursor shape: {s}", .{v.value});
|
||||
return;
|
||||
};
|
||||
|
||||
try self.handler.setMouseShape(shape);
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
else => if (@hasDecl(T, "oscUnimplemented"))
|
||||
try self.handler.oscUnimplemented(cmd)
|
||||
else
|
||||
|
@ -1682,6 +1682,15 @@ const StreamHandler = struct {
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
pub fn setMouseShape(
|
||||
self: *StreamHandler,
|
||||
shape: terminal.MouseShape,
|
||||
) !void {
|
||||
_ = self.ev.surface_mailbox.push(.{
|
||||
.set_mouse_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
|
||||
|
Reference in New Issue
Block a user