raps_kernel/
interactive.rs1#[cfg(not(test))]
10use std::io::IsTerminal;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13static NON_INTERACTIVE: AtomicBool = AtomicBool::new(false);
14static YES: AtomicBool = AtomicBool::new(false);
15
16#[cfg(test)]
17pub(crate) static MOCK_IS_TERMINAL: AtomicBool = AtomicBool::new(true);
18
19pub fn init(non_interactive: bool, yes: bool) {
21 NON_INTERACTIVE.store(non_interactive, Ordering::Relaxed);
22 YES.store(yes, Ordering::Relaxed);
23}
24
25pub fn is_non_interactive() -> bool {
27 #[cfg(test)]
28 let is_term = MOCK_IS_TERMINAL.load(Ordering::Relaxed);
29 #[cfg(not(test))]
30 let is_term = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
31
32 NON_INTERACTIVE.load(Ordering::Relaxed) || !is_term
33}
34
35#[allow(dead_code)] pub fn is_yes() -> bool {
38 YES.load(Ordering::Relaxed)
39}
40
41pub fn is_headless() -> bool {
49 if is_non_interactive() {
50 return true;
51 }
52
53 if std::env::var_os("SSH_CONNECTION").is_some() || std::env::var_os("SSH_TTY").is_some() {
55 return true;
56 }
57
58 #[cfg(target_os = "linux")]
60 if std::env::var_os("DISPLAY").is_none() && std::env::var_os("WAYLAND_DISPLAY").is_none() {
61 return true;
62 }
63
64 if std::env::var_os("container").is_some()
66 || std::path::Path::new("/.dockerenv").exists()
67 || std::env::var_os("CI").is_some()
68 {
69 return true;
70 }
71
72 false
73}
74
75#[allow(dead_code)] pub fn require_value<T>(value: Option<T>, name: &str) -> Result<T, anyhow::Error> {
80 match value {
81 Some(v) => Ok(v),
82 None => {
83 if is_non_interactive() {
84 anyhow::bail!(
85 "{} is required in non-interactive mode. Use --{} flag or set environment variable.",
86 name,
87 name.replace('_', "-")
88 );
89 }
90 anyhow::bail!("{name} is required");
92 }
93 }
94}
95
96pub fn should_proceed_destructive(action: &str) -> bool {
101 if is_yes() {
102 return true;
103 }
104
105 if is_non_interactive() {
106 return false; }
108
109 dialoguer::Confirm::new()
111 .with_prompt(format!("Are you sure you want to {}?", action))
112 .default(false)
113 .interact()
114 .unwrap_or(false)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn reset_state() {
125 init(false, false);
126 MOCK_IS_TERMINAL.store(true, Ordering::Relaxed);
127 }
128
129 #[test]
130 fn test_init_non_interactive() {
131 reset_state();
132 init(true, false);
133 assert!(is_non_interactive());
134 assert!(!is_yes());
135 reset_state();
136 }
137
138 #[test]
139 fn test_init_yes() {
140 reset_state();
141 init(false, true);
142 assert!(!is_non_interactive());
143 assert!(is_yes());
144 reset_state();
145 }
146
147 #[test]
148 fn test_init_both() {
149 reset_state();
150 init(true, true);
151 assert!(is_non_interactive());
152 assert!(is_yes());
153 reset_state();
154 }
155
156 #[test]
157 fn test_default_state() {
158 reset_state();
159 assert!(!is_non_interactive());
160 assert!(!is_yes());
161 }
162
163 #[test]
164 fn test_require_value_some() {
165 reset_state();
166 let result = require_value(Some("test"), "name");
167 assert!(result.is_ok());
168 assert_eq!(result.unwrap(), "test");
169 }
170
171 #[test]
172 fn test_require_value_none_interactive() {
173 reset_state();
174 let result = require_value::<String>(None, "name");
175 assert!(result.is_err());
176 let err_msg = result.unwrap_err().to_string();
178 assert!(err_msg.contains("required"));
179 }
180
181 #[test]
182 fn test_require_value_none_non_interactive() {
183 reset_state();
184 init(true, false);
185 let result = require_value::<String>(None, "name");
186 assert!(result.is_err());
187 let err_msg = result.unwrap_err().to_string();
189 assert!(err_msg.contains("non-interactive"));
190 reset_state();
191 }
192
193 #[test]
194 fn test_should_proceed_destructive_yes() {
195 reset_state();
196 init(false, true); assert!(should_proceed_destructive("delete bucket"));
198 reset_state();
199 }
200
201 #[test]
202 fn test_should_proceed_destructive_non_interactive_no_yes() {
203 reset_state();
204 init(true, false); assert!(!should_proceed_destructive("delete bucket"));
206 reset_state();
207 }
208
209 #[test]
210 fn test_should_proceed_destructive_interactive() {
211 reset_state();
212 init(false, false); assert!(!should_proceed_destructive("delete bucket")); reset_state();
215 }
216
217 #[test]
218 fn test_should_proceed_destructive_non_interactive_with_yes() {
219 reset_state();
220 init(true, true); assert!(should_proceed_destructive("delete bucket"));
222 reset_state();
223 }
224}