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