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};
11
12use gtk::Widget;
13#[cfg(taskers_ghostty_bridge)]
14use gtk::glib::translate::from_glib_full;
15#[cfg(taskers_ghostty_bridge)]
16use gtk::prelude::ObjectType;
17#[cfg(taskers_ghostty_bridge)]
18use libloading::Library;
19use thiserror::Error;
20
21use crate::backend::SurfaceDescriptor;
22use crate::runtime::{configure_runtime_environment, runtime_bridge_path};
23
24#[derive(Debug, Error)]
25pub enum GhosttyError {
26    #[error("ghostty bridge is unavailable in this build")]
27    Unavailable,
28    #[error("failed to initialize ghostty host")]
29    HostInit,
30    #[error("failed to tick ghostty host")]
31    Tick,
32    #[error("failed to create ghostty surface")]
33    SurfaceInit,
34    #[error("surface metadata contains NUL bytes: {0}")]
35    InvalidString(&'static str),
36    #[error("failed to load ghostty bridge library from {path}: {message}")]
37    LibraryLoad { path: PathBuf, message: String },
38    #[error("ghostty bridge library path is unavailable")]
39    LibraryPathUnavailable,
40}
41
42#[cfg(taskers_ghostty_bridge)]
43pub struct GhosttyHost {
44    bridge: GhosttyBridgeLibrary,
45    raw: NonNull<taskers_ghostty_host_t>,
46}
47
48#[cfg(not(taskers_ghostty_bridge))]
49pub struct GhosttyHost;
50
51#[cfg(taskers_ghostty_bridge)]
52struct GhosttyBridgeLibrary {
53    _library: Library,
54    host_new: unsafe extern "C" fn() -> *mut taskers_ghostty_host_t,
55    host_free: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
56    host_tick: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int,
57    surface_new: unsafe extern "C" fn(
58        *mut taskers_ghostty_host_t,
59        *const taskers_ghostty_surface_options_s,
60    ) -> *mut c_void,
61    surface_grab_focus: unsafe extern "C" fn(*mut c_void) -> c_int,
62}
63
64impl GhosttyHost {
65    pub fn new() -> Result<Self, GhosttyError> {
66        configure_runtime_environment();
67
68        #[cfg(taskers_ghostty_bridge)]
69        unsafe {
70            let bridge = load_bridge_library()?;
71            let raw = (bridge.host_new)();
72            let raw = NonNull::new(raw).ok_or(GhosttyError::HostInit)?;
73            Ok(Self { bridge, raw })
74        }
75
76        #[cfg(not(taskers_ghostty_bridge))]
77        {
78            Err(GhosttyError::Unavailable)
79        }
80    }
81
82    pub fn tick(&self) -> Result<(), GhosttyError> {
83        #[cfg(taskers_ghostty_bridge)]
84        unsafe {
85            let ok = (self.bridge.host_tick)(self.raw.as_ptr());
86            if ok == 0 {
87                Err(GhosttyError::Tick)
88            } else {
89                Ok(())
90            }
91        }
92
93        #[cfg(not(taskers_ghostty_bridge))]
94        {
95            Err(GhosttyError::Unavailable)
96        }
97    }
98
99    pub fn create_surface(&self, descriptor: &SurfaceDescriptor) -> Result<Widget, GhosttyError> {
100        #[cfg(taskers_ghostty_bridge)]
101        unsafe {
102            let cwd = descriptor
103                .cwd
104                .as_deref()
105                .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("cwd")))
106                .transpose()?;
107            let title = descriptor
108                .title
109                .as_deref()
110                .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("title")))
111                .transpose()?;
112            let command_argv = descriptor
113                .command_argv
114                .iter()
115                .map(|value| {
116                    CString::new(value.as_str())
117                        .map_err(|_| GhosttyError::InvalidString("command_argv"))
118                })
119                .collect::<Result<Vec<_>, _>>()?;
120            let command_argv_ptrs = command_argv
121                .iter()
122                .map(|value| value.as_ptr())
123                .collect::<Vec<_>>();
124            let env_entries = descriptor
125                .env
126                .iter()
127                .map(|(key, value)| {
128                    CString::new(format!("{key}={value}"))
129                        .map_err(|_| GhosttyError::InvalidString("env"))
130                })
131                .collect::<Result<Vec<_>, _>>()?;
132            let env_entry_ptrs = env_entries
133                .iter()
134                .map(|value| value.as_ptr())
135                .collect::<Vec<_>>();
136
137            let options = taskers_ghostty_surface_options_s {
138                working_directory: cwd
139                    .as_ref()
140                    .map_or(std::ptr::null(), |value| value.as_ptr()),
141                title: title
142                    .as_ref()
143                    .map_or(std::ptr::null(), |value| value.as_ptr()),
144                command_argv: if command_argv_ptrs.is_empty() {
145                    std::ptr::null()
146                } else {
147                    command_argv_ptrs.as_ptr()
148                },
149                command_argc: command_argv_ptrs.len(),
150                env_entries: if env_entry_ptrs.is_empty() {
151                    std::ptr::null()
152                } else {
153                    env_entry_ptrs.as_ptr()
154                },
155                env_count: env_entry_ptrs.len(),
156            };
157
158            let widget = (self.bridge.surface_new)(self.raw.as_ptr(), &options);
159            if widget.is_null() {
160                return Err(GhosttyError::SurfaceInit);
161            }
162
163            Ok(from_glib_full(widget.cast()))
164        }
165
166        #[cfg(not(taskers_ghostty_bridge))]
167        {
168            let _ = descriptor;
169            Err(GhosttyError::Unavailable)
170        }
171    }
172
173    pub fn focus_surface(&self, widget: &Widget) -> Result<(), GhosttyError> {
174        #[cfg(taskers_ghostty_bridge)]
175        unsafe {
176            let ok = (self.bridge.surface_grab_focus)(widget.as_ptr().cast());
177            if ok == 0 {
178                Err(GhosttyError::SurfaceInit)
179            } else {
180                Ok(())
181            }
182        }
183
184        #[cfg(not(taskers_ghostty_bridge))]
185        {
186            let _ = widget;
187            Err(GhosttyError::Unavailable)
188        }
189    }
190}
191
192#[cfg(taskers_ghostty_bridge)]
193impl Drop for GhosttyHost {
194    fn drop(&mut self) {
195        unsafe {
196            (self.bridge.host_free)(self.raw.as_ptr());
197        }
198    }
199}
200
201#[cfg(taskers_ghostty_bridge)]
202fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
203    let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;
204    let library = unsafe {
205        Library::new(&path).map_err(|error| GhosttyError::LibraryLoad {
206            path: path.clone(),
207            message: error.to_string(),
208        })?
209    };
210
211    unsafe {
212        let host_new = *library
213            .get::<unsafe extern "C" fn() -> *mut taskers_ghostty_host_t>(
214                b"taskers_ghostty_host_new\0",
215            )
216            .map_err(|error| GhosttyError::LibraryLoad {
217                path: path.clone(),
218                message: error.to_string(),
219            })?;
220        let host_free = *library
221            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
222                b"taskers_ghostty_host_free\0",
223            )
224            .map_err(|error| GhosttyError::LibraryLoad {
225                path: path.clone(),
226                message: error.to_string(),
227            })?;
228        let host_tick = *library
229            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int>(
230                b"taskers_ghostty_host_tick\0",
231            )
232            .map_err(|error| GhosttyError::LibraryLoad {
233                path: path.clone(),
234                message: error.to_string(),
235            })?;
236        let surface_new = *library
237            .get::<unsafe extern "C" fn(
238                *mut taskers_ghostty_host_t,
239                *const taskers_ghostty_surface_options_s,
240            ) -> *mut c_void>(b"taskers_ghostty_surface_new\0")
241            .map_err(|error| GhosttyError::LibraryLoad {
242                path: path.clone(),
243                message: error.to_string(),
244            })?;
245        let surface_grab_focus = *library
246            .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
247                b"taskers_ghostty_surface_grab_focus\0",
248            )
249            .map_err(|error| GhosttyError::LibraryLoad {
250                path: path.clone(),
251                message: error.to_string(),
252            })?;
253
254        Ok(GhosttyBridgeLibrary {
255            _library: library,
256            host_new,
257            host_free,
258            host_tick,
259            surface_new,
260            surface_grab_focus,
261        })
262    }
263}
264
265#[cfg(taskers_ghostty_bridge)]
266#[repr(C)]
267struct taskers_ghostty_host_t {
268    _private: [u8; 0],
269}
270
271#[cfg(taskers_ghostty_bridge)]
272#[repr(C)]
273struct taskers_ghostty_surface_options_s {
274    working_directory: *const c_char,
275    title: *const c_char,
276    command_argv: *const *const c_char,
277    command_argc: usize,
278    env_entries: *const *const c_char,
279    env_count: usize,
280}