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