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
view: add scrolling viewport to build and run modes
oppi.li
11 months ago
7311ca82
8b064ad7
+134
-48
4 changed files
expand all
collapse all
unified
split
config.go
flake.nix
model.go
view.go
+5
-1
config.go
···
17
17
Help bool `toml:"help"`
18
18
Styles Styles `toml:"styles"`
19
19
Signs Signs `toml:"signs"`
20
20
+
Context int `toml:"context"`
20
21
}
21
22
22
23
type Styles struct {
···
45
46
Error string `toml:"error"`
46
47
Warning string `toml:"warning"`
47
48
System string `toml:"system"`
49
49
+
HorizontalBar string `toml:"line"`
48
50
Separator string `toml:"separator"`
49
51
PassiveIndent string `toml:"passiveIndent"`
50
52
ActiveIndent string `toml:"activeIndent"`
···
216
218
Help: true,
217
219
Mode: "build",
218
220
Package: "./cmd/gust",
221
221
+
Context: 0,
219
222
Styles: Styles{
220
223
Mode: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("5"))},
221
224
Success: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("2"))},
···
224
227
Warning: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))},
225
228
File: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("4"))},
226
229
Duration: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))},
227
227
-
Separator: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("8"))},
230
230
+
Separator: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("12"))},
228
231
Line: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("7"))},
229
232
Stdout: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("3"))},
230
233
Stderr: Style{lipgloss.NewStyle().Foreground(lipgloss.Color("1"))},
···
239
242
Error: "err",
240
243
Warning: "wrn",
241
244
System: "sys",
245
245
+
HorizontalBar: "─",
242
246
Separator: " · ",
243
247
PassiveIndent: " | ",
244
248
ActiveIndent: " > ",
+1
-1
flake.nix
···
30
30
version = "0.1.0";
31
31
src = gitignoreSource ./.;
32
32
subPackages = ["cmd/gust"];
33
33
-
vendorHash = "sha256-BXjnNZMITu8uC/B5hFv41QRXtOv5tDMBRySU4dcyu6E=";
33
33
+
vendorHash = "sha256-XKydHm/KX6sGU5ndQTNsIqaF0BpHew9NB+Q7gcAKBh4=";
34
34
env.CGO_ENABLED = 0;
35
35
};
36
36
};
+79
-19
model.go
···
7
7
"os"
8
8
"os/exec"
9
9
"path/filepath"
10
10
+
"strings"
10
11
"sync"
11
12
"time"
12
13
13
14
"github.com/charmbracelet/bubbles/spinner"
15
15
+
"github.com/charmbracelet/bubbles/viewport"
14
16
tea "github.com/charmbracelet/bubbletea"
17
17
+
"github.com/charmbracelet/lipgloss"
15
18
"github.com/fsnotify/fsnotify"
16
19
)
17
20
···
121
124
122
125
// Model represents the application state
123
126
type Model struct {
124
124
-
stderr string
125
125
-
stdout string
127
127
+
stdio []IoLine
126
128
127
129
messages []CompilerMessage
128
130
fs Fs
129
131
w *fsnotify.Watcher
130
132
config Config
131
131
-
context int
132
133
133
134
status Status
134
135
systemErrors []error
···
137
138
start *time.Time
138
139
end *time.Time
139
140
141
141
+
viewport viewport.Model
142
142
+
ready bool
140
143
width, height int
141
144
}
142
145
146
146
+
type Io []IoLine
147
147
+
type IoLine struct {
148
148
+
Kind IoKind
149
149
+
Line string
150
150
+
}
151
151
+
type IoKind int
152
152
+
153
153
+
const (
154
154
+
Stdout IoKind = iota
155
155
+
Stderr
156
156
+
)
157
157
+
158
158
+
func (m Model) stderr() string {
159
159
+
var b strings.Builder
160
160
+
161
161
+
for _, l := range m.stdio {
162
162
+
if l.Kind == Stderr {
163
163
+
b.WriteString(l.Line)
164
164
+
}
165
165
+
}
166
166
+
167
167
+
return b.String()
168
168
+
}
169
169
+
143
170
func NewModel(watcher *fsnotify.Watcher, config Config, options ...func(*Config)) Model {
144
171
for _, o := range options {
145
172
o(&config)
146
173
}
147
174
148
175
return Model{
149
149
-
w: watcher,
150
150
-
status: NewStatus(),
151
151
-
context: 1,
152
152
-
config: config,
176
176
+
w: watcher,
177
177
+
status: NewStatus(),
178
178
+
config: config,
153
179
}
154
180
}
155
181
···
158
184
}
159
185
160
186
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
187
187
+
var cmd tea.Cmd
188
188
+
161
189
switch msg := msg.(type) {
162
190
case tea.WindowSizeMsg:
163
163
-
m.width, m.height = msg.Width, msg.Height
164
164
-
return m, nil
191
191
+
headerHeight := lipgloss.Height(m.viewStatus())
192
192
+
footerHeight := lipgloss.Height(m.viewHelp())
193
193
+
verticalMarginHeight := headerHeight + footerHeight
194
194
+
195
195
+
m.width, m.height = msg.Width, msg.Height-verticalMarginHeight
196
196
+
if !m.ready {
197
197
+
m.viewport = viewport.New(m.width, m.height)
198
198
+
m.viewport.YPosition = headerHeight
199
199
+
m.ready = true
200
200
+
} else {
201
201
+
m.viewport.Width, m.viewport.Height = m.width, m.height
202
202
+
}
203
203
+
204
204
+
return m, updateViewport
165
205
case tea.KeyMsg:
166
206
switch msg.String() {
167
207
case "q", "ctrl+c":
···
190
230
m.config.Help = !m.config.Help
191
231
return m, nil
192
232
default:
193
193
-
return m, nil
233
233
+
m.viewport, cmd = m.viewport.Update(msg)
234
234
+
return m, cmd
194
235
}
195
236
case StatusKind, spinner.TickMsg:
196
237
var cmd tea.Cmd
···
200
241
m, cmd := m.reload()
201
242
return m, cmd
202
243
case stdoutMsg:
203
203
-
m.stdout = m.stdout + msg.line
244
244
+
m.stdio = append(m.stdio, IoLine{
245
245
+
Kind: Stdout,
246
246
+
Line: msg.line,
247
247
+
})
204
248
return m, m.checkStdout()
205
249
case stderrMsg:
206
206
-
m.stderr = m.stderr + msg.line
250
250
+
m.stdio = append(m.stdio, IoLine{
251
251
+
Kind: Stderr,
252
252
+
Line: msg.line,
253
253
+
})
207
254
if m.config.Mode == "build" {
208
208
-
messages := Parse(m.stderr)
255
255
+
messages := Parse(m.stderr())
209
256
fs := Fs{}
210
257
if !m.config.Summarized {
211
258
fs = BuildFs(messages)
212
212
-
fs.PopulateContext(messages, m.context)
259
259
+
fs.PopulateContext(messages, m.config.Context)
213
260
}
214
261
m.messages = messages
215
262
m.fs = fs
···
218
265
case processState:
219
266
end := time.Now()
220
267
if msg.state == nil {
221
221
-
return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout())
268
268
+
return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout(), updateViewport)
222
269
} else if msg.state.Success() {
223
270
m.end = &end
224
271
return m, Success.Cmd()
···
226
273
m.end = &end
227
274
return m, Error.Cmd()
228
275
} else {
229
229
-
return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout())
276
276
+
return m, tea.Batch(m.checkCmdStatus(), m.checkStderr(), m.checkStdout(), updateViewport)
277
277
+
}
278
278
+
case updateViewportMsg:
279
279
+
m.viewport.SetContent(m.viewBody())
280
280
+
if m.config.Mode == "build" {
281
281
+
m.viewport.GotoTop()
282
282
+
} else {
283
283
+
m.viewport.GotoBottom()
230
284
}
285
285
+
return m, nil
231
286
default:
232
287
return m, nil
233
288
}
···
237
292
238
293
func reload() tea.Msg {
239
294
return reloadMsg{}
295
295
+
}
296
296
+
297
297
+
type updateViewportMsg struct{}
298
298
+
299
299
+
func updateViewport() tea.Msg {
300
300
+
return updateViewportMsg{}
240
301
}
241
302
242
303
type modifiedMsg struct{}
···
279
340
}
280
341
281
342
// Reset state
282
282
-
m.stderr = ""
283
283
-
m.stdout = ""
343
343
+
m.stdio = []IoLine{}
284
344
m.messages = nil
285
345
m.fs = nil
286
346
···
303
363
return m, Error.Cmd()
304
364
}
305
365
306
306
-
return m, tea.Batch(status, m.checkCmdStatus(), m.checkStdout(), m.checkStderr())
366
366
+
return m, tea.Batch(status, m.checkCmdStatus(), m.checkStdout(), m.checkStderr(), updateViewport)
307
367
}
308
368
309
369
type stdoutMsg struct {
+49
-27
view.go
···
5
5
"strconv"
6
6
"strings"
7
7
"time"
8
8
+
"unicode/utf8"
8
9
9
10
"github.com/charmbracelet/lipgloss"
10
11
"github.com/muesli/reflow/wordwrap"
···
12
13
13
14
func (m Model) View() string {
14
15
var b strings.Builder
15
15
-
style := m.config.Styles
16
16
-
sign := m.config.Signs
17
16
18
17
b.WriteString(m.viewStatus())
19
18
19
19
+
m.viewport.SetContent(m.viewBody())
20
20
+
b.WriteString(m.viewport.View())
21
21
+
22
22
+
b.WriteString("\n")
23
23
+
24
24
+
b.WriteString(m.viewHelp())
25
25
+
26
26
+
return b.String()
27
27
+
}
28
28
+
29
29
+
func (m Model) viewBody() string {
30
30
+
style := m.config.Styles
31
31
+
sign := m.config.Signs
32
32
+
33
33
+
var b strings.Builder
20
34
if m.config.Mode == "build" {
21
21
-
m.viewBuild(&b)
35
35
+
b.WriteString(m.viewBuild())
22
36
} else {
23
23
-
m.viewRun(&b)
37
37
+
b.WriteString(m.viewRun())
24
38
}
25
39
26
40
for _, e := range m.systemErrors {
···
31
45
}
32
46
33
47
b.WriteString("\n")
34
34
-
35
35
-
if m.config.Help {
36
36
-
b.WriteString(m.viewHelp())
37
37
-
}
38
38
-
39
48
return b.String()
40
49
}
41
50
42
42
-
func (m Model) viewBuild(b *strings.Builder) {
51
51
+
func (m Model) viewBuild() string {
52
52
+
var b strings.Builder
43
53
maxLineLen := 1
44
54
maxColLen := 1
45
55
style := m.config.Styles
···
128
138
129
139
b.WriteString("\n")
130
140
}
141
141
+
142
142
+
return b.String()
131
143
}
132
144
133
133
-
func (m Model) viewRun(b *strings.Builder) {
145
145
+
func (m Model) viewRun() string {
146
146
+
var b strings.Builder
134
147
style := m.config.Styles
135
148
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"))
149
149
+
for _, l := range m.stdio {
150
150
+
switch l.Kind {
151
151
+
case Stdout:
152
152
+
b.WriteString(style.Stdout.Render("stdout"))
153
153
+
case Stderr:
154
154
+
b.WriteString(style.Stderr.Render("stderr"))
155
155
+
default:
156
156
+
b.WriteString(strings.Repeat(" ", 6))
157
157
+
}
158
158
+
b.WriteString(" ")
159
159
+
b.WriteString(l.Line)
145
160
}
146
161
147
147
-
leftSide := fmt.Sprintf("\n%s", m.stdout)
148
148
-
rightSide := fmt.Sprintf("\n%s", m.stderr)
149
149
-
150
150
-
columns := lipgloss.JoinHorizontal(lipgloss.Top, leftSide, rightSide)
151
151
-
152
152
-
b.WriteString(columns)
162
162
+
return b.String()
153
163
}
154
164
155
165
func (m Model) viewStatus() string {
···
210
220
b.WriteString(style.Duration.Render(time.Now().Sub(*m.start).String()))
211
221
}
212
222
223
223
+
line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String()))/utf8.RuneCountInString(sign.HorizontalBar)))
224
224
+
b.WriteString(style.Separator.Render(line))
225
225
+
213
226
b.WriteString("\n")
214
227
215
228
return b.String()
216
229
}
217
230
218
231
func (m Model) viewHelp() string {
232
232
+
if !m.config.Help {
233
233
+
return ""
234
234
+
}
235
235
+
219
236
var b strings.Builder
220
237
style := m.config.Styles
221
238
sign := m.config.Signs
···
247
264
b.WriteString("h/? ")
248
265
b.WriteString(style.Separator.Render("hide help"))
249
266
250
250
-
return lipgloss.NewStyle().PaddingTop(2).Render(b.String())
267
267
+
percentageBox := fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)
268
268
+
line := strings.Repeat(sign.HorizontalBar, max(0, (m.viewport.Width-lipgloss.Width(b.String())-lipgloss.Width(percentageBox))/utf8.RuneCountInString(sign.HorizontalBar)))
269
269
+
b.WriteString(style.Separator.Render(line))
270
270
+
b.WriteString(style.Duration.Render(percentageBox))
271
271
+
272
272
+
return lipgloss.NewStyle().Render(b.String())
251
273
}