diff --git a/Cargo.lock b/Cargo.lock index 7ae4bcc8b1..13f58ca778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -970,12 +970,12 @@ dependencies = [ [[package]] name = "console_static_text" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4be93df536dfbcbd39ff7c129635da089901116b88bfc29ec1acb9b56f8ff35" +checksum = "55d8a913e62f6444b79e038be3eb09839e9cfc34d55d85f9336460710647d2f6" dependencies = [ "unicode-width", - "vte", + "vte 0.13.1", ] [[package]] @@ -7427,7 +7427,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" dependencies = [ - "vte", + "vte 0.11.1", ] [[package]] @@ -8804,6 +8804,16 @@ name = "vte" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a0b683b20ef64071ff03745b14391751f6beab06a54347885459b77a3f2caa5" dependencies = [ "arrayvec", "utf8parse", diff --git a/Cargo.toml b/Cargo.toml index ca32b029df..a26b293642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,7 +123,7 @@ cbc = { version = "=0.1.2", features = ["alloc"] } # Instead use util::time::utc_now() chrono = { version = "0.4", default-features = false, features = ["std", "serde"] } color-print = "0.3.5" -console_static_text = "=0.8.1" +console_static_text = "=0.8.3" ctr = { version = "0.9.2", features = ["alloc"] } dashmap = "5.5.3" data-encoding = "2.3.3" diff --git a/cli/tools/registry/pm/outdated.rs b/cli/tools/registry/pm/outdated.rs index ae3fc77e6c..cf6502d02c 100644 --- a/cli/tools/registry/pm/outdated.rs +++ b/cli/tools/registry/pm/outdated.rs @@ -350,14 +350,10 @@ async fn update( interactive::PackageInfo { current_version: current_version .as_ref() - .map(|nv| nv.version.to_string()) - .unwrap_or_default(), + .map(|nv| nv.version.clone()), name: dep.alias_or_name().into(), kind: dep.kind, - new_version: new_req - .version_text() - .trim_start_matches('^') - .to_string(), + new_version: new_req.clone(), } }, ) diff --git a/cli/tools/registry/pm/outdated/interactive.rs b/cli/tools/registry/pm/outdated/interactive.rs index 7788ebdccb..3cfa5184e7 100644 --- a/cli/tools/registry/pm/outdated/interactive.rs +++ b/cli/tools/registry/pm/outdated/interactive.rs @@ -1,53 +1,101 @@ // Copyright 2018-2025 the Deno authors. MIT license. use std::collections::HashSet; +use std::fmt::Write as _; use std::io; -use std::io::Write; +use console_static_text::ConsoleSize; +use console_static_text::TextItem; 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 deno_core::anyhow; -use deno_core::anyhow::Context; +use deno_semver::Version; +use deno_semver::VersionReq; +use deno_terminal::colors; use crate::tools::registry::pm::deps::DepKind; #[derive(Debug)] pub struct PackageInfo { - pub current_version: String, - pub new_version: String, + pub current_version: Option, + pub new_version: VersionReq, pub name: String, pub kind: DepKind, } +#[derive(Debug)] +struct FormattedPackageInfo { + current_version_string: Option, + new_version_highlighted: String, + formatted_name: String, + formatted_name_len: usize, +} + #[derive(Debug)] struct State { - packages: Vec, + packages: Vec, currently_selected: usize, checked: HashSet, name_width: usize, current_width: usize, - start_row: u16, + // start_row: u16, +} + +impl From for FormattedPackageInfo { + fn from(package: PackageInfo) -> Self { + let new_version_string = + package.new_version.version_text().trim_start_matches('^'); + + let new_version_highlighted = + if let (Some(current_version), Ok(new_version)) = ( + &package.current_version, + Version::parse_standard(new_version_string), + ) { + highlight_new_version(current_version, &new_version) + } else { + new_version_string.to_string() + }; + FormattedPackageInfo { + current_version_string: package + .current_version + .as_ref() + .map(|v| v.to_string()), + new_version_highlighted, + formatted_name: format!( + "{}{}", + colors::gray(format!("{}:", package.kind.scheme())), + package.name + ), + formatted_name_len: package.kind.scheme().len() + 1 + package.name.len(), + } + } } impl State { fn new(packages: Vec) -> anyhow::Result { + let packages: Vec<_> = packages + .into_iter() + .map(FormattedPackageInfo::from) + .collect(); let name_width = packages .iter() - .map(|p| p.name.len() + p.kind.scheme().len() + 1) + .map(|p| p.formatted_name_len) .max() .unwrap_or_default(); let current_width = packages .iter() - .map(|p| p.current_version.len()) + .map(|p| { + p.current_version_string + .as_ref() + .map(|s| s.len()) + .unwrap_or_default() + }) .max() .unwrap_or_default(); @@ -58,77 +106,52 @@ impl State { name_width, current_width, - start_row: cursor::position()?.1, }) } - fn render(&self, out: &mut W) -> anyhow::Result<()> { - use cursor::MoveTo; - use style::Print; - use style::PrintStyledContent; + fn render(&self) -> anyhow::Result> { + let mut items = Vec::with_capacity(self.packages.len() + 1); - crossterm::queue!( - out, - MoveTo(0, self.start_row), - terminal::Clear(terminal::ClearType::FromCursorDown), - PrintStyledContent("?".blue()), - Print(" Select which packages to update ( to select, ↑/↓/j/k to navigate, enter to accept, to cancel)") - )?; - - let base = self.start_row + 1; + items.push(TextItem::new_owned(format!( + "{} Select which packages to update ( to select, ↑/↓/j/k to navigate, enter to accept, to cancel)", + colors::intense_blue("?") + ))); for (i, package) in self.packages.iter().enumerate() { - if self.currently_selected == i { - crossterm::queue!( - out, - MoveTo(0, base + (self.currently_selected as u16)), - PrintStyledContent("❯".blue()), - Print(' '), - )?; - } + let mut line = String::new(); + let f = &mut line; + let checked = self.checked.contains(&i); - let selector = if checked { "●" } else { "○" }; - crossterm::queue!( - out, - MoveTo(2, base + (i as u16)), - Print(selector), - Print(" "), + write!( + f, + "{} {} ", + if self.currently_selected == i { + colors::intense_blue("❯").to_string() + } else { + " ".to_string() + }, + if checked { "●" } else { "○" } )?; - if self.currently_selected == i { - out.queue(style::SetStyle( - style::ContentStyle::new().on_black().white().bold(), - ))?; - } - let want = &package.new_version; - let new_version_highlight = - highlight_new_version(&package.current_version, want)?; - let formatted_name = format!( - "{}{}", - deno_terminal::colors::gray(format!("{}:", package.kind.scheme())), - package.name - ); - let name_pad = " ".repeat(self.name_width + 2 - (package.name.len() + 4)); - - crossterm::queue!( - out, - Print(format!( - "{formatted_name}{name_pad} {: {}", - package.current_version, - new_version_highlight, - current_width = self.current_width - )), + let name_pad = + " ".repeat(self.name_width + 2 - package.formatted_name_len); + write!( + f, + "{formatted_name}{name_pad} {: {}", + package + .current_version_string + .as_deref() + .unwrap_or_default(), + &package.new_version_highlighted, + name_pad = name_pad, + formatted_name = package.formatted_name, + current_width = self.current_width )?; - if self.currently_selected == i { - out.queue(style::ResetColor)?; - } + + items.push(TextItem::with_hanging_indent_owned(line, 1)); } - out.queue(MoveTo(0, base + self.packages.len() as u16))?; - - out.flush()?; - - Ok(()) + Ok(items) } } @@ -138,45 +161,7 @@ enum VersionDifference { Patch, } -struct VersionParts { - major: u64, - minor: u64, - patch: u64, - pre: Option, -} - -impl VersionParts { - fn parse(s: &str) -> Result { - let mut parts = s.splitn(3, '.'); - let major = parts - .next() - .ok_or_else(|| anyhow::anyhow!("expected major version"))? - .parse()?; - let minor = parts - .next() - .ok_or_else(|| anyhow::anyhow!("expected minor version"))? - .parse()?; - let patch = parts - .next() - .ok_or_else(|| anyhow::anyhow!("expected patch version"))?; - let (patch, pre) = if patch.contains('-') { - let (patch, pre) = patch.split_once('-').unwrap(); - (patch, Some(pre.into())) - } else { - (patch, None) - }; - let patch = patch.parse()?; - let pre = pre.clone(); - Ok(Self { - patch, - pre, - minor, - major, - }) - } -} - -fn version_diff(a: &VersionParts, b: &VersionParts) -> VersionDifference { +fn version_diff(a: &Version, b: &Version) -> VersionDifference { if a.major != b.major { VersionDifference::Major } else if a.minor != b.minor { @@ -186,48 +171,43 @@ fn version_diff(a: &VersionParts, b: &VersionParts) -> VersionDifference { } } -fn highlight_new_version( - current: &str, - new: &str, -) -> Result { - let current_parts = VersionParts::parse(current) - .with_context(|| format!("parsing current version: {current}"))?; - let new_parts = VersionParts::parse(new) - .with_context(|| format!("parsing new version: {new}"))?; - let diff = version_diff(¤t_parts, &new_parts); +fn highlight_new_version(current: &Version, new: &Version) -> String { + let diff = version_diff(current, new); - Ok(match diff { + let new_pre = if new.pre.is_empty() { + String::new() + } else { + let mut s = String::new(); + s.push('-'); + for p in &new.pre { + s.push_str(p); + } + s + }; + + 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() + colors::red_bold(new.major), + colors::red(new.minor), + colors::red(new.patch), + colors::red(new_pre) ), 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() + new.major, + colors::yellow_bold(new.minor), + colors::yellow(new.patch), + colors::yellow(new_pre) ), 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() + new.major, + new.minor, + colors::green_bold(new.patch), + colors::green(new_pre) ), - }) + } } struct RawMode { @@ -258,26 +238,42 @@ impl Drop for RawMode { pub fn select_interactive( packages: Vec, ) -> anyhow::Result>> { - let mut stdout = io::stdout(); - let raw_mode = RawMode::enable()?; + let mut stderr = io::stderr(); - let (_, rows) = terminal::size()?; + let raw_mode = RawMode::enable()?; + let mut static_text = + console_static_text::ConsoleStaticText::new(move || { + if let Ok((cols, rows)) = terminal::size() { + ConsoleSize { + cols: Some(cols), + rows: Some(rows), + } + } else { + ConsoleSize { + cols: None, + rows: None, + } + } + }); + static_text.keep_cursor_zero_column(true); let (_, start_row) = cursor::position().unwrap_or_default(); + let (_, rows) = terminal::size()?; if rows - start_row < (packages.len() + 2) as u16 { let pad = ((packages.len() + 2) as u16) - (rows - start_row); - stdout.execute(terminal::ScrollUp(pad))?; - stdout.execute(cursor::MoveUp(pad))?; + stderr.execute(terminal::ScrollUp(pad))?; + stderr.execute(cursor::MoveUp(pad))?; } let mut state = State::new(packages)?; - stdout.execute(cursor::Hide)?; - - state.render(&mut stdout)?; + stderr.execute(cursor::Hide)?; let mut do_it = false; loop { + let items = state.render()?; + static_text.eprint_items(items.iter()); + let event = crossterm::event::read()?; #[allow(clippy::single_match)] match event { @@ -312,16 +308,11 @@ pub fn select_interactive( }, _ => {} } - state.render(&mut stdout)?; } - crossterm::queue!( - &mut stdout, - cursor::MoveTo(0, state.start_row), - terminal::Clear(terminal::ClearType::FromCursorDown), - cursor::Show, - )?; - stdout.flush()?; + static_text.eprint_clear(); + + crossterm::execute!(&mut stderr, cursor::Show)?; raw_mode.disable()?;