thd75_repl/confirm.rs
1//! Transmit confirmation prompt and script-mode gating.
2//!
3//! Every transmit command in the REPL (cq, beacon, position, msg,
4//! echo, link) calls `tx_confirm` before keying the radio. This
5//! guards blind operators from accidentally transmitting on air by
6//! mistyping an argument or pasting the wrong line at the prompt.
7//!
8//! Two global atomic flags steer the prompt behaviour:
9//!
10//! - `CONFIRM_TX` (default `true`) - require the prompt at all.
11//! Turned off globally by `--yes` or `confirm off`.
12//! - `SCRIPT_MODE` (default `false`) - the REPL is running a
13//! non-interactive script (stdin piped or `--script` passed). In
14//! script mode the prompt cannot be answered, so every transmit
15//! command aborts with a clear error unless confirmation has been
16//! explicitly disabled via `--yes`.
17
18use std::io::BufRead as _;
19use std::io::Write as _;
20use std::sync::atomic::{AtomicBool, Ordering};
21
22/// When `true` (the default) every transmit command prompts the user
23/// before keying the radio. Cleared by `--yes` on the command line
24/// and by the interactive `confirm off` command.
25pub static CONFIRM_TX: AtomicBool = AtomicBool::new(true);
26
27/// When `true` the REPL is running under a script or `--script`
28/// file. Prompts cannot be answered, so [`tx_confirm`] prints an
29/// error and returns `false` unless [`CONFIRM_TX`] is also cleared.
30pub static SCRIPT_MODE: AtomicBool = AtomicBool::new(false);
31
32/// Whether a transmit confirmation prompt is currently required.
33#[must_use]
34pub fn is_required() -> bool {
35 CONFIRM_TX.load(Ordering::Relaxed)
36}
37
38/// Enable or disable transmit confirmation.
39pub fn set_required(required: bool) {
40 CONFIRM_TX.store(required, Ordering::Relaxed);
41}
42
43/// Mark the REPL as running under a non-interactive script.
44pub fn set_script_mode(on: bool) {
45 SCRIPT_MODE.store(on, Ordering::Relaxed);
46}
47
48/// Prompt the user to confirm a transmit action.
49///
50/// Returns `true` when the caller may proceed:
51/// - [`CONFIRM_TX`] is false — confirmation globally disabled.
52/// - The user typed `y` or `yes` (case-insensitive).
53///
54/// Returns `false` when the caller must abort:
55/// - Script mode is active and confirmation is required.
56/// - The user typed anything else, including blank lines.
57/// - Reading from stdin failed.
58///
59/// In the abort path the function prints an explanatory line via
60/// plain `println!` (so the message is *not* suppressed by the
61/// history buffer or the quiet flag — confirmation is always loud).
62pub fn tx_confirm() -> bool {
63 if !is_required() {
64 return true;
65 }
66 if SCRIPT_MODE.load(Ordering::Relaxed) {
67 println!("Error: transmit commands require --yes in script mode.");
68 return false;
69 }
70 println!("Confirm transmit? Type yes or y to proceed, anything else to cancel.");
71 // Flush stdout so the prompt is visible before the blocking read.
72 // A flush failure is fine — the prompt was already sent via
73 // `println!` so there is nothing left buffered.
74 let _ = std::io::stdout().flush();
75
76 let stdin = std::io::stdin();
77 let mut line = String::new();
78 if stdin.lock().read_line(&mut line).is_err() {
79 println!("Transmission cancelled.");
80 return false;
81 }
82 let trimmed = line.trim().to_lowercase();
83 if trimmed == "y" || trimmed == "yes" {
84 true
85 } else {
86 println!("Transmission cancelled.");
87 false
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{CONFIRM_TX, SCRIPT_MODE, is_required, set_required, set_script_mode};
94 use std::sync::Mutex;
95 use std::sync::atomic::Ordering;
96
97 /// Tests mutate global atomics so they must serialise.
98 static GUARD: Mutex<()> = Mutex::new(());
99
100 fn reset() {
101 CONFIRM_TX.store(true, Ordering::Relaxed);
102 SCRIPT_MODE.store(false, Ordering::Relaxed);
103 }
104
105 #[test]
106 fn defaults_require_confirmation() {
107 let _g = GUARD
108 .lock()
109 .unwrap_or_else(std::sync::PoisonError::into_inner);
110 reset();
111 assert!(is_required());
112 }
113
114 #[test]
115 fn set_required_toggles_flag() {
116 let _g = GUARD
117 .lock()
118 .unwrap_or_else(std::sync::PoisonError::into_inner);
119 reset();
120 set_required(false);
121 assert!(!is_required());
122 set_required(true);
123 assert!(is_required());
124 }
125
126 #[test]
127 fn script_mode_persists() {
128 let _g = GUARD
129 .lock()
130 .unwrap_or_else(std::sync::PoisonError::into_inner);
131 reset();
132 set_script_mode(true);
133 assert!(SCRIPT_MODE.load(Ordering::Relaxed));
134 set_script_mode(false);
135 assert!(!SCRIPT_MODE.load(Ordering::Relaxed));
136 }
137}