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