entrenar/train/tui/capability/
capabilities.rs1use super::TerminalMode;
4
5#[allow(clippy::struct_excessive_bools)]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct TerminalCapabilities {
9 pub width: u16,
11 pub height: u16,
13 pub unicode: bool,
15 pub ansi_color: bool,
17 pub true_color: bool,
19 pub is_tty: bool,
21}
22
23impl Default for TerminalCapabilities {
24 fn default() -> Self {
25 Self {
26 width: 80,
27 height: 24,
28 unicode: true,
29 ansi_color: true,
30 true_color: false,
31 is_tty: true,
32 }
33 }
34}
35
36impl TerminalCapabilities {
37 pub fn detect() -> Self {
39 use std::env;
40 use std::io::{stdout, IsTerminal};
41
42 let is_tty = stdout().is_terminal();
43
44 let (width, height) = Self::get_size();
46
47 let lang = env::var("LANG").unwrap_or_default();
49 let unicode = lang.contains("UTF") || lang.contains("utf");
50
51 let term = env::var("TERM").unwrap_or_default();
53 let ansi_color = !term.is_empty() && term != "dumb";
54
55 let colorterm = env::var("COLORTERM").unwrap_or_default();
57 let true_color = colorterm == "truecolor" || colorterm == "24bit";
58
59 Self { width, height, unicode, ansi_color, true_color, is_tty }
60 }
61
62 pub(crate) fn get_size() -> (u16, u16) {
64 use std::env;
65
66 if let (Ok(cols), Ok(rows)) = (env::var("COLUMNS"), env::var("LINES")) {
68 if let (Ok(c), Ok(r)) = (cols.parse(), rows.parse()) {
69 return (c, r);
70 }
71 }
72
73 #[cfg(unix)]
75 {
76 use std::io::{stdout, IsTerminal};
77 if stdout().is_terminal() {
78 #[repr(C)]
80 #[allow(clippy::struct_field_names)]
81 struct WinSize {
82 ws_row: u16,
83 ws_col: u16,
84 ws_xpixel: u16,
85 ws_ypixel: u16,
86 }
87 extern "C" {
88 fn ioctl(fd: i32, request: u64, ...) -> i32;
89 }
90 const TIOCGWINSZ: u64 = 0x5413; let mut ws = WinSize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0 };
92 #[allow(unsafe_code)]
94 if unsafe { ioctl(1, TIOCGWINSZ, &mut ws) } == 0 && ws.ws_col > 0 {
95 return (ws.ws_col, ws.ws_row);
96 }
97 }
98 }
99
100 (80, 24)
102 }
103
104 pub fn recommended_mode(&self) -> TerminalMode {
106 if !self.is_tty {
107 TerminalMode::Ascii
108 } else if self.true_color {
109 TerminalMode::Ansi
110 } else if self.unicode {
111 TerminalMode::Unicode
112 } else {
113 TerminalMode::Ascii
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_terminal_capabilities_default() {
124 let caps = TerminalCapabilities::default();
125 assert_eq!(caps.width, 80);
126 assert_eq!(caps.height, 24);
127 assert!(caps.unicode);
128 assert!(caps.ansi_color);
129 assert!(!caps.true_color);
130 assert!(caps.is_tty);
131 }
132
133 #[test]
134 fn test_terminal_capabilities_clone() {
135 let caps = TerminalCapabilities::default();
136 let cloned = caps;
137 assert_eq!(caps.width, cloned.width);
138 assert_eq!(caps.unicode, cloned.unicode);
139 }
140
141 #[test]
142 fn test_terminal_capabilities_eq() {
143 let caps1 = TerminalCapabilities::default();
144 let caps2 = TerminalCapabilities::default();
145 assert_eq!(caps1, caps2);
146
147 let caps3 = TerminalCapabilities { width: 120, ..Default::default() };
148 assert_ne!(caps1, caps3);
149 }
150
151 #[test]
152 fn test_terminal_capabilities_debug() {
153 let caps = TerminalCapabilities::default();
154 let debug = format!("{caps:?}");
155 assert!(debug.contains("TerminalCapabilities"));
156 assert!(debug.contains("width: 80"));
157 }
158
159 #[test]
160 fn test_recommended_mode_not_tty() {
161 let caps = TerminalCapabilities { is_tty: false, ..Default::default() };
162 assert_eq!(caps.recommended_mode(), TerminalMode::Ascii);
163 }
164
165 #[test]
166 fn test_recommended_mode_true_color() {
167 let caps = TerminalCapabilities {
168 is_tty: true,
169 true_color: true,
170 unicode: true,
171 ..Default::default()
172 };
173 assert_eq!(caps.recommended_mode(), TerminalMode::Ansi);
174 }
175
176 #[test]
177 fn test_recommended_mode_unicode() {
178 let caps = TerminalCapabilities {
179 is_tty: true,
180 true_color: false,
181 unicode: true,
182 ..Default::default()
183 };
184 assert_eq!(caps.recommended_mode(), TerminalMode::Unicode);
185 }
186
187 #[test]
188 fn test_recommended_mode_ascii_fallback() {
189 let caps = TerminalCapabilities {
190 is_tty: true,
191 true_color: false,
192 unicode: false,
193 ansi_color: false,
194 ..Default::default()
195 };
196 assert_eq!(caps.recommended_mode(), TerminalMode::Ascii);
197 }
198
199 #[test]
200 fn test_detect_returns_valid_capabilities() {
201 let caps = TerminalCapabilities::detect();
203 assert!(caps.width > 0);
204 assert!(caps.height > 0);
205 }
206
207 #[test]
208 fn test_get_size_returns_valid_size() {
209 let (width, height) = TerminalCapabilities::get_size();
210 assert!(width > 0);
211 assert!(height > 0);
212 }
213}