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