diff --git a/cmd/gust/main.go b/cmd/gust/main.go index 2d5db33..7f1983b 100644 --- a/cmd/gust/main.go +++ b/cmd/gust/main.go @@ -23,7 +23,7 @@ func main() { } if len(os.Args) < 2 { - fmt.Println("Usage: gust [build|run|dump] [package path]") + fmt.Println("Usage: gust [build|run|dump] [PACKAGE] [ARG...]") os.Exit(1) } @@ -38,14 +38,20 @@ func main() { if len(os.Args) >= 3 { pkg = os.Args[2] } - run(cmd, pkg) + + args := []string{} + if len(os.Args) >= 4 { + args = os.Args[3:] + } + + run(cmd, pkg, args) default: fmt.Printf("Unknown command: %s\n", cmd) os.Exit(1) } } -func run(mode, pkg string) { +func run(mode, pkg string, args []string) { w, err := fsnotify.NewWatcher() if err != nil { log.Println(err) @@ -59,7 +65,7 @@ func run(mode, pkg string) { cfg := gust.LoadConfig() - m := gust.NewModel(w, cfg, gust.WithMode(mode), gust.WithPackage(pkg)) + m := gust.NewModel(w, cfg, gust.WithMode(mode), gust.WithPackage(pkg), gust.WithArgs(args)) p := tea.NewProgram(m, tea.WithAltScreen()) if _, err := p.Run(); err != nil { diff --git a/config.go b/config.go index b0b04d4..bcf8328 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,8 @@ import ( type Config struct { Mode string `toml:"mode"` + Stream string `toml:"stream"` + Args []string Package string `toml:"package"` Summarized bool `toml:"summarized"` Help bool `toml:"help"` @@ -217,6 +219,7 @@ func DefaultConfig() Config { Summarized: false, Help: true, Mode: "build", + Stream: "stdout", Package: "./cmd/gust", Context: 0, Styles: Styles{ @@ -278,3 +281,9 @@ func WithMode(mode string) func(*Config) { cfg.Mode = mode } } + +func WithArgs(args []string) func(*Config) { + return func(cfg *Config) { + cfg.Args = args + } +} diff --git a/model.go b/model.go index 3fb1762..4af804b 100644 --- a/model.go +++ b/model.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" "strings" "sync" "time" @@ -25,8 +26,8 @@ type CommandProcess struct { cancel context.CancelFunc stdoutReader io.ReadCloser stderrReader io.ReadCloser - stdoutChan chan string - stderrChan chan string + stdoutChan chan stdoutMsg + stderrChan chan stderrMsg done chan struct{} waitDone sync.Once } @@ -54,8 +55,8 @@ func NewCommandProcess(cmdName string, args ...string) (*CommandProcess, error) cancel: cancel, stdoutReader: stdoutPipe, stderrReader: stderrPipe, - stdoutChan: make(chan string, 100), - stderrChan: make(chan string, 100), + stdoutChan: make(chan stdoutMsg, 1024), + stderrChan: make(chan stderrMsg, 1024), done: make(chan struct{}), } @@ -71,12 +72,14 @@ func (p *CommandProcess) Start() error { // Start stdout scanner go func() { + seq := 0 scanner := bufio.NewScanner(p.stdoutReader) for scanner.Scan() { select { case <-p.ctx.Done(): return - case p.stdoutChan <- scanner.Text() + "\n": + case p.stdoutChan <- stdoutMsg{line: scanner.Text() + "\n", seq: seq}: + seq++ // Line sent } } @@ -84,12 +87,14 @@ func (p *CommandProcess) Start() error { // Start stderr scanner go func() { + seq := 0 scanner := bufio.NewScanner(p.stderrReader) for scanner.Scan() { select { case <-p.ctx.Done(): return - case p.stderrChan <- scanner.Text() + "\n": + case p.stderrChan <- stderrMsg{line: scanner.Text() + "\n", seq: seq}: + seq++ // Line sent } } @@ -147,6 +152,7 @@ type Io []IoLine type IoLine struct { Kind IoKind Line string + Seq int } type IoKind int @@ -155,18 +161,43 @@ const ( Stderr ) -func (m Model) stderr() string { - var b strings.Builder +func (m Model) stdfilter(filter func(kind IoKind) bool) string { + var lines []stderrMsg for _, l := range m.stdio { - if l.Kind == Stderr { - b.WriteString(l.Line) + 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) @@ -226,6 +257,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.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 @@ -244,23 +282,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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, }) - if m.config.Mode == "build" { - messages := Parse(m.stderr()) - fs := Fs{} - if !m.config.Summarized { - fs = BuildFs(messages) - fs.PopulateContext(messages, m.config.Context) - } - m.messages = messages - m.fs = fs + messages := Parse(m.stderr()) + fs := Fs{} + if !m.config.Summarized { + fs = BuildFs(messages) + fs.PopulateContext(messages, m.config.Context) } + m.messages = messages + m.fs = fs return m, m.checkStderr() case processState: end := time.Now() @@ -345,7 +383,14 @@ func (m Model) reload() (Model, tea.Cmd) { m.fs = nil // Create new process - process, err := NewCommandProcess("go", m.config.Mode, m.config.Package) + 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() @@ -368,10 +413,12 @@ func (m Model) reload() (Model, tea.Cmd) { type stdoutMsg struct { line string + seq int } type stderrMsg struct { line string + seq int } func (m Model) checkStdout() tea.Cmd { @@ -381,11 +428,11 @@ func (m Model) checkStdout() tea.Cmd { return func() tea.Msg { select { - case line, ok := <-m.process.stdoutChan: + case msg, ok := <-m.process.stdoutChan: if !ok { return nil } - return stdoutMsg{line: line} + return msg case <-time.After(10 * time.Millisecond): return nil } @@ -399,11 +446,11 @@ func (m Model) checkStderr() tea.Cmd { return func() tea.Msg { select { - case line, ok := <-m.process.stderrChan: + case msg, ok := <-m.process.stderrChan: if !ok { return nil } - return stderrMsg{line: line} + return msg case <-time.After(10 * time.Millisecond): return nil } diff --git a/view.go b/view.go index 05cc695..557d997 100644 --- a/view.go +++ b/view.go @@ -31,7 +31,7 @@ func (m Model) viewBody() string { sign := m.config.Signs var b strings.Builder - if m.config.Mode == "build" { + if m.config.Mode == "build" || m.end != nil && m.process.cmd.ProcessState.ExitCode() != 0 { b.WriteString(m.viewBuild()) } else { b.WriteString(m.viewRun()) @@ -144,19 +144,12 @@ func (m Model) viewBuild() string { func (m Model) viewRun() string { var b strings.Builder - style := m.config.Styles + //style := m.config.Styles - for _, l := range m.stdio { - switch l.Kind { - case Stdout: - b.WriteString(style.Stdout.Render("stdout")) - case Stderr: - b.WriteString(style.Stderr.Render("stderr")) - default: - b.WriteString(strings.Repeat(" ", 6)) - } - b.WriteString(" ") - b.WriteString(l.Line) + if m.config.Stream == "stderr" { + b.WriteString(m.stderr()) + } else { + b.WriteString(m.stdout()) } return b.String() @@ -220,6 +213,7 @@ func (m Model) viewStatus() string { b.WriteString(style.Duration.Render(time.Now().Sub(*m.start).String())) } + b.WriteString(" ") line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String()))/utf8.RuneCountInString(sign.HorizontalBar))) b.WriteString(style.Separator.Render(line)) @@ -249,6 +243,16 @@ func (m Model) viewHelp() string { b.WriteString("b ") b.WriteString(style.Separator.Render("build")) b.WriteString(style.Separator.Render(sign.Separator)) + + b.WriteString("o ") + if m.config.Stream != "stdout" { + b.WriteString(style.Separator.Render("stdout")) + b.WriteString(style.Separator.Render(sign.Separator)) + } + if m.config.Stream != "stderr" { + b.WriteString(style.Separator.Render("stderr")) + b.WriteString(style.Separator.Render(sign.Separator)) + } } if m.config.Mode != "run" { @@ -263,11 +267,22 @@ func (m Model) viewHelp() string { b.WriteString("h/? ") b.WriteString(style.Separator.Render("hide help")) + b.WriteString(" ") + + var rightStatus strings.Builder + rightStatus.WriteString(" ") + if m.config.Stream == "stderr" { + rightStatus.WriteString(style.Stderr.Render("stderr")) + } else { + rightStatus.WriteString(style.Stdout.Render("stdout")) + } + rightStatus.WriteString(style.Separator.Render(sign.Separator)) + rightStatus.WriteString(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) - percentageBox := fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100) - line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String())-lipgloss.Width(percentageBox))/utf8.RuneCountInString(sign.HorizontalBar))) + right := rightStatus.String() + line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String())-lipgloss.Width(right))/utf8.RuneCountInString(sign.HorizontalBar))) b.WriteString(style.Separator.Render(line)) - b.WriteString(style.Duration.Render(percentageBox)) + b.WriteString(style.Duration.Render(right)) return lipgloss.NewStyle().Render(b.String()) }