From 7fbc73ad374d93ea6aed227021fa649be54dcd48 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 1 Jul 2024 10:05:05 -0700 Subject: [PATCH] macos: implement ctrl+command+d for quicklook under cursor --- include/ghostty.h | 4 ++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 32 ++++++++++- src/Surface.zig | 2 +- src/apprt/embedded.zig | 55 +++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index a082d1a6a..04233287f 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -565,6 +565,10 @@ uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); #ifdef __APPLE__ void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); void* ghostty_surface_quicklook_font(ghostty_surface_t); +uintptr_t ghostty_surface_quicklook_word(ghostty_surface_t, + char*, + uintptr_t, + ghostty_selection_s*); bool ghostty_surface_selection_info(ghostty_surface_t, ghostty_selection_s*); #endif diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 499f85b0e..bfd896be1 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -623,7 +623,7 @@ extension Ghostty { guard UserDefaults.standard.bool(forKey: "com.apple.trackpad.forceClick") else { return } quickLook(with: event) } - + override func cursorUpdate(with event: NSEvent) { switch (cursorVisible) { case .visible, .hidden: @@ -867,6 +867,36 @@ extension Ghostty { } } + override func quickLook(with event: NSEvent) { + guard let surface = self.surface else { return super.quickLook(with: event) } + + // Grab the text under the cursor + var info: ghostty_selection_s = ghostty_selection_s(); + let text = String(unsafeUninitializedCapacity: 1000000) { + Int(ghostty_surface_quicklook_word(surface, $0.baseAddress, UInt($0.count), &info)) + } + guard !text.isEmpty else { return super.quickLook(with: event) } + + // If we can get a font then we use the font. This should always work + // since we always have a primary font. The only scenario this doesn't + // work is if someone is using a non-CoreText build which would be + // unofficial. + var attributes: [ NSAttributedString.Key : Any ] = [:]; + if let fontRaw = ghostty_surface_quicklook_font(surface) { + // Memory management here is wonky: ghostty_surface_quicklook_font + // will create a copy of a CTFont, Swift will auto-retain the + // unretained value passed into the dict, so we release the original. + let font = Unmanaged.fromOpaque(fontRaw) + attributes[.font] = font.takeUnretainedValue() + font.release() + } + + // Ghostty coordinate system is top-left, conver to bottom-left for AppKit + let pt = NSMakePoint(info.tl_px_x - 2, frame.size.height - info.tl_px_y + 2) + let str = NSAttributedString.init(string: text, attributes: attributes) + self.showDefinition(for: str, at: pt); + } + override func menu(for event: NSEvent) -> NSMenu? { // We only support right-click menus switch event.type { diff --git a/src/Surface.zig b/src/Surface.zig index a63c85cb2..6c8d0edb4 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3061,7 +3061,7 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { if (report) try self.reportColorScheme(); } -fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { +pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { // xpos/ypos need to be adjusted for window padding // (i.e. "window-padding-*" settings. const pad = if (self.config.window_padding_balance) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index d1557eaa0..113d9379a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1829,6 +1829,61 @@ pub const CAPI = struct { return copy; } + /// This returns the selected word for quicklook. This will populate + /// the buffer with the word under the cursor and the selection + /// info so that quicklook can be rendered. + /// + /// This does not modify the selection active on the surface (if any). + export fn ghostty_surface_quicklook_word( + ptr: *Surface, + buf: [*]u8, + cap: usize, + info: *Selection, + ) usize { + const surface = &ptr.core_surface; + surface.renderer_state.mutex.lock(); + defer surface.renderer_state.mutex.unlock(); + + // To make everything in this function easier, we modify the + // selection to be the word under the cursor and call normal APIs. + // We restore the old selection so it isn't ever changed. Since we hold + // the renderer mutex it'll never show up in a frame. + const prev = surface.io.terminal.screen.selection; + defer surface.io.terminal.screen.selection = prev; + + // Get our word selection + const sel = sel: { + const screen = &surface.renderer_state.terminal.screen; + const pos = try ptr.getCursorPos(); + const pt_viewport = surface.posToViewport(pos.x, pos.y); + const pin = screen.pages.pin(.{ + .viewport = .{ + .x = pt_viewport.x, + .y = pt_viewport.y, + }, + }) orelse { + if (comptime std.debug.runtime_safety) unreachable; + return 0; + }; + break :sel surface.io.terminal.screen.selectWord(pin) orelse return 0; + }; + + // Set the selection + surface.io.terminal.screen.selection = sel; + + // No we call normal functions. These require that the lock + // is unlocked. This may cause a frame flicker with the fake + // selection but I think the lack of new complexity is worth it + // for now. + { + surface.renderer_state.mutex.unlock(); + defer surface.renderer_state.mutex.lock(); + const len = ghostty_surface_selection(ptr, buf, cap); + if (!ghostty_surface_selection_info(ptr, info)) return 0; + return len; + } + } + /// This returns the selection metadata for the current selection. /// This will return false if there is no selection or the /// selection is not fully contained in the viewport (since the