From e65360ae267c302164366ae06ba73c06fd6f67e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Jan 2026 12:21:21 +0000 Subject: [PATCH 1/2] Fix scrolling in topic tree and message list views The ListState was being created fresh on every render with default offset=0, causing the scroll position to reset. This prevented users from scrolling through large lists of topics. Changes: - Update tree_view to track scroll offset and ensure selection is visible - Update message_view with same scroll behavior - Persist scroll offset in App state (tree_scroll, message_scroll) - Pass mutable App reference to render functions Fixes #1 --- src/main.rs | 2 +- src/ui/message_view.rs | 34 ++++++++++++++++++++++++---------- src/ui/mod.rs | 2 +- src/ui/tree_view.rs | 15 ++++++++++++++- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 495b265..0896532 100644 --- a/src/main.rs +++ b/src/main.rs @@ -359,7 +359,7 @@ async fn run_app(config: Config, config_path: PathBuf, needs_server_setup: bool) // Main loop loop { // Draw UI - terminal.draw(|f| ui::render(f, &app))?; + terminal.draw(|f| ui::render(f, &mut app))?; // Handle events with timeout let timeout = tick_rate; diff --git a/src/ui/message_view.rs b/src/ui/message_view.rs index cfc686d..302f8d2 100644 --- a/src/ui/message_view.rs +++ b/src/ui/message_view.rs @@ -10,7 +10,7 @@ use super::bordered_block; use crate::app::{App, Panel, PayloadMode}; use crate::mqtt::MqttMessage; -pub fn render_messages(frame: &mut Frame, app: &App, area: Rect) { +pub fn render_messages(frame: &mut Frame, app: &mut App, area: Rect) { let focused = app.focused_panel == Panel::Messages; let title = match &app.selected_topic { @@ -23,6 +23,28 @@ pub fn render_messages(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(block, area); + // Split view: message list on top, payload detail below + let chunks = ratatui::layout::Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .constraints([ + ratatui::layout::Constraint::Percentage(40), + ratatui::layout::Constraint::Percentage(60), + ]) + .split(inner); + + // Update message scroll to keep selection visible (before borrowing messages) + let message_count = app.get_current_messages().len(); + if message_count > 0 { + let visible_height = chunks[0].height as usize; + let selected = app.selected_message_index.min(message_count.saturating_sub(1)); + + if selected < app.message_scroll { + app.message_scroll = selected; + } else if selected >= app.message_scroll + visible_height { + app.message_scroll = selected.saturating_sub(visible_height.saturating_sub(1)); + } + } + let messages = app.get_current_messages(); if messages.is_empty() { @@ -42,15 +64,6 @@ pub fn render_messages(frame: &mut Frame, app: &App, area: Rect) { return; } - // Split view: message list on top, payload detail below - let chunks = ratatui::layout::Layout::default() - .direction(ratatui::layout::Direction::Vertical) - .constraints([ - ratatui::layout::Constraint::Percentage(40), - ratatui::layout::Constraint::Percentage(60), - ]) - .split(inner); - // Message list render_message_list(frame, app, &messages, chunks[0]); @@ -72,6 +85,7 @@ fn render_message_list(frame: &mut Frame, app: &App, messages: &[&MqttMessage], let mut state = ListState::default(); state.select(Some(app.selected_message_index)); + *state.offset_mut() = app.message_scroll; let list = List::new(items).highlight_style( Style::default() diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 76088cd..09b690c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -31,7 +31,7 @@ pub use stats_view::render_stats; pub use tree_view::render_tree; /// Main render function -pub fn render(frame: &mut Frame, app: &App) { +pub fn render(frame: &mut Frame, app: &mut App) { let size = frame.area(); // Create main layout: header, content, footer diff --git a/src/ui/tree_view.rs b/src/ui/tree_view.rs index c77e247..30dc1b9 100644 --- a/src/ui/tree_view.rs +++ b/src/ui/tree_view.rs @@ -11,7 +11,7 @@ use crate::app::{App, FilterMode, Panel}; use crate::config::TopicColorRule; use crate::state::TopicInfo; -pub fn render_tree(frame: &mut Frame, app: &App, area: Rect) { +pub fn render_tree(frame: &mut Frame, app: &mut App, area: Rect) { let focused = app.focused_panel == Panel::TopicTree; let title = match app.filter_mode { FilterMode::All => "Topics", @@ -35,6 +35,18 @@ pub fn render_tree(frame: &mut Frame, app: &App, area: Rect) { return; } + // Calculate visible window and ensure selection is visible + let visible_height = inner.height as usize; + let total = topics.len(); + let selected = app.selected_topic_index.min(total.saturating_sub(1)); + + // Ensure scroll keeps selection in view + if selected < app.tree_scroll { + app.tree_scroll = selected; + } else if selected >= app.tree_scroll + visible_height { + app.tree_scroll = selected.saturating_sub(visible_height.saturating_sub(1)); + } + let color_rules = &app.config.ui.topic_colors; let items: Vec = topics .iter() @@ -48,6 +60,7 @@ pub fn render_tree(frame: &mut Frame, app: &App, area: Rect) { let mut state = ListState::default(); state.select(Some(app.selected_topic_index)); + *state.offset_mut() = app.tree_scroll; let list = List::new(items).highlight_style( Style::default() From 6501006d04f0e978b09aca279552de98633170f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Jan 2026 12:26:34 +0000 Subject: [PATCH 2/2] Bump version to 0.4.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8852e4..aa109b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -869,7 +869,7 @@ dependencies = [ [[package]] name = "mqtop" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index b9d5af3..ace40de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mqtop" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "High-performance MQTT explorer TUI - like htop for your broker" authors = ["Sourceful Energy"]