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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ cargo test --test tui_e2e # run the end-to-end TUI tests (Unix only)
- `src/main.rs` has `#![deny(warnings)]`, so any compiler warning fails the build. Keep the tree warning-clean.
- Global CLI options (before the subcommand): `-c/--capacity` (scrollback lines, default 2000), `-t/--tag-file` (default `tags.yml`), `-l/--latency` (ms, clamped 0..=100000, default 100), `-n/--name` (session record base name, default a timestamp), `--headless` (see below). The session can also be renamed at runtime with `!rename <name>` in the command bar.
- **Headless mode** (`--headless`): no TUI — a raw terminal↔wire bridge. A `graphics/headless.rs` task takes the graphics slot (same `GraphicsCommand` channel + tx/rx/logger consumers) and just writes RX bytes to stdout (logs colored via ANSI, no timestamps/scrollback/persistence). The Inputs task carries a `raw: bool` overlay on `InputsShared` (not a new `InputMode`): raw keys are encoded to VT bytes (`inputs/key_encode.rs`) and sent straight to `tx`; `Ctrl+K` drops into the existing `Normal` command bar (blinking `> ` prompt rendered by the headless task), Enter runs the command and returns to raw, Esc quits. The interface tasks forward RX immediately (per-byte / per-chunk) instead of `\n`-framing when `headless` is set.
- Optional config file (`infra/config.rs`): `<config_dir>/scope/config.toml` (e.g. `~/.config/scope/config.toml`, alongside the crash backups). Currently supports `capacity` and `tag_file`. Resolution precedence is **CLI flag > config.toml > built-in default** (`Config::load` is folded into `main`'s single fatal-error flow). A missing file/field falls through to defaults; a malformed file or unknown key is a fatal error (`deny_unknown_fields`). Path values (`tag_file`) are used verbatim — there is no shell involved, so `~` and `$VAR` are **not** expanded; use an absolute path.
- Optional config file (`infra/config.rs`): `<config_dir>/scope/config.toml` (e.g. `~/.config/scope/config.toml`, alongside the crash backups). Supports `capacity`, `tag_file`, and an optional `[shortcuts]` table (see below). Resolution precedence is **CLI flag > config.toml > built-in default** (`Config::load` is folded into `main`'s single fatal-error flow). A missing file/field falls through to defaults; a malformed file or unknown key is a fatal error (`deny_unknown_fields`). Path values (`tag_file`) are used verbatim — there is no shell involved, so `~` and `$VAR` are **not** expanded; use an absolute path.
- **Custom shortcuts** (`inputs/keymap.rs`, issue #211): the optional `[shortcuts]` config table (`action = "Key+Combo"`) remaps the 15 action/navigation keys. `Action`/`KeyBinding`/`Keymap` own all key knowledge; `config.rs` stays a `BTreeMap<String,String>` and never enumerates the actions. Shortcuts have no CLI flag, so precedence is config.toml > default; `Keymap::from_config(config.shortcuts)` is built in `main` and threaded (by value, no lock) through `app_serial`/`app_rtt` into `InputsConnections`. Config modifier names are **logical** and lowered to the real per-platform crossterm event in `keymap::resolve` — this replaces the old `CTRL_MODIFIER`/`ACTION_MODIFIER` consts in `handle_key_input` (only `ACTION_MODIFIER`, for the fixed `Alt+Enter` arm, remains). `handle_key_input` resolves a remappable action via `private.keymap.action_for()` **before** the intrinsic key match (`try_run_action`); a `Tab`-bound `next_bookmark` falls through to the `@tag` autocomplete arm while the pop-up is up. Text-editing/intrinsic keys (typing, Enter, Esc, arrows, Home/End, Backspace/Delete, headless `Ctrl+K`/`Ctrl+Q`) stay hardcoded and are rejected as override targets (`reserved_reason`). Unknown actions, bad key strings, reserved keys and duplicate bindings are fatal config errors. An unbound `Ctrl`/`Alt`+letter is now swallowed rather than typed literally.
- `Ble` is declared as a subcommand but is not implemented (returns an error).

## Architecture
Expand Down
74 changes: 49 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,30 +248,36 @@ Anything typed on the command bar that starts with `!` is a command. A line with

## Keyboard & Mouse Shortcuts

| Shortcut | Action |
|----------|--------|
| `Enter` | Send the message / run the command. In search mode: next match. |
| `Alt`+`Enter` | Send without the trailing `\r\n`. |
| `Alt`+`Enter` (`Ctrl`+`Enter` on Windows) | In search mode: previous match. |
| `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 (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. |
| Terminal paste shortcut | Paste text into the command bar (via bracketed paste). |
| `Ctrl`+`L` | Clear the screen. |
| `Esc` | Leave search mode, or quit `Scope` when in normal mode. |
| `PageUp` / `PageDown` | Scroll the history one page up / down. |
| `Alt`+`PageUp` / `Alt`+`PageDown` (`Ctrl` on Windows) | Jump to the start / end of the history. |
| `Home` / `End` | Move the cursor to the start / end of the input. |
| `Ctrl`+`Left` / `Ctrl`+`Right` (`Alt` on macOS) | Move the cursor one word left / right. |
| `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`). |
Rows with a **Config action** name are remappable via the `[shortcuts]` table in
`config.toml` (see [Configuration File](#configuration-file)). The rest — typing,
`Enter`, `Esc`, arrows, `Home`/`End`, `Backspace`/`Delete`, and the headless
`Ctrl`+`K` / `Ctrl`+`Q` chords — are fixed and cannot be rebound.

| Shortcut | Action | Config action |
|----------|--------|---------------|
| `Enter` | Send the message / run the command. In search mode: next match. | — |
| `Alt`+`Enter` | Send without the trailing `\r\n`. | — |
| `Alt`+`Enter` (`Ctrl`+`Enter` on Windows) | In search mode: previous match. | — |
| `Up` / `Down` | Navigate the command history. In search mode: previous / next match. | — |
| `Ctrl`+`F` | Toggle search mode. | `search_toggle` |
| `Ctrl`+`W` | In search mode: toggle case sensitivity. | `toggle_case` |
| `Ctrl`+`E` | In search mode: toggle regular-expression matching. | `toggle_regex` |
| `Tab` | Autocomplete a `@tag` from the tag file (while the pop-up is up); otherwise jump to the next bookmark. | `next_bookmark` |
| `Shift`+`Tab` | Jump to the previous bookmark. | `prev_bookmark` |
| `Ctrl`+`S` | Save the whole session to a `.txt` file. | `save` |
| `Ctrl`+`R` | Start / stop a record session. | `record` |
| `Ctrl`+`C` | Copy the current selection to the clipboard. | `copy` |
| Terminal paste shortcut | Paste text into the command bar (via bracketed paste). | — |
| `Ctrl`+`L` | Clear the screen. | `clear` |
| `Esc` | Leave search mode, or quit `Scope` when in normal mode. | — |
| `PageUp` / `PageDown` | Scroll the history one page up / down. | `page_up` / `page_down` |
| `Alt`+`PageUp` / `Alt`+`PageDown` (`Ctrl` on Windows) | Jump to the start / end of the history. | `jump_start` / `jump_end` |
| `Home` / `End` | Move the cursor to the start / end of the input. | — |
| `Ctrl`+`Left` / `Ctrl`+`Right` (`Alt` on macOS) | Move the cursor one word left / right. | `word_left` / `word_right` |
| `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 All @@ -298,15 +304,33 @@ Global options (given before the command):

## Configuration File

The options above can also be set in an optional `config.toml` placed in your platform config directory under `scope/` (for example `~/.config/scope/config.toml` on Linux). It currently supports the `capacity` and `tag_file` fields:
The options above can also be set in an optional `config.toml` placed in your platform config directory under `scope/` (for example `~/.config/scope/config.toml` on Linux). It supports the `capacity` and `tag_file` fields and an optional `[shortcuts]` table:

```toml
capacity = 5000
tag_file = "/home/user/.config/scope/tags.yml"

[shortcuts]
record = "Ctrl+G" # move record off Ctrl+R
next_bookmark = "F2"
prev_bookmark = "F3"
# every other action keeps its default
```

Values resolve as **CLI flag > `config.toml` > built-in default**, so a flag always wins over the file, and the file wins over the defaults. A missing file (or a missing field) just falls back to the defaults; a malformed file or an unknown key is reported as an error. Paths are used verbatim — `~` and environment variables are **not** expanded, so use an absolute path.

### Custom shortcuts

The `[shortcuts]` table remaps any of the action/navigation keys listed in the [Keyboard & Mouse Shortcuts](#keyboard--mouse-shortcuts) table — the entries with a **Config action** name. Each entry is `action = "Key+Combo"`; there is no CLI flag, so a shortcut resolves as `config.toml` > built-in default, and every action you don't list keeps its default. The valid action names are:

`copy`, `clear`, `save`, `record`, `search_toggle`, `toggle_case`, `toggle_regex`, `page_up`, `page_down`, `jump_start`, `jump_end`, `word_left`, `word_right`, `next_bookmark`, `prev_bookmark`.

A key combo is zero or more modifiers followed by a key, joined with `+` (case-insensitive): modifiers `Ctrl`, `Alt`, `Shift`; keys are a single character (`c`, `/`, `.`), a named key (`Space`, `Tab`, `Enter`, `Esc`, `Backspace`, `Delete`, `Insert`, `Home`, `End`, `PageUp`, `PageDown`, `Up`, `Down`, `Left`, `Right`) or a function key (`F1`–`F12`). `Shift+Tab` means the "back-tab" key. Examples: `"Ctrl+G"`, `"F2"`, `"Alt+Home"`, `"Ctrl+Alt+End"`.

Modifiers are **logical** and translated to what your terminal actually delivers, so a config file is portable: `Ctrl+Left` keeps working on macOS (where the terminal sends it as `Alt+Left`), and the `jump_start` / `jump_end` defaults are `Ctrl+PageUp` / `Ctrl+PageDown` on Windows but `Alt+PageUp` / `Alt+PageDown` elsewhere. An unknown action name, an unparseable combo, a binding onto a fixed key (e.g. `Enter`, an arrow, a bare character, or the headless `Ctrl+K` / `Ctrl+Q`), or two actions bound to the same key are all reported as errors on startup.

> **Note:** terminals send `Ctrl+I`, `Ctrl+J`, and `Ctrl+M` as `Tab`, `Enter`, and `Enter` respectively, so those three combos can't be used as shortcuts — they parse without error but the keystroke never reaches the app as `Ctrl+<letter>`. Pick another key (e.g. `Ctrl+G`).

## Plugins

You can extend the basic functions of `Scope` with plugins! Plugins are scripts written in the `lua` language. The code below shows a plugin that prepends `Received:` to every received message. It also prints `Hello, World!` when the user types `!echo hello` (if the plugin file is `echo.lua`).
Expand Down
34 changes: 34 additions & 0 deletions src/infra/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//! silently do nothing.

use serde::Deserialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

const CONFIG_FILE_NAME: &str = "config.toml";
Expand All @@ -27,6 +28,17 @@ pub struct Config {
/// Path to the tag file (CLI: `-t/--tag-file`). Used verbatim — no shell is
/// involved, so `~` and `$VAR` are not expanded; use an absolute path.
pub tag_file: Option<PathBuf>,
/// Optional shortcut overrides: `action-name = "Key+Combo"` (e.g.
/// `record = "Ctrl+G"`). There is no CLI flag, so precedence is
/// config.toml > built-in default; an omitted action keeps its default.
///
/// The action-name vocabulary and combo parsing live in
/// [`crate::inputs::keymap`] — this module deliberately does not enumerate
/// them. Unknown *top-level* keys are still rejected by
/// `deny_unknown_fields`; unknown *action* names (and bad combos, reserved
/// keys, duplicate bindings) are rejected by `Keymap::from_config`.
#[serde(default)]
pub shortcuts: Option<BTreeMap<String, String>>,
}

impl Config {
Expand Down Expand Up @@ -109,6 +121,28 @@ mod tests {
let config = Config::load_from(&path).unwrap();
assert_eq!(config.capacity, None);
assert_eq!(config.tag_file, None);
assert!(config.shortcuts.is_none());

let _ = std::fs::remove_file(&path);
}

#[test]
fn parses_shortcuts_table() {
let path = temp_path("shortcuts");
std::fs::write(
&path,
"capacity = 100\n\n[shortcuts]\nrecord = \"Ctrl+G\"\nnext_bookmark = \"F2\"\n",
)
.unwrap();

let config = Config::load_from(&path).unwrap();
assert_eq!(config.capacity, Some(100));
let shortcuts = config.shortcuts.expect("shortcuts table present");
assert_eq!(shortcuts.get("record").map(String::as_str), Some("Ctrl+G"));
assert_eq!(
shortcuts.get("next_bookmark").map(String::as_str),
Some("F2")
);

let _ = std::fs::remove_file(&path);
}
Expand Down
Loading
Loading