use crate::app::{App, Mode};
use crate::repo::RemoteInfo;
use crate::ui::layout::{centered_rect, centered_rect_fixed};
use crate::ui::style::{
ACCENT, CARD_BORDER, DANGER, SUCCESS, WARNING, accent_style, muted_style, parse_color,
primary_style,
};
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{
Block, BorderType, Borders, Clear, Gauge, List, ListItem, ListState, Padding, Paragraph, Wrap,
};
pub fn get_help_lines(app: &App) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let is_compat = app.config.compatibility_mode;
let format_key = |k: &str| {
let mut key = k.to_string();
if is_compat {
key = key
.replace("↑", "^")
.replace("↓", "v")
.replace("⇟", "PgDn")
.replace("⇞", "PgUp")
.replace("↵", "Enter")
.replace("→", ">")
.replace("⎋", "Esc")
.replace("⌫", "Backspace")
.replace("⇥", "Tab")
.replace("⇧⇥", "Shift+Tab")
.replace("⇧", "Shift+");
}
key
};
let categories: Vec<(&str, Vec<(&str, &str)>)> = vec![
(
"Global & Navigation",
vec![
("↑ [Up] / k", "Move selection up / scroll up"),
("↓ [Down] / j", "Move selection down / scroll down"),
("⇞ [PgUp]", "Jump one page up"),
("⇟ [PgDn]", "Jump one page down"),
("Home", "Go to top / scroll to top"),
("End", "Go to bottom / scroll to bottom"),
("⎋ [Esc]", "Cancel input, close dialog, leave detail view, or quit"),
("?", "Toggle this help overlay"),
("v", "Show about popup / creator profile"),
("q", "Quit (also closes detail view)"),
],
),
(
"Main List Operations",
vec![
("a", "Add a new repository"),
("A", "Bulk add folders in a directory"),
("i", "Import remote repository"),
("e", "Edit selected item"),
("d", "Delete selected item"),
("f", "Enter repository search mode"),
("R", "Refresh status of selected item"),
("o / O", "Cycle sorting mode / Toggle reverse sorting"),
("p", "Toggle pin status of selected item"),
("g", "Launch preferred Git client for selected repository"),
("s", "Open options/settings page"),
("l", "Open debug logs panel"),
("⌫ [Backspace]", "Erase character while typing"),
],
),
(
"Repository Detail View",
vec![
("↵ [Enter] / → [Right]", "Open detail view / Stage file"),
("⇥ [Tab] / ⇧⇥", "Cycle detail view tabs"),
("w / W", "Cycle panel focus forward (w) / backward (W)"),
("R", "Resync active tab (Detail)"),
],
),
(
"Git Operations (Detail)",
vec![
(
"c / C",
"Commit (c) / Amend last commit (C) (Workspace) / Create branch (Branches)",
),
("s", "Stash changes (Workspace changes or Stashes tab)"),
("p", "Pull branch (Branches) / Push tag (Tags)"),
("⇧P", "Push branch (Branches) / Push all tags (Tags)"),
("f / F", "Fetch remote (Branches / Tags / Remotes tabs)"),
],
),
(
"Mouse Interactions",
vec![
("Left-Click", "Focus clicked panel / change tab (mouse support)"),
("Left-Click+Drag", "Drag boundaries to resize split panels"),
],
),
];
let mut max_key_width = 0;
for (_, keys) in &categories {
for (key, _) in keys {
let width = format_key(key).chars().count();
if width > max_key_width {
max_key_width = width;
}
}
}
for (cat_title, keys) in categories {
lines.push(Line::from(""));
let title_style = if is_compat {
Style::default().add_modifier(Modifier::BOLD)
} else {
primary_style().add_modifier(Modifier::BOLD)
};
let prefix = if is_compat { "=== " } else { "■ " };
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(prefix, if is_compat { Style::default() } else { accent_style() }),
Span::styled(cat_title.to_uppercase(), title_style),
]));
for (key, desc) in keys {
let k_str = format_key(key);
let padded_key = format!("{:>width$}", k_str, width = max_key_width);
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(padded_key, accent_style()),
Span::raw(" "),
Span::raw(desc.to_string()),
]));
}
}
lines.push(Line::from(""));
let title_style = if is_compat {
Style::default().add_modifier(Modifier::BOLD)
} else {
primary_style().add_modifier(Modifier::BOLD)
};
let prefix = if is_compat { "=== " } else { "■ " };
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(prefix, if is_compat { Style::default() } else { accent_style() }),
Span::styled("STATUS INDICATORS", title_style),
]));
let mut pad_symbol = |sym: &str, color: Color, label: &'static str, desc: &'static str| {
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(sym.to_string(), Style::default().fg(color)),
Span::raw(" "),
Span::styled(format!("{:<8}", label), muted_style()),
Span::raw(desc),
]));
};
pad_symbol(app.sym("bullet_filled"), SUCCESS(), "git", "Directory is a git repository");
pad_symbol(app.sym("bullet_empty"), WARNING(), "dir", "Directory exists but is not a git repo");
pad_symbol(app.sym("close"), DANGER(), "missing", "Path does not exist or is not a directory");
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(prefix, if is_compat { Style::default() } else { accent_style() }),
Span::styled("REPO STATE SUFFIXES", title_style),
]));
let mut pad_suffix = |sym: &str, style: Style, desc: &'static str| {
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(format!("N{}", sym), style),
Span::raw(" "),
Span::raw(desc),
]));
};
pad_suffix("+", Style::default().fg(ACCENT()), "files staged for commit");
pad_suffix("!", Style::default().fg(WARNING()), "files modified but not staged");
pad_suffix("?", muted_style(), "untracked files");
pad_suffix(app.sym("up"), primary_style(), "commits ahead of upstream (need push)");
pad_suffix(app.sym("down"), Style::default().fg(WARNING()), "commits behind upstream");
lines.push(Line::from(""));
lines
}
pub fn get_help_lines_len(app: &App) -> usize {
get_help_lines(app).len()
}
pub fn draw_help_overlay(f: &mut Frame, app: &App, area: Rect, scroll: usize) {
let popup_area = centered_rect(60, 70, area);
let lines = get_help_lines(app);
let help_block = Block::default()
.borders(Borders::ALL)
.border_type(CARD_BORDER())
.border_style(Style::default().fg(ACCENT()))
.title(
Line::from(vec![
Span::raw(" "),
Span::styled("Shortcuts & Legend", accent_style()),
Span::raw(" "),
])
.alignment(Alignment::Left),
)
.padding(Padding::horizontal(1));
let inner_height = popup_area.height.saturating_sub(2) as usize;
let max_scroll = lines.len().saturating_sub(inner_height);
let scroll = scroll.min(max_scroll);
let lines_len = lines.len();
let help = Paragraph::new(lines)
.block(help_block)
.wrap(Wrap { trim: false })
.scroll((scroll as u16, 0));
f.render_widget(Clear, popup_area);
f.render_widget(help, popup_area);
crate::ui::scrollbar::draw_vertical_scrollbar(f, popup_area, scroll, lines_len, inner_height);
}
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
pub struct HelpPopup;
impl HelpPopup {
pub fn handle_event(app: &mut crate::app::App, key: KeyEvent) -> bool {
let code = key.code;
match code {
KeyCode::Char('?') | KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
app.close_dialog();
}
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
app.help_scroll_up();
}
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
app.help_scroll_down();
}
KeyCode::PageUp => {
app.help_scroll_page_up(app.config.page_size);
}
KeyCode::PageDown => {
app.help_scroll_page_down(app.config.page_size);
}
KeyCode::Home => {
app.help_scroll_to_top();
}
KeyCode::End => {
app.help_scroll_to_bottom();
}
_ => {}
}
false
}
}
pub struct DetailHelpPopup;
impl DetailHelpPopup {
pub fn handle_event(app: &mut crate::app::App, key: KeyEvent) -> bool {
let code = key.code;
match code {
KeyCode::Char('?') | KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
app.close_detail_help();
}
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
app.help_scroll_up();
}
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
app.help_scroll_down();
}
KeyCode::PageUp => {
app.help_scroll_page_up(app.config.page_size);
}
KeyCode::PageDown => {
app.help_scroll_page_down(app.config.page_size);
}
KeyCode::Home => {
app.help_scroll_to_top();
}
KeyCode::End => {
app.help_scroll_to_bottom();
}
_ => {}
}
false
}
}