forked from Mirrors/bubbletea
feat: add Tracer, a debugging aid
Adds a Tracer interface that is capable of logging all executed tea.Cmd and all processed tea.Msg. RemoteTracer is one standard implementation, which provides access to a live stream of these event logs, through a TCP socket.
This commit is contained in:
parent
2d10416631
commit
d7516fc570
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/charmbracelet/bubbletea/tracer"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
|
@ -151,3 +152,10 @@ func WithANSICompressor() ProgramOption {
|
|||
p.startupOptions |= withANSICompressor
|
||||
}
|
||||
}
|
||||
|
||||
// WithTracer sets a tracer for the program. This is useful for debugging.
|
||||
func WithTracer(t tracer.Tracer) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.tracer = t
|
||||
}
|
||||
}
|
||||
|
|
44
tea.go
44
tea.go
|
@ -16,10 +16,13 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbletea/tracer"
|
||||
"github.com/containerd/console"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
"github.com/muesli/cancelreader"
|
||||
|
@ -122,6 +125,8 @@ type Program struct {
|
|||
// as this value only comes into play on Windows, hence the ignore comment
|
||||
// below.
|
||||
windowsStdin *os.File //nolint:golint,structcheck,unused
|
||||
|
||||
tracer tracer.Tracer
|
||||
}
|
||||
|
||||
// Quit is a special command that tells the Bubble Tea program to exit.
|
||||
|
@ -244,8 +249,17 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
|
|||
// possible to cancel them so we'll have to leak the goroutine
|
||||
// until Cmd returns.
|
||||
go func() {
|
||||
tc := tracer.NewCommand()
|
||||
if p.tracer != nil {
|
||||
p.tracer.Command(tc)
|
||||
}
|
||||
|
||||
msg := cmd() // this can be long.
|
||||
p.Send(msg)
|
||||
|
||||
if p.tracer != nil {
|
||||
p.traceCommand(tc, msg)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
@ -254,6 +268,34 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
|
|||
return ch
|
||||
}
|
||||
|
||||
func (p *Program) traceCommand(tc tracer.Command, msg Msg) {
|
||||
t := "nil"
|
||||
if msg != nil {
|
||||
t = reflect.TypeOf(msg).String()
|
||||
}
|
||||
|
||||
tc.Finished = time.Now()
|
||||
tc.Type = t
|
||||
tc.Msg = fmt.Sprintf("%v", msg)
|
||||
|
||||
p.tracer.Command(tc)
|
||||
}
|
||||
|
||||
func (p *Program) traceMessage(model Model, msg Msg) {
|
||||
if p.tracer != nil {
|
||||
t := "nil"
|
||||
if msg != nil {
|
||||
t = reflect.TypeOf(msg).String()
|
||||
}
|
||||
|
||||
p.tracer.Message(tracer.Message{
|
||||
Model: reflect.TypeOf(model).String(),
|
||||
Type: t,
|
||||
Msg: fmt.Sprintf("%v", msg),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// eventLoop is the central message loop. It receives and handles the default
|
||||
// Bubble Tea messages, update the model and triggers redraws.
|
||||
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
||||
|
@ -266,6 +308,8 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
|||
return model, err
|
||||
|
||||
case msg := <-p.msgs:
|
||||
p.traceMessage(model, msg)
|
||||
|
||||
// Handle special internal messages.
|
||||
switch msg := msg.(type) {
|
||||
case quitMsg:
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package tracer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Tracer interface {
|
||||
Command(c Command)
|
||||
Message(m Message)
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Timestamp time.Time
|
||||
Message Message
|
||||
Command Command
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Model string
|
||||
Type string
|
||||
Msg string
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
ID int
|
||||
Started time.Time
|
||||
Finished time.Time
|
||||
Type string
|
||||
Msg string
|
||||
}
|
||||
|
||||
func NewCommand() Command {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
id++
|
||||
|
||||
return Command{
|
||||
ID: id,
|
||||
Started: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ch = make(chan Event)
|
||||
mtx sync.Mutex
|
||||
id int
|
||||
)
|
||||
|
||||
type RemoteTracer struct {
|
||||
}
|
||||
|
||||
func NewRemoteTracer() (*RemoteTracer, error) {
|
||||
listen, err := net.Listen("tcp", ":13337")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listen.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go handleTraceConn(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
t := &RemoteTracer{}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (rt *RemoteTracer) Command(c Command) {
|
||||
ev := Event{
|
||||
Timestamp: time.Now(),
|
||||
Command: c,
|
||||
}
|
||||
|
||||
ch <- ev
|
||||
}
|
||||
|
||||
func (rt *RemoteTracer) Message(m Message) {
|
||||
ev := Event{
|
||||
Timestamp: time.Now(),
|
||||
Message: m,
|
||||
}
|
||||
|
||||
ch <- ev
|
||||
}
|
||||
|
||||
func handleTraceConn(conn net.Conn) {
|
||||
for ev := range ch {
|
||||
b, _ := json.Marshal(ev)
|
||||
_, _ = conn.Write(b)
|
||||
_, err := conn.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// close conn
|
||||
_ = conn.Close()
|
||||
}
|
Loading…
Reference in New Issue