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