macos: add mouse button intent
parent
71b6e223af
commit
4445a9c637
|
|
@ -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
|
// MARK: Mods
|
||||||
|
|
||||||
enum KeyEventMods: String, AppEnum, CaseIterable {
|
enum KeyEventMods: String, AppEnum, CaseIterable {
|
||||||
|
|
|
||||||
|
|
@ -215,11 +215,126 @@ extension Ghostty.Input.Action: AppEnum {
|
||||||
|
|
||||||
static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [
|
static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [
|
||||||
.release: "Release",
|
.release: "Release",
|
||||||
.press: "Press",
|
.press: "Press",
|
||||||
.repeat: "Repeat"
|
.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
|
// MARK: Ghostty.Input.Mods
|
||||||
|
|
||||||
extension Ghostty.Input {
|
extension Ghostty.Input {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// Perform a keybinding action.
|
||||||
///
|
///
|
||||||
/// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`
|
/// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`
|
||||||
|
|
|
||||||
|
|
@ -1312,8 +1312,8 @@ extension Ghostty {
|
||||||
// In this case, AppKit calls menu BEFORE calling any mouse events.
|
// In this case, AppKit calls menu BEFORE calling any mouse events.
|
||||||
// If mouse capturing is enabled then we never show the context menu
|
// If mouse capturing is enabled then we never show the context menu
|
||||||
// so that we can handle ctrl+left-click in the terminal app.
|
// so that we can handle ctrl+left-click in the terminal app.
|
||||||
guard let surface = self.surface else { return nil }
|
guard let surfaceModel else { return nil }
|
||||||
if ghostty_surface_mouse_captured(surface) {
|
if surfaceModel.mouseCaptured {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1323,13 +1323,10 @@ extension Ghostty {
|
||||||
//
|
//
|
||||||
// Note this never sounds a right mouse up event but that's the
|
// Note this never sounds a right mouse up event but that's the
|
||||||
// same as normal right-click with capturing disabled from AppKit.
|
// same as normal right-click with capturing disabled from AppKit.
|
||||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
surfaceModel.sendMouseButton(.init(
|
||||||
ghostty_surface_mouse_button(
|
action: .press,
|
||||||
surface,
|
button: .right,
|
||||||
GHOSTTY_MOUSE_PRESS,
|
mods: .init(nsFlags: event.modifierFlags)))
|
||||||
GHOSTTY_MOUSE_RIGHT,
|
|
||||||
mods
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue