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::{Issue, IssueKind, IssuePriority, IssueState};
11use crate::tui::app::App;
12
13pub struct IssuesView;
15
16impl IssuesView {
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.issues.is_empty() {
33 vec![
34 ListItem::new(Line::from(Span::styled(
35 "No issues 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.issues
46 .iter()
47 .map(|issue| Self::issue_to_list_item(issue))
48 .collect()
49 };
50
51 let list = List::new(items)
52 .block(Block::default().borders(Borders::ALL).title(" Issues "))
53 .highlight_style(
54 Style::default()
55 .bg(Color::DarkGray)
56 .add_modifier(Modifier::BOLD),
57 )
58 .highlight_symbol("▶ ");
59
60 let mut state = ratatui::widgets::ListState::default();
61 if !app.issues.is_empty() {
62 state.select(Some(app.view_state.selected_index));
63 }
64 f.render_stateful_widget(list, area, &mut state);
65 }
66
67 fn render_details(f: &mut Frame, app: &App, area: Rect) {
68 let content = if let Some(issue) = app.issues.get(app.view_state.selected_index) {
69 let state_color = Self::state_color(&issue.state);
70 let priority_color = Self::priority_color(&issue.priority);
71
72 vec![
73 Line::from(vec![
74 Span::styled(
75 format!("#{} ", issue.id),
76 Style::default().fg(Color::DarkGray),
77 ),
78 Span::styled(&issue.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!("{}", issue.state), Style::default().fg(state_color)),
84 ]),
85 Line::from(vec![
86 Span::styled("Type: ", Style::default().fg(Color::DarkGray)),
87 Span::raw(format!("{}", issue.kind)),
88 ]),
89 Line::from(vec![
90 Span::styled("Priority: ", Style::default().fg(Color::DarkGray)),
91 Span::styled(
92 format!("{}", issue.priority),
93 Style::default().fg(priority_color),
94 ),
95 ]),
96 Line::from(""),
97 if let Some(reporter) = &issue.reporter {
98 Line::from(vec![
99 Span::styled("Reporter: ", Style::default().fg(Color::DarkGray)),
100 Span::raw(&reporter.display_name),
101 ])
102 } else {
103 Line::from("")
104 },
105 if let Some(assignee) = &issue.assignee {
106 Line::from(vec![
107 Span::styled("Assignee: ", Style::default().fg(Color::DarkGray)),
108 Span::raw(&assignee.display_name),
109 ])
110 } else {
111 Line::from(vec![
112 Span::styled("Assignee: ", Style::default().fg(Color::DarkGray)),
113 Span::styled("Unassigned", Style::default().fg(Color::DarkGray)),
114 ])
115 },
116 Line::from(""),
117 Line::from(vec![
118 Span::styled("Created: ", Style::default().fg(Color::DarkGray)),
119 Span::raw(issue.created_on.format("%Y-%m-%d %H:%M").to_string()),
120 ]),
121 Line::from(""),
122 if issue
123 .content
124 .as_ref()
125 .and_then(|c| c.raw.as_ref())
126 .is_some()
127 {
128 Line::from(vec![Span::styled(
129 "Description: ",
130 Style::default().fg(Color::DarkGray),
131 )])
132 } else {
133 Line::from("")
134 },
135 ]
136 } else {
137 vec![Line::from(Span::styled(
138 "Select an issue to view details",
139 Style::default().fg(Color::DarkGray),
140 ))]
141 };
142
143 let details = Paragraph::new(content)
144 .block(Block::default().borders(Borders::ALL).title(" Details "));
145 f.render_widget(details, area);
146 }
147
148 fn issue_to_list_item(issue: &Issue) -> ListItem<'static> {
149 let kind_icon = match issue.kind {
150 IssueKind::Bug => "🐛",
151 IssueKind::Enhancement => "✨",
152 IssueKind::Proposal => "💡",
153 IssueKind::Task => "📋",
154 };
155
156 let state_color = Self::state_color(&issue.state);
157
158 ListItem::new(Line::from(vec![
159 Span::raw(format!("{} ", kind_icon)),
160 Span::styled(format!("#{} ", issue.id), Style::default().fg(state_color)),
161 Span::raw(issue.title.chars().take(45).collect::<String>()),
162 ]))
163 }
164
165 fn state_color(state: &IssueState) -> Color {
166 match state {
167 IssueState::New => Color::Cyan,
168 IssueState::Open => Color::Green,
169 IssueState::Resolved => Color::Blue,
170 IssueState::OnHold => Color::Yellow,
171 IssueState::Invalid | IssueState::Duplicate | IssueState::Wontfix => Color::DarkGray,
172 IssueState::Closed => Color::Magenta,
173 }
174 }
175
176 fn priority_color(priority: &IssuePriority) -> Color {
177 match priority {
178 IssuePriority::Trivial => Color::DarkGray,
179 IssuePriority::Minor => Color::White,
180 IssuePriority::Major => Color::Yellow,
181 IssuePriority::Critical => Color::Red,
182 IssuePriority::Blocker => Color::LightRed,
183 }
184 }
185}