srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-05-18 23:30:00 +0200
committersrdusr <[email protected]>2026-05-18 23:30:00 +0200
commit4fa511ac5f2aa422456c49610a2cf29dfee46dfd (patch)
treec033651e2fcd3a3f1131998f86a5118bcfc56034 /cmd
parent1707cf827592bc3de7ea9c2805b7adea49b3cbc5 (diff)
downloadmitmux-4fa511ac5f2aa422456c49610a2cf29dfee46dfd.tar.gz
mitmux-4fa511ac5f2aa422456c49610a2cf29dfee46dfd.zip
History deletion: delete one entry (x) or clear everything (X)
Store had full CRUD for match-and-replace rules but no way to delete or prune history at all - it only ever grew, with no way to remove an accidental capture or start a new engagement clean short of manually deleting the DB file outside the tool entirely. internal/store: DeleteEntry(id) removes one history row and its history_fts search index row in a transaction. ClearHistory() empties both tables entirely; rules are untouched. internal/ipc: new "delete_entry" and "clear_history" request types, Client.DeleteEntry/ ClearHistory methods. TUI: 'x' deletes the selected history entry, 'X' clears the whole database. Both gated behind a y/n confirmation - a small reusable confirmPrompt/confirmYes model state, checked first in the history list's key handling, so any key other than y/Y safely cancels rather than falling through to whatever that key normally does elsewhere (this also means ctrl+c during a pending confirmation cancels the prompt rather than quitting - a deliberate fail-safe, not an oversight: quick to dismiss, and a second ctrl+c then quits normally). 'X' is explicitly NOT scoped to an active search filter - it always clears the true total (read from daemon status, not len(m.entries), which would understate the count under a filter and make the confirmation prompt itself misleading about what's about to happen). Verified live in tmux against a running daemon with real captured entries: 'x' shows "delete #N? y/n", 'n' cancels with the entry untouched, 'y' deletes it and the list/count both refresh correctly; 'X' shows "clear all N history entries (not just this view)? y/n" with the true count, 'y' empties the database (confirmed via direct SQLite query: both history and history_fts at 0 rows afterward) and the TUI correctly shows "history (0)" / "0 requests"; 'x'/'X' on an empty list correctly no-op without crashing. go build/vet/gofmt/test/mod tidy all clean.
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/main.go92
1 files changed, 89 insertions, 3 deletions
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index 83e59cd..dbc919d 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -203,6 +203,14 @@ type model struct {
decoderInput viTextarea
decoderOutput viewport.Model
+ // confirmPrompt, when non-empty, takes over the status line and
+ // every keypress until resolved: 'y' runs confirmYes, anything else
+ // cancels. Used to gate destructive actions (deleting a history
+ // entry, clearing all of it) behind an explicit second keypress
+ // rather than a single accidental one.
+ confirmPrompt string
+ confirmYes func() tea.Cmd
+
statusMsg string
width int
height int
@@ -352,6 +360,27 @@ func (m *model) setFlagged(id int64, flagged bool) tea.Cmd {
}
}
+type entryDeletedMsg struct {
+ id int64
+ err error
+}
+
+func (m *model) deleteEntry(id int64) tea.Cmd {
+ return func() tea.Msg {
+ return entryDeletedMsg{id: id, err: m.client.DeleteEntry(id)}
+ }
+}
+
+type historyClearedMsg struct {
+ err error
+}
+
+func (m *model) clearHistory() tea.Cmd {
+ return func() tea.Msg {
+ return historyClearedMsg{err: m.client.ClearHistory()}
+ }
+}
+
func (m *model) loadList() tea.Msg {
var entries []store.Summary
var err error
@@ -750,6 +779,24 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
+ case entryDeletedMsg:
+ if msg.err != nil {
+ m.statusMsg = "delete error: " + msg.err.Error()
+ return m, nil
+ }
+ m.statusMsg = fmt.Sprintf("deleted #%d", msg.id)
+ return m, tea.Batch(m.loadList, m.loadStatus)
+
+ case historyClearedMsg:
+ if msg.err != nil {
+ m.statusMsg = "clear error: " + msg.err.Error()
+ return m, nil
+ }
+ m.compareBaseID = 0
+ m.pendingNew = 0
+ m.statusMsg = "history cleared"
+ return m, tea.Batch(m.loadList, m.loadStatus)
+
case detailLoadedMsg:
if msg.err != nil {
m.statusMsg = "get error: " + msg.err.Error()
@@ -850,6 +897,17 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch m.mode {
case viewList:
+ if m.confirmPrompt != "" {
+ yes := msg.String() == "y" || msg.String() == "Y"
+ action := m.confirmYes
+ m.confirmPrompt = ""
+ m.confirmYes = nil
+ if yes && action != nil {
+ return m, action()
+ }
+ m.statusMsg = "cancelled"
+ return m, nil
+ }
if m.searching {
switch msg.String() {
case "enter":
@@ -902,6 +960,29 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if row := m.table.Cursor(); row >= 0 && row < len(m.entries) {
return m, m.markOrCompare(m.entries[row].ID)
}
+ case "x":
+ if row := m.table.Cursor(); row >= 0 && row < len(m.entries) {
+ id := m.entries[row].ID
+ m.confirmPrompt = fmt.Sprintf("delete #%d? y/n", id)
+ m.confirmYes = func() tea.Cmd { return m.deleteEntry(id) }
+ }
+ return m, nil
+ case "X":
+ // Always clears the whole database, never just the
+ // current filtered view - m.entries under an active
+ // search query would understate the real count and make
+ // the prompt itself misleading about what's about to
+ // happen.
+ count := "all"
+ if m.daemonStatus != nil {
+ count = fmt.Sprintf("all %d", m.daemonStatus.HistoryCount)
+ }
+ if m.daemonStatus != nil && m.daemonStatus.HistoryCount == 0 {
+ return m, nil
+ }
+ m.confirmPrompt = fmt.Sprintf("clear %s history entries (not just this view)? y/n", count)
+ m.confirmYes = func() tea.Cmd { return m.clearHistory() }
+ return m, nil
case "d":
m.mode = viewDecoder
m.statusMsg = ""
@@ -1424,6 +1505,8 @@ func (m *model) helpView() string {
"i open in Intruder",
"f toggle flag (★ mark this, revisit later)",
"c mark for comparison, then press c on another entry to diff",
+ "x delete the selected entry (asks to confirm)",
+ "X clear ALL history, not just the current filter (asks to confirm)",
"d decoder (URL/Base64/Hex/HTML encode/decode)",
"/ search: plain text, host:value, AND/OR/NOT,",
" status:404 / status:4xx / status:>=400,",
@@ -1506,13 +1589,16 @@ func (m *model) listView() string {
}
b.WriteString(m.table.View())
b.WriteString("\n")
- if m.statusMsg != "" {
+ if m.confirmPrompt != "" {
+ b.WriteString(statusStyle.Render(m.confirmPrompt))
+ b.WriteString("\n")
+ } else if m.statusMsg != "" {
b.WriteString(statusStyle.Render(sanitizeLine(m.statusMsg)))
b.WriteString("\n")
}
- help := "enter view · r/i/c/d tools · f flag · / search · m rules · ? help · q quit"
+ help := "enter view · r/i/c/d tools · f flag · x delete · X clear all · / search · m rules · ? help · q quit"
if m.query != "" {
- help = "enter view · r/i/c/d tools · f flag · / search · esc clear filter · ? help · q quit"
+ help = "enter view · r/i/c/d tools · f flag · x delete · X clear all · / search · esc clear filter · ? help · q quit"
}
b.WriteString(helpStyle.Render(help))
return b.String()