Skip to main content

libghostty_vt/
mouse.rs

1//! Encoding mouse events into terminal escape sequences.
2//!
3//! Supports X10, UTF-8, SGR, urxvt, and SGR-Pixels mouse protocols.
4//!
5//! # Basic Usage
6//!
7//!  1. Create an encoder instance with [`Encoder::new`].
8//!  2. Configure encoder options with the various `Encoder::with_*` methods
9//!     or [`Encoder::set_options_from_terminal`].
10//!  3. For each mouse event:
11//!     *  Create a mouse event with [`Event::new`] (or reuse an old one).
12//!     *  Set event properties (action, button, modifiers, position).
13//!     *  Encode with [`Encoder::encode_to_vec`] (with a growable `Vec` buffer)
14//!        or [`Encoder::encode`] (with a fixed byte buffer).
15
16use std::mem::MaybeUninit;
17
18use crate::{
19    alloc::{Allocator, Object},
20    error::{Error, Result, from_result, from_result_with_len},
21    ffi, key,
22    terminal::Terminal,
23};
24
25#[doc(inline)]
26pub use ffi::GhosttyMousePosition as Position;
27
28/// Mouse encoder that converts normalized mouse events into
29/// terminal escape sequences.
30#[derive(Debug)]
31pub struct Encoder<'alloc>(Object<'alloc, ffi::GhosttyMouseEncoder>);
32
33impl<'alloc> Encoder<'alloc> {
34    /// Create a new mouse encoder instance.
35    pub fn new() -> Result<Self> {
36        // SAFETY: A NULL allocator is always valid
37        unsafe { Self::new_inner(std::ptr::null()) }
38    }
39
40    /// Create a new mouse encoder instance with a custom allocator.
41    ///
42    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
43    /// regarding custom memory management and lifetimes.
44    pub fn new_with_alloc<'ctx: 'alloc, Ctx>(alloc: &'alloc Allocator<'ctx, Ctx>) -> Result<Self> {
45        // SAFETY: Borrow checking should forbid invalid allocators
46        unsafe { Self::new_inner(alloc.to_raw()) }
47    }
48
49    unsafe fn new_inner(alloc: *const ffi::GhosttyAllocator) -> Result<Self> {
50        let mut raw: ffi::GhosttyMouseEncoder_ptr = std::ptr::null_mut();
51        let result = unsafe { ffi::ghostty_mouse_encoder_new(alloc, &raw mut raw) };
52        from_result(result)?;
53        Ok(Self(Object::new(raw)?))
54    }
55
56    unsafe fn setopt(
57        &mut self,
58        option: ffi::GhosttyMouseEncoderOption,
59        value: *const std::ffi::c_void,
60    ) {
61        unsafe { ffi::ghostty_mouse_encoder_setopt(self.0.as_raw(), option, value) }
62    }
63
64    /// Encode a key event into a terminal escape sequence.
65    ///
66    /// Converts a key event into the appropriate terminal escape sequence
67    /// based on the encoder's current options. The provided `Vec` byte buffer
68    /// will be grown automatically if more capacity is needed.
69    ///
70    /// Not all key events produce output. For example, unmodified modifier
71    /// keys typically don't generate escape sequences. Check the returned
72    /// `usize` to determine if any data was written.
73    pub fn encode_to_vec(&mut self, event: &Event, vec: &mut Vec<u8>) -> Result<()> {
74        let remaining = vec.capacity() - vec.len();
75
76        let written = match self.encode_to_uninit_buf(event, vec.spare_capacity_mut()) {
77            Ok(v) => Ok(v),
78            Err(Error::OutOfSpace { required }) => {
79                // Retry with more capacity
80                vec.reserve(required - remaining);
81                self.encode_to_uninit_buf(event, vec.spare_capacity_mut())
82            }
83            Err(e) => Err(e),
84        };
85
86        // SAFETY: A successful call to `encode_to_uninit_buf` assures us
87        // that a `written` number of bytes have been initialized.
88        unsafe { vec.set_len(vec.len() + written?) };
89        Ok(())
90    }
91
92    /// Encode a mouse event into a terminal escape sequence.
93    ///
94    /// Not all mouse events produce output. In such cases this returns `Ok(0)`.
95    ///
96    /// If the output buffer is too small, this returns
97    /// `Err(Error::OutOfSpace { required })` where `required` is the required size.
98    pub fn encode(&mut self, event: &Event, buf: &mut [u8]) -> Result<usize> {
99        // SAFETY: It is always safe to reinterpret T as a MaybeUninit<T>.
100        self.encode_to_uninit_buf(event, unsafe {
101            std::slice::from_raw_parts_mut(buf.as_mut_ptr().cast(), buf.len())
102        })
103    }
104
105    fn encode_to_uninit_buf(
106        &mut self,
107        event: &Event,
108        buf: &mut [MaybeUninit<u8>],
109    ) -> Result<usize> {
110        let mut written: usize = 0;
111        let result = unsafe {
112            ffi::ghostty_mouse_encoder_encode(
113                self.0.as_raw(),
114                event.0.as_raw(),
115                buf.as_mut_ptr().cast(),
116                buf.len(),
117                &raw mut written,
118            )
119        };
120        from_result_with_len(result, written)
121    }
122
123    /// Set encoder options from a terminal's current state.
124    ///
125    /// This sets tracking mode and output format from terminal state.
126    /// It does not modify size or any-button state.
127    pub fn set_options_from_terminal(&mut self, terminal: &Terminal<'_, '_>) -> &mut Self {
128        unsafe {
129            ffi::ghostty_mouse_encoder_setopt_from_terminal(
130                self.0.as_raw(),
131                terminal.inner.as_raw(),
132            );
133        }
134        self
135    }
136    /// Set mouse tracking mode.
137    pub fn set_tracking_mode(&mut self, value: TrackingMode) -> &mut Self {
138        unsafe {
139            self.setopt(
140                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_EVENT,
141                std::ptr::from_ref(&value).cast(),
142            );
143        }
144        self
145    }
146    /// Set mouse output format.
147    pub fn set_format(&mut self, value: Format) -> &mut Self {
148        unsafe {
149            self.setopt(
150                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_EVENT,
151                std::ptr::from_ref(&value).cast(),
152            );
153        }
154        self
155    }
156    /// Set renderer size context.
157    pub fn set_size(&mut self, value: EncoderSize) -> &mut Self {
158        let raw: ffi::GhosttyMouseEncoderSize = value.into();
159        unsafe {
160            self.setopt(
161                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_SIZE,
162                std::ptr::from_ref(&raw).cast(),
163            );
164        }
165        self
166    }
167    /// Set whether any mouse button is currently pressed.
168    pub fn set_any_button_pressed(&mut self, value: bool) -> &mut Self {
169        unsafe {
170            self.setopt(
171                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED,
172                std::ptr::from_ref(&value).cast(),
173            );
174        }
175        self
176    }
177    /// Set whether to enable motion deduplication by last cell.
178    pub fn set_track_last_cell(&mut self, value: bool) -> &mut Self {
179        unsafe {
180            self.setopt(
181                ffi::GhosttyMouseEncoderOption_GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL,
182                std::ptr::from_ref(&value).cast(),
183            );
184        }
185        self
186    }
187
188    /// Reset internal encoder state.
189    ///
190    /// This clears motion deduplication state (last tracked cell).
191    pub fn reset(&mut self) {
192        unsafe { ffi::ghostty_mouse_encoder_reset(self.0.as_raw()) }
193    }
194}
195
196impl Drop for Encoder<'_> {
197    fn drop(&mut self) {
198        unsafe { ffi::ghostty_mouse_encoder_free(self.0.as_raw()) }
199    }
200}
201
202/// Normalized mouse input event containing action, button, modifiers, and
203/// surface-space position.
204#[derive(Debug)]
205pub struct Event<'alloc>(Object<'alloc, ffi::GhosttyMouseEvent>);
206
207impl<'alloc> Event<'alloc> {
208    /// Create a new mouse event instance.
209    pub fn new() -> Result<Self> {
210        // SAFETY: A NULL allocator is always valid
211        unsafe { Self::new_inner(std::ptr::null()) }
212    }
213
214    /// Create a new mouse event instance with a custom allocator.
215    ///
216    /// See the [crate-level documentation](crate#memory-management-and-lifetimes)
217    /// regarding custom memory management and lifetimes.
218    pub fn new_with_alloc<'ctx: 'alloc, Ctx>(alloc: &'alloc Allocator<'ctx, Ctx>) -> Result<Self> {
219        // SAFETY: Borrow checking should forbid invalid allocators
220        unsafe { Self::new_inner(alloc.to_raw()) }
221    }
222
223    unsafe fn new_inner(alloc: *const ffi::GhosttyAllocator) -> Result<Self> {
224        let mut raw: ffi::GhosttyMouseEvent_ptr = std::ptr::null_mut();
225        let result = unsafe { ffi::ghostty_mouse_event_new(alloc, &raw mut raw) };
226        from_result(result)?;
227        Ok(Self(Object::new(raw)?))
228    }
229
230    /// Set the event action.
231    pub fn set_action(&mut self, action: Action) -> &mut Self {
232        unsafe {
233            ffi::ghostty_mouse_event_set_action(self.0.as_raw(), action as ffi::GhosttyMouseAction);
234        }
235        self
236    }
237
238    /// Get the event action.
239    #[must_use]
240    pub fn action(&self) -> Action {
241        unsafe { ffi::ghostty_mouse_event_get_action(self.0.as_raw()) }
242            .try_into()
243            .unwrap_or(Action::Press)
244    }
245
246    /// Set the event button.
247    pub fn set_button(&mut self, button: Option<Button>) -> &mut Self {
248        if let Some(button) = button {
249            unsafe {
250                ffi::ghostty_mouse_event_set_button(
251                    self.0.as_raw(),
252                    button as ffi::GhosttyMouseButton,
253                );
254            }
255        } else {
256            unsafe { ffi::ghostty_mouse_event_clear_button(self.0.as_raw()) }
257        }
258        self
259    }
260
261    /// Get the event button.
262    #[must_use]
263    pub fn button(&self) -> Option<Button> {
264        let mut button: ffi::GhosttyMouseButton = 0;
265        let has_button =
266            unsafe { ffi::ghostty_mouse_event_get_button(self.0.as_raw(), &raw mut button) };
267        if has_button {
268            Some(button.try_into().unwrap_or(Button::Unknown))
269        } else {
270            None
271        }
272    }
273
274    /// Set keyboard modifiers held during the event.
275    pub fn set_mods(&mut self, mods: key::Mods) -> &mut Self {
276        unsafe { ffi::ghostty_mouse_event_set_mods(self.0.as_raw(), mods.bits()) }
277        self
278    }
279
280    /// Get keyboard modifiers held during the event.
281    #[must_use]
282    pub fn mods(&self) -> key::Mods {
283        key::Mods::from_bits_retain(unsafe { ffi::ghostty_mouse_event_get_mods(self.0.as_raw()) })
284    }
285
286    /// Set the event position in surface-space pixels.
287    pub fn set_position(&mut self, pos: Position) -> &mut Self {
288        unsafe { ffi::ghostty_mouse_event_set_position(self.0.as_raw(), pos) }
289        self
290    }
291
292    /// Get the event position in surface-space pixels.
293    #[must_use]
294    pub fn position(&self) -> Position {
295        unsafe { ffi::ghostty_mouse_event_get_position(self.0.as_raw()) }
296    }
297}
298
299impl Drop for Event<'_> {
300    fn drop(&mut self) {
301        unsafe { ffi::ghostty_mouse_event_free(self.0.as_raw()) }
302    }
303}
304
305/// Mouse tracking mode.
306#[repr(u32)]
307#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
308#[non_exhaustive]
309pub enum TrackingMode {
310    /// Mouse reporting disabled.
311    None = ffi::GhosttyMouseTrackingMode_GHOSTTY_MOUSE_TRACKING_NONE,
312    /// X10 mouse mode.
313    X10 = ffi::GhosttyMouseTrackingMode_GHOSTTY_MOUSE_TRACKING_X10,
314    /// Normal mouse mode (press/release only).
315    Normal = ffi::GhosttyMouseTrackingMode_GHOSTTY_MOUSE_TRACKING_NORMAL,
316    /// Button-event tracking mode.
317    Button = ffi::GhosttyMouseTrackingMode_GHOSTTY_MOUSE_TRACKING_BUTTON,
318    /// Any-event tracking mode.
319    Any = ffi::GhosttyMouseTrackingMode_GHOSTTY_MOUSE_TRACKING_ANY,
320}
321
322/// Mouse output format.
323#[repr(u32)]
324#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
325#[non_exhaustive]
326#[expect(missing_docs, reason = "missing upstream docs")]
327pub enum Format {
328    X10 = ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_X10,
329    Utf8 = ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_UTF8,
330    Sgr = ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_SGR,
331    Urxvt = ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_URXVT,
332    SgrPixels = ffi::GhosttyMouseFormat_GHOSTTY_MOUSE_FORMAT_SGR_PIXELS,
333}
334
335/// Mouse encoder size and geometry context.
336///
337/// This describes the rendered terminal geometry used to convert surface-space
338/// positions into encoded coordinates.
339#[derive(Clone, Copy, Debug, PartialEq, Eq)]
340pub struct EncoderSize {
341    /// Full screen width in pixels.
342    pub screen_width: u32,
343    /// Full screen height in pixels.
344    pub screen_height: u32,
345    /// Cell width in pixels. Must be non-zero.
346    pub cell_width: u32,
347    /// Cell height in pixels. Must be non-zero.
348    pub cell_height: u32,
349    /// Top padding in pixels.
350    pub padding_top: u32,
351    /// Bottom padding in pixels.
352    pub padding_bottom: u32,
353    /// Right padding in pixels.
354    pub padding_right: u32,
355    /// Left padding in pixels.
356    pub padding_left: u32,
357}
358
359impl From<EncoderSize> for ffi::GhosttyMouseEncoderSize {
360    fn from(value: EncoderSize) -> Self {
361        Self {
362            size: std::mem::size_of::<Self>(),
363            screen_width: value.screen_width,
364            screen_height: value.screen_height,
365            cell_width: value.cell_width,
366            cell_height: value.cell_height,
367            padding_top: value.padding_top,
368            padding_bottom: value.padding_bottom,
369            padding_right: value.padding_right,
370            padding_left: value.padding_left,
371        }
372    }
373}
374
375/// Mouse event action type.
376#[repr(u32)]
377#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
378#[non_exhaustive]
379pub enum Action {
380    /// Mouse button was pressed.
381    Press = ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_PRESS,
382    /// Mouse button was released.
383    Release = ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_RELEASE,
384    /// Mouse moved.
385    Motion = ffi::GhosttyMouseAction_GHOSTTY_MOUSE_ACTION_MOTION,
386}
387
388/// Mouse event action identity.
389#[repr(u32)]
390#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
391#[non_exhaustive]
392#[expect(missing_docs, reason = "self-explanatory")]
393pub enum Button {
394    Unknown = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_UNKNOWN,
395    Left = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_LEFT,
396    Right = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_RIGHT,
397    Middle = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_MIDDLE,
398    Four = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_FOUR,
399    Five = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_FIVE,
400    Six = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SIX,
401    Seven = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_SEVEN,
402    Eight = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_EIGHT,
403    Nine = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_NINE,
404    Ten = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_TEN,
405    Eleven = ffi::GhosttyMouseButton_GHOSTTY_MOUSE_BUTTON_ELEVEN,
406}