macos: add mouse button intent

This commit is contained in:
Mitchell Hashimoto
2025-06-19 13:49:36 -07:00
parent 71b6e223af
commit 4445a9c637
4 changed files with 206 additions and 10 deletions

View File

@@ -95,6 +95,64 @@ struct KeyEventIntent: AppIntent {
}
}
// MARK: MouseButtonIntent
/// App intent to trigger a mouse button event.
struct MouseButtonIntent: AppIntent {
static var title: LocalizedStringResource = "Send Mouse Button Event to Terminal"
@Parameter(
title: "Button",
description: "The mouse button to press or release.",
default: .left
)
var button: Ghostty.Input.MouseButton
@Parameter(
title: "Action",
description: "Whether to press or release the button.",
default: .press
)
var action: Ghostty.Input.MouseState
@Parameter(
title: "Modifier(s)",
description: "The modifiers to send with the mouse event.",
default: []
)
var mods: [KeyEventMods]
@Parameter(
title: "Terminal",
description: "The terminal to scope this action to."
)
var terminal: TerminalEntity
@available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground]
@MainActor
func perform() async throws -> some IntentResult {
guard let surface = terminal.surfaceModel else {
throw GhosttyIntentError.surfaceNotFound
}
// Convert KeyEventMods array to Ghostty.Input.Mods
let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
result.union(mod.ghosttyMod)
}
let mouseEvent = Ghostty.Input.MouseButtonEvent(
action: action,
button: button,
mods: ghosttyMods
)
surface.sendMouseButton(mouseEvent)
return .result()
}
}
// MARK: Mods
enum KeyEventMods: String, AppEnum, CaseIterable {

View File

@@ -215,11 +215,126 @@ extension Ghostty.Input.Action: AppEnum {
static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [
.release: "Release",
.press: "Press",
.press: "Press",
.repeat: "Repeat"
]
}
// MARK: Ghostty.Input.MouseEvent
extension Ghostty.Input {
/// Represents a mouse input event with button state, button type, and modifier keys.
struct MouseButtonEvent {
let action: MouseState
let button: MouseButton
let mods: Mods
init(
action: MouseState,
button: MouseButton,
mods: Mods = []
) {
self.action = action
self.button = button
self.mods = mods
}
/// Creates a MouseEvent from C enum values.
///
/// This initializer converts C-style mouse input enums to Swift types.
/// Returns nil if any of the C enum values are invalid or unsupported.
///
/// - Parameters:
/// - state: The mouse button state (press/release)
/// - button: The mouse button that was pressed/released
/// - mods: The modifier keys held during the mouse event
init?(state: ghostty_input_mouse_state_e, button: ghostty_input_mouse_button_e, mods: ghostty_input_mods_e) {
// Convert state
switch state {
case GHOSTTY_MOUSE_RELEASE: self.action = .release
case GHOSTTY_MOUSE_PRESS: self.action = .press
default: return nil
}
// Convert button
switch button {
case GHOSTTY_MOUSE_UNKNOWN: self.button = .unknown
case GHOSTTY_MOUSE_LEFT: self.button = .left
case GHOSTTY_MOUSE_RIGHT: self.button = .right
case GHOSTTY_MOUSE_MIDDLE: self.button = .middle
default: return nil
}
// Convert modifiers
self.mods = Mods(cMods: mods)
}
}
}
// MARK: Ghostty.Input.MouseState
extension Ghostty.Input {
/// `ghostty_input_mouse_state_e`
enum MouseState: String, CaseIterable {
case release
case press
var cMouseState: ghostty_input_mouse_state_e {
switch self {
case .release: GHOSTTY_MOUSE_RELEASE
case .press: GHOSTTY_MOUSE_PRESS
}
}
}
}
extension Ghostty.Input.MouseState: AppEnum {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse State")
static var caseDisplayRepresentations: [Ghostty.Input.MouseState : DisplayRepresentation] = [
.release: "Release",
.press: "Press"
]
}
// MARK: Ghostty.Input.MouseButton
extension Ghostty.Input {
/// `ghostty_input_mouse_button_e`
enum MouseButton: String, CaseIterable {
case unknown
case left
case right
case middle
var cMouseButton: ghostty_input_mouse_button_e {
switch self {
case .unknown: GHOSTTY_MOUSE_UNKNOWN
case .left: GHOSTTY_MOUSE_LEFT
case .right: GHOSTTY_MOUSE_RIGHT
case .middle: GHOSTTY_MOUSE_MIDDLE
}
}
}
}
extension Ghostty.Input.MouseButton: AppEnum {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse Button")
static var caseDisplayRepresentations: [Ghostty.Input.MouseButton : DisplayRepresentation] = [
.unknown: "Unknown",
.left: "Left",
.right: "Right",
.middle: "Middle"
]
static var allCases: [Ghostty.Input.MouseButton] = [
.left,
.right,
.middle,
]
}
// MARK: Ghostty.Input.Mods
extension Ghostty.Input {

View File

@@ -62,6 +62,32 @@ extension Ghostty {
}
}
/// Whether the terminal has captured mouse input.
///
/// When the mouse is captured, the terminal application is receiving mouse events
/// directly rather than the host system handling them. This typically occurs when
/// a terminal application enables mouse reporting mode.
@MainActor
var mouseCaptured: Bool {
ghostty_surface_mouse_captured(surface)
}
/// Send a mouse button event to the terminal.
///
/// This sends a complete mouse button event including the button state (press/release),
/// which button was pressed, and any modifier keys that were held during the event.
/// The terminal processes this event according to its mouse handling configuration.
///
/// - Parameter event: The mouse button event to send to the terminal
@MainActor
func sendMouseButton(_ event: Input.MouseButtonEvent) {
ghostty_surface_mouse_button(
surface,
event.action.cMouseState,
event.button.cMouseButton,
event.mods.cMods)
}
/// Perform a keybinding action.
///
/// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`

View File

@@ -1312,8 +1312,8 @@ extension Ghostty {
// In this case, AppKit calls menu BEFORE calling any mouse events.
// If mouse capturing is enabled then we never show the context menu
// so that we can handle ctrl+left-click in the terminal app.
guard let surface = self.surface else { return nil }
if ghostty_surface_mouse_captured(surface) {
guard let surfaceModel else { return nil }
if surfaceModel.mouseCaptured {
return nil
}
@@ -1323,13 +1323,10 @@ extension Ghostty {
//
// Note this never sounds a right mouse up event but that's the
// same as normal right-click with capturing disabled from AppKit.
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_mouse_button(
surface,
GHOSTTY_MOUSE_PRESS,
GHOSTTY_MOUSE_RIGHT,
mods
)
surfaceModel.sendMouseButton(.init(
action: .press,
button: .right,
mods: .init(nsFlags: event.modifierFlags)))
default:
return nil