Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Every subsystem is a `Task<S, M>`: it owns shared state `S` behind an `Arc<RwLoc

- **Interface** (`interfaces/`) — owns the serial port or RTT connection. Enum-dispatched: `InterfaceTask` / `InterfaceCommand` / `InterfaceShared` / `InterfaceType` select between `serial_if.rs` and `rtt_if.rs`.
- **Inputs** (`inputs/inputs_task.rs`) — the command bar. Parses keystrokes, manages input history (`inputs/history.rs`), and has two `InputMode`s: `Normal` and `Search` (plus a `raw` passthrough flag used only in headless mode).
- **Graphics** (`graphics/graphics_task.rs`) — renders the TUI, owns the scrollback buffer, handles selection/scrolling, persists the session to a timestamped `.txt` file, and is the sink for log messages. In headless mode it is replaced by `graphics/headless.rs` (same task slot, plain stdout, no TUI).
- **Graphics** (`graphics/graphics_task.rs`) — renders the TUI, owns the scrollback buffer, handles selection/scrolling, persists the session to a timestamped `.txt` file, and is the sink for log messages. In headless mode it is replaced by `graphics/headless.rs` (same task slot, plain stdout, no TUI). Line-pinned features (bookmarks: right-click to toggle, `Tab`/`Shift+Tab` to jump, yellow timestamp) live in `graphics/screen.rs` and key off the stable per-line `BufferLine::id` so they survive scrollback rotation and filter changes.
- **PluginEngine** (`plugin/engine.rs`) — runs a Tokio runtime hosting Lua plugins.

### Data buses (`infra/mpmc.rs`)
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ Press `Ctrl+F` to enter **search mode** and type a query to find it in the captu

![Search gif](videos/015_search/video.gif)

#### Bookmarks

Jumping to a specific line in a long history is tedious. **Right-click** a line to bookmark it (right-click again to remove the bookmark); a bookmarked line is marked by a **yellow timestamp**. Press `Tab` to jump to the next bookmark and `Shift+Tab` for the previous one, cycling around the ends — the same navigation feel as search. Bookmarks follow their line as the history scrolls and survive changing the filter (a bookmark hidden by a filter reappears once the line is shown again); `Ctrl+L` clears them along with the screen.

#### Message Filter

When received data floods the history — for example a stream of `dbg` lines — use the `!filter` and `!mute` commands to control which received messages are shown. The current filter is shown between square brackets at the top-right of the command bar (e.g. `[.*]`). The two commands share that slot, so setting one replaces the other.
Expand Down Expand Up @@ -252,7 +256,8 @@ Anything typed on the command bar that starts with `!` is a command. A line with
| `Up` / `Down` | Navigate the command history. In search mode: previous / next match. |
| `Ctrl`+`F` | Toggle search mode. |
| `Ctrl`+`W` | In search mode: toggle case sensitivity. |
| `Tab` | Autocomplete a `@tag` from the tag file. |
| `Tab` | Autocomplete a `@tag` from the tag file (while the pop-up is up); otherwise jump to the next bookmark. |
| `Shift`+`Tab` | Jump to the previous bookmark. |
| `Ctrl`+`S` | Save the whole session to a `.txt` file. |
| `Ctrl`+`R` | Start / stop a record session. |
| `Ctrl`+`C` | Copy the current selection to the clipboard. |
Expand All @@ -266,6 +271,7 @@ Anything typed on the command bar that starts with `!` is a command. A line with
| `Backspace` / `Delete` | Delete the character before / at the cursor. |
| Mouse wheel | Scroll the history (hold `Ctrl` to scroll horizontally). |
| Mouse drag | Select text (copy it with `Ctrl`+`C`). |
| Mouse right-click | Toggle a bookmark on the clicked line (navigate with `Tab` / `Shift`+`Tab`). |

## Command-Line Options

Expand Down
22 changes: 22 additions & 0 deletions src/graphics/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ use crate::{
use chrono::{DateTime, Local};
use std::ops::AddAssign;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

/// Monotonic source of per-line identifiers. A line keeps its `id` for its whole
/// lifetime, independent of its position in the buffer, so features that pin to a
/// specific line (bookmarks) survive re-indexing on capacity drop and the
/// re-derivation of the displayed view when the filter changes. Every
/// `BufferLine` is created on the graphics thread, so a `Relaxed` counter is
/// enough — we only need uniqueness, not cross-thread ordering.
static NEXT_LINE_ID: AtomicU64 = AtomicU64::new(0);

fn next_line_id() -> u64 {
NEXT_LINE_ID.fetch_add(1, Ordering::Relaxed)
}

/// The payload of a stored buffer line. It is reference-counted so the filtered
/// view (`Buffer`) can hold cheap clones of the lines in the full history
Expand Down Expand Up @@ -126,6 +139,11 @@ where
T: AsRef<[u8]>,
{
pub line: usize,
/// Stable identity assigned at creation and preserved across cloning,
/// re-indexing and filter changes (see [`next_line_id`]). Unlike `line`
/// (the current index, which shifts as lines drop off the top), this never
/// changes for a given logical line, so it is what bookmarks pin to.
pub id: u64,
pub timestamp: DateTime<Local>,
pub level: Option<LogLevel>,
pub message: T,
Expand All @@ -136,6 +154,7 @@ impl BufferLine<LineBytes> {
pub fn decode(&self, decoder: ScreenDecoder) -> BufferLine<String> {
BufferLine {
line: self.line,
id: self.id,
timestamp: self.timestamp,
level: self.level,
message: decoder.decode(&self.message),
Expand All @@ -146,6 +165,7 @@ impl BufferLine<LineBytes> {
pub fn new_rx(timestamp: DateTime<Local>, message: Vec<u8>) -> Self {
Self {
line: 0,
id: next_line_id(),
timestamp,
level: None,
message: message.into(),
Expand All @@ -156,6 +176,7 @@ impl BufferLine<LineBytes> {
pub fn new_tx(timestamp: DateTime<Local>, message: Vec<u8>) -> Self {
Self {
line: 0,
id: next_line_id(),
timestamp,
level: None,
message: message.into(),
Expand All @@ -166,6 +187,7 @@ impl BufferLine<LineBytes> {
pub fn new_log(timestamp: DateTime<Local>, level: LogLevel, message: Vec<u8>) -> Self {
Self {
line: 0,
id: next_line_id(),
timestamp,
level: Some(level),
message: message.into(),
Expand Down
20 changes: 20 additions & 0 deletions src/graphics/graphics_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ pub enum GraphicsCommand {
Click(ScreenPosition),
Move(ScreenPosition),
CopyToClipboard,
ToggleBookmark(ScreenPosition),
NextBookmark,
PrevBookmark,
}

pub struct SaveStats {
Expand Down Expand Up @@ -583,6 +586,23 @@ impl GraphicsTask {
GraphicsCommand::Move(end_pos) => {
private.screen.set_selection_end(end_pos);
}
GraphicsCommand::ToggleBookmark(pos) => {
private.screen.toggle_bookmark(&private.buffer, pos);
}
GraphicsCommand::NextBookmark => {
let max_main_axis = Self::max_main_axis(&private);

private
.screen
.jump_to_next_bookmark(&private.buffer, max_main_axis as usize);
}
GraphicsCommand::PrevBookmark => {
let max_main_axis = Self::max_main_axis(&private);

private
.screen
.jump_to_previous_bookmark(&private.buffer, max_main_axis as usize);
}
GraphicsCommand::CopyToClipboard => {
if let Err(res) =
Self::handle_copy_to_clipboard(&mut private, &mut copy_blink)
Expand Down
Loading
Loading