From 72e1ccb39c504c7061248481f55bfbed74100fb8 Mon Sep 17 00:00:00 2001 From: daiagi Date: Mon, 19 Oct 2026 13:54:07 +0700 Subject: [PATCH 1/2] m turns the mouse off for text selection; zoomed pane without side borders --- CHANGELOG.md | 2 ++ README.md | 7 ++++++- src/app.rs | 4 ++++ src/event.rs | 26 +++++++++++++++++++++++++- src/ui/focused.rs | 7 ++++--- src/ui/help.rs | 3 ++- src/ui/menus.rs | 15 +++++++++++++-- 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eef03fd..16dcf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - The help fits short terminals: no banner, two columns when one is too tall, and it scrolls only when neither fits. Shift+drag text selection is listed under NODES. +- `m` turns the mouse off and on, so a plain drag selects text; the zoomed pane has no side + borders, so a selection there is just the log text. ## 0.6.0 diff --git a/README.md b/README.md index 89bad81..d880dc3 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ Every run is saved to `logs//node_.log`, with the command line in - `c` clears the search and the filter. - `Enter` zooms into the focused pane, `Esc` goes back, `1`-`6` hide or show node 0 to 5. - `--search 'error,warning'` sets the terms at start, also with `--logs`. +- To copy log lines: `Enter` to zoom the pane, scroll to them, `m` to turn the mouse off, then + drag to select and copy as usual. The zoomed pane has no side borders, so the selection is + just the log text. `m` again turns the mouse back on. ## Read saved logs @@ -304,10 +307,12 @@ jamboree --nodes 6 --move-ports --listen-port 41000 \ | `c` | Clear the search and the filter | | `s` `i` `p` `r` | Stop, isolate, split, restart | | `x` | Shut down all nodes (TERM, then KILL after 3 s); their logs stay | +| `m` | Mouse off, so a plain drag selects text / on again | | `?` | Help (two columns on a short terminal, `↑` `↓` scroll it when even that does not fit) | | `q` | Quit (stops all nodes first) | -The mouse is on, so to select text hold Shift while dragging (Option in iTerm2). +While the mouse is on, Shift+drag also selects text (Option+drag in iTerm2), but in the grid it +takes the borders and neighbouring panes too; zoom first. ### Config file diff --git a/src/app.rs b/src/app.rs index 9994e7d..e8164b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,6 +25,8 @@ pub struct AppState { pub panes: RefCell>, /// Node and time of the last click, to tell a double click pub last_click: Option<(usize, Instant)>, + /// Mouse capture off (`m`), so a plain drag selects text in the terminal + pub mouse_off: bool, } impl AppState { @@ -116,6 +118,7 @@ impl AppState { last_key: None, panes: RefCell::new(Vec::new()), last_click: None, + mouse_off: false, } } @@ -139,6 +142,7 @@ impl AppState { last_key: None, panes: RefCell::new(Vec::new()), last_click: None, + mouse_off: false, } } diff --git a/src/event.rs b/src/event.rs index 560a5fb..a614194 100644 --- a/src/event.rs +++ b/src/event.rs @@ -2,7 +2,12 @@ use crate::AppState; use crate::ViewMode; use crate::net::Net; use crate::node::{self, Node}; -use crossterm::event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; +use crossterm::event::{ + DisableMouseCapture, EnableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, + MouseEventKind, +}; +use crossterm::execute; +use std::io::stdout; use ratatui::layout::Position; use std::time::{Duration, Instant}; @@ -246,6 +251,16 @@ pub fn handle_key_event( Action::Continue } + // Mouse off: a plain drag selects text in the terminal; on again for click and wheel + KeyCode::Char('m') => { + app.mouse_off = !app.mouse_off; + let _ = match app.mouse_off { + true => execute!(stdout(), DisableMouseCapture), + false => execute!(stdout(), EnableMouseCapture), + }; + Action::Continue + } + // Quit KeyCode::Char('q') => Action::Quit, @@ -696,6 +711,15 @@ mod tests { assert!(matches!(press(&mut ui, &["Esc"]), Action::ChangeView(ViewMode::Grid))); } + #[test] + fn m_turns_the_mouse_off_and_on_again() { + let mut ui = panes(1); + press(&mut ui, &["m"]); + assert!(ui.0.mouse_off); + press(&mut ui, &["m"]); + assert!(!ui.0.mouse_off); + } + #[test] fn help_scrolls_with_the_arrows_and_any_other_key_only_closes_it() { let mut ui = panes(1); diff --git a/src/ui/focused.rs b/src/ui/focused.rs index cf849d5..3323830 100644 --- a/src/ui/focused.rs +++ b/src/ui/focused.rs @@ -54,8 +54,9 @@ pub fn render(f: &mut Frame, node: &Node, app: &AppState, net: Option<&Net>) { node.resize_pty_if_needed(new_size); // Fast tail extraction with wrap calculation for focused view - let visible_lines = area.height.saturating_sub(2) as usize; // borders - let available_width = area.width.saturating_sub(2).max(1) as usize; + // Only a top border (the title), so a terminal selection takes no border characters + let visible_lines = area.height.saturating_sub(1) as usize; + let available_width = area.width.max(1) as usize; let raw_text = node.get_colored_tail(5_000_000); @@ -102,7 +103,7 @@ pub fn render(f: &mut Frame, node: &Node, app: &AppState, net: Option<&Net>) { super::grid::fault_tags(net, node), super::grid::frozen_tag(app, node) )) - .borders(Borders::ALL) + .borders(Borders::TOP) .border_style(Style::default().fg(Color::Green)), ) .wrap(Wrap { trim: false }); diff --git a/src/ui/help.rs b/src/ui/help.rs index 5a22a74..3a6b964 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -10,7 +10,8 @@ const SECTIONS: [(&str, &[(&str, &str)]); 4] = [ ("NODES", &[ ("← → h l Tab", "Focus previous / next node"), ("click", "Focus a node; double-click to zoom"), - ("Shift+drag", "Select text (Option+drag in iTerm2)"), + ("m", "Mouse off: drag selects text (m again: on)"), + ("Shift+drag", "Select with the mouse on (iTerm2: Option)"), ("Enter z", "Zoom into the focused node"), ("Esc", "{esc}"), ("1-6", "Hide or show node 0 to 5"), diff --git a/src/ui/menus.rs b/src/ui/menus.rs index 9109fad..c1b08d0 100644 --- a/src/ui/menus.rs +++ b/src/ui/menus.rs @@ -15,6 +15,7 @@ const MATCH_HINT: &str = " [n/N] Match [c] Clear "; const MATCH_HINT_ESC: &str = " [n/N] Match [c/Esc] Clear "; const PAUSED_INDICATOR: &str = " [ALL FROZEN: Space resumes]"; const GREP_INDICATOR: &str = " [FILTER ON]"; +const MOUSE_OFF_INDICATOR: &str = " [MOUSE OFF: drag selects, m restores]"; /// The base menu, with Ctrl+G offered as "Show all" while the filter is on fn base(menu: &str, app: &AppState) -> String { @@ -41,7 +42,12 @@ pub fn grid_menu(app: &AppState, has_tags: bool) -> String { _ => "", }; - let grep_status = if app.grep_mode { GREP_INDICATOR } else { "" }; + let grep_status = match (app.mouse_off, app.grep_mode) { + (true, true) => format!("{MOUSE_OFF_INDICATOR}{GREP_INDICATOR}"), + (true, false) => MOUSE_OFF_INDICATOR.to_string(), + (false, true) => GREP_INDICATOR.to_string(), + (false, false) => String::new(), + }; let match_hint = match has_tags { true if app.esc_clears_search => MATCH_HINT_ESC, @@ -72,7 +78,12 @@ pub fn focused_menu(app: &AppState, has_tags: bool) -> String { _ => "", }; - let grep_status = if app.grep_mode { GREP_INDICATOR } else { "" }; + let grep_status = match (app.mouse_off, app.grep_mode) { + (true, true) => format!("{MOUSE_OFF_INDICATOR}{GREP_INDICATOR}"), + (true, false) => MOUSE_OFF_INDICATOR.to_string(), + (false, true) => GREP_INDICATOR.to_string(), + (false, false) => String::new(), + }; let match_hint = match has_tags { true if app.esc_clears_search => MATCH_HINT_ESC, From 5f25f5f12c04ec0340ccc7ae8f8ea0084f9f61a1 Mon Sep 17 00:00:00 2001 From: daiagi Date: Mon, 19 Oct 2026 13:58:51 +0700 Subject: [PATCH 2/2] Release 0.7.0 --- CHANGELOG.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16dcf06..f921822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.7.0 - The help fits short terminals: no banner, two columns when one is too tall, and it scrolls only when neither fits. Shift+drag text selection is listed under NODES. diff --git a/Cargo.lock b/Cargo.lock index 75e9ebf..63a116b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,7 +409,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jamboree" -version = "0.6.0" +version = "0.7.0" dependencies = [ "ansi-to-tui", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index f88ce6f..193a163 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jamboree" -version = "0.6.0" +version = "0.7.0" edition = "2024" license = "MIT OR Apache-2.0" description = "Launch a local network of any JAM client, watch every node's logs side by side, and break it on purpose"