background code checker for golang

model: show build errors even in run mode

Changed files
+121 -44
cmd
gust
+10 -4
cmd/gust/main.go
··· 23 23 } 24 24 25 25 if len(os.Args) < 2 { 26 - fmt.Println("Usage: gust [build|run|dump] [package path]") 26 + fmt.Println("Usage: gust [build|run|dump] [PACKAGE] [ARG...]") 27 27 os.Exit(1) 28 28 } 29 29 ··· 38 38 if len(os.Args) >= 3 { 39 39 pkg = os.Args[2] 40 40 } 41 - run(cmd, pkg) 41 + 42 + args := []string{} 43 + if len(os.Args) >= 4 { 44 + args = os.Args[3:] 45 + } 46 + 47 + run(cmd, pkg, args) 42 48 default: 43 49 fmt.Printf("Unknown command: %s\n", cmd) 44 50 os.Exit(1) 45 51 } 46 52 } 47 53 48 - func run(mode, pkg string) { 54 + func run(mode, pkg string, args []string) { 49 55 w, err := fsnotify.NewWatcher() 50 56 if err != nil { 51 57 log.Println(err) ··· 59 65 60 66 cfg := gust.LoadConfig() 61 67 62 - m := gust.NewModel(w, cfg, gust.WithMode(mode), gust.WithPackage(pkg)) 68 + m := gust.NewModel(w, cfg, gust.WithMode(mode), gust.WithPackage(pkg), gust.WithArgs(args)) 63 69 64 70 p := tea.NewProgram(m, tea.WithAltScreen()) 65 71 if _, err := p.Run(); err != nil {
+9
config.go
··· 12 12 13 13 type Config struct { 14 14 Mode string `toml:"mode"` 15 + Stream string `toml:"stream"` 16 + Args []string 15 17 Package string `toml:"package"` 16 18 Summarized bool `toml:"summarized"` 17 19 Help bool `toml:"help"` ··· 217 219 Summarized: false, 218 220 Help: true, 219 221 Mode: "build", 222 + Stream: "stdout", 220 223 Package: "./cmd/gust", 221 224 Context: 0, 222 225 Styles: Styles{ ··· 278 281 cfg.Mode = mode 279 282 } 280 283 } 284 + 285 + func WithArgs(args []string) func(*Config) { 286 + return func(cfg *Config) { 287 + cfg.Args = args 288 + } 289 + }
+71 -24
model.go
··· 7 7 "os" 8 8 "os/exec" 9 9 "path/filepath" 10 + "sort" 10 11 "strings" 11 12 "sync" 12 13 "time" ··· 25 26 cancel context.CancelFunc 26 27 stdoutReader io.ReadCloser 27 28 stderrReader io.ReadCloser 28 - stdoutChan chan string 29 - stderrChan chan string 29 + stdoutChan chan stdoutMsg 30 + stderrChan chan stderrMsg 30 31 done chan struct{} 31 32 waitDone sync.Once 32 33 } ··· 54 55 cancel: cancel, 55 56 stdoutReader: stdoutPipe, 56 57 stderrReader: stderrPipe, 57 - stdoutChan: make(chan string, 100), 58 - stderrChan: make(chan string, 100), 58 + stdoutChan: make(chan stdoutMsg, 1024), 59 + stderrChan: make(chan stderrMsg, 1024), 59 60 done: make(chan struct{}), 60 61 } 61 62 ··· 71 72 72 73 // Start stdout scanner 73 74 go func() { 75 + seq := 0 74 76 scanner := bufio.NewScanner(p.stdoutReader) 75 77 for scanner.Scan() { 76 78 select { 77 79 case <-p.ctx.Done(): 78 80 return 79 - case p.stdoutChan <- scanner.Text() + "\n": 81 + case p.stdoutChan <- stdoutMsg{line: scanner.Text() + "\n", seq: seq}: 82 + seq++ 80 83 // Line sent 81 84 } 82 85 } ··· 84 87 85 88 // Start stderr scanner 86 89 go func() { 90 + seq := 0 87 91 scanner := bufio.NewScanner(p.stderrReader) 88 92 for scanner.Scan() { 89 93 select { 90 94 case <-p.ctx.Done(): 91 95 return 92 - case p.stderrChan <- scanner.Text() + "\n": 96 + case p.stderrChan <- stderrMsg{line: scanner.Text() + "\n", seq: seq}: 97 + seq++ 93 98 // Line sent 94 99 } 95 100 } ··· 147 152 type IoLine struct { 148 153 Kind IoKind 149 154 Line string 155 + Seq int 150 156 } 151 157 type IoKind int 152 158 ··· 155 161 Stderr 156 162 ) 157 163 158 - func (m Model) stderr() string { 159 - var b strings.Builder 164 + func (m Model) stdfilter(filter func(kind IoKind) bool) string { 165 + var lines []stderrMsg 160 166 161 167 for _, l := range m.stdio { 162 - if l.Kind == Stderr { 163 - b.WriteString(l.Line) 168 + if filter(l.Kind) { 169 + lines = append(lines, stderrMsg{ 170 + line: l.Line, 171 + seq: l.Seq, 172 + }) 164 173 } 165 174 } 166 175 176 + sort.Slice(lines, func(i, j int) bool { 177 + return lines[i].seq < lines[j].seq 178 + }) 179 + 180 + var b strings.Builder 181 + 182 + for _, l := range lines { 183 + b.WriteString(l.line) 184 + } 185 + 167 186 return b.String() 168 187 } 169 188 189 + func (m Model) stderr() string { 190 + return m.stdfilter(func(kind IoKind) bool { 191 + return kind == Stderr 192 + }) 193 + } 194 + 195 + func (m Model) stdout() string { 196 + return m.stdfilter(func(kind IoKind) bool { 197 + return kind == Stdout 198 + }) 199 + } 200 + 170 201 func NewModel(watcher *fsnotify.Watcher, config Config, options ...func(*Config)) Model { 171 202 for _, o := range options { 172 203 o(&config) ··· 226 257 case "s": 227 258 m.config.Summarized = !m.config.Summarized 228 259 return m, nil 260 + case "o": 261 + if m.config.Stream == "stdout" { 262 + m.config.Stream = "stderr" 263 + } else { 264 + m.config.Stream = "stdout" 265 + } 266 + return m, nil 229 267 case "h", "?": 230 268 m.config.Help = !m.config.Help 231 269 return m, nil ··· 244 282 m.stdio = append(m.stdio, IoLine{ 245 283 Kind: Stdout, 246 284 Line: msg.line, 285 + Seq: msg.seq, 247 286 }) 248 287 return m, m.checkStdout() 249 288 case stderrMsg: 250 289 m.stdio = append(m.stdio, IoLine{ 251 290 Kind: Stderr, 252 291 Line: msg.line, 292 + Seq: msg.seq, 253 293 }) 254 - if m.config.Mode == "build" { 255 - messages := Parse(m.stderr()) 256 - fs := Fs{} 257 - if !m.config.Summarized { 258 - fs = BuildFs(messages) 259 - fs.PopulateContext(messages, m.config.Context) 260 - } 261 - m.messages = messages 262 - m.fs = fs 294 + messages := Parse(m.stderr()) 295 + fs := Fs{} 296 + if !m.config.Summarized { 297 + fs = BuildFs(messages) 298 + fs.PopulateContext(messages, m.config.Context) 263 299 } 300 + m.messages = messages 301 + m.fs = fs 264 302 return m, m.checkStderr() 265 303 case processState: 266 304 end := time.Now() ··· 345 383 m.fs = nil 346 384 347 385 // Create new process 348 - process, err := NewCommandProcess("go", m.config.Mode, m.config.Package) 386 + allArgs := []string{} 387 + allArgs = append(allArgs, m.config.Mode) 388 + allArgs = append(allArgs, m.config.Package) 389 + if m.config.Mode == "run" { 390 + allArgs = append(allArgs, m.config.Args...) 391 + } 392 + process, err := NewCommandProcess("go", allArgs...) 393 + 349 394 if err != nil { 350 395 m.systemErrors = append(m.systemErrors, err) 351 396 return m, Error.Cmd() ··· 368 413 369 414 type stdoutMsg struct { 370 415 line string 416 + seq int 371 417 } 372 418 373 419 type stderrMsg struct { 374 420 line string 421 + seq int 375 422 } 376 423 377 424 func (m Model) checkStdout() tea.Cmd { ··· 381 428 382 429 return func() tea.Msg { 383 430 select { 384 - case line, ok := <-m.process.stdoutChan: 431 + case msg, ok := <-m.process.stdoutChan: 385 432 if !ok { 386 433 return nil 387 434 } 388 - return stdoutMsg{line: line} 435 + return msg 389 436 case <-time.After(10 * time.Millisecond): 390 437 return nil 391 438 } ··· 399 446 400 447 return func() tea.Msg { 401 448 select { 402 - case line, ok := <-m.process.stderrChan: 449 + case msg, ok := <-m.process.stderrChan: 403 450 if !ok { 404 451 return nil 405 452 } 406 - return stderrMsg{line: line} 453 + return msg 407 454 case <-time.After(10 * time.Millisecond): 408 455 return nil 409 456 }
+31 -16
view.go
··· 31 31 sign := m.config.Signs 32 32 33 33 var b strings.Builder 34 - if m.config.Mode == "build" { 34 + if m.config.Mode == "build" || m.end != nil && m.process.cmd.ProcessState.ExitCode() != 0 { 35 35 b.WriteString(m.viewBuild()) 36 36 } else { 37 37 b.WriteString(m.viewRun()) ··· 144 144 145 145 func (m Model) viewRun() string { 146 146 var b strings.Builder 147 - style := m.config.Styles 147 + //style := m.config.Styles 148 148 149 - for _, l := range m.stdio { 150 - switch l.Kind { 151 - case Stdout: 152 - b.WriteString(style.Stdout.Render("stdout")) 153 - case Stderr: 154 - b.WriteString(style.Stderr.Render("stderr")) 155 - default: 156 - b.WriteString(strings.Repeat(" ", 6)) 157 - } 158 - b.WriteString(" ") 159 - b.WriteString(l.Line) 149 + if m.config.Stream == "stderr" { 150 + b.WriteString(m.stderr()) 151 + } else { 152 + b.WriteString(m.stdout()) 160 153 } 161 154 162 155 return b.String() ··· 220 213 b.WriteString(style.Duration.Render(time.Now().Sub(*m.start).String())) 221 214 } 222 215 216 + b.WriteString(" ") 223 217 line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String()))/utf8.RuneCountInString(sign.HorizontalBar))) 224 218 b.WriteString(style.Separator.Render(line)) 225 219 ··· 249 243 b.WriteString("b ") 250 244 b.WriteString(style.Separator.Render("build")) 251 245 b.WriteString(style.Separator.Render(sign.Separator)) 246 + 247 + b.WriteString("o ") 248 + if m.config.Stream != "stdout" { 249 + b.WriteString(style.Separator.Render("stdout")) 250 + b.WriteString(style.Separator.Render(sign.Separator)) 251 + } 252 + if m.config.Stream != "stderr" { 253 + b.WriteString(style.Separator.Render("stderr")) 254 + b.WriteString(style.Separator.Render(sign.Separator)) 255 + } 252 256 } 253 257 254 258 if m.config.Mode != "run" { ··· 263 267 264 268 b.WriteString("h/? ") 265 269 b.WriteString(style.Separator.Render("hide help")) 270 + b.WriteString(" ") 266 271 267 - percentageBox := fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100) 268 - line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String())-lipgloss.Width(percentageBox))/utf8.RuneCountInString(sign.HorizontalBar))) 272 + var rightStatus strings.Builder 273 + rightStatus.WriteString(" ") 274 + if m.config.Stream == "stderr" { 275 + rightStatus.WriteString(style.Stderr.Render("stderr")) 276 + } else { 277 + rightStatus.WriteString(style.Stdout.Render("stdout")) 278 + } 279 + rightStatus.WriteString(style.Separator.Render(sign.Separator)) 280 + rightStatus.WriteString(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) 281 + 282 + right := rightStatus.String() 283 + line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String())-lipgloss.Width(right))/utf8.RuneCountInString(sign.HorizontalBar))) 269 284 b.WriteString(style.Separator.Render(line)) 270 - b.WriteString(style.Duration.Render(percentageBox)) 285 + b.WriteString(style.Duration.Render(right)) 271 286 272 287 return lipgloss.NewStyle().Render(b.String()) 273 288 }