1use std::collections::HashSet;
2use std::io::{Write, stdout};
3use std::time::Duration;
4
5use anyhow::Result;
6use crossterm::{
7 cursor::{Hide, MoveTo, MoveToColumn, Show},
8 event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
9 execute,
10 style::{Attribute, Print, SetAttribute},
11 terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
12};
13
14use crate::changes::safe_text;
15use crate::dependencies::version_change;
16use crate::output::Pending;
17
18struct Screen;
19
20impl Screen {
21 fn open() -> Result<Self> {
22 terminal::enable_raw_mode()?;
23 let guard = Self;
24 execute!(stdout(), EnterAlternateScreen, Hide)?;
25 Ok(guard)
26 }
27}
28
29impl Drop for Screen {
30 fn drop(&mut self) {
31 let _ = execute!(
32 stdout(),
33 SetAttribute(Attribute::Reset),
34 Show,
35 LeaveAlternateScreen
36 );
37 let _ = terminal::disable_raw_mode();
38 }
39}
40
41fn read_key() -> Result<KeyEvent> {
42 loop {
43 crate::check_interrupt()?;
44 if event::poll(Duration::from_millis(100))?
45 && let Event::Key(key) = event::read()?
46 && key.kind != KeyEventKind::Release
47 {
48 if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
49 return Err(crate::Interrupted.into());
50 }
51 return Ok(key);
52 }
53 }
54}
55
56#[derive(Default)]
57struct Selector {
58 rows: Vec<(usize, Option<usize>)>,
59 selected: HashSet<(usize, usize)>,
60 cursor: usize,
61}
62
63impl Selector {
64 fn new(pending: &Pending) -> Self {
65 let mut selector = Self::default();
66 for (project, (_, updates)) in pending.iter().enumerate() {
67 selector.rows.push((project, None));
68 selector
69 .rows
70 .extend((0..updates.len()).map(|index| (project, Some(index))));
71 }
72 selector
73 }
74
75 fn toggle(&mut self, pending: &Pending) {
76 let (project, index) = self.rows[self.cursor];
77 if let Some(index) = index {
78 if !self.selected.remove(&(project, index)) {
79 self.selected.insert((project, index));
80 }
81 } else {
82 let children: Vec<_> = (0..pending[project].1.len())
83 .map(|index| (project, index))
84 .collect();
85 let clear = children.iter().all(|child| self.selected.contains(child));
86 for child in children {
87 if clear {
88 self.selected.remove(&child);
89 } else {
90 self.selected.insert(child);
91 }
92 }
93 }
94 }
95
96 fn render(&self, pending: &Pending) -> Result<()> {
97 let (width, height) = terminal::size()?;
98 let visible = usize::from(height.saturating_sub(3).max(1));
99 let start = self.cursor.saturating_sub(visible - 1);
100 let mut output = stdout();
101 execute!(
102 output,
103 MoveTo(0, 0),
104 Clear(ClearType::All),
105 Print(format!(
106 "Select dependency updates ({} selected). Labels do not prove compatibility.\r\nArrows: move | Space: toggle entry/project | Enter: review | Esc: cancel | Ctrl+C: interrupt\r\n",
107 self.selected.len()
108 ))
109 )?;
110 for (row, &(project_index, index)) in self.rows.iter().enumerate().skip(start).take(visible)
111 {
112 let (project, updates) = &pending[project_index];
113 let label = if let Some(index) = index {
114 let update = &updates[index];
115 format!(
116 " [{}] {}: {} -> {} [{}]",
117 if self.selected.contains(&(project_index, index)) {
118 "x"
119 } else {
120 " "
121 },
122 update.dependency.section.join("."),
123 update.dependency.requirement,
124 update.version,
125 version_change(update)
126 )
127 } else {
128 let selected = (0..updates.len())
129 .filter(|index| self.selected.contains(&(project_index, *index)))
130 .count();
131 format!(
132 "[{selected}/{}] {} (select/clear project)",
133 updates.len(),
134 project.path.display()
135 )
136 };
137 let label: String = safe_text(&label, false)
139 .chars()
140 .take(usize::from(width.saturating_sub(1)))
141 .collect();
142 if row == self.cursor {
143 execute!(output, SetAttribute(Attribute::Reverse))?;
144 }
145 execute!(
146 output,
147 Print(label),
148 SetAttribute(Attribute::Reset),
149 Print("\r\n")
150 )?;
151 }
152 output.flush()?;
153 Ok(())
154 }
155}
156
157pub fn select_updates(pending: &Pending) -> Result<Pending> {
158 let _screen = Screen::open()?;
159 let mut selector = Selector::new(pending);
160 if selector.rows.is_empty() {
161 return Ok(Vec::new());
162 }
163 loop {
164 selector.render(pending)?;
165 match read_key()?.code {
166 KeyCode::Up => {
167 selector.cursor = (selector.cursor + selector.rows.len() - 1) % selector.rows.len()
168 }
169 KeyCode::Down => selector.cursor = (selector.cursor + 1) % selector.rows.len(),
170 KeyCode::Char(' ') => selector.toggle(pending),
171 KeyCode::Esc => return Ok(Vec::new()),
172 KeyCode::Enter => break,
173 _ => (),
174 }
175 }
176 Ok(pending
177 .iter()
178 .enumerate()
179 .filter_map(|(project_index, (project, updates))| {
180 let chosen: Vec<_> = updates
181 .iter()
182 .enumerate()
183 .filter(|(index, _)| selector.selected.contains(&(project_index, *index)))
184 .map(|(_, update)| update.clone())
185 .collect();
186 (!chosen.is_empty()).then(|| (project.clone(), chosen))
187 })
188 .collect())
189}
190
191struct Confirmation;
192
193impl Drop for Confirmation {
194 fn drop(&mut self) {
195 let _ = terminal::disable_raw_mode();
196 let _ = execute!(
197 stdout(),
198 MoveToColumn(0),
199 Clear(ClearType::CurrentLine),
200 Show
201 );
202 }
203}
204
205pub fn confirm_changes() -> Result<bool> {
206 stdout().flush()?;
208 terminal::enable_raw_mode()?;
209 let _guard = Confirmation;
210 let mut accepted = false;
211 loop {
212 execute!(
213 stdout(),
214 MoveToColumn(0),
215 Clear(ClearType::CurrentLine),
216 Print(format!(
217 "Apply these changes? {} (Arrows/Space: choose | Enter: confirm | y/n | Esc: cancel)",
218 if accepted { "No / [Yes]" } else { "[No] / Yes" }
219 ))
220 )?;
221 stdout().flush()?;
222 match read_key()?.code {
223 KeyCode::Char('y' | 'Y') => return Ok(true),
224 KeyCode::Esc | KeyCode::Char('n' | 'N') => return Ok(false),
225 KeyCode::Enter => return Ok(accepted),
226 KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down | KeyCode::Char(' ') => {
227 accepted = !accepted
228 }
229 _ => (),
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::dependencies::{Dependency, Update};
238 use crate::workspace::Project;
239
240 #[test]
241 fn project_toggle_and_occurrence_toggle_are_independent() {
242 let update = Update {
243 dependency: Dependency {
244 requirement: "demo>=1".into(),
245 section: vec!["project".into(), "dependencies".into()],
246 index: 0,
247 },
248 replacement: "demo>=2".into(),
249 version: "2".parse().unwrap(),
250 release_link: None,
251 project_link: None,
252 };
253 let pending = vec![(
254 Project {
255 path: "pyproject.toml".into(),
256 document: Default::default(),
257 original: Vec::new(),
258 },
259 vec![update.clone(), update],
260 )];
261 let mut selector = Selector::new(&pending);
262 assert!(selector.selected.is_empty());
263 selector.toggle(&pending);
264 assert_eq!(selector.selected.len(), 2);
265 selector.cursor = 1;
266 selector.toggle(&pending);
267 assert_eq!(selector.selected.len(), 1);
268 assert!(selector.selected.contains(&(0, 1)));
269 selector.cursor = 0;
270 selector.toggle(&pending);
271 assert_eq!(selector.selected.len(), 2);
272 selector.toggle(&pending);
273 assert!(selector.selected.is_empty());
274 }
275}