1use log::{debug, info};
2use ratatui::prelude::*;
3use std::io::{self, Stdout};
4use std::path::Path;
5use std::time::Duration;
6
7use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode};
8use crossterm::execute;
9use crossterm::terminal::{
10 Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
11};
12use ratatui::backend::CrosstermBackend;
13use ratatui::layout::{Constraint, Direction, Layout};
14use ratatui::{Frame, Terminal};
15
16use super::popups::run_script::RunScriptPopup;
17use super::state::{App, AppMode, UiOptions};
18use super::widgets::category_list::render_category_list;
19use super::widgets::header::render_header;
20use super::widgets::script_list::render_script_list;
21use super::widgets::status_bar::render_status_bar;
22use crate::error::Result;
23use crate::ui::popups;
24
25fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
26 let popup_layout = Layout::default()
27 .direction(Direction::Vertical)
28 .constraints([
29 Constraint::Percentage((100 - percent_y) / 2),
30 Constraint::Percentage(percent_y),
31 Constraint::Percentage((100 - percent_y) / 2),
32 ])
33 .split(r);
34
35 Layout::default()
36 .direction(Direction::Horizontal)
37 .constraints([
38 Constraint::Percentage((100 - percent_x) / 2),
39 Constraint::Percentage(percent_x),
40 Constraint::Percentage((100 - percent_x) / 2),
41 ])
42 .split(popup_layout[1])[1]
43}
44
45fn render_normal_ui(f: &mut Frame, app: &mut App, _options: &UiOptions) {
46 let area = Layout::default()
47 .direction(Direction::Vertical)
48 .margin(1)
49 .constraints([Constraint::Min(0)])
50 .split(f.area())[0];
51
52 let chunks = Layout::default()
53 .direction(Direction::Vertical)
54 .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(1)])
55 .split(area);
56
57 render_header(f, app, chunks[0]);
58
59 let main_chunks = Layout::default()
60 .direction(Direction::Horizontal)
61 .constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
62 .split(chunks[1]);
63
64 app.script_panel_area = main_chunks[1];
65
66 render_category_list(f, app, main_chunks[0]);
67 render_script_list(f, app, main_chunks[1]);
68
69 render_status_bar(f, app, chunks[2]);
70}
71
72fn ui(f: &mut Frame, app: &mut App, options: &UiOptions) {
73 render_normal_ui(f, app, options);
74
75 match app.mode {
76 AppMode::RunScript => {
77 if let Some(popup) = &mut app.run_script_popup {
78 let area = app.script_panel_area;
79 let popup_area = centered_rect(83, 80, area);
80 f.render_widget(popup, popup_area);
81 }
82 }
83 AppMode::Search => {
84 let area = app.script_panel_area;
85 let popup_width = std::cmp::min(70, area.width.saturating_sub(8));
86 let popup_height = std::cmp::min(16, area.height.saturating_sub(6));
87
88 let percent_x = if area.width > 0 { (popup_width * 100) / area.width } else { 100 };
89 let percent_y = if area.height > 0 { (popup_height * 100) / area.height } else { 100 };
90
91 let popup_area = centered_rect(percent_x, percent_y, area);
92 popups::search::render_search_popup(f, app, popup_area);
93 }
94 AppMode::Confirm => {
95 let area = app.script_panel_area;
96 let popup_width = std::cmp::min(60, area.width.saturating_sub(8));
97 let popup_height = if app.multi_select.enabled && !app.multi_select.scripts.is_empty() {
98 std::cmp::min(20, area.height.saturating_sub(6))
99 } else {
100 11
101 };
102
103 let percent_x = if area.width > 0 { (popup_width * 100) / area.width } else { 100 };
104 let percent_y = if area.height > 0 { (popup_height * 100) / area.height } else { 100 };
105
106 let popup_area = centered_rect(percent_x, percent_y, area);
107 popups::confirmation::render_confirmation_popup(f, app, popup_area);
108 }
109 AppMode::Help => {
110 let area = app.script_panel_area;
111 let popup_width = std::cmp::min(80, area.width.saturating_sub(4));
112 let popup_height = std::cmp::min(20, area.height.saturating_sub(4));
113
114 let percent_x = if area.width > 0 { (popup_width * 100) / area.width } else { 100 };
115 let percent_y = if area.height > 0 { (popup_height * 100) / area.height } else { 100 };
116
117 let popup_area = centered_rect(percent_x, percent_y, area);
118 let max_scroll = popups::help::render_help_popup(f, app, popup_area);
119 app.help.max_scroll = max_scroll;
120 }
121 AppMode::Preview => {
122 let area = app.script_panel_area;
123 let popup_area = centered_rect(83, 80, area);
124 popups::preview::render_preview_popup(f, app, popup_area);
125 }
126 AppMode::Description => {
127 let area = app.script_panel_area;
128 let popup_area = centered_rect(80, 80, area);
129 popups::description::render_description_popup(f, &mut *app, popup_area);
130 }
131 AppMode::Normal => {
132 }
134 AppMode::RootWarning => {
135 let area = app.script_panel_area;
136 let popup_area = centered_rect(80, 50, area);
137 popups::root_warning::render_root_warning_popup(f, app, popup_area);
138 }
139 }
140}
141
142fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
143 enable_raw_mode()?;
144 let mut stdout = io::stdout();
145 execute!(stdout, EnterAlternateScreen, EnableMouseCapture, Clear(ClearType::All))?;
146 let backend = CrosstermBackend::new(stdout);
147 Terminal::new(backend).map_err(Into::into)
148}
149
150fn cleanup_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
151 disable_raw_mode()?;
152 execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
153 terminal.show_cursor()?;
154 Ok(())
155}
156
157pub fn run_ui_with_options(modules_dir: &Path, options: UiOptions) -> Result<()> {
158 if options.log_mode {
159 info!("UI initialization started");
160 }
161
162 let mut terminal = setup_terminal()?;
163 let mut app = App::new(&options);
164 app.log_mode = options.log_mode;
165 app.modules_dir = modules_dir.to_path_buf();
166
167 if options.log_mode {
168 info!("Loading scripts from modules directory");
169 }
170
171 app.load_scripts(modules_dir)?;
172
173 if options.log_mode {
174 info!(
175 "Loaded {} scripts in {} categories",
176 app.all_scripts.values().map(|v| v.len()).sum::<usize>(),
177 app.categories.items.len()
178 );
179 }
180
181 while !app.quit {
182 terminal.autoresize()?;
183 terminal.draw(|f| ui(f, &mut app, &options))?;
184
185 let poll_duration = if app.mode == AppMode::RunScript {
186 Duration::from_millis(16)
187 } else {
188 Duration::from_millis(100)
189 };
190
191 if event::poll(poll_duration)?
192 && let Ok(event) = event::read()
193 {
194 handle_event(&mut app, event, &options)?;
195 }
196 }
197
198 cleanup_terminal(&mut terminal)?;
199
200 if options.log_mode {
201 info!("UI terminated normally");
202 }
203
204 Ok(())
205}
206
207fn handle_event(app: &mut App, event: Event, options: &UiOptions) -> Result<()> {
208 match event {
209 Event::Key(key) => {
210 if options.log_mode {
211 let key_name = match key.code {
212 KeyCode::Char(c) => format!("Char('{c}')"),
213 _ => format!("{:?}", key.code),
214 };
215 debug!("Key pressed: {} in mode: {:?}", key_name, app.mode);
216 }
217
218 if app.mode == AppMode::RunScript {
219 if let Some(popup) = &mut app.run_script_popup {
220 match popup.handle_key_event(key) {
221 crate::ui::popups::run_script::PopupEvent::Close => {
222 app.run_script_popup = None;
223 if let Some(script_path) = app.script_execution_queue.pop() {
224 let next_popup = RunScriptPopup::new(
225 script_path,
226 app.log_mode,
227 app.theme.clone(),
228 );
229 app.run_script_popup = Some(next_popup);
230 } else {
231 app.mode = AppMode::Normal;
232 }
233 }
234 crate::ui::popups::run_script::PopupEvent::None => {}
235 }
236 }
237 } else {
238 match app.mode {
239 AppMode::Normal => app.handle_key_normal_mode(key),
240 AppMode::Preview => app.handle_key_preview_mode(key),
241 AppMode::Search => app.handle_search_input(key),
242 AppMode::Confirm => app.handle_key_confirmation_mode(key),
243 AppMode::Help => app.handle_key_help_mode(key),
244 AppMode::Description => app.handle_key_description_mode(key),
245 AppMode::RootWarning => app.handle_key_root_warning_mode(key),
246 AppMode::RunScript => {
247 }
249 }
250 }
251 }
252 Event::Mouse(mouse_event) => {
253 if options.log_mode {
254 debug!("Mouse event: {mouse_event:?}");
255 }
256 app.handle_mouse(mouse_event);
257 }
258 _ => {}
259 }
260 Ok(())
261}