taskers_ghostty/
backend.rs1use crate::bridge::{runtime_bridge_path, runtime_resources_dir};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum BackendChoice {
8 Auto,
9 Ghostty,
10 Mock,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum BackendAvailability {
16 Ready,
17 Fallback,
18 Unavailable,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct BackendProbe {
23 pub requested: BackendChoice,
24 pub selected: BackendChoice,
25 pub availability: BackendAvailability,
26 pub notes: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct SurfaceDescriptor {
31 pub cols: u16,
32 pub rows: u16,
33 pub cwd: Option<String>,
34 pub title: Option<String>,
35}
36
37#[derive(Debug, Error)]
38pub enum AdapterError {
39 #[error("terminal backend is unavailable: {0}")]
40 Unavailable(String),
41 #[error("terminal backend initialization failed: {0}")]
42 Initialization(String),
43}
44
45pub trait TerminalBackend {
46 fn probe(requested: BackendChoice) -> BackendProbe;
47}
48
49pub struct DefaultBackend;
50
51impl TerminalBackend for DefaultBackend {
52 fn probe(requested: BackendChoice) -> BackendProbe {
53 let env_override = std::env::var("TASKERS_TERMINAL_BACKEND").ok();
54 let requested = match env_override.as_deref() {
55 Some("ghostty") => BackendChoice::Ghostty,
56 Some("mock") => BackendChoice::Mock,
57 _ => requested,
58 };
59
60 match requested {
61 BackendChoice::Auto => auto_probe(requested),
62 BackendChoice::Mock => BackendProbe {
63 requested,
64 selected: BackendChoice::Mock,
65 availability: BackendAvailability::Fallback,
66 notes: "Using placeholder terminal surfaces.".into(),
67 },
68 BackendChoice::Ghostty => BackendProbe {
69 requested,
70 selected: BackendChoice::Ghostty,
71 availability: ghostty_availability(),
72 notes: ghostty_notes(),
73 },
74 }
75 }
76}
77
78fn auto_probe(requested: BackendChoice) -> BackendProbe {
79 let availability = ghostty_availability();
80 if matches!(availability, BackendAvailability::Ready) {
81 BackendProbe {
82 requested,
83 selected: BackendChoice::Ghostty,
84 availability,
85 notes: ghostty_notes(),
86 }
87 } else {
88 BackendProbe {
89 requested,
90 selected: BackendChoice::Mock,
91 availability: BackendAvailability::Fallback,
92 notes: "Ghostty bridge unavailable, using placeholder terminal surfaces.".into(),
93 }
94 }
95}
96
97fn ghostty_availability() -> BackendAvailability {
98 #[cfg(all(target_os = "linux", taskers_ghostty_bridge))]
99 {
100 if runtime_bridge_path().is_some() {
101 BackendAvailability::Ready
102 } else {
103 BackendAvailability::Unavailable
104 }
105 }
106
107 #[cfg(not(all(target_os = "linux", taskers_ghostty_bridge)))]
108 {
109 BackendAvailability::Unavailable
110 }
111}
112
113fn ghostty_notes() -> String {
114 let mut notes = String::from("Ghostty GTK bridge compiled in.");
115 if let Some(path) = runtime_bridge_path() {
116 notes.push_str(" Bridge: ");
117 notes.push_str(&path.display().to_string());
118 } else {
119 notes.push_str(" Bridge library not found.");
120 }
121 if let Some(path) = runtime_resources_dir() {
122 notes.push_str(" Resources: ");
123 notes.push_str(&path.display().to_string());
124 }
125 notes
126}
127
128#[cfg(test)]
129mod tests {
130 use super::{BackendAvailability, BackendChoice, DefaultBackend, TerminalBackend};
131
132 #[test]
133 fn auto_probe_matches_runtime_availability() {
134 let probe = DefaultBackend::probe(BackendChoice::Auto);
135 match probe.availability {
136 BackendAvailability::Ready => assert_eq!(probe.selected, BackendChoice::Ghostty),
137 BackendAvailability::Fallback | BackendAvailability::Unavailable => {
138 assert_eq!(probe.selected, BackendChoice::Mock);
139 }
140 }
141 }
142}