use std::path::{Path, PathBuf};
use std::sync::mpsc::Receiver;
use std::time::Duration;
use color_eyre::Result;
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use crate::config::ThemeMode;
use crate::{APP_NAME, AppConfig, AppEvent, Args, OpenOptions, RunInput, Theme, logging};
pub(crate) const GRACE: Duration = Duration::from_millis(25);
pub struct Settings {
pub(crate) config: AppConfig,
pub(crate) theme: Theme,
pub(crate) input: RunInput,
pub(crate) opts: OpenOptions,
pub(crate) notes: Vec<String>,
}
pub(crate) fn polars_threads(threads: usize, env: Option<&std::ffi::OsStr>) -> Option<String> {
(threads > 0 && env.is_none()).then(|| threads.to_string())
}
pub(crate) fn read(input: RunInput, config: Option<AppConfig>) -> Result<Settings> {
let log_level = match &input {
RunInput::Cli(args) | RunInput::Host(args, _) => args.log_level.clone().or_else(|| {
args.config
.iter()
.rev()
.find(|o| o.key == "log.level")
.and_then(|o| o.value.as_str().map(str::to_string))
}),
_ => None,
}
.or_else(|| std::env::var("DATUI_LOG").ok());
let config = match config {
Some(config) => config,
None => load_config(&input)?,
};
crate::cloud::user_agent::configure(&config.http.user_agent);
let (input, config) = match input {
RunInput::Cli(args) => {
let mut config = config;
apply_args(&mut config, &args);
let opts = OpenOptions::from_args_and_config(&args, &config);
let piped = crate::loading::stdin::piped();
let paths = crate::loading::stdin::paths_or_stdin(args.paths, piped);
if let Some(refused) = crate::loading::stdin::refuse(&paths, piped) {
return Err(color_eyre::eyre::eyre!(refused));
}
if let Some(tee) = &opts.tee {
if !paths
.iter()
.any(|path| crate::loading::stdin::is_stdin(path))
{
return Err(color_eyre::eyre::eyre!(
"--tee records standard input: pipe data in, as in: some_logger | datui --tee run1.csv -"
));
}
if !opts.force && !crate::loading::stdin::is_stdin(tee) && tee.exists() {
return Err(color_eyre::eyre::eyre!(crate::loading::tee::refusal(tee)));
}
}
if opts.follow && paths.is_empty() {
return Err(color_eyre::eyre::eyre!(
"--follow needs a file to follow, or data piped in: some_logger | datui -f -"
));
}
(RunInput::Paths(paths, opts), config)
}
RunInput::Host(args, frame) => {
let mut config = config;
apply_args(&mut config, &args);
let opts = OpenOptions::from_args_and_config(&args, &config);
match frame {
Some(lf) => (RunInput::LazyFrame(lf, opts), config),
None => {
let paths = args
.paths
.into_iter()
.map(crate::loading::stdin::as_file)
.collect();
(RunInput::Paths(paths, opts), config)
}
}
}
RunInput::Paths(paths, opts) => {
let paths = paths
.into_iter()
.map(crate::loading::stdin::as_file)
.collect();
(RunInput::Paths(paths, opts), config)
}
input => (input, config),
};
let opts = match &input {
RunInput::Paths(_, o) | RunInput::LazyFrame(_, o) => o.clone(),
RunInput::Cli(_) | RunInput::Host(..) => unreachable!("resolved above"),
};
let mut config = config;
let mut notes = Vec::new();
if let Ok(dir) = std::env::current_dir() {
notes.extend(crate::cloud::cloud_env::load(&config.cloud, &dir));
}
config.cloud = opts.effective_cloud(&config.cloud);
notes.extend(config.theme.warnings());
let cache_dir = crate::cache::CacheManager::new(APP_NAME).ok();
notes.extend(logging::init(&logging::LogSettings::resolve(
config.log.file.as_deref(),
log_level.as_deref().or(config.log.level.as_deref()),
cache_dir.as_ref().map(|c| c.cache_dir()),
)));
for secret in [
&config.cloud.s3_access_key_id,
&config.cloud.s3_secret_access_key,
]
.into_iter()
.flatten()
{
logging::keep_out_of_log(secret);
}
for connection in &config.cloud.connections {
for name in [
&connection.secret_access_key_env,
&connection.session_token_env,
&connection.account_key_env,
]
.into_iter()
.flatten()
{
if let Some(value) = crate::cloud::cloud_env::var(name) {
logging::keep_out_of_log(&value);
}
}
}
let theme = Theme::from_config(&config.theme)
.or_else(|e| Theme::from_config(&AppConfig::default().theme).map_err(|_| e))?;
Ok(Settings {
config,
theme,
input,
opts,
notes,
})
}
pub(crate) fn load_config(input: &RunInput) -> Result<AppConfig> {
let overrides = match input {
RunInput::Cli(args) | RunInput::Host(args, _) => args.config.as_slice(),
_ => &[],
};
AppConfig::load_with(APP_NAME, overrides).map_err(|e| match input {
RunInput::Cli(_) => color_eyre::eyre::eyre!(
"{e}\nFix the configuration and try again, or remove/rename the config file to \
use defaults."
),
_ => e,
})
}
pub(crate) fn expand_home(input: RunInput) -> RunInput {
match input {
RunInput::Cli(mut args) => {
let spec = match args.format.as_mut() {
Some(crate::cli::FormatChoice::File(path)) => Some(path),
_ => None,
};
for path in args
.paths
.iter_mut()
.chain(spec)
.chain(args.dict.iter_mut())
.chain(args.temp_dir.as_mut())
{
*path = crate::config::expand_home(path);
}
RunInput::Cli(args)
}
input => input,
}
}
fn apply_args(config: &mut AppConfig, args: &Args) {
if let Some(nf) = args.number_format.as_deref() {
config.display.number_format = config.display.number_format.with_grouping_override(nf);
}
if let Some(row_numbers) = args.row_numbers {
config.display.row_numbers = row_numbers.into();
}
if let Some(mouse) = args.mouse {
config.display.mouse = mouse;
}
if let Some(rows) = args.sample_rows {
config.analysis.sample_rows = rows;
}
if let Some(path) = &args.log_file {
config.log.file = Some(path.to_string_lossy().into_owned());
}
}
pub(crate) fn named_paths(input: &RunInput) -> &[PathBuf] {
match input {
RunInput::Cli(args) | RunInput::Host(args, None) => &args.paths,
RunInput::Host(_, Some(_)) => &[],
RunInput::Paths(paths, _) => paths,
RunInput::LazyFrame(..) => &[],
}
}
pub(crate) fn named(input: &RunInput) -> Option<PathBuf> {
named_paths(input)
.first()
.map(|path| crate::loading::stdin::named(path))
}
pub(crate) fn draw_waiting(frame: &mut Frame, path: Option<&Path>) {
let area = frame.area();
let mut lines = vec![Line::from("Reading settings...")];
if let Some(path) = path {
lines.push(Line::from(""));
lines.push(Line::from(crate::home::display_path(path)));
}
let height = (lines.len() as u16).min(area.height);
let top = area.y + area.height.saturating_sub(height) / 2;
frame.render_widget(
Paragraph::new(lines).centered(),
Rect {
y: top,
height,
..area
},
);
}
pub(crate) fn take_answer(
rx: &Receiver<AppEvent>,
answered: Option<ThemeMode>,
backlog: &mut Vec<AppEvent>,
) -> Option<ThemeMode> {
let mut answered = answered;
while let Ok(event) = rx.try_recv() {
match event {
AppEvent::TerminalBackground(mode) => answered = Some(mode),
event => backlog.push(event),
}
}
answered
}
#[cfg(test)]
mod tests;