Skip to main content

taskers_ghostty/
backend.rs

1use crate::runtime::{runtime_bridge_path, runtime_resources_dir};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum BackendChoice {
9    Auto,
10    Ghostty,
11    GhosttyEmbedded,
12    Mock,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum BackendAvailability {
18    Ready,
19    Fallback,
20    Unavailable,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct BackendProbe {
25    pub requested: BackendChoice,
26    pub selected: BackendChoice,
27    pub availability: BackendAvailability,
28    pub notes: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct SurfaceDescriptor {
33    pub cols: u16,
34    pub rows: u16,
35    pub cwd: Option<String>,
36    pub title: Option<String>,
37    #[serde(default)]
38    pub command_argv: Vec<String>,
39    #[serde(default)]
40    pub env: BTreeMap<String, String>,
41}
42
43#[derive(Debug, Error)]
44pub enum AdapterError {
45    #[error("terminal backend is unavailable: {0}")]
46    Unavailable(String),
47    #[error("terminal backend initialization failed: {0}")]
48    Initialization(String),
49}
50
51pub trait TerminalBackend {
52    fn probe(requested: BackendChoice) -> BackendProbe;
53}
54
55pub struct DefaultBackend;
56
57impl TerminalBackend for DefaultBackend {
58    fn probe(requested: BackendChoice) -> BackendProbe {
59        let env_override = std::env::var("TASKERS_TERMINAL_BACKEND").ok();
60        let requested = match env_override.as_deref() {
61            Some("ghostty") => BackendChoice::Ghostty,
62            Some("ghostty_embedded") | Some("ghostty-embedded") => BackendChoice::GhosttyEmbedded,
63            Some("mock") => BackendChoice::Mock,
64            _ => requested,
65        };
66
67        match requested {
68            BackendChoice::Auto => auto_probe(requested),
69            BackendChoice::Mock => BackendProbe {
70                requested,
71                selected: BackendChoice::Mock,
72                availability: BackendAvailability::Fallback,
73                notes: "Using placeholder terminal surfaces.".into(),
74            },
75            BackendChoice::GhosttyEmbedded => BackendProbe {
76                requested,
77                selected: BackendChoice::GhosttyEmbedded,
78                availability: embedded_ghostty_availability(),
79                notes: embedded_ghostty_notes(),
80            },
81            BackendChoice::Ghostty => BackendProbe {
82                requested,
83                selected: BackendChoice::Ghostty,
84                availability: ghostty_availability(),
85                notes: ghostty_notes(),
86            },
87        }
88    }
89}
90
91fn auto_probe(requested: BackendChoice) -> BackendProbe {
92    let availability = ghostty_availability();
93    if matches!(availability, BackendAvailability::Ready) {
94        BackendProbe {
95            requested,
96            selected: BackendChoice::Ghostty,
97            availability,
98            notes: ghostty_notes(),
99        }
100    } else {
101        BackendProbe {
102            requested,
103            selected: BackendChoice::Mock,
104            availability: BackendAvailability::Fallback,
105            notes: "Ghostty bridge unavailable, using placeholder terminal surfaces.".into(),
106        }
107    }
108}
109
110fn ghostty_availability() -> BackendAvailability {
111    #[cfg(all(target_os = "linux", taskers_ghostty_bridge))]
112    {
113        if runtime_bridge_path().is_some() {
114            BackendAvailability::Ready
115        } else {
116            BackendAvailability::Unavailable
117        }
118    }
119
120    #[cfg(not(all(target_os = "linux", taskers_ghostty_bridge)))]
121    {
122        BackendAvailability::Unavailable
123    }
124}
125
126fn embedded_ghostty_availability() -> BackendAvailability {
127    #[cfg(target_os = "macos")]
128    {
129        BackendAvailability::Ready
130    }
131
132    #[cfg(not(target_os = "macos"))]
133    {
134        BackendAvailability::Unavailable
135    }
136}
137
138fn ghostty_notes() -> String {
139    let mut notes = String::from("Ghostty GTK bridge compiled in.");
140    if let Some(path) = runtime_bridge_path() {
141        notes.push_str(" Bridge: ");
142        notes.push_str(&path.display().to_string());
143    } else {
144        notes.push_str(" Bridge library not found.");
145    }
146    if let Some(path) = runtime_resources_dir() {
147        notes.push_str(" Resources: ");
148        notes.push_str(&path.display().to_string());
149    }
150    notes
151}
152
153fn embedded_ghostty_notes() -> String {
154    String::from("Embedded Ghostty surfaces require the native macOS host.")
155}
156
157#[cfg(test)]
158mod tests {
159    use super::{BackendAvailability, BackendChoice, DefaultBackend, TerminalBackend};
160
161    #[test]
162    fn auto_probe_matches_runtime_availability() {
163        let probe = DefaultBackend::probe(BackendChoice::Auto);
164        match probe.availability {
165            BackendAvailability::Ready => assert_eq!(probe.selected, BackendChoice::Ghostty),
166            BackendAvailability::Fallback | BackendAvailability::Unavailable => {
167                assert_eq!(probe.selected, BackendChoice::Mock);
168            }
169        }
170    }
171
172    #[test]
173    fn embedded_probe_stays_explicit() {
174        let probe = DefaultBackend::probe(BackendChoice::GhosttyEmbedded);
175        assert_eq!(probe.selected, BackendChoice::GhosttyEmbedded);
176
177        #[cfg(target_os = "macos")]
178        assert_eq!(probe.availability, BackendAvailability::Ready);
179
180        #[cfg(not(target_os = "macos"))]
181        assert_eq!(probe.availability, BackendAvailability::Unavailable);
182    }
183
184    #[test]
185    fn env_override_accepts_hyphenated_embedded_backend() {
186        unsafe { std::env::set_var("TASKERS_TERMINAL_BACKEND", "ghostty-embedded") };
187        let probe = DefaultBackend::probe(BackendChoice::Mock);
188        unsafe { std::env::remove_var("TASKERS_TERMINAL_BACKEND") };
189        assert_eq!(probe.selected, BackendChoice::GhosttyEmbedded);
190    }
191}