Skip to main content

taskers_ghostty/
bridge.rs

1use std::{
2    ffi::CString,
3    path::PathBuf,
4};
5
6#[cfg(taskers_ghostty_bridge)]
7use std::{
8    ffi::{c_char, c_int, c_void},
9    ptr::NonNull,
10};
11
12#[cfg(taskers_ghostty_bridge)]
13use gtk::glib::translate::from_glib_full;
14#[cfg(taskers_ghostty_bridge)]
15use gtk::prelude::ObjectType;
16use gtk::Widget;
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
112            let options = taskers_ghostty_surface_options_s {
113                working_directory: cwd
114                    .as_ref()
115                    .map_or(std::ptr::null(), |value| value.as_ptr()),
116                title: title
117                    .as_ref()
118                    .map_or(std::ptr::null(), |value| value.as_ptr()),
119            };
120
121            let widget = (self.bridge.surface_new)(self.raw.as_ptr(), &options);
122            if widget.is_null() {
123                return Err(GhosttyError::SurfaceInit);
124            }
125
126            Ok(from_glib_full(widget.cast()))
127        }
128
129        #[cfg(not(taskers_ghostty_bridge))]
130        {
131            let _ = descriptor;
132            Err(GhosttyError::Unavailable)
133        }
134    }
135
136    pub fn focus_surface(&self, widget: &Widget) -> Result<(), GhosttyError> {
137        #[cfg(taskers_ghostty_bridge)]
138        unsafe {
139            let ok = (self.bridge.surface_grab_focus)(widget.as_ptr().cast());
140            if ok == 0 {
141                Err(GhosttyError::SurfaceInit)
142            } else {
143                Ok(())
144            }
145        }
146
147        #[cfg(not(taskers_ghostty_bridge))]
148        {
149            let _ = widget;
150            Err(GhosttyError::Unavailable)
151        }
152    }
153}
154
155#[cfg(taskers_ghostty_bridge)]
156impl Drop for GhosttyHost {
157    fn drop(&mut self) {
158        unsafe {
159            (self.bridge.host_free)(self.raw.as_ptr());
160        }
161    }
162}
163
164pub fn configure_runtime_environment() {
165    if std::env::var_os("GHOSTTY_RESOURCES_DIR").is_some() {
166        return;
167    }
168
169    if let Some(path) = installed_runtime_dir().filter(|path| path.exists()) {
170        unsafe {
171            std::env::set_var("GHOSTTY_RESOURCES_DIR", &path);
172        }
173        return;
174    }
175
176    if let Some(path) = option_env!("TASKERS_GHOSTTY_BUILD_RESOURCES_DIR")
177        .map(PathBuf::from)
178        .filter(|path| path.exists())
179    {
180        unsafe {
181            std::env::set_var("GHOSTTY_RESOURCES_DIR", &path);
182        }
183    }
184}
185
186pub fn runtime_resources_dir() -> Option<PathBuf> {
187    if let Some(path) = std::env::var_os("GHOSTTY_RESOURCES_DIR")
188        .map(PathBuf::from)
189        .filter(|path| path.exists())
190    {
191        return Some(path);
192    }
193
194    if let Some(path) = installed_runtime_dir().filter(|path| path.exists()) {
195        return Some(path);
196    }
197
198    option_env!("TASKERS_GHOSTTY_BUILD_RESOURCES_DIR")
199        .map(PathBuf::from)
200        .filter(|path| path.exists())
201}
202
203pub fn runtime_bridge_path() -> Option<PathBuf> {
204    if let Some(path) = std::env::var_os("TASKERS_GHOSTTY_BRIDGE_PATH")
205        .map(PathBuf::from)
206        .filter(|path| path.exists())
207    {
208        return Some(path);
209    }
210
211    if let Some(path) = installed_runtime_dir()
212        .map(|root| root.join("lib").join("libtaskers_ghostty_bridge.so"))
213        .filter(|path| path.exists())
214    {
215        return Some(path);
216    }
217
218    option_env!("TASKERS_GHOSTTY_BUILD_BRIDGE_PATH")
219        .map(PathBuf::from)
220        .filter(|path| path.exists())
221}
222
223fn installed_runtime_dir() -> Option<PathBuf> {
224    if let Some(path) = std::env::var_os("TASKERS_GHOSTTY_RUNTIME_DIR").map(PathBuf::from) {
225        return Some(path);
226    }
227
228    if let Some(path) = std::env::var_os("XDG_DATA_HOME")
229        .map(PathBuf::from)
230        .map(|path| path.join("taskers").join("ghostty"))
231    {
232        return Some(path);
233    }
234
235    std::env::var_os("HOME").map(PathBuf::from).map(|path| {
236        path.join(".local")
237            .join("share")
238            .join("taskers")
239            .join("ghostty")
240    })
241}
242
243#[cfg(taskers_ghostty_bridge)]
244fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
245    let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;
246    let library = unsafe {
247        Library::new(&path).map_err(|error| GhosttyError::LibraryLoad {
248            path: path.clone(),
249            message: error.to_string(),
250        })?
251    };
252
253    unsafe {
254        let host_new = *library
255            .get::<unsafe extern "C" fn() -> *mut taskers_ghostty_host_t>(
256                b"taskers_ghostty_host_new\0",
257            )
258            .map_err(|error| GhosttyError::LibraryLoad {
259                path: path.clone(),
260                message: error.to_string(),
261            })?;
262        let host_free = *library
263            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
264                b"taskers_ghostty_host_free\0",
265            )
266            .map_err(|error| GhosttyError::LibraryLoad {
267                path: path.clone(),
268                message: error.to_string(),
269            })?;
270        let host_tick = *library
271            .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int>(
272                b"taskers_ghostty_host_tick\0",
273            )
274            .map_err(|error| GhosttyError::LibraryLoad {
275                path: path.clone(),
276                message: error.to_string(),
277            })?;
278        let surface_new = *library
279            .get::<unsafe extern "C" fn(
280                *mut taskers_ghostty_host_t,
281                *const taskers_ghostty_surface_options_s,
282            ) -> *mut c_void>(b"taskers_ghostty_surface_new\0")
283            .map_err(|error| GhosttyError::LibraryLoad {
284                path: path.clone(),
285                message: error.to_string(),
286            })?;
287        let surface_grab_focus = *library
288            .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
289                b"taskers_ghostty_surface_grab_focus\0",
290            )
291            .map_err(|error| GhosttyError::LibraryLoad {
292                path: path.clone(),
293                message: error.to_string(),
294            })?;
295
296        Ok(GhosttyBridgeLibrary {
297            _library: library,
298            host_new,
299            host_free,
300            host_tick,
301            surface_new,
302            surface_grab_focus,
303        })
304    }
305}
306
307#[cfg(taskers_ghostty_bridge)]
308#[repr(C)]
309struct taskers_ghostty_host_t {
310    _private: [u8; 0],
311}
312
313#[cfg(taskers_ghostty_bridge)]
314#[repr(C)]
315struct taskers_ghostty_surface_options_s {
316    working_directory: *const c_char,
317    title: *const c_char,
318}