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