bitbucket_cli/tui/views/
prs.rs

1/// Pull request 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::{PullRequest, PullRequestState};
11use crate::tui::app::App;
12
13/// Pull request list view
14pub struct PrsView;
15
16impl PrsView {
17    /// Render the pull request 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.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}