cmd/httptap/cmd/run.go

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
}