Skip to main content

gitkraft_tui/widgets/
header.rs

1use ratatui::layout::Rect;
2use ratatui::style::{Modifier, Style};
3use ratatui::text::{Line, Span};
4use ratatui::widgets::{Block, Borders, Paragraph};
5use ratatui::Frame;
6
7use crate::app::{App, AppScreen};
8
9/// Render the top header bar showing repo name, current branch, state, and
10/// keyboard shortcuts.
11pub fn render(app: &mut App, frame: &mut Frame, area: Rect) {
12    if app.screen != AppScreen::Main {
13        return;
14    }
15
16    let theme = app.theme();
17
18    let block = Block::default()
19        .borders(Borders::ALL)
20        .border_style(Style::default().fg(theme.border_inactive))
21        .style(Style::default().bg(theme.border_inactive));
22
23    let tab = app.tab();
24
25    let repo_name = tab
26        .repo_path
27        .as_ref()
28        .and_then(|p| p.file_name())
29        .map(|n| n.to_string_lossy().to_string())
30        .unwrap_or_else(|| "unknown".to_string());
31
32    let branch_name = tab
33        .repo_info
34        .as_ref()
35        .and_then(|info| info.head_branch.clone())
36        .unwrap_or_else(|| "detached".to_string());
37
38    let state = tab
39        .repo_info
40        .as_ref()
41        .map(|info| format!("{}", info.state))
42        .unwrap_or_else(|| "?".to_string());
43
44    let mut spans: Vec<Span> = Vec::new();
45
46    // Show tab indicators if there are multiple tabs
47    if app.tabs.len() > 1 {
48        for (i, t) in app.tabs.iter().enumerate() {
49            let name = t.display_name();
50            let style = if i == app.active_tab_index {
51                Style::default()
52                    .fg(theme.accent)
53                    .add_modifier(Modifier::BOLD)
54            } else {
55                Style::default().fg(theme.text_muted)
56            };
57            spans.push(Span::styled(format!(" {} ", name), style));
58            if i < app.tabs.len() - 1 {
59                spans.push(Span::styled("|", Style::default().fg(theme.text_muted)));
60            }
61        }
62        // Tab switching hint
63        spans.push(Span::styled(
64            " [/]",
65            Style::default()
66                .fg(theme.warning)
67                .add_modifier(Modifier::BOLD),
68        ));
69        spans.push(Span::styled(
70            " switch tab ",
71            Style::default().fg(theme.text_primary),
72        ));
73        spans.push(Span::styled(
74            "│ ",
75            Style::default().fg(theme.text_secondary),
76        ));
77    }
78
79    // When tabs are shown, the active tab name already displays the repo.
80    // Only show the standalone repo name when there's a single tab.
81    if app.tabs.len() <= 1 {
82        spans.push(Span::styled(
83            format!(" {} ", repo_name),
84            Style::default()
85                .fg(theme.text_primary)
86                .add_modifier(Modifier::BOLD),
87        ));
88        spans.push(Span::styled("│", Style::default().fg(theme.text_secondary)));
89    }
90
91    spans.extend([
92        Span::styled(
93            format!("  {} ", branch_name),
94            Style::default()
95                .fg(theme.success)
96                .add_modifier(Modifier::BOLD),
97        ),
98        Span::styled("│", Style::default().fg(theme.text_secondary)),
99        Span::styled(format!(" {} ", state), Style::default().fg(theme.accent)),
100        Span::styled("│", Style::default().fg(theme.text_secondary)),
101        Span::styled(
102            " [←→]",
103            Style::default()
104                .fg(theme.warning)
105                .add_modifier(Modifier::BOLD),
106        ),
107        Span::styled(" pane ", Style::default().fg(theme.text_primary)),
108        Span::styled(
109            "[r]",
110            Style::default()
111                .fg(theme.warning)
112                .add_modifier(Modifier::BOLD),
113        ),
114        Span::styled(" refresh ", Style::default().fg(theme.text_primary)),
115        Span::styled(
116            "[f]",
117            Style::default()
118                .fg(theme.warning)
119                .add_modifier(Modifier::BOLD),
120        ),
121        Span::styled(" fetch ", Style::default().fg(theme.text_primary)),
122        Span::styled(
123            "[T]",
124            Style::default()
125                .fg(theme.warning)
126                .add_modifier(Modifier::BOLD),
127        ),
128        Span::styled(" theme ", Style::default().fg(theme.text_primary)),
129        Span::styled(
130            "[O]",
131            Style::default()
132                .fg(theme.warning)
133                .add_modifier(Modifier::BOLD),
134        ),
135        Span::styled(" options ", Style::default().fg(theme.text_primary)),
136        Span::styled(
137            "[o]",
138            Style::default()
139                .fg(theme.warning)
140                .add_modifier(Modifier::BOLD),
141        ),
142        Span::styled(" open ", Style::default().fg(theme.text_primary)),
143        Span::styled(
144            "[W]",
145            Style::default()
146                .fg(theme.warning)
147                .add_modifier(Modifier::BOLD),
148        ),
149        Span::styled(" close ", Style::default().fg(theme.text_primary)),
150        Span::styled(
151            "[q]",
152            Style::default()
153                .fg(theme.warning)
154                .add_modifier(Modifier::BOLD),
155        ),
156        Span::styled(" quit", Style::default().fg(theme.text_primary)),
157    ]);
158
159    let line = Line::from(spans);
160    let paragraph = Paragraph::new(line).block(block);
161
162    frame.render_widget(paragraph, area);
163}