mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
feat(outdated): interactive update (#27812)
interactively select which packages to upgrade. a future improvement could be to add a way to select the version as well, though not sure how valuable that would be.
This commit is contained in:
parent
28834a89bb
commit
b440d2d4f7
7 changed files with 597 additions and 28 deletions
76
Cargo.lock
generated
76
Cargo.lock
generated
|
@ -990,12 +990,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]]
|
||||
|
@ -1125,6 +1125,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"
|
||||
|
@ -1295,6 +1320,7 @@ dependencies = [
|
|||
"clap_complete_fig",
|
||||
"color-print",
|
||||
"console_static_text",
|
||||
"crossterm",
|
||||
"dashmap",
|
||||
"data-encoding",
|
||||
"deno_ast",
|
||||
|
@ -1390,6 +1416,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"twox-hash",
|
||||
"typed-arena",
|
||||
"unicode-width",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"which",
|
||||
|
@ -5177,6 +5204,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"
|
||||
|
@ -5359,7 +5398,7 @@ dependencies = [
|
|||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
@ -6710,9 +6749,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",
|
||||
|
@ -7150,6 +7189,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"
|
||||
|
@ -7405,7 +7455,7 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa"
|
||||
dependencies = [
|
||||
"vte",
|
||||
"vte 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -8220,7 +8270,7 @@ dependencies = [
|
|||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 0.8.11",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
|
@ -8783,6 +8833,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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
@ -169,6 +170,7 @@ tower-lsp.workspace = true
|
|||
tracing = { version = "0.1", features = ["log", "default"] }
|
||||
twox-hash.workspace = true
|
||||
typed-arena = "=2.0.2"
|
||||
unicode-width = "0.1.3"
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
walkdir.workspace = true
|
||||
which.workspace = true
|
||||
|
|
|
@ -475,7 +475,7 @@ pub enum DenoSubcommand {
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OutdatedKind {
|
||||
Update { latest: bool },
|
||||
Update { latest: bool, interactive: bool },
|
||||
PrintOutdated { compatible: bool },
|
||||
}
|
||||
|
||||
|
@ -2660,7 +2660,7 @@ Specific version requirements to update to can be specified:
|
|||
.long("latest")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help(
|
||||
"Update to the latest version, regardless of semver constraints",
|
||||
"Consider the latest version, regardless of semver constraints",
|
||||
)
|
||||
.conflicts_with("compatible"),
|
||||
)
|
||||
|
@ -2669,15 +2669,21 @@ Specific version requirements to update to can be specified:
|
|||
.long("update")
|
||||
.short('u')
|
||||
.action(ArgAction::SetTrue)
|
||||
.conflicts_with("compatible")
|
||||
.help("Update dependency versions"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("interactive")
|
||||
.long("interactive")
|
||||
.short('i')
|
||||
.action(ArgAction::SetTrue)
|
||||
.requires("update")
|
||||
.help("Interactively select which dependencies to update")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("compatible")
|
||||
.long("compatible")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Only output versions that satisfy semver requirements")
|
||||
.conflicts_with("update"),
|
||||
.help("Only consider versions that satisfy semver requirements")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("recursive")
|
||||
|
@ -4462,7 +4468,11 @@ fn outdated_parse(
|
|||
let update = matches.get_flag("update");
|
||||
let kind = if update {
|
||||
let latest = matches.get_flag("latest");
|
||||
OutdatedKind::Update { latest }
|
||||
let interactive = matches.get_flag("interactive");
|
||||
OutdatedKind::Update {
|
||||
latest,
|
||||
interactive,
|
||||
}
|
||||
} else {
|
||||
let compatible = matches.get_flag("compatible");
|
||||
OutdatedKind::PrintOutdated { compatible }
|
||||
|
@ -11646,7 +11656,10 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
|
|||
svec!["--update"],
|
||||
OutdatedFlags {
|
||||
filters: vec![],
|
||||
kind: OutdatedKind::Update { latest: false },
|
||||
kind: OutdatedKind::Update {
|
||||
latest: false,
|
||||
interactive: false,
|
||||
},
|
||||
recursive: false,
|
||||
},
|
||||
),
|
||||
|
@ -11654,7 +11667,10 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
|
|||
svec!["--update", "--latest"],
|
||||
OutdatedFlags {
|
||||
filters: vec![],
|
||||
kind: OutdatedKind::Update { latest: true },
|
||||
kind: OutdatedKind::Update {
|
||||
latest: true,
|
||||
interactive: false,
|
||||
},
|
||||
recursive: false,
|
||||
},
|
||||
),
|
||||
|
@ -11662,7 +11678,10 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
|
|||
svec!["--update", "--recursive"],
|
||||
OutdatedFlags {
|
||||
filters: vec![],
|
||||
kind: OutdatedKind::Update { latest: false },
|
||||
kind: OutdatedKind::Update {
|
||||
latest: false,
|
||||
interactive: false,
|
||||
},
|
||||
recursive: true,
|
||||
},
|
||||
),
|
||||
|
@ -11670,7 +11689,10 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
|
|||
svec!["--update", "@foo/bar"],
|
||||
OutdatedFlags {
|
||||
filters: svec!["@foo/bar"],
|
||||
kind: OutdatedKind::Update { latest: false },
|
||||
kind: OutdatedKind::Update {
|
||||
latest: false,
|
||||
interactive: false,
|
||||
},
|
||||
recursive: false,
|
||||
},
|
||||
),
|
||||
|
@ -11682,6 +11704,17 @@ Usage: deno repl [OPTIONS] [-- [ARGS]...]\n"
|
|||
recursive: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
svec!["--update", "--latest", "--interactive"],
|
||||
OutdatedFlags {
|
||||
filters: svec![],
|
||||
kind: OutdatedKind::Update {
|
||||
latest: true,
|
||||
interactive: true,
|
||||
},
|
||||
recursive: false,
|
||||
},
|
||||
),
|
||||
];
|
||||
for (input, expected) in cases {
|
||||
let mut args = svec!["deno", "outdated"];
|
||||
|
|
|
@ -194,6 +194,12 @@ pub struct Dep {
|
|||
pub alias: Option<String>,
|
||||
}
|
||||
|
||||
impl Dep {
|
||||
pub fn alias_or_name(&self) -> &str {
|
||||
self.alias.as_deref().unwrap_or_else(|| &self.req.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn import_map_entries(
|
||||
import_map: &ImportMap,
|
||||
) -> impl Iterator<Item = (KeyPath, SpecifierMapEntry<'_>)> {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||
|
||||
mod interactive;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -13,6 +15,7 @@ use deno_semver::VersionReq;
|
|||
use deno_terminal::colors;
|
||||
|
||||
use super::deps::Dep;
|
||||
use super::deps::DepId;
|
||||
use super::deps::DepManager;
|
||||
use super::deps::DepManagerArgs;
|
||||
use super::deps::PackageLatestVersion;
|
||||
|
@ -240,8 +243,11 @@ pub async fn outdated(
|
|||
deps.resolve_versions().await?;
|
||||
|
||||
match update_flags.kind {
|
||||
crate::args::OutdatedKind::Update { latest } => {
|
||||
update(deps, latest, &filter_set, flags).await?;
|
||||
crate::args::OutdatedKind::Update {
|
||||
latest,
|
||||
interactive,
|
||||
} => {
|
||||
update(deps, latest, &filter_set, interactive, flags).await?;
|
||||
}
|
||||
crate::args::OutdatedKind::PrintOutdated { compatible } => {
|
||||
print_outdated(&mut deps, compatible)?;
|
||||
|
@ -299,9 +305,10 @@ async fn update(
|
|||
mut deps: DepManager,
|
||||
update_to_latest: bool,
|
||||
filter_set: &filter::FilterSet,
|
||||
interactive: bool,
|
||||
flags: Arc<Flags>,
|
||||
) -> Result<(), AnyError> {
|
||||
let mut updated = Vec::new();
|
||||
let mut to_update = Vec::new();
|
||||
|
||||
for (dep_id, resolved, latest_versions) in deps
|
||||
.deps_with_resolved_latest_versions()
|
||||
|
@ -320,19 +327,54 @@ async fn update(
|
|||
continue;
|
||||
};
|
||||
|
||||
updated.push((
|
||||
to_update.push((
|
||||
dep_id,
|
||||
format!("{}:{}", dep.kind.scheme(), dep.req.name),
|
||||
deps.resolved_version(dep.id).cloned(),
|
||||
new_version_req.clone(),
|
||||
));
|
||||
|
||||
deps.update_dep(dep_id, new_version_req);
|
||||
}
|
||||
|
||||
deps.commit_changes()?;
|
||||
if interactive && !to_update.is_empty() {
|
||||
let selected = interactive::select_interactive(
|
||||
to_update
|
||||
.iter()
|
||||
.map(
|
||||
|(dep_id, _, current_version, new_req): &(
|
||||
DepId,
|
||||
String,
|
||||
Option<PackageNv>,
|
||||
VersionReq,
|
||||
)| {
|
||||
let dep = deps.get_dep(*dep_id);
|
||||
interactive::PackageInfo {
|
||||
id: *dep_id,
|
||||
current_version: current_version
|
||||
.as_ref()
|
||||
.map(|nv| nv.version.clone()),
|
||||
name: dep.alias_or_name().into(),
|
||||
kind: dep.kind,
|
||||
new_version: new_req.clone(),
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
)?;
|
||||
if let Some(selected) = selected {
|
||||
to_update.retain(|(id, _, _, _)| selected.contains(id));
|
||||
} else {
|
||||
log::info!("Cancelled, not updating");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if !to_update.is_empty() {
|
||||
for (dep_id, _, _, new_version_req) in &to_update {
|
||||
deps.update_dep(*dep_id, new_version_req.clone());
|
||||
}
|
||||
|
||||
deps.commit_changes()?;
|
||||
|
||||
if !updated.is_empty() {
|
||||
let factory = super::npm_install_after_modification(
|
||||
flags.clone(),
|
||||
Some(deps.jsr_fetch_resolver.clone()),
|
||||
|
@ -352,7 +394,7 @@ async fn update(
|
|||
let mut deps = deps.reloaded_after_modification(args);
|
||||
deps.resolve_current_versions().await?;
|
||||
for (dep_id, package_name, maybe_current_version, new_version_req) in
|
||||
updated
|
||||
to_update
|
||||
{
|
||||
if let Some(nv) = deps.resolved_version(dep_id) {
|
||||
updated_to_versions.insert((
|
||||
|
|
426
cli/tools/registry/pm/outdated/interactive.rs
Normal file
426
cli/tools/registry/pm/outdated/interactive.rs
Normal file
|
@ -0,0 +1,426 @@
|
|||
// Copyright 2018-2025 the Deno authors. MIT license.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write as _;
|
||||
use std::io;
|
||||
|
||||
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::terminal;
|
||||
use crossterm::ExecutableCommand;
|
||||
use deno_core::anyhow;
|
||||
use deno_semver::Version;
|
||||
use deno_semver::VersionReq;
|
||||
use deno_terminal::colors;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::tools::registry::pm::deps::DepId;
|
||||
use crate::tools::registry::pm::deps::DepKind;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PackageInfo {
|
||||
pub id: DepId,
|
||||
pub current_version: Option<Version>,
|
||||
pub new_version: VersionReq,
|
||||
pub name: String,
|
||||
pub kind: DepKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FormattedPackageInfo {
|
||||
dep_ids: Vec<DepId>,
|
||||
current_version_string: Option<String>,
|
||||
new_version_highlighted: String,
|
||||
formatted_name: String,
|
||||
formatted_name_len: usize,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
packages: Vec<FormattedPackageInfo>,
|
||||
currently_selected: usize,
|
||||
checked: HashSet<usize>,
|
||||
|
||||
name_width: usize,
|
||||
current_width: usize,
|
||||
}
|
||||
|
||||
impl From<PackageInfo> 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 {
|
||||
dep_ids: vec![package.id],
|
||||
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(),
|
||||
name: package.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(packages: Vec<PackageInfo>) -> anyhow::Result<Self> {
|
||||
let mut deduped_packages: HashMap<
|
||||
(String, Option<Version>, VersionReq),
|
||||
FormattedPackageInfo,
|
||||
> = HashMap::with_capacity(packages.len());
|
||||
for package in packages {
|
||||
match deduped_packages.entry((
|
||||
package.name.clone(),
|
||||
package.current_version.clone(),
|
||||
package.new_version.clone(),
|
||||
)) {
|
||||
std::collections::hash_map::Entry::Occupied(mut occupied_entry) => {
|
||||
occupied_entry.get_mut().dep_ids.push(package.id)
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(vacant_entry) => {
|
||||
vacant_entry.insert(FormattedPackageInfo::from(package));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut packages: Vec<_> = deduped_packages.into_values().collect();
|
||||
packages.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let name_width = packages
|
||||
.iter()
|
||||
.map(|p| p.formatted_name_len)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let current_width = packages
|
||||
.iter()
|
||||
.map(|p| {
|
||||
p.current_version_string
|
||||
.as_ref()
|
||||
.map(|s| s.len())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Self {
|
||||
packages,
|
||||
currently_selected: 0,
|
||||
checked: HashSet::new(),
|
||||
|
||||
name_width,
|
||||
current_width,
|
||||
})
|
||||
}
|
||||
|
||||
fn instructions_line() -> &'static str {
|
||||
"Select which packages to update (<space> to select, ↑/↓/j/k to navigate, a to select all, i to invert selection, enter to accept, <Ctrl-c> to cancel)"
|
||||
}
|
||||
|
||||
fn render(&self) -> anyhow::Result<Vec<TextItem>> {
|
||||
let mut items = Vec::with_capacity(self.packages.len() + 1);
|
||||
|
||||
items.push(TextItem::new_owned(format!(
|
||||
"{} {}",
|
||||
colors::intense_blue("?"),
|
||||
Self::instructions_line()
|
||||
)));
|
||||
|
||||
for (i, package) in self.packages.iter().enumerate() {
|
||||
let mut line = String::new();
|
||||
let f = &mut line;
|
||||
|
||||
let checked = self.checked.contains(&i);
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
if self.currently_selected == i {
|
||||
colors::intense_blue("❯").to_string()
|
||||
} else {
|
||||
" ".to_string()
|
||||
},
|
||||
if checked { "●" } else { "○" }
|
||||
)?;
|
||||
|
||||
let name_pad =
|
||||
" ".repeat(self.name_width + 2 - package.formatted_name_len);
|
||||
write!(
|
||||
f,
|
||||
"{formatted_name}{name_pad} {:<current_width$} -> {}",
|
||||
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
|
||||
)?;
|
||||
|
||||
items.push(TextItem::with_hanging_indent_owned(line, 1));
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
enum VersionDifference {
|
||||
Major,
|
||||
Minor,
|
||||
Patch,
|
||||
Prerelease,
|
||||
}
|
||||
|
||||
fn version_diff(a: &Version, b: &Version) -> VersionDifference {
|
||||
if a.major != b.major {
|
||||
VersionDifference::Major
|
||||
} else if a.minor != b.minor {
|
||||
VersionDifference::Minor
|
||||
} else if a.patch != b.patch {
|
||||
VersionDifference::Patch
|
||||
} else {
|
||||
VersionDifference::Prerelease
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_new_version(current: &Version, new: &Version) -> String {
|
||||
let diff = version_diff(current, new);
|
||||
|
||||
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!(
|
||||
"{}.{}.{}{}",
|
||||
colors::red_bold(new.major),
|
||||
colors::red_bold(new.minor),
|
||||
colors::red_bold(new.patch),
|
||||
colors::red_bold(new_pre)
|
||||
),
|
||||
VersionDifference::Minor => format!(
|
||||
"{}.{}.{}{}",
|
||||
new.major,
|
||||
colors::yellow_bold(new.minor),
|
||||
colors::yellow_bold(new.patch),
|
||||
colors::yellow_bold(new_pre)
|
||||
),
|
||||
VersionDifference::Patch => format!(
|
||||
"{}.{}.{}{}",
|
||||
new.major,
|
||||
new.minor,
|
||||
colors::green_bold(new.patch),
|
||||
colors::green_bold(new_pre)
|
||||
),
|
||||
VersionDifference::Prerelease => format!(
|
||||
"{}.{}.{}{}",
|
||||
new.major,
|
||||
new.minor,
|
||||
new.patch,
|
||||
colors::red_bold(new_pre)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
struct RawMode {
|
||||
needs_disable: bool,
|
||||
}
|
||||
|
||||
impl RawMode {
|
||||
fn enable() -> io::Result<Self> {
|
||||
terminal::enable_raw_mode()?;
|
||||
Ok(Self {
|
||||
needs_disable: true,
|
||||
})
|
||||
}
|
||||
fn disable(mut self) -> io::Result<()> {
|
||||
self.needs_disable = false;
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RawMode {
|
||||
fn drop(&mut self) {
|
||||
if self.needs_disable {
|
||||
let _ = terminal::disable_raw_mode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_interactive(
|
||||
packages: Vec<PackageInfo>,
|
||||
) -> anyhow::Result<Option<HashSet<DepId>>> {
|
||||
let mut stderr = io::stderr();
|
||||
|
||||
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);
|
||||
stderr.execute(terminal::ScrollUp(pad.min(rows)))?;
|
||||
stderr.execute(cursor::MoveUp(pad.min(rows)))?;
|
||||
}
|
||||
|
||||
let mut state = State::new(packages)?;
|
||||
stderr.execute(cursor::Hide)?;
|
||||
|
||||
let instructions_width = format!("? {}", State::instructions_line()).width();
|
||||
|
||||
let mut do_it = false;
|
||||
let mut scroll_offset = 0;
|
||||
loop {
|
||||
let mut items = state.render()?;
|
||||
let size = static_text.console_size();
|
||||
let first_line_rows = size
|
||||
.cols
|
||||
.map(|cols| (instructions_width / cols as usize) + 1)
|
||||
.unwrap_or(1);
|
||||
if let Some(rows) = size.rows {
|
||||
if items.len() + first_line_rows >= rows as usize {
|
||||
let adj = if scroll_offset == 0 {
|
||||
first_line_rows.saturating_sub(1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if state.currently_selected < scroll_offset {
|
||||
scroll_offset = state.currently_selected;
|
||||
} else if state.currently_selected + 1
|
||||
>= scroll_offset + (rows as usize).saturating_sub(adj)
|
||||
{
|
||||
scroll_offset =
|
||||
(state.currently_selected + 1).saturating_sub(rows as usize) + 1;
|
||||
}
|
||||
let adj = if scroll_offset == 0 {
|
||||
first_line_rows.saturating_sub(1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut new_items = Vec::with_capacity(rows as usize);
|
||||
|
||||
scroll_offset = scroll_offset.clamp(0, items.len() - 1);
|
||||
new_items.extend(
|
||||
items.drain(
|
||||
scroll_offset
|
||||
..(scroll_offset + (rows as usize).saturating_sub(adj))
|
||||
.min(items.len()),
|
||||
),
|
||||
);
|
||||
items = new_items;
|
||||
}
|
||||
}
|
||||
static_text.eprint_items(items.iter());
|
||||
|
||||
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::Char('a'), _) => {
|
||||
if (0..state.packages.len()).all(|idx| state.checked.contains(&idx)) {
|
||||
state.checked.clear();
|
||||
} else {
|
||||
state.checked.extend(0..state.packages.len());
|
||||
}
|
||||
}
|
||||
(KeyCode::Char('i'), _) => {
|
||||
for idx in 0..state.packages.len() {
|
||||
if state.checked.contains(&idx) {
|
||||
state.checked.remove(&idx);
|
||||
} else {
|
||||
state.checked.insert(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
(KeyCode::Enter, _) => {
|
||||
do_it = true;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
static_text.eprint_clear();
|
||||
|
||||
crossterm::execute!(&mut stderr, cursor::Show)?;
|
||||
|
||||
raw_mode.disable()?;
|
||||
|
||||
if do_it {
|
||||
Ok(Some(
|
||||
state
|
||||
.checked
|
||||
.into_iter()
|
||||
.flat_map(|idx| &state.packages[idx].dep_ids)
|
||||
.copied()
|
||||
.collect(),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue