mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 01:32:33 +02:00
172a77e13b
[37m- SQL query mode uses read-only SQLite connection with PRAGMA query_only=ON[0m [37m- Lua sandbox removes dofile/loadfile/load after OpenBase to block file access[0m [37m- Plugin manager sorts by priority once at load time; GetPlugins is a plain copy[0m [37m- Proxy appends [body truncated] marker when body hits size limit[0m [37m- App startup exits with os.Exit(1) on DB open failure[0m [37m- tickCmd uses tea.Tick instead of time.Sleep in a goroutine[0m [37m- ErrMsg with non-nil error shows notification then quits[0m [37m- DB stores path for use by read-only query connection[0m [37m- WAL journal mode + NORMAL synchronous set in migrate()[0m [37m- config.go uses errors.Is(err, os.ErrNotExist)[0m [37m- main.go uses os.UserHomeDir() and removes racy port pre-check[0m [37m- findings renderer is cached and rebuilt only on width change[0m [37mCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>[0m
286 lines
7.2 KiB
Go
286 lines
7.2 KiB
Go
package app
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"charm.land/bubbles/v2/key"
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/anotherhadi/spilltea/internal/config"
|
|
"github.com/anotherhadi/spilltea/internal/intercept"
|
|
"github.com/anotherhadi/spilltea/internal/keys"
|
|
"github.com/anotherhadi/spilltea/internal/plugins"
|
|
proxyPkg "github.com/anotherhadi/spilltea/internal/proxy"
|
|
copyUI "github.com/anotherhadi/spilltea/internal/ui/components/copy"
|
|
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
|
notificationsUI "github.com/anotherhadi/spilltea/internal/ui/components/notifications"
|
|
diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
|
|
findingsUI "github.com/anotherhadi/spilltea/internal/ui/findings"
|
|
historyUI "github.com/anotherhadi/spilltea/internal/ui/history"
|
|
interceptUI "github.com/anotherhadi/spilltea/internal/ui/intercept"
|
|
replayUI "github.com/anotherhadi/spilltea/internal/ui/replay"
|
|
)
|
|
|
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
// Broker messages must always re-register their watchers
|
|
switch msg := msg.(type) {
|
|
case notificationsUI.NotificationMsg:
|
|
var cmd tea.Cmd
|
|
m.notifications, cmd = m.notifications.Update(msg)
|
|
return m, cmd
|
|
case notificationsUI.DismissMsg:
|
|
var cmd tea.Cmd
|
|
m.notifications, cmd = m.notifications.Update(msg)
|
|
return m, cmd
|
|
case intercept.RequestArrivedMsg:
|
|
updated, cmd := m.intercept.Update(msg)
|
|
m.intercept = updated.(interceptUI.Model)
|
|
return m, tea.Batch(cmd, intercept.WaitForRequest(m.broker))
|
|
case intercept.ResponseArrivedMsg:
|
|
updated, cmd := m.intercept.Update(msg)
|
|
m.intercept = updated.(interceptUI.Model)
|
|
return m, tea.Batch(cmd, intercept.WaitForResponse(m.broker))
|
|
|
|
case plugins.PluginNotifMsg:
|
|
cmd := plugins.WaitForNotif(m.pluginManager)
|
|
kind := notificationsUI.KindInfo
|
|
switch msg.Kind {
|
|
case "success":
|
|
kind = notificationsUI.KindSuccess
|
|
case "warning":
|
|
kind = notificationsUI.KindWarning
|
|
case "error":
|
|
kind = notificationsUI.KindError
|
|
}
|
|
notifCmd := func() tea.Msg {
|
|
return notificationsUI.NotificationMsg{
|
|
Title: msg.Title,
|
|
Body: msg.Body,
|
|
Kind: kind,
|
|
}
|
|
}
|
|
return m, tea.Batch(cmd, notifCmd)
|
|
|
|
case plugins.PluginQuitMsg:
|
|
log.Printf("plugin quit: %s", msg.Reason)
|
|
m.pluginManager.RunOnQuit()
|
|
return m, tea.Quit
|
|
}
|
|
|
|
if m.copyAs.IsOpen() {
|
|
if ws, ok := msg.(tea.WindowSizeMsg); ok {
|
|
m.width = ws.Width
|
|
m.height = ws.Height
|
|
m.copyAs.SetSize(ws.Width, ws.Height)
|
|
m.resizeChildren()
|
|
return m, nil
|
|
}
|
|
var cmd tea.Cmd
|
|
m.copyAs, cmd = m.copyAs.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
if m.copy.IsOpen() {
|
|
if ws, ok := msg.(tea.WindowSizeMsg); ok {
|
|
m.width = ws.Width
|
|
m.height = ws.Height
|
|
m.copy.SetSize(ws.Width, ws.Height)
|
|
m.resizeChildren()
|
|
return m, nil
|
|
}
|
|
var cmd tea.Cmd
|
|
m.copy, cmd = m.copy.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
m.resizeChildren()
|
|
|
|
case proxyPkg.ErrMsg:
|
|
if msg.Err != nil {
|
|
log.Printf("proxy error: %v", msg.Err)
|
|
return m, tea.Batch(
|
|
func() tea.Msg {
|
|
return notificationsUI.NotificationMsg{
|
|
Title: "Proxy Error",
|
|
Body: msg.Err.Error(),
|
|
Kind: notificationsUI.KindError,
|
|
}
|
|
},
|
|
tea.Quit,
|
|
)
|
|
}
|
|
return m, nil
|
|
|
|
case tickMsg:
|
|
var cmds []tea.Cmd
|
|
cmds = append(cmds, tickCmd())
|
|
if m.page == pageHistory {
|
|
cmds = append(cmds, m.history.RefreshCmd())
|
|
}
|
|
cmds = append(cmds, findingsUI.RefreshCmd(m.database))
|
|
return m, tea.Batch(cmds...)
|
|
|
|
case findingsUI.FindingsLoadedMsg:
|
|
updated, cmd := m.findingsPage.Update(msg)
|
|
m.findingsPage = updated.(findingsUI.Model)
|
|
return m, cmd
|
|
|
|
case replayUI.SendToReplayMsg:
|
|
updated, cmd := m.replay.Update(msg)
|
|
m.replay = updated.(replayUI.Model)
|
|
if config.Global.Replay.SwitchToPageOnSend {
|
|
m.page = pageReplay
|
|
m.resizeChildren()
|
|
} else {
|
|
return m, tea.Batch(cmd, func() tea.Msg {
|
|
return notificationsUI.NotificationMsg{
|
|
Title: "Replay",
|
|
Body: "Request queued in replay",
|
|
Kind: notificationsUI.KindInfo,
|
|
}
|
|
})
|
|
}
|
|
return m, cmd
|
|
|
|
case diffUI.SendToDiffMsg:
|
|
updated, cmd := m.diff.Update(msg)
|
|
m.diff = updated.(diffUI.Model)
|
|
return m, cmd
|
|
|
|
case diffUI.DiffReadyMsg:
|
|
m.page = pageDiff
|
|
m.resizeChildren()
|
|
return m, nil
|
|
|
|
case historyUI.EntriesLoadedMsg:
|
|
updated, cmd := m.history.Update(msg)
|
|
m.history = updated.(historyUI.Model)
|
|
return m, cmd
|
|
|
|
case tea.KeyPressMsg:
|
|
// ctrl+c always quits, even when a textarea is focused.
|
|
if msg.String() == "ctrl+c" {
|
|
m.pluginManager.RunOnQuit()
|
|
return m, tea.Quit
|
|
}
|
|
if key.Matches(msg, keys.Keys.Global.Quit) && !m.activeIsEditing() {
|
|
m.pluginManager.RunOnQuit()
|
|
return m, tea.Quit
|
|
}
|
|
|
|
if key.Matches(msg, keys.Keys.Global.OpenLogs) {
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
editor = "vi"
|
|
}
|
|
logPath := filepath.Join(filepath.Dir(m.projectPath), "logs.log")
|
|
return m, tea.ExecProcess(exec.Command(editor, logPath), nil)
|
|
}
|
|
|
|
if !m.activeIsEditing() {
|
|
switch {
|
|
case key.Matches(msg, keys.Keys.Global.CopyAs):
|
|
var raw, scheme string
|
|
switch m.page {
|
|
case pageDiff:
|
|
raw = m.diff.CurrentRaw()
|
|
scheme = "https"
|
|
case pageIntercept:
|
|
raw = m.intercept.CurrentRaw()
|
|
scheme = m.intercept.CurrentScheme()
|
|
case pageHistory:
|
|
raw = m.history.CurrentRaw()
|
|
scheme = m.history.CurrentScheme()
|
|
case pageReplay:
|
|
raw = m.replay.CurrentRaw()
|
|
scheme = m.replay.CurrentScheme()
|
|
}
|
|
if raw != "" {
|
|
m.copyAs.SetSize(m.width, m.height)
|
|
m.copyAs.Open(copyasUI.OpenMsg{RawRequest: raw, Scheme: scheme})
|
|
}
|
|
return m, nil
|
|
|
|
case key.Matches(msg, keys.Keys.Global.Copy):
|
|
var raw, scheme string
|
|
switch m.page {
|
|
case pageIntercept:
|
|
raw = m.intercept.CurrentRaw()
|
|
scheme = m.intercept.CurrentScheme()
|
|
case pageDiff:
|
|
raw = m.diff.CurrentRaw()
|
|
scheme = "https"
|
|
case pageHistory:
|
|
raw = m.history.CurrentRaw()
|
|
scheme = m.history.CurrentScheme()
|
|
case pageReplay:
|
|
raw = m.replay.CurrentRaw()
|
|
scheme = m.replay.CurrentScheme()
|
|
}
|
|
if raw != "" {
|
|
m.copy.SetSize(m.width, m.height)
|
|
m.copy.Open(copyUI.OpenMsg{RawRequest: raw, Scheme: scheme})
|
|
}
|
|
return m, nil
|
|
|
|
case key.Matches(msg, keys.Keys.Global.ToggleSidebar):
|
|
m.cycleSidebarState()
|
|
m.resizeChildren()
|
|
|
|
default:
|
|
if p, ok := pageShortcuts[msg.String()]; ok {
|
|
prev := m.page
|
|
m.page = p
|
|
if p == pageHistory && prev != pageHistory {
|
|
return m, m.history.RefreshCmd()
|
|
}
|
|
if p == pageFindings {
|
|
return m, findingsUI.RefreshCmd(m.database)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var cmd tea.Cmd
|
|
m, cmd = m.updateActivePage(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m Model) activeIsEditing() bool {
|
|
for _, e := range pageRegistry {
|
|
if e.id == m.page && e.isEditing != nil {
|
|
return e.isEditing(&m)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m Model) updateActivePage(msg tea.Msg) (Model, tea.Cmd) {
|
|
for _, e := range pageRegistry {
|
|
if e.id == m.page && e.update != nil {
|
|
cmd := e.update(&m, msg)
|
|
return m, cmd
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *Model) resizeChildren() {
|
|
sidebarW := m.getSidebarWidth()
|
|
h := m.height
|
|
for _, e := range pageRegistry {
|
|
if e.resize == nil {
|
|
continue
|
|
}
|
|
e.resize(m, m.width-sidebarW, h)
|
|
}
|
|
m.notifications.SetSize(m.width, m.height)
|
|
}
|