taskers_ghostty/
bridge.rs1use 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}