Expanded shortcut groups previously differed from their collapsed form primarily through parentheses and an underlined key. That cue was easy to miss in the dense history footer, particularly when deciding whether the next key would invoke a direct action or a prefixed one. Reverse every cell belonging to the open view, edit, or information group while preserving its existing colors, dimmed toggles, and underlines. Keep separators and neighboring top-level actions outside the reversed range so the active mode has a strong, theme-independent boundary.
Unified · Split
| @@ -219,8 +219,9 @@ for signature verification, alignment, message, and changes actions, but these · 218 unmodified lines | ||
| 219 | 219 | actions retain their direct shortcuts while the group is closed. While expanded, |
| 220 | 220 | the group visually contains every following status action through `<enter> diff`, |
| 221 | 221 | including pane switching and navigation; none of those hints is visible while |
| 222 | −the group is closed, and quit remains outside it. The footer | |
| 223 | −underlines the `v` in `view`; while active, `view (` contains | |
| 222 | +the group is closed, and quit remains outside it. The footer underlines the `v` | |
| 223 | +in `view`; an open prefix reverses its complete expanded group for a strong, | |
| 224 | +terminal-theme-independent cue. While active, `view (` contains | |
| 224 | 225 | every applicable display option and a closing `)` so direct shortcuts remain visibly |
| 225 | 226 | outside the prefix. The history status starts with the history position, then the |
| 226 | 227 | `v` prefix and the `e` prefix when it is addressable. Remaining history-level |
| @@ -878,6 +878,7 @@ pub(crate) fn draw_with_worktree( · 877 unmodified lines | ||
| 878 | 878 | } |
| 879 | 879 | } |
| 880 | 880 | edit_prefix_spans.push(Span::raw(")")); |
| 881 | + emphasize_prefix(&mut edit_prefix_spans[1..]); | |
| 881 | 882 | } else if !app.history_display_expanded { |
| 882 | 883 | edit_prefix_spans.push(Span::raw(" · ")); |
| 883 | 884 | edit_prefix_spans.push(Span::styled("e", Style::default().add_modifier(Modifier::UNDERLINED))); |
| @@ -929,6 +930,7 @@ pub(crate) fn draw_with_worktree( · 45 unmodified lines | ||
| 929 | 930 | )); |
| 930 | 931 | } |
| 931 | 932 | view_prefix_spans.push(Span::raw(")")); |
| 933 | + emphasize_prefix(&mut view_prefix_spans[1..]); | |
| 932 | 934 | } else { |
| 933 | 935 | view_prefix_spans.push(Span::raw(" · ")); |
| 934 | 936 | view_prefix_spans.push(Span::styled("v", Style::default().add_modifier(Modifier::UNDERLINED))); |
| @@ -946,6 +948,7 @@ pub(crate) fn draw_with_worktree( · 11 unmodified lines | ||
| 946 | 948 | ordered.push(Span::raw(" · ")); |
| 947 | 949 | ordered.extend(shortcut("refs", 'r', app.ref_mode != RefMode::None)); |
| 948 | 950 | ordered.push(Span::raw(" · ")); |
| 951 | + let information_prefix_start = app.information_expanded.then_some(ordered.len()); | |
| 949 | 952 | ordered.push(Span::styled("?", Style::default().add_modifier(Modifier::UNDERLINED))); |
| 950 | 953 | if app.information_expanded { |
| 951 | 954 | ordered.push(Span::raw(" (")); |
| @@ -981,6 +984,7 @@ pub(crate) fn draw_with_worktree( · 29 unmodified lines | ||
| 981 | 984 | footer_spans.push(Span::raw(" · <enter> diff")); |
| 982 | 985 | } |
| 983 | 986 | footer_spans.push(Span::raw(")")); |
| 987 | + emphasize_prefix(&mut footer_spans[information_prefix_start.expect("the prefix is expanded")..]); | |
| 984 | 988 | } |
| 985 | 989 | if app.changes_focus.is_none() { |
| 986 | 990 | footer_spans.push(Span::raw(" · ")); |
| @@ -1623,6 +1627,12 @@ fn shortcut(label: &'static str, key: char, enabled: bool) -> Vec<Span<'static>> · 636 unmodified lines | ||
| 1623 | 1627 | ] |
| 1624 | 1628 | } |
| 1625 | 1629 | |
| 1630 | +fn emphasize_prefix(spans: &mut [Span<'_>]) { | |
| 1631 | + for span in spans { | |
| 1632 | + span.style = span.style.add_modifier(Modifier::REVERSED); | |
| 1633 | + } | |
| 1634 | +} | |
| 1635 | + | |
| 1626 | 1636 | fn notification_discs(spans: Vec<Span<'_>>) -> Vec<Span<'_>> { |
| 1627 | 1637 | let mut out = Vec::with_capacity(spans.len()); |
| 1628 | 1638 | for span in spans { |
| @@ -2924,6 +2934,7 @@ mod tests { · 1295 unmodified lines | ||
| 2924 | 2934 | .contains(Modifier::UNDERLINED), |
| 2925 | 2935 | "the prefix key is underlined in its verb" |
| 2926 | 2936 | ); |
| 2937 | + assert_reversed_group(&terminal, 1, view); | |
| 2927 | 2938 | |
| 2928 | 2939 | app.history_display_expanded = false; |
| 2929 | 2940 | app.edit_expanded = true; |
| @@ -2932,6 +2943,7 @@ mod tests { · 2 unmodified lines | ||
| 2932 | 2943 | rendered_line(&terminal, 1).starts_with("0 commits · view · edit (no actions) · copy · refs · ?"), |
| 2933 | 2944 | "the edit prefix follows the view prefix even when no action is available" |
| 2934 | 2945 | ); |
| 2946 | + assert_reversed_group(&terminal, 1, "edit (no actions)"); | |
| 2935 | 2947 | |
| 2936 | 2948 | app.edit_expanded = false; |
| 2937 | 2949 | app.information_expanded = true; |
| @@ -2943,6 +2955,11 @@ mod tests { · 5 unmodified lines | ||
| 2943 | 2955 | ), |
| 2944 | 2956 | "the information prefix contains every following action except quit: {footer}" |
| 2945 | 2957 | ); |
| 2958 | + assert_reversed_group( | |
| 2959 | + &terminal, | |
| 2960 | + 1, | |
| 2961 | + "? ([ align · message · changes · Esc cancel · ↑↓/jk move · h/l pan · <enter> diff)", | |
| 2962 | + ); | |
| 2946 | 2963 | Ok(()) |
| 2947 | 2964 | } |
| 2948 | 2965 | |
| @@ -5108,6 +5125,24 @@ mod tests { · 2159 unmodified lines | ||
| 5108 | 5125 | }) |
| 5109 | 5126 | } |
| 5110 | 5127 | |
| 5128 | + fn assert_reversed_group(terminal: &Terminal<TestBackend>, y: u16, group: &str) { | |
| 5129 | + let line = rendered_line(terminal, y); | |
| 5130 | + let start = line[..line.find(group).expect("the active prefix group is visible")] | |
| 5131 | + .chars() | |
| 5132 | + .count() as u16; | |
| 5133 | + let end = start + group.chars().count() as u16; | |
| 5134 | + let buffer = terminal.backend().buffer(); | |
| 5135 | + assert!( | |
| 5136 | + (start..end).all(|x| buffer[(x, y)].modifier.contains(Modifier::REVERSED)), | |
| 5137 | + "every cell in {group:?} is reversed" | |
| 5138 | + ); | |
| 5139 | + assert!( | |
| 5140 | + !buffer[(start - 1, y)].modifier.contains(Modifier::REVERSED) | |
| 5141 | + && !buffer[(end, y)].modifier.contains(Modifier::REVERSED), | |
| 5142 | + "the active treatment is bounded to {group:?}" | |
| 5143 | + ); | |
| 5144 | + } | |
| 5145 | + | |
| 5111 | 5146 | fn footer_is_dim(terminal: &Terminal<TestBackend>, label: &str) -> bool { |
| 5112 | 5147 | let y = terminal.backend().buffer().area.height - 1; |
| 5113 | 5148 | let footer = rendered_line(terminal, y); |