From 3356146bb444bf716d8496d7dd88f1ffd42dbba0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 14 Sep 2023 10:40:40 -0700 Subject: [PATCH] macos: support cursor style --- include/ghostty.h | 39 ++++ macos/Sources/Ghostty/AppState.swift | 6 + macos/Sources/Ghostty/SurfaceView.swift | 286 +++++++++++++++--------- src/apprt/embedded.zig | 11 + src/terminal/cursor_shape.zig | 2 + 5 files changed, 237 insertions(+), 107 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index eab70bac2..5c5ba7d59 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -70,6 +70,43 @@ typedef enum { GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN, } ghostty_input_mouse_momentum_e; +typedef enum { + GHOSTTY_CURSOR_SHAPE_DEFAULT, + GHOSTTY_CURSOR_SHAPE_CONTEXT_MENU, + GHOSTTY_CURSOR_SHAPE_HELP, + GHOSTTY_CURSOR_SHAPE_POINTER, + GHOSTTY_CURSOR_SHAPE_PROGRESS, + GHOSTTY_CURSOR_SHAPE_WAIT, + GHOSTTY_CURSOR_SHAPE_CELL, + GHOSTTY_CURSOR_SHAPE_CROSSHAIR, + GHOSTTY_CURSOR_SHAPE_TEXT, + GHOSTTY_CURSOR_SHAPE_VERTICAL_TEXT, + GHOSTTY_CURSOR_SHAPE_ALIAS, + GHOSTTY_CURSOR_SHAPE_COPY, + GHOSTTY_CURSOR_SHAPE_MOVE, + GHOSTTY_CURSOR_SHAPE_NO_DROP, + GHOSTTY_CURSOR_SHAPE_NOT_ALLOWED, + GHOSTTY_CURSOR_SHAPE_GRAB, + GHOSTTY_CURSOR_SHAPE_GRABBING, + GHOSTTY_CURSOR_SHAPE_ALL_SCROLL, + GHOSTTY_CURSOR_SHAPE_COL_RESIZE, + GHOSTTY_CURSOR_SHAPE_ROW_RESIZE, + GHOSTTY_CURSOR_SHAPE_N_RESIZE, + GHOSTTY_CURSOR_SHAPE_E_RESIZE, + GHOSTTY_CURSOR_SHAPE_S_RESIZE, + GHOSTTY_CURSOR_SHAPE_W_RESIZE, + GHOSTTY_CURSOR_SHAPE_NE_RESIZE, + GHOSTTY_CURSOR_SHAPE_NW_RESIZE, + GHOSTTY_CURSOR_SHAPE_SE_RESIZE, + GHOSTTY_CURSOR_SHAPE_SW_RESIZE, + GHOSTTY_CURSOR_SHAPE_EW_RESIZE, + GHOSTTY_CURSOR_SHAPE_NS_RESIZE, + GHOSTTY_CURSOR_SHAPE_NESW_RESIZE, + GHOSTTY_CURSOR_SHAPE_NWSE_RESIZE, + GHOSTTY_CURSOR_SHAPE_ZOOM_IN, + GHOSTTY_CURSOR_SHAPE_ZOOM_OUT, +} ghostty_cursor_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_cursor_shape_cb)(void *, ghostty_cursor_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_cursor_shape_cb set_cursor_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; diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index b6dec9dec..fb043991f 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -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_cursor_shape_cb: { userdata, shape in AppState.setCursorShape(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) }, @@ -332,6 +333,11 @@ extension Ghostty { surfaceView.title = titleStr } } + + static func setCursorShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_cursor_shape_e) { + let surfaceView = Unmanaged.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 } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index ae7f73652..48d932b06 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -6,7 +6,7 @@ extension Ghostty { struct Terminal: View { @Environment(\.ghosttyApp) private var app @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? - + var body: some View { if let app = self.app { SurfaceForApp(app) { surfaceView in @@ -16,44 +16,44 @@ extension Ghostty { } } } - + /// Yields a SurfaceView for a ghostty app that can then be used however you want. struct SurfaceForApp: View { let content: ((SurfaceView) -> Content) - + @StateObject private var surfaceView: SurfaceView - + init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) { _surfaceView = StateObject(wrappedValue: SurfaceView(app, nil)) self.content = content } - + var body: some View { content(surfaceView) } } - + struct SurfaceWrapper: View { // The surface to create a view for. This must be created upstream. As long as this // remains the same, the surface that is being rendered remains the same. @ObservedObject var surfaceView: SurfaceView - + // True if this surface is part of a split view. This is important to know so // we know whether to dim the surface out of focus. var isSplit: Bool = false - + // Maintain whether our view has focus or not @FocusState private var surfaceFocus: Bool - + // Maintain whether our window has focus (is key) or not @State private var windowFocus: Bool = true - + @Environment(\.ghosttyConfig) private var ghostty_config - + // This is true if the terminal is considered "focused". The terminal is focused if // it is both individually focused and the containing window is key. private var hasFocus: Bool { surfaceFocus && windowFocus } - + // The opacity of the rectangle when unfocused. private var unfocusedOpacity: Double { var opacity: Double = 0.85 @@ -61,7 +61,7 @@ extension Ghostty { _ = ghostty_config_get(ghostty_config, &opacity, key, UInt(key.count)) return 1 - opacity } - + var body: some View { ZStack { // We use a GeometryReader to get the frame bounds so that our metal surface @@ -73,7 +73,7 @@ extension Ghostty { let pubBecomeFocused = NotificationCenter.default.publisher(for: Notification.didBecomeFocusedSurface, object: surfaceView) let pubBecomeKey = NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification) let pubResign = NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification) - + Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size) .focused($surfaceFocus) .focusedValue(\.ghosttySurfaceTitle, surfaceView.title) @@ -95,7 +95,7 @@ extension Ghostty { // method doesn't work properly. See the dispatch of this notification // for more information. if #available(macOS 13, *) { return } - + DispatchQueue.main.async { surfaceFocus = true } @@ -125,13 +125,13 @@ extension Ghostty { surfaceFocus = true } } - + // I don't know how older macOS versions behave but Ghostty only // supports back to macOS 12 so its moot. } } .ghosttySurfaceView(surfaceView) - + // If we're part of a split view and don't have focus, we put a semi-transparent // rectangle above our view to make it look unfocused. We use "surfaceFocus" // because we want to keep our focused surface dark even if we don't have window @@ -145,7 +145,7 @@ extension Ghostty { } } } - + /// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn /// and interacted with. The word "surface" is used because a surface may represent a window, a tab, /// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it. @@ -156,12 +156,12 @@ extension Ghostty { struct Surface: NSViewRepresentable { /// The view to render for the terminal surface. let view: SurfaceView - + /// This should be set to true when the surface has focus. This is up to the parent because /// focus is also defined by window focus. It is important this is set correctly since if it is /// false then the surface will idle at almost 0% CPU. let hasFocus: Bool - + /// The size of the frame containing this view. We use this to update the the underlying /// surface. This does not actually SET the size of our frame, this only sets the size /// of our Metal surface for drawing. @@ -171,74 +171,76 @@ extension Ghostty { /// /// The best approach is to wrap this view in a GeometryReader and pass in the geo.size. let size: CGSize - + func makeNSView(context: Context) -> SurfaceView { // We need the view as part of the state to be created previously because // the view is sent to the Ghostty API so that it can manipulate it // directly since we draw on a render thread. return view; } - + func updateNSView(_ view: SurfaceView, context: Context) { view.focusDidChange(hasFocus) view.sizeDidChange(size) } } - + /// The NSView implementation for a terminal surface. class SurfaceView: NSView, NSTextInputClient, ObservableObject { // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go // to the app level and it is set from there. @Published var title: String = "👻" - + 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 } - + // I don't think we need this but this lets us know we should redraw our layer // so we'll use that to tell ghostty to refresh. override var wantsUpdateLayer: Bool { return true } - + init(_ app: ghostty_app_t, _ baseConfig: ghostty_surface_config_s?) { self.markedText = NSMutableAttributedString() - + // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer // can do SOMETHING. super.init(frame: NSMakeRect(0, 0, 800, 600)) - + // Setup our surface. This will also initialize all the terminal IO. var surface_cfg = baseConfig ?? ghostty_surface_config_new() surface_cfg.userdata = Unmanaged.passUnretained(self).toOpaque() surface_cfg.nsview = Unmanaged.passUnretained(self).toOpaque() surface_cfg.scale_factor = NSScreen.main!.backingScaleFactor - + guard let surface = ghostty_surface_new(app, &surface_cfg) else { self.error = AppError.surfaceCreateError return } self.surface = surface; - + // Setup our tracking area so we get mouse moved events updateTrackingAreas() } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { trackingAreas.forEach { removeTrackingArea($0) } - + guard let surface = self.surface else { return } ghostty_surface_free(surface) } - + /// Close the surface early. This will free the associated Ghostty surface and the view will /// no longer render. The view can never be used again. This is a way for us to free the /// Ghostty resources while references may still be held to this view. I've found that SwiftUI @@ -248,15 +250,15 @@ extension Ghostty { ghostty_surface_free(surface) self.surface = nil } - + func focusDidChange(_ focused: Bool) { guard let surface = self.surface else { return } ghostty_surface_set_focus(surface, focused) } - + func sizeDidChange(_ size: CGSize) { guard let surface = self.surface else { return } - + // Ghostty wants to know the actual framebuffer size... It is very important // here that we use "size" and NOT the view frame. If we're in the middle of // an animation (i.e. a fullscreen animation), the frame will not yet be updated. @@ -264,35 +266,93 @@ extension Ghostty { let scaledSize = self.convertToBacking(size) ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height)) } - + + func setCursorShape(_ shape: ghostty_cursor_shape_e) { + switch (shape) { + case GHOSTTY_CURSOR_SHAPE_DEFAULT: + cursor = .arrow + + case GHOSTTY_CURSOR_SHAPE_CONTEXT_MENU: + cursor = .contextualMenu + + case GHOSTTY_CURSOR_SHAPE_TEXT: + cursor = .iBeam + + case GHOSTTY_CURSOR_SHAPE_CROSSHAIR: + cursor = .crosshair + + case GHOSTTY_CURSOR_SHAPE_GRAB: + cursor = .openHand + + case GHOSTTY_CURSOR_SHAPE_GRABBING: + cursor = .closedHand + + case GHOSTTY_CURSOR_SHAPE_POINTER: + cursor = .pointingHand + + case GHOSTTY_CURSOR_SHAPE_W_RESIZE: + cursor = .resizeLeft + + case GHOSTTY_CURSOR_SHAPE_E_RESIZE: + cursor = .resizeRight + + case GHOSTTY_CURSOR_SHAPE_N_RESIZE: + cursor = .resizeUp + + case GHOSTTY_CURSOR_SHAPE_S_RESIZE: + cursor = .resizeDown + + case GHOSTTY_CURSOR_SHAPE_NS_RESIZE: + cursor = .resizeUpDown + + case GHOSTTY_CURSOR_SHAPE_EW_RESIZE: + cursor = .resizeLeftRight + + case GHOSTTY_CURSOR_SHAPE_VERTICAL_TEXT: + cursor = .iBeamCursorForVerticalLayout + + case GHOSTTY_CURSOR_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 } guard ghostty_surface_transparent(surface) else { return } - + // Set the window transparency settings window.isOpaque = false window.hasShadow = false window.backgroundColor = .clear - + // If we have a blur, set the blur ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque()) } - + override func resignFirstResponder() -> Bool { let result = super.resignFirstResponder() - + // We sometimes call this manually (see SplitView) as a way to force us to // yield our focus state. if (result) { focusDidChange(false) } - + return result } - + override func updateTrackingAreas() { // To update our tracking area we just recreate it all. trackingAreas.forEach { removeTrackingArea($0) } - + // This tracking area is across the entire frame to notify us of mouse movements. addTrackingArea(NSTrackingArea( rect: frame, @@ -300,7 +360,7 @@ extension Ghostty { .mouseEnteredAndExited, .mouseMoved, .inVisibleRect, - + // It is possible this is incorrect when we have splits. This will make // mouse events only happen while the terminal is focused. Is that what // we want? @@ -309,12 +369,12 @@ extension Ghostty { owner: self, userInfo: nil)) } - + override func resetCursorRects() { discardCursorRects() addCursorRect(frame, cursor: .iBeam) } - + override func viewDidChangeBackingProperties() { guard let surface = self.surface else { return } @@ -323,71 +383,79 @@ extension Ghostty { let xScale = fbFrame.size.width / self.frame.size.width let yScale = fbFrame.size.height / self.frame.size.height ghostty_surface_set_content_scale(surface, xScale, yScale) - + // When our scale factor changes, so does our fb size so we send that too ghostty_surface_set_size(surface, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height)) } - + override func updateLayer() { guard let surface = self.surface else { return } ghostty_surface_refresh(surface); } - + override func mouseDown(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods) } - + override func mouseUp(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods) } - + override func rightMouseDown(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods) } - + override func rightMouseUp(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods) } - + override func mouseMoved(with event: NSEvent) { guard let surface = self.surface else { return } // Convert window position to view position. Note (0, 0) is bottom left. let pos = self.convert(event.locationInWindow, from: nil) ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y) - + } - + override func mouseDragged(with event: NSEvent) { 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 } - + // Builds up the "input.ScrollMods" bitmask var mods: Int32 = 0 - + var x = event.scrollingDeltaX var y = event.scrollingDeltaY if event.hasPreciseScrollingDeltas { mods = 1 - + // We do a 2x speed multiplier. This is subjective, it "feels" better to me. x *= 2; y *= 2; - + // TODO(mitchellh): do we have to scale the x/y here by window scale factor? } - + // Determine our momentum value var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE switch (event.momentumPhase) { @@ -406,17 +474,21 @@ extension Ghostty { default: break } - + // Pack our momentum value into the mods bitmask mods |= Int32(momentum.rawValue) << 1 - + 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) - + // We specifically DO NOT call interpretKeyEvents because ghostty_surface_key // automatically handles all key translation, and we don't handle any commands // currently. @@ -427,11 +499,11 @@ extension Ghostty { // // self.interpretKeyEvents([event]) } - + override func keyUp(with event: NSEvent) { keyAction(GHOSTTY_ACTION_RELEASE, event: event) } - + override func flagsChanged(with event: NSEvent) { let mod: UInt32; switch (event.keyCode) { @@ -442,26 +514,26 @@ extension Ghostty { case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue default: return } - + // The keyAction function will do this AGAIN below which sucks to repeat // but this is super cheap and flagsChanged isn't that common. let mods = Self.translateFlags(event.modifierFlags) - + // If the key that pressed this is active, its a press, else release var action = GHOSTTY_ACTION_RELEASE if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS } - + keyAction(action, event: event) } - + private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let surface = self.surface else { return } let mods = Self.translateFlags(event.modifierFlags) ghostty_surface_key(surface, action, UInt32(event.keyCode), mods) } - + // MARK: Menu Handlers - + @IBAction func copy(_ sender: Any?) { guard let surface = self.surface else { return } let action = "copy_to_clipboard" @@ -469,7 +541,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func paste(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" @@ -477,7 +549,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func pasteAsPlainText(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" @@ -485,75 +557,75 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + // MARK: NSTextInputClient - + func hasMarkedText() -> Bool { return markedText.length > 0 } - + func markedRange() -> NSRange { guard markedText.length > 0 else { return NSRange() } return NSRange(0...(markedText.length-1)) } - + func selectedRange() -> NSRange { return NSRange() } - + func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) { switch string { case let v as NSAttributedString: self.markedText = NSMutableAttributedString(attributedString: v) - + case let v as String: self.markedText = NSMutableAttributedString(string: v) - + default: print("unknown marked text: \(string)") } } - + func unmarkText() { self.markedText.mutableString.setString("") } - + func validAttributesForMarkedText() -> [NSAttributedString.Key] { return [] } - + func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? { return nil } - + func characterIndex(for point: NSPoint) -> Int { return 0 } - + func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { guard let surface = self.surface else { return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) } - + // Ghostty will tell us where it thinks an IME keyboard should render. var x: Double = 0; var y: Double = 0; ghostty_surface_ime_point(surface, &x, &y) - + // Ghostty coordinates are in top-left (0, 0) so we have to convert to // bottom-left since that is what UIKit expects let rect = NSMakeRect(x, frame.size.height - y, 0, 0) - + // Convert from view to screen coordinates guard let window = self.window else { return rect } return window.convertToScreen(rect) } - + func insertText(_ string: Any, replacementRange: NSRange) { // We must have an associated event guard NSApp.currentEvent != nil else { return } guard let surface = self.surface else { return } - + // We want the string view of the any value var chars = "" switch (string) { @@ -564,39 +636,39 @@ extension Ghostty { default: return } - + for codepoint in chars.unicodeScalars { ghostty_surface_char(surface, codepoint.value) } } - + override func doCommand(by selector: Selector) { // This currently just prevents NSBeep from interpretKeyEvents but in the future // we may want to make some of this work. - + print("SEL: \(selector)") } - + private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue - + if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue } if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue } if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue } if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } - + // Handle sided input. We can't tell that both are pressed in the // Ghostty structure but thats okay -- we don't use that information. let rawFlags = flags.rawValue if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } - + if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } + return ghostty_input_mods_e(mods) } - + // Mapping of event keyCode to ghostty input key values. This is cribbed from // glfw mostly since we started as a glfw-based app way back in the day! static let keycodes: [UInt16 : ghostty_input_key_e] = [ @@ -742,7 +814,7 @@ extension FocusedValues { get { self[FocusedGhosttySurface.self] } set { self[FocusedGhosttySurface.self] = newValue } } - + struct FocusedGhosttySurface: FocusedValueKey { typealias Value = Ghostty.SurfaceView } @@ -753,7 +825,7 @@ extension FocusedValues { get { self[FocusedGhosttySurfaceTitle.self] } set { self[FocusedGhosttySurfaceTitle.self] = newValue } } - + struct FocusedGhosttySurfaceTitle: FocusedValueKey { typealias Value = String } @@ -764,7 +836,7 @@ extension FocusedValues { get { self[FocusedGhosttySurfaceZoomed.self] } set { self[FocusedGhosttySurfaceZoomed.self] = newValue } } - + struct FocusedGhosttySurfaceZoomed: FocusedValueKey { typealias Value = Bool } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 3420fdd05..80d296c9b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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_cursor_shape: *const fn (SurfaceUD, terminal.CursorShape) 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 setCursorShape(self: *Surface, shape: terminal.CursorShape) !void { + self.app.opts.set_cursor_shape( + self.opts.userdata, + shape, + ); + } + pub fn supportsClipboard( self: *const Surface, clipboard_type: apprt.Clipboard, diff --git a/src/terminal/cursor_shape.zig b/src/terminal/cursor_shape.zig index 021ee74d4..99dfed461 100644 --- a/src/terminal/cursor_shape.zig +++ b/src/terminal/cursor_shape.zig @@ -3,6 +3,8 @@ 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 CursorShape = enum(c_int) { default, context_menu,