diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 240af7e..adc9ca2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,6 +5,10 @@ on: pull_request: workflow_dispatch: +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + jobs: rustfmt: runs-on: ubuntu-latest @@ -38,7 +42,7 @@ jobs: toolchain: ${{ matrix.toolchain }} components: clippy - uses: actions/checkout@v4 - - run: cargo fetch --locked + - run: cargo fetch - uses: actions/cache@v4 with: key: clippy-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('**/Cargo.*') }} @@ -57,8 +61,6 @@ jobs: - macos-latest - windows-latest env: - CARGO_INCREMENTAL: 0 - CARGO_TERM_COLOR: always RUSTFLAGS: --allow unknown-lints --deny warnings steps: - name: Install cargo-hack @@ -95,13 +97,13 @@ jobs: with: toolchain: ${{ matrix.toolchain }} - uses: actions/checkout@v4 - - run: cargo fetch --locked + - run: cargo fetch - uses: actions/cache@v4 with: key: test-${{ matrix.os }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('**/Cargo.*') }} path: target/ - run: cargo build --offline --all-targets - - run: cargo test --offline + - run: cargo test --offline --no-fail-fast release: name: Release ${{ matrix.triple }} diff --git a/Cargo.lock b/Cargo.lock index e0f5a03..0904bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -931,7 +931,7 @@ dependencies = [ [[package]] name = "ratatui-binary-data-widget" version = "0.1.0" -source = "git+https://github.com/EdJoPaTo/ratatui-binary-data-widget?branch=main#f36e5c2036b15aca3b8f531bc957d8767757fceb" +source = "git+https://github.com/EdJoPaTo/ratatui-binary-data-widget?branch=main#4405e4547d25917f217a810920813bf1a00de1fc" dependencies = [ "ratatui", ] @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "tui-tree-widget" version = "0.19.0" -source = "git+https://github.com/EdJoPaTo/tui-rs-tree-widget?branch=tree-data-trait#81692d0255b28986c57262b841c6d3fc5cc14026" +source = "git+https://github.com/EdJoPaTo/tui-rs-tree-widget?branch=tree-data-trait#b90b150631d9261b70ecb6acefbcc6dd42a4024e" dependencies = [ "ratatui", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 226a14a..fdf3032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ rustls-pemfile = "2" rustls-pki-types = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" -tui-tree-widget = { git="https://github.com/EdJoPaTo/tui-rs-tree-widget", branch = "tree-data-trait", features = ["serde_json"] } +tui-tree-widget = { git = "https://github.com/EdJoPaTo/tui-rs-tree-widget", branch = "tree-data-trait", features = ["serde_json"] } url = "2" # https://crates.io/crates/cargo-deb diff --git a/src/interactive/details/mod.rs b/src/interactive/details/mod.rs index cb219ec..803006f 100644 --- a/src/interactive/details/mod.rs +++ b/src/interactive/details/mod.rs @@ -24,20 +24,20 @@ impl Details { .min(topic_history_length.saturating_sub(1)) } - const fn table_index_of_click(&self, column: u16, row: u16) -> Option { + const fn table_index_of_click(&self, position: Position) -> Option { let area = self.last_table_area; - if !area.contains(Position { x: column, y: row }) { + if !area.contains(position) { return None; } - let visible = row.saturating_sub(area.top()).saturating_sub(2); // subtract block & header + let visible_index = position.y.saturating_sub(area.top()).saturating_sub(2); // subtract block & header let offset = self.table_state.offset(); - let index = (visible as usize) + offset; + let index = (visible_index as usize) + offset; Some(index) } /// Handles a click. Checks if its on the table. When it is the index get selected and true is returned. - pub fn table_click(&mut self, column: u16, row: u16) -> bool { - let Some(index) = self.table_index_of_click(column, row) else { + pub fn table_click(&mut self, position: Position) -> bool { + let Some(index) = self.table_index_of_click(position) else { return false; }; self.table_state.select(Some(index)); @@ -64,7 +64,7 @@ impl Details { let json_selector = self.payload.json_state.selected(); let table_area = - graph::Graph::parse(topic_history, binary_address.unwrap_or(0), &json_selector).map_or( + graph::Graph::parse(topic_history, binary_address.unwrap_or(0), json_selector).map_or( history_area, |graph| { let (table_area, graph_area) = @@ -79,7 +79,7 @@ impl Details { table_area, topic_history, binary_address, - &json_selector, + json_selector, &mut self.table_state, matches!(focus, ElementInFocus::HistoryTable), ); diff --git a/src/interactive/details/payload_view.rs b/src/interactive/details/payload_view.rs index 58b34e4..e9b3802 100644 --- a/src/interactive/details/payload_view.rs +++ b/src/interactive/details/payload_view.rs @@ -8,9 +8,7 @@ use ratatui::Frame; use ratatui_binary_data_widget::{BinaryDataWidget, BinaryDataWidgetState}; use tui_tree_widget::{Selector, Tree, TreeData, TreeState}; -use crate::interactive::ui::{ - focus_color, get_row_inside, split_area_vertically, BORDERS_TOP_RIGHT, -}; +use crate::interactive::ui::{focus_color, split_area_vertically, BORDERS_TOP_RIGHT}; use crate::mqtt::HistoryEntry; use crate::payload::{messagepack, Payload}; @@ -40,13 +38,6 @@ impl PayloadView { } } - pub fn json_index_of_click(&self, column: u16, row: u16) -> Option { - get_row_inside(self.last_area, column, row).map(|index| { - let offset = self.json_state.get_offset(); - (index as usize) + offset - }) - } - fn areas(&mut self, area: Rect, has_focus: bool, content_height: usize) -> (Rect, Rect) { let max_payload_height = if has_focus { area.height.saturating_mul(2) / 3 @@ -102,7 +93,7 @@ impl PayloadView { ) -> Rect { let title = format!("JSON Payload (Bytes: {payload_bytes})"); - let content_height = json.total_required_height(self.json_state.get_open()); + let content_height = json.total_required_height(self.json_state.opened()); let (payload_area, remaining_area) = self.areas(area, has_focus, content_height); let focus_color = focus_color(has_focus); @@ -137,7 +128,7 @@ impl PayloadView { let title = format!("MessagePack Payload (Bytes: {payload_bytes})"); let items = messagepack::tree_items(messagepack); - let content_height = items.total_required_height(self.json_state.get_open()); + let content_height = items.total_required_height(self.json_state.opened()); let (payload_area, remaining_area) = self.areas(area, has_focus, content_height); let focus_color = focus_color(has_focus); diff --git a/src/interactive/mod.rs b/src/interactive/mod.rs index 76aa7c9..f1cd83f 100644 --- a/src/interactive/mod.rs +++ b/src/interactive/mod.rs @@ -2,7 +2,7 @@ use std::time::{Duration, Instant}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind}; use ratatui::backend::{Backend, CrosstermBackend}; -use ratatui::layout::{Alignment, Rect}; +use ratatui::layout::{Alignment, Position, Rect}; use ratatui::text::Span; use ratatui::widgets::Paragraph; use ratatui::{Frame, Terminal}; @@ -375,36 +375,7 @@ impl App { } _ => false, }, - Some(Payload::Json(_)) => match key.code { - KeyCode::Esc => self.details.payload.json_state.select(vec![]), - KeyCode::Enter | KeyCode::Char(' ') => { - self.details.payload.json_state.toggle_selected() - } - KeyCode::Down | KeyCode::Char('j') => { - self.details.payload.json_state.key_down() - } - KeyCode::Up | KeyCode::Char('k') => { - self.details.payload.json_state.key_up() - } - KeyCode::Left | KeyCode::Char('h') => { - self.details.payload.json_state.key_left() - } - KeyCode::Right | KeyCode::Char('l') => { - self.details.payload.json_state.key_right() - } - KeyCode::Home => self.details.payload.json_state.select_first(), - KeyCode::End => self.details.payload.json_state.select_last(), - KeyCode::PageUp => self.details.payload.json_state.scroll_up(3), - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.details.payload.json_state.scroll_up(3) - } - KeyCode::PageDown => self.details.payload.json_state.scroll_down(3), - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.details.payload.json_state.scroll_down(3) - } - _ => false, - }, - Some(Payload::MessagePack(_)) => match key.code { + Some(Payload::Json(_) | Payload::MessagePack(_)) => match key.code { KeyCode::Esc => self.details.payload.json_state.select(vec![]), KeyCode::Enter | KeyCode::Char(' ') => { self.details.payload.json_state.toggle_selected() @@ -561,38 +532,39 @@ impl App { } fn on_click(&mut self, column: u16, row: u16) -> Refresh { - if let Some(index) = self.topic_overview.index_of_click(column, row) { - let changed = self.topic_overview.state.select_visible_index(index); - if !changed { - self.topic_overview.state.toggle_selected(); + let position = Position::new(column, row); + + if let Some(identifier) = self.topic_overview.state.rendered_at(position) { + let is_already_selected = identifier == self.topic_overview.state.selected(); + if is_already_selected { + // change focus or toggle, don't do both + if matches!(self.focus, ElementInFocus::TopicOverview) { + self.topic_overview.state.toggle_selected(); + } else { + self.focus = ElementInFocus::TopicOverview; + } + } else { + self.focus = ElementInFocus::TopicOverview; + self.topic_overview.state.select(identifier.to_vec()); } + + return Refresh::Update; + } + if self.topic_overview.state.click_at(position) { self.focus = ElementInFocus::TopicOverview; return Refresh::Update; } - if let Some(index) = self.details.payload.json_index_of_click(column, row) { + if self.details.payload.last_area.contains(position) { match self.get_selected_payload() { None => return Refresh::Update, // No payload but click into payload area -> redraw Some(Payload::Binary(_)) => { - if let Some(address) = self - .details - .payload - .binary_state - .clicked_address(column, row) - { - self.details - .payload - .binary_state - .select_address(Some(address)); - self.focus = ElementInFocus::Payload; - return Refresh::Update; - } + self.details.payload.binary_state.select_at(column, row); + self.focus = ElementInFocus::Payload; + return Refresh::Update; } Some(Payload::Json(_) | Payload::MessagePack(_)) => { - let changed = self.details.payload.json_state.select_visible_index(index); - if !changed { - self.details.payload.json_state.toggle_selected(); - } + self.details.payload.json_state.click_at(position); self.focus = ElementInFocus::Payload; return Refresh::Update; } @@ -600,7 +572,7 @@ impl App { } } - if self.details.table_click(column, row) { + if self.details.table_click(position) { self.focus = ElementInFocus::HistoryTable; return Refresh::Update; } diff --git a/src/interactive/topic_overview.rs b/src/interactive/topic_overview.rs index ba397cc..0f5c6c1 100644 --- a/src/interactive/topic_overview.rs +++ b/src/interactive/topic_overview.rs @@ -5,7 +5,7 @@ use ratatui::Frame; use tui_tree_widget::{Tree, TreeState}; use super::mqtt_history::MqttHistory; -use crate::interactive::ui::{focus_color, get_row_inside, BORDERS_TOP_RIGHT}; +use super::ui::{focus_color, BORDERS_TOP_RIGHT}; #[derive(Default)] pub struct TopicOverview { @@ -50,14 +50,4 @@ impl TopicOverview { frame.render_stateful_widget(widget, area, &mut self.state); self.last_area = area; } - - pub const fn index_of_click(&self, column: u16, row: u16) -> Option { - if let Some(index) = get_row_inside(self.last_area, column, row) { - let offset = self.state.get_offset(); - let new_index = (index as usize) + offset; - Some(new_index) - } else { - None - } - } } diff --git a/src/interactive/ui.rs b/src/interactive/ui.rs index b440228..1a37cce 100644 --- a/src/interactive/ui.rs +++ b/src/interactive/ui.rs @@ -1,4 +1,4 @@ -use ratatui::layout::{Position, Rect}; +use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use ratatui::widgets::Borders; @@ -21,38 +21,6 @@ pub const fn focus_color(has_focus: bool) -> Color { } } -/// When the column/row is inside the area, return the row relative to the area. -/// Otherwise `None` is returned. -pub const fn get_row_inside(area: Rect, column: u16, row: u16) -> Option { - #[allow(clippy::if_then_some_else_none)] - if area.contains(Position { x: column, y: row }) { - Some(row.saturating_sub(area.top()).saturating_sub(1)) - } else { - None - } -} - -#[test] -fn row_outside() { - let area = Rect::new(5, 5, 5, 10); - let result = get_row_inside(area, 7, 1); - assert_eq!(result, None); -} - -#[test] -fn column_outside() { - let area = Rect::new(5, 5, 5, 10); - let result = get_row_inside(area, 1, 7); - assert_eq!(result, None); -} - -#[test] -fn is_inside() { - let area = Rect::new(5, 5, 5, 10); - let result = get_row_inside(area, 7, 10); - assert_eq!(result, Some(4)); -} - pub const fn split_area_vertically(area: Rect, height_first: u16) -> (Rect, Rect) { let first = Rect { height: height_first,