diff --git a/crates/spk-solve/src/io.rs b/crates/spk-solve/src/io.rs index 476971883..69e5e1e77 100644 --- a/crates/spk-solve/src/io.rs +++ b/crates/spk-solve/src/io.rs @@ -23,6 +23,7 @@ use spk_schema::foundation::format::{ FormatIdent, FormatOptionMap, FormatRequest, + FormatSolution, }; use spk_schema::prelude::*; use spk_solve_graph::{ @@ -31,6 +32,7 @@ use spk_solve_graph::{ Graph, Node, Note, + State, DUPLICATE_REQUESTS_COUNT, REQUESTS_FOR_SAME_PACKAGE_COUNT, }; @@ -193,17 +195,163 @@ where Ok(()) } - fn wait_for_user_to_hit_enter(&self) -> Result<()> { - let mut _input = String::new(); + fn wait_for_user_selection(&self) -> Result { + let mut input = String::new(); use std::io::Write; let _ = std::io::stdout().flush(); - if let Err(err) = std::io::stdin().read_line(&mut _input) { + if let Err(err) = std::io::stdin().read_line(&mut input) { // If there's some stdin can't be read, it is probably // better to continue with the solve than error out. tracing::warn!("{err}"); } + Ok(input) + } + + fn show_resolved_packages(&self, state: &Arc) { + match state.as_solution() { + Err(err) => { + tracing::error!("{err}") + } + Ok(solution) => { + // Can't use this without access to the solver repos + // and async-ing this method. Could look at enabling + // this in future. + // solution + // .format_solution_with_highest_versions( + // self.settings.verbosity, + // runtime.solver.repositories(), + // false, + // ) + // .await? + tracing::info!( + "{}{}", + self.settings.heading_prefix, + solution.format_solution(self.settings.verbosity) + ); + } + } + } + + fn show_unresolved_requests(&self, state: &Arc) { + let unresolved_requests = state + .get_unresolved_requests() + .iter() + .map(|(n, r)| { + r.format_request( + None.as_ref(), + n, + &FormatChangeOptions { + verbosity: self.verbosity, + level: self.level, + }, + ) + }) + .collect::>(); + tracing::info!( + "{}\n {}\nNumber of Unresolved Requests: {}", + "Unresolved Requests:".yellow(), + unresolved_requests.join("\n "), + unresolved_requests.len() + ); + } + + fn show_var_requests(&self, state: &Arc) { + let vars = state + .get_var_requests() + .iter() + .map(|v| format!("{}: {}", v.var, v.value.as_pinned().unwrap_or_default())) + .collect::>(); + tracing::info!( + "{}\n {}\nNumber of Var Requests: {:?}", + "Var Requests:".yellow(), + vars.join("\n "), + vars.len() + ); + } + + fn show_options(&self, state: &Arc) { + let options = state.get_option_map(); + tracing::info!( + "{}\n {}\nNumber of Options: {}", + "Options:".yellow(), + options.format_option_map().replace(", ", "\n "), + options.len() + ); + } + + fn show_state(&self, state: &Arc) { + self.show_resolved_packages(state); + self.show_unresolved_requests(state); + self.show_var_requests(state); + self.show_options(state); + } + + fn show_full_menu(&self, prompt_prefix: &str) { + println!("{} Enter a letter for an action:", prompt_prefix.yellow()); + println!(" ? - Print help (these details)"); + println!(" r - Show resolved packages"); + println!(" u - Show unresolved requests"); + println!(" v - Show var requests"); + println!(" o - Show options"); + println!(" s, a - Show state [all of the above]"); + println!(" c - Run solver to completion, removes step/stop"); + println!(" Ctrl-c - Interrupt this program"); + println!(" any other - Continue solving"); + } + + fn remove_step_and_stop_setting(&mut self) { + self.settings.stop_on_block = false; + self.settings.step_on_block = false; + self.settings.step_on_decision = false; + } + + fn show_state_menu( + &mut self, + current_state: &Option>, + prompt_prefix: String, + ) -> Result<()> { + // TODO: change the timeout that auto-increases the verbosity + // or disable when this menu is active + if let Some(state) = current_state { + // Simplistic menu for now + loop { + // Show a compressed version of the menu + print!( + "{} Select one of [r,u,v,o,s,a,c,?,C-c]> ", + prompt_prefix.yellow() + ); + + // Get selection + let response = self.wait_for_user_selection()?; + let selection = match response.to_lowercase().chars().next() { + None => continue, + Some(c) => c, + }; + + // Act on the selection + match selection { + '?' => self.show_full_menu(&prompt_prefix), + 'r' => self.show_resolved_packages(state), + 'u' => self.show_unresolved_requests(state), + 'v' => self.show_var_requests(state), + 'o' => self.show_options(state), + 's' | 'a' => self.show_state(state), + 'c' => { + self.remove_step_and_stop_setting(); + break; + } + // TODO: could look at adding other things in future: + // - show dep graph image based on current resolved/unresolved + // - a breakpoint for a request, with continue till such request + // - launch spk env based on current resolved packages + // - rewind the solve + // - save/restore point for the solve, for use in tests + _ => break, + } + } + } Ok(()) } @@ -226,11 +374,7 @@ where stream! { let mut stop_because_blocked = false; let mut step_because_blocked = false; - // Used to avoid decision pauses related to setting up the - // initial state when using --step-on-decision. It'll be - // replaced by the current state when displaying the state - // when paused it added in future - let mut made_a_decision = false; + let mut current_state: Option> = None; 'outer: loop { if let Some(next) = self.output_queue.pop_front() { yield Ok(next); @@ -240,8 +384,9 @@ where // Check if the solver should pause because the last // decision was a step-back (BLOCKED) with step-on-block set if step_because_blocked { - yield(Ok(format!("{}", "Pausing at BLOCKED state. Press 'Enter' to continue.".to_string().yellow()))); - if let Err(err) = self.wait_for_user_to_hit_enter() { + if let Err(err) = self.show_state_menu(¤t_state, + "Paused at BLOCKED state.".to_string() + ) { yield(Err(err)); } step_because_blocked = false; @@ -250,15 +395,17 @@ where // Check if the solver should stop because the last decision // was a step-back (BLOCKED) with stop-on-block set if stop_because_blocked { + if let Err(err) = self.show_state_menu(¤t_state, "Hit at BLOCKED state. Stopping after menu.".to_string()) { + yield(Err(err)); + } yield(Err(Error::SolverInterrupted( - format!("hit BLOCKED state with {STOP_ON_BLOCK_FLAG} enabled."), - ))); + format!("At BLOCKED state with {STOP_ON_BLOCK_FLAG} enabled. Stopping."), + ))); continue 'outer; } - if self.settings.step_on_decision && made_a_decision { - yield(Ok(format!("{}", "Pausing. Press 'Enter' to continue.".to_string().yellow()))); - if let Err(err) = self.wait_for_user_to_hit_enter() { + if self.settings.step_on_decision && current_state.is_some() { + if let Err(err) = self.show_state_menu(¤t_state, "Pausing after decision.".to_string()) { yield(Err(err)); } } @@ -279,8 +426,7 @@ where continue 'outer; } }; - - made_a_decision = true; + current_state = Some(Arc::clone(&node.state)); self.render_statusbar(&node)?;