use ratatui::{
Frame,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
};
use unicode_width::UnicodeWidthStr;
use crate::{theme::Theme, ui::centered_rect};
struct HelpSection<'a> {
title: &'a str,
shuire: bool,
items: &'a [(&'a str, &'a str)],
}
pub fn render(f: &mut Frame, theme: &Theme) {
let area = centered_rect(80, 80, f.area());
f.render_widget(Clear, area);
let sections: [HelpSection; 3] = [
HelpSection {
title: "Navigation",
shuire: false,
items: &[
("j / k", "Next / previous line"),
("Ctrl-d / u", "Half page down / up"),
("Ctrl-f / b", "One page down / up"),
("gg / G", "Top / bottom"),
("n / p", "Next / prev hunk (search match)"),
("N / P", "Next / prev comment"),
("h / l", "Focus files / diff"),
("Tab / S-Tab", "Next / prev file"),
("] / [", "Next / prev file"),
("} / {", "Last / first file"),
(".", "Center cursor"),
],
},
HelpSection {
title: "Comments",
shuire: true,
items: &[
("c / i", "Comment on selected line"),
("V → c", "Multi-line select → comment"),
("Enter / Ctrl-S", "Save comment"),
("S-Enter / C-Enter / C-j", "Newline"),
("i", "Edit focused comment"),
("dd", "Delete focused comment"),
("N / P", "Next / prev comment"),
("y / Y", "Copy one / all"),
("e", "Open in $EDITOR"),
("C", "Comment list"),
],
},
HelpSection {
title: "View / search",
shuire: false,
items: &[
("/", "Search (diff) / filter (files)"),
("s", "Toggle side-by-side"),
("F", "Toggle file list"),
("T", "Cycle theme (classic/washi/sumi)"),
("1 / 2 / 3", "Classic / Washi / Sumi direct"),
(":", "Revision selector"),
("< / >", "File list width"),
("v", "Toggle file read state"),
("R", "Reload"),
("?", "This help"),
("q / Ctrl-c", "Quit"),
],
},
];
const COLS: usize = 2;
let inner_w = area.width.saturating_sub(2) as usize;
let col_w = inner_w / COLS;
let mut lines: Vec<Line> = Vec::new();
let shuire_bold = Style::default()
.fg(theme.shuire)
.add_modifier(Modifier::BOLD);
let shuire_line = Style::default().fg(theme.shuire);
lines.push(Line::from(vec![
Span::styled(" ╱╲ ", shuire_bold),
Span::styled(" 朱入レ", shuire_bold),
Span::styled(" / shuire ", Style::default().fg(theme.dim_fg)),
Span::styled("— git diff review", Style::default().fg(theme.context_fg)),
]));
lines.push(Line::from(Span::styled(" ╱ ╲ ", shuire_line)));
lines.push(Line::from(Span::styled(" ╱ ╲ ", shuire_line)));
let rule_w = inner_w.saturating_sub(4);
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled("━".repeat(rule_w), Style::default().fg(theme.shuire_dim)),
]));
lines.push(Line::from(""));
const KEY_W: usize = 14;
for (row_idx, section_row) in sections.chunks(COLS).enumerate() {
if row_idx > 0 {
lines.push(Line::from(""));
}
let mut header_spans: Vec<Span> = Vec::new();
for s in section_row {
let prefix = if s.shuire { "朱 " } else { " " };
let color = if s.shuire {
theme.shuire
} else {
theme.context_fg
};
let title = format!("{prefix}{title}", title = s.title);
let pad = col_w.saturating_sub(title.width());
header_spans.push(Span::styled(
title,
Style::default().fg(color).add_modifier(Modifier::BOLD),
));
header_spans.push(Span::raw(" ".repeat(pad)));
}
lines.push(Line::from(header_spans));
let mut div_spans: Vec<Span> = Vec::new();
for s in section_row {
let color = if s.shuire {
theme.shuire_dim
} else {
theme.border_unfocused
};
let bar_w = col_w.saturating_sub(2);
div_spans.push(Span::styled("─".repeat(bar_w), Style::default().fg(color)));
div_spans.push(Span::raw(" "));
}
lines.push(Line::from(div_spans));
let max_items = section_row.iter().map(|s| s.items.len()).max().unwrap_or(0);
for i in 0..max_items {
let mut row: Vec<Span> = Vec::new();
for s in section_row {
let key_color = if s.shuire {
theme.shuire
} else {
theme.header_context_fg
};
if let Some((k, d)) = s.items.get(i) {
let k_w = k.width();
let key_used = (k_w + 1).max(KEY_W);
let key_text = format!("{k}{}", " ".repeat(key_used - k_w));
row.push(Span::styled(
key_text,
Style::default().fg(key_color).add_modifier(Modifier::BOLD),
));
let desc_w = col_w.saturating_sub(key_used);
let end = crate::ui::byte_offset_at_width(d, desc_w);
let truncated = &d[..end];
let used = truncated.width();
row.push(Span::styled(
truncated.to_string(),
Style::default().fg(theme.dialog_body_fg),
));
if desc_w > used {
row.push(Span::raw(" ".repeat(desc_w - used)));
}
} else {
row.push(Span::raw(" ".repeat(col_w)));
}
}
lines.push(Line::from(row));
}
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
" 朱入レ (shu-ire) — marking up a manuscript with a red pen.",
Style::default()
.fg(theme.dim_fg)
.add_modifier(Modifier::ITALIC),
)));
let block = Block::default()
.borders(Borders::ALL)
.title(Span::styled(
" 朱入レ · Help (? to close) ",
Style::default()
.fg(theme.bg)
.bg(theme.shuire)
.add_modifier(Modifier::BOLD),
))
.border_style(Style::default().fg(theme.shuire).bg(theme.bg_alt))
.style(Style::default().bg(theme.bg_alt));
let paragraph = Paragraph::new(lines).block(block);
f.render_widget(paragraph, area);
}