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}