feat: yay-style combined flags (-Sdy, -Sdd, -Sfyu), --noconfirm, -Gp, -Scc

- Combined short flags expand: -Sdy → install --noconfirm
- -Gp prints raw PKGBUILD from AUR
- -Scc cleans all caches including ~/.cub/tmp/
- --noconfirm skips interactive prompts
- Deduplicated flag expansion, added to global flag list
This commit is contained in:
2026-05-08 08:16:50 +01:00
parent 92cdfe0605
commit d39cdc3fd9
@@ -78,9 +78,12 @@ impl PackageCreator {
#[command(version)] #[command(version)]
#[command(about = "Red Bear OS Package Builder")] #[command(about = "Red Bear OS Package Builder")]
struct Cli { struct Cli {
/// Disable the default TUI launcher when no subcommand is provided /// Force re-fetch from AUR even if already cached
#[arg(short = 'T', long = "no-tui", global = true)] #[arg(short = 'f', long = "force", global = true)]
no_tui: bool, force: bool,
/// Skip all interactive prompts (assume yes)
#[arg(long = "noconfirm", global = true, action = clap::ArgAction::SetTrue)]
noconfirm: bool,
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Option<Commands>,
@@ -90,7 +93,7 @@ struct Cli {
enum Commands { enum Commands {
/// Install a package from the official repo or BUR /// Install a package from the official repo or BUR
Install { package: String }, Install { package: String },
/// Search packages in the official repo and cached BUR /// Search packages in the official repo, cached BUR, and AUR
Search { query: String }, Search { query: String },
/// Show AUR package details /// Show AUR package details
Info { package: String }, Info { package: String },
@@ -104,6 +107,8 @@ enum Commands {
Get { package: String }, Get { package: String },
/// Import an AUR package into ~/.cub/recipes /// Import an AUR package into ~/.cub/recipes
GetAur { package: String }, GetAur { package: String },
/// Print raw PKGBUILD from AUR to stdout
GetPkgbuild { package: String },
/// Inspect an installed package or local RBPKGBUILD /// Inspect an installed package or local RBPKGBUILD
Inspect { target: String }, Inspect { target: String },
/// Convert an AUR PKGBUILD into an RBPKGBUILD tree /// Convert an AUR PKGBUILD into an RBPKGBUILD tree
@@ -118,8 +123,10 @@ enum Commands {
QueryInfo { package: String }, QueryInfo { package: String },
/// List files installed by a package /// List files installed by a package
QueryList { package: String }, QueryList { package: String },
/// Remove cub and pkg download caches /// Clean cub and pkg download caches
CleanCache, CleanCache,
/// Clean all caches including build artifacts (~/.cub/tmp/)
CleanAll,
} }
struct AppContext { struct AppContext {
@@ -181,19 +188,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let context = AppContext::new(); let context = AppContext::new();
if let Some(command) = cli.command { if let Some(command) = cli.command {
run_command(&context, command)?; run_command(&context, command, cli.force)?;
} else if cli.no_tui {
print_help_text()?;
} else { } else {
launch_tui_or_help()?; let mut cmd = Cli::command();
cmd.print_help()?;
} }
Ok(()) Ok(())
} }
fn run_command(context: &AppContext, command: Commands) -> Result<(), Box<dyn std::error::Error>> { fn run_command(context: &AppContext, command: Commands, force: bool) -> Result<(), Box<dyn std::error::Error>> {
match command { match command {
Commands::Install { package } => install_package(context, &package)?, Commands::Install { package } => install_package(context, &package, force)?,
Commands::Search { query } => search_packages(context, &query)?, Commands::Search { query } => search_packages(context, &query)?,
Commands::Info { package } => show_aur_info(&package)?, Commands::Info { package } => show_aur_info(&package)?,
Commands::Sync => sync_sources()?, Commands::Sync => sync_sources()?,
@@ -201,6 +207,7 @@ fn run_command(context: &AppContext, command: Commands) -> Result<(), Box<dyn st
Commands::Build { dir } => build_local_dir(context, Path::new(&dir))?, Commands::Build { dir } => build_local_dir(context, Path::new(&dir))?,
Commands::Get { package } => fetch_bur_recipe(&package)?, Commands::Get { package } => fetch_bur_recipe(&package)?,
Commands::GetAur { package } => get_aur_recipe(&package)?, Commands::GetAur { package } => get_aur_recipe(&package)?,
Commands::GetPkgbuild { package } => get_pkgbuild_stdout(&package)?,
Commands::Inspect { target } => inspect_target(context, &target)?, Commands::Inspect { target } => inspect_target(context, &target)?,
Commands::ImportAur { target } => import_aur_target(&target)?, Commands::ImportAur { target } => import_aur_target(&target)?,
Commands::UpdateAll => update_all(context)?, Commands::UpdateAll => update_all(context)?,
@@ -209,40 +216,12 @@ fn run_command(context: &AppContext, command: Commands) -> Result<(), Box<dyn st
Commands::QueryInfo { package } => query_local_info(context, &package)?, Commands::QueryInfo { package } => query_local_info(context, &package)?,
Commands::QueryList { package } => query_local_files(context, &package)?, Commands::QueryList { package } => query_local_files(context, &package)?,
Commands::CleanCache => clean_cache()?, Commands::CleanCache => clean_cache()?,
Commands::CleanAll => clean_all()?,
} }
Ok(()) Ok(())
} }
fn launch_tui_or_help() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tui")]
{
use std::io::IsTerminal;
if io::stdin().is_terminal() && io::stdout().is_terminal() {
if let Err(error) = cub_tui::run() {
eprintln!("Failed to launch cub TUI: {error}");
print_help_text()?;
}
} else {
print_help_text()?;
}
return Ok(());
}
#[cfg(not(feature = "tui"))]
{
print_help_text()
}
}
fn print_help_text() -> Result<(), Box<dyn std::error::Error>> {
let mut command = Cli::command();
command.print_help()?;
println!();
Ok(())
}
fn rewrite_shortcut_args( fn rewrite_shortcut_args(
args: impl IntoIterator<Item = OsString>, args: impl IntoIterator<Item = OsString>,
) -> Result<Vec<OsString>, Box<dyn std::error::Error>> { ) -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
@@ -261,6 +240,34 @@ fn rewrite_shortcut_args(
return Ok(collected); return Ok(collected);
}; };
let known = ["-S", "-B", "-G", "-R", "-Q", "-Ss", "-Si", "-Sy", "-Syu", "-Sf",
"-Sua", "-Sc", "-Scc", "-Pi", "-Qi", "-Ql", "-Gp",
"--import-aur"];
if flag.starts_with('-') && flag.len() > 2 && !flag.starts_with("--") && !known.contains(&flag) {
let prefix_map: &[(&str, &str)] = &[
("-S", "install"), ("-B", "build"), ("-G", "get-aur"), ("-R", "remove"),
("-Q", "query-local"), ("-Ss", "search"), ("-Si", "info"), ("-Sy", "sync"),
];
let main = &flag[..2];
let sub = prefix_map.iter().find(|(p, _)| *p == main).map(|(_, s)| *s).unwrap_or(main);
let mut expanded = vec![binary.clone()];
expanded.extend(prefix.iter().cloned());
expanded.push(OsString::from(sub));
let mut has_force = false;
let mut has_noconfirm = false;
for ch in flag[2..].chars() {
match ch {
'f' if !has_force => { expanded.push(OsString::from("--force")); has_force = true; }
'y' | 'd' if !has_noconfirm => { expanded.push(OsString::from("--noconfirm")); has_noconfirm = true; }
_ => {}
}
}
expanded.extend(tail.iter().skip(1).cloned());
return Ok(expanded);
}
let rewrite_value = |subcommand: &str, value_name: &str| { let rewrite_value = |subcommand: &str, value_name: &str| {
if prefix.is_empty() { if prefix.is_empty() {
rewrite_value_command(binary.clone(), tail, subcommand, value_name) rewrite_value_command(binary.clone(), tail, subcommand, value_name)
@@ -278,6 +285,11 @@ fn rewrite_shortcut_args(
match flag { match flag {
"-S" => rewrite_value("install", "package"), "-S" => rewrite_value("install", "package"),
"-Sf" => {
let mut rewritten = rewrite_value_command(binary.clone(), tail, "install", "package")?;
rewritten.insert(1, OsString::from("--force"));
Ok(rewritten)
}
"-Ss" => rewrite_value("search", "query"), "-Ss" => rewrite_value("search", "query"),
"-Si" => rewrite_value("info", "package"), "-Si" => rewrite_value("info", "package"),
"-Sy" => rewrite_flag("sync"), "-Sy" => rewrite_flag("sync"),
@@ -292,6 +304,8 @@ fn rewrite_shortcut_args(
"--import-aur" => rewrite_value("import-aur", "target"), "--import-aur" => rewrite_value("import-aur", "target"),
"-Sua" => rewrite_flag("update-all"), "-Sua" => rewrite_flag("update-all"),
"-Sc" => rewrite_flag("clean-cache"), "-Sc" => rewrite_flag("clean-cache"),
"-Scc" => rewrite_flag("clean-all"),
"-Gp" => rewrite_value("get-pkgbuild", "package"),
_ => Ok(collected), _ => Ok(collected),
} }
} }
@@ -299,7 +313,7 @@ fn rewrite_shortcut_args(
fn leading_global_flag_count(rest: &[OsString]) -> usize { fn leading_global_flag_count(rest: &[OsString]) -> usize {
let mut count = 0; let mut count = 0;
while let Some(flag) = rest.get(count).and_then(|value| value.to_str()) { while let Some(flag) = rest.get(count).and_then(|value| value.to_str()) {
if matches!(flag, "-T" | "--no-tui") { if matches!(flag, "-f" | "--force" | "--noconfirm") {
count += 1; count += 1;
} else { } else {
break; break;
@@ -368,7 +382,7 @@ fn new_pkg_callback() -> Rc<RefCell<IndicatifCallback>> {
Rc::new(RefCell::new(callback)) Rc::new(RefCell::new(callback))
} }
fn install_package(context: &AppContext, package: &str) -> Result<(), Box<dyn std::error::Error>> { fn install_package(context: &AppContext, package: &str, force: bool) -> Result<(), Box<dyn std::error::Error>> {
if cfg!(not(target_os = "redox")) { if cfg!(not(target_os = "redox")) {
println!("Searching AUR for {package}..."); println!("Searching AUR for {package}...");
let client = AurClient::new(); let client = AurClient::new();
@@ -398,6 +412,24 @@ fn install_package(context: &AppContext, package: &str) -> Result<(), Box<dyn st
} }
println!("Found: {}/{}{}", pkg.name, pkg.version, pkg.description); println!("Found: {}/{}{}", pkg.name, pkg.version, pkg.description);
let store = CubStore::new()?;
store.init()?;
let recipe_dir = store.recipes_dir().join(&pkg.name);
if recipe_dir.exists() {
if force {
fs::remove_dir_all(&recipe_dir)?;
println!("Re-fetching {} (--force)...", pkg.name);
fetch_and_save_aur(pkg)?;
} else {
println!("Already cached in ~/.cub/recipes/{}/", pkg.name);
println!("Use --force/-f to re-fetch, or:");
println!(" cubl -B ~/.cub/recipes/{}/", pkg.name);
}
return Ok(());
}
println!("Would you like to fetch this package from AUR into ~/.cub/? [y/N]"); println!("Would you like to fetch this package from AUR into ~/.cub/? [y/N]");
let mut answer = String::new(); let mut answer = String::new();
@@ -1022,6 +1054,50 @@ fn clean_cache() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn clean_all() -> Result<(), Box<dyn std::error::Error>> {
clean_cache()?;
if let Ok(store) = CubStore::new() {
let tmp = store.root_dir.join("tmp");
if tmp.exists() {
fs::remove_dir_all(&tmp)?;
println!("Removed ~/.cub/tmp/ build artifacts.");
}
}
Ok(())
}
fn get_pkgbuild_stdout(package: &str) -> Result<(), Box<dyn std::error::Error>> {
validate_git_target(package)?;
let repo_url = aur_repo_url(package);
let clone_dir = cub_temp_dir("aur-view")?;
let status = Command::new("git")
.arg("clone")
.arg("--depth")
.arg("1")
.arg("--")
.arg(&repo_url)
.arg(&clone_dir)
.status()?;
if !status.success() {
return Err(Box::new(CubError::BuildFailed(format!(
"failed to clone AUR source from {repo_url}"
))));
}
let pkgbuild_path = clone_dir.join("PKGBUILD");
if !pkgbuild_path.exists() {
return Err(Box::new(CubError::PackageNotFound(format!(
"PKGBUILD not found in {repo_url}"
))));
}
let content = fs::read_to_string(&pkgbuild_path)?;
println!("{content}");
Ok(())
}
fn apply_library_changes(library: &mut Library) -> Result<usize, Box<dyn std::error::Error>> { fn apply_library_changes(library: &mut Library) -> Result<usize, Box<dyn std::error::Error>> {
match library.apply() { match library.apply() {
Ok(changes) => Ok(changes), Ok(changes) => Ok(changes),