cmd/httptap/cmd/replay.go

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
}