use std::io;
use crossterm::{
event::{DisableBracketedPaste, PopKeyboardEnhancementFlags},
execute,
terminal::{LeaveAlternateScreen, disable_raw_mode, supports_keyboard_enhancement},
};
use crate::app::{self, App, FocusedPanel};
use crate::cli::Cli;
use crate::theme::{Theme, resolve_theme_with_config};
use crate::{SparEntryOutcome, enter_spar_mode};
use travelagent_core::config::{AppConfig, ConfigLoadOutcome};
pub(crate) fn install_panic_hook() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), DisableBracketedPaste, LeaveAlternateScreen);
original_hook(panic_info);
}));
}
pub(crate) fn apply_path_filter_implies_working_tree(cli: &mut Cli) {
if cli.path_filter.is_some() && !cli.working_tree && cli.revisions.is_none() {
cli.working_tree = true;
}
}
pub(crate) fn keyboard_enhancement_supported_for(render_to_tty: bool) -> bool {
if render_to_tty {
false
} else {
matches!(supports_keyboard_enhancement(), Ok(true))
}
}
pub(crate) fn load_global_config_and_theme(
cli: &Cli,
startup_warnings: &mut Vec<String>,
) -> (ConfigLoadOutcome, Theme) {
let config_outcome = match travelagent_core::config::load_config() {
Ok(outcome) => outcome,
Err(e) => {
startup_warnings.push(format!("Failed to load config: {e}"));
travelagent_core::config::ConfigLoadOutcome::default()
}
};
startup_warnings.extend(config_outcome.warnings.clone());
let (theme, theme_warnings) = resolve_theme_with_config(
cli.theme,
cli.appearance,
config_outcome
.config
.as_ref()
.and_then(|cfg| cfg.theme.as_deref()),
config_outcome
.config
.as_ref()
.and_then(|cfg| cfg.theme_dark.as_deref()),
config_outcome
.config
.as_ref()
.and_then(|cfg| cfg.theme_light.as_deref()),
config_outcome
.config
.as_ref()
.and_then(|cfg| cfg.appearance.as_deref()),
);
startup_warnings.extend(theme_warnings);
(config_outcome, theme)
}
pub(crate) fn apply_repo_config_overrides(
app: &mut App,
cli: &Cli,
config_outcome: &mut ConfigLoadOutcome,
startup_warnings: &mut Vec<String>,
) {
if cli.demo || !app.vcs_info.root_path.is_absolute() {
return;
}
match travelagent_core::config::load_repo_config(&app.vcs_info.root_path) {
Ok(repo_outcome) => {
startup_warnings.extend(repo_outcome.warnings.clone());
if let Some(repo_cfg) = repo_outcome.config {
let mut merge_warnings: Vec<String> = Vec::new();
let global_cfg = config_outcome.config.clone().unwrap_or_default();
let merged = travelagent_core::config::merge_overrides(
global_cfg,
repo_cfg,
&repo_outcome.sections_present,
&mut merge_warnings,
);
startup_warnings.extend(merge_warnings);
config_outcome.config = Some(merged);
app.set_message(format!(
"Loaded repo overrides from {}",
repo_outcome.path.display()
));
}
}
Err(e) => {
startup_warnings.push(format!(
"Warning: Failed to load per-repo config at {}: {e}",
travelagent_core::config::repo_config_path(&app.vcs_info.root_path).display()
));
}
}
}
pub(crate) fn apply_blind_tests_config(
app: &mut App,
cli: &Cli,
startup_warnings: &mut Vec<String>,
) {
if cli.demo || !app.vcs_info.root_path.is_absolute() {
return;
}
match travelagent_core::review_config::load_review_config(&app.vcs_info.root_path) {
Ok(outcome) => {
startup_warnings.extend(outcome.warnings);
app.blind_patterns = outcome.config.hidden_from_reviewer;
if cli.blind_tests {
if app.blind_patterns.is_empty() {
startup_warnings.push(
"Warning: --blind-tests set but hidden_from_reviewer is empty in .travelagent/review.toml; nothing to hide".to_string(),
);
} else {
app.blind_mode = true;
let matcher = travelagent_core::trvignore::matcher_from_patterns(
&app.vcs_info.root_path,
&app.blind_patterns,
);
let before = app.diff_files.len();
let filtered = travelagent_core::trvignore::filter_diff_files_with_matcher(
matcher.as_ref(),
std::mem::take(&mut app.diff_files),
);
let after = filtered.len();
app.diff_files = filtered;
app.set_message(format!(
"Blind-tests on: hiding {} file(s)",
before.saturating_sub(after)
));
}
}
}
Err(e) => {
startup_warnings.push(format!(
"Warning: Failed to load {}: {e}",
travelagent_core::review_config::review_config_path(&app.vcs_info.root_path)
.display()
));
}
}
}
pub(crate) fn try_enter_spar_mode(app: &mut App, cli: &Cli, startup_warnings: &mut Vec<String>) {
if !cli.spar {
return;
}
match enter_spar_mode(app) {
Ok(SparEntryOutcome::Created(branch)) => {
app.spar_mode = true;
app.set_message(format!("Sparring mode on: created branch {branch}"));
}
Ok(SparEntryOutcome::Resumed(branch)) => {
app.spar_mode = true;
app.set_message(format!("Sparring mode on: resumed branch {branch}"));
}
Ok(SparEntryOutcome::FlagOnly(reason)) => {
app.spar_mode = true;
startup_warnings.push(format!(
"Warning: Sparring mode active as flag only — {}",
reason.user_message()
));
}
Err(err) => {
startup_warnings.push(format!("Warning: Could not enter sparring mode: {err}"));
}
}
}
pub(crate) fn apply_config_defaults_to_app(app: &mut App, config: &AppConfig) {
app.risk_config = config.risk.clone();
app.invalidate_tour_score_cache();
app.auto_collapse_cfg = config.auto_collapse.clone();
app.set_comment_templates(config.comment_templates.clone());
if config.show_file_list == Some(false) {
app.ui_layout.show_file_list = false;
app.nav.focused_panel = FocusedPanel::Diff;
}
if let Some(width) = config.file_list_width {
app.ui_layout.set_file_list_width_pct(width);
}
if config.diff_view.as_deref() == Some("side-by-side") {
app.nav.diff_view_mode = app::DiffViewMode::SideBySide;
}
if config.wrap == Some(true) {
app.set_diff_wrap(true);
}
if config.export_legend == Some(false) {
app.export_legend = false;
}
if config.auto_stage == Some(true) {
app.auto_stage = true;
}
if config.confirm_on_quit == Some(false) {
app.confirm_on_quit = false;
}
if config.command_palette == Some(false) {
app.ui_layout.command_palette = false;
}
if let Some(word_diff) = config.word_diff {
app.word_diff_enabled = word_diff;
}
if let Some(md) = config.markdown_rendering {
app.markdown_rendering_enabled = md;
}
app.mental_model_byte_limit = config.mental_model.byte_limit;
}
pub(crate) fn apply_narrow_terminal_default(app: &mut App) {
if let Ok((width, _)) = crossterm::terminal::size()
&& width < crate::MIN_WIDTH_FOR_FILE_LIST
{
app.ui_layout.show_file_list = false;
app.nav.focused_panel = FocusedPanel::Diff;
}
}
pub(crate) fn seed_tour_plan_if_requested(app: &mut App, cli: &Cli) {
if cli.tour.is_none() {
return;
}
let app::DiffSource::CommitRange(ref commit_ids) = app.diff_source else {
return;
};
let stops: Vec<travelagent_core::model::TourStop> = commit_ids
.iter()
.map(|sha| travelagent_core::model::TourStop {
commit_ids: vec![sha.clone()],
summary: String::new(),
risk: travelagent_core::risk::RiskScore::MIN,
})
.collect();
if !stops.is_empty() {
let _ = app.tour_start(stops);
}
}