1use std::env;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Backend {
17 Kitty,
18 Ratatui,
19 Text,
20}
21
22#[must_use]
27pub fn detect_backend<F>(env_lookup: F) -> Backend
28where
29 F: Fn(&str) -> Option<String>,
30{
31 if let Some(forced) = env_lookup("NOMISYNC_TUI_CHART") {
32 match forced.to_ascii_lowercase().as_str() {
33 "kitty" => return Backend::Kitty,
34 "ratatui" => return Backend::Ratatui,
35 "text" => return Backend::Text,
36 _ => {}
37 }
38 }
39 if supports_kitty(&env_lookup) {
40 return Backend::Kitty;
41 }
42 Backend::Ratatui
43}
44
45fn supports_kitty<F>(env_lookup: &F) -> bool
46where
47 F: Fn(&str) -> Option<String>,
48{
49 if env_lookup("KITTY_WINDOW_ID").is_some() {
50 return true;
51 }
52 let term = env_lookup("TERM").unwrap_or_default();
53 if term.contains("kitty") {
54 return true;
55 }
56 let term_program = env_lookup("TERM_PROGRAM").unwrap_or_default();
57 matches!(term_program.as_str(), "WezTerm" | "ghostty")
58}
59
60#[must_use]
62pub fn detect_from_env() -> Backend {
63 detect_backend(|name| env::var(name).ok())
64}
65
66#[must_use]
73pub fn detect_for<T>(transport: &T) -> Backend
74where
75 T: crate::transport::Transport,
76{
77 let client_term = transport.client_term().map(str::to_string);
78 detect_backend(|name| match name {
79 "TERM" => client_term.clone(),
80 other => env::var(other).ok(),
81 })
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use std::collections::HashMap;
88
89 fn env_from(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
90 let map: HashMap<String, String> = pairs
91 .iter()
92 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
93 .collect();
94 move |k: &str| map.get(k).cloned()
95 }
96
97 #[test]
98 fn plain_xterm_picks_ratatui() {
99 let env = env_from(&[("TERM", "xterm-256color")]);
100 assert_eq!(detect_backend(env), Backend::Ratatui);
101 }
102
103 #[test]
104 fn kitty_window_id_picks_kitty() {
105 let env = env_from(&[("KITTY_WINDOW_ID", "1"), ("TERM", "xterm-256color")]);
106 assert_eq!(detect_backend(env), Backend::Kitty);
107 }
108
109 #[test]
110 fn xterm_kitty_term_picks_kitty() {
111 let env = env_from(&[("TERM", "xterm-kitty")]);
112 assert_eq!(detect_backend(env), Backend::Kitty);
113 }
114
115 #[test]
116 fn wezterm_program_picks_kitty() {
117 let env = env_from(&[("TERM", "xterm-256color"), ("TERM_PROGRAM", "WezTerm")]);
118 assert_eq!(detect_backend(env), Backend::Kitty);
119 }
120
121 #[test]
122 fn ghostty_program_picks_kitty() {
123 let env = env_from(&[("TERM_PROGRAM", "ghostty")]);
124 assert_eq!(detect_backend(env), Backend::Kitty);
125 }
126
127 #[test]
128 fn empty_env_falls_back_to_ratatui() {
129 let env = env_from(&[]);
130 assert_eq!(detect_backend(env), Backend::Ratatui);
131 }
132
133 #[test]
134 fn explicit_override_forces_text_backend() {
135 let env = env_from(&[("KITTY_WINDOW_ID", "1"), ("NOMISYNC_TUI_CHART", "text")]);
136 assert_eq!(detect_backend(env), Backend::Text);
137 }
138
139 #[test]
140 fn explicit_override_forces_ratatui_backend() {
141 let env = env_from(&[("KITTY_WINDOW_ID", "1"), ("NOMISYNC_TUI_CHART", "ratatui")]);
142 assert_eq!(detect_backend(env), Backend::Ratatui);
143 }
144
145 #[test]
146 fn explicit_override_forces_kitty_backend() {
147 let env = env_from(&[("TERM", "xterm"), ("NOMISYNC_TUI_CHART", "kitty")]);
148 assert_eq!(detect_backend(env), Backend::Kitty);
149 }
150}