package cmd
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"mercemay.top/httptap/internal/parser"
"mercemay.top/httptap/internal/source"
"mercemay.top/httptap/internal/source/unix"
"mercemay.top/httptap/internal/tui"
)
// runFlags is the per-subcommand flag set.
type runFlags struct {
FromStdin bool
}
func newRunCmd() *cobra.Command {
var rf runFlags
cmd := &cobra.Command{
Use: "run",
Short: "Attach to the shim socket and render the TUI",
RunE: func(c *cobra.Command, args []string) error {
return runTUI(rf)
},
}
cmd.Flags().BoolVar(&rf.FromStdin, "stdin", false,
"read raw HTTP from stdin instead of the socket")
return cmd
}
// runTUI is the shared implementation used by `httptap run` and when
// `httptap` is invoked with no subcommand (handled in main.go).
func runTUI(rf runFlags) error {
ctx, cancel := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer cancel()
var src source.Source
if rf.FromStdin {
src = source.NewStdin()
} else {
r, err := unix.Dial(globalOpts.SocketPath)
if err != nil {
return fmt.Errorf("run: %w", err)
}
src = r
}
defer src.Close()
app := tui.New()
prog := tea.NewProgram(app, tea.WithAltScreen(), tea.WithContext(ctx))
// Pump source events into the bubbletea program on a goroutine.
go func() {
for {
ev, err := src.Next(ctx)
if err != nil {
return
}
p := parser.New(byteReader(ev.Payload), func(m parser.Message) {
prog.Send(tui.MessageReceivedMsg{M: m})
})
_ = p.Run()
}
}()
_, err := prog.Run()
return err
}
// byteReader wraps a []byte as an io.Reader so the parser can consume
// it; we avoid bytes.NewReader here to reduce allocations in a hot loop.
type byteReader []byte
func (b *byteReader) Read(p []byte) (int, error) {
n := copy(p, *b)
*b = (*b)[n:]
if n == 0 {
return 0, os.ErrClosed
}
return n, nil
}