Skip to main content

alopex_cli/ui/
mode.rs

1//! UI mode resolution for TUI vs batch output.
2
3use crate::batch::BatchMode;
4use crate::cli::{Cli, Command};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum UiMode {
8    Tui,
9    Batch,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum UiModeSource {
14    Explicit,
15    OutputFormat,
16    BatchMode,
17    Default,
18}
19
20#[allow(clippy::enum_variant_names)]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum UiModeWarning {
23    TuiRequiresTty,
24    TuiDisabledByOutput,
25    TuiDisabledByBatch,
26    TuiDisabledByNonTty,
27}
28
29impl UiModeWarning {
30    pub fn message(&self) -> &'static str {
31        match self {
32            UiModeWarning::TuiRequiresTty => {
33                "Warning: --tui requires a TTY, falling back to batch output."
34            }
35            UiModeWarning::TuiDisabledByOutput => {
36                "Warning: --tui is ignored when --output is set, falling back to batch output."
37            }
38            UiModeWarning::TuiDisabledByBatch => {
39                "Warning: --tui is ignored in batch mode, falling back to batch output."
40            }
41            UiModeWarning::TuiDisabledByNonTty => {
42                "Warning: no TTY detected, default TUI is disabled, falling back to batch output."
43            }
44        }
45    }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct UiModeResolution {
50    pub mode: UiMode,
51    pub source: UiModeSource,
52    pub warnings: Vec<UiModeWarning>,
53}
54
55pub fn resolve_ui_mode(
56    cli: &Cli,
57    command: Option<&Command>,
58    batch_mode: &BatchMode,
59) -> UiModeResolution {
60    let mut warnings = Vec::new();
61    let explicit_tui = command_requests_tui(command);
62
63    if cli.output_is_explicit() {
64        if explicit_tui {
65            warnings.push(UiModeWarning::TuiDisabledByOutput);
66        }
67        return UiModeResolution {
68            mode: UiMode::Batch,
69            source: UiModeSource::OutputFormat,
70            warnings,
71        };
72    }
73
74    if batch_mode.is_batch {
75        if explicit_tui {
76            warnings.push(UiModeWarning::TuiDisabledByBatch);
77            if !batch_mode.is_tty {
78                warnings.push(UiModeWarning::TuiRequiresTty);
79            }
80        } else if !batch_mode.is_tty {
81            warnings.push(UiModeWarning::TuiDisabledByNonTty);
82        }
83        return UiModeResolution {
84            mode: UiMode::Batch,
85            source: UiModeSource::BatchMode,
86            warnings,
87        };
88    }
89
90    if explicit_tui {
91        if !batch_mode.is_tty {
92            warnings.push(UiModeWarning::TuiRequiresTty);
93            return UiModeResolution {
94                mode: UiMode::Batch,
95                source: UiModeSource::BatchMode,
96                warnings,
97            };
98        }
99        return UiModeResolution {
100            mode: UiMode::Tui,
101            source: UiModeSource::Explicit,
102            warnings,
103        };
104    }
105
106    UiModeResolution {
107        mode: UiMode::Tui,
108        source: UiModeSource::Default,
109        warnings,
110    }
111}
112
113fn command_requests_tui(command: Option<&Command>) -> bool {
114    match command {
115        Some(Command::Sql(cmd)) => cmd.tui,
116        _ => false,
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use clap::Parser;
124
125    fn parse_cli(args: &[&str]) -> Cli {
126        Cli::try_parse_from(args).expect("cli parse")
127    }
128
129    #[test]
130    fn resolves_default_tui_on_tty() {
131        let cli = parse_cli(&["alopex", "kv", "list"]);
132        let batch_mode = BatchMode::detect_with(&cli, true, None);
133        let resolution = resolve_ui_mode(&cli, cli.command.as_ref(), &batch_mode);
134
135        assert_eq!(resolution.mode, UiMode::Tui);
136        assert_eq!(resolution.source, UiModeSource::Default);
137        assert!(resolution.warnings.is_empty());
138    }
139
140    #[test]
141    fn output_format_forces_batch_without_explicit_tui() {
142        let cli = parse_cli(&["alopex", "--output", "json", "kv", "list"]);
143        let batch_mode = BatchMode::detect_with(&cli, true, None);
144        let resolution = resolve_ui_mode(&cli, cli.command.as_ref(), &batch_mode);
145
146        assert_eq!(resolution.mode, UiMode::Batch);
147        assert_eq!(resolution.source, UiModeSource::OutputFormat);
148        assert!(resolution.warnings.is_empty());
149    }
150
151    #[test]
152    fn batch_forces_batch() {
153        let cli = parse_cli(&["alopex", "--batch", "sql", "SELECT 1"]);
154        let batch_mode = BatchMode::detect_with(&cli, true, None);
155        let resolution = resolve_ui_mode(&cli, cli.command.as_ref(), &batch_mode);
156
157        assert_eq!(resolution.mode, UiMode::Batch);
158        assert_eq!(resolution.source, UiModeSource::BatchMode);
159        assert!(resolution.warnings.is_empty());
160    }
161
162    #[test]
163    fn non_tty_warns_and_falls_back() {
164        let cli = parse_cli(&["alopex", "sql", "--tui", "SELECT 1"]);
165        let batch_mode = BatchMode::detect_with(&cli, false, None);
166        let resolution = resolve_ui_mode(&cli, cli.command.as_ref(), &batch_mode);
167
168        assert_eq!(resolution.mode, UiMode::Batch);
169        assert_eq!(resolution.source, UiModeSource::BatchMode);
170        assert!(resolution.warnings.contains(&UiModeWarning::TuiRequiresTty));
171    }
172
173    #[test]
174    fn output_explicit_overrides_tui() {
175        let cli = parse_cli(&["alopex", "--output", "json", "sql", "--tui", "SELECT 1"]);
176        let batch_mode = BatchMode::detect_with(&cli, true, None);
177        let resolution = resolve_ui_mode(&cli, cli.command.as_ref(), &batch_mode);
178
179        assert_eq!(resolution.mode, UiMode::Batch);
180        assert_eq!(resolution.source, UiModeSource::OutputFormat);
181        assert!(resolution
182            .warnings
183            .contains(&UiModeWarning::TuiDisabledByOutput));
184    }
185
186    #[test]
187    fn non_tty_default_warns() {
188        let cli = parse_cli(&["alopex", "kv", "list"]);
189        let batch_mode = BatchMode::detect_with(&cli, false, None);
190        let resolution = resolve_ui_mode(&cli, cli.command.as_ref(), &batch_mode);
191
192        assert_eq!(resolution.mode, UiMode::Batch);
193        assert_eq!(resolution.source, UiModeSource::BatchMode);
194        assert!(resolution
195            .warnings
196            .contains(&UiModeWarning::TuiDisabledByNonTty));
197    }
198}