upgate_presentation/tui/selection/
mod.rs1mod input;
2mod render;
3mod screen;
4
5use std::io;
6use std::sync::mpsc::{Receiver, TryRecvError};
7use std::time::Duration;
8
9use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture};
10use crossterm::execute;
11use crossterm::terminal::{
12 EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
13};
14use ratatui::Terminal;
15use ratatui::backend::CrosstermBackend;
16use ratatui::layout::Rect;
17use upgate_domain::{ManagerId, SelectedItem, UpdateSelectionPolicy, VersionPolicy};
18
19use self::input::handle_selection_ready_events;
20use self::render::draw_selection;
21use self::screen::InteractiveSelectionScreen;
22use crate::SelectionView;
23
24const MAX_DRAINED_INPUT_EVENTS: usize = 256;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct InteractiveManagerSelectionDraft {
28 pub manager_id: ManagerId,
29 pub selected_items: Vec<SelectedItem>,
30 pub selection_policy: UpdateSelectionPolicy,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum InteractiveSelectionOutcome {
35 Confirmed(Vec<InteractiveManagerSelectionDraft>),
36 Cancelled,
37 Interrupted,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum SelectionInput {
42 Up,
43 Down,
44 NextTab,
45 PreviousTab,
46 ToggleCurrent,
47 SelectVisible,
48 SelectNoneVisible,
49 ToggleViewAll,
50 OpenTargetPicker,
51 PickerUp,
52 PickerDown,
53 PickerPreviousRow,
54 PickerNextRow,
55 PickerConfirm,
56 PickerCancel,
57 RecommendedTarget,
58 Confirm,
59 Cancel,
60 Interrupt,
61 Ignore,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum SelectionControl {
66 Continue,
67 Confirm,
68 Cancel,
69 Interrupt,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum InteractiveSelectionPlanningEvent {
74 ManagerStarted {
75 manager_id: ManagerId,
76 },
77 CommandStarted {
78 command: String,
79 },
80 ManagerReady {
81 view: SelectionView,
82 selection_policy: UpdateSelectionPolicy,
83 version_policy: VersionPolicy,
84 },
85 ManagerError {
86 manager_id: ManagerId,
87 detail: String,
88 },
89 PlanningFailed {
90 detail: String,
91 },
92 Finished,
93}
94
95#[expect(clippy::needless_pass_by_value)]
102pub fn run_interactive_selection_with_planning_events(
103 manager_ids: Vec<ManagerId>,
104 planning_events: Receiver<InteractiveSelectionPlanningEvent>,
105 trace_commands: bool,
106) -> io::Result<InteractiveSelectionOutcome> {
107 run_interactive_selection_screen(
108 InteractiveSelectionScreen::from_manager_ids(manager_ids).trace_commands(trace_commands),
109 &planning_events,
110 )
111}
112
113fn run_interactive_selection_screen(
114 mut screen: InteractiveSelectionScreen,
115 planning_events: &Receiver<InteractiveSelectionPlanningEvent>,
116) -> io::Result<InteractiveSelectionOutcome> {
117 let mut stdout = io::stdout();
118 enable_raw_mode()?;
119 if let Err(err) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) {
120 let _ = execute!(io::stdout(), DisableMouseCapture, LeaveAlternateScreen);
121 let _ = disable_raw_mode();
122 return Err(err);
123 }
124 let backend = CrosstermBackend::new(stdout);
125 let mut terminal = match Terminal::new(backend) {
126 Ok(terminal) => terminal,
127 Err(err) => {
128 let _ = execute!(io::stdout(), DisableMouseCapture, LeaveAlternateScreen);
129 let _ = disable_raw_mode();
130 return Err(err);
131 }
132 };
133
134 let result = run_selection_loop(&mut terminal, &mut screen, planning_events);
135
136 let cleanup = cleanup_selection_terminal(&mut terminal);
137 match (result, cleanup) {
138 (Ok(outcome), Ok(())) => Ok(outcome),
139 (Err(err), Ok(()) | Err(_)) | (Ok(_), Err(err)) => Err(err),
140 }
141}
142
143fn cleanup_selection_terminal(
144 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
145) -> io::Result<()> {
146 let raw_mode = disable_raw_mode();
147 let screen = execute!(
148 terminal.backend_mut(),
149 DisableMouseCapture,
150 LeaveAlternateScreen
151 );
152 let cursor = terminal.show_cursor();
153 raw_mode?;
154 screen?;
155 cursor
156}
157
158fn run_selection_loop(
159 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
160 screen: &mut InteractiveSelectionScreen,
161 planning_events: &Receiver<InteractiveSelectionPlanningEvent>,
162) -> io::Result<InteractiveSelectionOutcome> {
163 loop {
164 drain_planning_events(screen, planning_events);
165 terminal.draw(|frame| draw_selection(frame, screen))?;
166 if !event::poll(Duration::from_millis(100))? {
167 screen.tick();
168 continue;
169 }
170 drain_planning_events(screen, planning_events);
171 let size = terminal.size()?;
172 let area = Rect::new(0, 0, size.width, size.height);
173 let control = handle_selection_ready_events(screen, area)?.map_err(io::Error::other)?;
174 match control {
175 SelectionControl::Continue => {}
176 SelectionControl::Confirm => {
177 return Ok(InteractiveSelectionOutcome::Confirmed(
178 screen.selection_drafts(),
179 ));
180 }
181 SelectionControl::Cancel => return Ok(InteractiveSelectionOutcome::Cancelled),
182 SelectionControl::Interrupt => return Ok(InteractiveSelectionOutcome::Interrupted),
183 }
184 }
185}
186
187fn drain_planning_events(
188 screen: &mut InteractiveSelectionScreen,
189 planning_events: &Receiver<InteractiveSelectionPlanningEvent>,
190) {
191 loop {
192 match planning_events.try_recv() {
193 Ok(event) => screen.apply_planning_event(event),
194 Err(TryRecvError::Empty) => break,
195 Err(TryRecvError::Disconnected) => {
196 screen.planning_events_disconnected();
197 break;
198 }
199 }
200 }
201}