commit 8af1ff6ec13c0fca6908a2c7a98463238a671460 Author: Pit Friedrich Date: Sun Aug 9 18:06:06 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16c5af9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Binaries +/dashboard +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binaries and coverage +*.test +*.out +coverage.txt + +# Go workspace / vendor +/vendor/ +go.work +go.work.sum + +# Generated code (from tui generate) +dashboard_gsx.go + +# Editor/OS +.DS_Store +.idea/ +.vscode/ diff --git a/dashboard.gsx b/dashboard.gsx new file mode 100644 index 0000000..53c2cc6 --- /dev/null +++ b/dashboard.gsx @@ -0,0 +1,267 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + tui "github.com/grindlemire/go-tui" +) + +type dashboardApp struct { + cpu *tui.State[int] + mem *tui.State[int] + disk *tui.State[int] + netIn *tui.State[int] + netOut *tui.State[int] + sparkIn *tui.State[[]int] + sparkOut *tui.State[[]int] + events *tui.State[[]string] + eventCh <-chan string + scrollY *tui.State[int] + eventsRef *tui.Ref +} + +func Dashboard(eventCh <-chan string) *dashboardApp { + return &dashboardApp{ + cpu: tui.NewState(45), + mem: tui.NewState(62), + disk: tui.NewState(38), + netIn: tui.NewState(142), + netOut: tui.NewState(89), + sparkIn: tui.NewState([]int{3, 5, 4, 6, 7, 5, 4, 3, 5, 6, 7, 8, 6, 5, 4, 3, 5, 6, 7, 5}), + sparkOut: tui.NewState([]int{2, 3, 4, 3, 5, 4, 3, 2, 3, 4, 5, 6, 4, 3, 2, 3, 4, 5, 4, 3}), + events: tui.NewState([]string{}), + eventCh: eventCh, + scrollY: tui.NewState(0), + eventsRef: tui.NewRef(), + } +} + +func (d *dashboardApp) scrollBy(delta int) { + el := d.eventsRef.El() + if el == nil { + return + } + + _, maxY := el.MaxScroll() + newY := d.scrollY.Get() + delta + if newY < 0 { + newY = 0 + } else if newY > maxY { + newY = maxY + } + d.scrollY.Set(newY) +} + +func (d *dashboardApp) KeyMap() tui.KeyMap { + return tui.KeyMap{ + tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }), + tui.On(tui.Rune('q'), func(ke tui.KeyEvent) { ke.App().Stop() }), + tui.On(tui.Rune('j'), func(ke tui.KeyEvent) { d.scrollBy(1) }), + tui.On(tui.Rune('k'), func(ke tui.KeyEvent) { d.scrollBy(-1) }), + tui.On(tui.KeyDown, func(ke tui.KeyEvent) { d.scrollBy(1) }), + tui.On(tui.KeyUp, func(ke tui.KeyEvent) { d.scrollBy(-1) }), + } +} + +func (d *dashboardApp) HandleMouse(me tui.MouseEvent) bool { + switch me.Button { + case tui.MouseWheelUp: + d.scrollBy(-1) + return true + case tui.MouseWheelDown: + d.scrollBy(1) + return true + } + return false +} + +func (d *dashboardApp) Watchers() []tui.Watcher { + return []tui.Watcher{ + tui.OnTimer(500*time.Millisecond, d.updateMetrics), + tui.Watch(d.eventCh, d.addEvent), + } +} + +func (d *dashboardApp) updateMetrics() { + d.cpu.Set(clampVal(d.cpu.Get()+rand.Intn(11)-5, 5, 95)) + d.mem.Set(clampVal(d.mem.Get()+rand.Intn(7)-3, 20, 90)) + d.disk.Set(clampVal(d.disk.Get()+rand.Intn(3)-1, 20, 80)) + d.netIn.Set(clampVal(d.netIn.Get()+rand.Intn(41)-20, 50, 300)) + d.netOut.Set(clampVal(d.netOut.Get()+rand.Intn(31)-15, 30, 200)) + + inData := d.sparkIn.Get() + inData = append(inData[1:], d.netIn.Get()/30) + d.sparkIn.Set(inData) + + outData := d.sparkOut.Get() + outData = append(outData[1:], d.netOut.Get()/30) + d.sparkOut.Set(outData) +} + +func (d *dashboardApp) addEvent(event string) { + current := d.events.Get() + ts := time.Now().Format("15:04:05") + entry := fmt.Sprintf("%s %s", ts, event) + current = append(current, entry) + if len(current) > 50 { + current = current[len(current)-50:] + } + d.events.Set(current) + + // Auto-scroll to bottom + el := d.eventsRef.El() + if el != nil { + _, maxY := el.MaxScroll() + d.scrollY.Set(maxY + 1) + } +} + +func clampVal(v, min, max int) int { + if v < min { + return min + } + if v > max { + return max + } + return v +} + +func metricBar(value, max int) string { + width := 20 + filled := value * width / max + bar := "" + for i := 0; i < width; i++ { + if i < filled { + bar += "█" + } else { + bar += "░" + } + } + return bar +} + +func metricColor(value int) string { + if value >= 80 { + return "text-red font-bold" + } + if value >= 60 { + return "text-yellow" + } + return "text-green" +} + +func sparkline(data []int) string { + blocks := []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + maxVal := 1 + for _, v := range data { + if v > maxVal { + maxVal = v + } + } + s := "" + for _, v := range data { + idx := v * 7 / maxVal + if idx > 7 { + idx = 7 + } + s += string(blocks[idx]) + } + return s +} + +func produceEvents(ch chan<- string, stopCh <-chan struct{}) { + defer close(ch) + events := []string{ + "Deploy completed", + "Health check passed", + "New connection from 10.0.0.5", + "Cache invalidated", + "Backup complete", + "Certificate renewed", + "Config reloaded", + "Scale up: 3 replicas", + "Alert cleared: cpu", + "Metrics exported", + } + for { + delay := time.Duration(2000+rand.Intn(3000)) * time.Millisecond + select { + case <-stopCh: + return + case <-time.After(delay): + } + event := events[rand.Intn(len(events))] + select { + case <-stopCh: + return + case ch <- event: + } + } +} + +templ (d *dashboardApp) Render() { +
+
+ Dashboard +
+ + // Metric gauges +
+
+ CPU + {metricBar(d.cpu.Get(), 100)} + {fmt.Sprintf("%d%%", d.cpu.Get())} +
+
+ Memory + {metricBar(d.mem.Get(), 100)} + {fmt.Sprintf("%d%%", d.mem.Get())} +
+
+ Disk + {metricBar(d.disk.Get(), 100)} + {fmt.Sprintf("%d%%", d.disk.Get())} +
+
+ + // Network Traffic + Recent Events +
+
+ Network Traffic +
+ In: + {sparkline(d.sparkIn.Get())} +
+
+ Out: + {sparkline(d.sparkOut.Get())} +
+
+ {fmt.Sprintf("In: %d MB/s", d.netIn.Get())} + {fmt.Sprintf("Out: %d MB/s", d.netOut.Get())} +
+
+ +
+ Recent Events + for _, event := range d.events.Get() { + {event} + } + if len(d.events.Get()) == 0 { + Waiting for events... + } +
+
+ +
+ j/k scroll events | q to quit +
+
+} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6395e0e --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module dashboard + +go 1.26.5 + +require ( + github.com/grindlemire/go-tui v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dba9aa6 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/grindlemire/go-tui v0.19.0 h1:4k4MRoYUFChUCfO5Lr4Z05SI3jlTSuH3W1H5/HKVH8s= +github.com/grindlemire/go-tui v0.19.0/go.mod h1:95oaPy6Iu6MZpiByUqxYz2Wcbckzvaw3K0jgQeI4dD0= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ad70451 --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + + tui "github.com/grindlemire/go-tui" +) + +func main() { + eventCh := make(chan string, 100) + + app, err := tui.NewApp( + tui.WithRootComponent(Dashboard(eventCh)), + tui.WithMouse(), + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create app: %v\n", err) + os.Exit(1) + } + defer app.Close() + + go produceEvents(eventCh, app.StopCh()) + + if err := app.Run(); err != nil { + fmt.Fprintf(os.Stderr, "App error: %v\n", err) + os.Exit(1) + } +}