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;
22use crate::runtime::{configure_runtime_environment, runtime_bridge_path};
23
24#[derive(Debug, Error)]
25pub enum GhosttyError {
26 #[error("ghostty bridge is unavailable in this build")]
27 Unavailable,
28 #[error("failed to initialize ghostty host")]
29 HostInit,
30 #[error("failed to tick ghostty host")]
31 Tick,
32 #[error("failed to create ghostty surface")]
33 SurfaceInit,
34 #[error("surface metadata contains NUL bytes: {0}")]
35 InvalidString(&'static str),
36 #[error("failed to load ghostty bridge library from {path}: {message}")]
37 LibraryLoad { path: PathBuf, message: String },
38 #[error("ghostty bridge library path is unavailable")]
39 LibraryPathUnavailable,
40}
41
42#[cfg(taskers_ghostty_bridge)]
43pub struct GhosttyHost {
44 bridge: GhosttyBridgeLibrary,
45 raw: NonNull<taskers_ghostty_host_t>,
46}
47
48#[cfg(not(taskers_ghostty_bridge))]
49pub struct GhosttyHost;
50
51#[cfg(taskers_ghostty_bridge)]
52struct GhosttyBridgeLibrary {
53 _library: Library,
54 host_new: unsafe extern "C" fn() -> *mut taskers_ghostty_host_t,
55 host_free: unsafe extern "C" fn(*mut taskers_ghostty_host_t),
56 host_tick: unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int,
57 surface_new: unsafe extern "C" fn(
58 *mut taskers_ghostty_host_t,
59 *const taskers_ghostty_surface_options_s,
60 ) -> *mut c_void,
61 surface_grab_focus: unsafe extern "C" fn(*mut c_void) -> c_int,
62}
63
64impl GhosttyHost {
65 pub fn new() -> Result<Self, GhosttyError> {
66 configure_runtime_environment();
67
68 #[cfg(taskers_ghostty_bridge)]
69 unsafe {
70 let bridge = load_bridge_library()?;
71 let raw = (bridge.host_new)();
72 let raw = NonNull::new(raw).ok_or(GhosttyError::HostInit)?;
73 Ok(Self { bridge, raw })
74 }
75
76 #[cfg(not(taskers_ghostty_bridge))]
77 {
78 Err(GhosttyError::Unavailable)
79 }
80 }
81
82 pub fn tick(&self) -> Result<(), GhosttyError> {
83 #[cfg(taskers_ghostty_bridge)]
84 unsafe {
85 let ok = (self.bridge.host_tick)(self.raw.as_ptr());
86 if ok == 0 {
87 Err(GhosttyError::Tick)
88 } else {
89 Ok(())
90 }
91 }
92
93 #[cfg(not(taskers_ghostty_bridge))]
94 {
95 Err(GhosttyError::Unavailable)
96 }
97 }
98
99 pub fn create_surface(&self, descriptor: &SurfaceDescriptor) -> Result<Widget, GhosttyError> {
100 #[cfg(taskers_ghostty_bridge)]
101 unsafe {
102 let cwd = descriptor
103 .cwd
104 .as_deref()
105 .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("cwd")))
106 .transpose()?;
107 let title = descriptor
108 .title
109 .as_deref()
110 .map(|value| CString::new(value).map_err(|_| GhosttyError::InvalidString("title")))
111 .transpose()?;
112 let command_argv = descriptor
113 .command_argv
114 .iter()
115 .map(|value| {
116 CString::new(value.as_str())
117 .map_err(|_| GhosttyError::InvalidString("command_argv"))
118 })
119 .collect::<Result<Vec<_>, _>>()?;
120 let command_argv_ptrs = command_argv
121 .iter()
122 .map(|value| value.as_ptr())
123 .collect::<Vec<_>>();
124 let env_entries = descriptor
125 .env
126 .iter()
127 .map(|(key, value)| {
128 CString::new(format!("{key}={value}"))
129 .map_err(|_| GhosttyError::InvalidString("env"))
130 })
131 .collect::<Result<Vec<_>, _>>()?;
132 let env_entry_ptrs = env_entries
133 .iter()
134 .map(|value| value.as_ptr())
135 .collect::<Vec<_>>();
136
137 let options = taskers_ghostty_surface_options_s {
138 working_directory: cwd
139 .as_ref()
140 .map_or(std::ptr::null(), |value| value.as_ptr()),
141 title: title
142 .as_ref()
143 .map_or(std::ptr::null(), |value| value.as_ptr()),
144 command_argv: if command_argv_ptrs.is_empty() {
145 std::ptr::null()
146 } else {
147 command_argv_ptrs.as_ptr()
148 },
149 command_argc: command_argv_ptrs.len(),
150 env_entries: if env_entry_ptrs.is_empty() {
151 std::ptr::null()
152 } else {
153 env_entry_ptrs.as_ptr()
154 },
155 env_count: env_entry_ptrs.len(),
156 };
157
158 let widget = (self.bridge.surface_new)(self.raw.as_ptr(), &options);
159 if widget.is_null() {
160 return Err(GhosttyError::SurfaceInit);
161 }
162
163 Ok(from_glib_full(widget.cast()))
164 }
165
166 #[cfg(not(taskers_ghostty_bridge))]
167 {
168 let _ = descriptor;
169 Err(GhosttyError::Unavailable)
170 }
171 }
172
173 pub fn focus_surface(&self, widget: &Widget) -> Result<(), GhosttyError> {
174 #[cfg(taskers_ghostty_bridge)]
175 unsafe {
176 let ok = (self.bridge.surface_grab_focus)(widget.as_ptr().cast());
177 if ok == 0 {
178 Err(GhosttyError::SurfaceInit)
179 } else {
180 Ok(())
181 }
182 }
183
184 #[cfg(not(taskers_ghostty_bridge))]
185 {
186 let _ = widget;
187 Err(GhosttyError::Unavailable)
188 }
189 }
190}
191
192#[cfg(taskers_ghostty_bridge)]
193impl Drop for GhosttyHost {
194 fn drop(&mut self) {
195 unsafe {
196 (self.bridge.host_free)(self.raw.as_ptr());
197 }
198 }
199}
200
201#[cfg(taskers_ghostty_bridge)]
202fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
203 let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;
204 let library = unsafe {
205 Library::new(&path).map_err(|error| GhosttyError::LibraryLoad {
206 path: path.clone(),
207 message: error.to_string(),
208 })?
209 };
210
211 unsafe {
212 let host_new = *library
213 .get::<unsafe extern "C" fn() -> *mut taskers_ghostty_host_t>(
214 b"taskers_ghostty_host_new\0",
215 )
216 .map_err(|error| GhosttyError::LibraryLoad {
217 path: path.clone(),
218 message: error.to_string(),
219 })?;
220 let host_free = *library
221 .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t)>(
222 b"taskers_ghostty_host_free\0",
223 )
224 .map_err(|error| GhosttyError::LibraryLoad {
225 path: path.clone(),
226 message: error.to_string(),
227 })?;
228 let host_tick = *library
229 .get::<unsafe extern "C" fn(*mut taskers_ghostty_host_t) -> c_int>(
230 b"taskers_ghostty_host_tick\0",
231 )
232 .map_err(|error| GhosttyError::LibraryLoad {
233 path: path.clone(),
234 message: error.to_string(),
235 })?;
236 let surface_new = *library
237 .get::<unsafe extern "C" fn(
238 *mut taskers_ghostty_host_t,
239 *const taskers_ghostty_surface_options_s,
240 ) -> *mut c_void>(b"taskers_ghostty_surface_new\0")
241 .map_err(|error| GhosttyError::LibraryLoad {
242 path: path.clone(),
243 message: error.to_string(),
244 })?;
245 let surface_grab_focus = *library
246 .get::<unsafe extern "C" fn(*mut c_void) -> c_int>(
247 b"taskers_ghostty_surface_grab_focus\0",
248 )
249 .map_err(|error| GhosttyError::LibraryLoad {
250 path: path.clone(),
251 message: error.to_string(),
252 })?;
253
254 Ok(GhosttyBridgeLibrary {
255 _library: library,
256 host_new,
257 host_free,
258 host_tick,
259 surface_new,
260 surface_grab_focus,
261 })
262 }
263}
264
265#[cfg(taskers_ghostty_bridge)]
266#[repr(C)]
267struct taskers_ghostty_host_t {
268 _private: [u8; 0],
269}
270
271#[cfg(taskers_ghostty_bridge)]
272#[repr(C)]
273struct taskers_ghostty_surface_options_s {
274 working_directory: *const c_char,
275 title: *const c_char,
276 command_argv: *const *const c_char,
277 command_argc: usize,
278 env_entries: *const *const c_char,
279 env_count: usize,
280}