diff options
| author | srdusr <[email protected]> | 2026-06-09 14:35:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-06-09 14:35:00 +0200 |
| commit | 9028f8175bda63d6a85e5a2dee2b4039020317a4 (patch) | |
| tree | 6046c5c1dc2271b2acd7a0a8bd2fa72ea2d42fb5 | |
| parent | f3558f18ecabba983aba001468b06394471e6d8b (diff) | |
| download | mitmux-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.md | 75 | ||||
| -rw-r--r-- | README.md | 43 | ||||
| -rw-r--r-- | cmd/mitmux/contextmenu.go | 68 | ||||
| -rw-r--r-- | cmd/mitmux/main.go | 57 | ||||
| -rw-r--r-- | cmd/mitmux/mouse.go | 233 | ||||
| -rw-r--r-- | cmd/mitmux/vimode.go | 19 |
6 files changed, 494 insertions, 1 deletions
@@ -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). @@ -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 |