git_same/setup/screens/
orgs.rs1use crate::setup::state::SetupState;
4use ratatui::layout::Rect;
5use ratatui::style::{Color, Modifier, Style};
6use ratatui::text::{Line, Span};
7use ratatui::widgets::Paragraph;
8use ratatui::Frame;
9
10const SPINNER: [char; 10] = [
12 '\u{280b}', '\u{2819}', '\u{2839}', '\u{2838}', '\u{283c}', '\u{2834}', '\u{2826}', '\u{2827}',
13 '\u{2807}', '\u{280f}',
14];
15
16pub fn render(state: &SetupState, frame: &mut Frame, area: Rect) {
17 let mut lines: Vec<Line> = Vec::new();
18
19 let selected_count = state.orgs.iter().filter(|o| o.selected).count();
21 let total_repos: usize = state.orgs.iter().map(|o| o.repo_count).sum();
22 let selected_repos: usize = state
23 .orgs
24 .iter()
25 .filter(|o| o.selected)
26 .map(|o| o.repo_count)
27 .sum();
28
29 lines.push(Line::from(Span::styled(
30 " Select organizations to sync",
31 Style::default()
32 .fg(Color::Cyan)
33 .add_modifier(Modifier::BOLD),
34 )));
35
36 if !state.orgs.is_empty() {
37 lines.push(Line::from(vec![
38 Span::styled(
39 format!(" {} of {} selected", selected_count, state.orgs.len()),
40 Style::default().fg(Color::DarkGray),
41 ),
42 Span::styled(" \u{00b7} ", Style::default().fg(Color::DarkGray)),
43 Span::styled(
44 format!("{} repos", selected_repos),
45 Style::default().fg(Color::Rgb(21, 128, 61)),
46 ),
47 Span::styled(
48 format!(" of {} total", total_repos),
49 Style::default().fg(Color::DarkGray),
50 ),
51 ]));
52 }
53 lines.push(Line::raw(""));
54
55 if state.org_loading {
57 let spinner_char = SPINNER[(state.tick_count as usize) % SPINNER.len()];
58 lines.push(Line::from(Span::styled(
59 format!(" {} Discovering organizations...", spinner_char),
60 Style::default().fg(Color::Yellow),
61 )));
62 } else if let Some(ref err) = state.org_error {
63 lines.push(Line::from(Span::styled(
64 " \u{2717} Failed to discover organizations",
65 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
66 )));
67 lines.push(Line::raw(""));
68 lines.push(Line::from(Span::styled(
69 format!(" {}", err),
70 Style::default().fg(Color::White),
71 )));
72 lines.push(Line::raw(""));
73 lines.push(Line::from(Span::styled(
74 " Press Enter to retry",
75 Style::default().fg(Color::Yellow),
76 )));
77 } else if state.orgs.is_empty() {
78 lines.push(Line::from(Span::styled(
79 " No organizations found. Press Enter to continue.",
80 Style::default().fg(Color::DarkGray),
81 )));
82 lines.push(Line::from(Span::styled(
83 " Your personal repos will still be synced.",
84 Style::default().fg(Color::DarkGray),
85 )));
86 } else {
87 let max_repos = state.orgs.iter().map(|o| o.repo_count).max().unwrap_or(1);
88 let bar_width = 16;
89
90 for (i, org) in state.orgs.iter().enumerate() {
91 let is_selected = i == state.org_index;
92 let marker = if is_selected { " \u{25b8}" } else { " " };
93 let checkbox = if org.selected { "[x]" } else { "[ ]" };
94
95 let green = Color::Rgb(21, 128, 61);
96
97 let (marker_style, name_style, count_style) = if is_selected {
98 (
99 Style::default()
100 .fg(Color::Cyan)
101 .add_modifier(Modifier::BOLD),
102 Style::default()
103 .fg(Color::Cyan)
104 .add_modifier(Modifier::BOLD),
105 Style::default().fg(Color::DarkGray),
106 )
107 } else if org.selected {
108 (
109 Style::default().fg(green),
110 Style::default().fg(Color::White),
111 Style::default().fg(Color::DarkGray),
112 )
113 } else {
114 (
115 Style::default().fg(Color::White),
116 Style::default().fg(Color::White),
117 Style::default().fg(Color::DarkGray),
118 )
119 };
120
121 let filled = if max_repos > 0 {
123 (org.repo_count * bar_width) / max_repos
124 } else {
125 0
126 }
127 .max(if org.repo_count > 0 { 1 } else { 0 });
128 let empty = bar_width - filled;
129
130 let bar_color = if org.selected { green } else { Color::DarkGray };
131
132 let mut spans = vec![
133 Span::styled(format!("{} {} ", marker, checkbox), marker_style),
134 Span::styled(format!("{:<20}", org.name), name_style),
135 Span::styled(format!("{:>4} repos ", org.repo_count), count_style),
136 Span::styled("\u{2588}".repeat(filled), Style::default().fg(bar_color)),
137 Span::styled(
138 "\u{2591}".repeat(empty),
139 Style::default().fg(Color::DarkGray),
140 ),
141 ];
142
143 if total_repos > 0 {
145 let pct = (org.repo_count * 100) / total_repos;
146 spans.push(Span::styled(
147 format!(" {:>3}%", pct),
148 Style::default().fg(Color::DarkGray),
149 ));
150 }
151
152 lines.push(Line::from(spans));
153 }
154 }
155
156 let widget = Paragraph::new(lines);
157 frame.render_widget(widget, area);
158}
159
160#[cfg(test)]
161#[path = "orgs_tests.rs"]
162mod tests;