mod app;
mod auto;
mod catalog;
mod compare;
mod config_watch;
mod cost;
mod dataset;
mod editor;
mod event_loop;
mod follow;
mod format;
mod keys;
mod motion;
mod pipeline;
mod pod_logs;
mod project;
mod project_edit;
mod project_save;
#[cfg(test)]
mod snapshots;
mod start;
mod start_pick;
mod tasks;
mod terminal;
mod theme;
mod training;
mod ui;
mod views;
mod widgets;
mod wizard;
use std::io::{self, IsTerminal, Write};
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;
use anyhow::{Context, bail};
use tokio::task::JoinHandle;
use self::app::{App, Project};
use self::config_watch::ConfigWatch;
use self::motion::{Motion, MotionLevel};
use self::project::ProjectConfig;
use self::terminal::TerminalGuard;
use self::theme::{ColorLevel, LookEnv, Theme};
use crate::config::{CONFIG_FILE, ConfigError, DotenvKeys, EnvSource, stamp};
use crate::events::Observer;
use crate::logging::LogBuffer;
use crate::update::Newer;
pub use self::wizard::Ended as WizardEnded;
#[derive(Debug, Default)]
pub struct Start {
pub dotenv: DotenvKeys,
pub auto: bool,
}
pub fn wizard(project_dir: &Path) -> anyhow::Result<WizardEnded> {
if !std::io::stdout().is_terminal() {
bail!("overbrainer tui needs a terminal: stdout is not a TTY");
}
wizard::run(project_dir)
}
#[must_use]
pub fn wizard_refusal(project_dir: &Path) -> Option<String> {
wizard::existing(project_dir).map(|path| {
format!(
"{} exists, and the init wizard never overwrites a file: move it away and run \
`overbrainer tui` again, or write the project with `overbrainer init`",
path.display()
)
})
}
pub async fn run(
project_dir: &Path,
logs: LogBuffer,
check: Option<JoinHandle<Option<Newer>>>,
start: Start,
observer: Option<Arc<dyn Observer>>,
) -> anyhow::Result<()> {
if !std::io::stdout().is_terminal() {
bail!("overbrainer tui needs a terminal: stdout is not a TTY");
}
let path = project_dir.join(CONFIG_FILE);
let read_at = stamp(project_dir);
let text = std::fs::read_to_string(&path).map_err(|error| ConfigError::Read { path, error })?;
let mut config = ProjectConfig::new(&text, &EnvSource::Process)?;
config.stamp = Some(read_at);
let project = Project::new(project_dir, &config.settings);
let env = LookEnv::from_process();
let color = ColorLevel::detect(&env);
let theme = Theme::new(color);
let mut app = App::new(project, logs, &theme, SystemTime::now());
app.set_config(config);
app.watch = Some(ConfigWatch::new(start.dotenv, read_at));
if start.auto {
app.opening = app::Opening::Auto;
}
app.motion = Motion::new(MotionLevel::detect(&env, color)).colored(&theme);
warn_on_start(&env, project_dir);
app.editor = editor::command(std::env::var_os("VISUAL"), std::env::var_os("EDITOR"));
let guard = TerminalGuard::enter();
let mut terminal = terminal::init().context("cannot set up the terminal")?;
let result = event_loop::run(&mut terminal, &mut app, check, observer).await;
drop(guard);
app.abandon_edit();
for note in &app.exit_notes {
writeln!(io::stderr(), "{note}").ok();
}
result
}
fn warn_on_start(env: &LookEnv, project_dir: &Path) {
let mut warnings = env.warnings();
if crate::project_format::predates_versions(project_dir) {
warnings.push(crate::cli::migrate::HINT.to_string());
}
for warning in warnings {
tracing::warn!("{warning}");
}
}