use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use unicode_width::UnicodeWidthStr;
use crate::theme::Colors;
const SHORTCUTS: &[(&str, &[(&str, &str)])] = &[
(
"Everywhere",
&[
("?", "This help, and settings"),
(":", "Type a command: secret, key, timer, leave or logout"),
("s", "Find a chat or person: a name, @username or t.me link"),
("H", "Highlight or unhighlight the selected chat"),
(
"Ctrl-o / Ctrl-i",
"Back to the chat or reply you left / forward again",
),
(
"Ctrl-r",
"Resize the panes: h / l, = as at first, Enter keeps, Esc cancels",
),
("q", "Quit (press again to stop waiting)"),
("Ctrl-c", "Quit, except while writing"),
],
),
(
"Chat list",
&[
("j / k", "Move down / up"),
("gg / G", "First / last chat"),
("Ctrl-d / Ctrl-u", "Half a page down / up"),
(
"Enter / l",
"Open the chat (Enter / h with the list on the right)",
),
("i", "Open the chat and write"),
(
"Tab / Shift-Tab",
"Next / previous folder, then the archive",
),
("/", "Filter chats by name"),
("p", "Pin or unpin the chat (on all your devices)"),
("m", "Mute or unmute the chat"),
("Esc", "Clear the filter"),
],
),
(
"Forum topics",
&[
("j / k", "Move down / up"),
("gg / G", "First / last topic"),
(
"Enter / l",
"Open the topic's messages (Enter / h with the list on the right)",
),
("i", "Open the topic and write"),
("Ctrl-r", "Resize the topics pane: h / l, = as at first"),
(
"h / Esc",
"Back to the chat list (l with the list on the right)",
),
],
),
(
"Messages",
&[
("j / k", "Newer / older message"),
("gg / G", "Oldest / newest message"),
("Ctrl-d / Ctrl-u", "Half a page newer / older"),
(
"Enter",
"Show spoilers, view a photo shown only while open, play or pause a voice message, vote in a poll, list a bot's buttons, show the photo, or open the file or link",
),
("y", "Copy the text, a link, or the photo or file"),
("r", "Reply"),
("f", "Forward to another chat (an album goes whole)"),
("e", "Edit your message"),
(
"R",
"React, or take your reaction back (/ finds an emoji by name)",
),
("X", "Take back all your reactions"),
("d", "Delete"),
("P", "Pin the message (asks how), or unpin it"),
("gp", "Pinned messages: Enter goes to one, P unpins it"),
("gd", "Go to the message a reply answers"),
(
"Tab",
"Forward again, as Ctrl-i (most terminals send Ctrl-i as Tab)",
),
("i", "Write a message"),
("a", "Attach a file by its path (Tab completes it)"),
("p", "Paste a photo, files or text from the clipboard"),
("/", "Search the whole chat"),
(
"from: has:",
"In a search: from:@alice, from:me or from:a name; has:photo (video, media, file, link, voice, gif, audio)",
),
(
"before: after:",
"In a search: before:2025-10-01, after:2025-09-01 (that day counts)",
),
("n / N", "Next older / newer match"),
(
"Esc",
"End the search, cancel the edit, remove the files, cancel the reply, or go back",
),
(
"h",
"Back to the chat list, or a forum's topics (l with the list on the right)",
),
],
),
(
"Writing",
&[
("Enter", "Send, or save the edit"),
("Alt-Enter / Ctrl-j", "New line"),
("Ctrl-u", "Delete back to the start of the line"),
(
"**bold**",
"Formatting: __italic__ ~~strike~~ ||spoiler|| `code` ```block``` [text](url)",
),
(
"@name / :smile",
"Suggests people in the group, or emoji; Tab puts one in",
),
("/command", "Starting a message: the bots' commands"),
("Ctrl-v", "Paste a photo, files or text from the clipboard"),
(
"Drop a file",
"Attach it; the text you write is its caption",
),
(
"Ctrl-z",
"Turn files just pasted back into their path as text",
),
(
"Ctrl-t",
"Send attached photos as files, uncompressed, or back",
),
("Tab", "Stickers"),
("Esc / Ctrl-c", "Back to Normal mode"),
],
),
(
"Stickers",
&[
("h / j / k / l", "Move"),
("H / L", "Previous / next tab: Recent, Favorites, your sets"),
("Enter", "Send the sticker, or the reply"),
("/", "Find stickers by emoji or word"),
("Tab / Esc", "Back to writing"),
],
),
(
"Photo viewer",
&[
("h / l", "The photo before / after it in the chat"),
(
"j / k",
"Zoom in / out (also + / -), up to filling the window",
),
("o", "Open the photo in your computer's viewer"),
("y", "Copy the photo"),
("Enter / Esc / q", "Close"),
],
),
(
"Bot buttons",
&[
("h / j / k / l", "Move, as the buttons are laid out"),
("Tab / Shift-Tab", "Next / previous button"),
(
"Enter",
"Press it: the bot answers, a link opens (asking first), a reply button sends its words",
),
("Esc / q", "Close"),
],
),
(
"Secret chats",
&[
(
":secret",
"Start one with the person in this chat (asks first); it's kept on this computer only",
),
(
":key",
"Its key as a picture, to compare with the other person's",
),
(":timer", "How long new messages last once they're seen"),
(
":leave",
"End it for both of you, and delete it here (asks first)",
),
(
"Enter",
"On a 🔥 photo: view it; it's covered again when you move on",
),
],
),
(
"Search and command prompts",
&[
("Enter", "Search, keep the chat filter, or run the command"),
(
"Tab / Shift-Tab",
"Complete a command or a search filter (again for the next one), or the path of a file to attach",
),
("Esc / Ctrl-c", "Cancel"),
],
),
(
"Forward and find popups",
&[
("Type", "Search chats by name, or @username"),
("Up / Down", "Move (also Ctrl-p / Ctrl-n, Tab)"),
("Enter", "Forward there, or open it"),
("Esc", "Cancel"),
],
),
(
"Menus and popups",
&[
("j / k", "Move"),
("Enter", "Choose"),
("1-9", "Choose by number"),
(
"Space",
"Turn a setting on or off, or use a theme (Enter too); saved at once",
),
("Esc / q", "Close"),
("Tab / h / l", "Switch tabs in this popup"),
(
"y / n",
"Go ahead or not, when asked about a file, a link or joining",
),
],
),
];
fn key_width() -> usize {
SHORTCUTS
.iter()
.flat_map(|(_, keys)| keys.iter())
.map(|(key, _)| key.width())
.max()
.unwrap_or(0)
}
fn lines(colors: &Colors) -> Vec<Line<'static>> {
let key_width = key_width();
let mut lines = Vec::new();
for (i, (group, keys)) in SHORTCUTS.iter().enumerate() {
if i > 0 {
lines.push(Line::default());
}
lines.push(Line::from(format!(" {group}")).fg(colors.accent).bold());
for (key, what) in keys.iter() {
lines.push(Line::from(vec![
Span::from(format!(" {key:<key_width$} ")).fg(colors.primary),
Span::from(*what),
]));
}
}
lines
}
pub fn width() -> usize {
let what = SHORTCUTS
.iter()
.flat_map(|(_, keys)| keys.iter())
.map(|(_, what)| what.width())
.max()
.unwrap_or(0);
3 + key_width() + 2 + what + 1
}
pub fn height() -> usize {
SHORTCUTS
.iter()
.map(|(_, keys)| keys.len() + 2)
.sum::<usize>()
- 1
}
pub fn draw(frame: &mut Frame, area: Rect, scroll: &mut usize, colors: &Colors) {
let lines = lines(colors);
let max = lines.len().saturating_sub(usize::from(area.height));
*scroll = (*scroll).min(max);
frame.render_widget(Paragraph::new(lines).scroll((*scroll as u16, 0)), area);
super::scrollbar(frame, area, max, *scroll, colors);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn height_counts_every_line() {
assert_eq!(height(), lines(&Colors::default()).len());
}
#[test]
fn width_leaves_room_for_every_line_and_the_scrollbar() {
let widest = lines(&Colors::default())
.iter()
.map(Line::width)
.max()
.unwrap();
assert_eq!(width(), widest + 1);
}
}