mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 01:32:33 +02:00
Fix scroll & copy buttons
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -187,21 +187,22 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch {
|
||||
case key.Matches(msg, keys.Keys.Global.CopyAs):
|
||||
var raw, scheme string
|
||||
var responseFocused bool
|
||||
switch m.page {
|
||||
case pageDiff:
|
||||
raw = m.diff.CurrentRaw()
|
||||
scheme = "https"
|
||||
case pageIntercept:
|
||||
raw = m.intercept.CurrentRaw()
|
||||
scheme = m.intercept.CurrentScheme()
|
||||
responseFocused = m.intercept.IsResponseFocused()
|
||||
case pageHistory:
|
||||
raw = m.history.CurrentRaw()
|
||||
scheme = m.history.CurrentScheme()
|
||||
responseFocused = m.history.IsResponseFocused()
|
||||
case pageReplay:
|
||||
raw = m.replay.CurrentRaw()
|
||||
scheme = m.replay.CurrentScheme()
|
||||
responseFocused = m.replay.IsResponseFocused()
|
||||
}
|
||||
if raw != "" {
|
||||
if raw != "" && !responseFocused {
|
||||
m.copyAs.SetSize(m.width, m.height)
|
||||
m.copyAs.Open(copyasUI.OpenMsg{RawRequest: raw, Scheme: scheme})
|
||||
}
|
||||
@@ -209,23 +210,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case key.Matches(msg, keys.Keys.Global.Copy):
|
||||
var raw, scheme string
|
||||
var responseFocused bool
|
||||
switch m.page {
|
||||
case pageIntercept:
|
||||
raw = m.intercept.CurrentRaw()
|
||||
scheme = m.intercept.CurrentScheme()
|
||||
case pageDiff:
|
||||
raw = m.diff.CurrentRaw()
|
||||
scheme = "https"
|
||||
responseFocused = m.intercept.IsResponseFocused()
|
||||
case pageHistory:
|
||||
raw = m.history.CurrentRaw()
|
||||
scheme = m.history.CurrentScheme()
|
||||
responseFocused = m.history.IsResponseFocused()
|
||||
case pageReplay:
|
||||
raw = m.replay.CurrentRaw()
|
||||
scheme = m.replay.CurrentScheme()
|
||||
responseFocused = m.replay.IsResponseFocused()
|
||||
}
|
||||
if raw != "" {
|
||||
m.copy.SetSize(m.width, m.height)
|
||||
m.copy.Open(copyUI.OpenMsg{RawRequest: raw, Scheme: scheme})
|
||||
m.copy.Open(copyUI.OpenMsg{RawRequest: raw, Scheme: scheme, ShowURL: !responseFocused})
|
||||
}
|
||||
return m, nil
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ func writeClipboard(text string) {
|
||||
type OpenMsg struct {
|
||||
RawRequest string
|
||||
Scheme string
|
||||
ShowURL bool
|
||||
}
|
||||
|
||||
type copyItem struct {
|
||||
@@ -90,6 +91,17 @@ func (m *Model) Open(msg OpenMsg) {
|
||||
m.rawRequest = msg.RawRequest
|
||||
m.scheme = msg.Scheme
|
||||
m.open = true
|
||||
items := allItems
|
||||
if !msg.ShowURL {
|
||||
filtered := make([]list.Item, 0, len(allItems))
|
||||
for _, it := range allItems {
|
||||
if it.(copyItem).id != "url" {
|
||||
filtered = append(filtered, it)
|
||||
}
|
||||
}
|
||||
items = filtered
|
||||
}
|
||||
m.list.SetItems(items)
|
||||
m.list.ResetFilter()
|
||||
m.list.Select(0)
|
||||
m.list.SetSize(m.popupInnerWidth(), m.listHeight())
|
||||
|
||||
@@ -405,7 +405,7 @@ func (diffKeyMap) ShortHelp() []key.Binding {
|
||||
|
||||
func (m diffKeyMap) FullHelp() [][]key.Binding {
|
||||
g := keys.Keys.Global
|
||||
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Copy, g.CopyAs}
|
||||
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right}
|
||||
all := append(keys.Keys.Diff.Bindings(), pageGlobals...)
|
||||
all = append(all, g.CommonBindings()...)
|
||||
return keys.ChunkByWidth(all, m.width)
|
||||
|
||||
@@ -113,6 +113,14 @@ type FindingsLoadedMsg struct {
|
||||
}
|
||||
|
||||
func (m *Model) refreshBody() {
|
||||
m.refreshBodyScroll(true)
|
||||
}
|
||||
|
||||
func (m *Model) refreshBodyKeepScroll() {
|
||||
m.refreshBodyScroll(false)
|
||||
}
|
||||
|
||||
func (m *Model) refreshBodyScroll(reset bool) {
|
||||
if len(m.findings) == 0 {
|
||||
m.bodyViewport.SetContent("")
|
||||
return
|
||||
@@ -120,7 +128,9 @@ func (m *Model) refreshBody() {
|
||||
f := m.findings[m.cursor]
|
||||
rendered := m.renderMarkdownCached(f.Description, m.bodyViewport.Width())
|
||||
m.bodyViewport.SetContent(rendered)
|
||||
m.bodyViewport.GotoTop()
|
||||
if reset {
|
||||
m.bodyViewport.GotoTop()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) renderMarkdownCached(src string, width int) string {
|
||||
|
||||
@@ -15,6 +15,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
log.Printf("findings load error: %v", msg.Err)
|
||||
return m, nil
|
||||
}
|
||||
var prevID int64
|
||||
if len(m.findings) > 0 && m.cursor < len(m.findings) {
|
||||
prevID = m.findings[m.cursor].ID
|
||||
}
|
||||
m.findings = msg.Findings
|
||||
if m.cursor >= len(m.findings) {
|
||||
m.cursor = max(0, len(m.findings)-1)
|
||||
@@ -26,7 +30,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.pager.SetTotalPages(len(m.findings))
|
||||
}
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
var newID int64
|
||||
if len(m.findings) > 0 && m.cursor < len(m.findings) {
|
||||
newID = m.findings[m.cursor].ID
|
||||
}
|
||||
if newID != prevID {
|
||||
m.refreshBody()
|
||||
} else {
|
||||
m.refreshBodyKeepScroll()
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tea.MouseWheelMsg:
|
||||
|
||||
@@ -60,9 +60,16 @@ func (m Model) CurrentRaw() string {
|
||||
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
||||
return ""
|
||||
}
|
||||
if m.focusedPanel == panelResponse {
|
||||
return m.entries[m.cursor].ResponseRaw
|
||||
}
|
||||
return m.entries[m.cursor].RequestRaw
|
||||
}
|
||||
|
||||
func (m Model) IsResponseFocused() bool {
|
||||
return m.focusedPanel == panelResponse
|
||||
}
|
||||
|
||||
func (m Model) CurrentScheme() string {
|
||||
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
||||
return "https"
|
||||
|
||||
@@ -78,6 +78,10 @@ func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) IsEditing() bool { return m.editing }
|
||||
|
||||
func (m Model) IsResponseFocused() bool {
|
||||
return m.captureResponse && m.focusedPanel == panelResponses
|
||||
}
|
||||
|
||||
func (m Model) CurrentScheme() string {
|
||||
if len(m.queue) == 0 {
|
||||
return "https"
|
||||
|
||||
@@ -34,11 +34,20 @@ type Entry struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
type panel int
|
||||
|
||||
const (
|
||||
panelList panel = iota
|
||||
panelRequest
|
||||
panelResponse
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
entries []Entry
|
||||
cursor int
|
||||
editing bool
|
||||
database *db.DB
|
||||
entries []Entry
|
||||
cursor int
|
||||
editing bool
|
||||
focusedPanel panel
|
||||
database *db.DB
|
||||
|
||||
listViewport viewport.Model
|
||||
requestViewport viewport.Model
|
||||
@@ -68,10 +77,17 @@ func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) IsEditing() bool { return m.editing }
|
||||
|
||||
func (m Model) IsResponseFocused() bool {
|
||||
return m.focusedPanel == panelResponse
|
||||
}
|
||||
|
||||
func (m Model) CurrentRaw() string {
|
||||
if len(m.entries) == 0 || m.cursor >= len(m.entries) {
|
||||
return ""
|
||||
}
|
||||
if m.focusedPanel == panelResponse {
|
||||
return m.entries[m.cursor].ResponseRaw
|
||||
}
|
||||
return m.entries[m.cursor].RequestRaw
|
||||
}
|
||||
|
||||
@@ -183,12 +199,12 @@ type replayKeyMap struct{ width int }
|
||||
func (replayKeyMap) ShortHelp() []key.Binding {
|
||||
g := keys.Keys.Global
|
||||
r := keys.Keys.Replay
|
||||
return []key.Binding{g.Up, g.Down, r.Send, r.Edit, g.Help}
|
||||
return []key.Binding{g.Up, g.Down, g.CycleFocus, r.Send, r.Edit, g.Help}
|
||||
}
|
||||
|
||||
func (m replayKeyMap) FullHelp() [][]key.Binding {
|
||||
g := keys.Keys.Global
|
||||
pageGlobals := []key.Binding{g.Up, g.Down, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Escape, g.Copy, g.CopyAs}
|
||||
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Escape, g.Copy, g.CopyAs}
|
||||
all := append(keys.Keys.Replay.Bindings(), pageGlobals...)
|
||||
all = append(all, g.CommonBindings()...)
|
||||
return keys.ChunkByWidth(all, m.width)
|
||||
|
||||
+102
-14
@@ -1,6 +1,9 @@
|
||||
package replay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"compress/zlib"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -9,8 +12,11 @@ import (
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
@@ -91,14 +97,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.requestViewport.ScrollLeft(6)
|
||||
m.responseViewport.ScrollLeft(6)
|
||||
} else {
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() - 1)
|
||||
m.scrollFocusedViewportVertical(-1)
|
||||
}
|
||||
case tea.MouseWheelDown:
|
||||
if msg.Mod.Contains(tea.ModShift) {
|
||||
m.requestViewport.ScrollRight(6)
|
||||
m.responseViewport.ScrollRight(6)
|
||||
} else {
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() + 1)
|
||||
m.scrollFocusedViewportVertical(1)
|
||||
}
|
||||
case tea.MouseWheelLeft:
|
||||
m.requestViewport.ScrollLeft(6)
|
||||
@@ -124,17 +130,35 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
r := keys.Keys.Replay
|
||||
switch {
|
||||
case key.Matches(msg, g.Up):
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
if m.focusedPanel == panelList {
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
} else {
|
||||
m.scrollFocusedViewportVertical(-1)
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.Down):
|
||||
if m.cursor < len(m.entries)-1 {
|
||||
m.cursor++
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
if m.focusedPanel == panelList {
|
||||
if m.cursor < len(m.entries)-1 {
|
||||
m.cursor++
|
||||
m.refreshListViewport()
|
||||
m.refreshBody()
|
||||
}
|
||||
} else {
|
||||
m.scrollFocusedViewportVertical(1)
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.CycleFocus):
|
||||
switch m.focusedPanel {
|
||||
case panelList:
|
||||
m.focusedPanel = panelRequest
|
||||
case panelRequest:
|
||||
m.focusedPanel = panelResponse
|
||||
default:
|
||||
m.focusedPanel = panelList
|
||||
}
|
||||
|
||||
case key.Matches(msg, r.Send):
|
||||
@@ -166,18 +190,22 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case key.Matches(msg, g.ScrollUp):
|
||||
step := m.responseViewport.Height() / 2
|
||||
vp := m.focusedViewport()
|
||||
step := vp.Height() / 2
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() - step)
|
||||
vp.SetYOffset(vp.YOffset() - step)
|
||||
m.setFocusedViewport(vp)
|
||||
|
||||
case key.Matches(msg, g.ScrollDown):
|
||||
step := m.responseViewport.Height() / 2
|
||||
vp := m.focusedViewport()
|
||||
step := vp.Height() / 2
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.responseViewport.SetYOffset(m.responseViewport.YOffset() + step)
|
||||
vp.SetYOffset(vp.YOffset() + step)
|
||||
m.setFocusedViewport(vp)
|
||||
|
||||
case key.Matches(msg, g.Left):
|
||||
m.requestViewport.ScrollLeft(6)
|
||||
@@ -280,6 +308,29 @@ func (m Model) updateEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// focusedViewport returns the viewport that should receive scroll events.
|
||||
// When the list is focused, scroll targets the request panel.
|
||||
func (m *Model) focusedViewport() viewport.Model {
|
||||
if m.focusedPanel == panelResponse {
|
||||
return m.responseViewport
|
||||
}
|
||||
return m.requestViewport
|
||||
}
|
||||
|
||||
func (m *Model) setFocusedViewport(vp viewport.Model) {
|
||||
if m.focusedPanel == panelResponse {
|
||||
m.responseViewport = vp
|
||||
} else {
|
||||
m.requestViewport = vp
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) scrollFocusedViewportVertical(delta int) {
|
||||
vp := m.focusedViewport()
|
||||
vp.SetYOffset(vp.YOffset() + delta)
|
||||
m.setFocusedViewport(vp)
|
||||
}
|
||||
|
||||
func (m *Model) refreshListViewport() {
|
||||
if m.pager.PerPage > 0 {
|
||||
if len(m.entries) == 0 {
|
||||
@@ -369,6 +420,14 @@ func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
||||
limit := int64(config.Global.App.MaxBodySizeMB) * 1024 * 1024
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, limit))
|
||||
|
||||
if enc := resp.Header.Get("Content-Encoding"); enc != "" {
|
||||
if decoded, decErr := decodeBody(enc, respBody); decErr == nil {
|
||||
respBody = decoded
|
||||
resp.Header.Del("Content-Encoding")
|
||||
resp.Header.Del("Content-Length")
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "%s %d %s\n", resp.Proto, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
for _, line := range util.SortedHeaderLines(resp.Header) {
|
||||
@@ -380,6 +439,35 @@ func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
||||
return sb.String(), resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func decodeBody(encoding string, body []byte) ([]byte, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(encoding)) {
|
||||
case "gzip":
|
||||
r, err := gzip.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
case "br":
|
||||
return io.ReadAll(brotli.NewReader(bytes.NewReader(body)))
|
||||
case "deflate":
|
||||
r, err := zlib.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
case "zstd":
|
||||
r, err := zstd.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
|
||||
}
|
||||
|
||||
func entryToDB(e Entry) db.ReplayEntry {
|
||||
errMsg := ""
|
||||
if e.Err != nil {
|
||||
|
||||
@@ -34,9 +34,9 @@ func (m Model) View() tea.View {
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
panelStyle := s.PanelFocused
|
||||
if m.editing {
|
||||
panelStyle = s.Panel
|
||||
panelStyle := s.Panel
|
||||
if !m.editing && m.focusedPanel == panelList {
|
||||
panelStyle = s.PanelFocused
|
||||
}
|
||||
var dots string
|
||||
if len(m.entries) > 0 {
|
||||
@@ -58,13 +58,20 @@ func (m *Model) renderRequestPanel(w, h int) string {
|
||||
border = s.PanelFocused
|
||||
} else {
|
||||
body = m.requestViewport.View()
|
||||
if m.focusedPanel == panelRequest {
|
||||
border = s.PanelFocused
|
||||
}
|
||||
}
|
||||
return style.RenderWithTitle(border, icons.I.Request+"Request", body, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderResponsePanel(w, h int) string {
|
||||
s := style.S
|
||||
return style.RenderWithTitle(s.Panel, icons.I.Response+"Response", m.responseViewport.View(), w, h)
|
||||
border := s.Panel
|
||||
if !m.editing && m.focusedPanel == panelResponse {
|
||||
border = s.PanelFocused
|
||||
}
|
||||
return style.RenderWithTitle(border, icons.I.Response+"Response", m.responseViewport.View(), w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
|
||||
Reference in New Issue
Block a user