From ab57937dd419d2cea36138c1e4485086c46e4d83 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 16:56:52 +0700 Subject: [PATCH] Implement cook TUI --- Cargo.lock | 198 ++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/bin/repo.rs | 272 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 456 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d6b149a..58737f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -374,6 +380,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.29" @@ -446,7 +467,7 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width 0.1.14", "vec_map", @@ -458,6 +479,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if 1.0.1", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.11" @@ -467,7 +502,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -602,6 +637,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.4.0" @@ -974,9 +1044,17 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1211,6 +1289,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1267,10 +1351,19 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "web-time", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -1280,6 +1373,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "io-uring" version = "0.7.8" @@ -1379,6 +1485,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1500,6 +1615,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbr" version = "1.1.1" @@ -1804,6 +1925,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384c2842d4e069d5ccacf5fe1dca4ef8d07a5444329715f0fc3c61813502d4d1" +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "termion", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1871,6 +2013,7 @@ dependencies = [ "pkgar 0.1.19", "pkgar-core 0.1.19", "pkgar-keys 0.1.19", + "ratatui", "redox-pkg", "redoxer", "regex", @@ -2357,6 +2500,34 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2724,6 +2895,23 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -2732,9 +2920,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" diff --git a/Cargo.toml b/Cargo.toml index d82ada6b..17b6d972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ serde = { version = "=1.0.197", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" +ratatui = { version = "0.29.0", default-features = false, features = [ + "termion", +] } [dev-dependencies] tempfile = "3" diff --git a/src/bin/repo.rs b/src/bin/repo.rs index cab4ddcd..1faa76d8 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,7 +1,10 @@ +use std::io::stdout; use std::path::PathBuf; -use std::process; use std::str::FromStr; +use std::sync::mpsc; +use std::time::Duration; use std::{env, fs}; +use std::{process, thread}; use anyhow::{Context, anyhow, bail}; use cookbook::WALK_DEPTH; @@ -13,6 +16,12 @@ use cookbook::cook::package::package; use cookbook::recipe::CookRecipe; use pkg::PackageName; use pkg::package::PackageError; +use ratatui::Terminal; +use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::prelude::TermionBackend; +use ratatui::style::{Color, Style}; +use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use termion::screen::{ToAlternateScreen, ToMainScreen}; // A repo manager, to replace repo.sh @@ -25,6 +34,7 @@ const REPO_HELP_STR: &str = r#" unfetch delete recipe sources clean delete recipe artifacts push extract package into sysroot + tree show tree of recipe packages common flags: --cookbook= the "recipes" folder, default to $PWD/recipes @@ -40,6 +50,7 @@ const REPO_HELP_STR: &str = r#" -q, --quiet surpress build logs unless error "#; +#[derive(Clone)] struct CliConfig { cookbook_dir: PathBuf, repo_dir: PathBuf, @@ -56,6 +67,7 @@ enum CliCommand { Unfetch, Clean, Push, + Tree, } impl FromStr for CliCommand { @@ -68,6 +80,7 @@ impl FromStr for CliCommand { "unfetch" => Ok(CliCommand::Unfetch), "clean" => Ok(CliCommand::Clean), "push" => Ok(CliCommand::Push), + "tree" => Ok(CliCommand::Tree), _ => Err(anyhow!("Unknown command '{}'", s)), } } @@ -81,6 +94,7 @@ impl ToString for CliCommand { CliCommand::Unfetch => "unfetch".to_string(), CliCommand::Clean => "clean".to_string(), CliCommand::Push => "push".to_string(), + CliCommand::Tree => "tree".to_string(), } } } @@ -119,13 +133,24 @@ 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)?; + return Ok(()); + } + for recipe in &recipe_names { match command { - CliCommand::Fetch => handle_cook(recipe, &config, true, recipe.is_deps)?, - CliCommand::Cook => handle_cook(recipe, &config, false, recipe.is_deps)?, + CliCommand::Fetch => { + handle_fetch(recipe, &config)?; + } + CliCommand::Cook => { + let source_dir = handle_fetch(recipe, &config)?; + handle_cook(recipe, &config, source_dir, recipe.is_deps)? + } CliCommand::Unfetch => handle_clean(recipe, &config, true, true)?, CliCommand::Clean => handle_clean(recipe, &config, false, true)?, CliCommand::Push => handle_push(recipe, &config)?, + CliCommand::Tree => todo!("tree command is WIP"), } } @@ -187,6 +212,7 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result<()> { +fn handle_fetch(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result { let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline { true => fetch_offline(recipe_dir, &recipe.recipe), @@ -228,10 +249,16 @@ fn handle_cook( } .map_err(|e| anyhow!(e))?; - if fetch_only { - return Ok(()); - } + Ok(source_dir) +} +fn handle_cook( + recipe: &CookRecipe, + config: &CliConfig, + source_dir: PathBuf, + is_deps: bool, +) -> anyhow::Result<()> { + let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; let (stage_dir, auto_deps) = build( @@ -288,3 +315,224 @@ fn handle_push(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<()> { config.sysroot_dir.display(), )) } + +#[derive(Debug, Clone, PartialEq)] +enum RecipeStatus { + Pending, + Fetching, + Fetched, + Cooking, + Done, + Failed(String), +} + +#[derive(Debug, Clone)] +enum StatusUpdate { + StartFetch(PackageName), + Fetched(PackageName), + FailFetch(PackageName, String), + StartCook(PackageName), + Cooked(PackageName), + FailCook(PackageName, String), +} + +struct TuiApp { + recipes: Vec<(CookRecipe, RecipeStatus)>, + fetch_queue: Vec, + cook_queue: Vec, + done: Vec, + failed: Vec, +} + +impl TuiApp { + fn new(recipes: Vec) -> Self { + let recipe_names = recipes.iter().map(|r| r.name.clone()).collect(); + Self { + recipes: recipes + .into_iter() + .map(|r| (r, RecipeStatus::Pending)) + .collect(), + fetch_queue: recipe_names, + cook_queue: Vec::new(), + done: Vec::new(), + failed: Vec::new(), + } + } + + // Update the state based on a message from a worker thread + fn update_status(&mut self, update: StatusUpdate) { + let (name, new_status) = match update { + StatusUpdate::StartFetch(name) => (name, RecipeStatus::Fetching), + StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), + StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), + StatusUpdate::StartCook(name) => (name, RecipeStatus::Cooking), + StatusUpdate::Cooked(name) => (name, RecipeStatus::Done), + StatusUpdate::FailCook(name, err) => (name, RecipeStatus::Failed(err)), + }; + + if let Some((_, status)) = self.recipes.iter_mut().find(|(r, _)| r.name == name) { + *status = new_status; + } + + // Re-compute the queues for display + self.fetch_queue = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) + .map(|(r, _)| r.name.clone()) + .collect(); + self.cook_queue = self + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Fetched || *s == RecipeStatus::Cooking) + .map(|(r, _)| r.name.clone()) + .collect(); + self.done = self + .recipes + .iter() + .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<()> { + let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); + let (status_tx, status_rx) = mpsc::channel::(); + + // ---- Cooker Thread ---- + let cooker_config = config.clone(); + let cooker_status_tx = status_tx.clone(); + let cooker_handle = thread::spawn(move || { + for (recipe, source_dir) in work_rx { + let name = recipe.name.clone(); + let is_deps = recipe.is_deps; + cooker_status_tx + .send(StatusUpdate::StartCook(name.clone())) + .unwrap(); + + match handle_cook(&recipe, &cooker_config, source_dir, is_deps) { + Ok(_) => cooker_status_tx.send(StatusUpdate::Cooked(name)).unwrap(), + Err(e) => cooker_status_tx + .send(StatusUpdate::FailCook(name, e.to_string())) + .unwrap(), + } + } + }); + + // ---- Fetcher Thread ---- + let fetcher_config = config.clone(); + let fetcher_handle = thread::spawn(move || { + for recipe in recipes { + let name = recipe.name.clone(); + status_tx + .send(StatusUpdate::StartFetch(name.clone())) + .unwrap(); + + match handle_fetch(&recipe, &fetcher_config) { + Ok(source_dir) => { + status_tx.send(StatusUpdate::Fetched(name)).unwrap(); + if work_tx.send((recipe, source_dir)).is_err() { + // Cooker thread died + break; + } + } + Err(e) => status_tx + .send(StatusUpdate::FailFetch(name, e.to_string())) + .unwrap(), + } + } + }); + + print!("{}", ToAlternateScreen); + // enable_raw_mode()?; + let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; + terminal.clear()?; + + let mut app = TuiApp::new(Vec::new()); + let total_recipes = app.recipes.len(); + let mut running = true; + + while running { + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.area()); + + // Left Pane + let fetch_items: Vec = app + .recipes + .iter() + .filter(|(_, s)| *s == RecipeStatus::Pending || *s == RecipeStatus::Fetching) + .map(|(r, s)| { + let style = if *s == RecipeStatus::Fetching { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + ListItem::new(r.name.as_str()).style(style) + }) + .collect(); + let fetch_list = List::new(fetch_items) + .block(Block::default().title("Fetch Queue").borders(Borders::ALL)); + f.render_widget(fetch_list, chunks[0]); + + // Right Pane + let cook_items: Vec = app + .recipes + .iter() + .filter(|(_, s)| { + *s == RecipeStatus::Fetched + || *s == RecipeStatus::Cooking + || *s == RecipeStatus::Done + || matches!(s, RecipeStatus::Failed(_)) + }) + .map(|(r, s)| { + let style = match s { + RecipeStatus::Fetched => Style::default().fg(Color::Cyan), + RecipeStatus::Cooking => Style::default().fg(Color::Yellow), + RecipeStatus::Done => Style::default().fg(Color::Green), + RecipeStatus::Failed(_) => Style::default().fg(Color::Red), + _ => Style::default(), + }; + ListItem::new(r.name.as_str()).style(style) + }) + .collect(); + let cook_list = List::new(cook_items) + .block(Block::default().title("Cook Queue").borders(Borders::ALL)); + f.render_widget(cook_list, chunks[1]); + + let footer = Paragraph::new(format!( + "Done: {}/{} | Failed: {}", + app.done.len(), + total_recipes, + app.failed.len() + )); + f.render_widget(footer, f.area()); + })?; + + while let Ok(update) = status_rx.try_recv() { + app.update_status(update); + } + + if fetcher_handle.is_finished() && cooker_handle.is_finished() { + thread::sleep(Duration::from_secs(5)); + running = false; + } + } + + // disable_raw_mode()?; + print!("{}", ToMainScreen); + + fetcher_handle.join().unwrap(); + cooker_handle.join().unwrap(); + + Ok(()) +}