From 7bba2c7d0ba4b988d8e7c957d69b56a039e75854 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 23 Jul 2026 07:25:37 +0200 Subject: [PATCH] fix: help-screen drift, dead Shift+D binding, byte/char title truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit '?' help never mentioned the M (Profiles & Sync) menu at all, and documented 'd'/'p'/'c' only for their generic meaning while Stats/ Activity tabs silently overload the same keys for something else — same class of bug the earlier B4 fix addressed, just new drift since. Also: 'D' with an explicit SHIFT-modifier guard never matched, since terminals deliver Shift+d as an already-uppercase char with no modifier flag (same root cause as the earlier Ctrl+A fix) — merged into the identical plain-Delete arm instead of leaving it dead code. Also: PR/issue title continuation used title.len() (bytes) to decide whether to render a second line, while the actual text used .chars() — a non-ASCII title could get a spurious blank line. --- src/hub/keys_dispatch.rs | 20 ++++++-------------- src/hub/render.rs | 13 +++++++++++-- src/hub/right_panel.rs | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/hub/keys_dispatch.rs b/src/hub/keys_dispatch.rs index 84b5f9a..9c4dd63 100644 --- a/src/hub/keys_dispatch.rs +++ b/src/hub/keys_dispatch.rs @@ -362,20 +362,12 @@ pub fn handle_global_key(state: &mut HubState, key: KeyEvent) -> Option { - if state.tab == Tab::Todos { - if let Some(ti) = state.cur_todo_idx() { - let title = state.todos[ti].title.clone(); - state.modal = Modal::Confirm { message: format!("Delete '{}'?", title), tag: ConfirmTag::DeleteTodo(ti) }; - } - } else if state.tab == Tab::Projects { - if let Some(pi) = state.cur_proj_idx() { - let name = state.projects[pi].name.clone(); - state.modal = Modal::Confirm { message: format!("Delete project '{}'?", name), tag: ConfirmTag::DeleteProject(pi) }; - } - } - } - KeyCode::Delete => { + // Terminals deliver Shift+d as an already-uppercase 'D' without a + // separate SHIFT modifier flag on the char event (same class of bug + // as the Ctrl+A fix above) — match the bare char instead of gating + // on a modifier that never actually arrives, and fold the identical + // plain-`Delete` handling into the same arm rather than duplicating it. + KeyCode::Delete | KeyCode::Char('D') => { if state.tab == Tab::Todos { if let Some(ti) = state.cur_todo_idx() { let title = state.todos[ti].title.clone(); diff --git a/src/hub/render.rs b/src/hub/render.rs index 99720f3..98eb023 100644 --- a/src/hub/render.rs +++ b/src/hub/render.rs @@ -30,7 +30,7 @@ pub static HELP_LINES: &[&str] = &[ "── Filtering & Sorting ───────────────────────", " / Fuzzy filter repos (↑↓ move · Enter open · Esc clear)", " S Sort & filter menu", - " d Toggle: show only dirty repos", + " d Toggle: show only dirty repos (Stats tab: preview digest instead)", " b Toggle: show only behind repos", " f Toggle: show only ahead repos", " a Toggle: show archived repos (no commits >90d)", @@ -58,7 +58,7 @@ pub static HELP_LINES: &[&str] = &[ " Space Select repo for batch action", "", "── Projects & Todos ──────────────────────────", - " p Project menu (create / assign / rename / delete)", + " p Project menu (create / assign / rename / delete) (Stats tab: cycle period instead)", " + New todo", " e Edit todo (Todos tab)", " Space (Todos tab) Cycle todo status, or expand/collapse a group", @@ -69,6 +69,15 @@ pub static HELP_LINES: &[&str] = &[ " D / Del Delete todo or project", " T Scan repos for TODO/FIXME/HACK → import to Todos", "", + "── Stats & Activity ──────────────────────────", + " p (Stats tab) Cycle period forward: 1d → 7d → 30d → 90d → 1y → all", + " < (Stats tab) Cycle period backward", + " d (Stats tab) Preview weekly digest", + " c (Activity tab) Cycle activity-kind filter", + "", + "── Settings ───────────────────────────────────", + " M (Settings tab) Profiles & Sync menu — switch/save/push/pull settings", + "", "── Help ──────────────────────────────────────", " ? Show this help", " v (Overview) Open release notes in browser", diff --git a/src/hub/right_panel.rs b/src/hub/right_panel.rs index 6d3b6ae..52a0a6d 100644 --- a/src/hub/right_panel.rs +++ b/src/hub/right_panel.rs @@ -424,7 +424,7 @@ fn render_right_pr(f: &mut Frame, state: &HubState, area: Rect) { lines.push(r_kv("Number", &format!("#{}", pr.number), Color::DarkGray)); lines.push(Line::from("")); lines.push(Line::from(Span::styled(format!(" \"{}\"", pr.title.chars().take(30).collect::()), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)))); - if pr.title.len() > 30 { + if pr.title.chars().count() > 30 { lines.push(Line::from(Span::styled(format!(" {}", pr.title.chars().skip(30).take(30).collect::()), Style::default().fg(Color::White)))); } lines.push(Line::from("")); @@ -459,7 +459,7 @@ fn render_right_issue(f: &mut Frame, state: &HubState, area: Rect) { lines.push(r_kv("Number", &format!("#{}", issue.number), Color::DarkGray)); lines.push(Line::from("")); lines.push(Line::from(Span::styled(format!(" \"{}\"", issue.title.chars().take(30).collect::()), Style::default().fg(Color::White).add_modifier(Modifier::BOLD)))); - if issue.title.len() > 30 { + if issue.title.chars().count() > 30 { lines.push(Line::from(Span::styled(format!(" {}", issue.title.chars().skip(30).take(30).collect::()), Style::default().fg(Color::White)))); } lines.push(Line::from(""));