From ff3cd11099a18fcfebf28b8c370399275b97868f Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 25 Oct 2025 05:38:54 +0700 Subject: [PATCH] Now mostly usable --- Cargo.lock | 53 +--- Cargo.toml | 3 +- clean.sh | 10 +- src/bin/repo.rs | 559 ++++++++++++++++++++++++++++++----------- src/cook/cook_build.rs | 44 +++- src/cook/fetch.rs | 4 +- unfetch.sh | 11 +- 7 files changed, 457 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2319a2344..58737f927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -611,17 +611,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ctrlc" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" -dependencies = [ - "dispatch", - "nix", - "windows-sys 0.61.2", -] - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -753,12 +742,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "displaydoc" version = "0.2.5" @@ -1554,18 +1537,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.1", - "cfg-if 1.0.1", - "cfg_aliases", - "libc", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2036,7 +2007,6 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3 1.5.3", - "ctrlc", "ignore", "object", "pbr", @@ -3194,7 +3164,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] @@ -3227,19 +3197,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3248,7 +3212,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3287,15 +3251,6 @@ dependencies = [ "windows-targets 0.53.2", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index b4ba691bd..8b28c1c1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "redox_cookbook" version = "0.1.0" authors = ["Jeremy Soller "] edition = "2024" -default-run = "cook" +default-run = "repo" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,7 +23,6 @@ doctest = false [dependencies] anyhow = "1" blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar -ctrlc = { version = "3.5.0", features = ["termination"] } ignore = "0.4" object = { version = "0.36", features = ["build_core"] } pbr = "1.0.2" diff --git a/clean.sh b/clean.sh index 6f7d0b0fd..211fcb7cb 100755 --- a/clean.sh +++ b/clean.sh @@ -5,15 +5,9 @@ source config.sh if [ $# = 0 ] then - recipes="$(list_recipes --short)" + recipes="--all" else recipes="$@" fi -for recipe_name in $recipes -do - recipe_path=`find_recipe $recipe_name` - - echo -e "\033[01;38;5;215mcook - clean $recipe_name\033[0m" - rm -rf "${ROOT}/$recipe_path/target/${TARGET}" -done +repo clean $recipes diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 4105f6d83..8f867db73 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; -use std::io::{BufRead, BufReader, PipeReader, stdout}; +use std::collections::{HashMap, VecDeque}; +use std::io::{BufRead, BufReader, PipeReader, Write, stderr, stdin, stdout}; use std::path::PathBuf; +use std::process::Command; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, mpsc}; use std::time::Duration; use std::{env, fs}; @@ -22,11 +23,13 @@ use ratatui::Terminal; use ratatui::layout::{Constraint, Direction, Layout, Position, Rect}; use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}; use termion::event::{Event, Key, MouseEvent}; -use termion::input::{MouseTerminal, TermRead}; -use termion::raw::RawTerminal; -use termion::screen::{AlternateScreen, ToAlternateScreen, ToMainScreen}; +use termion::input::TermRead; +use termion::raw::IntoRawMode; +use termion::screen::IntoAlternateScreen; +use termion::{color, style}; // A repo manager, to replace repo.sh @@ -125,7 +128,10 @@ impl CliConfig { fn main() { init_config(); - main_inner().unwrap(); + if let Err(e) = main_inner() { + eprintln!("{:?}", e); + process::exit(1); + }; } fn main_inner() -> anyhow::Result<()> { @@ -139,7 +145,19 @@ fn main_inner() -> anyhow::Result<()> { let (config, command, recipe_names) = parse_args(args)?; if command == CliCommand::Cook && config.cook.tui { - run_tui_cook(config, recipe_names)?; + if let Some(e) = run_tui_cook(config, recipe_names)? { + let _ = stderr().write(e.as_bytes()); + let _ = stderr().write(b"\n\n"); + return Err(anyhow!("Execution has failed")); + } else { + eprintln!( + "{}{}cook - successful{}{}", + style::Bold, + color::Fg(color::AnsiValue(215)), + color::Fg(color::Reset), + style::Reset, + ); + } return Ok(()); } @@ -343,27 +361,32 @@ enum RecipeStatus { #[derive(Debug, Clone)] enum StatusUpdate { StartFetch(PackageName), - Fetched(PackageName), - FailFetch(PackageName, String), + Fetched(CookRecipe), + FailFetch(CookRecipe, String), StartCook(PackageName), - CookLog(PackageName, String), - Cooked(PackageName), - FailCook(PackageName, String), + Cooked(CookRecipe), + FailCook(CookRecipe, String), + PushLog(PackageName, String), FetchThreadFinished, CookThreadFinished, } +#[derive(PartialEq)] +enum JobType { + Fetch, + Cook, +} + struct TuiApp { recipes: Vec<(CookRecipe, RecipeStatus)>, - fetch_queue: Vec, - cook_queue: Vec, + fetch_queue: VecDeque, + cook_queue: VecDeque, done: Vec, - failed: Vec, active_fetch: Option, active_cook: Option, logs: HashMap>, log_scroll: u16, - log_view_cook: bool, + log_view_job: JobType, auto_scroll: bool, fetch_scroll: u16, cook_scroll: u16, @@ -372,26 +395,27 @@ struct TuiApp { fetch_panel_rect: Option, cook_panel_rect: Option, log_panel_rect: Option, + prompt: Option, + dump_logs_on_exit: Option, } impl TuiApp { fn new(recipes: Vec) -> Self { - let recipe_names = recipes.iter().map(|r| r.name.clone()).collect(); Self { recipes: recipes - .into_iter() + .iter() + .cloned() .map(|r| (r, RecipeStatus::Pending)) .collect(), - fetch_queue: recipe_names, - cook_queue: Vec::new(), + fetch_queue: recipes.iter().cloned().map(|r| r.clone()).collect(), + cook_queue: VecDeque::new(), done: Vec::new(), - failed: Vec::new(), active_fetch: None, active_cook: None, logs: HashMap::new(), log_scroll: 0, auto_scroll: true, - log_view_cook: false, + log_view_job: JobType::Fetch, fetch_scroll: 0, cook_scroll: 0, fetch_complete: false, @@ -399,6 +423,8 @@ impl TuiApp { fetch_panel_rect: None, cook_panel_rect: None, log_panel_rect: None, + prompt: None, + dump_logs_on_exit: None, } } @@ -412,14 +438,17 @@ impl TuiApp { self.auto_scroll = true; (name.clone(), RecipeStatus::Fetching) } - StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), - StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), + StatusUpdate::Fetched(recipe) => (recipe.name.clone(), RecipeStatus::Fetched), + StatusUpdate::FailFetch(recipe, err) => { + self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); + (recipe.name.clone(), RecipeStatus::Failed(err)) + } StatusUpdate::StartCook(name) => { self.active_cook = Some(name.clone()); self.logs.insert(name.clone(), Vec::new()); (name.clone(), RecipeStatus::Cooking) } - StatusUpdate::CookLog(name, line) => { + StatusUpdate::PushLog(name, line) => { self.logs.entry(name.clone()).or_default().push(line); // No status change, just return the current state if let Some((_, status)) = self.recipes.iter().find(|(r, _)| r.name == name) { @@ -428,23 +457,21 @@ impl TuiApp { return; // Should not happen } } - StatusUpdate::Cooked(name) => { - if self.active_cook.as_ref() == Some(&name) { + StatusUpdate::Cooked(recipe) => { + if self.active_cook.as_ref() == Some(&recipe.name) { self.active_cook = None; } self.auto_scroll = true; - (name.clone(), RecipeStatus::Done) + (recipe.name.clone(), RecipeStatus::Done) } - StatusUpdate::FailCook(name, err) => { - if self.active_cook.as_ref() == Some(&name) { - self.active_cook = None; - } - self.auto_scroll = false; - (name.clone(), RecipeStatus::Failed(err)) + StatusUpdate::FailCook(recipe, err) => { + self.prompt = Some(FailurePrompt::new(recipe.clone(), err.clone())); + + (recipe.name.clone(), RecipeStatus::Failed(err)) } StatusUpdate::FetchThreadFinished => { self.fetch_complete = true; - self.log_view_cook = true; + self.log_view_job = JobType::Cook; return; } StatusUpdate::CookThreadFinished => { @@ -461,14 +488,14 @@ impl TuiApp { self.fetch_queue = self .recipes .iter() - .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) - .map(|(r, _)| r.name.clone()) + .filter(|(_, s)| *s == RecipeStatus::Pending) + .map(|(r, _)| r.clone()) .collect(); self.cook_queue = self .recipes .iter() - .filter(|(_, s)| *s == RecipeStatus::Fetched || *s == RecipeStatus::Cooking) - .map(|(r, _)| r.name.clone()) + .filter(|(_, s)| *s == RecipeStatus::Fetched) + .map(|(r, _)| (r.clone())) .collect(); self.done = self .recipes @@ -476,24 +503,22 @@ impl TuiApp { .filter(|(_, s)| *s == RecipeStatus::Done) .map(|(r, _)| r.name.clone()) .collect(); - self.failed = self - .recipes - .iter() - .filter(|(_, s)| matches!(s, RecipeStatus::Failed(_))) - .map(|(r, _)| r.name.clone()) - .collect(); } } -fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<()> { +fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result> { let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); let (status_tx, status_rx) = mpsc::channel::(); + let running = Arc::new(AtomicBool::new(true)); + let prompting = Arc::new(AtomicU32::new(0)); + // ---- Cooker Thread ---- let cooker_config = config.clone(); let cooker_status_tx = status_tx.clone(); + let cooker_prompting = prompting.clone(); let cooker_handle = thread::spawn(move || { - for (recipe, source_dir) in work_rx { + 'done: for (recipe, source_dir) in work_rx { let name = recipe.name.clone(); let is_deps = recipe.is_deps; cooker_status_tx @@ -501,11 +526,51 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap(); let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name); let logger = Some((&mut stdout_writer, &mut stderr_writer)); - match handle_cook(&recipe, &cooker_config, source_dir, is_deps, &logger) { - Ok(_) => cooker_status_tx.send(StatusUpdate::Cooked(name)).unwrap(), - Err(e) => cooker_status_tx - .send(StatusUpdate::FailCook(name, e.to_string())) - .unwrap(), + 'again: loop { + match handle_cook( + &recipe, + &cooker_config, + source_dir.clone(), + is_deps, + &logger, + ) { + Ok(()) => { + cooker_status_tx + .send(StatusUpdate::Cooked(recipe)) + .unwrap_or_default(); + break; + } + Err(e) => { + cooker_status_tx + .send(StatusUpdate::FailCook(recipe.clone(), e.to_string())) + .unwrap_or_default(); + if !cooker_config.cook.nonstop { + while cooker_prompting.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_millis(101)); // wait other prompt + } + cooker_prompting.swap(1, Ordering::SeqCst); + 'wait: loop { + match cooker_prompting.load(Ordering::SeqCst) { + 0 => break 'again, + 1 => thread::sleep(Duration::from_millis(101)), + 2 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'wait; + } // retry + 3 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'again; + } // skip + 4 => { + cooker_prompting.swap(0, Ordering::SeqCst); + break 'done; + } // done + _ => unreachable!(), + } + } + } + } + } } } cooker_status_tx @@ -513,11 +578,17 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap_or_default(); }); + let mstdin = stdin(); + let mstdout = stdout() + .into_raw_mode() + .unwrap() + .into_alternate_screen() + .unwrap(); + // ----- Input Thread ----- let (input_tx, input_rx) = mpsc::channel::(); let _input_handle = thread::spawn(move || { - let stdin = std::io::stdin(); - for evt in stdin.events() { + for evt in mstdin.events() { if let Ok(evt) = evt { if input_tx.send(evt).is_err() { return; @@ -528,28 +599,62 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( // ---- Fetcher Thread ---- let fetcher_recipes = recipes.clone(); + let fetcher_status_tx = status_tx.clone(); let fetcher_config = config.clone(); + let fetcher_prompting = prompting.clone(); let fetcher_handle = thread::spawn(move || { - for recipe in fetcher_recipes { + 'done: for recipe in fetcher_recipes { let name = recipe.name.clone(); - status_tx + fetcher_status_tx .send(StatusUpdate::StartFetch(name.clone())) .unwrap(); - let (mut stdout_writer, mut stderr_writer) = setup_logger(&status_tx, &name); + let (mut stdout_writer, mut stderr_writer) = setup_logger(&fetcher_status_tx, &name); let logger = Some((&mut stdout_writer, &mut stderr_writer)); - match handle_fetch(&recipe, &fetcher_config, &logger) { - Ok(source_dir) => { - status_tx.send(StatusUpdate::Fetched(name)).unwrap(); - if work_tx.send((recipe.clone(), source_dir)).is_err() { - // Cooker thread died + 'again: loop { + match handle_fetch(&recipe, &fetcher_config, &logger) { + Ok(source_dir) => { + fetcher_status_tx + .send(StatusUpdate::Fetched(recipe.clone())) + .unwrap(); + if work_tx.send((recipe.clone(), source_dir)).is_err() { + // Cooker thread died + break 'done; + } break; } + Err(e) => { + fetcher_status_tx + .send(StatusUpdate::FailFetch(recipe.clone(), e.to_string())) + .unwrap_or_default(); + if !fetcher_config.cook.nonstop { + while fetcher_prompting.load(Ordering::SeqCst) != 0 { + thread::sleep(Duration::from_millis(101)); // wait other prompt + } + fetcher_prompting.swap(1, Ordering::SeqCst); + 'wait: loop { + match fetcher_prompting.load(Ordering::SeqCst) { + 0 => break 'again, + 1 => thread::sleep(Duration::from_millis(101)), + 2 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'wait; + } // retry + 3 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'again; + } // skip + 4 => { + fetcher_prompting.swap(0, Ordering::SeqCst); + break 'done; + } // done + _ => unreachable!(), + } + } + } + } } - Err(e) => status_tx - .send(StatusUpdate::FailFetch(name, e.to_string())) - .unwrap(), } } status_tx @@ -557,28 +662,19 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .unwrap_or_default(); }); - print!("{}", ToAlternateScreen); let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; terminal.clear()?; let mut app = TuiApp::new(recipes); - // let total_recipes = app.recipes.len(); - let running = Arc::new(AtomicBool::new(true)); - ctrlc::set_handler(move || { - print!("{}", ToMainScreen); - process::exit(1); - }) - .context("Error setting Ctrl-C handler")?; - - while running.load(Ordering::Relaxed) { + while running.load(Ordering::SeqCst) { terminal.draw(|f| { let mut constraints = Vec::new(); if !app.fetch_complete { constraints.push(Constraint::Length(30)); } constraints.push(Constraint::Length(30)); - constraints.push(Constraint::Min(20)); // Log panel always exists + constraints.push(Constraint::Min(20)); let chunks = Layout::default() .direction(Direction::Horizontal) .constraints(constraints) @@ -633,7 +729,7 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( ); f.render_widget(cook_list, chunks[if app.fetch_complete { 0 } else { 1 }]); - let active_name = if app.log_view_cook { + let active_name = if app.log_view_job == JobType::Cook { &app.active_cook } else { &app.active_fetch @@ -667,8 +763,9 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( } } else { if total_log_lines > log_pane_height { - if app.log_scroll > total_log_lines - log_pane_height { + if app.log_scroll >= total_log_lines - log_pane_height { app.log_scroll = total_log_lines - log_pane_height; + app.auto_scroll = true; } } else { app.log_scroll = 0; @@ -685,66 +782,21 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( chunks[if app.fetch_complete { 1 } else { 2 }], ); + if let Some(prompt) = &app.prompt { + draw_prompt(f, prompt); + } + while let Ok(event) = input_rx.try_recv() { - match event { - Event::Key(key) => match key { - Key::Char('\t') => { - app.log_view_cook = !app.log_view_cook; - } - Key::Char('1') => { - app.log_view_cook = false; - } - Key::Char('2') => { - app.log_view_cook = true; - } - Key::Up => { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_sub(1); - } - Key::Down => { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_add(1); - } - _ => {} - }, - - Event::Mouse(mouse_event) => { - match mouse_event { - MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { - // termion is 1-based, ratatui rects are 0-based - let pos = Position { - x: x.saturating_sub(1), - y: y.saturating_sub(1), - }; - - if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { - app.fetch_scroll = app.fetch_scroll.saturating_sub(1); - } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { - app.cook_scroll = app.cook_scroll.saturating_sub(1); - } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_sub(1); - } - } - MouseEvent::Press(termion::event::MouseButton::WheelDown, x, y) => { - let pos = Position { - x: x.saturating_sub(1), - y: y.saturating_sub(1), - }; - - if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { - app.fetch_scroll = app.fetch_scroll.saturating_add(1); - } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { - app.cook_scroll = app.cook_scroll.saturating_add(1); - } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { - app.auto_scroll = false; - app.log_scroll = app.log_scroll.saturating_add(1); - } - } - _ => {} + if app.prompt.is_some() { + if let Some(res) = handle_prompt_input(event, &mut app, &running) { + prompting.swap(res as u32, Ordering::SeqCst); + if res == PromptOption::Exit { + app.dump_logs_on_exit = Some(log_text.join("\n")) } + app.prompt = None; } - _ => {} + } else { + handle_main_event(&mut app, event); } } })?; @@ -753,19 +805,194 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( app.update_status(update); } - if fetcher_handle.is_finished() && cooker_handle.is_finished() { - thread::sleep(Duration::from_secs(5)); + if app.cook_complete { running.swap(false, Ordering::SeqCst); } } - // disable_raw_mode()?; - print!("{}", ToMainScreen); + drop(mstdout); + let _ = stdout().flush(); fetcher_handle.join().unwrap(); cooker_handle.join().unwrap(); - Ok(()) + Ok(app.dump_logs_on_exit) +} + +fn handle_main_event(app: &mut TuiApp, event: Event) { + match event { + Event::Key(key) => match key { + Key::Char('1') => { + app.log_view_job = JobType::Fetch; + } + Key::Char('2') => { + app.log_view_job = JobType::Cook; + } + Key::Char('c') => { + // as compilers still running, we use this way to stop it + let pid = std::process::id(); + Command::new("pkill") + .arg("-9") + .arg("-P") + .arg(pid.to_string()) + .spawn() + .expect("unable to spawn pkill"); + } + Key::Up => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + Key::Down => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + Key::PageUp => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(20); + } + Key::PageDown => { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(20); + } + Key::End => { + app.auto_scroll = true; + } + Key::Home => { + app.auto_scroll = false; + app.log_scroll = 0; + } + _ => {} + }, + + Event::Mouse(mouse_event) => { + match mouse_event { + MouseEvent::Press(termion::event::MouseButton::WheelUp, x, y) => { + // termion is 1-based, ratatui rects are 0-based + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_sub(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_sub(1); + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_sub(1); + } + } + MouseEvent::Press(termion::event::MouseButton::WheelDown, x, y) => { + let pos = Position { + x: x.saturating_sub(1), + y: y.saturating_sub(1), + }; + + if app.fetch_panel_rect.map_or(false, |r| r.contains(pos)) { + app.fetch_scroll = app.fetch_scroll.saturating_add(1); + } else if app.cook_panel_rect.map_or(false, |r| r.contains(pos)) { + app.cook_scroll = app.cook_scroll.saturating_add(1); + } else if app.log_panel_rect.map_or(false, |r| r.contains(pos)) { + app.auto_scroll = false; + app.log_scroll = app.log_scroll.saturating_add(1); + } + } + _ => {} + } + } + _ => {} + } +} + +fn handle_prompt_input( + event: Event, + app: &mut TuiApp, + running: &Arc, +) -> Option { + if let Some(prompt) = &mut app.prompt { + match event { + Event::Key(key) => match key { + Key::Char('q') | Key::Ctrl('c') | Key::Esc => { + // Treat as "Exit" + running.store(false, Ordering::SeqCst); + return Some(PromptOption::Exit); + } + Key::Left | Key::BackTab => prompt.prev(), + Key::Right | Key::Char('\t') => prompt.next(), + Key::Char('\n') => { + let prompt = app.prompt.take().unwrap(); + if prompt.selected == PromptOption::Exit { + running.store(false, Ordering::SeqCst); + } + return Some(prompt.selected); + } + _ => {} + }, + _ => {} // Ignore mouse events + } + } + None +} + +fn draw_prompt(f: &mut ratatui::Frame, prompt: &FailurePrompt) { + let title = format!(" FAILURE in {} ", prompt.recipe.name); + let mut error_text = prompt.error.clone(); + if error_text.len() > 100 { + error_text = error_text[0..100].to_string() + ".."; + } + + // Style for options + let retry_style = if prompt.selected == PromptOption::Retry { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + let skip_style = if prompt.selected == PromptOption::Skip { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + let exit_style = if prompt.selected == PromptOption::Exit { + Style::default().bg(Color::White).fg(Color::Black) + } else { + Style::default() + }; + + let text = vec![ + Line::from(error_text).style(Style::default().fg(Color::Yellow)), + Line::from(""), + Line::from(vec![ + Span::styled(" [Retry] ", retry_style), + Span::raw(" "), + Span::styled(" [Skip] ", skip_style), + Span::raw(" "), + Span::styled(" [Exit] ", exit_style), + ]), + ]; + + let block = Block::default() + .title(Span::styled( + title, + Style::default().fg(Color::White).bg(Color::Red), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)); + + let paragraph = Paragraph::new(text) + .block(block) + .alignment(ratatui::layout::Alignment::Center) + .wrap(Wrap { trim: true }); + + let area = f.area(); + let popup_area = Rect { + x: area.width / 4, + y: area.height / 3, + width: area.width / 2, + height: 10, + }; + + f.render_widget(Clear, popup_area); // Clear the background + f.render_widget(paragraph, popup_area); } fn spawn_log_reader( @@ -778,7 +1005,7 @@ fn spawn_log_reader( for line in reader.lines() { let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); if status_tx - .send(StatusUpdate::CookLog(package_name.clone(), line_str)) + .send(StatusUpdate::PushLog(package_name.clone(), line_str)) .is_err() { // TUI thread hung up @@ -789,12 +1016,52 @@ fn spawn_log_reader( } fn setup_logger( - cooker_status_tx: &mpsc::Sender, + status_tx: &mpsc::Sender, name: &PackageName, ) -> (std::io::PipeWriter, std::io::PipeWriter) { let (stdout_reader, stdout_writer) = std::io::pipe().expect("Failed to create stdout pipe"); let (stderr_reader, stderr_writer) = std::io::pipe().expect("Failed to create stderr pipe"); - spawn_log_reader(stdout_reader, name.clone(), cooker_status_tx.clone()); - spawn_log_reader(stderr_reader, name.clone(), cooker_status_tx.clone()); + spawn_log_reader(stdout_reader, name.clone(), status_tx.clone()); + spawn_log_reader(stderr_reader, name.clone(), status_tx.clone()); (stdout_writer, stderr_writer) } + +#[derive(PartialEq, Clone, Copy)] +#[repr(u32)] +enum PromptOption { + Retry = 2, + Skip, + Exit, +} + +struct FailurePrompt { + recipe: CookRecipe, + error: String, + selected: PromptOption, +} + +impl FailurePrompt { + fn new(recipe: CookRecipe, error: String) -> Self { + Self { + recipe, + error, + selected: PromptOption::Exit, + } + } + + fn next(&mut self) { + self.selected = match self.selected { + PromptOption::Retry => PromptOption::Skip, + PromptOption::Skip => PromptOption::Exit, + PromptOption::Exit => PromptOption::Retry, + } + } + + fn prev(&mut self) { + self.selected = match self.selected { + PromptOption::Retry => PromptOption::Exit, + PromptOption::Skip => PromptOption::Retry, + PromptOption::Exit => PromptOption::Skip, + } + } +} diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index 016e840f0..a9c921644 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -21,9 +21,25 @@ use crate::is_redox; use crate::REMOTE_PKG_SOURCE; +macro_rules! log_warn { + ($logger:expr, $($arg:tt)+) => { + use std::io::Write; + + if $logger.is_some() { + let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write( + format!($($arg)+) + .as_bytes(), + ); + } else { + eprintln!($($arg)+); + } + }; +} + fn auto_deps( stage_dir: &Path, dep_pkgars: &BTreeSet<(PackageName, PathBuf)>, + logger: &Stdout, ) -> BTreeSet { let mut paths = BTreeSet::new(); let mut visited = BTreeSet::new(); @@ -43,7 +59,10 @@ fn auto_deps( }; if visited.contains(&dir) { #[cfg(debug_assertions)] - eprintln!("DEBUG: auto_deps => Skipping `{dir:?}` (already visited)"); + log_warn!( + logger, + "DEBUG: auto_deps => Skipping `{dir:?}` (already visited)" + ); continue; } assert!( @@ -90,7 +109,7 @@ fn auto_deps( continue; }; if let Ok(relative_path) = path.strip_prefix(stage_dir) { - eprintln!("DEBUG: {} needs {}", relative_path.display(), name); + log_warn!(logger, "DEBUG: {} needs {}", relative_path.display(), name); } needed.insert(name.to_string()); } @@ -124,7 +143,7 @@ fn auto_deps( continue; }; if needed.contains(child_name) { - eprintln!("DEBUG: {} provides {}", dep, child_name); + log_warn!(logger, "DEBUG: {} provides {}", dep, child_name); deps.insert(dep.clone()); missing.remove(child_name); } @@ -134,7 +153,7 @@ fn auto_deps( } for name in missing { - eprintln!("WARN: {} missing", name); + log_warn!(logger, "WARN: {} missing", name); } deps @@ -174,7 +193,7 @@ pub fn build( } if stage_dir.exists() && !check_source { - let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars)?; + let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars, logger)?; return Ok((stage_dir, auto_deps)); } @@ -190,7 +209,8 @@ pub fn build( if sysroot_dir.is_dir() { let sysroot_modified = modified_dir(&sysroot_dir)?; if sysroot_modified < source_modified || sysroot_modified < deps_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), sysroot_dir.display() @@ -239,7 +259,8 @@ pub fn build( if stage_dir.is_dir() { let stage_modified = modified_dir(&stage_dir)?; if stage_modified < source_modified || stage_modified < deps_modified { - eprintln!( + log_warn!( + logger, "DEBUG: '{}' newer than '{}'", source_dir.display(), stage_dir.display() @@ -318,7 +339,7 @@ pub fn build( } else { let cookbook_redoxer = Path::new("target/release/cookbook_redoxer") .canonicalize() - .unwrap(); + .unwrap_or(PathBuf::from("/bin/false")); let mut command = Command::new(&cookbook_redoxer); command.arg("env").arg("bash").arg("-ex"); command.env("COOKBOOK_REDOXER", &cookbook_redoxer); @@ -348,7 +369,7 @@ pub fn build( rename(&stage_dir_tmp, &stage_dir)?; } - let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars)?; + let auto_deps = build_auto_deps(target_dir, &stage_dir, dep_pkgars, logger)?; Ok((stage_dir, auto_deps)) } @@ -358,6 +379,7 @@ fn build_auto_deps( target_dir: &Path, stage_dir: &PathBuf, dep_pkgars: BTreeSet<(PackageName, PathBuf)>, + logger: &Stdout, ) -> Result, String> { let auto_deps_path = target_dir.join("auto_deps.toml"); if auto_deps_path.is_file() && modified(&auto_deps_path)? < modified(stage_dir)? { @@ -371,7 +393,7 @@ fn build_auto_deps( toml::from_str(&toml_content).map_err(|_| "failed to deserialize cached auto_deps")?; wrapper.packages } else { - let packages = auto_deps(stage_dir, &dep_pkgars); + let packages = auto_deps(stage_dir, &dep_pkgars, logger); let wrapper = AutoDeps { packages }; serialize_and_write(&auto_deps_path, &wrapper)?; wrapper.packages @@ -475,7 +497,7 @@ mod tests { "Expected a loop where {dir:?} points to {root:?}" ); - let entries = auto_deps(root, &Default::default()); + let entries = auto_deps(root, &Default::default(), &None); assert!( entries.is_empty(), "auto_deps shouldn't have yielded any libraries" diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 60df13143..6295cae74 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -76,7 +76,7 @@ pub fn fetch_offline( }) => { if !source_dir.is_dir() { let source_tar = recipe_dir.join("source.tar"); - let source_tar_blake3 = get_blake3(&source_tar, true)?; + let source_tar_blake3 = get_blake3(&source_tar, true && logger.is_none())?; if source_tar.exists() { if let Some(blake3) = blake3 { if source_tar_blake3 != *blake3 { @@ -257,7 +257,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result