From 3df12836396a33201917ce8300118bcd9a1aedde Mon Sep 17 00:00:00 2001 From: Nathan Whitaker Date: Wed, 22 Jan 2025 20:11:37 -0800 Subject: [PATCH] outline interactive update --- Cargo.lock | 57 +++- cli/Cargo.toml | 1 + cli/tools/registry/pm/outdated.rs | 2 + cli/tools/registry/pm/outdated/interactive.rs | 274 ++++++++++++++++++ 4 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 cli/tools/registry/pm/outdated/interactive.rs diff --git a/Cargo.lock b/Cargo.lock index 311a7ac885968f..d91b30d9d6e27c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,6 +1092,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio 1.0.3", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1262,6 +1287,7 @@ dependencies = [ "clap_complete_fig", "color-print", "console_static_text", + "crossterm", "dashmap", "data-encoding", "deno_ast", @@ -5112,6 +5138,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "moka" version = "0.12.10" @@ -5292,7 +5330,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -6643,9 +6681,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.6.0", "errno", @@ -7083,6 +7121,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 1.0.3", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -8152,7 +8201,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 0.8.11", "num_cpus", "parking_lot", "pin-project-lite", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a9b0b64577fc58..09ee4f4b37240a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -105,6 +105,7 @@ clap_complete = "=4.5.24" clap_complete_fig = "=4.5.2" color-print.workspace = true console_static_text.workspace = true +crossterm = "0.28.1" dashmap.workspace = true data-encoding.workspace = true dhat = { version = "0.3.3", optional = true } diff --git a/cli/tools/registry/pm/outdated.rs b/cli/tools/registry/pm/outdated.rs index 610ad48c1d6a20..38c88f0bc99811 100644 --- a/cli/tools/registry/pm/outdated.rs +++ b/cli/tools/registry/pm/outdated.rs @@ -1,5 +1,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. +mod interactive; + use std::collections::HashSet; use std::sync::Arc; diff --git a/cli/tools/registry/pm/outdated/interactive.rs b/cli/tools/registry/pm/outdated/interactive.rs new file mode 100644 index 00000000000000..70d677449ea43d --- /dev/null +++ b/cli/tools/registry/pm/outdated/interactive.rs @@ -0,0 +1,274 @@ +use std::collections::HashSet; +use std::io; +use std::io::Write; + +use crossterm::cursor; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; +use crossterm::style; +use crossterm::style::Stylize; +use crossterm::terminal; +use crossterm::ExecutableCommand; +use crossterm::QueueableCommand; + +use super::super::deps::DepLocation; +use super::OutdatedPackage; + +#[derive(Debug)] +struct PackageInfo { + location: DepLocation, + package: OutdatedPackage, +} + +#[derive(Debug)] +struct State { + packages: Vec, + currently_selected: usize, + checked: HashSet, + + name_width: usize, + current_width: usize, +} + +impl State { + fn new(packages: Vec) -> Self { + let name_width = packages + .iter() + .map(|p| p.package.name.len()) + .max() + .unwrap_or_default(); + let current_width = packages + .iter() + .map(|p| p.package.current.len()) + .max() + .unwrap_or_default(); + + let mut packages = packages; + packages + .sort_by(|a, b| a.location.file_path().cmp(&b.location.file_path())); + + Self { + packages, + currently_selected: 0, + checked: HashSet::new(), + + name_width, + current_width, + } + } + + fn render(&self, out: &mut W) -> std::io::Result<()> { + use cursor::MoveTo; + use style::Print; + use style::PrintStyledContent; + + crossterm::queue!( + out, + terminal::Clear(terminal::ClearType::All), + MoveTo(0, 0), + PrintStyledContent("?".blue()), + )?; + + let base = 1; + + for (i, package) in self.packages.iter().enumerate() { + if self.currently_selected == i { + crossterm::queue!( + out, + MoveTo(1, base + (self.currently_selected as u16)), + PrintStyledContent("❯".blue()), + Print(' '), + )?; + } + let checked = self.checked.contains(&i); + let selector = if checked { "●" } else { "○" }; + crossterm::queue!( + out, + MoveTo(3, base + (i as u16)), + Print(selector), + Print(" "), + )?; + + if self.currently_selected == i { + out.queue(style::SetStyle( + style::ContentStyle::new().on_black().white().bold(), + ))?; + } + let want = &package.package.latest; + crossterm::queue!( + out, + Print(format!( + "{: {}", + package.package.name, + package.package.current, + highlight_new_version(&package.package.current, want), + name_width = self.name_width + 2, + current_width = self.current_width + )), + )?; + // out.queue(Print(&package.package.name))?; + if self.currently_selected == i { + out.queue(style::ResetColor)?; + } + } + + out.queue(MoveTo(0, base + self.packages.len() as u16))?; + + out.flush()?; + + Ok(()) + } +} + +enum VersionDifference { + Major, + Minor, + Patch, +} + +struct VersionParts { + major: u64, + minor: u64, + patch: u64, + pre: Option, +} + +impl VersionParts { + fn parse(s: &str) -> VersionParts { + let mut parts = s.splitn(3, '.'); + let major = parts.next().unwrap().parse().unwrap(); + let minor = parts.next().unwrap().parse().unwrap(); + let patch = parts.next().unwrap(); + let (patch, pre) = if patch.contains('-') { + let (patch, pre) = patch.split_once('-').unwrap(); + (patch, Some(pre.into())) + } else { + (patch, None) + }; + let patch = patch.parse().unwrap(); + let pre = pre.clone(); + Self { + patch, + pre, + minor, + major, + } + } +} + +fn version_diff(a: &VersionParts, b: &VersionParts) -> VersionDifference { + if a.major != b.major { + VersionDifference::Major + } else if a.minor != b.minor { + VersionDifference::Minor + } else { + VersionDifference::Patch + } +} + +fn highlight_new_version(current: &str, new: &str) -> String { + let current_parts = VersionParts::parse(current); + let new_parts = VersionParts::parse(new); + let diff = version_diff(¤t_parts, &new_parts); + + match diff { + VersionDifference::Major => format!( + "{}.{}.{}{}", + style::style(new_parts.major).red().bold(), + style::style(new_parts.minor).red(), + style::style(new_parts.patch).red(), + new_parts + .pre + .map(|pre| pre.red().to_string()) + .unwrap_or_default() + ), + VersionDifference::Minor => format!( + "{}.{}.{}{}", + new_parts.major, + style::style(new_parts.minor).yellow().bold(), + style::style(new_parts.patch).yellow(), + new_parts + .pre + .map(|pre| pre.yellow().to_string()) + .unwrap_or_default() + ), + VersionDifference::Patch => format!( + "{}.{}.{}{}", + new_parts.major, + new_parts.minor, + style::style(new_parts.patch).green().bold(), + new_parts + .pre + .map(|pre| pre.green().to_string()) + .unwrap_or_default() + ), + } +} + +fn interactive() -> io::Result<()> { + let mut stdout = io::stdout(); + terminal::enable_raw_mode()?; + + let mut state = State::new(todo!()); + + stdout.execute(cursor::Hide)?; + + state.render(&mut stdout)?; + + let mut do_it = false; + loop { + let event = crossterm::event::read()?; + #[allow(clippy::single_match)] + match event { + crossterm::event::Event::Key(KeyEvent { + kind: KeyEventKind::Press, + code, + modifiers, + .. + }) => match (code, modifiers) { + (KeyCode::Char('c'), KeyModifiers::CONTROL) => break, + (KeyCode::Up | KeyCode::Char('k'), KeyModifiers::NONE) => { + state.currently_selected = if state.currently_selected == 0 { + state.packages.len() - 1 + } else { + state.currently_selected - 1 + }; + } + (KeyCode::Down | KeyCode::Char('j'), KeyModifiers::NONE) => { + state.currently_selected = + (state.currently_selected + 1) % state.packages.len() + } + (KeyCode::Char(' '), _) => { + if !state.checked.insert(state.currently_selected) { + state.checked.remove(&state.currently_selected); + } + } + (KeyCode::Enter, _) => { + do_it = true; + break; + } + _ => {} + }, + _ => {} + } + state.render(&mut stdout)?; + } + + crossterm::queue!( + &mut stdout, + terminal::Clear(terminal::ClearType::All), + cursor::Show, + cursor::MoveTo(0, 0), + )?; + stdout.flush()?; + + terminal::disable_raw_mode()?; + + if do_it { + println!("doing the thing... {state:?}"); + } + + Ok(()) +}