Skip to main content

git_same/setup/screens/
orgs.rs

1//! Step 3: Organization selection screen with summary and proportional bars.
2
3use 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
10/// Braille spinner frames (same as auth).
11const 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    // Title
20    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    // Content
56    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            // Proportional bar
122            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            // Percentage
144            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;