1use ratatui::{
2 Frame,
3 layout::{Constraint, Direction, Layout, Rect},
4 style::{Color, Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, Borders, List, ListItem, Paragraph, Tabs},
7};
8
9use super::app::App;
10use super::views::View;
11
12pub fn draw(f: &mut Frame, app: &App) {
14 let chunks = Layout::default()
15 .direction(Direction::Vertical)
16 .constraints([
17 Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
21 .split(f.area());
22
23 draw_header(f, app, chunks[0]);
24 draw_main(f, app, chunks[1]);
25 draw_footer(f, app, chunks[2]);
26}
27
28fn draw_header(f: &mut Frame, app: &App, area: Rect) {
29 let titles = vec!["Dashboard", "Repos", "PRs", "Issues", "Pipelines"];
30 let selected = match app.current_view {
31 View::Dashboard => 0,
32 View::Repositories => 1,
33 View::PullRequests => 2,
34 View::Issues => 3,
35 View::Pipelines => 4,
36 };
37
38 let tabs = Tabs::new(titles)
39 .block(
40 Block::default()
41 .borders(Borders::ALL)
42 .title(" Bitbucket CLI "),
43 )
44 .select(selected)
45 .style(Style::default().fg(Color::White))
46 .highlight_style(
47 Style::default()
48 .fg(Color::Cyan)
49 .add_modifier(Modifier::BOLD),
50 );
51
52 f.render_widget(tabs, area);
53}
54
55fn draw_main(f: &mut Frame, app: &App, area: Rect) {
56 match app.current_view {
57 View::Dashboard => draw_dashboard(f, app, area),
58 View::Repositories => draw_repositories(f, app, area),
59 View::PullRequests => draw_pull_requests(f, app, area),
60 View::Issues => draw_issues(f, app, area),
61 View::Pipelines => draw_pipelines(f, app, area),
62 }
63}
64
65fn draw_dashboard(f: &mut Frame, app: &App, area: Rect) {
66 let chunks = Layout::default()
67 .direction(Direction::Vertical)
68 .constraints([Constraint::Length(3), Constraint::Min(0)])
69 .split(area);
70
71 let workspace_text = match &app.workspace {
73 Some(ws) => format!("Workspace: {}", ws),
74 None => "No workspace selected".to_string(),
75 };
76 let workspace = Paragraph::new(workspace_text)
77 .block(Block::default().borders(Borders::ALL).title(" Workspace "));
78 f.render_widget(workspace, chunks[0]);
79
80 let items: Vec<ListItem> = vec![
82 ListItem::new(Line::from(vec![
83 Span::styled("π ", Style::default()),
84 Span::raw("Repositories"),
85 Span::styled(
86 format!(" ({})", app.repositories.len()),
87 Style::default().fg(Color::DarkGray),
88 ),
89 ])),
90 ListItem::new(Line::from(vec![
91 Span::styled("π ", Style::default()),
92 Span::raw("Pull Requests"),
93 Span::styled(
94 format!(" ({})", app.pull_requests.len()),
95 Style::default().fg(Color::DarkGray),
96 ),
97 ])),
98 ListItem::new(Line::from(vec![
99 Span::styled("π ", Style::default()),
100 Span::raw("Issues"),
101 Span::styled(
102 format!(" ({})", app.issues.len()),
103 Style::default().fg(Color::DarkGray),
104 ),
105 ])),
106 ListItem::new(Line::from(vec![
107 Span::styled("βοΈ ", Style::default()),
108 Span::raw("Pipelines"),
109 Span::styled(
110 format!(" ({})", app.pipelines.len()),
111 Style::default().fg(Color::DarkGray),
112 ),
113 ])),
114 ];
115
116 let list = List::new(items)
117 .block(
118 Block::default()
119 .borders(Borders::ALL)
120 .title(" Quick Access "),
121 )
122 .highlight_style(
123 Style::default()
124 .bg(Color::DarkGray)
125 .add_modifier(Modifier::BOLD),
126 )
127 .highlight_symbol("βΆ ");
128
129 let mut state = ratatui::widgets::ListState::default();
130 state.select(Some(app.view_state.selected_index));
131 f.render_stateful_widget(list, chunks[1], &mut state);
132}
133
134fn draw_repositories(f: &mut Frame, app: &App, area: Rect) {
135 let items: Vec<ListItem> = if app.repositories.is_empty() {
136 vec![ListItem::new(
137 "No repositories loaded. Press 'r' to refresh.",
138 )]
139 } else {
140 app.repositories
141 .iter()
142 .map(|repo| {
143 let private_badge = if repo.is_private.unwrap_or(false) {
144 "π"
145 } else {
146 "π"
147 };
148 ListItem::new(Line::from(vec![
149 Span::raw(format!("{} ", private_badge)),
150 Span::styled(&repo.full_name, Style::default().fg(Color::Cyan)),
151 Span::raw(" - "),
152 Span::styled(
153 repo.description.as_deref().unwrap_or("No description"),
154 Style::default().fg(Color::DarkGray),
155 ),
156 ]))
157 })
158 .collect()
159 };
160
161 let list = List::new(items)
162 .block(
163 Block::default()
164 .borders(Borders::ALL)
165 .title(" Repositories "),
166 )
167 .highlight_style(
168 Style::default()
169 .bg(Color::DarkGray)
170 .add_modifier(Modifier::BOLD),
171 )
172 .highlight_symbol("βΆ ");
173
174 let mut state = ratatui::widgets::ListState::default();
175 state.select(Some(app.view_state.selected_index));
176 f.render_stateful_widget(list, area, &mut state);
177}
178
179fn draw_pull_requests(f: &mut Frame, app: &App, area: Rect) {
180 let items: Vec<ListItem> = if app.pull_requests.is_empty() {
181 vec![ListItem::new(
182 "No pull requests loaded. Press 'r' to refresh.",
183 )]
184 } else {
185 app.pull_requests
186 .iter()
187 .map(|pr| {
188 let state_color = match pr.state {
189 crate::models::PullRequestState::Open => Color::Green,
190 crate::models::PullRequestState::Merged => Color::Magenta,
191 crate::models::PullRequestState::Declined => Color::Red,
192 crate::models::PullRequestState::Superseded => Color::Yellow,
193 };
194 ListItem::new(Line::from(vec![
195 Span::styled(format!("[{}] ", pr.state), Style::default().fg(state_color)),
196 Span::styled(format!("#{} ", pr.id), Style::default().fg(Color::DarkGray)),
197 Span::raw(&pr.title),
198 ]))
199 })
200 .collect()
201 };
202
203 let list = List::new(items)
204 .block(
205 Block::default()
206 .borders(Borders::ALL)
207 .title(" Pull Requests "),
208 )
209 .highlight_style(
210 Style::default()
211 .bg(Color::DarkGray)
212 .add_modifier(Modifier::BOLD),
213 )
214 .highlight_symbol("βΆ ");
215
216 let mut state = ratatui::widgets::ListState::default();
217 state.select(Some(app.view_state.selected_index));
218 f.render_stateful_widget(list, area, &mut state);
219}
220
221fn draw_issues(f: &mut Frame, app: &App, area: Rect) {
222 let items: Vec<ListItem> = if app.issues.is_empty() {
223 vec![ListItem::new("No issues loaded. Press 'r' to refresh.")]
224 } else {
225 app.issues
226 .iter()
227 .map(|issue| {
228 let kind_icon = match issue.kind {
229 crate::models::IssueKind::Bug => "π",
230 crate::models::IssueKind::Enhancement => "β¨",
231 crate::models::IssueKind::Proposal => "π‘",
232 crate::models::IssueKind::Task => "π",
233 };
234 ListItem::new(Line::from(vec![
235 Span::raw(format!("{} ", kind_icon)),
236 Span::styled(
237 format!("#{} ", issue.id),
238 Style::default().fg(Color::DarkGray),
239 ),
240 Span::raw(&issue.title),
241 ]))
242 })
243 .collect()
244 };
245
246 let list = List::new(items)
247 .block(Block::default().borders(Borders::ALL).title(" Issues "))
248 .highlight_style(
249 Style::default()
250 .bg(Color::DarkGray)
251 .add_modifier(Modifier::BOLD),
252 )
253 .highlight_symbol("βΆ ");
254
255 let mut state = ratatui::widgets::ListState::default();
256 state.select(Some(app.view_state.selected_index));
257 f.render_stateful_widget(list, area, &mut state);
258}
259
260fn draw_pipelines(f: &mut Frame, app: &App, area: Rect) {
261 let items: Vec<ListItem> = if app.pipelines.is_empty() {
262 vec![ListItem::new("No pipelines loaded. Press 'r' to refresh.")]
263 } else {
264 app.pipelines
265 .iter()
266 .map(|pipeline| {
267 let (status_icon, status_color) = match pipeline.state.name {
268 crate::models::PipelineStateName::Pending => ("β³", Color::Yellow),
269 crate::models::PipelineStateName::Building => ("π", Color::Blue),
270 crate::models::PipelineStateName::Completed => {
271 if let Some(result) = &pipeline.state.result {
272 match result.name {
273 crate::models::PipelineResultName::Successful => {
274 ("β
", Color::Green)
275 }
276 crate::models::PipelineResultName::Failed => ("β", Color::Red),
277 _ => ("βͺ", Color::Gray),
278 }
279 } else {
280 ("βͺ", Color::Gray)
281 }
282 }
283 crate::models::PipelineStateName::Halted => ("β", Color::Red),
284 crate::models::PipelineStateName::Paused => ("βΈοΈ", Color::Yellow),
285 };
286 ListItem::new(Line::from(vec![
287 Span::raw(format!("{} ", status_icon)),
288 Span::styled(
289 format!("#{} ", pipeline.build_number),
290 Style::default().fg(status_color),
291 ),
292 Span::raw(pipeline.target.ref_name.as_deref().unwrap_or("unknown")),
293 ]))
294 })
295 .collect()
296 };
297
298 let list = List::new(items)
299 .block(Block::default().borders(Borders::ALL).title(" Pipelines "))
300 .highlight_style(
301 Style::default()
302 .bg(Color::DarkGray)
303 .add_modifier(Modifier::BOLD),
304 )
305 .highlight_symbol("βΆ ");
306
307 let mut state = ratatui::widgets::ListState::default();
308 state.select(Some(app.view_state.selected_index));
309 f.render_stateful_widget(list, area, &mut state);
310}
311
312fn draw_footer(f: &mut Frame, app: &App, area: Rect) {
313 let status_text = if let Some(error) = &app.error {
314 Line::from(Span::styled(
315 format!("Error: {}", error),
316 Style::default().fg(Color::Red),
317 ))
318 } else if let Some(status) = &app.status {
319 Line::from(Span::styled(status, Style::default().fg(Color::Yellow)))
320 } else if app.loading {
321 Line::from(Span::styled(
322 "Loading...",
323 Style::default().fg(Color::Yellow),
324 ))
325 } else {
326 Line::from(vec![
327 Span::styled("q", Style::default().fg(Color::Cyan)),
328 Span::raw(" quit "),
329 Span::styled("1-5", Style::default().fg(Color::Cyan)),
330 Span::raw(" switch view "),
331 Span::styled("j/k", Style::default().fg(Color::Cyan)),
332 Span::raw(" navigate "),
333 Span::styled("Enter", Style::default().fg(Color::Cyan)),
334 Span::raw(" select "),
335 Span::styled("r", Style::default().fg(Color::Cyan)),
336 Span::raw(" refresh"),
337 ])
338 };
339
340 let footer =
341 Paragraph::new(status_text).block(Block::default().borders(Borders::ALL).title(" Help "));
342 f.render_widget(footer, area);
343}