bitbucket_cli/tui/views/
repos.rs

1/// Repository browser view
2use ratatui::{
3    layout::{Constraint, Direction, Layout, Rect},
4    style::{Color, Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, List, ListItem, Paragraph},
7    Frame,
8};
9
10use crate::models::Repository;
11use crate::tui::app::App;
12
13/// Repository list view
14pub struct ReposView;
15
16impl ReposView {
17    /// Render the repository browser
18    pub fn render(f: &mut Frame, app: &App, area: Rect) {
19        let chunks = Layout::default()
20            .direction(Direction::Horizontal)
21            .constraints([
22                Constraint::Percentage(60), // List
23                Constraint::Percentage(40), // Details
24            ])
25            .split(area);
26
27        Self::render_list(f, app, chunks[0]);
28        Self::render_details(f, app, chunks[1]);
29    }
30
31    fn render_list(f: &mut Frame, app: &App, area: Rect) {
32        let items: Vec<ListItem> = if app.repositories.is_empty() {
33            vec![
34                ListItem::new(Line::from(Span::styled(
35                    "No repositories loaded",
36                    Style::default().fg(Color::DarkGray),
37                ))),
38                ListItem::new(Line::from("")),
39                ListItem::new(Line::from(Span::styled(
40                    "Press 'r' to refresh",
41                    Style::default().fg(Color::Yellow),
42                ))),
43            ]
44        } else {
45            app.repositories
46                .iter()
47                .map(|repo| Self::repo_to_list_item(repo))
48                .collect()
49        };
50
51        let list = List::new(items)
52            .block(
53                Block::default()
54                    .borders(Borders::ALL)
55                    .title(" Repositories "),
56            )
57            .highlight_style(
58                Style::default()
59                    .bg(Color::DarkGray)
60                    .add_modifier(Modifier::BOLD),
61            )
62            .highlight_symbol("▶ ");
63
64        let mut state = ratatui::widgets::ListState::default();
65        if !app.repositories.is_empty() {
66            state.select(Some(app.view_state.selected_index));
67        }
68        f.render_stateful_widget(list, area, &mut state);
69    }
70
71    fn render_details(f: &mut Frame, app: &App, area: Rect) {
72        let content = if let Some(repo) = app.repositories.get(app.view_state.selected_index) {
73            vec![
74                Line::from(vec![Span::styled(
75                    &repo.full_name,
76                    Style::default()
77                        .fg(Color::Cyan)
78                        .add_modifier(Modifier::BOLD),
79                )]),
80                Line::from(""),
81                Line::from(vec![
82                    Span::styled("Description: ", Style::default().fg(Color::DarkGray)),
83                    Span::raw(repo.description.as_deref().unwrap_or("No description")),
84                ]),
85                Line::from(""),
86                Line::from(vec![
87                    Span::styled("Private: ", Style::default().fg(Color::DarkGray)),
88                    Span::raw(if repo.is_private { "Yes" } else { "No" }),
89                ]),
90                Line::from(vec![
91                    Span::styled("SCM: ", Style::default().fg(Color::DarkGray)),
92                    Span::raw(&repo.scm),
93                ]),
94                Line::from(vec![
95                    Span::styled("Language: ", Style::default().fg(Color::DarkGray)),
96                    Span::raw(repo.language.as_deref().unwrap_or("Not specified")),
97                ]),
98                Line::from(""),
99                Line::from(vec![
100                    Span::styled("Main branch: ", Style::default().fg(Color::DarkGray)),
101                    Span::raw(
102                        repo.mainbranch
103                            .as_ref()
104                            .map(|b| b.name.as_str())
105                            .unwrap_or("main"),
106                    ),
107                ]),
108                Line::from(""),
109                Line::from(vec![
110                    Span::styled("Created: ", Style::default().fg(Color::DarkGray)),
111                    Span::raw(
112                        repo.created_on
113                            .map(|d| d.format("%Y-%m-%d").to_string())
114                            .unwrap_or_default(),
115                    ),
116                ]),
117                Line::from(vec![
118                    Span::styled("Updated: ", Style::default().fg(Color::DarkGray)),
119                    Span::raw(
120                        repo.updated_on
121                            .map(|d| d.format("%Y-%m-%d").to_string())
122                            .unwrap_or_default(),
123                    ),
124                ]),
125            ]
126        } else {
127            vec![Line::from(Span::styled(
128                "Select a repository to view details",
129                Style::default().fg(Color::DarkGray),
130            ))]
131        };
132
133        let details = Paragraph::new(content)
134            .block(Block::default().borders(Borders::ALL).title(" Details "));
135        f.render_widget(details, area);
136    }
137
138    fn repo_to_list_item(repo: &Repository) -> ListItem<'static> {
139        let private_badge = if repo.is_private { "🔒" } else { "🌐" };
140        let lang_badge = repo.language.as_deref().unwrap_or("");
141
142        ListItem::new(Line::from(vec![
143            Span::raw(format!("{} ", private_badge)),
144            Span::styled(repo.full_name.clone(), Style::default().fg(Color::Cyan)),
145            if !lang_badge.is_empty() {
146                Span::styled(
147                    format!(" [{}]", lang_badge),
148                    Style::default().fg(Color::Yellow),
149                )
150            } else {
151                Span::raw("")
152            },
153        ]))
154    }
155}