Implement cook TUI

This commit is contained in:
Wildan M 2025-10-24 16:56:52 +07:00
parent 8e2ac316e4
commit ab57937dd4
3 changed files with 456 additions and 17 deletions

198
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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=<cookbook_dir> 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<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
if command == CliCommand::Cook
|| command == CliCommand::Fetch
|| command == CliCommand::Push
|| command == CliCommand::Tree
{
// because read_recipe is false below
// some recipes on wip folders are invalid anyway
@ -215,12 +241,7 @@ fn parse_args(args: Vec<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
Ok((config, command, recipes))
}
fn handle_cook(
recipe: &CookRecipe,
config: &CliConfig,
fetch_only: bool,
is_deps: bool,
) -> anyhow::Result<()> {
fn handle_fetch(recipe: &CookRecipe, config: &CliConfig) -> anyhow::Result<PathBuf> {
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<PackageName>,
cook_queue: Vec<PackageName>,
done: Vec<PackageName>,
failed: Vec<PackageName>,
}
impl TuiApp {
fn new(recipes: Vec<CookRecipe>) -> 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<CookRecipe>) -> anyhow::Result<()> {
let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>();
let (status_tx, status_rx) = mpsc::channel::<StatusUpdate>();
// ---- 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<ListItem> = 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<ListItem> = 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(())
}