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}