1use std::path::{Path, PathBuf};
10use std::sync::mpsc::Receiver;
11use std::time::Duration;
12
13use color_eyre::Result;
14use ratatui::Frame;
15use ratatui::layout::Rect;
16use ratatui::text::Line;
17use ratatui::widgets::Paragraph;
18
19use crate::config::ThemeMode;
20use crate::{APP_NAME, AppConfig, AppEvent, Args, OpenOptions, RunInput, Theme, logging};
21
22pub(crate) const GRACE: Duration = Duration::from_millis(25);
25
26pub struct Settings {
28 pub(crate) config: AppConfig,
29 pub(crate) theme: Theme,
30 pub(crate) input: RunInput,
31 pub(crate) opts: OpenOptions,
32 pub(crate) notes: Vec<String>,
35}
36
37pub(crate) fn polars_threads(threads: usize, env: Option<&std::ffi::OsStr>) -> Option<String> {
40 (threads > 0 && env.is_none()).then(|| threads.to_string())
41}
42
43pub(crate) fn read(input: RunInput, config: Option<AppConfig>) -> Result<Settings> {
46 let log_level = match &input {
49 RunInput::Cli(args) | RunInput::Host(args, _) => args.log_level.clone().or_else(|| {
50 args.config
51 .iter()
52 .rev()
53 .find(|o| o.key == "log.level")
54 .and_then(|o| o.value.as_str().map(str::to_string))
55 }),
56 _ => None,
57 }
58 .or_else(|| std::env::var("DATUI_LOG").ok());
59 let config = match config {
60 Some(config) => config,
61 None => load_config(&input)?,
62 };
63 crate::cloud::user_agent::configure(&config.http.user_agent);
64 let (input, config) = match input {
65 RunInput::Cli(args) => {
66 let mut config = config;
67 apply_args(&mut config, &args);
68 let opts = OpenOptions::from_args_and_config(&args, &config);
69 let piped = crate::loading::stdin::piped();
72 let paths = crate::loading::stdin::paths_or_stdin(args.paths, piped);
73 if let Some(refused) = crate::loading::stdin::refuse(&paths, piped) {
74 return Err(color_eyre::eyre::eyre!(refused));
75 }
76 if let Some(tee) = &opts.tee {
77 if !paths
78 .iter()
79 .any(|path| crate::loading::stdin::is_stdin(path))
80 {
81 return Err(color_eyre::eyre::eyre!(
82 "--tee records standard input: pipe data in, as in: some_logger | datui --tee run1.csv -"
83 ));
84 }
85 if !opts.force && !crate::loading::stdin::is_stdin(tee) && tee.exists() {
86 return Err(color_eyre::eyre::eyre!(crate::loading::tee::refusal(tee)));
87 }
88 }
89 if opts.follow && paths.is_empty() {
90 return Err(color_eyre::eyre::eyre!(
91 "--follow needs a file to follow, or data piped in: some_logger | datui -f -"
92 ));
93 }
94 (RunInput::Paths(paths, opts), config)
95 }
96 RunInput::Host(args, frame) => {
97 let mut config = config;
98 apply_args(&mut config, &args);
99 let opts = OpenOptions::from_args_and_config(&args, &config);
100 match frame {
101 Some(lf) => (RunInput::LazyFrame(lf, opts), config),
102 None => {
103 let paths = args
104 .paths
105 .into_iter()
106 .map(crate::loading::stdin::as_file)
107 .collect();
108 (RunInput::Paths(paths, opts), config)
109 }
110 }
111 }
112 RunInput::Paths(paths, opts) => {
113 let paths = paths
114 .into_iter()
115 .map(crate::loading::stdin::as_file)
116 .collect();
117 (RunInput::Paths(paths, opts), config)
118 }
119 input => (input, config),
120 };
121 let opts = match &input {
122 RunInput::Paths(_, o) | RunInput::LazyFrame(_, o) => o.clone(),
123 RunInput::Cli(_) | RunInput::Host(..) => unreachable!("resolved above"),
124 };
125 let mut config = config;
129 let mut notes = Vec::new();
130 if let Ok(dir) = std::env::current_dir() {
132 notes.extend(crate::cloud::cloud_env::load(&config.cloud, &dir));
133 }
134 config.cloud = opts.effective_cloud(&config.cloud);
135 notes.extend(config.theme.warnings());
137
138 let cache_dir = crate::cache::CacheManager::new(APP_NAME).ok();
139 notes.extend(logging::init(&logging::LogSettings::resolve(
140 config.log.file.as_deref(),
141 log_level.as_deref().or(config.log.level.as_deref()),
142 cache_dir.as_ref().map(|c| c.cache_dir()),
143 )));
144 for secret in [
145 &config.cloud.s3_access_key_id,
146 &config.cloud.s3_secret_access_key,
147 ]
148 .into_iter()
149 .flatten()
150 {
151 logging::keep_out_of_log(secret);
152 }
153 for connection in &config.cloud.connections {
155 for name in [
156 &connection.secret_access_key_env,
157 &connection.session_token_env,
158 &connection.account_key_env,
159 ]
160 .into_iter()
161 .flatten()
162 {
163 if let Some(value) = crate::cloud::cloud_env::var(name) {
164 logging::keep_out_of_log(&value);
165 }
166 }
167 }
168
169 let theme = Theme::from_config(&config.theme)
170 .or_else(|e| Theme::from_config(&AppConfig::default().theme).map_err(|_| e))?;
171 Ok(Settings {
172 config,
173 theme,
174 input,
175 opts,
176 notes,
177 })
178}
179
180pub(crate) fn load_config(input: &RunInput) -> Result<AppConfig> {
183 let overrides = match input {
184 RunInput::Cli(args) | RunInput::Host(args, _) => args.config.as_slice(),
185 _ => &[],
186 };
187 AppConfig::load_with(APP_NAME, overrides).map_err(|e| match input {
188 RunInput::Cli(_) => color_eyre::eyre::eyre!(
190 "{e}\nFix the configuration and try again, or remove/rename the config file to \
191 use defaults."
192 ),
193 _ => e,
194 })
195}
196
197pub(crate) fn expand_home(input: RunInput) -> RunInput {
201 match input {
202 RunInput::Cli(mut args) => {
203 let spec = match args.format.as_mut() {
204 Some(crate::cli::FormatChoice::File(path)) => Some(path),
205 _ => None,
206 };
207 for path in args
208 .paths
209 .iter_mut()
210 .chain(spec)
211 .chain(args.dict.iter_mut())
212 .chain(args.temp_dir.as_mut())
213 {
214 *path = crate::config::expand_home(path);
215 }
216 RunInput::Cli(args)
217 }
218 input => input,
219 }
220}
221
222fn apply_args(config: &mut AppConfig, args: &Args) {
225 if let Some(nf) = args.number_format.as_deref() {
226 config.display.number_format = config.display.number_format.with_grouping_override(nf);
227 }
228 if let Some(row_numbers) = args.row_numbers {
229 config.display.row_numbers = row_numbers.into();
230 }
231 if let Some(mouse) = args.mouse {
232 config.display.mouse = mouse;
233 }
234 if let Some(rows) = args.sample_rows {
235 config.analysis.sample_rows = rows;
236 }
237 if let Some(path) = &args.log_file {
238 config.log.file = Some(path.to_string_lossy().into_owned());
239 }
240}
241
242pub(crate) fn named_paths(input: &RunInput) -> &[PathBuf] {
244 match input {
245 RunInput::Cli(args) | RunInput::Host(args, None) => &args.paths,
246 RunInput::Host(_, Some(_)) => &[],
247 RunInput::Paths(paths, _) => paths,
248 RunInput::LazyFrame(..) => &[],
249 }
250}
251
252pub(crate) fn named(input: &RunInput) -> Option<PathBuf> {
254 named_paths(input)
255 .first()
256 .map(|path| crate::loading::stdin::named(path))
257}
258
259pub(crate) fn draw_waiting(frame: &mut Frame, path: Option<&Path>) {
262 let area = frame.area();
263 let mut lines = vec![Line::from("Reading settings...")];
264 if let Some(path) = path {
265 lines.push(Line::from(""));
266 lines.push(Line::from(crate::home::display_path(path)));
267 }
268 let height = (lines.len() as u16).min(area.height);
269 let top = area.y + area.height.saturating_sub(height) / 2;
270 frame.render_widget(
271 Paragraph::new(lines).centered(),
272 Rect {
273 y: top,
274 height,
275 ..area
276 },
277 );
278}
279
280pub(crate) fn take_answer(
284 rx: &Receiver<AppEvent>,
285 answered: Option<ThemeMode>,
286 backlog: &mut Vec<AppEvent>,
287) -> Option<ThemeMode> {
288 let mut answered = answered;
289 while let Ok(event) = rx.try_recv() {
290 match event {
291 AppEvent::TerminalBackground(mode) => answered = Some(mode),
292 event => backlog.push(event),
293 }
294 }
295 answered
296}
297
298#[cfg(test)]
299mod tests;