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, Clone, PartialEq, Eq)]
26pub struct GhosttyBridgeInfo {
27 pub version: String,
28 pub build_id: String,
29}
30
31#[derive(Debug, Error)]
32pub enum GhosttyError {
33 #[error("ghostty bridge is unavailable in this build")]
34 Unavailable,
35 #[error("failed to initialize ghostty host")]
36 HostInit,
37 #[error("failed to tick ghostty host")]
38 Tick,
39 #[error("failed to create ghostty surface")]
40 SurfaceInit,
41 #[error("failed to read text from ghostty surface")]
42 SurfaceReadText,
43 #[error("failed to write text to ghostty surface")]
44 SurfaceWriteText,
45 #[error("surface metadata contains NUL bytes: {0}")]
46 InvalidString(&'static str),
47 #[error("failed to load ghostty bridge library from {path}: {message}")]
48 LibraryLoad { path: PathBuf, message: String },
49 #[error("ghostty bridge library path is unavailable")]
50 LibraryPathUnavailable,
51}
52
53#[cfg(taskers_ghostty_bridge)]
54pub struct GhosttyHost {
55 bridge: GhosttyBridgeLibrary,
56 raw: NonNull<taskers_ghostty_host_t>,
57}
58
59#[cfg(not(taskers_ghostty_bridge))]
60pub struct GhosttyHost;
61
62#[cfg(taskers_ghostty_bridge)]
63struct GhosttyBridgeLibrary {
64 _library: Library,
65 host_new:
66 unsafe extern "C" fn(*const taskers_ghostty_host_options_s) -> *mut taskers_ghostty_host_t,
67 host_free: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
68 host_version: unsafe extern "C" fn() -> *const c_char,
69 host_build_id: unsafe extern "C" fn() -> *const c_char,
70 host_begin_shutdown: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
71 host_surface_count: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> usize,
72 host_tick: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int,
73 surface_new: unsafe extern "C" fn(
74 *mut taskers_ghostty_host_t,
75 *const taskers_ghostty_surface_options_s,
76 ) -> *mut c_void,
77 surface_destroy: unsafe extern "C" fn(*mut c_void),
78 surface_grab_focus: unsafe extern "C" fn(*mut c_void) -> c_int,
79 surface_has_selection: unsafe extern "C" fn(*mut c_void) -> c_int,
80 surface_send_text: unsafe extern "C" fn(*mut c_void, *const c_char, usize) -> c_int,
81 surface_read_all_text: unsafe extern "C" fn(*mut c_void, *mut taskers_ghostty_text_s) -> c_int,
82 surface_free_text: unsafe extern "C" fn(*mut taskers_ghostty_text_s),
83}
84
85impl GhosttyHost {
86 pub fn new() -> Result<Self, GhosttyError> {
87 Self::new_with_options(&GhosttyHostOptions::default())
88 }
89
90 pub fn new_with_options(options: &GhosttyHostOptions) -> Result<Self, GhosttyError> {
91 configure_runtime_environment();
92
93 #[cfg(taskers_ghostty_bridge)]
94 unsafe {
95 let bridge = load_bridge_library()?;
96 let command_argv = options
97 .command_argv
98 .iter()
99 .map(|value| {
100 CString::new(value.as_str())
101 .map_err(|_| GhosttyError::InvalidString("command_argv"))
102 })
103 .collect::<Result<Vec<_>, _>>()?;
104 let command_argv_ptrs = command_argv
105 .iter()
106 .map(|value| value.as_ptr())
107 .collect::<Vec<_>>();
108 let env_entries = options
109 .env
110 .iter()
111 .map(|(key, value)| {
112 CString::new(format!("{key}={value}"))
113 .map_err(|_| GhosttyError::InvalidString("env"))
114 })
115 .collect::<Result<Vec<_>, _>>()?;
116 let embedded_terminal_appearance =
117 CString::new(match options.embedded_terminal_appearance {
118 crate::backend::EmbeddedTerminalAppearance::Taskers => "taskers",
119 crate::backend::EmbeddedTerminalAppearance::Ghostty => "ghostty",
120 })
121 .map_err(|_| GhosttyError::InvalidString("embedded_terminal_appearance"))?;
122 let env_entry_ptrs = env_entries
123 .iter()
124 .map(|value| value.as_ptr())
125 .collect::<Vec<_>>();
126 let host_options = taskers_ghostty_host_options_s {
127 command_argv: if command_argv_ptrs.is_empty() {
128 std::ptr::null()
129 } else {
130 command_argv_ptrs.as_ptr()
131 },
132 command_argc: command_argv_ptrs.len(),
133 env_entries: if env_entry_ptrs.is_empty() {
134 std::ptr::null()
135 } else {
136 env_entry_ptrs.as_ptr()
137 },
138 env_count: env_entry_ptrs.len(),
139 embedded_terminal_appearance: embedded_terminal_appearance.as_ptr(),
140 };
141
142 let raw = (bridge.host_new)(&host_options);
143 let raw = NonNull::new(raw).ok_or(GhosttyError::HostInit)?;
144 Ok(Self { bridge, raw })
145 }
146
147 #[cfg(not(taskers_ghostty_bridge))]
148 {
149 let _ = options;
150 Err(GhosttyError::Unavailable)
151 }
152 }
153
154 pub fn tick(&self) -> Result<(), GhosttyError> {
155 #[cfg(taskers_ghostty_bridge)]
156 unsafe {
157 let ok = (self.bridge.host_tick)(self.raw.as_ptr());
158 if ok == 0 {
159 Err(GhosttyError::Tick)
160 } else {
161 Ok(())
162 }
163 }
164
165 #[cfg(not(taskers_ghostty_bridge))]
166 {
167 Err(GhosttyError::Unavailable)
168 }
169 }
170
171 pub fn bridge_info(&self) -> GhosttyBridgeInfo {
172 #[cfg(taskers_ghostty_bridge)]
173 unsafe {
174 let version = std::ffi::CStr::from_ptr((self.bridge.host_version)())
175 .to_string_lossy()
176 .into_owned();
177 let build_id = std::ffi::CStr::from_ptr((self.bridge.host_build_id)())
178 .to_string_lossy()
179 .into_owned();
180 GhosttyBridgeInfo { version, build_id }
181 }
182
183 #[cfg(not(taskers_ghostty_bridge))]
184 {
185 GhosttyBridgeInfo {
186 version: "unavailable".into(),
187 build_id: "unavailable".into(),
188 }
189 }
190 }
191
192 pub fn begin_shutdown(&self) {
193 #[cfg(taskers_ghostty_bridge)]
194 unsafe {
195 (self.bridge.host_begin_shutdown)(self.raw.as_ptr());
196 }
197 }
198
199 pub fn surface_count(&self) -> usize {
200 #[cfg(taskers_ghostty_bridge)]
201 unsafe {
202 (self.bridge.host_surface_count)(self.raw.as_ptr())
203 }
204
205 #[cfg(not(taskers_ghostty_bridge))]
206 {
207 0
208 }
209 }
210
211 pub fn create_surface(&self, descriptor: &SurfaceDescriptor) -> Result<Widget, GhosttyError> {
212 #[cfg(taskers_ghostty_bridge)]
213 unsafe {
214 let cwd = descriptor
215 .cwd
216 .as_deref()
217 .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("cwd")))
218 .transpose()?;
219 let title = descriptor
220 .title
221 .as_deref()
222 .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("title")))
223 .transpose()?;
224 let env_entries = descriptor
225 .env
226 .iter()
227 .map(|(key, value)| {
228 CString::new(format!("{key}={value}"))
229 .map_err(|_| GhosttyError::InvalidString("env"))
230 })
231 .collect::<Result<Vec<_>, _>>()?;
232 let env_entry_ptrs = env_entries
233 .iter()
234 .map(|value| value.as_ptr())
235 .collect::<Vec<_>>();
236
237 let options = taskers_ghostty_surface_options_s {
238 working_directory: cwd
239 .as_ref()
240 .map_or(std::ptr::null(), |value| value.as_ptr()),
241 title: title
242 .as_ref()
243 .map_or(std::ptr::null(), |value| value.as_ptr()),
244 env_entries: if env_entry_ptrs.is_empty() {
245 std::ptr::null()
246 } else {
247 env_entry_ptrs.as_ptr()
248 },
249 env_count: env_entry_ptrs.len(),
250 };
251
252 let widget = (self.bridge.surface_new)(self.raw.as_ptr(), &options);
253 if widget.is_null() {
254 return Err(GhosttyError::SurfaceInit);
255 }
256
257 Ok(from_glib_full(widget.cast()))
258 }
259
260 #[cfg(not(taskers_ghostty_bridge))]
261 {
262 let _ = descriptor;
263 Err(GhosttyError::Unavailable)
264 }
265 }
266
267 pub fn focus_surface(&self, widget: &Widget) -> Result<(), GhosttyError> {
268 #[cfg(taskers_ghostty_bridge)]
269 unsafe {
270 let ok = (self.bridge.surface_grab_focus)(widget.as_ptr().cast());
271 if ok == 0 {
272 Err(GhosttyError::SurfaceInit)
273 } else {
274 Ok(())
275 }
276 }
277
278 #[cfg(not(taskers_ghostty_bridge))]
279 {
280 let _ = widget;
281 Err(GhosttyError::Unavailable)
282 }
283 }
284
285 pub fn destroy_surface(&self, widget: &Widget) {
286 #[cfg(taskers_ghostty_bridge)]
287 unsafe {
288 (self.bridge.surface_destroy)(widget.as_ptr().cast());
289 }
290 }
291
292 pub fn surface_has_selection(&self, widget: &Widget) -> Result<bool, GhosttyError> {
293 #[cfg(taskers_ghostty_bridge)]
294 unsafe {
295 Ok((self.bridge.surface_has_selection)(widget.as_ptr().cast()) != 0)
296 }
297
298 #[cfg(not(taskers_ghostty_bridge))]
299 {
300 let _ = widget;
301 Err(GhosttyError::Unavailable)
302 }
303 }
304
305 pub fn read_surface_text(&self, widget: &Widget) -> Result<String, GhosttyError> {
306 #[cfg(taskers_ghostty_bridge)]
307 unsafe {
308 let mut text = taskers_ghostty_text_s::default();
309 let ok = (self.bridge.surface_read_all_text)(widget.as_ptr().cast(), &mut text);
310 if ok == 0 {
311 return Err(GhosttyError::SurfaceReadText);
312 }
313
314 let bytes = if text.text.is_null() || text.text_len == 0 {
315 &[]
316 } else {
317 slice::from_raw_parts(text.text.cast::<u8>(), text.text_len)
318 };
319 let output = String::from_utf8_lossy(bytes).into_owned();
320 (self.bridge.surface_free_text)(&mut text);
321 Ok(output)
322 }
323
324 #[cfg(not(taskers_ghostty_bridge))]
325 {
326 let _ = widget;
327 Err(GhosttyError::Unavailable)
328 }
329 }
330
331 pub fn send_surface_text(&self, widget: &Widget, text: &str) -> Result<(), GhosttyError> {
332 #[cfg(taskers_ghostty_bridge)]
333 unsafe {
334 let text =
335 CString::new(text).map_err(|_| GhosttyError::InvalidString("surface_text"))?;
336 let ok = (self.bridge.surface_send_text)(
337 widget.as_ptr().cast(),
338 text.as_ptr(),
339 text.as_bytes().len(),
340 );
341 if ok == 0 {
342 Err(GhosttyError::SurfaceWriteText)
343 } else {
344 Ok(())
345 }
346 }
347
348 #[cfg(not(taskers_ghostty_bridge))]
349 {
350 let _ = (widget, text);
351 Err(GhosttyError::Unavailable)
352 }
353 }
354}
355
356#[cfg(taskers_ghostty_bridge)]
357impl Drop for GhosttyHost {
358 fn drop(&mut self) {
359 unsafe {
360 (self.bridge.host_free)(self.raw.as_ptr());
361 }
362 }
363}
364
365#[cfg(taskers_ghostty_bridge)]
366fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
367 let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;
368 let library = unsafe {
369 Library::new(&path).map_err(|error| GhosttyError::LibraryLoad {
370 path: path.clone(),
371 message: error.to_string(),
372 })?
373 };
374
375 unsafe {
376 let host_new = *library
377 .get::<unsafe extern "C" fn(
378 *const taskers_ghostty_host_options_s,
379 ) -> *mut taskers_ghostty_host_t>(b"taskers_ghostty_host_new\0")
380 .map_err(|error| GhosttyError::LibraryLoad {
381 path: path.clone(),
382 message: error.to_string(),
383 })?;
384 let host_free = *library
385 .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
386 b"taskers_ghostty_host_free\0",
387 )
388 .map_err(|error| GhosttyError::LibraryLoad {
389 path: path.clone(),
390 message: error.to_string(),
391 })?;
392 let host_version = *library
393 .get::<unsafe extern "C" fn() -> *const c_char>(b"taskers_ghostty_host_version\0")
394 .map_err(|error| GhosttyError::LibraryLoad {
395 path: path.clone(),
396 message: error.to_string(),
397 })?;
398 let host_build_id = *library
399 .get::<unsafe extern "C" fn() -> *const c_char>(b"taskers_ghostty_host_build_id\0")
400 .map_err(|error| GhosttyError::LibraryLoad {
401 path: path.clone(),
402 message: error.to_string(),
403 })?;
404 let host_begin_shutdown = *library
405 .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
406 b"taskers_ghostty_host_begin_shutdown\0",
407 )
408 .map_err(|error| GhosttyError::LibraryLoad {
409 path: path.clone(),
410 message: error.to_string(),
411 })?;
412 let host_surface_count = *library
413 .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> usize>(
414 b"taskers_ghostty_host_surface_count\0",
415 )
416 .map_err(|error| GhosttyError::LibraryLoad {
417 path: path.clone(),
418 message: error.to_string(),
419 })?;
420 let host_tick = *library
421 .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int>(
422 b"taskers_ghostty_host_tick\0",
423 )
424 .map_err(|error| GhosttyError::LibraryLoad {
425 path: path.clone(),
426 message: error.to_string(),
427 })?;
428 let surface_new = *library
429 .get::<unsafe extern "C" fn(
430 *mut taskers_ghostty_host_t,
431 *const taskers_ghostty_surface_options_s,
432 ) -> *mut c_void>(b"taskers_ghostty_surface_new\0")
433 .map_err(|error| GhosttyError::LibraryLoad {
434 path: path.clone(),
435 message: error.to_string(),
436 })?;
437 let surface_destroy = *library
438 .get::<unsafe extern "C" fn(*mut c_void)>(b"taskers_ghostty_surface_destroy\0")
439 .map_err(|error| GhosttyError::LibraryLoad {
440 path: path.clone(),
441 message: error.to_string(),
442 })?;
443 let surface_grab_focus = *library
444 .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
445 b"taskers_ghostty_surface_grab_focus\0",
446 )
447 .map_err(|error| GhosttyError::LibraryLoad {
448 path: path.clone(),
449 message: error.to_string(),
450 })?;
451 let surface_has_selection = *library
452 .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
453 b"taskers_ghostty_surface_has_selection\0",
454 )
455 .map_err(|error| GhosttyError::LibraryLoad {
456 path: path.clone(),
457 message: error.to_string(),
458 })?;
459 let surface_send_text = *library
460 .get::<unsafe extern "C" fn(*mut c_void, *const c_char, usize) -> c_int>(
461 b"taskers_ghostty_surface_send_text\0",
462 )
463 .map_err(|error| GhosttyError::LibraryLoad {
464 path: path.clone(),
465 message: error.to_string(),
466 })?;
467 let surface_read_all_text = *library
468 .get::<unsafe extern "C" fn(*mut c_void, *mut taskers_ghostty_text_s) -> c_int>(
469 b"taskers_ghostty_surface_read_all_text\0",
470 )
471 .map_err(|error| GhosttyError::LibraryLoad {
472 path: path.clone(),
473 message: error.to_string(),
474 })?;
475 let surface_free_text = *library
476 .get::<unsafe extern "C" fn(*mut taskers_ghostty_text_s)>(
477 b"taskers_ghostty_surface_free_text\0",
478 )
479 .map_err(|error| GhosttyError::LibraryLoad {
480 path: path.clone(),
481 message: error.to_string(),
482 })?;
483
484 Ok(GhosttyBridgeLibrary {
485 _library: library,
486 host_new,
487 host_free,
488 host_version,
489 host_build_id,
490 host_begin_shutdown,
491 host_surface_count,
492 host_tick,
493 surface_new,
494 surface_destroy,
495 surface_grab_focus,
496 surface_has_selection,
497 surface_send_text,
498 surface_read_all_text,
499 surface_free_text,
500 })
501 }
502}
503
504#[cfg(taskers_ghostty_bridge)]
505#[repr(C)]
506struct taskers_ghostty_host_t {
507 _private: [u8; 0],
508}
509
510#[cfg(taskers_ghostty_bridge)]
511#[repr(C)]
512struct taskers_ghostty_host_options_s {
513 command_argv: *const *const c_char,
514 command_argc: usize,
515 env_entries: *const *const c_char,
516 env_count: usize,
517 embedded_terminal_appearance: *const c_char,
518}
519
520#[cfg(taskers_ghostty_bridge)]
521#[repr(C)]
522struct taskers_ghostty_surface_options_s {
523 working_directory: *const c_char,
524 title: *const c_char,
525 env_entries: *const *const c_char,
526 env_count: usize,
527}
528
529#[cfg(taskers_ghostty_bridge)]
530#[repr(C)]
531#[derive(Default)]
532struct taskers_ghostty_text_s {
533 text: *const c_char,
534 text_len: usize,
535}