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, Clone, PartialEq, Eq)]
26pub struct GhosttyBridgeInfo {
27    pub version: String,
28    pub build_id: String,
29}
30
31#[derive(Debug, Error)]
32pub enum GhosttyError {
33    #[error("ghostty bridge is unavailable in this build")]
34    Unavailable,
35    #[error("failed to initialize ghostty host")]
36    HostInit,
37    #[error("failed to tick ghostty host")]
38    Tick,
39    #[error("failed to create ghostty surface")]
40    SurfaceInit,
41    #[error("failed to read text from ghostty surface")]
42    SurfaceReadText,
43    #[error("failed to write text to ghostty surface")]
44    SurfaceWriteText,
45    #[error("surface metadata contains NUL bytes: {0}")]
46    InvalidString(&'static str),
47    #[error("failed to load ghostty bridge library from {path}: {message}")]
48    LibraryLoad { path: PathBuf, message: String },
49    #[error("ghostty bridge library path is unavailable")]
50    LibraryPathUnavailable,
51}
52
53#[cfg(taskers_ghostty_bridge)]
54pub struct GhosttyHost {
55    bridge: GhosttyBridgeLibrary,
56    raw: NonNull<taskers_ghostty_host_t>,
57}
58
59#[cfg(not(taskers_ghostty_bridge))]
60pub struct GhosttyHost;
61
62#[cfg(taskers_ghostty_bridge)]
63struct GhosttyBridgeLibrary {
64    _library: Library,
65    host_new:
66        unsafe extern "C" fn(*const taskers_ghostty_host_options_s) -> *mut taskers_ghostty_host_t,
67    host_free: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
68    host_version: unsafe extern "C" fn() -> *const c_char,
69    host_build_id: unsafe extern "C" fn() -> *const c_char,
70    host_begin_shutdown: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
71    host_surface_count: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> usize,
72    host_tick: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int,
73    surface_new: unsafe extern "C" fn(
74        *mut taskers_ghostty_host_t,
75        *const taskers_ghostty_surface_options_s,
76    ) -> *mut c_void,
77    surface_destroy: unsafe extern "C" fn(*mut c_void),
78    surface_grab_focus: unsafe extern "C" fn(*mut c_void) -> c_int,
79    surface_has_selection: unsafe extern "C" fn(*mut c_void) -> c_int,
80    surface_send_text: unsafe extern "C" fn(*mut c_void, *const c_char, usize) -> c_int,
81    surface_read_all_text: unsafe extern "C" fn(*mut c_void, *mut taskers_ghostty_text_s) -> c_int,
82    surface_free_text: unsafe extern "C" fn(*mut taskers_ghostty_text_s),
83}
84
85impl GhosttyHost {
86    pub fn new() -> Result<Self, GhosttyError> {
87        Self::new_with_options(&GhosttyHostOptions::default())
88    }
89
90    pub fn new_with_options(options: &GhosttyHostOptions) -> Result<Self, GhosttyError> {
91        configure_runtime_environment();
92
93        #[cfg(taskers_ghostty_bridge)]
94        unsafe {
95            let bridge = load_bridge_library()?;
96            let command_argv = options
97                .command_argv
98                .iter()
99                .map(|value| {
100                    CString::new(value.as_str())
101                        .map_err(|_| GhosttyError::InvalidString("command_argv"))
102                })
103                .collect::<Result<Vec<_>, _>>()?;
104            let command_argv_ptrs = command_argv
105                .iter()
106                .map(|value| value.as_ptr())
107                .collect::<Vec<_>>();
108            let env_entries = options
109                .env
110                .iter()
111                .map(|(key, value)| {
112                    CString::new(format!("{key}={value}"))
113                        .map_err(|_| GhosttyError::InvalidString("env"))
114                })
115                .collect::<Result<Vec<_>, _>>()?;
116            let embedded_terminal_appearance =
117                CString::new(match options.embedded_terminal_appearance {
118                    crate::backend::EmbeddedTerminalAppearance::Taskers => "taskers",
119                    crate::backend::EmbeddedTerminalAppearance::Ghostty => "ghostty",
120                })
121                .map_err(|_| GhosttyError::InvalidString("embedded_terminal_appearance"))?;
122            let env_entry_ptrs = env_entries
123                .iter()
124                .map(|value| value.as_ptr())
125                .collect::<Vec<_>>();
126            let host_options = taskers_ghostty_host_options_s {
127                command_argv: if command_argv_ptrs.is_empty() {
128                    std::ptr::null()
129                } else {
130                    command_argv_ptrs.as_ptr()
131                },
132                command_argc: command_argv_ptrs.len(),
133                env_entries: if env_entry_ptrs.is_empty() {
134                    std::ptr::null()
135                } else {
136                    env_entry_ptrs.as_ptr()
137                },
138                env_count: env_entry_ptrs.len(),
139                embedded_terminal_appearance: embedded_terminal_appearance.as_ptr(),
140            };
141
142            let raw = (bridge.host_new)(&host_options);
143            let raw = NonNull::new(raw).ok_or(GhosttyError::HostInit)?;
144            Ok(Self { bridge, raw })
145        }
146
147        #[cfg(not(taskers_ghostty_bridge))]
148        {
149            let _ = options;
150            Err(GhosttyError::Unavailable)
151        }
152    }
153
154    pub fn tick(&self) -> Result<(), GhosttyError> {
155        #[cfg(taskers_ghostty_bridge)]
156        unsafe {
157            let ok = (self.bridge.host_tick)(self.raw.as_ptr());
158            if ok == 0 {
159                Err(GhosttyError::Tick)
160            } else {
161                Ok(())
162            }
163        }
164
165        #[cfg(not(taskers_ghostty_bridge))]
166        {
167            Err(GhosttyError::Unavailable)
168        }
169    }
170
171    pub fn bridge_info(&self) -> GhosttyBridgeInfo {
172        #[cfg(taskers_ghostty_bridge)]
173        unsafe {
174            let version = std::ffi::CStr::from_ptr((self.bridge.host_version)())
175                .to_string_lossy()
176                .into_owned();
177            let build_id = std::ffi::CStr::from_ptr((self.bridge.host_build_id)())
178                .to_string_lossy()
179                .into_owned();
180            GhosttyBridgeInfo { version, build_id }
181        }
182
183        #[cfg(not(taskers_ghostty_bridge))]
184        {
185            GhosttyBridgeInfo {
186                version: "unavailable".into(),
187                build_id: "unavailable".into(),
188            }
189        }
190    }
191
192    pub fn begin_shutdown(&self) {
193        #[cfg(taskers_ghostty_bridge)]
194        unsafe {
195            (self.bridge.host_begin_shutdown)(self.raw.as_ptr());
196        }
197    }
198
199    pub fn surface_count(&self) -> usize {
200        #[cfg(taskers_ghostty_bridge)]
201        unsafe {
202            (self.bridge.host_surface_count)(self.raw.as_ptr())
203        }
204
205        #[cfg(not(taskers_ghostty_bridge))]
206        {
207            0
208        }
209    }
210
211    pub fn create_surface(&self, descriptor: &SurfaceDescriptor) -> Result<Widget, GhosttyError> {
212        #[cfg(taskers_ghostty_bridge)]
213        unsafe {
214            let cwd = descriptor
215                .cwd
216                .as_deref()
217                .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("cwd")))
218                .transpose()?;
219            let title = descriptor
220                .title
221                .as_deref()
222                .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("title")))
223                .transpose()?;
224            let env_entries = descriptor
225                .env
226                .iter()
227                .map(|(key, value)| {
228                    CString::new(format!("{key}={value}"))
229                        .map_err(|_| GhosttyError::InvalidString("env"))
230                })
231                .collect::<Result<Vec<_>, _>>()?;
232            let env_entry_ptrs = env_entries
233                .iter()
234                .map(|value| value.as_ptr())
235                .collect::<Vec<_>>();
236
237            let options = taskers_ghostty_surface_options_s {
238                working_directory: cwd
239                    .as_ref()
240                    .map_or(std::ptr::null(), |value| value.as_ptr()),
241                title: title
242                    .as_ref()
243                    .map_or(std::ptr::null(), |value| value.as_ptr()),
244                env_entries: if env_entry_ptrs.is_empty() {
245                    std::ptr::null()
246                } else {
247                    env_entry_ptrs.as_ptr()
248                },
249                env_count: env_entry_ptrs.len(),
250            };
251
252            let widget = (self.bridge.surface_new)(self.raw.as_ptr(), &options);
253            if widget.is_null() {
254                return Err(GhosttyError::SurfaceInit);
255            }
256
257            Ok(from_glib_full(widget.cast()))
258        }
259
260        #[cfg(not(taskers_ghostty_bridge))]
261        {
262            let _ = descriptor;
263            Err(GhosttyError::Unavailable)
264        }
265    }
266
267    pub fn focus_surface(&self, widget: &Widget) -> Result<(), GhosttyError> {
268        #[cfg(taskers_ghostty_bridge)]
269        unsafe {
270            let ok = (self.bridge.surface_grab_focus)(widget.as_ptr().cast());
271            if ok == 0 {
272                Err(GhosttyError::SurfaceInit)
273            } else {
274                Ok(())
275            }
276        }
277
278        #[cfg(not(taskers_ghostty_bridge))]
279        {
280            let _ = widget;
281            Err(GhosttyError::Unavailable)
282        }
283    }
284
285    pub fn destroy_surface(&self, widget: &Widget) {
286        #[cfg(taskers_ghostty_bridge)]
287        unsafe {
288            (self.bridge.surface_destroy)(widget.as_ptr().cast());
289        }
290    }
291
292    pub fn surface_has_selection(&self, widget: &Widget) -> Result<bool, GhosttyError> {
293        #[cfg(taskers_ghostty_bridge)]
294        unsafe {
295            Ok((self.bridge.surface_has_selection)(widget.as_ptr().cast()) != 0)
296        }
297
298        #[cfg(not(taskers_ghostty_bridge))]
299        {
300            let _ = widget;
301            Err(GhosttyError::Unavailable)
302        }
303    }
304
305    pub fn read_surface_text(&self, widget: &Widget) -> Result<String, GhosttyError> {
306        #[cfg(taskers_ghostty_bridge)]
307        unsafe {
308            let mut text = taskers_ghostty_text_s::default();
309            let ok = (self.bridge.surface_read_all_text)(widget.as_ptr().cast(), &mut text);
310            if ok == 0 {
311                return Err(GhosttyError::SurfaceReadText);
312            }
313
314            let bytes = if text.text.is_null() || text.text_len == 0 {
315                &[]
316            } else {
317                slice::from_raw_parts(text.text.cast::<u8>(), text.text_len)
318            };
319            let output = String::from_utf8_lossy(bytes).into_owned();
320            (self.bridge.surface_free_text)(&mut text);
321            Ok(output)
322        }
323
324        #[cfg(not(taskers_ghostty_bridge))]
325        {
326            let _ = widget;
327            Err(GhosttyError::Unavailable)
328        }
329    }
330
331    pub fn send_surface_text(&self, widget: &Widget, text: &str) -> Result<(), GhosttyError> {
332        #[cfg(taskers_ghostty_bridge)]
333        unsafe {
334            let text =
335                CString::new(text).map_err(|_| GhosttyError::InvalidString("surface_text"))?;
336            let ok = (self.bridge.surface_send_text)(
337                widget.as_ptr().cast(),
338                text.as_ptr(),
339                text.as_bytes().len(),
340            );
341            if ok == 0 {
342                Err(GhosttyError::SurfaceWriteText)
343            } else {
344                Ok(())
345            }
346        }
347
348        #[cfg(not(taskers_ghostty_bridge))]
349        {
350            let _ = (widget, text);
351            Err(GhosttyError::Unavailable)
352        }
353    }
354}
355
356#[cfg(taskers_ghostty_bridge)]
357impl Drop for GhosttyHost {
358    fn drop(&mut self) {
359        unsafe {
360            (self.bridge.host_free)(self.raw.as_ptr());
361        }
362    }
363}
364
365#[cfg(taskers_ghostty_bridge)]
366fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
367    let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;
368    let library = unsafe {
369        Library::new(&path).map_err(|error| GhosttyError::LibraryLoad {
370            path: path.clone(),
371            message: error.to_string(),
372        })?
373    };
374
375    unsafe {
376        let host_new = *library
377            .get::<unsafe extern "C" fn(
378                *const taskers_ghostty_host_options_s,
379            ) -> *mut taskers_ghostty_host_t>(b"taskers_ghostty_host_new\0")
380            .map_err(|error| GhosttyError::LibraryLoad {
381                path: path.clone(),
382                message: error.to_string(),
383            })?;
384        let host_free = *library
385            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
386                b"taskers_ghostty_host_free\0",
387            )
388            .map_err(|error| GhosttyError::LibraryLoad {
389                path: path.clone(),
390                message: error.to_string(),
391            })?;
392        let host_version = *library
393            .get::<unsafe extern "C" fn() -> *const c_char>(b"taskers_ghostty_host_version\0")
394            .map_err(|error| GhosttyError::LibraryLoad {
395                path: path.clone(),
396                message: error.to_string(),
397            })?;
398        let host_build_id = *library
399            .get::<unsafe extern "C" fn() -> *const c_char>(b"taskers_ghostty_host_build_id\0")
400            .map_err(|error| GhosttyError::LibraryLoad {
401                path: path.clone(),
402                message: error.to_string(),
403            })?;
404        let host_begin_shutdown = *library
405            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
406                b"taskers_ghostty_host_begin_shutdown\0",
407            )
408            .map_err(|error| GhosttyError::LibraryLoad {
409                path: path.clone(),
410                message: error.to_string(),
411            })?;
412        let host_surface_count = *library
413            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> usize>(
414                b"taskers_ghostty_host_surface_count\0",
415            )
416            .map_err(|error| GhosttyError::LibraryLoad {
417                path: path.clone(),
418                message: error.to_string(),
419            })?;
420        let host_tick = *library
421            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int>(
422                b"taskers_ghostty_host_tick\0",
423            )
424            .map_err(|error| GhosttyError::LibraryLoad {
425                path: path.clone(),
426                message: error.to_string(),
427            })?;
428        let surface_new = *library
429            .get::<unsafe extern "C" fn(
430                *mut taskers_ghostty_host_t,
431                *const taskers_ghostty_surface_options_s,
432            ) -> *mut c_void>(b"taskers_ghostty_surface_new\0")
433            .map_err(|error| GhosttyError::LibraryLoad {
434                path: path.clone(),
435                message: error.to_string(),
436            })?;
437        let surface_destroy = *library
438            .get::<unsafe extern "C" fn(*mut c_void)>(b"taskers_ghostty_surface_destroy\0")
439            .map_err(|error| GhosttyError::LibraryLoad {
440                path: path.clone(),
441                message: error.to_string(),
442            })?;
443        let surface_grab_focus = *library
444            .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
445                b"taskers_ghostty_surface_grab_focus\0",
446            )
447            .map_err(|error| GhosttyError::LibraryLoad {
448                path: path.clone(),
449                message: error.to_string(),
450            })?;
451        let surface_has_selection = *library
452            .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
453                b"taskers_ghostty_surface_has_selection\0",
454            )
455            .map_err(|error| GhosttyError::LibraryLoad {
456                path: path.clone(),
457                message: error.to_string(),
458            })?;
459        let surface_send_text = *library
460            .get::<unsafe extern "C" fn(*mut c_void, *const c_char, usize) -> c_int>(
461                b"taskers_ghostty_surface_send_text\0",
462            )
463            .map_err(|error| GhosttyError::LibraryLoad {
464                path: path.clone(),
465                message: error.to_string(),
466            })?;
467        let surface_read_all_text = *library
468            .get::<unsafe extern "C" fn(*mut c_void, *mut taskers_ghostty_text_s) -> c_int>(
469                b"taskers_ghostty_surface_read_all_text\0",
470            )
471            .map_err(|error| GhosttyError::LibraryLoad {
472                path: path.clone(),
473                message: error.to_string(),
474            })?;
475        let surface_free_text = *library
476            .get::<unsafe extern "C" fn(*mut taskers_ghostty_text_s)>(
477                b"taskers_ghostty_surface_free_text\0",
478            )
479            .map_err(|error| GhosttyError::LibraryLoad {
480                path: path.clone(),
481                message: error.to_string(),
482            })?;
483
484        Ok(GhosttyBridgeLibrary {
485            _library: library,
486            host_new,
487            host_free,
488            host_version,
489            host_build_id,
490            host_begin_shutdown,
491            host_surface_count,
492            host_tick,
493            surface_new,
494            surface_destroy,
495            surface_grab_focus,
496            surface_has_selection,
497            surface_send_text,
498            surface_read_all_text,
499            surface_free_text,
500        })
501    }
502}
503
504#[cfg(taskers_ghostty_bridge)]
505#[repr(C)]
506struct taskers_ghostty_host_t {
507    _private: [u8; 0],
508}
509
510#[cfg(taskers_ghostty_bridge)]
511#[repr(C)]
512struct taskers_ghostty_host_options_s {
513    command_argv: *const *const c_char,
514    command_argc: usize,
515    env_entries: *const *const c_char,
516    env_count: usize,
517    embedded_terminal_appearance: *const c_char,
518}
519
520#[cfg(taskers_ghostty_bridge)]
521#[repr(C)]
522struct taskers_ghostty_surface_options_s {
523    working_directory: *const c_char,
524    title: *const c_char,
525    env_entries: *const *const c_char,
526    env_count: usize,
527}
528
529#[cfg(taskers_ghostty_bridge)]
530#[repr(C)]
531#[derive(Default)]
532struct taskers_ghostty_text_s {
533    text: *const c_char,
534    text_len: usize,
535}