background code checker for golang
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

gust: init

oppi.li 57df359a

+888
+3
.gitignore
··· 1 + *.log 2 + *.txt 3 + main
+55
cmd/main.go
··· 1 + package main 2 + 3 + import ( 4 + "log" 5 + "os" 6 + "path/filepath" 7 + 8 + tea "github.com/charmbracelet/bubbletea" 9 + "github.com/fsnotify/fsnotify" 10 + "tangled.sh/oppili.bsky.social/gust" 11 + ) 12 + 13 + func main() { 14 + if len(os.Getenv("DEBUG")) > 0 { 15 + f, err := tea.LogToFile("debug.log", "debug") 16 + if err != nil { 17 + log.Println("fatal:", err) 18 + os.Exit(1) 19 + } 20 + defer f.Close() 21 + } 22 + 23 + // Create new watcher 24 + w, err := fsnotify.NewWatcher() 25 + if err != nil { 26 + log.Println(err) 27 + } 28 + defer w.Close() 29 + 30 + err = watchRecursively(w, ".") 31 + if err != nil { 32 + log.Println(err) 33 + } 34 + 35 + m := gust.NewModel(w) 36 + 37 + p := tea.NewProgram(m, tea.WithAltScreen()) 38 + if _, err := p.Run(); err != nil { 39 + log.Printf("Error running program: %v\n", err) 40 + os.Exit(1) 41 + } 42 + } 43 + 44 + func watchRecursively(watcher *fsnotify.Watcher, path string) error { 45 + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 46 + if err != nil { 47 + return err 48 + } 49 + if info.IsDir() { 50 + return watcher.Add(path) 51 + } 52 + return nil 53 + }) 54 + return err 55 + }
+90
fs.go
··· 1 + package gust 2 + 3 + import ( 4 + "bufio" 5 + "math" 6 + "os" 7 + "sort" 8 + "strconv" 9 + ) 10 + 11 + type Fs map[string]Lines 12 + type Lines map[int]string 13 + 14 + func BuildFs(messages []CompilerMessage) Fs { 15 + files := make(Fs) 16 + for _, msg := range messages { 17 + if msg.Location.File != "" { 18 + files[msg.Location.File] = nil 19 + } 20 + } 21 + 22 + for f := range files { 23 + fd, err := os.Open(f) 24 + if err != nil { 25 + // failed to open file 26 + continue 27 + } 28 + 29 + lines := make(Lines) 30 + scanner := bufio.NewScanner(fd) 31 + nr := 1 32 + for scanner.Scan() { 33 + lines[nr] = scanner.Text() 34 + nr++ 35 + } 36 + 37 + if err := scanner.Err(); err != nil { 38 + continue 39 + } 40 + 41 + files[f] = lines 42 + } 43 + 44 + return files 45 + } 46 + 47 + func (lines Lines) Start() int { 48 + start := math.MaxInt 49 + for lineNum := range lines { 50 + start = min(start, lineNum) 51 + } 52 + return start 53 + } 54 + 55 + func (lines Lines) Ordered() []string { 56 + lineNumbers := make([]int, 0, len(lines)) 57 + for lineNum := range lines { 58 + lineNumbers = append(lineNumbers, lineNum) 59 + } 60 + 61 + sort.Ints(lineNumbers) 62 + 63 + ordered := []string{} 64 + for _, nr := range lineNumbers { 65 + ordered = append(ordered, lines[nr]) 66 + } 67 + 68 + return ordered 69 + } 70 + 71 + func (fs *Fs) PopulateContext(msgs []CompilerMessage, ctxSize int) { 72 + for i, m := range msgs { 73 + path := m.Location.File 74 + file, _ := (*fs)[path] 75 + line := m.Location.Line 76 + 77 + if path != "" && line != "" { 78 + nr, _ := strconv.Atoi(line) 79 + ctx := make(Lines) 80 + for i := nr - ctxSize; i <= nr+ctxSize; i++ { 81 + if l, ok := file[i]; ok { 82 + ctx[i] = l 83 + } 84 + } 85 + 86 + // extract lines 87 + msgs[i].Context = ctx 88 + } 89 + } 90 + }
+32
go.mod
··· 1 + module tangled.sh/oppili.bsky.social/gust 2 + 3 + go 1.24.1 4 + 5 + require ( 6 + github.com/charmbracelet/bubbles v0.21.0 7 + github.com/charmbracelet/bubbletea v1.3.4 8 + github.com/charmbracelet/lipgloss v1.1.0 9 + github.com/fsnotify/fsnotify v1.9.0 10 + ) 11 + 12 + require ( 13 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 15 + github.com/charmbracelet/x/ansi v0.8.0 // indirect 16 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 17 + github.com/charmbracelet/x/term v0.2.1 // indirect 18 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 19 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 20 + github.com/mattn/go-isatty v0.0.20 // indirect 21 + github.com/mattn/go-localereader v0.0.1 // indirect 22 + github.com/mattn/go-runewidth v0.0.16 // indirect 23 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 24 + github.com/muesli/cancelreader v0.2.2 // indirect 25 + github.com/muesli/reflow v0.3.0 // indirect 26 + github.com/muesli/termenv v0.16.0 // indirect 27 + github.com/rivo/uniseg v0.4.7 // indirect 28 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 29 + golang.org/x/sync v0.11.0 // indirect 30 + golang.org/x/sys v0.32.0 // indirect 31 + golang.org/x/text v0.3.8 // indirect 32 + )
+53
go.sum
··· 1 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 2 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 3 + github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= 4 + github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 5 + github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= 6 + github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= 7 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 8 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 9 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 10 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 11 + github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 12 + github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 13 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 14 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 15 + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 16 + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 17 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 18 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 19 + github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 20 + github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 21 + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 22 + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 23 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 24 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 25 + github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 26 + github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 27 + github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 28 + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 29 + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 30 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 31 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 32 + github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 33 + github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 34 + github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 35 + github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 36 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 37 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 38 + github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 39 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 40 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 41 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 42 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 43 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 44 + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 45 + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 46 + golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 47 + golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 48 + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 + golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 51 + golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 52 + golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 53 + golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+414
gust.go
··· 1 + package gust 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "os/exec" 7 + "strings" 8 + "time" 9 + 10 + "github.com/charmbracelet/bubbles/spinner" 11 + tea "github.com/charmbracelet/bubbletea" 12 + "github.com/charmbracelet/lipgloss" 13 + "github.com/fsnotify/fsnotify" 14 + "github.com/muesli/reflow/wordwrap" 15 + ) 16 + 17 + // Model represents the application state 18 + type Model struct { 19 + stderr string 20 + stdout string 21 + 22 + mode string 23 + messages []CompilerMessage 24 + fs Fs 25 + w *fsnotify.Watcher 26 + options Options 27 + context int 28 + 29 + status Status 30 + duration time.Duration 31 + systemErrors []error 32 + 33 + width, height int 34 + } 35 + 36 + type Options struct { 37 + summarized bool 38 + help bool 39 + styles Styles 40 + signs Signs 41 + } 42 + 43 + type Styles struct { 44 + mode lipgloss.Style 45 + success lipgloss.Style 46 + running lipgloss.Style 47 + error lipgloss.Style 48 + warning lipgloss.Style 49 + duration lipgloss.Style 50 + separator lipgloss.Style 51 + file lipgloss.Style 52 + line lipgloss.Style 53 + context ContextStyles 54 + } 55 + 56 + type ContextStyles struct { 57 + activeLine lipgloss.Style 58 + activeLineNr lipgloss.Style 59 + passiveLine lipgloss.Style 60 + passiveLineNr lipgloss.Style 61 + } 62 + 63 + type Signs struct { 64 + // sign for error messages 65 + error string 66 + // sign for warning messages 67 + warning string 68 + // sign for system messages 69 + system string 70 + // separator for UI bits 71 + separator string 72 + // indentation prefix string for passive line in context 73 + passiveIndent string 74 + // indentation prefix string for active line in context 75 + activeIndent string 76 + } 77 + 78 + func NewModel(watcher *fsnotify.Watcher) Model { 79 + return Model{ 80 + w: watcher, 81 + mode: "build", 82 + status: NewStatus(), 83 + context: 1, 84 + options: Options{ 85 + summarized: false, 86 + help: true, 87 + styles: Styles{ 88 + mode: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), 89 + success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")), 90 + running: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), 91 + error: lipgloss.NewStyle().Foreground(lipgloss.Color("1")), 92 + warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), 93 + file: lipgloss.NewStyle().Foreground(lipgloss.Color("4")), 94 + duration: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), 95 + separator: lipgloss.NewStyle().Foreground(lipgloss.Color("8")), 96 + line: lipgloss.NewStyle().Foreground(lipgloss.Color("15")), 97 + context: ContextStyles{ 98 + activeLine: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 99 + activeLineNr: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 100 + passiveLine: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 101 + passiveLineNr: lipgloss.NewStyle().Foreground(lipgloss.Color("12")), 102 + }, 103 + }, 104 + signs: Signs{ 105 + error: "err", 106 + warning: "wrn", 107 + system: "sys", 108 + separator: " · ", 109 + passiveIndent: " | ", 110 + activeIndent: " > ", 111 + }, 112 + }, 113 + } 114 + } 115 + 116 + func (m Model) Init() tea.Cmd { 117 + return tea.Batch(reload, m.status.Init()) 118 + } 119 + 120 + func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 121 + switch msg := msg.(type) { 122 + case tea.WindowSizeMsg: 123 + m.width, m.height = msg.Width, msg.Height 124 + return m, nil 125 + case tea.KeyMsg: 126 + switch msg.String() { 127 + case "q", "ctrl+c": 128 + return m, tea.Quit 129 + case "r": 130 + return m, reload 131 + case "s": 132 + m.options.summarized = !m.options.summarized 133 + return m, nil 134 + case "h", "?": 135 + m.options.help = !m.options.help 136 + return m, nil 137 + default: 138 + return m, nil 139 + } 140 + case StatusKind, spinner.TickMsg: 141 + var cmd tea.Cmd 142 + m.status, cmd = m.status.Update(msg) 143 + return m, tea.Batch(cmd, m.listen()) 144 + case modifiedMsg, reloadMsg: 145 + m, cmd := m.reload() 146 + return m, tea.Batch(cmd, m.listen()) 147 + default: 148 + return m, nil 149 + } 150 + } 151 + 152 + func (m Model) View() string { 153 + var b strings.Builder 154 + 155 + b.WriteString(m.viewStatus()) 156 + 157 + maxLineLen := 1 158 + maxColLen := 1 159 + for _, msg := range m.messages { 160 + lineLen := len(msg.Location.Line) 161 + colLen := len(msg.Location.Column) 162 + 163 + if lineLen > maxLineLen { 164 + maxLineLen = lineLen 165 + } 166 + if colLen > maxColLen { 167 + maxColLen = colLen 168 + } 169 + } 170 + 171 + style := m.options.styles 172 + sign := m.options.signs 173 + 174 + for _, msg := range m.messages { 175 + if msg.Type == "error" { 176 + b.WriteString(style.error.Render(sign.error)) 177 + b.WriteString(" ") 178 + } else { 179 + b.WriteString(style.warning.Render(sign.warning)) 180 + b.WriteString(" ") 181 + } 182 + 183 + if msg.Location.Line != "" { 184 + b.WriteString(style.line.Align(lipgloss.Right).Width(maxLineLen).Render(msg.Location.Line)) 185 + 186 + if msg.Location.Column != "" { 187 + b.WriteString(":") 188 + b.WriteString(style.line.Align(lipgloss.Left).Width(maxColLen).Render(msg.Location.Column)) 189 + } 190 + } 191 + if msg.Location.File != "" { 192 + b.WriteString(" ") 193 + b.WriteString(style.file.Render(msg.Location.File)) 194 + b.WriteString(" ") 195 + } 196 + 197 + b.WriteString(wordwrap.String(msg.Message, m.width)) 198 + 199 + // display context 200 + if !m.options.summarized { 201 + b.WriteString("\n") 202 + start := msg.Context.Start() 203 + style := style.context 204 + for offset, line := range msg.Context.Ordered() { 205 + nr := fmt.Sprintf("%d", start+offset) 206 + nrStyle := style.passiveLineNr 207 + lineStyle := style.passiveLine 208 + indent := sign.passiveIndent 209 + if msg.Location.Line == nr { 210 + nrStyle = style.activeLineNr 211 + lineStyle = style.activeLine 212 + indent = sign.activeIndent 213 + } 214 + b.WriteString(nrStyle.Render(indent)) 215 + b.WriteString(nrStyle.Align(lipgloss.Right).Render(nr)) 216 + b.WriteString(" ") 217 + b.WriteString(lineStyle.Align(lipgloss.Right).Render(line)) 218 + b.WriteString("\n") 219 + } 220 + } 221 + 222 + b.WriteString("\n") 223 + } 224 + 225 + for _, e := range m.systemErrors { 226 + b.WriteString(style.error.Render(sign.system)) 227 + b.WriteString(" ") 228 + b.WriteString(e.Error()) 229 + b.WriteString("\n") 230 + } 231 + 232 + b.WriteString("\n") 233 + return b.String() 234 + } 235 + 236 + func (m Model) viewStatus() string { 237 + var b strings.Builder 238 + 239 + errorCount := 0 240 + warningCount := 0 241 + for _, msg := range m.messages { 242 + if msg.Type == "error" { 243 + errorCount++ 244 + } else if msg.Type == "warning" { 245 + warningCount++ 246 + } 247 + } 248 + 249 + style := m.options.styles 250 + sign := m.options.signs 251 + 252 + // write mode 253 + b.WriteString(style.mode.Render(m.mode)) 254 + 255 + // write status 256 + statusStyle := style.success 257 + switch m.status.Kind { 258 + case Running, Initializing: 259 + statusStyle = style.running 260 + case Error: 261 + statusStyle = style.error 262 + } 263 + if len(m.messages) == 0 { 264 + b.WriteString(style.separator.Render(sign.separator)) 265 + b.WriteString(statusStyle.Render(m.status.View())) 266 + } 267 + 268 + plural := func(n int) string { 269 + if n == 1 { 270 + return "" 271 + } else { 272 + return "s" 273 + } 274 + } 275 + 276 + // write message summary 277 + if errorCount != 0 { 278 + b.WriteString(style.separator.Render(sign.separator)) 279 + b.WriteString(style.error.Render(fmt.Sprintf("%d error%s", errorCount, plural(errorCount)))) 280 + } 281 + if warningCount != 0 { 282 + b.WriteString(style.separator.Render(sign.separator)) 283 + b.WriteString(style.warning.Render(fmt.Sprintf("%d warning%s", warningCount, plural(warningCount)))) 284 + } 285 + 286 + b.WriteString(style.separator.Render(sign.separator)) 287 + b.WriteString(style.duration.Render(m.duration.String())) 288 + 289 + b.WriteString("\n") 290 + 291 + if m.options.help { 292 + b.WriteString(m.viewHelp()) 293 + } 294 + return b.String() 295 + } 296 + 297 + func (m Model) viewHelp() string { 298 + var b strings.Builder 299 + style := m.options.styles 300 + sign := m.options.signs 301 + 302 + b.WriteString("s ") 303 + b.WriteString(style.separator.Render("toggle context")) 304 + b.WriteString(style.separator.Render(sign.separator)) 305 + 306 + if m.mode != "build" { 307 + b.WriteString("b ") 308 + b.WriteString(style.separator.Render("build")) 309 + b.WriteString(style.separator.Render(sign.separator)) 310 + } 311 + 312 + if m.mode != "run" { 313 + b.WriteString("r ") 314 + b.WriteString(style.separator.Render("run")) 315 + b.WriteString(style.separator.Render(sign.separator)) 316 + } 317 + 318 + b.WriteString("q ") 319 + b.WriteString(style.separator.Render("quit")) 320 + b.WriteString(style.separator.Render(sign.separator)) 321 + 322 + b.WriteString("h/? ") 323 + b.WriteString(style.separator.Render("hide help")) 324 + 325 + return lipgloss.NewStyle().MarginTop(1).Render(b.String()) 326 + } 327 + 328 + type reloadMsg struct{} 329 + 330 + func reload() tea.Msg { 331 + return reloadMsg{} 332 + } 333 + 334 + type modifiedMsg struct{} 335 + type watcherrMsg struct{} 336 + 337 + func (m Model) listen() tea.Cmd { 338 + return func() tea.Msg { 339 + select { 340 + case event, ok := <-m.w.Events: 341 + if !ok { 342 + return watcherrMsg{} 343 + } 344 + 345 + if event.Has(fsnotify.Write | fsnotify.Create | fsnotify.Remove) { 346 + return modifiedMsg{} 347 + } 348 + 349 + return nil 350 + 351 + case _, ok := <-m.w.Errors: 352 + if !ok { 353 + return watcherrMsg{} 354 + } 355 + return watcherrMsg{} 356 + } 357 + } 358 + } 359 + 360 + func (m Model) reload() (Model, tea.Cmd) { 361 + cmd := Success.Cmd() 362 + m.systemErrors = []error{} 363 + 364 + command := exec.Command("go", m.mode, "./cmd/main.go") 365 + stderr, err := command.StderrPipe() 366 + if err != nil { 367 + m.systemErrors = append(m.systemErrors, err) 368 + cmd = Error.Cmd() 369 + return m, cmd 370 + } 371 + 372 + stdout, err := command.StdoutPipe() 373 + if err != nil { 374 + m.systemErrors = append(m.systemErrors, err) 375 + cmd = Error.Cmd() 376 + } 377 + 378 + start := time.Now() 379 + if err := command.Start(); err != nil { 380 + m.systemErrors = append(m.systemErrors, err) 381 + cmd = Error.Cmd() 382 + return m, cmd 383 + } 384 + end := time.Now() 385 + 386 + stderrData, _ := io.ReadAll(stderr) 387 + stdoutData, _ := io.ReadAll(stdout) 388 + 389 + if err := command.Wait(); err != nil { 390 + m.systemErrors = append(m.systemErrors, err) 391 + cmd = Error.Cmd() 392 + } 393 + 394 + m.stderr = string(stderrData) 395 + m.stdout = string(stdoutData) 396 + 397 + if m.mode == "build" { 398 + messages := Parse(m.stderr) 399 + fs := Fs{} 400 + if !m.options.summarized { 401 + fs = BuildFs(messages) 402 + fs.PopulateContext(messages, m.context) 403 + } 404 + m.messages = messages 405 + m.fs = fs 406 + m.duration = end.Sub(start) 407 + 408 + if len(m.messages) != 0 { 409 + cmd = Error.Cmd() 410 + } 411 + } 412 + 413 + return m, cmd 414 + }
+146
parse.go
··· 1 + package gust 2 + 3 + import ( 4 + "bufio" 5 + "regexp" 6 + "strings" 7 + ) 8 + 9 + var ( 10 + errorWithColumn = regexp.MustCompile(`(?:\S+: )?([^:]+):(\d+):(\d+): (.+)`) 11 + errorWithoutColumn = regexp.MustCompile(`(?:\S+: )?([^:]+):(\d+): (.+)`) 12 + cantLoadPackage = regexp.MustCompile(`can't load package: (.+)`) 13 + continuation = regexp.MustCompile(`^\s+(.+)`) 14 + ) 15 + 16 + type CompilerMessage struct { 17 + Type string // "error" or "warning" 18 + Location Location 19 + Message string // error message 20 + Raw string // raw message 21 + Priority int // for sorting 22 + Context Lines 23 + } 24 + 25 + type Location struct { 26 + File string 27 + Line string 28 + Column string 29 + } 30 + 31 + func Parse(output string) []CompilerMessage { 32 + var messages []CompilerMessage 33 + var currentMultilineMsg *CompilerMessage 34 + 35 + scanner := bufio.NewScanner(strings.NewReader(output)) 36 + for scanner.Scan() { 37 + line := scanner.Text() 38 + 39 + if strings.TrimSpace(line) == "" { 40 + continue 41 + } 42 + 43 + if strings.HasPrefix(line, "#") { 44 + continue 45 + } 46 + 47 + if strings.Contains(line, "panic:") { 48 + continue 49 + } 50 + 51 + if currentMultilineMsg != nil && continuation.MatchString(line) { 52 + matches := continuation.FindStringSubmatch(line) 53 + if len(matches) >= 2 { 54 + currentMultilineMsg.Message += "\n" + matches[1] 55 + continue 56 + } 57 + } 58 + 59 + currentMultilineMsg = nil 60 + 61 + if matches := errorWithColumn.FindStringSubmatch(line); len(matches) >= 5 { 62 + msg := CompilerMessage{ 63 + Type: determineType(matches[4]), 64 + Location: Location{ 65 + File: matches[1], 66 + Line: matches[2], 67 + Column: matches[3], 68 + }, 69 + Message: matches[4], 70 + Raw: line, 71 + Priority: priorityFor(matches[4]), 72 + } 73 + 74 + messages = append(messages, msg) 75 + currentMultilineMsg = &messages[len(messages)-1] 76 + continue 77 + } 78 + 79 + if matches := errorWithoutColumn.FindStringSubmatch(line); len(matches) >= 4 { 80 + msg := CompilerMessage{ 81 + Type: determineType(matches[3]), 82 + Location: Location{ 83 + File: matches[1], 84 + Line: matches[2], 85 + }, 86 + Message: matches[3], 87 + Raw: line, 88 + Priority: priorityFor(matches[3]), 89 + } 90 + 91 + messages = append(messages, msg) 92 + currentMultilineMsg = &messages[len(messages)-1] 93 + continue 94 + } 95 + 96 + if matches := cantLoadPackage.FindStringSubmatch(line); len(matches) >= 2 { 97 + msg := CompilerMessage{ 98 + Type: "error", 99 + Message: "can't load package: " + matches[1], 100 + Raw: line, 101 + Priority: 1, // High priority error 102 + } 103 + 104 + messages = append(messages, msg) 105 + currentMultilineMsg = &messages[len(messages)-1] 106 + continue 107 + } 108 + 109 + if strings.Contains(line, "error") || strings.Contains(line, "failed to build") { 110 + msg := CompilerMessage{ 111 + Type: "error", 112 + Message: line, 113 + Raw: line, 114 + Priority: 10, // Lower priority for generic errors 115 + } 116 + 117 + messages = append(messages, msg) 118 + currentMultilineMsg = &messages[len(messages)-1] 119 + } 120 + } 121 + 122 + return messages 123 + } 124 + 125 + func determineType(message string) string { 126 + lowercase := strings.ToLower(message) 127 + if strings.Contains(lowercase, "error") { 128 + return "error" 129 + } else if strings.Contains(lowercase, "warning") || strings.Contains(lowercase, "vet") { 130 + return "warning" 131 + } 132 + return "error" 133 + } 134 + 135 + func priorityFor(message string) int { 136 + lowercase := strings.ToLower(message) 137 + if strings.Contains(lowercase, "warning") { 138 + return 0 // Warnings first 139 + } else if strings.Contains(lowercase, "syntax error") { 140 + return 1 // Syntax errors next 141 + } else if strings.Contains(lowercase, "undefined") { 142 + return 2 // Undefined references next 143 + } else { 144 + return 5 // Other errors 145 + } 146 + }
+95
status.go
··· 1 + package gust 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/charmbracelet/bubbles/spinner" 7 + tea "github.com/charmbracelet/bubbletea" 8 + ) 9 + 10 + type Status struct { 11 + Kind StatusKind 12 + Spinner spinner.Model 13 + } 14 + 15 + func NewStatus() Status { 16 + return Status{ 17 + Kind: Initializing, 18 + Spinner: spinner.New(spinner.WithSpinner(spinner.Line)), 19 + } 20 + } 21 + 22 + type StatusKind int 23 + 24 + const ( 25 + Initializing StatusKind = iota 26 + Success 27 + Running 28 + Error 29 + ) 30 + 31 + func (s StatusKind) String() string { 32 + switch s { 33 + case Initializing: 34 + return "initializing" 35 + case Success: 36 + return "success" 37 + case Running: 38 + return "running" 39 + case Error: 40 + return "error" 41 + default: 42 + return "error" 43 + } 44 + } 45 + 46 + func (s StatusKind) Cmd() tea.Cmd { 47 + return func() tea.Msg { 48 + return s 49 + } 50 + } 51 + 52 + func (s StatusKind) Style() spinner.Spinner { 53 + switch s { 54 + case Initializing: 55 + return spinner.Dot 56 + case Success: 57 + return spinner.Jump 58 + default: 59 + return spinner.Dot 60 + } 61 + } 62 + 63 + func (s Status) Init() tea.Cmd { 64 + return tea.Batch(Initializing.Cmd(), s.Spinner.Tick) 65 + } 66 + 67 + func (s Status) Update(msg tea.Msg) (Status, tea.Cmd) { 68 + switch msg := msg.(type) { 69 + case StatusKind: 70 + s.Kind = msg 71 + s.Spinner.Spinner = s.Kind.Style() 72 + return s, nil 73 + case spinner.TickMsg: 74 + var cmd tea.Cmd 75 + s.Spinner, cmd = s.Spinner.Update(msg) 76 + return s, cmd 77 + } 78 + 79 + return s, nil 80 + } 81 + 82 + func (s Status) View() string { 83 + switch s.Kind { 84 + case Initializing: 85 + return fmt.Sprintf("initializing %s", s.Spinner.View()) 86 + case Success: 87 + return fmt.Sprintf("success") 88 + case Running: 89 + return fmt.Sprintf("running %s", s.Spinner.View()) 90 + case Error: 91 + return "error" 92 + default: 93 + return "error" 94 + } 95 + }