1use super::screens;
4use super::state::{SetupState, SetupStep};
5use crate::banner;
6use ratatui::layout::{Alignment, Constraint, Layout, Rect};
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::{Block, Borders, Paragraph};
10use ratatui::Frame;
11
12pub fn render(state: &SetupState, frame: &mut Frame) {
14 let area = frame.area();
15 let height = area.height;
16 let path_popup_active = state.step == SetupStep::SelectPath && state.path_browse_mode;
17
18 let show_banner = height >= 30;
20 let show_progress = height >= 20;
21
22 let mut constraints = Vec::new();
23 if show_banner {
24 constraints.push(Constraint::Length(6)); }
26 constraints.push(Constraint::Length(2)); if show_progress {
28 constraints.push(Constraint::Length(4)); }
30 constraints.push(Constraint::Min(10)); constraints.push(Constraint::Length(2)); let chunks = Layout::vertical(constraints).split(area);
34
35 let mut idx = 0;
36
37 if show_banner {
39 if state.step == SetupStep::Complete {
40 let phase = (state.tick_count % 100) as f64 / 100.0;
41 banner::render_animated_banner(frame, chunks[idx], phase);
42 } else {
43 banner::render_banner(frame, chunks[idx]);
44 }
45 idx += 1;
46 }
47
48 let title_text = if state.is_first_setup {
50 "Workspace Setup"
51 } else {
52 "New Workspace"
53 };
54 let title = Paragraph::new(title_text)
55 .style(
56 Style::default()
57 .fg(if path_popup_active {
58 Color::DarkGray
59 } else {
60 Color::White
61 })
62 .add_modifier(Modifier::BOLD),
63 )
64 .alignment(Alignment::Center);
65 frame.render_widget(title, chunks[idx]);
66 idx += 1;
67
68 if show_progress {
70 let progress_block = Block::default()
71 .borders(Borders::ALL)
72 .border_style(Style::default().fg(Color::DarkGray));
73 let progress_inner = progress_block.inner(chunks[idx]);
74 frame.render_widget(progress_block, chunks[idx]);
75 render_step_progress(state, frame, progress_inner, path_popup_active);
76 idx += 1;
77 }
78
79 let content_area = chunks[idx];
81 let content_block = Block::default()
82 .borders(Borders::ALL)
83 .border_style(Style::default().fg(Color::DarkGray));
84 let content_inner = content_block.inner(content_area);
85 frame.render_widget(content_block, content_area);
86 idx += 1;
87
88 match state.step {
89 SetupStep::Requirements => screens::requirements::render(state, frame, content_inner),
90 SetupStep::SelectProvider => screens::provider::render(state, frame, content_inner),
91 SetupStep::Authenticate => screens::auth::render(state, frame, content_inner),
92 SetupStep::SelectOrgs => screens::orgs::render(state, frame, content_inner),
93 SetupStep::SelectPath => screens::path::render(state, frame, content_inner),
94 SetupStep::Confirm => screens::confirm::render(state, frame, content_inner),
95 SetupStep::Complete => screens::complete::render(state, frame, content_inner),
96 }
97
98 render_status_bar(state, frame, chunks[idx]);
100}
101
102fn render_step_progress(state: &SetupState, frame: &mut Frame, area: Rect, dimmed: bool) {
104 let steps = ["Reqs", "Provider", "Auth", "Orgs", "Path", "Save"];
105 let current = state.step_number(); let green = if dimmed {
108 Style::default().fg(Color::DarkGray)
109 } else {
110 Style::default().fg(Color::Rgb(21, 128, 61))
111 };
112 let green_bold = green.add_modifier(Modifier::BOLD);
113 let cyan_bold = if dimmed {
114 Style::default()
115 .fg(Color::DarkGray)
116 .add_modifier(Modifier::BOLD)
117 } else {
118 Style::default()
119 .fg(Color::Cyan)
120 .add_modifier(Modifier::BOLD)
121 };
122 let dim = Style::default().fg(Color::DarkGray);
123
124 let segments = Layout::horizontal([
126 Constraint::Ratio(3, 28),
127 Constraint::Ratio(2, 28),
128 Constraint::Ratio(3, 28),
129 Constraint::Ratio(2, 28),
130 Constraint::Ratio(3, 28),
131 Constraint::Ratio(2, 28),
132 Constraint::Ratio(3, 28),
133 Constraint::Ratio(2, 28),
134 Constraint::Ratio(3, 28),
135 Constraint::Ratio(2, 28),
136 Constraint::Ratio(3, 28),
137 ])
138 .split(area);
139
140 let mut node_spans: Vec<Span> = Vec::new();
141 let mut label_spans: Vec<Span> = Vec::new();
142
143 for (i, label) in steps.iter().enumerate() {
144 let step_num = i + 1;
145 let node_width = segments[i * 2].width as usize;
146 let node_style = if step_num < current || state.step == SetupStep::Complete {
147 green_bold
148 } else if step_num == current {
149 cyan_bold
150 } else {
151 dim
152 };
153 let label_style = if step_num < current || state.step == SetupStep::Complete {
154 green
155 } else if step_num == current {
156 cyan_bold
157 } else {
158 dim
159 };
160
161 let node_text = if step_num < current || state.step == SetupStep::Complete {
162 "(\u{2713})".to_string()
163 } else {
164 format!("({})", step_num)
165 };
166
167 node_spans.push(Span::styled(
168 center_cell(&node_text, node_width),
169 node_style,
170 ));
171 label_spans.push(Span::styled(center_cell(label, node_width), label_style));
172
173 if i < steps.len() - 1 {
174 let connector_width = segments[i * 2 + 1].width as usize;
175 let connector_done = step_num < current || state.step == SetupStep::Complete;
176 let connector_style = if connector_done { green } else { dim };
177 node_spans.push(Span::styled(
178 connector_cell(connector_width, connector_done),
179 connector_style,
180 ));
181 label_spans.push(Span::raw(" ".repeat(connector_width)));
182 }
183 }
184
185 let widget = Paragraph::new(vec![Line::from(node_spans), Line::from(label_spans)]);
186 frame.render_widget(widget, area);
187}
188
189fn center_cell(text: &str, width: usize) -> String {
190 if width == 0 {
191 return String::new();
192 }
193
194 let text = if text.chars().count() > width {
195 text.chars().take(width).collect::<String>()
196 } else {
197 text.to_string()
198 };
199 let text_width = text.chars().count();
200 let left_pad = (width - text_width) / 2;
201 let right_pad = width - text_width - left_pad;
202 format!("{}{}{}", " ".repeat(left_pad), text, " ".repeat(right_pad))
203}
204
205fn connector_cell(width: usize, completed: bool) -> String {
206 if width == 0 {
207 return String::new();
208 }
209
210 if completed {
211 return "\u{2501}".repeat(width);
212 }
213
214 let mut out = String::with_capacity(width);
216 for i in 0..width {
217 if i % 2 == 0 {
218 out.push('\u{2500}');
219 } else {
220 out.push(' ');
221 }
222 }
223 out
224}
225
226fn render_status_bar(state: &SetupState, frame: &mut Frame, area: Rect) {
228 let path_popup_active = state.step == SetupStep::SelectPath && state.path_browse_mode;
229 let blue = if path_popup_active {
230 Style::default()
231 .fg(Color::DarkGray)
232 .add_modifier(Modifier::BOLD)
233 } else {
234 Style::default()
235 .fg(Color::Rgb(37, 99, 235))
236 .add_modifier(Modifier::BOLD)
237 };
238 let dim = Style::default().fg(Color::DarkGray);
239 let yellow = if path_popup_active {
240 Style::default().fg(Color::DarkGray)
241 } else {
242 Style::default().fg(Color::Yellow)
243 };
244
245 let top_center = match state.step {
246 SetupStep::Requirements => {
247 if state.checks_loading {
248 vec![Span::styled("Checking system requirements...", yellow)]
249 } else if state.check_results.iter().any(|r| r.critical && !r.passed) {
250 vec![Span::styled(
251 "Fix critical requirements to continue",
252 yellow,
253 )]
254 } else if !state.check_results.is_empty() {
255 vec![
256 Span::styled("[Enter]", blue),
257 Span::styled(" Continue to setup", dim),
258 ]
259 } else {
260 vec![Span::styled("Preparing...", dim)]
261 }
262 }
263 SetupStep::SelectProvider => vec![
264 Span::styled("[↑] [↓]", blue),
265 Span::styled(" Select provider", dim),
266 ],
267 SetupStep::Authenticate => {
268 use super::state::AuthStatus;
269 match &state.auth_status {
270 AuthStatus::Pending | AuthStatus::Failed(_) => vec![
271 Span::styled("[Enter]", blue),
272 Span::styled(" Authenticate", dim),
273 ],
274 AuthStatus::Success => vec![
275 Span::styled("[Enter]", blue),
276 Span::styled(" Continue", dim),
277 ],
278 AuthStatus::Checking => vec![Span::styled("Authenticating...", yellow)],
279 }
280 }
281 SetupStep::SelectPath => {
282 if state.path_browse_mode {
283 vec![Span::styled("Folder popup active", dim)]
284 } else {
285 vec![
286 Span::styled("[b]", blue),
287 Span::styled(" Open Folder Navigator", dim),
288 ]
289 }
290 }
291 SetupStep::SelectOrgs => {
292 if state.org_loading {
293 vec![Span::styled("Discovering organizations...", yellow)]
294 } else {
295 vec![
296 Span::styled("[Space]", blue),
297 Span::styled(" Toggle ", dim),
298 Span::styled("[a]", blue),
299 Span::styled(" All ", dim),
300 Span::styled("[n]", blue),
301 Span::styled(" None", dim),
302 ]
303 }
304 }
305 SetupStep::Confirm => vec![
306 Span::styled("[Enter]", blue),
307 Span::styled(" Save workspace", dim),
308 ],
309 SetupStep::Complete => vec![
310 Span::styled("[Enter]", blue),
311 Span::styled(" Dashboard ", dim),
312 Span::styled("[s]", blue),
313 Span::styled(" Sync Now", dim),
314 ],
315 };
316
317 let bottom_left = if path_popup_active {
318 vec![
319 Span::styled("[Esc]", blue),
320 Span::styled(" Close Popup", dim),
321 ]
322 } else {
323 vec![
324 Span::styled("[q]", blue),
325 Span::styled(" Quit ", dim),
326 Span::styled("[Esc]", blue),
327 Span::styled(" Back", dim),
328 ]
329 };
330
331 let bottom_right = match state.step {
332 SetupStep::SelectProvider | SetupStep::SelectOrgs => vec![
333 Span::styled("[↑] [↓]", blue),
334 Span::styled(" Move ", dim),
335 Span::styled("[←] [→]", blue),
336 Span::styled(" Step ", dim),
337 Span::styled("[Enter]", blue),
338 Span::styled(" Next Step", dim),
339 ],
340 SetupStep::SelectPath => {
341 if state.path_browse_mode {
342 vec![Span::styled("Use popup arrows and Enter", dim)]
343 } else {
344 vec![
345 Span::styled("[←]", blue),
346 Span::styled(" Back Step ", dim),
347 Span::styled("[Enter]", blue),
348 Span::styled(" Next Step ", dim),
349 Span::styled("[b]", blue),
350 Span::styled(" Browse folders", dim),
351 ]
352 }
353 }
354 _ => vec![
355 Span::styled("[←] [→]", blue),
356 Span::styled(" Step ", dim),
357 Span::styled("[Enter]", blue),
358 Span::styled(" Next Step", dim),
359 ],
360 };
361
362 let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(area);
363 let step_num = state.step_number();
364 let step_text = if step_num > 0 {
365 Some(format!("Step {} of {}", step_num, SetupState::TOTAL_STEPS))
366 } else {
367 None
368 };
369
370 let step_width = step_text
371 .as_ref()
372 .map(|s| s.chars().count() as u16 + 1)
373 .unwrap_or(0);
374 let top_cols =
375 Layout::horizontal([Constraint::Length(step_width), Constraint::Min(0)]).split(rows[0]);
376
377 if let Some(text) = step_text {
378 frame.render_widget(
379 Paragraph::new(Line::from(Span::styled(text, dim))),
380 top_cols[0],
381 );
382 }
383 frame.render_widget(
384 Paragraph::new(Line::from(top_center)).alignment(Alignment::Center),
385 top_cols[1],
386 );
387
388 let bottom_cols =
389 Layout::horizontal([Constraint::Length(24), Constraint::Min(0)]).split(rows[1]);
390 frame.render_widget(Paragraph::new(Line::from(bottom_left)), bottom_cols[0]);
391 frame.render_widget(
392 Paragraph::new(Line::from(bottom_right)).right_aligned(),
393 bottom_cols[1],
394 );
395}
396
397#[cfg(test)]
398#[path = "ui_tests.rs"]
399mod tests;