bitbucket_cli/tui/views/
repos.rs

1/// Repository browser view
2use ratatui::{
3    Frame,
4    layout::{Constraint, Direction, Layout, Rect},
5    style::{Color, Modifier, Style},
6    text::{Line, Span},
7    widgets::{Block, Borders, List, ListItem, Paragraph},
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.unwrap_or(false) {
89                        "Yes"
90                    } else {
91                        "No"
92                    }),
93                ]),
94                Line::from(vec![
95                    Span::styled("SCM: ", Style::default().fg(Color::DarkGray)),
96                    Span::raw(repo.scm.as_deref().unwrap_or("unknown")),
97                ]),
98                Line::from(vec![
99                    Span::styled("Language: ", Style::default().fg(Color::DarkGray)),
100                    Span::raw(repo.language.as_deref().unwrap_or("Not specified")),
101                ]),
102                Line::from(""),
103                Line::from(vec![
104                    Span::styled("Main branch: ", Style::default().fg(Color::DarkGray)),
105                    Span::raw(
106                        repo.mainbranch
107                            .as_ref()
108                            .map(|b| b.name.as_str())
109                            .unwrap_or("main"),
110                    ),
111                ]),
112                Line::from(""),
113                Line::from(vec![
114                    Span::styled("Created: ", Style::default().fg(Color::DarkGray)),
115                    Span::raw(
116                        repo.created_on
117                            .map(|d| d.format("%Y-%m-%d").to_string())
118                            .unwrap_or_default(),
119                    ),
120                ]),
121                Line::from(vec![
122                    Span::styled("Updated: ", Style::default().fg(Color::DarkGray)),
123                    Span::raw(
124                        repo.updated_on
125                            .map(|d| d.format("%Y-%m-%d").to_string())
126                            .unwrap_or_default(),
127                    ),
128                ]),
129            ]
130        } else {
131            vec![Line::from(Span::styled(
132                "Select a repository to view details",
133                Style::default().fg(Color::DarkGray),
134            ))]
135        };
136
137        let details = Paragraph::new(content)
138            .block(Block::default().borders(Borders::ALL).title(" Details "));
139        f.render_widget(details, area);
140    }
141
142    fn repo_to_list_item(repo: &Repository) -> ListItem<'static> {
143        let private_badge = if repo.is_private.unwrap_or(false) {
144            "🔒"
145        } else {
146            "🌐"
147        };
148        let lang_badge = repo.language.as_deref().unwrap_or("");
149
150        ListItem::new(Line::from(vec![
151            Span::raw(format!("{} ", private_badge)),
152            Span::styled(repo.full_name.clone(), Style::default().fg(Color::Cyan)),
153            if !lang_badge.is_empty() {
154                Span::styled(
155                    format!(" [{}]", lang_badge),
156                    Style::default().fg(Color::Yellow),
157                )
158            } else {
159                Span::raw("")
160            },
161        ]))
162    }
163}