taskers_ghostty/
backend.rs1use 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}