1use crate::app::{ActiveView, App, DiffMode};
2use crate::dialogs::FileDialog;
3use crate::util::syntax_highlight::as_styled;
4use crate::widgets::branches_view::{BranchList, BranchListItem};
5use crate::widgets::commit_view::CommitView;
6use crate::widgets::files_view::{FileList, FileListItem};
7use crate::widgets::graph_view::GraphView;
8use crate::widgets::models_view::ModelListState;
9use lazy_static::lazy_static;
10use tui::backend::Backend;
11use tui::layout::{Constraint, Direction, Layout, Rect};
12use tui::style::{Color, Modifier, Style};
13use tui::text::{Span, Spans, Text};
14use tui::widgets::{
15 Block, BorderType, Borders, Clear, List, ListItem as TuiListItem, Paragraph, Wrap,
16};
17use tui::Frame;
18
19lazy_static! {
20 pub static ref HINT_STYLE: Style = Style::default().fg(Color::Cyan);
21}
22
23pub fn draw_open_repo<B: Backend>(f: &mut Frame<B>, dialog: &mut FileDialog) {
24 let chunks = Layout::default()
25 .direction(Direction::Vertical)
26 .constraints([Constraint::Length(4), Constraint::Min(0)].as_ref())
27 .split(f.size());
28
29 let top_chunks = Layout::default()
30 .direction(Direction::Vertical)
31 .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
32 .split(chunks[0]);
33
34 let location_block = Block::default().borders(Borders::ALL).title(" Path ");
35
36 let paragraph = Paragraph::new(format!("{}", &dialog.location.display())).block(location_block);
37 f.render_widget(paragraph, top_chunks[0]);
38
39 let help = Paragraph::new(" Navigate with Arrows, confirm with Enter, abort with Esc.");
40 f.render_widget(help, top_chunks[1]);
41
42 let list_block = Block::default()
43 .borders(Borders::ALL)
44 .title(" Open repository ");
45
46 let items: Vec<_> = dialog
47 .dirs
48 .iter()
49 .map(|f| {
50 if dialog.color {
51 if f.1 {
52 TuiListItem::new(&f.0[..]).style(Style::default().fg(Color::LightGreen))
53 } else {
54 TuiListItem::new(&f.0[..])
55 }
56 } else if f.1 {
57 TuiListItem::new(format!("+ {}", &f.0[..]))
58 } else {
59 TuiListItem::new(format!(" {}", &f.0[..]))
60 }
61 })
62 .collect();
63
64 let mut list = List::new(items).block(list_block).highlight_symbol("> ");
65
66 if dialog.color {
67 list = list.highlight_style(Style::default().add_modifier(Modifier::UNDERLINED));
68 }
69
70 f.render_stateful_widget(list, chunks[1], &mut dialog.state);
71
72 if let Some(error) = &dialog.error_message {
73 draw_error_dialog(f, f.size(), error, dialog.color);
74 }
75}
76
77pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
78 if let ActiveView::Help(scroll) = app.active_view {
79 draw_help(f, f.size(), scroll);
80 return;
81 }
82
83 if let (ActiveView::Models, Some(model_state)) = (&app.active_view, &mut app.models_state) {
84 let chunks = Layout::default()
85 .direction(Direction::Vertical)
86 .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref())
87 .split(f.size());
88
89 let help = Paragraph::new(" Enter = confirm, P = permanent, Esc = abort.");
90 f.render_widget(help, chunks[0]);
91
92 draw_models(f, chunks[1], app.color, model_state);
93 return;
94 }
95
96 if app.is_fullscreen {
97 let view = if app.active_view == ActiveView::Search {
98 app.prev_active_view.as_ref().unwrap_or(&ActiveView::Graph)
99 } else {
100 &app.active_view
101 };
102 match view {
103 ActiveView::Branches => draw_branches(f, f.size(), app),
104 ActiveView::Graph => draw_graph(f, f.size(), app),
105 ActiveView::Commit => draw_commit(f, f.size(), app),
106 ActiveView::Files => draw_files(f, f.size(), app),
107 ActiveView::Diff => draw_diff(f, f.size(), app),
108 _ => {}
109 }
110 } else {
111 let base_split = if app.horizontal_split {
112 Direction::Horizontal
113 } else {
114 Direction::Vertical
115 };
116 let sub_split = if app.horizontal_split {
117 Direction::Vertical
118 } else {
119 Direction::Horizontal
120 };
121
122 let show_branches = app.show_branches || app.active_view == ActiveView::Branches;
123
124 let top_chunks = Layout::default()
125 .direction(Direction::Horizontal)
126 .constraints(
127 [
128 Constraint::Length(if show_branches { 25 } else { 0 }),
129 Constraint::Min(0),
130 ]
131 .as_ref(),
132 )
133 .split(f.size());
134
135 let chunks = Layout::default()
136 .direction(base_split)
137 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
138 .split(top_chunks[1]);
139
140 let right_chunks = Layout::default()
141 .direction(sub_split)
142 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
143 .split(chunks[1]);
144
145 match app.active_view {
146 ActiveView::Search => {
147 if let Some(prev) = &app.prev_active_view {
148 match prev {
149 ActiveView::Files | ActiveView::Diff => draw_diff(f, chunks[0], app),
150 _ => draw_graph(f, chunks[0], app),
151 }
152 } else {
153 draw_graph(f, chunks[0], app)
154 }
155 }
156 ActiveView::Files | ActiveView::Diff => draw_diff(f, chunks[0], app),
157 _ => draw_graph(f, chunks[0], app),
158 }
159
160 if show_branches {
161 draw_branches(f, top_chunks[0], app);
162 }
163 draw_commit(f, right_chunks[0], app);
164 draw_files(f, right_chunks[1], app);
165 }
166
167 if let Some(error) = &app.error_message {
168 draw_error_dialog(f, f.size(), error, app.color);
169 } else if app.active_view == ActiveView::Search {
170 draw_search_dialog(f, f.size(), &app.search_term);
171 }
172}
173
174fn create_title<'a>(title: &'a str, hint: &'a str, color: bool) -> Spans<'a> {
175 Spans(vec![
176 Span::raw(format!(" {} ", title)),
177 if color {
178 Span::styled(hint, *HINT_STYLE)
179 } else {
180 Span::raw(hint)
181 },
182 ])
183}
184
185fn draw_graph<B: Backend>(f: &mut Frame<B>, target: Rect, app: &mut App) {
186 let title = format!("Graph - {}", app.repo_name);
187 let mut block = Block::default().borders(Borders::ALL).title(create_title(
188 &title,
189 " <-Branches | Commit-> ",
190 app.color,
191 ));
192
193 if app.active_view == ActiveView::Graph {
194 block = block.border_type(BorderType::Thick);
195 }
196
197 let mut graph = GraphView::default().block(block).highlight_symbol(">", "#");
198
199 if app.color {
200 graph = graph.highlight_style(Style::default().add_modifier(Modifier::UNDERLINED));
201 }
202
203 f.render_stateful_widget(graph, target, &mut app.graph_state);
204}
205
206fn draw_branches<B: Backend>(f: &mut Frame<B>, target: Rect, app: &mut App) {
207 let color = app.color;
208
209 let mut block = Block::default().borders(Borders::ALL).title(create_title(
210 "Branches",
211 " Graph-> ",
212 app.color,
213 ));
214
215 if let Some(state) = &mut app.graph_state.branches {
216 if app.active_view == ActiveView::Branches {
217 block = block.border_type(BorderType::Thick);
218 }
219
220 let items: Vec<_> = state
221 .items
222 .iter()
223 .map(|item| {
224 BranchListItem::new(
225 if color {
226 Span::styled(&item.name, Style::default().fg(Color::Indexed(item.color)))
227 } else {
228 Span::raw(&item.name)
229 },
230 &item.branch_type,
231 )
232 })
233 .collect();
234
235 let mut list = BranchList::new(items).block(block).highlight_symbol("> ");
236
237 if color {
238 list = list.highlight_style(Style::default().add_modifier(Modifier::UNDERLINED));
239 }
240
241 f.render_stateful_widget(list, target, &mut state.state);
242 } else {
243 if app.active_view == ActiveView::Files {
244 block = block.border_type(BorderType::Thick);
245 }
246 f.render_widget(block, target);
247 }
248}
249
250fn draw_commit<B: Backend>(f: &mut Frame<B>, target: Rect, app: &mut App) {
251 let mut block = Block::default().borders(Borders::ALL).title(create_title(
252 "Commit",
253 " <-Graph | Files-> ",
254 app.color,
255 ));
256
257 if app.active_view == ActiveView::Commit {
258 block = block.border_type(BorderType::Thick);
259 }
260
261 let commit = CommitView::default().block(block).highlight_symbol(">");
262
263 f.render_stateful_widget(commit, target, &mut app.commit_state);
264}
265
266fn draw_files<B: Backend>(f: &mut Frame<B>, target: Rect, app: &mut App) {
267 let color = app.color;
268 if let Some(state) = &mut app.commit_state.content {
269 let title = format!(
270 "Files ({}..{})",
271 &state.compare_oid.to_string()[..7],
272 &state.oid.to_string()[..7]
273 );
274 let mut block = Block::default().borders(Borders::ALL).title(create_title(
275 &title,
276 " <-Commit | Diff-> ",
277 app.color,
278 ));
279
280 if app.active_view == ActiveView::Files {
281 block = block.border_type(BorderType::Thick);
282 }
283
284 let items: Vec<_> = state
285 .diffs
286 .items
287 .iter()
288 .map(|item| {
289 if color {
290 let style = Style::default().fg(item.diff_type.to_color());
291 FileListItem::new(
292 Span::styled(&item.file, style),
293 Span::styled(format!("{} ", item.diff_type.to_string()), style),
294 )
295 } else {
296 FileListItem::new(
297 Span::raw(&item.file),
298 Span::raw(format!("{} ", item.diff_type.to_string())),
299 )
300 }
301 })
302 .collect();
303
304 let mut list = FileList::new(items).block(block).highlight_symbol("> ");
305
306 if color {
307 list = list.highlight_style(Style::default().add_modifier(Modifier::UNDERLINED));
308 }
309
310 f.render_stateful_widget(list, target, &mut state.diffs.state);
311 } else {
312 let mut block = Block::default().borders(Borders::ALL).title(create_title(
313 "Files",
314 " <-Commit | Diff-> ",
315 app.color,
316 ));
317 if app.active_view == ActiveView::Files {
318 block = block.border_type(BorderType::Thick);
319 }
320 f.render_widget(block, target);
321 }
322}
323
324fn draw_diff<B: Backend>(f: &mut Frame<B>, target: Rect, app: &mut App) {
325 if let Some(state) = &app.diff_state.content {
326 let title = match app.diff_options.diff_mode {
327 DiffMode::Diff => format!(
328 "Diff ({}..{})",
329 &state.compare_oid.to_string()[..7],
330 &state.oid.to_string()[..7]
331 ),
332 DiffMode::Old => format!("Diff (old: {})", &state.compare_oid.to_string()[..7],),
333 DiffMode::New => format!("Diff (new: {})", &state.oid.to_string()[..7],),
334 };
335 let mut block = Block::default().borders(Borders::ALL).title(create_title(
336 &title,
337 " <-Files ",
338 app.color,
339 ));
340 if app.active_view == ActiveView::Diff {
341 block = block.border_type(BorderType::Thick);
342 }
343
344 let styles = [
345 Style::default().fg(Color::LightGreen),
346 Style::default().fg(Color::LightRed),
347 Style::default().fg(Color::LightBlue),
348 Style::default(),
349 ];
350
351 let mut text = Text::from("");
352 if app.diff_options.diff_mode == DiffMode::Diff {
353 let (space_old_ln, space_new_ln, empty_old_ln, empty_new_ln) =
354 if app.diff_options.line_numbers {
355 let mut max_old_ln = None;
356 let mut max_new_ln = None;
357
358 for (_, old_ln, new_ln) in state.diffs.iter().rev() {
359 if max_old_ln.is_none() {
360 if let Some(old_ln) = old_ln {
361 max_old_ln = Some(*old_ln);
362 }
363 }
364 if max_new_ln.is_none() {
365 if let Some(new_ln) = new_ln {
366 max_new_ln = Some(*new_ln);
367 }
368 }
369 if max_old_ln.is_some() && max_new_ln.is_some() {
370 break;
371 }
372 }
373
374 let space_old_ln =
375 std::cmp::max(3, (max_old_ln.unwrap_or(0) as f32).log10().ceil() as usize);
376 let space_new_ln =
377 std::cmp::max(3, (max_new_ln.unwrap_or(0) as f32).log10().ceil() as usize)
378 + 1;
379
380 (
381 space_old_ln,
382 space_new_ln,
383 " ".repeat(space_old_ln),
384 " ".repeat(space_new_ln),
385 )
386 } else {
387 (0, 0, String::new(), String::new())
388 };
389
390 for (line, old_ln, new_ln) in &state.diffs {
391 let ln = if line.starts_with("@@ ") {
392 if let Some(pos) = line.find(" @@ ") {
393 &line[..pos + 3]
394 } else {
395 line
396 }
397 } else {
398 line
399 };
400
401 if app.diff_options.line_numbers && (old_ln.is_some() || new_ln.is_some()) {
402 let l1 = old_ln
403 .map(|v| format!("{:>width$}", v, width = space_old_ln))
404 .unwrap_or_else(|| empty_old_ln.clone());
405 let l2 = new_ln
406 .map(|v| format!("{:>width$}", v, width = space_new_ln))
407 .unwrap_or_else(|| empty_new_ln.clone());
408 let fmt = format!("{}{}|", l1, l2);
409
410 text.extend(style_diff_line(Some(fmt), ln, &styles, app.color));
411 } else {
412 text.extend(style_diff_line(None, ln, &styles, app.color));
413 }
414 }
415 } else {
416 if !state.diffs.is_empty() {
417 text.extend(style_diff_line(None, &state.diffs[0].0, &styles, false));
418 }
419 if !state.diffs.len() > 1 {
420 if let Some(txt) = &state.highlighted {
421 text.extend(as_styled(txt));
422 } else {
423 if state.diffs.len() > 1 {
426 for line in state.diffs[1].0.lines() {
427 let trim = line.trim_end();
428 if trim.is_empty() {
429 text.extend(Text::raw("\n"));
430 } else {
431 let styled = style_diff_line(None, trim, &styles, false);
432 text.extend(styled);
433 }
434 }
435 }
436 }
437 }
438 }
439
440 let mut paragraph = Paragraph::new(text).block(block).scroll(state.scroll);
441
442 if app.diff_options.wrap_lines {
443 paragraph = paragraph.wrap(Wrap { trim: false });
444 }
445
446 f.render_widget(paragraph, target);
447 } else {
448 let mut block = Block::default().borders(Borders::ALL).title(create_title(
449 "Diff",
450 " <-Files ",
451 app.color,
452 ));
453 if app.active_view == ActiveView::Diff {
454 block = block.border_type(BorderType::Thick);
455 }
456 f.render_widget(block, target);
457 }
458}
459
460fn style_diff_line<'a>(
461 prefix: Option<String>,
462 line: &'a str,
463 styles: &'a [Style; 4],
464 color: bool,
465) -> Text<'a> {
466 if !color {
467 if let Some(prefix) = prefix {
468 Text::raw(format!("{}{}", prefix, line))
469 } else {
470 Text::raw(line)
471 }
472 } else {
473 let style = if line.starts_with('+') {
474 styles[0]
475 } else if line.starts_with('-') {
476 styles[1]
477 } else if line.starts_with('@') {
478 styles[2]
479 } else {
480 styles[3]
481 };
482 if let Some(prefix) = prefix {
483 Text::styled(format!("{}{}", prefix, line), style)
484 } else {
485 Text::styled(line, style)
486 }
487 }
488}
489
490fn draw_models<B: Backend>(
491 f: &mut Frame<B>,
492 target: Rect,
493 color: bool,
494 state: &mut ModelListState,
495) {
496 let block = Block::default()
497 .borders(Borders::ALL)
498 .title(" Branching model ");
499
500 let items: Vec<_> = state
501 .models
502 .iter()
503 .map(|m| TuiListItem::new(&m[..]))
504 .collect();
505
506 let mut list = List::new(items).block(block).highlight_symbol("> ");
507
508 if color {
509 list = list.highlight_style(Style::default().add_modifier(Modifier::UNDERLINED));
510 }
511
512 f.render_stateful_widget(list, target, &mut state.state);
513}
514
515fn draw_help<B: Backend>(f: &mut Frame<B>, target: Rect, scroll: u16) {
516 let block = Block::default()
517 .borders(Borders::ALL)
518 .title(" Help [back with Esc] ");
519
520 let paragraph = Paragraph::new(
521 "\n\
522 General\n \
523 \n \
524 F1/H Show this help\n \
525 Q Quit\n \
526 Ctrl + O Open repository\n \
527 M Set branching model\n \
528 \n\
529 Layout/panels\n \
530 \n \
531 Left/Right Change panel\n \
532 Tab Panel to fullscreen\n \
533 Esc Return to default view\n \
534 L Toggle horizontal/vertical layout\n \
535 B Toggle show branch list\n \
536 \n\
537 Navigate/select\n \
538 \n \
539 Up/Down Select / navigate / scroll\n \
540 Shift + Up/Down Navigate fast\n \
541 Home/End Navigate to HEAD/last\n \
542 Ctrl + Up/Down Secondary selection (compare arbitrary commits)\n \
543 Backspace Clear secondary selection\n \
544 Ctrl + Left/Right Scroll horizontal\n \
545 Enter Jump to selected branch/tag\n \
546 \n\
547 Search\n \
548 \n \
549 F3/Ctrl+F Open search dialog\n \
550 F3 Continue search\n \
551 \n\
552 Diffs panel\n \
553 \n \
554 +/- Increase/decrease number of diff context lines\n \
555 D/N/O Show diff or new/old version of file\n \
556 Ctrl + L Toggle line numbers\n \
557 Ctrl + W Toggle line wrapping\n \
558 S Toggle syntax highlighting (new/old file only, turn off if too slow)",
559 )
560 .block(block)
561 .scroll((scroll, 0));
562
563 f.render_widget(paragraph, target);
564}
565
566fn draw_error_dialog<B: Backend>(f: &mut Frame<B>, target: Rect, error: &str, color: bool) {
567 let mut block = Block::default()
568 .title(" Error - Press Enter to continue ")
569 .borders(Borders::ALL)
570 .border_type(BorderType::Thick);
571
572 if color {
573 block = block.border_style(Style::default().fg(Color::LightRed));
574 }
575
576 let paragraph = Paragraph::new(error).block(block).wrap(Wrap { trim: true });
577
578 let area = centered_rect(60, 12, target);
579 f.render_widget(Clear, area);
580 f.render_widget(paragraph, area);
581}
582
583fn draw_search_dialog<B: Backend>(f: &mut Frame<B>, target: Rect, search: &Option<String>) {
584 let block = Block::default()
585 .title(" Search - Search with Enter, abort with Esc ")
586 .borders(Borders::ALL)
587 .border_type(BorderType::Thick);
588
589 let empty = "".to_string();
590 let text = &search.as_ref().unwrap_or(&empty)[..];
591 let paragraph = Paragraph::new(format!("{}_", text))
592 .block(block)
593 .wrap(Wrap { trim: true });
594
595 let area = centered_rect(60, 12, target);
596 f.render_widget(Clear, area);
597 f.render_widget(paragraph, area);
598}
599
600fn centered_rect(size_x: u16, size_y: u16, r: Rect) -> Rect {
603 let size_x = std::cmp::min(size_x, r.width);
604 let size_y = std::cmp::min(size_y, r.height);
605
606 let popup_layout = Layout::default()
607 .direction(Direction::Vertical)
608 .constraints(
609 [
610 Constraint::Length((r.height - size_y) / 2),
611 Constraint::Min(size_y),
612 Constraint::Length((r.height - size_y) / 2),
613 ]
614 .as_ref(),
615 )
616 .split(r);
617
618 Layout::default()
619 .direction(Direction::Horizontal)
620 .constraints(
621 [
622 Constraint::Length((r.width - size_x) / 2),
623 Constraint::Min(size_x),
624 Constraint::Length((r.width - size_x) / 2),
625 ]
626 .as_ref(),
627 )
628 .split(popup_layout[1])[1]
629}