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) case viewClientCerts: return m.handleClientCertMouse(msg) case viewWebSocket: if isWheel(msg) { var cmd tea.Cmd if m.wsShowingDetail { m.wsDetailViewport, cmd = m.wsDetailViewport.Update(msg) } else { switch msg.Button { case tea.MouseButtonWheelUp: m.wsTable.MoveUp(3) case tea.MouseButtonWheelDown: m.wsTable.MoveDown(3) } } return m, cmd } case viewTags: if isWheel(msg) { var cmd tea.Cmd if m.tagsShowingDetail { m.tagsDetailViewport, cmd = m.tagsDetailViewport.Update(msg) } else { switch msg.Button { case tea.MouseButtonWheelUp: m.tagsTable.MoveUp(3) case tea.MouseButtonWheelDown: m.tagsTable.MoveDown(3) } } return m, cmd } } 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 } func (m *model) handleClientCertMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { if m.clientCertForm { return m, nil } switch { case msg.Button == tea.MouseButtonWheelUp: m.clientCertTable.MoveUp(3) case msg.Button == tea.MouseButtonWheelDown: m.clientCertTable.MoveDown(3) case msg.Button == tea.MouseButtonRight && msg.Action == tea.MouseActionPress: row := m.clientCertTable.Cursor() if row < 0 || row >= len(m.clientCertRows) { return m, nil } sel := m.clientCertRows[row] enableLabel := "enable" if sel.Enabled { enableLabel = "disable" } m.openContextMenu([]contextMenuItem{ {label: enableLabel, action: func() tea.Cmd { return m.toggleSelectedClientCert() }}, {label: "delete", action: func() tea.Cmd { return m.deleteSelectedClientCert() }}, }) } return m, nil }