Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ legend = true
| `show_file_list` | `true` | Whether the file list panel is visible on startup. Toggle with `<leader>e`. |
| `show_commits` | `true` | Whether the inline commit selector pane is visible on startup for multi-commit reviews. Toggle with `<leader>s` or `:set commits!`. |
| `mouse` | `true` | Wheel scrolling, clicks, and drag-to-select. |
| `leader` | `;` | Single-character prefix for panel focus, sidebar toggles, and review-comment shortcuts. Invalid multi-character values are ignored with a startup warning. |
| `leader` | `;` | Single-character prefix for panel focus, sidebar toggles, and review-comment shortcuts. Whitespace leaders such as `leader = " "` are shown as `<space>` / `<tab>` in the in-app help. Invalid multi-character values are ignored with a startup warning. |
| `comment_vim` | `false` | Vim modal editing in the comment box; toggle at runtime with `:vim`. When off, default emacs/readline bindings. |
| `comment_tab_width` | `4` | Spaces inserted by Tab while typing in the vim comment box (Insert mode). |
| `wrap` | `false` | Line wrap in the diff view. Toggle with `:set wrap!`. |
Expand Down
1 change: 1 addition & 0 deletions docs/KEYBINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Full reference. Press `?` inside tuicr for an in-app version of this list.

`<leader>` defaults to `;`. Override it with `leader = ","` in `~/.config/tuicr/config.toml`.
Whitespace leaders such as `leader = " "` show up as `<space>` (or `<tab>`) in the in-app help.

## Navigation

Expand Down
21 changes: 10 additions & 11 deletions src/ui/help_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use ratatui::{

use crate::app::App;
use crate::ui::styles;
use crate::ui::text_utils::{key_label, shortcut_column};

pub fn render_help(frame: &mut Frame, app: &mut App) {
let leader = key_label(app.leader_key);
let theme = &app.theme;
// Center over the diff pane (matches the submit-modal anchoring) so the
// file list doesn't tug the popup's visual centre off to one side. Fall
Expand Down Expand Up @@ -135,35 +137,35 @@ pub fn render_help(frame: &mut Frame, app: &mut App) {
]),
Line::from(vec![
Span::styled(
format!(" {}h/{}l ", app.leader_key, app.leader_key),
shortcut_column(&format!("{leader}h/{leader}l")),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw("Focus file list/diff"),
]),
Line::from(vec![
Span::styled(
format!(" {}k/{}j ", app.leader_key, app.leader_key),
shortcut_column(&format!("{leader}k/{leader}j")),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw("Move focus up/down between panes"),
]),
Line::from(vec![
Span::styled(
format!(" {}e ", app.leader_key),
shortcut_column(&format!("{leader}e")),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw("Toggle file list visibility"),
]),
Line::from(vec![
Span::styled(
format!(" {}s ", app.leader_key),
shortcut_column(&format!("{leader}s")),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw("Toggle commit selector visibility (also `:set commits!`)"),
]),
Line::from(vec![
Span::styled(
format!(" {}f ", app.leader_key),
shortcut_column(&format!("{leader}f")),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw("Toggle single-file view (also `:focus` / `:f`)"),
Expand All @@ -188,7 +190,7 @@ pub fn render_help(frame: &mut Frame, app: &mut App) {
]),
Line::from(""),
Line::from(Span::styled(
format!("Single-file view (`:focus`, `:f`, {}f)", app.leader_key),
format!("Single-file view (`:focus`, `:f`, {leader}f)"),
Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)),
Line::from(""),
Expand Down Expand Up @@ -411,7 +413,7 @@ pub fn render_help(frame: &mut Frame, app: &mut App) {
]),
Line::from(vec![
Span::styled(
format!(" {}c ", app.leader_key),
shortcut_column(&format!("{leader}c")),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw("Add review comment"),
Expand Down Expand Up @@ -634,10 +636,7 @@ pub fn render_help(frame: &mut Frame, app: &mut App) {
" :focus ",
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(
"Toggle single-file view (alias `:f`, {}f)",
app.leader_key
)),
Span::raw(format!("Toggle single-file view (alias `:f`, {leader}f)")),
]),
Line::from(vec![
Span::styled(
Expand Down
74 changes: 74 additions & 0 deletions src/ui/text_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ pub(super) fn truncate_or_pad(s: &str, width: usize) -> String {
}
}

/// Display label for a key that has no visible glyph, so shortcuts stay
/// readable when the leader is configured as whitespace.
pub(super) fn key_label(key: char) -> String {
match key {
' ' => "<space>".to_string(),
'\t' => "<tab>".to_string(),
other => other.to_string(),
}
}

const KEY_COLUMN_WIDTH: usize = 10;

/// Help popup key gutter: two-space indent plus a padded key column, keeping
/// at least one space before the description when a label overflows it.
pub(super) fn shortcut_column(keys: &str) -> String {
let width = keys.width();
if width >= KEY_COLUMN_WIDTH {
format!(" {keys} ")
} else {
format!(" {keys}{}", " ".repeat(KEY_COLUMN_WIDTH - width))
}
}

/// Truncate or pad highlighted spans to a specific display width
/// Uses unicode width to properly handle wide characters (CJK, emoji, etc.)
/// Returns a vector of spans that fits exactly within the width
Expand Down Expand Up @@ -298,6 +321,57 @@ mod tests {
assert!(result.is_char_boundary(result.len()));
}

#[test]
fn should_render_printable_key_as_itself() {
// given
let key = ';';
// when
let label = key_label(key);
// then
assert_eq!(label, ";");
}

#[test]
fn should_label_space_key_readably() {
// given - `leader = " "` in config.toml
let key = ' ';
// when
let label = key_label(key);
// then
assert_eq!(label, "<space>");
}

#[test]
fn should_label_tab_key_readably() {
// given
let key = '\t';
// when
let label = key_label(key);
// then
assert_eq!(label, "<tab>");
}

#[test]
fn should_pad_shortcut_column_to_gutter_width() {
// given - the default leader chord shown in the help popup
let keys = ";e";
// when
let column = shortcut_column(keys);
// then - two-space indent plus a 10-cell key field
assert_eq!(column, " ;e ");
assert_eq!(column.width(), 12);
}

#[test]
fn should_keep_a_separator_space_when_shortcut_overflows_gutter() {
// given - a chord built from two <space> labels blows past the gutter
let keys = "<space>h/<space>l";
// when
let column = shortcut_column(keys);
// then - nothing is dropped and the description stays detached
assert_eq!(column, " <space>h/<space>l ");
}

#[test]
fn should_pad_highlighted_spans_to_exact_width() {
// given - highlighted spans from the syntax highlighter (which strips
Expand Down
Loading