Skip to main content

fresh/services/gpm/
client.rs

1//! High-level GPM client for connecting to the daemon and reading events
2
3use super::ffi::{self, GpmConnect, GpmEventRaw, GpmLib};
4use super::types::{GpmButtons, GpmEvent, GpmModifiers};
5use std::io;
6use std::os::unix::io::RawFd;
7
8/// GPM client connection
9pub struct GpmClient {
10    fd: RawFd,
11    lib: &'static GpmLib,
12}
13
14impl GpmClient {
15    /// Try to connect to GPM daemon
16    ///
17    /// Returns `Ok(Some(client))` if connected successfully,
18    /// `Ok(None)` if GPM is not available (e.g., running in xterm, no GPM daemon, or libgpm not installed),
19    /// `Err` on unexpected error.
20    pub fn connect() -> io::Result<Option<Self>> {
21        tracing::debug!("GPM: Attempting to connect...");
22
23        // First check if libgpm is available
24        let Some(lib) = ffi::get_gpm_lib() else {
25            tracing::debug!("GPM: libgpm not available on this system");
26            return Ok(None);
27        };
28        tracing::debug!("GPM: libgpm loaded successfully");
29
30        // Check if we're on a Linux virtual console
31        let is_console = Self::is_linux_console();
32        tracing::debug!("GPM: is_linux_console() = {}", is_console);
33        if !is_console {
34            tracing::debug!("GPM: Not a Linux console, skipping GPM");
35            return Ok(None);
36        }
37
38        // Try to connect to the GPM daemon
39        let mut conn = GpmConnect {
40            // Request all mouse events
41            event_mask: ffi::GPM_MOVE
42                | ffi::GPM_DRAG
43                | ffi::GPM_DOWN
44                | ffi::GPM_UP
45                | ffi::GPM_SINGLE
46                | ffi::GPM_DOUBLE
47                | ffi::GPM_TRIPLE,
48            // Let GPM handle events we don't want (none in our case)
49            default_mask: 0,
50            // Accept events with any modifier combination
51            min_mod: 0,
52            max_mod: !0,
53            pid: 0, // Let GPM fill this
54            vc: 0,  // Current virtual console
55        };
56
57        tracing::debug!("GPM: Calling Gpm_Open...");
58        let result = lib.open(&mut conn);
59        tracing::debug!("GPM: Gpm_Open returned {}", result);
60
61        match result {
62            -2 => {
63                // Running in xterm or similar - use xterm mouse protocol instead
64                tracing::debug!("GPM: Reports xterm mode (-2), using standard mouse protocol");
65                Ok(None)
66            }
67            -1 => {
68                // Error connecting to GPM (daemon not running, permission denied, etc.)
69                let err = io::Error::last_os_error();
70                tracing::debug!("GPM: Connection failed (-1): {}", err);
71                Ok(None) // Don't treat as error, just fall back to no GPM
72            }
73            fd if fd >= 0 => {
74                tracing::info!("GPM: Connected successfully, fd={}", fd);
75                // Enable GPM's built-in pointer drawing
76                lib.set_visible_pointer(true);
77                Ok(Some(Self { fd, lib }))
78            }
79            _ => {
80                // Unexpected return value
81                tracing::warn!("GPM: Unexpected Gpm_Open return value: {}", result);
82                Ok(None)
83            }
84        }
85    }
86
87    /// Get the file descriptor for use with poll/select
88    pub fn fd(&self) -> RawFd {
89        self.fd
90    }
91
92    /// Read a GPM event (call only when poll indicates data is ready)
93    pub fn read_event(&self) -> io::Result<Option<GpmEvent>> {
94        let mut raw = GpmEventRaw {
95            buttons: 0,
96            modifiers: 0,
97            vc: 0,
98            dx: 0,
99            dy: 0,
100            x: 0,
101            y: 0,
102            event_type: 0,
103            clicks: 0,
104            margin: 0,
105            wdx: 0,
106            wdy: 0,
107        };
108
109        let result = self.lib.get_event(&mut raw);
110
111        match result {
112            1 => {
113                // Event received
114                let event = GpmEvent {
115                    buttons: GpmButtons(raw.buttons),
116                    modifiers: GpmModifiers(raw.modifiers),
117                    // GPM uses 1-based coordinates, convert to 0-based
118                    x: raw.x.saturating_sub(1),
119                    y: raw.y.saturating_sub(1),
120                    dx: raw.dx,
121                    dy: raw.dy,
122                    event_type: raw.event_type as u32,
123                    clicks: raw.clicks,
124                    wdx: raw.wdx,
125                    wdy: raw.wdy,
126                };
127                tracing::trace!(
128                    "GPM event: x={}, y={}, buttons={:?}, type=0x{:x}, wdy={}",
129                    event.x,
130                    event.y,
131                    event.buttons.0,
132                    event.event_type,
133                    event.wdy
134                );
135                Ok(Some(event))
136            }
137            0 => {
138                // No event available
139                Ok(None)
140            }
141            _ => {
142                // Error
143                Err(io::Error::last_os_error())
144            }
145        }
146    }
147
148    /// Check if we're running on a Linux virtual console (TTY)
149    fn is_linux_console() -> bool {
150        use std::fs;
151        use std::io;
152
153        // Check if stdin is a TTY
154        let is_tty = nix::unistd::isatty(io::stdin()).unwrap_or(false);
155        tracing::debug!("GPM: stdin isatty = {}", is_tty);
156        if !is_tty {
157            return false;
158        }
159
160        // Check if we're on a Linux virtual console (/dev/tty[0-9]+)
161        // by checking the TTY name
162        match fs::read_link("/proc/self/fd/0") {
163            Ok(tty_path) => {
164                let tty_str = tty_path.to_string_lossy();
165                tracing::debug!("GPM: stdin tty path = {}", tty_str);
166
167                // Linux virtual consoles are /dev/tty1, /dev/tty2, etc.
168                // Pseudo-terminals are /dev/pts/0, /dev/pts/1, etc.
169                if tty_str.starts_with("/dev/tty") && !tty_str.starts_with("/dev/ttyS") {
170                    // Check if it's a numbered tty (not just /dev/tty which is the controlling terminal)
171                    let suffix = &tty_str[8..];
172                    tracing::debug!("GPM: tty suffix = '{}'", suffix);
173                    if suffix.chars().all(|c| c.is_ascii_digit()) && !suffix.is_empty() {
174                        tracing::debug!("GPM: Detected Linux console: {}", tty_str);
175                        return true;
176                    }
177                }
178                tracing::debug!(
179                    "GPM: Not a Linux virtual console (tty path doesn't match pattern)"
180                );
181                false
182            }
183            Err(e) => {
184                tracing::debug!("GPM: Failed to read /proc/self/fd/0 link: {}", e);
185                false
186            }
187        }
188    }
189}
190
191impl Drop for GpmClient {
192    fn drop(&mut self) {
193        self.lib.close();
194        tracing::debug!("GPM connection closed");
195    }
196}