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::{PullRequest, PullRequestState};
11use crate::tui::app::App;
12
13pub struct PrsView;
15
16impl PrsView {
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.pull_requests.is_empty() {
33 vec![
34 ListItem::new(Line::from(Span::styled(
35 "No pull requests 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.pull_requests
46 .iter()
47 .map(|pr| Self::pr_to_list_item(pr))
48 .collect()
49 };
50
51 let list = List::new(items)
52 .block(
53 Block::default()
54 .borders(Borders::ALL)
55 .title(" Pull Requests "),
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.pull_requests.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(pr) = app.pull_requests.get(app.view_state.selected_index) {
73 let state_color = Self::state_color(&pr.state);
74
75 vec![
76 Line::from(vec![
77 Span::styled(format!("#{} ", pr.id), Style::default().fg(Color::DarkGray)),
78 Span::styled(&pr.title, Style::default().add_modifier(Modifier::BOLD)),
79 ]),
80 Line::from(""),
81 Line::from(vec![
82 Span::styled("Status: ", Style::default().fg(Color::DarkGray)),
83 Span::styled(format!("{}", pr.state), Style::default().fg(state_color)),
84 ]),
85 Line::from(""),
86 Line::from(vec![
87 Span::styled("Author: ", Style::default().fg(Color::DarkGray)),
88 Span::raw(&pr.author.display_name),
89 ]),
90 Line::from(""),
91 Line::from(vec![Span::styled(
92 "Branches: ",
93 Style::default().fg(Color::DarkGray),
94 )]),
95 Line::from(vec![
96 Span::styled(" ", Style::default()),
97 Span::styled(&pr.source.branch.name, Style::default().fg(Color::Cyan)),
98 Span::styled(" → ", Style::default().fg(Color::DarkGray)),
99 Span::styled(
100 &pr.destination.branch.name,
101 Style::default().fg(Color::Green),
102 ),
103 ]),
104 Line::from(""),
105 Line::from(vec![
106 Span::styled("Created: ", Style::default().fg(Color::DarkGray)),
107 Span::raw(pr.created_on.format("%Y-%m-%d %H:%M").to_string()),
108 ]),
109 Line::from(vec![
110 Span::styled("Updated: ", Style::default().fg(Color::DarkGray)),
111 Span::raw(pr.updated_on.format("%Y-%m-%d %H:%M").to_string()),
112 ]),
113 Line::from(""),
114 if let Some(count) = pr.comment_count {
115 Line::from(vec![
116 Span::styled("Comments: ", Style::default().fg(Color::DarkGray)),
117 Span::raw(format!("{}", count)),
118 ])
119 } else {
120 Line::from("")
121 },
122 ]
123 } else {
124 vec![Line::from(Span::styled(
125 "Select a pull request to view details",
126 Style::default().fg(Color::DarkGray),
127 ))]
128 };
129
130 let details = Paragraph::new(content)
131 .block(Block::default().borders(Borders::ALL).title(" Details "));
132 f.render_widget(details, area);
133 }
134
135 fn pr_to_list_item(pr: &PullRequest) -> ListItem<'static> {
136 let state_color = Self::state_color(&pr.state);
137 let state_icon = match pr.state {
138 PullRequestState::Open => "○",
139 PullRequestState::Merged => "●",
140 PullRequestState::Declined => "✗",
141 PullRequestState::Superseded => "◌",
142 };
143
144 ListItem::new(Line::from(vec![
145 Span::styled(format!("{} ", state_icon), Style::default().fg(state_color)),
146 Span::styled(format!("#{} ", pr.id), Style::default().fg(Color::DarkGray)),
147 Span::raw(pr.title.chars().take(50).collect::<String>()),
148 ]))
149 }
150
151 fn state_color(state: &PullRequestState) -> Color {
152 match state {
153 PullRequestState::Open => Color::Green,
154 PullRequestState::Merged => Color::Magenta,
155 PullRequestState::Declined => Color::Red,
156 PullRequestState::Superseded => Color::Yellow,
157 }
158 }
159}