Skip to main content

tui_test/terminal/
backend.rs

1//! Terminal emulator selection and construction.
2
3use serde::{Deserialize, Serialize};
4
5use crate::event::BellTracker;
6use crate::profile::Profile;
7use crate::terminal::alacritty::AlacrittyEmu;
8use crate::terminal::emu::Emulator;
9
10#[cfg(feature = "rio")]
11use crate::terminal::rio::RioEmu;
12
13#[cfg(feature = "ghostty")]
14use crate::terminal::ghostty::GhosttyEmu;
15
16/// The terminal emulator a session uses.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Backend {
20    #[default]
21    Alacritty,
22    #[cfg(feature = "ghostty")]
23    Ghostty,
24    #[cfg(feature = "rio")]
25    Rio,
26}
27
28impl Backend {
29    #[cfg(all(not(feature = "ghostty"), not(feature = "rio")))]
30    pub const ALL: [Self; 1] = [Self::Alacritty];
31    #[cfg(all(feature = "ghostty", not(feature = "rio")))]
32    pub const ALL: [Self; 2] = [Self::Alacritty, Self::Ghostty];
33    #[cfg(all(not(feature = "ghostty"), feature = "rio"))]
34    pub const ALL: [Self; 2] = [Self::Alacritty, Self::Rio];
35    #[cfg(all(feature = "ghostty", feature = "rio"))]
36    pub const ALL: [Self; 3] = [Self::Alacritty, Self::Ghostty, Self::Rio];
37
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Self::Alacritty => "alacritty",
41            #[cfg(feature = "ghostty")]
42            Self::Ghostty => "ghostty",
43            #[cfg(feature = "rio")]
44            Self::Rio => "rio",
45        }
46    }
47
48    pub fn build(
49        self,
50        cols: u16,
51        rows: u16,
52        profile: &Profile,
53    ) -> anyhow::Result<Box<dyn Emulator>> {
54        match self {
55            Self::Alacritty => Ok(Box::new(AlacrittyEmu::new(cols, rows, profile))),
56            #[cfg(feature = "ghostty")]
57            Self::Ghostty => Ok(Box::new(GhosttyEmu::new(cols, rows, profile)?)),
58            #[cfg(feature = "rio")]
59            Self::Rio => Ok(Box::new(RioEmu::new(cols, rows, profile))),
60        }
61    }
62
63    /// Like [`Self::build`], but wires up bell tracking where the backend
64    /// supports it. Backends without native bell support simply won't
65    /// report bell events.
66    pub(crate) fn build_with_bells(
67        self,
68        cols: u16,
69        rows: u16,
70        profile: &Profile,
71        bells: BellTracker,
72    ) -> anyhow::Result<Box<dyn Emulator>> {
73        match self {
74            Self::Alacritty => Ok(Box::new(AlacrittyEmu::with_bell_tracker(
75                cols, rows, profile, bells,
76            ))),
77            #[cfg(feature = "ghostty")]
78            Self::Ghostty => self.build(cols, rows, profile),
79            #[cfg(feature = "rio")]
80            Self::Rio => Ok(Box::new(RioEmu::with_bell_tracker(
81                cols, rows, profile, bells,
82            ))),
83        }
84    }
85
86    const fn expected() -> &'static str {
87        match (cfg!(feature = "ghostty"), cfg!(feature = "rio")) {
88            (false, false) => "alacritty",
89            (true, false) => "alacritty, ghostty",
90            (false, true) => "alacritty, rio",
91            (true, true) => "alacritty, ghostty, rio",
92        }
93    }
94}
95
96impl std::str::FromStr for Backend {
97    type Err = String;
98
99    fn from_str(value: &str) -> Result<Self, Self::Err> {
100        match value.trim().to_ascii_lowercase().as_str() {
101            "alacritty" => Ok(Self::Alacritty),
102            #[cfg(feature = "ghostty")]
103            "ghostty" => Ok(Self::Ghostty),
104            #[cfg(feature = "rio")]
105            "rio" => Ok(Self::Rio),
106            other => Err(format!(
107                "unknown terminal backend {other:?}; expected one of: {}",
108                Self::expected()
109            )),
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn names_round_trip() {
120        for backend in Backend::ALL {
121            assert_eq!(backend.as_str().parse(), Ok(backend));
122        }
123    }
124
125    #[test]
126    fn alacritty_remains_the_default() {
127        assert_eq!(Backend::default(), Backend::Alacritty);
128    }
129
130    #[cfg(not(feature = "rio"))]
131    #[test]
132    fn rio_is_rejected_when_its_feature_is_disabled() {
133        assert!("rio".parse::<Backend>().is_err());
134        assert!(serde_json::from_str::<Backend>("\"rio\"").is_err());
135    }
136
137    #[cfg(feature = "ghostty")]
138    #[test]
139    fn legacy_backend_spelling_is_rejected() {
140        assert!("libghostty".parse::<Backend>().is_err());
141        assert!(serde_json::from_str::<Backend>("\"libghostty\"").is_err());
142    }
143
144    #[test]
145    fn every_enabled_backend_constructs() {
146        for backend in Backend::ALL {
147            let mut emulator = backend
148                .build(10, 2, &Profile::default())
149                .unwrap_or_else(|error| panic!("{}: {error:#}", backend.as_str()));
150            emulator.process(b"ok");
151            assert_eq!(emulator.viewable_rows()[0][0].ch, "o");
152        }
153    }
154}