diff --git a/home/apps/vscodium.nix b/home/apps/vscodium.nix --- a/home/apps/vscodium.nix +++ b/home/apps/vscodium.nix @@ -34,14 +34,16 @@ }; mutableExtensionsDir = false; profiles.default = { - extensions = with pkgs.vscode-marketplace; [ - jjk.jjk - jeronimoekerdt.color-picker-universal - jnoortheen.nix-ide - pkief.material-icon-theme - redhat.vscode-yaml - tamasfe.even-better-toml - ]; + extensions = with pkgs.vscode-marketplace; + [ + jeronimoekerdt.color-picker-universal + jnoortheen.nix-ide + pkief.material-icon-theme + redhat.vscode-yaml + tamasfe.even-better-toml + ] + ++ lib.optional config.modules.terminal.gitIdentity.enable jjk.jjk + ++ lib.optional config.modules.terminal.cargo.enable rust-lang.rust-analyzer; userSettings = { accessibility.verbosity.walkthrough = false; diff --git a/home/console/cargo.nix b/home/console/cargo.nix --- a/home/console/cargo.nix +++ b/home/console/cargo.nix @@ -20,7 +20,12 @@ config = lib.mkIf config.modules.terminal.cargo.enable { home.packages = with pkgs; - [pkg-config gcc] ++ lib.optional config.modules.terminal.cargo.withAnalyzer rust-analyzer; + [ + rustfmt + pkg-config + gcc + ] + ++ lib.optional config.modules.terminal.cargo.withAnalyzer rust-analyzer; programs.cargo = { enable = true; diff --git a/pkgs/flake-runner/src/main.rs b/pkgs/flake-runner/src/main.rs --- a/pkgs/flake-runner/src/main.rs +++ b/pkgs/flake-runner/src/main.rs @@ -1,12 +1,22 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; use anyhow::{bail, Context, Result}; use clap::Parser; use indicatif::{ProgressBar, ProgressStyle}; +const SUBDIVISIONS: u64 = 100; +const MIN_STEP_DURATION: Duration = Duration::from_millis(450); +const ANIMATE_DURATION: Duration = Duration::from_millis(350); +const ANIMATE_FRAMES: u32 = 30; + #[derive(Parser)] -#[command(name = "flake-runner", about = "Pull/update/format/rebuild pipeline for a NixOS flake repo")] +#[command( + name = "flake-runner", + about = "Pull/update/format/rebuild pipeline for a NixOS flake repo" +)] struct Args { /// jj git fetch, hard gate on unresolved conflicts #[arg(short, long)] @@ -37,58 +47,100 @@ pedantix: bool, } -struct Stage { - name: &'static str, +struct Stage<'a> { + name: &'a str, run: fn(&Args, &ProgressBar) -> Result<()>, } fn main() -> Result<()> { let args = Args::parse(); + let mut stages: Vec = Vec::new(); if args.pull { - stages.push(Stage { name: "pull", run: stage_pull }); + stages.push(Stage { + name: "pull", + run: stage_pull, + }); } if args.update { - stages.push(Stage { name: "update", run: stage_update }); + stages.push(Stage { + name: "update", + run: stage_update, + }); } if args.format { - stages.push(Stage { name: "format", run: stage_format }); + stages.push(Stage { + name: "format", + run: stage_format, + }); } if args.rebuild { - stages.push(Stage { name: "rebuild", run: stage_rebuild }); + stages.push(Stage { + name: "rebuild", + run: stage_rebuild, + }); } if stages.is_empty() { println!("⚠️ No stage flags given — nothing to do. Try --help."); return Ok(()); } + let total = stages.len(); - let pb = ProgressBar::new(stages.len() as u64); + let pb = ProgressBar::new((total as u64) * SUBDIVISIONS); pb.set_style( - ProgressStyle::with_template("{prefix:.bold} [{bar:30.cyan/black}] {pos}/{len} {msg}") + ProgressStyle::with_template("{spinner:.cyan.bold} [{bar:40.cyan/black}] {msg}") .unwrap() - .progress_chars("█░░"), + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", "✔"]) + .progress_chars("█░"), ); - pb.set_prefix("flake-runner"); - for stage in &stages { - pb.set_message(format!("{}...", stage.name)); - (stage.run)(&args, &pb).with_context(|| format!("stage '{}' failed", stage.name))?; - pb.inc(1); + pb.enable_steady_tick(Duration::from_millis(80)); + + let mut current_units: u64 = 0; + + for (idx, stage) in stages.iter().enumerate() { + pb.set_message(format!("{} ({}/{total})", stage.name, idx + 1)); + + let start = Instant::now(); + if let Err(e) = (stage.run)(&args, &pb) { + pb.abandon_with_message(format!("❌ {} failed", stage.name)); + return Err(e); + } + + let elapsed = start.elapsed(); + if elapsed < MIN_STEP_DURATION { + thread::sleep(MIN_STEP_DURATION - elapsed); + } + + let target = ((idx + 1) as u64) * SUBDIVISIONS; + animate_to(&pb, &mut current_units, target, ANIMATE_DURATION); } - pb.finish_with_message("done ✅"); + pb.finish_with_message("✅ done"); Ok(()) } -/// Runs a command with inherited stdio, so its own output prints normally. -/// `pb.suspend` hides the bar for the duration so subprocess writes don't -/// get interleaved with bar redraws, then redraws the (now-advanced) bar -/// pinned at the bottom once the closure returns. -fn run_command(pb: &ProgressBar, program: &str, args: &[&str], cwd: &Path) -> Result<()> { +fn animate_to(pb: &ProgressBar, current: &mut u64, target: u64, duration: Duration) { + let start_val = *current as f64; + let delta = target as f64 - start_val; + let frame_time = duration / ANIMATE_FRAMES; + + for i in 1..=ANIMATE_FRAMES { + let t = i as f64 / ANIMATE_FRAMES as f64; + let eased = 1.0 - (1.0 - t).powi(3); + pb.set_position((start_val + delta * eased).round() as u64); + thread::sleep(frame_time); + } + + *current = target; + pb.set_position(target); +} + +fn run_command(pb: &ProgressBar, program: &str, cmd_args: &[&str], cwd: &Path) -> Result<()> { pb.suspend(|| { let status = Command::new(program) - .args(args) + .args(cmd_args) .current_dir(cwd) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) @@ -97,40 +149,52 @@ .with_context(|| format!("failed to spawn `{program}`"))?; if !status.success() { - bail!("`{program} {}` exited with {status}", args.join(" ")); + bail!("`{program} {}` exited with {status}", cmd_args.join(" ")); } Ok(()) }) } -/// Runs a command and captures stdout, for cases where we need to parse -/// the output (jj conflict check, generation number) rather than stream it. -fn run_command_capture(program: &str, args: &[&str], cwd: &Path) -> Result { +fn run_command_capture(program: &str, cmd_args: &[&str], cwd: &Path) -> Result { let output = Command::new(program) - .args(args) + .args(cmd_args) .current_dir(cwd) .output() .with_context(|| format!("failed to spawn `{program}`"))?; if !output.status.success() { - bail!("`{program} {}` exited with {}", args.join(" "), output.status); + bail!( + "`{program} {}` exited with {}", + cmd_args.join(" "), + output.status + ); } Ok(String::from_utf8_lossy(&output.stdout).to_string()) } fn stage_pull(args: &Args, pb: &ProgressBar) -> Result<()> { let repo = args.repo.to_str().context("repo path is not valid UTF-8")?; - run_command(pb, "jj", &["-R", repo, "git", "fetch"], &args.repo)?; let conflicted = run_command_capture( "jj", - &["-R", repo, "log", "-r", "conflicts()", "--no-graph", "-T", "change_id ++ \"\\n\""], + &[ + "-R", + repo, + "log", + "-r", + "conflicts()", + "--no-graph", + "-T", + "change_id ++ \"\\n\"", + ], &args.repo, )?; - if !conflicted.trim().is_empty() { - bail!("jj fetch produced conflicts, resolve before continuing:\n{}", conflicted.trim()); + bail!( + "jj fetch produced conflicts, resolve before continuing:\n{}", + conflicted.trim() + ); } Ok(()) } @@ -163,7 +227,16 @@ run_command( pb, "nh", - &["os", "switch", repo, "-H", &args.host, "--quiet", "--elevation-strategy", "sudo"], + &[ + "os", + "switch", + repo, + "-H", + &args.host, + "--quiet", + "--elevation-strategy", + "sudo", + ], &args.repo, )?; @@ -171,7 +244,7 @@ let latest = generations .lines() .nth(1) - .and_then(|line| line.split_whitespace().next()) + .and_then(|l| l.split_whitespace().next()) .unwrap_or("unknown"); pb.println(format!("✅ Successfully built Generation {latest}"));