diff --git a/crates/openlogi-core/src/binding.rs b/crates/openlogi-core/src/binding.rs index 1463492a..0c183c4b 100644 --- a/crates/openlogi-core/src/binding.rs +++ b/crates/openlogi-core/src/binding.rs @@ -369,6 +369,16 @@ pub enum Action { /// Mouse "forward" side button (extra button 5). Native counterpart to /// [`Action::MouseBack`]; see [`Action::BrowserForward`] for the ⌘] form. MouseForward, + /// Extra mouse button 6. Emitted as the real button-6 event for apps, games, + /// and CAD software that bind it. macOS/Linux only — Windows `SendInput` + /// caps at button 5, so this logs-and-skips there. + MouseButton6, + /// Extra mouse button 7. See [`Action::MouseButton6`]. + MouseButton7, + /// Extra mouse button 8. See [`Action::MouseButton6`]. + MouseButton8, + /// Extra mouse button 9. See [`Action::MouseButton6`]. + MouseButton9, // ── Editing ────────────────────────────────────────────────────────────── /// Copy the current selection (⌘C / Ctrl+C). @@ -684,6 +694,10 @@ impl Action { Action::MiddleClick => "Middle Click".into(), Action::MouseBack => "Back (Button 4)".into(), Action::MouseForward => "Forward (Button 5)".into(), + Action::MouseButton6 => "Button 6".into(), + Action::MouseButton7 => "Button 7".into(), + Action::MouseButton8 => "Button 8".into(), + Action::MouseButton9 => "Button 9".into(), Action::Copy => "Copy".into(), Action::Paste => "Paste".into(), Action::Cut => "Cut".into(), @@ -734,7 +748,11 @@ impl Action { | Action::RightClick | Action::MiddleClick | Action::MouseBack - | Action::MouseForward => Category::Mouse, + | Action::MouseForward + | Action::MouseButton6 + | Action::MouseButton7 + | Action::MouseButton8 + | Action::MouseButton9 => Category::Mouse, // CustomShortcut is assigned to Editing so it doesn't need a // separate arm (it's not in the picker catalog). Action::Copy @@ -792,6 +810,10 @@ impl Action { Action::MiddleClick, Action::MouseBack, Action::MouseForward, + Action::MouseButton6, + Action::MouseButton7, + Action::MouseButton8, + Action::MouseButton9, // Editing Action::Copy, Action::Paste, @@ -1347,6 +1369,20 @@ mod tests { assert_eq!(Action::LeftClick.category(), Category::Mouse); assert_eq!(Action::RightClick.category(), Category::Mouse); assert_eq!(Action::MiddleClick.category(), Category::Mouse); + assert_eq!(Action::MouseBack.category(), Category::Mouse); + assert_eq!(Action::MouseForward.category(), Category::Mouse); + assert_eq!(Action::MouseButton6.category(), Category::Mouse); + assert_eq!(Action::MouseButton7.category(), Category::Mouse); + assert_eq!(Action::MouseButton8.category(), Category::Mouse); + assert_eq!(Action::MouseButton9.category(), Category::Mouse); + } + + #[test] + fn extra_mouse_button_labels() { + assert_eq!(Action::MouseButton6.label(), "Button 6"); + assert_eq!(Action::MouseButton7.label(), "Button 7"); + assert_eq!(Action::MouseButton8.label(), "Button 8"); + assert_eq!(Action::MouseButton9.label(), "Button 9"); } #[test] diff --git a/crates/openlogi-gui/locales/da.yml b/crates/openlogi-gui/locales/da.yml index 2e2f041f..c6ae361a 100644 --- a/crates/openlogi-gui/locales/da.yml +++ b/crates/openlogi-gui/locales/da.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Midterklik" "Back (Button 4)": "Tilbage (knap 4)" "Forward (Button 5)": "Frem (knap 5)" +"Button 6": "knap 6" +"Button 7": "knap 7" +"Button 8": "knap 8" +"Button 9": "knap 9" "Back": "Tilbage" "Forward": "Frem" "DPI Toggle": "DPI-skift" diff --git a/crates/openlogi-gui/locales/de.yml b/crates/openlogi-gui/locales/de.yml index 99480ebe..3dd75277 100644 --- a/crates/openlogi-gui/locales/de.yml +++ b/crates/openlogi-gui/locales/de.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Mittelklick" "Back (Button 4)": "Zurück (Taste 4)" "Forward (Button 5)": "Vor (Taste 5)" +"Button 6": "Taste 6" +"Button 7": "Taste 7" +"Button 8": "Taste 8" +"Button 9": "Taste 9" "Back": "Zurück" "Forward": "Vor" "DPI Toggle": "DPI-Umschaltung" diff --git a/crates/openlogi-gui/locales/el.yml b/crates/openlogi-gui/locales/el.yml index 3b96ae1c..d9d09b3f 100644 --- a/crates/openlogi-gui/locales/el.yml +++ b/crates/openlogi-gui/locales/el.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Μεσαίο κλικ" "Back (Button 4)": "Πίσω (κουμπί 4)" "Forward (Button 5)": "Εμπρός (κουμπί 5)" +"Button 6": "κουμπί 6" +"Button 7": "κουμπί 7" +"Button 8": "κουμπί 8" +"Button 9": "κουμπί 9" "Back": "Πίσω" "Forward": "Εμπρός" "DPI Toggle": "Εναλλαγή DPI" diff --git a/crates/openlogi-gui/locales/en.yml b/crates/openlogi-gui/locales/en.yml index 7b567aa7..c3ca5d9b 100644 --- a/crates/openlogi-gui/locales/en.yml +++ b/crates/openlogi-gui/locales/en.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Middle Click" "Back (Button 4)": "Back (Button 4)" "Forward (Button 5)": "Forward (Button 5)" +"Button 6": "Button 6" +"Button 7": "Button 7" +"Button 8": "Button 8" +"Button 9": "Button 9" "Back": "Back" "Forward": "Forward" "DPI Toggle": "DPI Toggle" diff --git a/crates/openlogi-gui/locales/es.yml b/crates/openlogi-gui/locales/es.yml index 9374362f..05b43c9b 100644 --- a/crates/openlogi-gui/locales/es.yml +++ b/crates/openlogi-gui/locales/es.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Clic central" "Back (Button 4)": "Atrás (botón 4)" "Forward (Button 5)": "Adelante (botón 5)" +"Button 6": "botón 6" +"Button 7": "botón 7" +"Button 8": "botón 8" +"Button 9": "botón 9" "Back": "Atrás" "Forward": "Adelante" "DPI Toggle": "Cambiar DPI" diff --git a/crates/openlogi-gui/locales/fi.yml b/crates/openlogi-gui/locales/fi.yml index 44fe3b8c..a1a10769 100644 --- a/crates/openlogi-gui/locales/fi.yml +++ b/crates/openlogi-gui/locales/fi.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Keskinapsautus" "Back (Button 4)": "Takaisin (painike 4)" "Forward (Button 5)": "Eteenpäin (painike 5)" +"Button 6": "painike 6" +"Button 7": "painike 7" +"Button 8": "painike 8" +"Button 9": "painike 9" "Back": "Takaisin" "Forward": "Eteenpäin" "DPI Toggle": "DPI-vaihto" diff --git a/crates/openlogi-gui/locales/fr.yml b/crates/openlogi-gui/locales/fr.yml index 1317cd65..4c9a9a78 100644 --- a/crates/openlogi-gui/locales/fr.yml +++ b/crates/openlogi-gui/locales/fr.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Clic du milieu" "Back (Button 4)": "Précédent (bouton 4)" "Forward (Button 5)": "Suivant (bouton 5)" +"Button 6": "bouton 6" +"Button 7": "bouton 7" +"Button 8": "bouton 8" +"Button 9": "bouton 9" "Back": "Précédent" "Forward": "Suivant" "DPI Toggle": "Bascule DPI" diff --git a/crates/openlogi-gui/locales/it.yml b/crates/openlogi-gui/locales/it.yml index 08b290b7..b83acdbb 100644 --- a/crates/openlogi-gui/locales/it.yml +++ b/crates/openlogi-gui/locales/it.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Click centrale" "Back (Button 4)": "Indietro (pulsante 4)" "Forward (Button 5)": "Avanti (pulsante 5)" +"Button 6": "pulsante 6" +"Button 7": "pulsante 7" +"Button 8": "pulsante 8" +"Button 9": "pulsante 9" "Back": "Indietro" "Forward": "Avanti" "DPI Toggle": "Cambia DPI" diff --git a/crates/openlogi-gui/locales/ja.yml b/crates/openlogi-gui/locales/ja.yml index 61c9d9d0..e138a1ae 100644 --- a/crates/openlogi-gui/locales/ja.yml +++ b/crates/openlogi-gui/locales/ja.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "中クリック" "Back (Button 4)": "戻る(サイドボタン4)" "Forward (Button 5)": "進む(サイドボタン5)" +"Button 6": "ボタン6" +"Button 7": "ボタン7" +"Button 8": "ボタン8" +"Button 9": "ボタン9" "Back": "戻る" "Forward": "進む" "DPI Toggle": "DPI 切り替え" diff --git a/crates/openlogi-gui/locales/ko.yml b/crates/openlogi-gui/locales/ko.yml index c4504c0e..8784811e 100644 --- a/crates/openlogi-gui/locales/ko.yml +++ b/crates/openlogi-gui/locales/ko.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "가운데 클릭" "Back (Button 4)": "뒤로 (버튼 4)" "Forward (Button 5)": "앞으로 (버튼 5)" +"Button 6": "버튼 6" +"Button 7": "버튼 7" +"Button 8": "버튼 8" +"Button 9": "버튼 9" "Back": "뒤로" "Forward": "앞으로" "DPI Toggle": "DPI 전환" diff --git a/crates/openlogi-gui/locales/nb.yml b/crates/openlogi-gui/locales/nb.yml index 2f0e55c4..6b9f8ccc 100644 --- a/crates/openlogi-gui/locales/nb.yml +++ b/crates/openlogi-gui/locales/nb.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Midtklikk" "Back (Button 4)": "Tilbake (knapp 4)" "Forward (Button 5)": "Frem (knapp 5)" +"Button 6": "knapp 6" +"Button 7": "knapp 7" +"Button 8": "knapp 8" +"Button 9": "knapp 9" "Back": "Tilbake" "Forward": "Frem" "DPI Toggle": "DPI-veksling" diff --git a/crates/openlogi-gui/locales/nl.yml b/crates/openlogi-gui/locales/nl.yml index 51f32483..8a6f9df3 100644 --- a/crates/openlogi-gui/locales/nl.yml +++ b/crates/openlogi-gui/locales/nl.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Middenklik" "Back (Button 4)": "Vorige (knop 4)" "Forward (Button 5)": "Volgende (knop 5)" +"Button 6": "knop 6" +"Button 7": "knop 7" +"Button 8": "knop 8" +"Button 9": "knop 9" "Back": "Vorige" "Forward": "Volgende" "DPI Toggle": "DPI wisselen" diff --git a/crates/openlogi-gui/locales/pl.yml b/crates/openlogi-gui/locales/pl.yml index fbec6b45..625ead41 100644 --- a/crates/openlogi-gui/locales/pl.yml +++ b/crates/openlogi-gui/locales/pl.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Środkowy przycisk" "Back (Button 4)": "Wstecz (przycisk 4)" "Forward (Button 5)": "Do przodu (przycisk 5)" +"Button 6": "przycisk 6" +"Button 7": "przycisk 7" +"Button 8": "przycisk 8" +"Button 9": "przycisk 9" "Back": "Wstecz" "Forward": "Do przodu" "DPI Toggle": "Przełącznik DPI" diff --git a/crates/openlogi-gui/locales/pt-BR.yml b/crates/openlogi-gui/locales/pt-BR.yml index 4508e5ae..d1df7f1f 100644 --- a/crates/openlogi-gui/locales/pt-BR.yml +++ b/crates/openlogi-gui/locales/pt-BR.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Clique do Meio" "Back (Button 4)": "Voltar (botão 4)" "Forward (Button 5)": "Avançar (botão 5)" +"Button 6": "botão 6" +"Button 7": "botão 7" +"Button 8": "botão 8" +"Button 9": "botão 9" "Back": "Voltar" "Forward": "Avançar" "DPI Toggle": "Alternar DPI" diff --git a/crates/openlogi-gui/locales/pt-PT.yml b/crates/openlogi-gui/locales/pt-PT.yml index 0a13f08c..ff364332 100644 --- a/crates/openlogi-gui/locales/pt-PT.yml +++ b/crates/openlogi-gui/locales/pt-PT.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Clique central" "Back (Button 4)": "Retroceder (botão 4)" "Forward (Button 5)": "Avançar (botão 5)" +"Button 6": "botão 6" +"Button 7": "botão 7" +"Button 8": "botão 8" +"Button 9": "botão 9" "Back": "Retroceder" "Forward": "Avançar" "DPI Toggle": "Alternar DPI" diff --git a/crates/openlogi-gui/locales/ru.yml b/crates/openlogi-gui/locales/ru.yml index 32e1be75..70d19878 100644 --- a/crates/openlogi-gui/locales/ru.yml +++ b/crates/openlogi-gui/locales/ru.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Средний щелчок" "Back (Button 4)": "Назад (боковая кнопка 4)" "Forward (Button 5)": "Вперёд (боковая кнопка 5)" +"Button 6": "кнопка 6" +"Button 7": "кнопка 7" +"Button 8": "кнопка 8" +"Button 9": "кнопка 9" "Back": "Назад" "Forward": "Вперёд" "DPI Toggle": "Переключение DPI" diff --git a/crates/openlogi-gui/locales/sv.yml b/crates/openlogi-gui/locales/sv.yml index cc6cf27f..d46da5b9 100644 --- a/crates/openlogi-gui/locales/sv.yml +++ b/crates/openlogi-gui/locales/sv.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "Mittenklick" "Back (Button 4)": "Bakåt (knapp 4)" "Forward (Button 5)": "Framåt (knapp 5)" +"Button 6": "knapp 6" +"Button 7": "knapp 7" +"Button 8": "knapp 8" +"Button 9": "knapp 9" "Back": "Bakåt" "Forward": "Framåt" "DPI Toggle": "DPI-växling" diff --git a/crates/openlogi-gui/locales/zh-CN.yml b/crates/openlogi-gui/locales/zh-CN.yml index aa217b46..ab7d0d51 100644 --- a/crates/openlogi-gui/locales/zh-CN.yml +++ b/crates/openlogi-gui/locales/zh-CN.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "中键单击" "Back (Button 4)": "后退(侧键4)" "Forward (Button 5)": "前进(侧键5)" +"Button 6": "键6" +"Button 7": "键7" +"Button 8": "键8" +"Button 9": "键9" "Back": "后退" "Forward": "前进" "DPI Toggle": "灵敏度切换" diff --git a/crates/openlogi-gui/locales/zh-HK.yml b/crates/openlogi-gui/locales/zh-HK.yml index e11ad21a..11faaeae 100644 --- a/crates/openlogi-gui/locales/zh-HK.yml +++ b/crates/openlogi-gui/locales/zh-HK.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "中鍵單擊" "Back (Button 4)": "後退(側鍵4)" "Forward (Button 5)": "前進(側鍵5)" +"Button 6": "鍵6" +"Button 7": "鍵7" +"Button 8": "鍵8" +"Button 9": "鍵9" "Back": "後退" "Forward": "前進" "DPI Toggle": "靈敏度切換" diff --git a/crates/openlogi-gui/locales/zh-TW.yml b/crates/openlogi-gui/locales/zh-TW.yml index 5d51239b..c5768329 100644 --- a/crates/openlogi-gui/locales/zh-TW.yml +++ b/crates/openlogi-gui/locales/zh-TW.yml @@ -145,6 +145,10 @@ _version: 1 "Middle Click": "中鍵按一下" "Back (Button 4)": "後退(側鍵4)" "Forward (Button 5)": "前進(側鍵5)" +"Button 6": "鍵6" +"Button 7": "鍵7" +"Button 8": "鍵8" +"Button 9": "鍵9" "Back": "後退" "Forward": "前進" "DPI Toggle": "靈敏度切換" diff --git a/crates/openlogi-gui/src/mouse_model/picker.rs b/crates/openlogi-gui/src/mouse_model/picker.rs index cfdec04d..e2acc1bd 100644 --- a/crates/openlogi-gui/src/mouse_model/picker.rs +++ b/crates/openlogi-gui/src/mouse_model/picker.rs @@ -303,6 +303,12 @@ pub(crate) fn action_icon_path(action: &Action) -> &'static str { // BrowserBack/BrowserForward's bare arrows in the Navigation section. Action::MouseBack => "action-icons/circle-arrow-left.svg", Action::MouseForward => "action-icons/circle-arrow-right.svg", + // Buttons 6–9 have no canonical glyph; reuse the generic mouse icon + // (same as MiddleClick). The button number is in the label. + Action::MouseButton6 + | Action::MouseButton7 + | Action::MouseButton8 + | Action::MouseButton9 => "action-icons/mouse.svg", Action::Copy => "action-icons/copy.svg", Action::Paste => "action-icons/clipboard-paste.svg", Action::Cut => "action-icons/scissors.svg", diff --git a/crates/openlogi-inject/src/inject.rs b/crates/openlogi-inject/src/inject.rs index 48f8c603..6a994733 100644 --- a/crates/openlogi-inject/src/inject.rs +++ b/crates/openlogi-inject/src/inject.rs @@ -76,6 +76,11 @@ fn execute_linux(action: &Action) { // buttons ("back"/"forward") browsers handle natively. Action::MouseBack => linux::click(KeyCode::BTN_SIDE), Action::MouseForward => linux::click(KeyCode::BTN_EXTRA), + // Buttons 6–9 use the evdev extra-button codes beyond the side pair. + Action::MouseButton6 => linux::click(KeyCode::BTN_FORWARD), + Action::MouseButton7 => linux::click(KeyCode::BTN_BACK), + Action::MouseButton8 => linux::click(KeyCode::BTN_TASK), + Action::MouseButton9 => linux::click(KeyCode::BTN_0), // ── Editing ─────────────────────────────────────────────────────── Action::Copy => linux::press_key(&[ctrl], KeyCode::KEY_C), Action::Paste => linux::press_key(&[ctrl], KeyCode::KEY_V), @@ -185,6 +190,13 @@ fn execute_macos(action: &Action) { // 4 = forward / "button 5"). Action::MouseBack => macos::post_other_button(3), Action::MouseForward => macos::post_other_button(4), + // Buttons 6–9 (button numbers 5–8, 0-indexed). Same path as 4/5 — + // post_other_button stamps MOUSE_EVENT_BUTTON_NUMBER to address any + // button ≥ 3. + Action::MouseButton6 => macos::post_other_button(5), + Action::MouseButton7 => macos::post_other_button(6), + Action::MouseButton8 => macos::post_other_button(7), + Action::MouseButton9 => macos::post_other_button(8), // ── Editing ─────────────────────────────────────────────────────── Action::Copy => macos::post_key(VK_C, cmd), Action::Paste => macos::post_key(VK_V, cmd), @@ -289,6 +301,18 @@ fn execute_windows(action: &Action) { Action::MiddleClick => windows::post_click(windows::MouseButton::Middle), Action::MouseBack => windows::post_click(windows::MouseButton::Back), Action::MouseForward => windows::post_click(windows::MouseButton::Forward), + // Windows SendInput carries flags for buttons 1–5 only; there is no + // flag for button 6+, so these log-and-skip (same pattern as the + // macOS-only navigation actions). macOS/Linux emit them natively. + Action::MouseButton6 + | Action::MouseButton7 + | Action::MouseButton8 + | Action::MouseButton9 => { + tracing::debug!( + action = action.label(), + "mouse buttons 6-9 are not supported on Windows — press ignored" + ); + } Action::Copy => windows::post_key(windows::VK_C, &[windows::VK_CONTROL]), Action::Paste => windows::post_key(windows::VK_V, &[windows::VK_CONTROL]), Action::Cut => windows::post_key(windows::VK_X, &[windows::VK_CONTROL]), diff --git a/docs/superpowers/plans/2026-06-29-mouse-buttons-6-9.md b/docs/superpowers/plans/2026-06-29-mouse-buttons-6-9.md new file mode 100644 index 00000000..c9974608 --- /dev/null +++ b/docs/superpowers/plans/2026-06-29-mouse-buttons-6-9.md @@ -0,0 +1,410 @@ +# Mouse Buttons 6–9 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add four pickable actions (`MouseButton6`–`MouseButton9`) that synthesize mouse buttons 6–9 on macOS and Linux, surfaced in the action picker under the MOUSE section. + +**Architecture:** Pure data-driven addition. The `Action` enum in `openlogi-core` is the single source of truth — the GUI picker, category grouping, TOML schema, and per-platform injection all derive from it. Four new unit variants mirror the existing `MouseBack`/`MouseForward` pattern. On macOS the existing `post_other_button(n)` already accepts any button number; on Linux the evdev `BTN_*` family covers it; on Windows `SendInput` caps at button 5, so 6–9 log-and-skip there (documented gap, same pattern as existing platform-limited actions). + +**Tech Stack:** Rust (workspace), serde/TOML for config, GPUI for the GUI, core-graphics / evdev / windows-sys for injection, rust-i18n for locale strings. + +**Spec:** `docs/superpowers/specs/2026-06-29-mouse-buttons-6-9-design.md` + +--- + +## File Structure + +| File | Responsibility | Change | +|---|---|---| +| `crates/openlogi-core/src/binding.rs` | The `Action` enum + `label()`/`category()`/`catalog()` | Add 4 variants + their 3 match arms + extend 2 tests | +| `crates/openlogi-inject/src/inject.rs` | Per-platform `Action` → OS event synthesis | Add arms in `execute_macos` / `execute_linux` / `execute_windows` | +| `crates/openlogi-gui/src/mouse_model/picker.rs` | Picker icon mapping (exhaustive `match`) | Add 4 arms (compiler-forced) | +| `crates/openlogi-gui/locales/*.yml` (20 files) | i18n translation keys, keyed by English label | Add `"Button 6"`–`"Button 9"` keys after line 147 | + +No new files. The exhaustive `match` arms across the codebase are the safety net — the compiler refuses to build if any variant is missed. + +--- + +## Task 1: Add the `Action` variants and core metadata + +This task adds the four variants and threads them through `label()`, `category()`, and `catalog()`. Tests fail first, then pass. + +**Files:** +- Modify: `crates/openlogi-core/src/binding.rs` + +- [ ] **Step 1: Write the failing test (extend `category_mouse_variants`)** + +In `crates/openlogi-core/src/binding.rs`, find the test at line ~1346 and replace it: + +```rust + #[test] + fn category_mouse_variants() { + assert_eq!(Action::LeftClick.category(), Category::Mouse); + assert_eq!(Action::RightClick.category(), Category::Mouse); + assert_eq!(Action::MiddleClick.category(), Category::Mouse); + assert_eq!(Action::MouseBack.category(), Category::Mouse); + assert_eq!(Action::MouseForward.category(), Category::Mouse); + assert_eq!(Action::MouseButton6.category(), Category::Mouse); + assert_eq!(Action::MouseButton7.category(), Category::Mouse); + assert_eq!(Action::MouseButton8.category(), Category::Mouse); + assert_eq!(Action::MouseButton9.category(), Category::Mouse); + } +``` + +- [ ] **Step 2: Add a label test (append to the `#[cfg(test)] mod tests` block, after `category_mouse_variants`)** + +```rust + #[test] + fn extra_mouse_button_labels() { + assert_eq!(Action::MouseButton6.label(), "Button 6"); + assert_eq!(Action::MouseButton7.label(), "Button 7"); + assert_eq!(Action::MouseButton8.label(), "Button 8"); + assert_eq!(Action::MouseButton9.label(), "Button 9"); + } +``` + +- [ ] **Step 3: Run the tests to verify they fail** + +Run: `cargo test -p openlogi-core --lib binding::tests::category_mouse_variants binding::tests::extra_mouse_button_labels` +Expected: FAIL with `cannot find variant MouseButton6` (and 7/8/9) — compile error. + +- [ ] **Step 4: Add the four variants to the `Action` enum** + +In `crates/openlogi-core/src/binding.rs`, find the `MouseForward` variant (line ~371) and add the four new variants immediately after it, before the `// ── Editing ───` comment: + +```rust + /// Mouse "forward" side button (extra button 5). Native counterpart to + /// [`Action::MouseBack`]; see [`Action::BrowserForward`] for the ⌘] form. + MouseForward, + /// Extra mouse button 6. Emitted as the real button-6 event for apps, games, + /// and CAD software that bind it. macOS/Linux only — Windows `SendInput` + /// caps at button 5, so this logs-and-skips there. + MouseButton6, + /// Extra mouse button 7. See [`Action::MouseButton6`]. + MouseButton7, + /// Extra mouse button 8. See [`Action::MouseButton6`]. + MouseButton8, + /// Extra mouse button 9. See [`Action::MouseButton6`]. + MouseButton9, +``` + +- [ ] **Step 5: Add the four labels to `Action::label()`** + +In the `label()` match (around line ~686, after the `MouseForward` arm), add: + +```rust + Action::MouseForward => "Forward (Button 5)".into(), + Action::MouseButton6 => "Button 6".into(), + Action::MouseButton7 => "Button 7".into(), + Action::MouseButton8 => "Button 8".into(), + Action::MouseButton9 => "Button 9".into(), +``` + +- [ ] **Step 6: Add the four to `Action::category()`** + +In the `category()` match (around line ~737), extend the existing Mouse arm: + +```rust + Action::LeftClick + | Action::RightClick + | Action::MiddleClick + | Action::MouseBack + | Action::MouseForward + | Action::MouseButton6 + | Action::MouseButton7 + | Action::MouseButton8 + | Action::MouseButton9 => Category::Mouse, +``` + +- [ ] **Step 7: Add the four to `Action::catalog()`** + +In the `catalog()` vec (around line ~793, after `Action::MouseForward`), add: + +```rust + // Mouse + Action::LeftClick, + Action::RightClick, + Action::MiddleClick, + Action::MouseBack, + Action::MouseForward, + Action::MouseButton6, + Action::MouseButton7, + Action::MouseButton8, + Action::MouseButton9, +``` + +- [ ] **Step 8: Run the tests to verify they pass** + +Run: `cargo test -p openlogi-core --lib binding` +Expected: PASS — `category_mouse_variants`, `extra_mouse_button_labels`, and `all_catalog_variants_roundtrip_toml` (which iterates `catalog()`) all pass. + +- [ ] **Step 9: Commit** + +```bash +git add crates/openlogi-core/src/binding.rs +git commit -m "feat(core): add MouseButton6-9 actions to the binding vocabulary" +``` + +--- + +## Task 2: Inject the buttons on macOS + +macOS already has `post_other_button(n)` which stamps `MOUSE_EVENT_BUTTON_NUMBER`. Buttons 6–9 map to numbers 5–8 (0-indexed: Back=3, Forward=4). + +**Files:** +- Modify: `crates/openlogi-inject/src/inject.rs` (the `execute_macos` function, around line 186–187) + +- [ ] **Step 1: Add the macOS injection arms** + +Find the macOS extra-button arms (line ~186–187): + +```rust + Action::MouseBack => macos::post_other_button(3), + Action::MouseForward => macos::post_other_button(4), +``` + +Add immediately after them: + +```rust + Action::MouseBack => macos::post_other_button(3), + Action::MouseForward => macos::post_other_button(4), + // Buttons 6–9 (button numbers 5–8, 0-indexed). Same path as 4/5 — + // post_other_button stamps MOUSE_EVENT_BUTTON_NUMBER to address any + // button ≥ 3. + Action::MouseButton6 => macos::post_other_button(5), + Action::MouseButton7 => macos::post_other_button(6), + Action::MouseButton8 => macos::post_other_button(7), + Action::MouseButton9 => macos::post_other_button(8), +``` + +- [ ] **Step 2: Verify the macOS build compiles** + +Run: `cargo build -p openlogi-inject` +Expected: BUILD SUCCEEDS (on macOS the `execute_macos` arms are the ones compiled). + +- [ ] **Step 3: Commit** + +```bash +git add crates/openlogi-inject/src/inject.rs +git commit -m "feat(inject): synthesize mouse buttons 6-9 on macOS" +``` + +--- + +## Task 3: Inject the buttons on Linux + +evdev 0.13.2 exposes `BTN_BACK`, `BTN_FORWARD`, `BTN_TASK`, and `BTN_0` as `KeyCode` constants. These are the conventional codes for extra mouse buttons beyond the side pair. + +**Files:** +- Modify: `crates/openlogi-inject/src/inject.rs` (the `execute_linux` function, around line 77–78) + +- [ ] **Step 1: Add the Linux injection arms** + +Find the Linux extra-button arms (line ~77–78): + +```rust + Action::MouseBack => linux::click(KeyCode::BTN_SIDE), + Action::MouseForward => linux::click(KeyCode::BTN_EXTRA), +``` + +Add immediately after them: + +```rust + Action::MouseBack => linux::click(KeyCode::BTN_SIDE), + Action::MouseForward => linux::click(KeyCode::BTN_EXTRA), + // Buttons 6–9 use the evdev extra-button codes beyond the side pair. + Action::MouseButton6 => linux::click(KeyCode::BTN_FORWARD), + Action::MouseButton7 => linux::click(KeyCode::BTN_BACK), + Action::MouseButton8 => linux::click(KeyCode::BTN_TASK), + Action::MouseButton9 => linux::click(KeyCode::BTN_0), +``` + +- [ ] **Step 2: Verify it compiles on Linux (cross-check)** + +This is a `#[cfg(target_os = "linux")]` block. On macOS it won't be compiled, so to verify the `KeyCode::BTN_*` constants resolve, run a Linux target check: + +Run: `cargo check -p openlogi-inject --target x86_64-unknown-linux-gnu` +Expected: If the target is installed, CHECK SUCCEEDS. If not installed, this step is skipped — the constant names (`BTN_FORWARD`/`BTN_BACK`/`BTN_TASK`/`BTN_0`) are confirmed present in evdev 0.13.2 (see spec's button-number table), and CI will catch any mismatch on Linux. + +- [ ] **Step 3: Commit** + +```bash +git add crates/openlogi-inject/src/inject.rs +git commit -m "feat(inject): synthesize mouse buttons 6-9 on Linux via evdev BTN_*" +``` + +--- + +## Task 4: Log-and-skip on Windows + +Windows `SendInput` mouse input carries flags for buttons 1–5 only; there is no flag for button 6+. The codebase's established pattern for "no platform equivalent" is a `tracing::debug!` log and skip (see the macOS-only navigation actions at `inject.rs:104`). Mirror that. + +**Files:** +- Modify: `crates/openlogi-inject/src/inject.rs` (the `execute_windows` function, around line 290–291) + +- [ ] **Step 1: Add the Windows log-and-skip arms** + +Find the Windows extra-button arms (line ~290–291): + +```rust + Action::MouseBack => windows::post_click(windows::MouseButton::Back), + Action::MouseForward => windows::post_click(windows::MouseButton::Forward), +``` + +Add immediately after them: + +```rust + Action::MouseBack => windows::post_click(windows::MouseButton::Back), + Action::MouseForward => windows::post_click(windows::MouseButton::Forward), + // Windows SendInput carries flags for buttons 1–5 only; there is no + // flag for button 6+, so these log-and-skip (same pattern as the + // macOS-only navigation actions). macOS/Linux emit them natively. + Action::MouseButton6 + | Action::MouseButton7 + | Action::MouseButton8 + | Action::MouseButton9 => { + tracing::debug!( + action = action.label(), + "mouse buttons 6-9 are not supported on Windows — press ignored" + ); + } +``` + +- [ ] **Step 2: Verify it compiles on Windows (cross-check)** + +Run: `cargo check -p openlogi-inject --target x86_64-pc-windows-msvc` +Expected: If the target is installed, CHECK SUCCEEDS. If not, skip — CI covers Windows. (No new types are referenced; `tracing::debug!` and `action.label()` already exist.) + +- [ ] **Step 3: Commit** + +```bash +git add crates/openlogi-inject/src/inject.rs +git commit -m "feat(inject): log-and-skip mouse buttons 6-9 on Windows" +``` + +--- + +## Task 5: Add picker icons (compiler-forced) + +The picker's `action_icon_path` is an exhaustive `match` with no wildcard. After Task 1, the macOS/GUI build will fail here until the four variants are mapped. Reuse the existing generic mouse icon (`action-icons/mouse.svg`, already used by `MiddleClick`). + +**Files:** +- Modify: `crates/openlogi-gui/src/mouse_model/picker.rs` (the `action_icon_path` match, line ~304–305) + +- [ ] **Step 1: Add the four icon arms** + +Find the MouseBack/MouseForward arms (line ~304–305): + +```rust + Action::MouseBack => "action-icons/circle-arrow-left.svg", + Action::MouseForward => "action-icons/circle-arrow-right.svg", +``` + +Add immediately after them: + +```rust + Action::MouseBack => "action-icons/circle-arrow-left.svg", + Action::MouseForward => "action-icons/circle-arrow-right.svg", + // Buttons 6–9 have no canonical glyph; reuse the generic mouse icon + // (same as MiddleClick). The button number is in the label. + Action::MouseButton6 + | Action::MouseButton7 + | Action::MouseButton8 + | Action::MouseButton9 => "action-icons/mouse.svg", +``` + +- [ ] **Step 2: Verify the full workspace builds (this is the compile-time gate)** + +Run: `cargo build -p openlogi-gui` +Expected: BUILD SUCCEEDS. If any variant is still missing an arm anywhere, this fails — that's the safety net working. + +- [ ] **Step 3: Commit** + +```bash +git add crates/openlogi-gui/src/mouse_model/picker.rs +git commit -m "feat(gui): pick icons for mouse buttons 6-9 in the action picker" +``` + +--- + +## Task 6: Add i18n keys to all 20 locale files + +The picker translates action labels via `t!(action.label())` — keyed by the English string. The existing `"Back (Button 4)"` / `"Forward (Button 5)"` keys live at line ~146–147 of every locale file. New keys `"Button 6"`–`"Button 9"` must be added so the labels translate. For non-English locales, the translation mirrors the English form plus the localized "Button" word where the locale already uses one — but since the English label is intentionally number-only, the safest correct default is to leave the translated value identical to the key (English fallback) except where the locale clearly localizes "Button" (most do not for raw button numbers). + +**Files:** +- Modify: all 20 files in `crates/openlogi-gui/locales/*.yml` + +- [ ] **Step 1: Add the four keys to `en.yml` (the source)** + +In `crates/openlogi-gui/locales/en.yml`, after line 147 (`"Forward (Button 5)": "Forward (Button 5)"`), add: + +```yaml +"Forward (Button 5)": "Forward (Button 5)" +"Button 6": "Button 6" +"Button 7": "Button 7" +"Button 8": "Button 8" +"Button 9": "Button 9" +``` + +- [ ] **Step 2: Add the same four keys to each of the other 19 locale files** + +For each file in `crates/openlogi-gui/locales/` except `en.yml`, after the `"Forward (Button 5)"` line (which exists at line ~147 in every file — confirmed), append: + +```yaml +"Button 6": "Button 6" +"Button 7": "Button 7" +"Button 8": "Button 8" +"Button 9": "Button 9" +``` + +The 19 files are: `da.yml de.yml el.yml es.yml fi.yml fr.yml it.yml ja.yml ko.yml nb.yml nl.yml pl.yml pt-BR.yml pt-PT.yml ru.yml sv.yml zh-CN.yml zh-HK.yml zh-TW.yml`. + +(Values are left as the English string — these are raw button numbers with no semantic name to localize. A crowdin pass can refine later; the project already uses crowdin per `crowdin.yml`. Leaving them English-identical is the correct fallback and matches how `en.yml` itself is authored.) + +- [ ] **Step 3: Verify the GUI still builds and the i18n test passes** + +Run: `cargo test -p openlogi-gui --lib i18n` +Expected: PASS. (The i18n test at `i18n.rs:185+` checks specific known strings, not exhaustively, so it won't break — but it confirms the locale loader still parses all files.) + +- [ ] **Step 4: Commit** + +```bash +git add crates/openlogi-gui/locales/*.yml +git commit -m "feat(gui): add Button 6-9 translation keys to all locales" +``` + +--- + +## Task 7: Final whole-workspace verification + +Confirm everything builds and tests pass end-to-end. + +- [ ] **Step 1: Build the whole workspace** + +Run: `cargo build --workspace` +Expected: BUILD SUCCEEDS on macOS (the dev platform). This compiles `execute_macos`, the picker, the core — everything reachable on this host. + +- [ ] **Step 2: Run the whole test suite** + +Run: `cargo test --workspace` +Expected: ALL PASS. Key tests: `binding::tests::category_mouse_variants`, `binding::tests::extra_mouse_button_labels`, `binding::tests::all_catalog_variants_roundtrip_toml` (now exercises the 4 new variants' TOML roundtrip). + +- [ ] **Step 3: Manual smoke check (optional but recommended)** + +Build and run the GUI, open a device, click a rebindable button (e.g. Gesture Button), and confirm "Button 6"–"Button 9" appear in the MOUSE section of the action picker. Bind one and confirm it fires (e.g. an app that binds MB6). + +Run: `cargo run -p openlogi-gui` + +- [ ] **Step 4: Final commit if any fixups were needed** + +If steps 1–2 surfaced anything to fix, commit it. Otherwise this task produces no commit. + +--- + +## Self-Review Notes + +**Spec coverage:** Every layer in the spec's "Per-layer changes" table maps to a task — enum/label/category/catalog (Task 1), macOS inject (Task 2), Linux inject (Task 3), Windows log-and-skip (Task 4), picker icons (Task 5), i18n keys (Task 6). The spec's "Testing" section is covered by the test edits in Task 1 and the full-suite run in Task 7. Platform-coverage table matches (Windows gap explicit in Task 4). + +**No placeholders:** Every code step shows the exact code. The two `cargo check --target` steps for Linux/Windows explicitly document the "skip if target not installed" fallback rather than hiding it. + +**Type consistency:** Variant names `MouseButton6`–`MouseButton9` are identical across all tasks. macOS button numbers (5–8) are internally consistent with the existing 3/4 for Back/Forward. Linux `KeyCode` constants are confirmed in evdev 0.13.2. diff --git a/docs/superpowers/specs/2026-06-29-mouse-buttons-6-9-design.md b/docs/superpowers/specs/2026-06-29-mouse-buttons-6-9-design.md new file mode 100644 index 00000000..b88a4561 --- /dev/null +++ b/docs/superpowers/specs/2026-06-29-mouse-buttons-6-9-design.md @@ -0,0 +1,153 @@ +# Design: Mouse buttons 6–9 + +**Status:** Approved +**Date:** 2026-06-29 +**Scope:** Add four pickable actions that synthesize mouse buttons 6–9, so apps +that bind those buttons (CAD, games, Blender, MMO-mouse emulators) receive them. + +## Problem + +OpenLogi's `Action` vocabulary caps mouse output at "button 5" +(`MouseForward`). There is no way to emit mouse buttons 6–9, even though the +underlying injection layer on macOS and Linux already supports arbitrary button +numbers. Users who want a physical button to produce a button-6-or-higher event +have no path today. + +## Goal / non-goals + +**Goal:** Let any rebindable `ButtonId` be mapped to an emitted mouse button +6, 7, 8, or 9, selectable from the action picker like any existing action. + +**Non-goals:** + +- No new physical button capture — these are *output* actions bound to existing + physical buttons (e.g. Gesture Button → button 6). +- No new picker UI. The catalog auto-surfaces new variants under the MOUSE + section. +- No Windows support for buttons 6–9. `SendInput`'s mouse path carries flags + for buttons 1–5 only; 6–9 are a documented macOS/Linux-only gap (Windows + remains an "untested preview" per the README). See + [Platform coverage](#platform-coverage). + +## Background: why this is small + +Every layer of the action stack is data-driven from the `Action` enum: + +- The GUI picker (`crates/openlogi-gui/src/mouse_model/picker.rs`) builds its + rows by calling `Action::catalog()` and grouping by `Action::category()`. +- The picker's icon mapping (`picker.rs:298`) is an exhaustive `match` with + **no wildcard arm**, so the compiler refuses to build if a new variant lacks + an icon entry. +- `Action::label()` / `category()` / `catalog()` drive the picker text, + grouping, and TOML roundtrip tests. +- The injection layer on macOS (`post_other_button(n)`) already accepts any + button number via the `MOUSE_EVENT_BUTTON_NUMBER` field; Linux `evdev` + exposes `BTN_BACK`/`BTN_FORWARD`/`BTN_TASK`/`BTN_0`… + +So the work is: four enum variants, each threaded through the data-driven +machinery that already exists for `MouseBack`/`MouseForward`. + +## Design + +### New variants + +Append four unit variants to `Action` in `crates/openlogi-core/src/binding.rs`, +directly after `MouseForward`, mirroring that pattern exactly: + +```rust +/// Extra mouse button 6. Emitted as the real button-6 event for apps/games/CAD +/// that bind it. macOS/Linux only — Windows SendInput caps at button 5. +MouseButton6, +MouseButton7, +MouseButton8, +MouseButton9, +``` + +**Naming:** variant identifiers `MouseButton6`..`MouseButton9`; display labels +`"Button 6"`..`"Button 9"`. Unlike `Back`/`Forward`, these numbers have no +universal semantic meaning, so they carry no semantic name. + +### Per-layer changes + +| Layer | File | Change | +|---|---|---| +| Enum | `openlogi-core/src/binding.rs` | +4 variants after `MouseForward` | +| `Action::label()` | same | `"Button 6"` … `"Button 9"` | +| `Action::category()` | same | all 4 → `Category::Mouse` | +| `Action::catalog()` | same | append all 4 to the catalog (Mouse group) | +| Picker icon map | `openlogi-gui/src/mouse_model/picker.rs:298` | map all 4 to an existing generic icon (`action-icons/mouse.svg`) — the existing exhaustive `match` forces this | +| Inject — macOS | `openlogi-inject/src/inject.rs` (`execute_macos`) | `MouseButton6..9` → `macos::post_other_button(5..=8)` | +| Inject — Linux | `openlogi-inject/src/inject.rs` (`execute_linux`) | → `BTN_FORWARD` / `BTN_BACK` / `BTN_TASK` / `BTN_0` | +| Inject — Windows | `openlogi-inject/src/inject.rs` (`execute_windows`) | log-and-skip (`tracing::debug!`, same pattern as the macOS-only navigation actions at `inject.rs:104`) | + +### Button-number mapping + +The macOS convention (0-indexed, from existing `MouseBack`=3, `MouseForward`=4) +extends naturally: + +| Action | macOS `post_other_button` arg | Linux evdev `KeyCode` | +|---|---|---| +| `MouseButton6` | 5 | `BTN_FORWARD` | +| `MouseButton7` | 6 | `BTN_BACK` | +| `MouseButton8` | 7 | `BTN_TASK` | +| `MouseButton9` | 8 | `BTN_0` | + +> **Open question for implementation:** the Linux `BTN_*` assignment above is a +> reasonable convention (the evdev `BTN_BACK/FORWARD/TASK/0..9` family is how +> multi-button mice report extras), but exact code choice is a convention call +> that should be confirmed against how target apps read buttons on Linux. +> macOS numbers are unambiguous. + +### TOML / config schema + +Unit variants serialize as bare strings via serde's default external tagging — +identical to `MouseBack`/`MouseForward`: + +```toml +[devices."".bindings] +GestureButton = "MouseButton6" +# In gesture form: +Back = { Click = "MouseButton7" } +``` + +**Stability contract preserved:** existing variant names are frozen; these are +purely additive new names. **No `schema_version` bump, no migration.** Older +OpenLogi builds reading a config containing `MouseButton6` will error on the +unknown variant (acceptable — same as any newer-schema config on older code). + +### Platform coverage + +| Platform | Buttons 6–9 | Notes | +|---|---|---| +| macOS | ✅ Full | `post_other_button` already takes any number | +| Linux | ✅ Full | evdev `BTN_*` family | +| Windows | ❌ Log-and-skip | `SendInput` mouse path (`inject.rs:1416`) has flags for buttons 1–5 only; no flag exists for 6+. Documented gap, matches the codebase's existing "no platform equivalent → debug log + skip" pattern. | + +Windows users who bind these actions see nothing on press and a debug log line; +no crash, no misfire. Given Windows is an untested preview and the requester is +on macOS, this boundary is acceptable and explicitly out of scope to fix here. + +## Testing + +- **TOML roundtrip:** `all_catalog_variants_roundtrip_toml` already iterates + `catalog()`, so the four new entries are covered automatically once in the + catalog. +- **Category:** extend `category_mouse_variants` to assert all four map to + `Category::Mouse`. +- **Compile-time guarantee:** the exhaustive picker icon `match` (no wildcard) + fails to build if any variant is missed — this is the primary safety net. + +## Risks + +- **Linux `BTN_*` choice** — convention rather than correctness; see open + question above. Low impact (target apps are the test). +- **Pickup-row clutter** — four more entries in the MOUSE group. Acceptable; + matches user intent for a "full set". +- None to the input-capture path — these are pure output/synthesis actions. + +## Out of scope + +- Windows support for buttons 6–9. +- A parameterized `MouseButton(n)` variant (Approach B) — rejected as + disproportionate picker UI for a fixed set of four. +- Capturing buttons 6–9 as *input* from exotic hardware.