Skip to content

Commit 3df1283

Browse files
committed
outline interactive update
1 parent ab18dac commit 3df1283

File tree

4 files changed

+330
-4
lines changed

4 files changed

+330
-4
lines changed

Cargo.lock

+53-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ clap_complete = "=4.5.24"
105105
clap_complete_fig = "=4.5.2"
106106
color-print.workspace = true
107107
console_static_text.workspace = true
108+
crossterm = "0.28.1"
108109
dashmap.workspace = true
109110
data-encoding.workspace = true
110111
dhat = { version = "0.3.3", optional = true }

cli/tools/registry/pm/outdated.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

3+
mod interactive;
4+
35
use std::collections::HashSet;
46
use std::sync::Arc;
57

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
use std::collections::HashSet;
2+
use std::io;
3+
use std::io::Write;
4+
5+
use crossterm::cursor;
6+
use crossterm::event::KeyCode;
7+
use crossterm::event::KeyEvent;
8+
use crossterm::event::KeyEventKind;
9+
use crossterm::event::KeyModifiers;
10+
use crossterm::style;
11+
use crossterm::style::Stylize;
12+
use crossterm::terminal;
13+
use crossterm::ExecutableCommand;
14+
use crossterm::QueueableCommand;
15+
16+
use super::super::deps::DepLocation;
17+
use super::OutdatedPackage;
18+
19+
#[derive(Debug)]
20+
struct PackageInfo {
21+
location: DepLocation,
22+
package: OutdatedPackage,
23+
}
24+
25+
#[derive(Debug)]
26+
struct State {
27+
packages: Vec<PackageInfo>,
28+
currently_selected: usize,
29+
checked: HashSet<usize>,
30+
31+
name_width: usize,
32+
current_width: usize,
33+
}
34+
35+
impl State {
36+
fn new(packages: Vec<PackageInfo>) -> Self {
37+
let name_width = packages
38+
.iter()
39+
.map(|p| p.package.name.len())
40+
.max()
41+
.unwrap_or_default();
42+
let current_width = packages
43+
.iter()
44+
.map(|p| p.package.current.len())
45+
.max()
46+
.unwrap_or_default();
47+
48+
let mut packages = packages;
49+
packages
50+
.sort_by(|a, b| a.location.file_path().cmp(&b.location.file_path()));
51+
52+
Self {
53+
packages,
54+
currently_selected: 0,
55+
checked: HashSet::new(),
56+
57+
name_width,
58+
current_width,
59+
}
60+
}
61+
62+
fn render<W: std::io::Write>(&self, out: &mut W) -> std::io::Result<()> {
63+
use cursor::MoveTo;
64+
use style::Print;
65+
use style::PrintStyledContent;
66+
67+
crossterm::queue!(
68+
out,
69+
terminal::Clear(terminal::ClearType::All),
70+
MoveTo(0, 0),
71+
PrintStyledContent("?".blue()),
72+
)?;
73+
74+
let base = 1;
75+
76+
for (i, package) in self.packages.iter().enumerate() {
77+
if self.currently_selected == i {
78+
crossterm::queue!(
79+
out,
80+
MoveTo(1, base + (self.currently_selected as u16)),
81+
PrintStyledContent("❯".blue()),
82+
Print(' '),
83+
)?;
84+
}
85+
let checked = self.checked.contains(&i);
86+
let selector = if checked { "●" } else { "○" };
87+
crossterm::queue!(
88+
out,
89+
MoveTo(3, base + (i as u16)),
90+
Print(selector),
91+
Print(" "),
92+
)?;
93+
94+
if self.currently_selected == i {
95+
out.queue(style::SetStyle(
96+
style::ContentStyle::new().on_black().white().bold(),
97+
))?;
98+
}
99+
let want = &package.package.latest;
100+
crossterm::queue!(
101+
out,
102+
Print(format!(
103+
"{:<name_width$}{:<current_width$} -> {}",
104+
package.package.name,
105+
package.package.current,
106+
highlight_new_version(&package.package.current, want),
107+
name_width = self.name_width + 2,
108+
current_width = self.current_width
109+
)),
110+
)?;
111+
// out.queue(Print(&package.package.name))?;
112+
if self.currently_selected == i {
113+
out.queue(style::ResetColor)?;
114+
}
115+
}
116+
117+
out.queue(MoveTo(0, base + self.packages.len() as u16))?;
118+
119+
out.flush()?;
120+
121+
Ok(())
122+
}
123+
}
124+
125+
enum VersionDifference {
126+
Major,
127+
Minor,
128+
Patch,
129+
}
130+
131+
struct VersionParts {
132+
major: u64,
133+
minor: u64,
134+
patch: u64,
135+
pre: Option<String>,
136+
}
137+
138+
impl VersionParts {
139+
fn parse(s: &str) -> VersionParts {
140+
let mut parts = s.splitn(3, '.');
141+
let major = parts.next().unwrap().parse().unwrap();
142+
let minor = parts.next().unwrap().parse().unwrap();
143+
let patch = parts.next().unwrap();
144+
let (patch, pre) = if patch.contains('-') {
145+
let (patch, pre) = patch.split_once('-').unwrap();
146+
(patch, Some(pre.into()))
147+
} else {
148+
(patch, None)
149+
};
150+
let patch = patch.parse().unwrap();
151+
let pre = pre.clone();
152+
Self {
153+
patch,
154+
pre,
155+
minor,
156+
major,
157+
}
158+
}
159+
}
160+
161+
fn version_diff(a: &VersionParts, b: &VersionParts) -> VersionDifference {
162+
if a.major != b.major {
163+
VersionDifference::Major
164+
} else if a.minor != b.minor {
165+
VersionDifference::Minor
166+
} else {
167+
VersionDifference::Patch
168+
}
169+
}
170+
171+
fn highlight_new_version(current: &str, new: &str) -> String {
172+
let current_parts = VersionParts::parse(current);
173+
let new_parts = VersionParts::parse(new);
174+
let diff = version_diff(&current_parts, &new_parts);
175+
176+
match diff {
177+
VersionDifference::Major => format!(
178+
"{}.{}.{}{}",
179+
style::style(new_parts.major).red().bold(),
180+
style::style(new_parts.minor).red(),
181+
style::style(new_parts.patch).red(),
182+
new_parts
183+
.pre
184+
.map(|pre| pre.red().to_string())
185+
.unwrap_or_default()
186+
),
187+
VersionDifference::Minor => format!(
188+
"{}.{}.{}{}",
189+
new_parts.major,
190+
style::style(new_parts.minor).yellow().bold(),
191+
style::style(new_parts.patch).yellow(),
192+
new_parts
193+
.pre
194+
.map(|pre| pre.yellow().to_string())
195+
.unwrap_or_default()
196+
),
197+
VersionDifference::Patch => format!(
198+
"{}.{}.{}{}",
199+
new_parts.major,
200+
new_parts.minor,
201+
style::style(new_parts.patch).green().bold(),
202+
new_parts
203+
.pre
204+
.map(|pre| pre.green().to_string())
205+
.unwrap_or_default()
206+
),
207+
}
208+
}
209+
210+
fn interactive() -> io::Result<()> {
211+
let mut stdout = io::stdout();
212+
terminal::enable_raw_mode()?;
213+
214+
let mut state = State::new(todo!());
215+
216+
stdout.execute(cursor::Hide)?;
217+
218+
state.render(&mut stdout)?;
219+
220+
let mut do_it = false;
221+
loop {
222+
let event = crossterm::event::read()?;
223+
#[allow(clippy::single_match)]
224+
match event {
225+
crossterm::event::Event::Key(KeyEvent {
226+
kind: KeyEventKind::Press,
227+
code,
228+
modifiers,
229+
..
230+
}) => match (code, modifiers) {
231+
(KeyCode::Char('c'), KeyModifiers::CONTROL) => break,
232+
(KeyCode::Up | KeyCode::Char('k'), KeyModifiers::NONE) => {
233+
state.currently_selected = if state.currently_selected == 0 {
234+
state.packages.len() - 1
235+
} else {
236+
state.currently_selected - 1
237+
};
238+
}
239+
(KeyCode::Down | KeyCode::Char('j'), KeyModifiers::NONE) => {
240+
state.currently_selected =
241+
(state.currently_selected + 1) % state.packages.len()
242+
}
243+
(KeyCode::Char(' '), _) => {
244+
if !state.checked.insert(state.currently_selected) {
245+
state.checked.remove(&state.currently_selected);
246+
}
247+
}
248+
(KeyCode::Enter, _) => {
249+
do_it = true;
250+
break;
251+
}
252+
_ => {}
253+
},
254+
_ => {}
255+
}
256+
state.render(&mut stdout)?;
257+
}
258+
259+
crossterm::queue!(
260+
&mut stdout,
261+
terminal::Clear(terminal::ClearType::All),
262+
cursor::Show,
263+
cursor::MoveTo(0, 0),
264+
)?;
265+
stdout.flush()?;
266+
267+
terminal::disable_raw_mode()?;
268+
269+
if do_it {
270+
println!("doing the thing... {state:?}");
271+
}
272+
273+
Ok(())
274+
}

0 commit comments

Comments
 (0)