srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-09 14:35:00 +0200
committersrdusr <[email protected]>2026-06-09 14:35:00 +0200
commit9028f8175bda63d6a85e5a2dee2b4039020317a4 (patch)
tree6046c5c1dc2271b2acd7a0a8bd2fa72ea2d42fb5
parentf3558f18ecabba983aba001468b06394471e6d8b (diff)
downloadmitmux-9028f8175bda63d6a85e5a2dee2b4039020317a4.tar.gz
mitmux-9028f8175bda63d6a85e5a2dee2b4039020317a4.zip
Mouse support: wheel scroll everywhere, right-click context menus
Explicitly requested - this is a real terminal app meant to work in any terminal (the name is a naming convention, not a tmux runtime dependency), and should be genuinely mouse-driven, not keyboard-only. Enabled via tea.WithMouseCellMotion() (SGR mouse mode, the same protocol nvim and most modern TUI apps use); coexists with tmux's own mouse mode the same way it would for any other terminal app. Scope was decided by a real, verified library constraint, not convenience: bubbles/table exposes no way to learn its own scroll offset (confirmed by reading its source - no YOffset accessor, the rendered window comes from unexported fields via a second internal layer of viewport scrolling on top of that). Mapping a click's screen coordinates to a specific table row can't be done without reaching into that library's private internals, which this deliberately doesn't do - silently selecting the wrong row on a misjudged click is worse than not supporting precise click-to-row at all. What's shipped, the reliable subset: - Wheel scroll everywhere there's something to scroll: tables via the already-exported MoveUp/MoveDown (no scroll-state assumptions needed), viewports via their own native wheel handling (bubbles/ viewport already has this - nothing in the codebase was routing tea.MouseMsg to it yet), and vi-modal text editors via new viTextarea.ScrollUp/ScrollDown (bubbles/textarea has zero native mouse handling at all, confirmed the same way - feeds wheel events as repeated up/down keypresses through the same tested movement path h/j/k/l already use). - Right-click opens a context menu (cmd/mitmux/contextmenu.go) - a horizontal strip taking over the status/help line, the same "replace the bottom of the screen" pattern confirmPrompt and the export/import prompts already use, rather than a floating popup positioned at the click (lipgloss/bubbletea have no compositor for splicing an overlay into an arbitrary screen position - not worth building just for this). Wired into every list-based view: history (view/repeater/intruder/flag-unflag/delete), rules (edit/enable-disable/delete), scope (enable-disable/delete). Menu items ARE reliably clickable, unlike table rows - the menu renders its own strip, so every item's width is fully known rather than hidden behind a library's unexported scroll state. Navigable by mouse click or j/k/arrows+enter; esc or right-clicking again dismisses. Deferred, not silently dropped: click-to-select-a-different-row (same scroll-offset limitation), and click-to-switch-pane-focus in Repeater/ Intruder (tractable via the same Y-coordinate math WindowSizeMsg already computes, just not done yet - keyboard tab already covers it, so lower priority than what shipped). Verified live in tmux by injecting real SGR mouse escape sequences directly into the pane (tmux send-keys -l with hand-built ESC [ < Cb;Cx;Cy M/m sequences, since tmux has no built-in "synthesize a click" primitive) against a running daemon with real captured entries: wheel-down/up on the history table correctly moved the selected-row highlight (confirmed via ANSI-aware capture, not just "no crash"); right-click opened the menu with the right actions; clicking directly on a computed menu-item position correctly triggered that exact action (clicked "delete", saw the correct entry's ID in the resulting confirm prompt); keyboard navigation inside the menu moved the highlight correctly; esc and a second right-click both dismissed cleanly with no side effects; wheel events in Repeater's text pane and Detail's viewport caused no crash and left vi-mode state intact; an empty rules table's right-click correctly no-opped; adding a real rule then right-clicking and clicking "disable" correctly toggled it off (confirmed via the rendered checkmark disappearing). go build/vet/gofmt/test/mod tidy all clean.
-rw-r--r--PLAN.md75
-rw-r--r--README.md43
-rw-r--r--cmd/mitmux/contextmenu.go68
-rw-r--r--cmd/mitmux/main.go57
-rw-r--r--cmd/mitmux/mouse.go233
-rw-r--r--cmd/mitmux/vimode.go19
6 files changed, 494 insertions, 1 deletions
diff --git a/PLAN.md b/PLAN.md
index 0d1d52e..fe55cf8 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -436,3 +436,78 @@ support. No code needed here, just documented clearly in the README's
Quick start (previously this was implied but never actually spelled
out for a phone/tablet setup, which is a real, common daily workflow
this tool hadn't explicitly walked through before).
+
+## Mouse support
+
+Explicitly requested - this is a real terminal app meant to work in any
+terminal (not tmux-only; the name is a naming convention, not a runtime
+dependency), and should be genuinely mouse-driven, not keyboard-only.
+Enabled via `tea.WithMouseCellMotion()` (SGR mouse mode, the same
+protocol nvim and most modern TUI apps use) - coexists with tmux's own
+mouse mode the same way it would for any other terminal app, no
+mitmux-specific code needed for that part; documented clearly instead
+(README's new Mouse section) since it does need tmux's own `set -g
+mouse on` to forward events at all.
+
+Scope was decided by a real, verified library constraint, not
+convenience: bubbles/table exposes no way to learn its own scroll
+offset (confirmed by reading its source - no `YOffset` accessor, and
+the rendered window is computed from unexported fields via a second,
+internal layer of viewport scrolling on top of that). Mapping a click's
+screen coordinates to a specific table row therefore can't be done
+without reaching into that library's private internals, which this
+deliberately doesn't do - silently selecting the wrong row on a
+misjudged click is worse than not supporting precise click-to-row at
+all. What's actually shipped, chosen to be the reliable subset:
+
+- Wheel scroll everywhere there's something to scroll - tables via
+ `MoveUp`/`MoveDown` (exported, no scroll-state assumptions needed),
+ viewports via their own native wheel handling (bubbles/viewport
+ already has this, just needed `tea.MouseMsg` routed to it, which
+ nothing in the codebase was doing yet), and vi-modal text editors via
+ new `viTextarea.ScrollUp`/`ScrollDown` (bubbles/textarea has *zero*
+ native mouse handling at all, confirmed the same way - the value here
+ is feeding it as repeated up/down keypresses through the same tested
+ movement path h/j/k/l already use, not reimplementing cursor math).
+- Right-click opens a context menu - a horizontal strip taking over the
+ status/help line (cmd/mitmux/contextmenu.go), the same "replace the
+ bottom of the screen" pattern confirmPrompt and the export/import
+ prompts already use, rather than a floating popup positioned at the
+ click. That's also a deliberate simplification: lipgloss/bubbletea
+ have no compositor for splicing an ANSI-styled overlay into an
+ arbitrary screen position, and building one just for this would be a
+ lot of new, fragile machinery for what's fundamentally a nice-to-have.
+ Wired into history (view/repeater/intruder/flag-unflag/delete), rules
+ (edit/enable-disable/delete), and scope (enable-disable/delete) -
+ every list-based view. Menu items ARE reliably clickable, unlike table
+ rows: the menu renders its own strip, so every item's on-screen width
+ is fully known rather than hidden behind unexported scroll state.
+ Navigable by mouse click, or j/k/arrows + enter; esc or right-clicking
+ again dismisses without acting.
+
+Deferred, not silently dropped: click-to-select-a-specific-different-
+row in a table (blocked by the same scroll-offset limitation above),
+and click-to-switch-pane-focus in Repeater/Intruder (would need
+Y-coordinate matching against the exact same split-point math
+WindowSizeMsg already computes - tractable, just not done yet, lower
+priority than what shipped since keyboard `tab` already covers it).
+
+Verified live in tmux by injecting real SGR mouse escape sequences
+directly into the pane (`tmux send-keys -l` with hand-built `ESC [ <
+Cb;Cx;Cy M/m` sequences - there's no built-in "synthesize a mouse
+click" primitive in tmux's own tooling) against a running daemon with
+real captured entries: wheel-down/wheel-up on the history table
+correctly moved the selected-row highlight (confirmed via ANSI-aware
+capture, not just "no crash" - the actual background-color-highlighted
+row changed), right-click opened the menu with the right actions,
+clicking directly on a specific menu item (computed its expected X
+position from the same label-width logic contextMenuAt uses) correctly
+triggered that exact action - confirmed by clicking "delete" and seeing
+the correct entry's ID in the resulting confirmation prompt - keyboard
+navigation (j) inside the menu moved the highlight correctly, esc and a
+second right-click both dismissed cleanly without side effects, wheel
+events in Repeater's text pane and Detail's viewport caused no crash
+and left vi-mode state intact, an empty rules table's right-click
+correctly no-opped (no crash, no menu), and adding a real rule then
+right-clicking it and clicking "disable" correctly toggled it off
+(confirmed via the rendered checkmark disappearing).
diff --git a/README.md b/README.md
index dfa9f87..13e9429 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,10 @@ list of what's deliberately not implemented (and why), see
read-only response views), standard vi navigation (`j`/`k`, `g`/`G`,
`ctrl+u`/`ctrl+d`) already works - that's the underlying TUI
library's default, not something layered on top.
+- **Mouse support**: wheel scroll anywhere there's something to
+ scroll, right-click for a context menu on the history/rules/scope
+ lists. Coexists with tmux's own mouse mode the same way any other
+ mouse-aware terminal app does. See [Mouse](#mouse) below.
## Install / build
@@ -225,6 +229,9 @@ below is enough to get going.
| `s` | target scope (what gets recorded) |
| `q` | quit |
+Also mouse-driven - wheel to scroll, right-click a row for a context
+menu of the same actions. See [Mouse](#mouse) below.
+
### Detail view
`tab` switches request/response, `p` toggles pretty-printed JSON on the
@@ -417,6 +424,42 @@ Repeater and Intruder always record regardless of scope - a request you
deliberately resend or fuzz is something you clearly want to see the
result of, not noise scope exists to cut.
+### Mouse
+
+This is a real terminal application (any terminal, not just tmux - the
+name is a naming convention, not a runtime dependency) with genuine
+mouse support, not just a keyboard-only TUI:
+
+- **Wheel** scrolls whatever's focused - the history/rules/scope
+ tables, a Detail/Comparer/Decoder-output pane, or a Repeater/
+ Intruder text editor.
+- **Right-click** a row in the history, rules, or scope list opens a
+ context menu of the same actions the keyboard shortcuts already do
+ (view/repeater/intruder/flag/delete for history, edit/enable-disable/
+ delete for rules and scope). Click an item, or navigate with
+ `j`/`k`/arrows and `enter`; `esc` or right-clicking again dismisses
+ it without doing anything.
+
+One honest limitation, not an oversight: the menu always acts on the
+row that's currently *selected*, not necessarily the exact row your
+cursor happens to be over when you right-click. The underlying table
+widget doesn't expose its own scroll position, so there's no reliable
+way to map a click's screen coordinates back to a specific row without
+reaching into that library's private internals - which this
+deliberately doesn't do, rather than risk silently selecting the wrong
+one. Scroll or navigate to the row you want first; wheel scroll and
+keyboard navigation both work exactly as you'd expect.
+
+If you're running inside tmux, its own mouse mode needs to be on too
+(`set -g mouse on` in `.tmux.conf`) for mouse events to reach mitmux at
+all - tmux forwards them to whichever pane is focused once that's set,
+the same way it does for nvim or any other mouse-aware terminal app,
+nothing mitmux-specific to configure. If you ever want to select and
+copy on-screen text with the mouse instead (which an app with mouse
+mode on normally intercepts), most terminal emulators let you hold
+Shift while click-dragging to fall back to the terminal's own native
+selection.
+
## Architecture
`mitmuxd` owns the proxy listener and the SQLite database; `mitmux` is
diff --git a/cmd/mitmux/contextmenu.go b/cmd/mitmux/contextmenu.go
new file mode 100644
index 0000000..48df0f3
--- /dev/null
+++ b/cmd/mitmux/contextmenu.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// contextMenuItem is one entry in a right-click context menu.
+type contextMenuItem struct {
+ label string
+ action func() tea.Cmd
+}
+
+// A right-click menu is rendered as a horizontal strip taking over the
+// status/help line, the same "replace the bottom of the screen" pattern
+// confirmPrompt and the export/import prompts already use, rather than
+// a floating popup positioned at the click. That's a deliberate
+// simplification: lipgloss/bubbletea have no compositor for splicing an
+// ANSI-styled overlay into an arbitrary screen position, and building
+// one just for this would be a lot of new, fragile machinery for a
+// nice-to-have. Navigable by j/k/arrows + enter, or by clicking an item
+// directly (each item's on-screen width is fully known here, unlike a
+// table row's - see the table-click limitation noted where mouse
+// handling is wired into Update()), esc or right-click again to
+// dismiss.
+func (m *model) openContextMenu(items []contextMenuItem) {
+ m.contextMenu = items
+ m.contextMenuIndex = 0
+}
+
+func (m *model) closeContextMenu() {
+ m.contextMenu = nil
+ m.contextMenuIndex = 0
+}
+
+// contextMenuAt returns the item index at screen column x within the
+// menu's rendered strip, or -1 if x falls outside every item - used to
+// resolve a mouse click to a specific item.
+func (m *model) contextMenuAt(x int) int {
+ col := 0
+ for i, item := range m.contextMenu {
+ w := lipgloss.Width(contextMenuLabel(item.label))
+ if x >= col && x < col+w {
+ return i
+ }
+ col += w
+ }
+ return -1
+}
+
+func contextMenuLabel(label string) string {
+ return " " + label + " "
+}
+
+func (m *model) contextMenuView() string {
+ var b strings.Builder
+ for i, item := range m.contextMenu {
+ label := contextMenuLabel(item.label)
+ if i == m.contextMenuIndex {
+ b.WriteString(tabActive.Render(label))
+ } else {
+ b.WriteString(tabInactive.Render(label))
+ }
+ }
+ return b.String()
+}
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index c9563fe..f248ef0 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -67,7 +67,18 @@ func main() {
defer subClose()
m := newModel(client, subCh, path)
- p := tea.NewProgram(m, tea.WithAltScreen())
+ // WithMouseCellMotion enables click and wheel events (SGR mouse
+ // mode, the same protocol nvim and most modern TUI apps use) -
+ // see contextmenu.go and the mouse handling in Update() for what's
+ // actually wired up. Coexists with tmux's own mouse mode the same
+ // way it does for any other terminal app: tmux forwards these
+ // events to the focused pane's application when tmux's own `set -g
+ // mouse on` is active, nothing mitmux-specific required. The
+ // standard terminal-wide workaround for selecting on-screen text
+ // with the mouse while an app has mouse mode on (holding Shift
+ // while click-dragging, in most terminal emulators) still works -
+ // mitmux doesn't intercept or need to know about that at all.
+ p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "mitmux: %v\n", err)
os.Exit(1)
@@ -230,6 +241,13 @@ type model struct {
confirmPrompt string
confirmYes func() tea.Cmd
+ // contextMenu, when non-empty, is a right-click menu taking over
+ // the status line - see contextmenu.go. Checked globally, ahead of
+ // every mode's own key handling, the same priority confirmPrompt
+ // gets.
+ contextMenu []contextMenuItem
+ contextMenuIndex int
+
// exportEditing mirrors the grep-match/extract edit-buffer pattern:
// a modal textinput overlay for the destination path, enter writes
// and confirms, esc cancels. Used from two places sharing the same
@@ -897,6 +915,9 @@ func (m *model) Init() tea.Cmd {
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
+ case tea.MouseMsg:
+ return m.handleMouse(msg)
+
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
m.ready = true
@@ -1143,6 +1164,29 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.waitForIntrudeResult
case tea.KeyMsg:
+ if len(m.contextMenu) > 0 {
+ switch msg.String() {
+ case "esc":
+ m.closeContextMenu()
+ return m, nil
+ case "enter", " ":
+ item := m.contextMenu[m.contextMenuIndex]
+ m.closeContextMenu()
+ if item.action != nil {
+ return m, item.action()
+ }
+ return m, nil
+ case "up", "k", "left", "h":
+ m.contextMenuIndex = (m.contextMenuIndex - 1 + len(m.contextMenu)) % len(m.contextMenu)
+ return m, nil
+ case "down", "j", "right", "l", "tab":
+ m.contextMenuIndex = (m.contextMenuIndex + 1) % len(m.contextMenu)
+ return m, nil
+ case "ctrl+c":
+ return m, tea.Quit
+ }
+ return m, nil
+ }
switch m.mode {
case viewList:
if m.confirmPrompt != "" {
@@ -1865,6 +1909,9 @@ func (m *model) View() string {
default:
body = m.listView()
}
+ if len(m.contextMenu) > 0 {
+ return body + "\n" + m.contextMenuView()
+ }
return body + "\n" + m.statusBar()
}
@@ -1905,6 +1952,14 @@ func (m *model) helpView() string {
"? this screen (any key returns)",
"ctrl+c quit",
)
+ section("Mouse",
+ "wheel scroll whatever's focused (table, text, or response pane)",
+ "right-click context menu for the current selection (history/rules/scope lists)",
+ " - always the *selected* row's actions, not whatever's",
+ " literally under the cursor; click a menu item, or",
+ " j/k/arrows + enter, esc or right-click again to dismiss",
+ " (needs tmux's own 'set -g mouse on' if running inside tmux)",
+ )
section("History list",
"↑/↓ or j/k navigate (also g/G top/bottom, ctrl+u/d half-page)",
"enter view request/response detail",
diff --git a/cmd/mitmux/mouse.go b/cmd/mitmux/mouse.go
new file mode 100644
index 0000000..102dc4f
--- /dev/null
+++ b/cmd/mitmux/mouse.go
@@ -0,0 +1,233 @@
+package main
+
+import (
+ "fmt"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+// isWheel reports whether msg is a wheel event. tea.MouseMsg is a
+// distinct named type from tea.MouseEvent (type MouseMsg MouseEvent),
+// so MouseEvent's own IsWheel method isn't in MouseMsg's method set
+// despite the identical underlying struct - converting once here is
+// simpler than repeating the conversion at every call site.
+func isWheel(msg tea.MouseMsg) bool {
+ return tea.MouseEvent(msg).IsWheel()
+}
+
+// handleMouse dispatches a tea.MouseMsg by mode, the mouse counterpart
+// to Update()'s tea.KeyMsg dispatch. Deliberately scoped to what's
+// reliably achievable rather than everything a GUI might do: wheel
+// scroll works everywhere there's something to scroll, and right-click
+// opens a context menu for the *currently selected* row in every
+// list-based view - not the row directly under the cursor, because
+// bubbles/table exposes no way to learn its own scroll offset (no
+// YOffset accessor, and the visible window is computed from unexported
+// fields - confirmed by reading its source rather than guessing), so
+// mapping a click's Y coordinate to a specific row can't be done
+// without reflection into private library state. Left-click-to-select-
+// a-different-row and click-to-switch-pane-focus (Repeater/Intruder)
+// are deferred for the same reason: getting either subtly wrong (silently
+// selecting the wrong row) is worse than not having them yet.
+func (m *model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ if len(m.contextMenu) > 0 {
+ switch {
+ case msg.Action != tea.MouseActionPress:
+ return m, nil
+ case msg.Button == tea.MouseButtonRight:
+ m.closeContextMenu()
+ return m, nil
+ case msg.Button == tea.MouseButtonLeft:
+ if i := m.contextMenuAt(msg.X); i >= 0 {
+ item := m.contextMenu[i]
+ m.closeContextMenu()
+ if item.action != nil {
+ return m, item.action()
+ }
+ }
+ return m, nil
+ }
+ return m, nil
+ }
+
+ switch m.mode {
+ case viewList:
+ return m.handleListMouse(msg)
+ case viewDetail:
+ if isWheel(msg) {
+ var cmd tea.Cmd
+ m.viewport, cmd = m.viewport.Update(msg)
+ return m, cmd
+ }
+ case viewRepeater:
+ return m.handleRepeaterMouse(msg)
+ case viewIntruder:
+ return m.handleIntruderMouse(msg)
+ case viewCompare:
+ if isWheel(msg) {
+ var cmd tea.Cmd
+ m.compareViewport, cmd = m.compareViewport.Update(msg)
+ return m, cmd
+ }
+ case viewDecoder:
+ if isWheel(msg) {
+ var cmd tea.Cmd
+ m.decoderOutput, cmd = m.decoderOutput.Update(msg)
+ return m, cmd
+ }
+ case viewRules:
+ return m.handleRulesMouse(msg)
+ case viewScope:
+ return m.handleScopeMouse(msg)
+ }
+ return m, nil
+}
+
+func (m *model) handleListMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ if m.searching || m.exportEditing || m.importEditing || m.confirmPrompt != "" {
+ return m, nil // an active modal text input owns input right now
+ }
+ switch {
+ case msg.Button == tea.MouseButtonWheelUp:
+ m.table.MoveUp(3)
+ case msg.Button == tea.MouseButtonWheelDown:
+ m.table.MoveDown(3)
+ case msg.Button == tea.MouseButtonRight && msg.Action == tea.MouseActionPress:
+ row := m.table.Cursor()
+ if row < 0 || row >= len(m.entries) {
+ return m, nil
+ }
+ id := m.entries[row].ID
+ flagLabel := "flag"
+ if m.entries[row].Flagged {
+ flagLabel = "unflag"
+ }
+ m.openContextMenu([]contextMenuItem{
+ {label: "view", action: func() tea.Cmd {
+ m.statusMsg = ""
+ return m.loadDetail(id, "")
+ }},
+ {label: "repeater", action: func() tea.Cmd {
+ m.statusMsg = ""
+ return m.loadDetail(id, "repeater")
+ }},
+ {label: "intruder", action: func() tea.Cmd {
+ m.statusMsg = ""
+ return m.loadDetail(id, "intruder")
+ }},
+ {label: flagLabel, action: func() tea.Cmd {
+ m.entries[row].Flagged = !m.entries[row].Flagged
+ setTableRows(&m.table, rowsFor(m.entries))
+ return m.setFlagged(id, m.entries[row].Flagged)
+ }},
+ {label: "delete", action: func() tea.Cmd {
+ m.confirmPrompt = fmt.Sprintf("delete #%d? y/n", id)
+ m.confirmYes = func() tea.Cmd { return m.deleteEntry(id) }
+ return nil
+ }},
+ })
+ }
+ return m, nil
+}
+
+func (m *model) handleRepeaterMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ t := m.activeRepeaterTab()
+ if t == nil || !isWheel(msg) {
+ return m, nil
+ }
+ up := msg.Button == tea.MouseButtonWheelUp
+ if t.focus == focusRequest {
+ if up {
+ t.reqArea.ScrollUp(3)
+ } else {
+ t.reqArea.ScrollDown(3)
+ }
+ return m, nil
+ }
+ var cmd tea.Cmd
+ t.respView, cmd = t.respView.Update(msg)
+ return m, cmd
+}
+
+func (m *model) handleIntruderMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ if !isWheel(msg) {
+ return m, nil
+ }
+ up := msg.Button == tea.MouseButtonWheelUp
+ switch m.intruderFocus {
+ case focusTemplate:
+ if up {
+ m.intruderTemplate.ScrollUp(3)
+ } else {
+ m.intruderTemplate.ScrollDown(3)
+ }
+ case focusPayloads:
+ if up {
+ m.intruderPayloads.ScrollUp(3)
+ } else {
+ m.intruderPayloads.ScrollDown(3)
+ }
+ case focusResults:
+ if up {
+ m.intruderResults.MoveUp(3)
+ } else {
+ m.intruderResults.MoveDown(3)
+ }
+ }
+ return m, nil
+}
+
+func (m *model) handleRulesMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ if m.ruleForm {
+ return m, nil
+ }
+ switch {
+ case msg.Button == tea.MouseButtonWheelUp:
+ m.rulesTable.MoveUp(3)
+ case msg.Button == tea.MouseButtonWheelDown:
+ m.rulesTable.MoveDown(3)
+ case msg.Button == tea.MouseButtonRight && msg.Action == tea.MouseActionPress:
+ row := m.rulesTable.Cursor()
+ if row < 0 || row >= len(m.ruleRows) {
+ return m, nil
+ }
+ sel := m.ruleRows[row]
+ enableLabel := "enable"
+ if sel.Enabled {
+ enableLabel = "disable"
+ }
+ m.openContextMenu([]contextMenuItem{
+ {label: "edit", action: func() tea.Cmd { m.enterRuleForm(&sel); return nil }},
+ {label: enableLabel, action: func() tea.Cmd { return m.toggleSelectedRule() }},
+ {label: "delete", action: func() tea.Cmd { return m.deleteSelectedRule() }},
+ })
+ }
+ return m, nil
+}
+
+func (m *model) handleScopeMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
+ if m.scopeForm {
+ return m, nil
+ }
+ switch {
+ case msg.Button == tea.MouseButtonWheelUp:
+ m.scopeTable.MoveUp(3)
+ case msg.Button == tea.MouseButtonWheelDown:
+ m.scopeTable.MoveDown(3)
+ case msg.Button == tea.MouseButtonRight && msg.Action == tea.MouseActionPress:
+ row := m.scopeTable.Cursor()
+ if row < 0 || row >= len(m.scopeRows) {
+ return m, nil
+ }
+ sel := m.scopeRows[row]
+ enableLabel := "enable"
+ if sel.Enabled {
+ enableLabel = "disable"
+ }
+ m.openContextMenu([]contextMenuItem{
+ {label: enableLabel, action: func() tea.Cmd { return m.toggleSelectedScopeRule() }},
+ {label: "delete", action: func() tea.Cmd { return m.deleteSelectedScopeRule() }},
+ })
+ }
+ return m, nil
+}
diff --git a/cmd/mitmux/vimode.go b/cmd/mitmux/vimode.go
index 5da82a3..1af1fb9 100644
--- a/cmd/mitmux/vimode.go
+++ b/cmd/mitmux/vimode.go
@@ -65,6 +65,25 @@ func (v *viTextarea) Focus() tea.Cmd {
// itself.
func (v *viTextarea) InsertRune(r rune) { v.ta.InsertRune(r) }
+// ScrollUp/ScrollDown move the cursor n lines up/down - used for mouse
+// wheel scrolling. bubbles/textarea has no native mouse handling at all
+// (confirmed in its source: no case for tea.MouseMsg anywhere), so
+// there's nothing to forward a wheel event to; feeding it as n
+// up/down keypresses reuses the same tested movement path vi's own
+// h/j/k/l already go through rather than reaching into textarea's
+// internal line offset directly.
+func (v *viTextarea) ScrollUp(n int) {
+ for i := 0; i < n; i++ {
+ v.feed("up")
+ }
+}
+
+func (v *viTextarea) ScrollDown(n int) {
+ for i := 0; i < n; i++ {
+ v.feed("down")
+ }
+}
+
// Update processes one message. Callers should check Mode()=="normal"
// before treating "esc" as "leave this view" - in insert mode, esc
// belongs to viTextarea (it drops back to normal mode without