Skip to main content

taskers_ghostty/
bridge.rs

1use std::{
2    ffi::{CString, c_char},
3    path::PathBuf,
4};
5
6#[cfg(taskers_ghostty_bridge)]
7use std::{
8    ffi::{c_int, c_void},
9    ptr::NonNull,
10    slice,
11};
12
13use gtk::Widget;
14#[cfg(taskers_ghostty_bridge)]
15use gtk::glib::translate::from_glib_full;
16#[cfg(taskers_ghostty_bridge)]
17use gtk::prelude::ObjectType;
18#[cfg(taskers_ghostty_bridge)]
19use libloading::Library;
20use thiserror::Error;
21
22use crate::backend::{GhosttyHostOptions, SurfaceDescriptor};
23use crate::runtime::{configure_runtime_environment, runtime_bridge_path};
24
25#[derive(Debug, Error)]
26pub enum GhosttyError {
27    #[error("ghostty bridge is unavailable in this build")]
28    Unavailable,
29    #[error("failed to initialize ghostty host")]
30    HostInit,
31    #[error("failed to tick ghostty host")]
32    Tick,
33    #[error("failed to create ghostty surface")]
34    SurfaceInit,
35    #[error("failed to read text from ghostty surface")]
36    SurfaceReadText,
37    #[error("surface metadata contains NUL bytes: {0}")]
38    InvalidString(&'static str),
39    #[error("failed to load ghostty bridge library from {path}: {message}")]
40    LibraryLoad { path: PathBuf, message: String },
41    #[error("ghostty bridge library path is unavailable")]
42    LibraryPathUnavailable,
43}
44
45#[cfg(taskers_ghostty_bridge)]
46pub struct GhosttyHost {
47    bridge: GhosttyBridgeLibrary,
48    raw: NonNull<taskers_ghostty_host_t>,
49}
50
51#[cfg(not(taskers_ghostty_bridge))]
52pub struct GhosttyHost;
53
54#[cfg(taskers_ghostty_bridge)]
55struct GhosttyBridgeLibrary {
56    _library: Library,
57    host_new:
58        unsafe extern "C" fn(*const taskers_ghostty_host_options_s) -> *mut taskers_ghostty_host_t,
59    host_free: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
60    host_tick: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int,
61    surface_new: unsafe extern "C" fn(
62        *mut taskers_ghostty_host_t,
63        *const taskers_ghostty_surface_options_s,
64    ) -> *mut c_void,
65    surface_grab_focus: unsafe extern "C" fn(*mut c_void) -> c_int,
66    surface_has_selection: unsafe extern "C" fn(*mut c_void) -> c_int,
67    surface_read_all_text: unsafe extern "C" fn(*mut c_void, *mut taskers_ghostty_text_s) -> c_int,
68    surface_free_text: unsafe extern "C" fn(*mut taskers_ghostty_text_s),
69}
70
71impl GhosttyHost {
72    pub fn new() -> Result<Self, GhosttyError> {
73        Self::new_with_options(&GhosttyHostOptions::default())
74    }
75
76    pub fn new_with_options(options: &GhosttyHostOptions) -> Result<Self, GhosttyError> {
77        configure_runtime_environment();
78
79        #[cfg(taskers_ghostty_bridge)]
80        unsafe {
81            let bridge = load_bridge_library()?;
82            let command_argv = options
83                .command_argv
84                .iter()
85                .map(|value| {
86                    CString::new(value.as_str())
87                        .map_err(|_| GhosttyError::InvalidString("command_argv"))
88                })
89                .collect::<Result<Vec<_>, _>>()?;
90            let command_argv_ptrs = command_argv
91                .iter()
92                .map(|value| value.as_ptr())
93                .collect::<Vec<_>>();
94            let env_entries = options
95                .env
96                .iter()
97                .map(|(key, value)| {
98                    CString::new(format!("{key}={value}"))
99                        .map_err(|_| GhosttyError::InvalidString("env"))
100                })
101                .collect::<Result<Vec<_>, _>>()?;
102            let env_entry_ptrs = env_entries
103                .iter()
104                .map(|value| value.as_ptr())
105                .collect::<Vec<_>>();
106            let host_options = taskers_ghostty_host_options_s {
107                command_argv: if command_argv_ptrs.is_empty() {
108                    std::ptr::null()
109                } else {
110                    command_argv_ptrs.as_ptr()
111                },
112                command_argc: command_argv_ptrs.len(),
113                env_entries: if env_entry_ptrs.is_empty() {
114                    std::ptr::null()
115                } else {
116                    env_entry_ptrs.as_ptr()
117                },
118                env_count: env_entry_ptrs.len(),
119            };
120
121            let raw = (bridge.host_new)(&host_options);
122            let raw = NonNull::new(raw).ok_or(GhosttyError::HostInit)?;
123            Ok(Self { bridge, raw })
124        }
125
126        #[cfg(not(taskers_ghostty_bridge))]
127        {
128            let _ = options;
129            Err(GhosttyError::Unavailable)
130        }
131    }
132
133    pub fn tick(&self) -> Result<(), GhosttyError> {
134        #[cfg(taskers_ghostty_bridge)]
135        unsafe {
136            let ok = (self.bridge.host_tick)(self.raw.as_ptr());
137            if ok == 0 {
138                Err(GhosttyError::Tick)
139            } else {
140                Ok(())
141            }
142        }
143
144        #[cfg(not(taskers_ghostty_bridge))]
145        {
146            Err(GhosttyError::Unavailable)
147        }
148    }
149
150    pub fn create_surface(&self, descriptor: &SurfaceDescriptor) -> Result<Widget, GhosttyError> {
151        #[cfg(taskers_ghostty_bridge)]
152        unsafe {
153            let cwd = descriptor
154                .cwd
155                .as_deref()
156                .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("cwd")))
157                .transpose()?;
158            let title = descriptor
159                .title
160                .as_deref()
161                .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("title")))
162                .transpose()?;
163            let env_entries = descriptor
164                .env
165                .iter()
166                .map(|(key, value)| {
167                    CString::new(format!("{key}={value}"))
168                        .map_err(|_| GhosttyError::InvalidString("env"))
169                })
170                .collect::<Result<Vec<_>, _>>()?;
171            let env_entry_ptrs = env_entries
172                .iter()
173                .map(|value| value.as_ptr())
174                .collect::<Vec<_>>();
175
176            let options = taskers_ghostty_surface_options_s {
177                working_directory: cwd
178                    .as_ref()
179                    .map_or(std::ptr::null(), |value| value.as_ptr()),
180                title: title
181                    .as_ref()
182                    .map_or(std::ptr::null(), |value| value.as_ptr()),
183                env_entries: if env_entry_ptrs.is_empty() {
184                    std::ptr::null()
185                } else {
186                    env_entry_ptrs.as_ptr()
187                },
188                env_count: env_entry_ptrs.len(),
189            };
190
191            let widget = (self.bridge.surface_new)(self.raw.as_ptr(), &options);
192            if widget.is_null() {
193                return Err(GhosttyError::SurfaceInit);
194            }
195
196            Ok(from_glib_full(widget.cast()))
197        }
198
199        #[cfg(not(taskers_ghostty_bridge))]
200        {
201            let _ = descriptor;
202            Err(GhosttyError::Unavailable)
203        }
204    }
205
206    pub fn focus_surface(&self, widget: &Widget) -> Result<(), GhosttyError> {
207        #[cfg(taskers_ghostty_bridge)]
208        unsafe {
209            let ok = (self.bridge.surface_grab_focus)(widget.as_ptr().cast());
210            if ok == 0 {
211                Err(GhosttyError::SurfaceInit)
212            } else {
213                Ok(())
214            }
215        }
216
217        #[cfg(not(taskers_ghostty_bridge))]
218        {
219            let _ = widget;
220            Err(GhosttyError::Unavailable)
221        }
222    }
223
224    pub fn surface_has_selection(&self, widget: &Widget) -> Result<bool, GhosttyError> {
225        #[cfg(taskers_ghostty_bridge)]
226        unsafe {
227            Ok((self.bridge.surface_has_selection)(widget.as_ptr().cast()) != 0)
228        }
229
230        #[cfg(not(taskers_ghostty_bridge))]
231        {
232            let _ = widget;
233            Err(GhosttyError::Unavailable)
234        }
235    }
236
237    pub fn read_surface_text(&self, widget: &Widget) -> Result<String, GhosttyError> {
238        #[cfg(taskers_ghostty_bridge)]
239        unsafe {
240            let mut text = taskers_ghostty_text_s::default();
241            let ok = (self.bridge.surface_read_all_text)(widget.as_ptr().cast(), &mut text);
242            if ok == 0 {
243                return Err(GhosttyError::SurfaceReadText);
244            }
245
246            let bytes = if text.text.is_null() || text.text_len == 0 {
247                &[]
248            } else {
249                slice::from_raw_parts(text.text.cast::<u8>(), text.text_len)
250            };
251            let output = String::from_utf8_lossy(bytes).into_owned();
252            (self.bridge.surface_free_text)(&mut text);
253            Ok(output)
254        }
255
256        #[cfg(not(taskers_ghostty_bridge))]
257        {
258            let _ = widget;
259            Err(GhosttyError::Unavailable)
260        }
261    }
262}
263
264#[cfg(taskers_ghostty_bridge)]
265impl Drop for GhosttyHost {
266    fn drop(&mut self) {
267        unsafe {
268            (self.bridge.host_free)(self.raw.as_ptr());
269        }
270    }
271}
272
273#[cfg(taskers_ghostty_bridge)]
274fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
275    let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;
276    let library = unsafe {
277        Library::new(&path).map_err(|error| GhosttyError::LibraryLoad {
278            path: path.clone(),
279            message: error.to_string(),
280        })?
281    };
282
283    unsafe {
284        let host_new = *library
285            .get::<unsafe extern "C" fn(
286                *const taskers_ghostty_host_options_s,
287            ) -> *mut taskers_ghostty_host_t>(b"taskers_ghostty_host_new\0")
288            .map_err(|error| GhosttyError::LibraryLoad {
289                path: path.clone(),
290                message: error.to_string(),
291            })?;
292        let host_free = *library
293            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
294                b"taskers_ghostty_host_free\0",
295            )
296            .map_err(|error| GhosttyError::LibraryLoad {
297                path: path.clone(),
298                message: error.to_string(),
299            })?;
300        let host_tick = *library
301            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int>(
302                b"taskers_ghostty_host_tick\0",
303            )
304            .map_err(|error| GhosttyError::LibraryLoad {
305                path: path.clone(),
306                message: error.to_string(),
307            })?;
308        let surface_new = *library
309            .get::<unsafe extern "C" fn(
310                *mut taskers_ghostty_host_t,
311                *const taskers_ghostty_surface_options_s,
312            ) -> *mut c_void>(b"taskers_ghostty_surface_new\0")
313            .map_err(|error| GhosttyError::LibraryLoad {
314                path: path.clone(),
315                message: error.to_string(),
316            })?;
317        let surface_grab_focus = *library
318            .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
319                b"taskers_ghostty_surface_grab_focus\0",
320            )
321            .map_err(|error| GhosttyError::LibraryLoad {
322                path: path.clone(),
323                message: error.to_string(),
324            })?;
325        let surface_has_selection = *library
326            .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
327                b"taskers_ghostty_surface_has_selection\0",
328            )
329            .map_err(|error| GhosttyError::LibraryLoad {
330                path: path.clone(),
331                message: error.to_string(),
332            })?;
333        let surface_read_all_text = *library
334            .get::<unsafe extern "C" fn(*mut c_void, *mut taskers_ghostty_text_s) -> c_int>(
335                b"taskers_ghostty_surface_read_all_text\0",
336            )
337            .map_err(|error| GhosttyError::LibraryLoad {
338                path: path.clone(),
339                message: error.to_string(),
340            })?;
341        let surface_free_text = *library
342            .get::<unsafe extern "C" fn(*mut taskers_ghostty_text_s)>(
343                b"taskers_ghostty_surface_free_text\0",
344            )
345            .map_err(|error| GhosttyError::LibraryLoad {
346                path: path.clone(),
347                message: error.to_string(),
348            })?;
349
350        Ok(GhosttyBridgeLibrary {
351            _library: library,
352            host_new,
353            host_free,
354            host_tick,
355            surface_new,
356            surface_grab_focus,
357            surface_has_selection,
358            surface_read_all_text,
359            surface_free_text,
360        })
361    }
362}
363
364#[cfg(taskers_ghostty_bridge)]
365#[repr(C)]
366struct taskers_ghostty_host_t {
367    _private: [u8; 0],
368}
369
370#[cfg(taskers_ghostty_bridge)]
371#[repr(C)]
372struct taskers_ghostty_host_options_s {
373    command_argv: *const *const c_char,
374    command_argc: usize,
375    env_entries: *const *const c_char,
376    env_count: usize,
377}
378
379#[cfg(taskers_ghostty_bridge)]
380#[repr(C)]
381struct taskers_ghostty_surface_options_s {
382    working_directory: *const c_char,
383    title: *const c_char,
384    env_entries: *const *const c_char,
385    env_count: usize,
386}
387
388#[cfg(taskers_ghostty_bridge)]
389#[repr(C)]
390#[derive(Default)]
391struct taskers_ghostty_text_s {
392    text: *const c_char,
393    text_len: usize,
394}