035c56d2
Fix issue checkout and add status messages in TUI
a73x 2026-03-21 08:18
- o on issue now checks out the closing commit when no linked patch exists (falls back from linked patch head → closed_by OID) - g and o show a yellow status bar message when there's nothing to follow/checkout, instead of silently doing nothing - Status message clears on next keypress Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/src/tui.rs b/src/tui.rs index 88eb978..d0f92f2 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -41,6 +41,7 @@ struct App { pane: Pane, mode: ViewMode, show_all: bool, status_msg: Option<String>, } impl App { @@ -59,6 +60,7 @@ impl App { pane: Pane::ItemList, mode: ViewMode::Details, show_all: false, status_msg: None, } } @@ -132,7 +134,7 @@ impl App { self.pane = Pane::ItemList; } fn follow_link(&mut self) { fn follow_link(&mut self) -> bool { match self.tab { Tab::Issues => { // From an issue, jump to the first patch that fixes it @@ -140,21 +142,20 @@ impl App { if let Some(idx) = self.list_state.selected() { if let Some(issue) = visible.get(idx) { let issue_id = issue.id.clone(); // Find first patch with fixes == this issue let target = self .patches .iter() .enumerate() .find(|(_, p)| p.fixes.as_deref() == Some(&issue_id)); if let Some((patch_idx, _)) = target { // Need to find index in visible patches let visible_patches = self.visible_patches(); let visible_idx = visible_patches .iter() .position(|p| p.fixes.as_deref() == Some(&issue_id)); // If the patch isn't visible (filtered), toggle show_all if visible_idx.is_none() && !self.show_all { self.show_all = true; if !self.show_all { let visible_patches = self.visible_patches(); if !visible_patches .iter() .any(|p| p.fixes.as_deref() == Some(&issue_id)) { self.show_all = true; } } let visible_patches = self.visible_patches(); if let Some(vi) = visible_patches @@ -165,10 +166,12 @@ impl App { self.list_state.select(Some(vi)); self.scroll = 0; self.mode = ViewMode::Details; return true; } } } } return false; } Tab::Patches => { // From a patch, jump to the linked issue (fixes field) @@ -177,11 +180,11 @@ impl App { if let Some(patch) = visible.get(idx) { if let Some(ref fixes_id) = patch.fixes { let fixes_id = fixes_id.clone(); let visible_issues = self.visible_issues(); let visible_idx = visible_issues.iter().position(|i| i.id == fixes_id); if visible_idx.is_none() && !self.show_all { self.show_all = true; if !self.show_all { let visible_issues = self.visible_issues(); if !visible_issues.iter().any(|i| i.id == fixes_id) { self.show_all = true; } } let visible_issues = self.visible_issues(); if let Some(vi) = @@ -191,10 +194,12 @@ impl App { self.list_state.select(Some(vi)); self.scroll = 0; self.mode = ViewMode::Details; return true; } } } } return false; } } } @@ -267,6 +272,7 @@ fn run_loop( if event::poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { app.status_msg = None; // clear status on any keypress match key.code { KeyCode::Char('q') | KeyCode::Esc => return Ok(()), KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { @@ -312,8 +318,9 @@ fn run_loop( .select(if count > 0 { Some(0) } else { None }); } KeyCode::Char('g') => { // Follow cross-references between issues and patches app.follow_link(); if !app.follow_link() { app.status_msg = Some("No linked item to follow".to_string()); } } KeyCode::Char('o') => { // Check out the relevant commit for local browsing @@ -326,16 +333,19 @@ fn run_loop( .map(|p| p.head_commit.clone()) } Tab::Issues => { // Find linked patch's head commit // Find linked patch's head commit, or fall back to closing commit let visible = app.visible_issues(); app.list_state .selected() .and_then(|idx| visible.get(idx)) .and_then(|issue| { // Try linked patch first app.patches .iter() .find(|p| p.fixes.as_deref() == Some(&issue.id)) .map(|p| p.head_commit.clone()) // Fall back to closing commit .or_else(|| issue.closed_by.map(|oid| oid.to_string())) }) } }; @@ -359,6 +369,9 @@ fn run_loop( } } return Ok(()); } else { app.status_msg = Some("No linked patch to check out".to_string()); } } KeyCode::Char('r') => { @@ -969,10 +982,19 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { } else { "a:show all" }; let text = format!( " 1:issues 2:patches j/k:navigate Tab:pane {}{} g:follow o:checkout r:refresh q:quit", filter_hint, mode_hint ); let para = Paragraph::new(text).style(Style::default().bg(Color::DarkGray).fg(Color::White)); let text = if let Some(ref msg) = app.status_msg { format!(" {}", msg) } else { format!( " 1:issues 2:patches j/k:navigate Tab:pane {}{} g:follow o:checkout r:refresh q:quit", filter_hint, mode_hint ) }; let style = if app.status_msg.is_some() { Style::default().bg(Color::Yellow).fg(Color::Black) } else { Style::default().bg(Color::DarkGray).fg(Color::White) }; let para = Paragraph::new(text).style(style); frame.render_widget(para, area); }