1use std::cell::Cell;
23
24use crate::component::{Component, Context, Flow, View};
25use crate::components::{Theme, fit, plain, question, text};
26use crate::event::{Button, Event, Key, MouseKind};
27use crate::item::Item;
28use crate::keymap::{Keymap, keys};
29use crate::policy::{LineIo, NotInteractive};
30
31pub fn reorder_keymap() -> Keymap {
33 Keymap::new("reorder")
34 .bind("pick", keys("enter"), "done: return the new order")
35 .bind(
36 "cancel",
37 keys("escape"),
38 "cancel (while carrying: put it back)",
39 )
40 .bind("up", keys("up k ctrl+p"), "focus up")
41 .bind("down", keys("down j ctrl+n"), "focus down")
42 .bind("page-up", keys("pageup"), "page up")
43 .bind("page-down", keys("pagedown"), "page down")
44 .bind("first", keys("home"), "first")
45 .bind("last", keys("end"), "last")
46 .bind("move-up", keys("alt+up ctrl+up K"), "move the item up")
47 .bind(
48 "move-down",
49 keys("alt+down ctrl+down J"),
50 "move the item down",
51 )
52 .bind(
53 "move-first",
54 keys("alt+home ctrl+home"),
55 "move the item to the top",
56 )
57 .bind(
58 "move-last",
59 keys("alt+end ctrl+end"),
60 "move the item to the bottom",
61 )
62 .bind("grab", keys("space"), "grab or drop the item")
63 .bind("reset", keys("ctrl+r"), "restore the first order")
64}
65
66pub fn move_item<T>(items: &mut [T], from: usize, to: usize) {
75 if from < to {
76 items[from..=to].rotate_left(1);
77 } else if to < from {
78 items[to..=from].rotate_right(1);
79 }
80}
81
82#[must_use = "a component does nothing until it is run or mounted"]
96pub struct Reorder<T> {
97 prompt: String,
98 items: Vec<Item<T>>,
99 order: Vec<usize>,
101 cursor: usize,
102 offset: usize,
103 height: usize,
104 theme: Theme,
105 mouse: bool,
106 grabbed: Option<usize>,
108 dragging: bool,
110 keymap: Keymap,
111 answer: Option<Option<String>>,
114 page: Cell<usize>,
116}
117
118impl<T> Reorder<T> {
119 pub fn new<I, V>(prompt: impl Into<String>, items: I) -> Reorder<T>
120 where
121 I: IntoIterator<Item = V>,
122 V: Into<Item<T>>,
123 {
124 let items: Vec<Item<T>> = items.into_iter().map(Into::into).collect();
125 Reorder {
126 prompt: prompt.into(),
127 order: (0..items.len()).collect(),
128 items,
129 cursor: 0,
130 offset: 0,
131 height: 10,
132 theme: Theme::default(),
133 mouse: false,
134 grabbed: None,
135 dragging: false,
136 keymap: reorder_keymap(),
137 answer: None,
138 page: Cell::new(10),
139 }
140 }
141
142 pub fn height(mut self, rows: usize) -> Self {
144 self.height = rows.max(1);
145 self
146 }
147
148 pub fn theme(mut self, theme: Theme) -> Self {
149 self.theme = theme;
150 self
151 }
152
153 pub fn focus(mut self, position: usize) -> Self {
155 self.cursor = position.min(self.items.len().saturating_sub(1));
156 self
157 }
158
159 pub fn with_mouse(mut self, on: bool) -> Self {
162 self.mouse = on;
163 self
164 }
165
166 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
169 self.keymap.rebind(action, keys);
170 self
171 }
172
173 pub fn items(&self) -> &[Item<T>] {
174 &self.items
175 }
176
177 pub fn order(&self) -> &[usize] {
180 &self.order
181 }
182
183 pub fn cursor(&self) -> usize {
185 self.cursor
186 }
187
188 pub fn is_grabbed(&self) -> bool {
190 self.grabbed.is_some()
191 }
192
193 fn follow(&mut self) {
194 self.offset = self.offset_for(self.page.get());
195 }
196
197 fn offset_for(&self, rows: usize) -> usize {
202 let rows = rows.max(1);
203 let offset = self.offset.min(self.order.len().saturating_sub(rows));
204 if self.cursor < offset {
205 self.cursor
206 } else if self.cursor >= offset + rows {
207 self.cursor + 1 - rows
208 } else {
209 offset
210 }
211 }
212
213 fn focus_on(&mut self, position: usize) {
215 self.cursor = position.min(self.order.len().saturating_sub(1));
216 self.follow();
217 }
218
219 fn carry_to(&mut self, position: usize) {
221 if self.order.is_empty() {
222 return;
223 }
224 let to = position.min(self.order.len() - 1);
225 move_item(&mut self.order, self.cursor, to);
226 self.focus_on(to);
227 }
228
229 fn by(&self, delta: isize) -> usize {
230 self.cursor.saturating_add_signed(delta)
231 }
232
233 fn finish(&mut self) -> Flow<Vec<usize>> {
234 let labels: Vec<&str> = self
235 .order
236 .iter()
237 .map(|&index| self.items[index].label.as_str())
238 .collect();
239 self.answer = Some(Some(labels.join(", ")));
240 self.grabbed = None;
241 Flow::Done(self.order.clone())
242 }
243
244 fn act(&mut self, action: &str) -> Flow<Vec<usize>> {
245 let page = self.page.get().max(1) as isize;
246 let carrying = self.grabbed.is_some();
247 let (target, carry) = match action {
249 "up" => (self.by(-1), carrying),
250 "down" => (self.by(1), carrying),
251 "page-up" => (self.by(-page), carrying),
252 "page-down" => (self.by(page), carrying),
253 "first" => (0, carrying),
254 "last" => (usize::MAX, carrying),
255 "move-up" => (self.by(-1), true),
256 "move-down" => (self.by(1), true),
257 "move-first" => (0, true),
258 "move-last" => (usize::MAX, true),
259 "pick" => return self.finish(),
260 "cancel" => {
261 if let Some(from) = self.grabbed.take() {
262 self.carry_to(from);
263 return Flow::Continue;
264 }
265 self.answer = Some(None);
266 return Flow::Cancel;
267 }
268 "grab" => {
269 self.grabbed = match self.grabbed {
270 Some(_) => None,
271 None if !self.order.is_empty() => Some(self.cursor),
272 None => None,
273 };
274 return Flow::Continue;
275 }
276 "reset" => {
277 let focused = self.order.get(self.cursor).copied();
279 self.order = (0..self.items.len()).collect();
280 self.grabbed = None;
281 if let Some(index) = focused {
282 self.focus_on(index);
283 }
284 return Flow::Continue;
285 }
286 _ => return Flow::Ignored,
287 };
288 if carry {
289 self.carry_to(target);
290 } else {
291 self.focus_on(target);
292 }
293 Flow::Continue
294 }
295
296 fn mouse_event(&mut self, kind: MouseKind, row: usize) -> Flow<Vec<usize>> {
297 let page = self.page.get().max(1);
298 let top = 1;
300 let shown = self.order.len().saturating_sub(self.offset).min(page);
301 match kind {
302 MouseKind::ScrollUp => return self.act("up"),
304 MouseKind::ScrollDown => return self.act("down"),
305 MouseKind::Down(Button::Left) if row >= top && row < top + shown => {
306 self.grabbed = None;
307 self.focus_on(self.offset + row - top);
308 self.dragging = true;
309 }
310 MouseKind::Drag(Button::Left) if self.dragging => {
311 let position = if row < top {
314 self.offset.saturating_sub(1)
315 } else if row >= top + shown {
316 self.offset + shown
317 } else {
318 self.offset + row - top
319 };
320 self.carry_to(position);
321 }
322 MouseKind::Up(_) => self.dragging = false,
323 _ => return Flow::Ignored,
324 }
325 Flow::Continue
326 }
327
328 fn event(&mut self, event: &Event, context: &Context<'_>) -> Flow<Vec<usize>> {
329 self.page.set(self.rows(context.height));
330 self.follow();
332 if let Some(mouse) = event.mouse() {
333 return self.mouse_event(mouse.kind, mouse.row as usize);
334 }
335 let Some(key) = event.key() else {
336 return Flow::Ignored;
337 };
338 match self.keymap.action(key).map(str::to_string) {
339 Some(action) => self.act(&action),
340 None => Flow::Ignored,
341 }
342 }
343
344 fn rows(&self, height: usize) -> usize {
347 self.height
348 .min(self.order.len().max(1))
349 .min(height.saturating_sub(2).max(1))
350 }
351
352 fn render_view(&self, context: &Context<'_>) -> View {
353 let width = context.width;
354 let theme = &self.theme;
355 let mut header = question(theme, &self.prompt);
356 if let Some(answer) = &self.answer {
357 header.push(match answer {
358 Some(answer) => text(answer.clone(), &theme.answer),
359 None => text("cancelled", &theme.hint),
360 });
361 return View::new(vec![fit(header, width)]);
362 }
363 let rows = self.rows(context.height);
364 self.page.set(rows);
365 let mut lines = vec![fit(header, width)];
366 let offset = self.offset_for(rows);
367 let reverse = rich::Style::parse("reverse").expect("a built-in style");
368 for (position, &index) in self.order.iter().enumerate().skip(offset).take(rows) {
369 let item = &self.items[index];
370 let focused = position == self.cursor;
371 let carried = focused && (self.grabbed.is_some() || self.dragging);
372 let mut line = if carried {
373 vec![text("↕ ", &theme.pointer_style)]
375 } else if focused {
376 vec![text(format!("{} ", theme.pointer), &theme.pointer_style)]
377 } else {
378 vec![plain(" ")]
379 };
380 line.push(if carried {
381 text(item.label.clone(), &theme.focused.combine(&reverse))
382 } else if focused {
383 text(item.label.clone(), &theme.focused)
384 } else {
385 plain(item.label.clone())
386 });
387 if let Some(description) = &item.description {
388 line.push(text(format!(" {description}"), &theme.hint));
389 }
390 lines.push(fit(line, width));
391 }
392 if self.order.is_empty() {
393 lines.push(vec![text(" nothing to order", &theme.hint)]);
394 }
395 let hint = match self.grabbed {
396 Some(_) => {
397 let label = &self.items[self.order[self.cursor]].label;
398 format!(
399 " moving {label} ({}/{}) · ↑↓ move · space drop · esc put back · enter done",
400 self.cursor + 1,
401 self.order.len()
402 )
403 }
404 None => format!(
405 " {}/{} · ↑↓ focus · alt+↑↓ or K/J move · space grab · enter done · esc cancel",
406 (self.cursor + 1).min(self.order.len()),
407 self.order.len()
408 ),
409 };
410 lines.push(fit(vec![text(hint, &theme.hint)], width));
411 View::new(lines)
412 }
413
414 fn ask(&mut self, io: &mut dyn LineIo) -> Result<Option<Vec<usize>>, NotInteractive> {
417 io.write(&format!("{}\n", self.prompt));
418 for (number, &index) in self.order.iter().enumerate() {
419 let item = &self.items[index];
420 let mut row = format!(" {}) {}", number + 1, item.label);
421 if let Some(description) = &item.description {
422 row.push_str(&format!(" — {description}"));
423 }
424 io.write(&format!("{row}\n"));
425 }
426 io.write("New order, as numbers (the rest keep theirs; empty keeps it): ");
427 let Some(line) = io.read_line() else {
429 return Ok(Some(self.order.clone()));
430 };
431 let order = parse_order(&line, self.order.len()).map_err(NotInteractive::Invalid)?;
432 self.order = order.into_iter().map(|at| self.order[at]).collect();
433 Ok(Some(self.order.clone()))
434 }
435}
436
437pub(crate) fn parse_order(line: &str, len: usize) -> Result<Vec<usize>, String> {
441 let mut named = Vec::new();
442 let mut used = vec![false; len];
444 for word in line
445 .split(|c: char| c == ',' || c.is_whitespace())
446 .filter(|word| !word.is_empty())
447 {
448 let number = word
449 .parse::<usize>()
450 .ok()
451 .filter(|number| (1..=len).contains(number))
452 .ok_or_else(|| format!("{word:?} is not a number from 1 to {len}"))?;
453 if std::mem::replace(&mut used[number - 1], true) {
454 return Err(format!("{number} is given twice"));
455 }
456 named.push(number - 1);
457 }
458 named.extend((0..len).filter(|&at| !used[at]));
459 Ok(named)
460}
461
462impl<T: Clone> Reorder<T> {
463 fn values(&self, order: &[usize]) -> Vec<T> {
464 order
465 .iter()
466 .map(|&index| self.items[index].value.clone())
467 .collect()
468 }
469}
470
471impl<T: Clone> Component for Reorder<T> {
472 type Output = Vec<T>;
473
474 fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<Vec<T>> {
475 match self.event(event, context) {
476 Flow::Done(order) => Flow::Done(self.values(&order)),
477 Flow::Cancel => Flow::Cancel,
478 Flow::Ignored => Flow::Ignored,
479 _ => Flow::Continue,
480 }
481 }
482
483 fn render(&self, context: &Context<'_>) -> View {
484 self.render_view(context)
485 }
486
487 fn mouse(&self) -> bool {
488 self.mouse
489 }
490
491 fn keymap(&self) -> Keymap {
492 self.keymap.clone()
493 }
494
495 fn default_value(&self) -> Option<Vec<T>> {
498 Some(self.values(&self.order))
499 }
500
501 fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<Vec<T>>, NotInteractive> {
502 Ok(self.ask(io)?.map(|order| self.values(&order)))
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn moves_shift_the_items_between() {
512 let mut items = [0, 1, 2, 3, 4];
513 move_item(&mut items, 4, 1);
514 assert_eq!(items, [0, 4, 1, 2, 3]);
515 move_item(&mut items, 1, 4);
516 assert_eq!(items, [0, 1, 2, 3, 4]);
517 move_item(&mut items, 2, 2);
518 assert_eq!(items, [0, 1, 2, 3, 4]);
519 }
520
521 #[test]
522 fn orders_parse_from_numbers() {
523 assert_eq!(parse_order("3 1 2", 3), Ok(vec![2, 0, 1]));
524 assert_eq!(parse_order("3,1", 4), Ok(vec![2, 0, 1, 3]));
525 assert_eq!(parse_order("", 3), Ok(vec![0, 1, 2]));
526 assert_eq!(parse_order(" 2 ,, ", 2), Ok(vec![1, 0]));
527 assert!(parse_order("4", 3).is_err());
528 assert!(parse_order("0", 3).is_err());
529 assert!(parse_order("1 1", 3).is_err());
530 assert!(parse_order("one", 3).is_err());
531 }
532}