1use 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
13pub struct ReposView;
15
16impl ReposView {
17 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), Constraint::Percentage(40), ])
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}