Something went wrong. Try again.
background code checker for golang
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477package gust
import ( "bufio" "context" "io" "os" "os/exec" "path/filepath" "sort" "strings" "sync" "time"
"github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/fsnotify/fsnotify")
// CommandProcess represents a running command processtype CommandProcess struct { cmd *exec.Cmd ctx context.Context cancel context.CancelFunc stdoutReader io.ReadCloser stderrReader io.ReadCloser stdoutChan chan stdoutMsg stderrChan chan stderrMsg done chan struct{} waitDone sync.Once}
// NewCommandProcess creates a new command processfunc NewCommandProcess(cmdName string, args ...string) (*CommandProcess, error) { ctx, cancel := context.WithCancel(context.Background()) cmd := exec.CommandContext(ctx, cmdName, args...)
stdoutPipe, err := cmd.StdoutPipe() if err != nil { cancel() return nil, err }
stderrPipe, err := cmd.StderrPipe() if err != nil { cancel() return nil, err }
process := &CommandProcess{ cmd: cmd, ctx: ctx, cancel: cancel, stdoutReader: stdoutPipe, stderrReader: stderrPipe, stdoutChan: make(chan stdoutMsg, 1024), stderrChan: make(chan stderrMsg, 1024), done: make(chan struct{}), }
return process, nil}
// Start starts the command and output scannersfunc (p *CommandProcess) Start() error { if err := p.cmd.Start(); err != nil { p.cancel() return err }
// Start stdout scanner go func() { seq := 0 scanner := bufio.NewScanner(p.stdoutReader) for scanner.Scan() { select { case <-p.ctx.Done(): return case p.stdoutChan <- stdoutMsg{line: scanner.Text() + "\n", seq: seq}: seq++ // Line sent } } }()
// Start stderr scanner go func() { seq := 0 scanner := bufio.NewScanner(p.stderrReader) for scanner.Scan() { select { case <-p.ctx.Done(): return case p.stderrChan <- stderrMsg{line: scanner.Text() + "\n", seq: seq}: seq++ // Line sent } } }()
// Wait for process in background go func() { p.waitDone.Do(func() { p.cmd.Wait() close(p.done) }) }()
return nil}
func (p *CommandProcess) Stop() { if p.cmd == nil || p.cmd.Process == nil { return }
p.cancel()}
// CheckStatus returns the process state if availablefunc (p *CommandProcess) CheckStatus() *os.ProcessState { if p == nil || p.cmd == nil { return nil } return p.cmd.ProcessState}
// Model represents the application statetype Model struct { stdio []IoLine
messages []CompilerMessage extra []string // whatever was not parsed from the compiler output
fs Fs w *fsnotify.Watcher config Config
status Status systemErrors []error
process *CommandProcess start *time.Time end *time.Time
viewport viewport.Model ready bool width, height int}
type Io []IoLinetype IoLine struct { Kind IoKind Line string Seq int}type IoKind int
const ( Stdout IoKind = iota Stderr)
func (m Model) stdfilter(filter func(kind IoKind) bool) string { var lines []stderrMsg
for _, l := range m.stdio { if filter(l.Kind) { lines = append(lines, stderrMsg{ line: l.Line, seq: l.Seq, }) } }
sort.Slice(lines, func(i, j int) bool { return lines[i].seq < lines[j].seq })
var b strings.Builder
for _, l := range lines { b.WriteString(l.line) }
return b.String()}
func (m Model) stderr() string { return m.stdfilter(func(kind IoKind) bool { return kind == Stderr })}
func (m Model) stdout() string { return m.stdfilter(func(kind IoKind) bool { return kind == Stdout })}
func NewModel(watcher *fsnotify.Watcher, config Config, options ...func(*Config)) Model { for _, o := range options { o(&config) }
return Model{ w: watcher, status: NewStatus(), config: config, }}
func (m Model) Init() tea.Cmd { return tea.Batch(reload, m.status.Init())}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd
switch msg := msg.(type) { case tea.WindowSizeMsg: headerHeight := lipgloss.Height(m.viewStatus()) footerHeight := lipgloss.Height(m.viewHelp()) verticalMarginHeight := headerHeight + footerHeight
m.width, m.height = msg.Width, msg.Height-verticalMarginHeight if !m.ready { m.viewport = viewport.New(m.width, m.height) m.viewport.YPosition = headerHeight m.ready = true } else { m.viewport.Width, m.viewport.Height = m.width, m.height }
return m, updateViewport case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": if m.process != nil { m.process.Stop() } return m, tea.Quit case "r": var cmd tea.Cmd if m.config.Mode != "run" { m.config.Mode = "run" cmd = reload } return m, cmd case "b": var cmd tea.Cmd if m.config.Mode != "build" { m.config.Mode = "build" cmd = reload } return m, cmd case "s": m.config.Summarized = !m.config.Summarized return m, nil case "o": if m.config.Stream == "stdout" { m.config.Stream = "stderr" } else { m.config.Stream = "stdout" } return m, nil case "h", "?": m.config.Help = !m.config.Help return m, nil default: m.viewport, cmd = m.viewport.Update(msg) return m, cmd } case StatusKind, spinner.TickMsg: var cmd tea.Cmd m.status, cmd = m.status.Update(msg) return m, tea.Batch(cmd, m.listen()) case modifiedMsg, reloadMsg: m, cmd := m.reload() return m, cmd case stdoutMsg: m.stdio = append(m.stdio, IoLine{ Kind: Stdout, Line: msg.line, Seq: msg.seq, }) return m, m.checkStdout() case stderrMsg: m.stdio = append(m.stdio, IoLine{ Kind: Stderr, Line: msg.line, Seq: msg.seq, }) messages, extra := Parse(m.stderr()) fs := Fs{} if !m.config.Summarized { fs = BuildFs(messages) fs.PopulateContext(messages, m.config.Context) } m.messages = messages m.extra = extra m.fs = fs return m, m.checkStderr() case processState: end := time.Now() if msg.state == nil { return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout(), updateViewport) } else if msg.state.Success() { m.end = &end return m, Success.Cmd() } else if msg.state.Exited() { m.end = &end return m, Error.Cmd() } else { return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout(), updateViewport) } case updateViewportMsg: m.viewport.SetContent(m.viewBody()) if m.config.Mode == "build" { m.viewport.GotoTop() } else { m.viewport.GotoBottom() } return m, nil default: return m, nil }}
type reloadMsg struct{}
func reload() tea.Msg { return reloadMsg{}}
type updateViewportMsg struct{}
func updateViewport() tea.Msg { return updateViewportMsg{}}
type modifiedMsg struct{}type watcherrMsg struct{}
func (m Model) listen() tea.Cmd { return func() tea.Msg { select { case event, ok := <-m.w.Events: if !ok { return watcherrMsg{} }
if event.Has(fsnotify.Write|fsnotify.Create|fsnotify.Remove) && filepath.Ext(event.Name) == ".go" { return modifiedMsg{} }
return nil
case _, ok := <-m.w.Errors: if !ok { return watcherrMsg{} } return watcherrMsg{} default: return nil } }}
func (m Model) reload() (Model, tea.Cmd) { status := Running.Cmd() m.systemErrors = []error{}
// Clean up old process if m.process != nil { m.process.Stop() m.process = nil }
// Reset state m.stdio = []IoLine{} m.messages = nil m.extra = nil m.fs = nil
// Create new process allArgs := []string{} allArgs = append(allArgs, m.config.Mode) allArgs = append(allArgs, m.config.Package) if m.config.Mode == "run" { allArgs = append(allArgs, m.config.Args...) } process, err := NewCommandProcess("go", allArgs...)
if err != nil { m.systemErrors = append(m.systemErrors, err) return m, Error.Cmd() } m.process = process
// Record start time start := time.Now() m.start = &start m.end = nil
// Start the process if err := m.process.Start(); err != nil { m.systemErrors = append(m.systemErrors, err) return m, Error.Cmd() }
return m, tea.Batch(status, m.checkCmdStatus(), m.checkStdout(), m.checkStderr(), updateViewport)}
type stdoutMsg struct { line string seq int}
type stderrMsg struct { line string seq int}
func (m Model) checkStdout() tea.Cmd { if m.process == nil { return nil }
return func() tea.Msg { select { case msg, ok := <-m.process.stdoutChan: if !ok { return nil } return msg case <-time.After(10 * time.Millisecond): return nil } }}
func (m Model) checkStderr() tea.Cmd { if m.process == nil { return nil }
return func() tea.Msg { select { case msg, ok := <-m.process.stderrChan: if !ok { return nil } return msg case <-time.After(10 * time.Millisecond): return nil } }}
type processState struct { state *os.ProcessState}
func (m Model) checkCmdStatus() tea.Cmd { if m.process == nil { return nil }
return func() tea.Msg { return processState{m.process.CheckStatus()} }}