package cmd
import (
"context"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"mercemay.top/httptap/internal/parser"
"mercemay.top/httptap/internal/source"
"mercemay.top/httptap/internal/tui"
)
type replayFlags struct {
Path string
Loop bool
Filter string
}
func newReplayCmd() *cobra.Command {
var rf replayFlags
cmd := &cobra.Command{
Use: "replay <har-or-raw>",
Short: "Replay captured traffic from a HAR or raw file",
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
rf.Path = args[0]
return runReplay(rf)
},
}
cmd.Flags().BoolVar(&rf.Loop, "loop", false, "restart after EOF")
cmd.Flags().StringVar(&rf.Filter, "filter", "",
"apply a filter expression before rendering")
return cmd
}
func runReplay(rf replayFlags) error {
f, err := source.OpenFile(rf.Path)
if err != nil {
return fmt.Errorf("replay: %w", err)
}
defer f.Close()
app := tui.New()
prog := tea.NewProgram(app, tea.WithAltScreen())
ctx := context.Background()
go func() {
for {
ev, err := f.Next(ctx)
if err != nil {
if rf.Loop {
continue
}
return
}
p := parser.New(bytesReader(ev.Payload), func(m parser.Message) {
prog.Send(tui.MessageReceivedMsg{M: m})
})
_ = p.Run()
}
}()
_, err = prog.Run()
return err
}
// bytesReader is a read-once wrapper. Mirrors run.go's byteReader.
type bytesReader []byte
func (b *bytesReader) Read(p []byte) (int, error) {
n := copy(p, *b)
*b = (*b)[n:]
if n == 0 {
return 0, fmt.Errorf("EOF")
}
return n, nil
}