From c6947bbceecc09cc781c8e872b660a20d5aa4f0b Mon Sep 17 00:00:00 2001 From: Paul Gey Date: Fri, 16 May 2025 21:36:37 +0200 Subject: [PATCH] Implement changing `command`s via keyboard shortcut --- src/exec/executor.rs | 8 +++- src/help/list_jobs.rs | 12 ++++-- src/internal.rs | 7 ++++ src/jobs/job.rs | 94 ++++++++++++++++++++++++++++++++++++++++--- src/mission.rs | 25 ++++++++---- src/tui/app.rs | 17 +++++++- src/tui/app_state.rs | 3 ++ 7 files changed, 144 insertions(+), 22 deletions(-) diff --git a/src/exec/executor.rs b/src/exec/executor.rs index dc1e64dd..f343d4c7 100644 --- a/src/exec/executor.rs +++ b/src/exec/executor.rs @@ -1,6 +1,7 @@ use { crate::*, std::{ + collections::HashSet, io::{ self, BufRead, @@ -75,8 +76,11 @@ impl TaskExecutor { impl MissionExecutor { /// Prepare the executor (no task/process/thread is started at this point) - pub fn new(mission: &Mission) -> anyhow::Result { - let command_builder = mission.get_command()?; + pub fn new( + mission: &Mission, + options: &HashSet, + ) -> anyhow::Result { + let command_builder = mission.get_command(options)?; let kill_command = mission.kill_command(); let (line_sender, line_receiver) = channel::unbounded(); Ok(Self { diff --git a/src/help/list_jobs.rs b/src/help/list_jobs.rs index 0aa50e7c..b48ae918 100644 --- a/src/help/list_jobs.rs +++ b/src/help/list_jobs.rs @@ -24,10 +24,14 @@ pub fn print_jobs(settings: &Settings) { let mut jobs: Vec<_> = settings.jobs.iter().collect(); jobs.sort_by_key(|(name, _)| name.to_string()); for (name, job) in &jobs { - expander - .sub("jobs") - .set("job_name", name) - .set("job_command", job.command.join(" ")); + expander.sub("jobs").set("job_name", name).set( + "job_command", + job.command + .iter() + .map(|arg| arg.to_string()) + .collect::>() + .join(" "), + ); } expander.set("default_job", &settings.default_job); let skin = MadSkin::default(); diff --git a/src/internal.rs b/src/internal.rs index 08689239..b80c69c7 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -37,6 +37,7 @@ pub enum Internal { ScopeToFailures, Scroll(ScrollCommand), ToggleBacktrace(&'static str), + ToggleOption(String), TogglePause, // either pause or unpause ToggleRawOutput, ToggleSummary, @@ -70,6 +71,7 @@ impl Internal { Self::ScopeToFailures => "scope to failures".to_string(), Self::Scroll(scroll_command) => scroll_command.doc(), Self::ToggleBacktrace(level) => format!("toggle backtrace ({level})"), + Self::ToggleOption(option) => format!("toggle option {option:?}"), Self::TogglePause => "toggle pause".to_string(), Self::ToggleRawOutput => "toggle raw output".to_string(), Self::ToggleSummary => "toggle summary".to_string(), @@ -99,6 +101,7 @@ impl fmt::Display for Internal { Self::ScopeToFailures => write!(f, "scope-to-failures"), Self::Scroll(scroll_command) => scroll_command.fmt(f), Self::ToggleBacktrace(level) => write!(f, "toggle-backtrace({level})"), + Self::ToggleOption(option) => write!(f, "toggle-option({option})"), Self::TogglePause => write!(f, "toggle-pause"), Self::ToggleRawOutput => write!(f, "toggle-raw-output"), Self::ToggleSummary => write!(f, "toggle-summary"), @@ -183,6 +186,9 @@ impl std::str::FromStr for Internal { file: file.trim().to_string(), })); } + if let Some((_, option)) = regex_captures!(r"^toggle-option\((.*)\)$", s) { + return Ok(Self::ToggleOption(option.to_string())); + } Err("invalid internal".to_string()) } } @@ -228,6 +234,7 @@ fn test_internal_string_round_trip() { Internal::Scroll(ScrollCommand::MilliPages(1561)), Internal::Scroll(ScrollCommand::Top), Internal::ToggleBacktrace("1"), + Internal::ToggleOption("some-op\"tion".to_string()), Internal::ToggleBacktrace("full"), Internal::TogglePause, Internal::ToggleSummary, diff --git a/src/jobs/job.rs b/src/jobs/job.rs index 87725731..aa130acd 100644 --- a/src/jobs/job.rs +++ b/src/jobs/job.rs @@ -1,7 +1,13 @@ use { crate::*, serde::Deserialize, - std::collections::HashMap, + std::{ + collections::{ + HashMap, + HashSet, + }, + fmt::Display, + }, }; /// One of the possible jobs that bacon can run @@ -35,7 +41,7 @@ pub struct Job { /// The tokens making the command to execute (first one /// is the executable). #[serde(default)] - pub command: Vec, + pub command: Vec, /// Whether to apply the default watch list, which is /// `["src", "tests", "benches", "examples", "build.rs"]` @@ -113,14 +119,17 @@ impl Job { alias_name: &str, settings: &Settings, ) -> Self { - let mut command = vec!["cargo".to_string(), alias_name.to_string()]; + let mut command = vec![ + CommandItem::String("cargo".to_string()), + CommandItem::String(alias_name.to_string()), + ]; if let Some(additional_args) = settings.additional_alias_args.as_ref() { for arg in additional_args { - command.push(arg.to_string()) + command.push(CommandItem::String(arg.to_string())) } } else { for arg in DEFAULT_ARGS { - command.push(arg.to_string()) + command.push(CommandItem::String(arg.to_string())) } } Self { @@ -230,6 +239,76 @@ impl Job { } } +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum CommandItem { + String(String), + Conditional { + #[serde(rename = "if")] + condition: String, + then: Option, + #[serde(rename = "else")] + or_else: Option, + }, +} + +impl CommandItem { + pub(crate) fn resolve<'a>( + &'a self, + options: &HashSet, + ) -> Option<&'a str> { + match self { + CommandItem::String(s) => Some(s), + CommandItem::Conditional { + condition, + then, + or_else, + } => { + if options.contains(condition) { + then.as_deref() + } else { + or_else.as_deref() + } + } + } + } +} + +impl PartialEq<&str> for CommandItem { + fn eq( + &self, + other: &&str, + ) -> bool { + match self { + CommandItem::String(s) => s == other, + CommandItem::Conditional { .. } => false, + } + } +} + +impl Display for CommandItem { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + match self { + CommandItem::String(s) => write!(f, "{s}"), + CommandItem::Conditional { + condition, + then, + or_else, + } => { + write!( + f, + "${{{{ if {condition} {{ {} }} else {{ {} }} }}}}", + then.as_deref().unwrap_or("∅"), + or_else.as_deref().unwrap_or("∅"), + ) + } + } + } +} + #[test] fn test_job_apply() { use std::str::FromStr; @@ -240,7 +319,10 @@ fn test_job_apply() { analyzer: Some(AnalyzerRef::Nextest), apply_gitignore: Some(false), background: Some(false), - command: vec!["cargo".to_string(), "test".to_string()], + command: vec![ + CommandItem::String("cargo".to_string()), + CommandItem::String("test".to_string()), + ], default_watch: Some(false), env: vec![("RUST_LOG".to_string(), "debug".to_string())] .into_iter() diff --git a/src/mission.rs b/src/mission.rs index c4985fa3..a5b81bd6 100644 --- a/src/mission.rs +++ b/src/mission.rs @@ -3,7 +3,10 @@ use { lazy_regex::regex_replace_all, rustc_hash::FxHashSet, std::{ - collections::HashMap, + collections::{ + HashMap, + HashSet, + }, path::PathBuf, }, }; @@ -76,10 +79,18 @@ impl Mission<'_> { } /// build (and doesn't call) the external cargo command - pub fn get_command(&self) -> anyhow::Result { - let mut command = if self.job.expand_env_vars() { - self.job - .command + pub fn get_command( + &self, + options: &HashSet, + ) -> anyhow::Result { + let mut command: Vec<_> = self + .job + .command + .iter() + .filter_map(|arg| Some(arg.resolve(options)?.to_string())) + .collect(); + if self.job.expand_env_vars() { + command = command .iter() .map(|token| { regex_replace_all!(r"\$([A-Z0-9a-z_]+)", token, |whole: &str, name| { @@ -94,9 +105,7 @@ impl Mission<'_> { .to_string() }) .collect() - } else { - self.job.command.clone() - }; + } if command.is_empty() { anyhow::bail!( diff --git a/src/tui/app.rs b/src/tui/app.rs index 86336d8a..ced29680 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -129,9 +129,9 @@ fn run_mission( let config_watcher = Watcher::new(&mission.settings.config_files, IgnorerSet::default())?; // create the executor, mission, and state - let mut executor = MissionExecutor::new(&mission)?; - let on_change_strategy = mission.job.on_change_strategy(); let mut state = AppState::new(mission, headless)?; + let mut executor = MissionExecutor::new(&state.mission, &state.options)?; + let on_change_strategy = state.mission.job.on_change_strategy(); if let Some(message) = message { state.messages.push(message); } @@ -361,6 +361,19 @@ fn run_mission( Internal::Scroll(scroll_command) => { state.apply_scroll_command(scroll_command); } + Internal::ToggleOption(option) => { + if state.options.contains(&option) { + state.options.remove(&option); + } else { + state.options.insert(option); + } + warn!("options: {:?}", state.options); + task_executor.die(); + // recreate `executor` to account for the changed option + executor = MissionExecutor::new(&state.mission, &state.options)?; + task_executor = state.start_computation(&mut executor)?; + break; // drop following actions + } Internal::ToggleBacktrace(level) => { state.toggle_backtrace(level); task_executor.die(); diff --git a/src/tui/app_state.rs b/src/tui/app_state.rs index 81d8022e..c3dfd310 100644 --- a/src/tui/app_state.rs +++ b/src/tui/app_state.rs @@ -3,6 +3,7 @@ use { anyhow::Result, crokey::KeyCombination, std::{ + collections::HashSet, io::Write, process::ExitStatus, time::Instant, @@ -76,6 +77,7 @@ pub struct AppState<'s> { pub messages: Vec, /// the search state pub search: SearchState, + pub options: HashSet, } impl<'s> AppState<'s> { @@ -123,6 +125,7 @@ impl<'s> AppState<'s> { changes_since_last_job_start: 0, messages: Vec::new(), search: Default::default(), + options: HashSet::default(), }) } pub fn focus_file(