1use 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}