tangled
alpha
login
or
join now
oppi.li
/
gust
6
fork
atom
background code checker for golang
6
fork
atom
overview
issues
2
pulls
pipelines
model: rework command handling to stream stdout and stderr
oppi.li
11 months ago
8b064ad7
4211d1fa
+293
-54
4 changed files
expand all
collapse all
unified
split
config.go
model.go
parse.go
view.go
+4
config.go
···
29
29
Separator Style `toml:"separator"`
30
30
File Style `toml:"file"`
31
31
Line Style `toml:"line"`
32
32
+
Stdout Style `toml:"stdout"`
33
33
+
Stderr Style `toml:"stderr"`
32
34
Context ContextStyles `toml:"context"`
33
35
}
34
36
···
224
226
Duration: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))},
225
227
Separator: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("8"))},
226
228
Line: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))},
229
229
+
Stdout: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))},
230
230
+
Stderr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))},
227
231
Context: ContextStyles{
228
232
ActiveLine: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))},
229
233
ActiveLineNr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))},
+220
-35
model.go
···
1
1
package gust
2
2
3
3
import (
4
4
+
"bufio"
5
5
+
"context"
4
6
"io"
7
7
+
"os"
5
8
"os/exec"
6
9
"path/filepath"
10
10
+
"sync"
7
11
"time"
8
12
9
13
"github.com/charmbracelet/bubbles/spinner"
···
11
15
"github.com/fsnotify/fsnotify"
12
16
)
13
17
18
18
+
// CommandProcess represents a running command process
19
19
+
type CommandProcess struct {
20
20
+
cmd *exec.Cmd
21
21
+
ctx context.Context
22
22
+
cancel context.CancelFunc
23
23
+
stdoutReader io.ReadCloser
24
24
+
stderrReader io.ReadCloser
25
25
+
stdoutChan chan string
26
26
+
stderrChan chan string
27
27
+
done chan struct{}
28
28
+
waitDone sync.Once
29
29
+
}
30
30
+
31
31
+
// NewCommandProcess creates a new command process
32
32
+
func NewCommandProcess(cmdName string, args ...string) (*CommandProcess, error) {
33
33
+
ctx, cancel := context.WithCancel(context.Background())
34
34
+
cmd := exec.CommandContext(ctx, cmdName, args...)
35
35
+
36
36
+
stdoutPipe, err := cmd.StdoutPipe()
37
37
+
if err != nil {
38
38
+
cancel()
39
39
+
return nil, err
40
40
+
}
41
41
+
42
42
+
stderrPipe, err := cmd.StderrPipe()
43
43
+
if err != nil {
44
44
+
cancel()
45
45
+
return nil, err
46
46
+
}
47
47
+
48
48
+
process := &CommandProcess{
49
49
+
cmd: cmd,
50
50
+
ctx: ctx,
51
51
+
cancel: cancel,
52
52
+
stdoutReader: stdoutPipe,
53
53
+
stderrReader: stderrPipe,
54
54
+
stdoutChan: make(chan string, 100),
55
55
+
stderrChan: make(chan string, 100),
56
56
+
done: make(chan struct{}),
57
57
+
}
58
58
+
59
59
+
return process, nil
60
60
+
}
61
61
+
62
62
+
// Start starts the command and output scanners
63
63
+
func (p *CommandProcess) Start() error {
64
64
+
if err := p.cmd.Start(); err != nil {
65
65
+
p.cancel()
66
66
+
return err
67
67
+
}
68
68
+
69
69
+
// Start stdout scanner
70
70
+
go func() {
71
71
+
scanner := bufio.NewScanner(p.stdoutReader)
72
72
+
for scanner.Scan() {
73
73
+
select {
74
74
+
case <-p.ctx.Done():
75
75
+
return
76
76
+
case p.stdoutChan <- scanner.Text() + "\n":
77
77
+
// Line sent
78
78
+
}
79
79
+
}
80
80
+
}()
81
81
+
82
82
+
// Start stderr scanner
83
83
+
go func() {
84
84
+
scanner := bufio.NewScanner(p.stderrReader)
85
85
+
for scanner.Scan() {
86
86
+
select {
87
87
+
case <-p.ctx.Done():
88
88
+
return
89
89
+
case p.stderrChan <- scanner.Text() + "\n":
90
90
+
// Line sent
91
91
+
}
92
92
+
}
93
93
+
}()
94
94
+
95
95
+
// Wait for process in background
96
96
+
go func() {
97
97
+
p.waitDone.Do(func() {
98
98
+
p.cmd.Wait()
99
99
+
close(p.done)
100
100
+
})
101
101
+
}()
102
102
+
103
103
+
return nil
104
104
+
}
105
105
+
106
106
+
// Stop stops the command and cleans up resources
107
107
+
func (p *CommandProcess) Stop() {
108
108
+
p.cancel()
109
109
+
if p.cmd != nil && p.cmd.Process != nil {
110
110
+
p.cmd.Process.Kill()
111
111
+
}
112
112
+
}
113
113
+
114
114
+
// CheckStatus returns the process state if available
115
115
+
func (p *CommandProcess) CheckStatus() *os.ProcessState {
116
116
+
if p == nil || p.cmd == nil {
117
117
+
return nil
118
118
+
}
119
119
+
return p.cmd.ProcessState
120
120
+
}
121
121
+
14
122
// Model represents the application state
15
123
type Model struct {
16
124
stderr string
···
23
131
context int
24
132
25
133
status Status
26
26
-
duration time.Duration
27
134
systemErrors []error
28
135
136
136
+
process *CommandProcess
137
137
+
start *time.Time
138
138
+
end *time.Time
139
139
+
29
140
width, height int
30
141
}
31
142
···
54
165
case tea.KeyMsg:
55
166
switch msg.String() {
56
167
case "q", "ctrl+c":
168
168
+
if m.process != nil {
169
169
+
m.process.Stop()
170
170
+
}
57
171
return m, tea.Quit
58
172
case "r":
59
173
var cmd tea.Cmd
···
84
198
return m, tea.Batch(cmd, m.listen())
85
199
case modifiedMsg, reloadMsg:
86
200
m, cmd := m.reload()
87
87
-
return m, tea.Batch(cmd, m.listen())
201
201
+
return m, cmd
202
202
+
case stdoutMsg:
203
203
+
m.stdout = m.stdout + msg.line
204
204
+
return m, m.checkStdout()
205
205
+
case stderrMsg:
206
206
+
m.stderr = m.stderr + msg.line
207
207
+
if m.config.Mode == "build" {
208
208
+
messages := Parse(m.stderr)
209
209
+
fs := Fs{}
210
210
+
if !m.config.Summarized {
211
211
+
fs = BuildFs(messages)
212
212
+
fs.PopulateContext(messages, m.context)
213
213
+
}
214
214
+
m.messages = messages
215
215
+
m.fs = fs
216
216
+
}
217
217
+
return m, m.checkStderr()
218
218
+
case processState:
219
219
+
end := time.Now()
220
220
+
if msg.state == nil {
221
221
+
return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout())
222
222
+
} else if msg.state.Success() {
223
223
+
m.end = &end
224
224
+
return m, Success.Cmd()
225
225
+
} else if msg.state.Exited() {
226
226
+
m.end = &end
227
227
+
return m, Error.Cmd()
228
228
+
} else {
229
229
+
return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout())
230
230
+
}
88
231
default:
89
232
return m, nil
90
233
}
···
119
262
return watcherrMsg{}
120
263
}
121
264
return watcherrMsg{}
265
265
+
default:
266
266
+
return nil
122
267
}
123
268
}
124
269
}
125
270
126
271
func (m Model) reload() (Model, tea.Cmd) {
127
127
-
cmd := Success.Cmd()
272
272
+
status := Running.Cmd()
128
273
m.systemErrors = []error{}
129
274
130
130
-
command := exec.Command("go", m.config.Mode, m.config.Package)
131
131
-
stderr, err := command.StderrPipe()
132
132
-
if err != nil {
133
133
-
m.systemErrors = append(m.systemErrors, err)
134
134
-
cmd = Error.Cmd()
135
135
-
return m, cmd
275
275
+
// Clean up old process
276
276
+
if m.process != nil {
277
277
+
m.process.Stop()
278
278
+
m.process = nil
136
279
}
137
280
138
138
-
stdout, err := command.StdoutPipe()
281
281
+
// Reset state
282
282
+
m.stderr = ""
283
283
+
m.stdout = ""
284
284
+
m.messages = nil
285
285
+
m.fs = nil
286
286
+
287
287
+
// Create new process
288
288
+
process, err := NewCommandProcess("go", m.config.Mode, m.config.Package)
139
289
if err != nil {
140
290
m.systemErrors = append(m.systemErrors, err)
141
141
-
cmd = Error.Cmd()
291
291
+
return m, Error.Cmd()
142
292
}
293
293
+
m.process = process
143
294
295
295
+
// Record start time
144
296
start := time.Now()
145
145
-
if err := command.Start(); err != nil {
297
297
+
m.start = &start
298
298
+
m.end = nil
299
299
+
300
300
+
// Start the process
301
301
+
if err := m.process.Start(); err != nil {
146
302
m.systemErrors = append(m.systemErrors, err)
147
147
-
cmd = Error.Cmd()
148
148
-
return m, cmd
303
303
+
return m, Error.Cmd()
149
304
}
150
150
-
end := time.Now()
305
305
+
306
306
+
return m, tea.Batch(status, m.checkCmdStatus(), m.checkStdout(), m.checkStderr())
307
307
+
}
308
308
+
309
309
+
type stdoutMsg struct {
310
310
+
line string
311
311
+
}
312
312
+
313
313
+
type stderrMsg struct {
314
314
+
line string
315
315
+
}
151
316
152
152
-
stderrData, _ := io.ReadAll(stderr)
153
153
-
stdoutData, _ := io.ReadAll(stdout)
317
317
+
func (m Model) checkStdout() tea.Cmd {
318
318
+
if m.process == nil {
319
319
+
return nil
320
320
+
}
154
321
155
155
-
if err := command.Wait(); err != nil {
156
156
-
m.systemErrors = append(m.systemErrors, err)
157
157
-
cmd = Error.Cmd()
322
322
+
return func() tea.Msg {
323
323
+
select {
324
324
+
case line, ok := <-m.process.stdoutChan:
325
325
+
if !ok {
326
326
+
return nil
327
327
+
}
328
328
+
return stdoutMsg{line: line}
329
329
+
case <-time.After(10 * time.Millisecond):
330
330
+
return nil
331
331
+
}
158
332
}
333
333
+
}
159
334
160
160
-
m.stderr = string(stderrData)
161
161
-
m.stdout = string(stdoutData)
335
335
+
func (m Model) checkStderr() tea.Cmd {
336
336
+
if m.process == nil {
337
337
+
return nil
338
338
+
}
162
339
163
163
-
if m.config.Mode == "build" {
164
164
-
messages := Parse(m.stderr)
165
165
-
fs := Fs{}
166
166
-
if !m.config.Summarized {
167
167
-
fs = BuildFs(messages)
168
168
-
fs.PopulateContext(messages, m.context)
340
340
+
return func() tea.Msg {
341
341
+
select {
342
342
+
case line, ok := <-m.process.stderrChan:
343
343
+
if !ok {
344
344
+
return nil
345
345
+
}
346
346
+
return stderrMsg{line: line}
347
347
+
case <-time.After(10 * time.Millisecond):
348
348
+
return nil
169
349
}
170
170
-
m.messages = messages
171
171
-
m.fs = fs
172
172
-
m.duration = end.Sub(start)
350
350
+
}
351
351
+
}
352
352
+
353
353
+
type processState struct {
354
354
+
state *os.ProcessState
355
355
+
}
173
356
174
174
-
if len(m.messages) != 0 {
175
175
-
cmd = Error.Cmd()
176
176
-
}
357
357
+
func (m Model) checkCmdStatus() tea.Cmd {
358
358
+
if m.process == nil {
359
359
+
return nil
177
360
}
178
361
179
179
-
return m, cmd
362
362
+
return func() tea.Msg {
363
363
+
return processState{m.process.CheckStatus()}
364
364
+
}
180
365
}
+12
parse.go
···
48
48
continue
49
49
}
50
50
51
51
+
if strings.HasPrefix(line, "go:") {
52
52
+
msg := CompilerMessage{
53
53
+
Type: "error",
54
54
+
Location: Location{},
55
55
+
Message: strings.TrimPrefix(line, "go: "),
56
56
+
Raw: line,
57
57
+
Priority: 999,
58
58
+
}
59
59
+
60
60
+
messages = append(messages, msg)
61
61
+
}
62
62
+
51
63
if currentMultilineMsg != nil && continuation.MatchString(line) {
52
64
matches := continuation.FindStringSubmatch(line)
53
65
if len(matches) >= 2 {
+57
-19
view.go
···
4
4
"fmt"
5
5
"strconv"
6
6
"strings"
7
7
+
"time"
7
8
8
9
"github.com/charmbracelet/lipgloss"
9
10
"github.com/muesli/reflow/wordwrap"
···
11
12
12
13
func (m Model) View() string {
13
14
var b strings.Builder
15
15
+
style := m.config.Styles
16
16
+
sign := m.config.Signs
14
17
15
18
b.WriteString(m.viewStatus())
16
19
20
20
+
if m.config.Mode == "build" {
21
21
+
m.viewBuild(&b)
22
22
+
} else {
23
23
+
m.viewRun(&b)
24
24
+
}
25
25
+
26
26
+
for _, e := range m.systemErrors {
27
27
+
b.WriteString(style.Error.Render(sign.System))
28
28
+
b.WriteString(" ")
29
29
+
b.WriteString(e.Error())
30
30
+
b.WriteString("\n")
31
31
+
}
32
32
+
33
33
+
b.WriteString("\n")
34
34
+
35
35
+
if m.config.Help {
36
36
+
b.WriteString(m.viewHelp())
37
37
+
}
38
38
+
39
39
+
return b.String()
40
40
+
}
41
41
+
42
42
+
func (m Model) viewBuild(b *strings.Builder) {
17
43
maxLineLen := 1
18
44
maxColLen := 1
45
45
+
style := m.config.Styles
46
46
+
sign := m.config.Signs
47
47
+
19
48
for _, msg := range m.messages {
20
49
lineLen := len(msg.Location.Line)
21
50
colLen := len(msg.Location.Column)
···
27
56
maxColLen = colLen
28
57
}
29
58
}
30
30
-
31
31
-
style := m.config.Styles
32
32
-
sign := m.config.Signs
33
59
34
60
for _, msg := range m.messages {
35
61
var mb strings.Builder
···
102
128
103
129
b.WriteString("\n")
104
130
}
131
131
+
}
105
132
106
106
-
for _, e := range m.systemErrors {
107
107
-
b.WriteString(style.Error.Render(sign.System))
108
108
-
b.WriteString(" ")
109
109
-
b.WriteString(e.Error())
110
110
-
b.WriteString("\n")
133
133
+
func (m Model) viewRun(b *strings.Builder) {
134
134
+
style := m.config.Styles
135
135
+
136
136
+
width := m.width
137
137
+
if m.stdout != "" && m.stderr != "" {
138
138
+
width = width / 2
139
139
+
b.WriteString(style.Stdout.Width(width).Render("stdout"))
140
140
+
b.WriteString(style.Stdout.Width(width).Render("stderr"))
141
141
+
} else if m.stdout != "" {
142
142
+
b.WriteString(style.Stdout.Render("stdout"))
143
143
+
} else if m.stderr != "" {
144
144
+
b.WriteString(style.Stdout.Render("stderr"))
111
145
}
112
146
113
113
-
b.WriteString("\n")
147
147
+
leftSide := fmt.Sprintf("\n%s", m.stdout)
148
148
+
rightSide := fmt.Sprintf("\n%s", m.stderr)
114
149
115
115
-
if m.config.Help {
116
116
-
b.WriteString(m.viewHelp())
117
117
-
}
150
150
+
columns := lipgloss.JoinHorizontal(lipgloss.Top, leftSide, rightSide)
118
151
119
119
-
return b.String()
152
152
+
b.WriteString(columns)
120
153
}
121
154
122
155
func (m Model) viewStatus() string {
···
151
184
b.WriteString(statusStyle.Render(m.status.View()))
152
185
}
153
186
154
154
-
plural := func(n int) string {
187
187
+
plural := func(s string, n int) string {
155
188
if n == 1 {
156
156
-
return ""
189
189
+
return s
157
190
} else {
158
158
-
return "s"
191
191
+
return fmt.Sprintf("%ss", s)
159
192
}
160
193
}
161
194
162
195
// write message summary
163
196
if errorCount != 0 {
164
197
b.WriteString(style.Separator.Render(sign.Separator))
165
165
-
b.WriteString(style.Error.Render(fmt.Sprintf("%d error%s", errorCount, plural(errorCount))))
198
198
+
b.WriteString(style.Error.Render(fmt.Sprintf("%d %s", errorCount, plural("error", errorCount))))
166
199
}
167
200
if warningCount != 0 {
168
201
b.WriteString(style.Separator.Render(sign.Separator))
169
169
-
b.WriteString(style.Warning.Render(fmt.Sprintf("%d warning%s", warningCount, plural(warningCount))))
202
202
+
b.WriteString(style.Warning.Render(fmt.Sprintf("%d %s", warningCount, plural("warning", warningCount))))
170
203
}
171
204
172
205
b.WriteString(style.Separator.Render(sign.Separator))
173
173
-
b.WriteString(style.Duration.Render(m.duration.String()))
206
206
+
207
207
+
if m.start != nil && m.end != nil {
208
208
+
b.WriteString(style.Duration.Render(m.end.Sub(*m.start).String()))
209
209
+
} else if m.start != nil && m.end == nil {
210
210
+
b.WriteString(style.Duration.Render(time.Now().Sub(*m.start).String()))
211
211
+
}
174
212
175
213
b.WriteString("\n")
176
214