first commit

This commit is contained in:
2025-12-22 18:49:05 +01:00
commit 658ab8e961
19 changed files with 1356 additions and 0 deletions

57
widgets/clock.go Normal file
View File

@@ -0,0 +1,57 @@
package widgets
import (
"ArinDash/util"
"context"
"strings"
"time"
"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/segmentdisplay"
)
type ClockOptions struct {
}
func Clock() ClockOptions {
widgetOptions["ClockOptions"] = createClock
return ClockOptions{}
}
func createClock(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(segmentdisplay.New())
go util.Periodic(ctx, util.RedrawInterval, func() error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
now := time.Now()
nowStr := now.Format("15 04")
parts := strings.Split(nowStr, " ")
spacer := " "
if now.Second()%2 == 0 {
spacer = ":"
}
chunks := []*segmentdisplay.TextChunk{
segmentdisplay.NewChunk(parts[0], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorWhite))),
segmentdisplay.NewChunk(spacer),
segmentdisplay.NewChunk(parts[1], segmentdisplay.WriteCellOpts(cell.FgColor(cell.ColorWhite))),
}
if err := widget.Write(chunks, segmentdisplay.AlignHorizontal(align.HorizontalCenter), segmentdisplay.AlignVertical(align.VerticalBottom)); err != nil {
panic(err)
}
case <-ctx.Done():
return nil
}
}
})
return widget
}

97
widgets/dockerlist.go Normal file
View File

@@ -0,0 +1,97 @@
package widgets
import (
"ArinDash/apis/docker"
"ArinDash/util"
"context"
"fmt"
"sort"
"strings"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type DockerListOptions struct {
}
func DockerList() DockerListOptions {
widgetOptions["DockerListOptions"] = createDockerList
return DockerListOptions{}
}
func createDockerList(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
list := util.PanicOnErrorWithResult(text.New())
go util.Periodic(ctx, 1*time.Second, func() error {
ms, err := docker.FetchDockerMetrics(ctx)
if err != nil {
return nil
}
sort.Slice(ms, func(i, j int) bool { return ms[i].CPUPercent > ms[j].CPUPercent })
if err := list.Write(fmt.Sprintf("%-20s | %-6s | %-6s | %s\n", "Name", "CPU", "MEM", "Status"), text.WriteReplace()); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("─────────────────────┼────────┼────────┼─────────────────────\n")); err != nil {
return err
}
for _, m := range ms {
status := cell.ColorWhite
if strings.Contains(m.Status, "Exited") {
status = cell.ColorRed
}
if strings.Contains(m.Status, "unhealthy") {
status = cell.ColorYellow
}
if strings.Contains(m.Status, "Restarting") {
status = cell.ColorYellow
}
if strings.Contains(m.Status, "Paused") {
status = cell.ColorBlue
}
if err := list.Write(fmt.Sprintf("%-20s", m.Name), text.WriteCellOpts(cell.FgColor(status))); err != nil {
return err
}
if err := list.Write(fmt.Sprint(" | ")); err != nil {
return err
}
if err := writePercent(m.CPUPercent, list); err != nil {
return err
}
if err := list.Write(fmt.Sprint(" | ")); err != nil {
return err
}
if err := writePercent(m.MemPercent, list); err != nil {
return err
}
if err := list.Write(fmt.Sprint(" | ")); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("%s\n", m.Status), text.WriteCellOpts(cell.FgColor(status))); err != nil {
return err
}
}
return nil
})
return list
}
func writePercent(p float64, list *text.Text) error {
color := cell.ColorWhite
if p > 75 {
color = cell.ColorRed
}
if p > 50 {
color = cell.ColorYellow
}
if err := list.Write(fmt.Sprintf("%-5.1f%%", p), text.WriteCellOpts(cell.FgColor(color))); err != nil {
return err
}
return nil
}

48
widgets/piholestats.go Normal file
View File

@@ -0,0 +1,48 @@
package widgets
import (
"ArinDash/apis/pihole"
"ArinDash/util"
"context"
"fmt"
"time"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type PiholeStatsOptions struct {
}
func PiholeStats() PiholeStatsOptions {
widgetOptions["PiholeStatsOptions"] = createPiholeStats
return PiholeStatsOptions{}
}
func createPiholeStats(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
list := util.PanicOnErrorWithResult(text.New())
ph := pihole.Connect()
go util.Periodic(ctx, 1*time.Minute, func() error {
summary := ph.Summary()
if err := list.Write(fmt.Sprintf("Blocked Domains: %d\n", summary.Gravity.DomainsBeingBlocked), text.WriteReplace()); err != nil {
return err
}
if err := list.Write(fmt.Sprint("---------------------------\n")); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("Total Queries: %d\n", summary.Queries.Total)); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("Blocked Queries: %d\n", summary.Queries.Blocked)); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("Unique Domains: %d\n", summary.Queries.UniqueDomains)); err != nil {
return err
}
return nil
})
return list
}

54
widgets/qrcode.go Normal file
View File

@@ -0,0 +1,54 @@
package widgets
import (
"ArinDash/util"
"context"
"strings"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
"github.com/skip2/go-qrcode"
)
type QRCodeOptions struct {
data string
}
func QRCode(data string) QRCodeOptions {
widgetOptions["QRCodeOptions"] = createQRCode
return QRCodeOptions{
data: data,
}
}
func QrCodeASCII(data string) string {
bitmap := util.PanicOnErrorWithResult(qrcode.New(data, qrcode.Low)).Bitmap()
var stringBuilder strings.Builder
for y := range bitmap {
for x := range bitmap[y] {
if bitmap[y][x] {
stringBuilder.WriteString("██")
} else {
stringBuilder.WriteString(" ")
}
}
stringBuilder.WriteByte('\n')
}
return stringBuilder.String()
}
func createQRCode(_ context.Context, _ terminalapi.Terminal, opt interface{}) widgetapi.Widget {
options, ok := opt.(QRCodeOptions)
if !ok {
panic("invalid options type")
}
widget := util.PanicOnErrorWithResult(text.New())
util.PanicOnError(widget.Write(QrCodeASCII(options.data)))
return widget
}

112
widgets/segmentDisplay.go Normal file
View File

@@ -0,0 +1,112 @@
package widgets
import (
"ArinDash/util"
"context"
"image"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/segmentdisplay"
)
type RollingSegmentDisplayOptions struct {
UpdateText <-chan string
}
func RollingSegmentDisplay(options RollingSegmentDisplayOptions) RollingSegmentDisplayOptions {
widgetOptions["RollingSegmentDisplayOptions"] = createRollingSegmentDisplay
return RollingSegmentDisplayOptions{
UpdateText: options.UpdateText,
}
}
func createRollingSegmentDisplay(ctx context.Context, t terminalapi.Terminal, opt interface{}) widgetapi.Widget {
options, ok := opt.(RollingSegmentDisplayOptions)
if !ok {
panic("invalid options type")
}
segmentDisplayWidget := util.PanicOnErrorWithResult(segmentdisplay.New())
colors := []cell.Color{
cell.ColorNumber(33),
cell.ColorRed,
cell.ColorYellow,
cell.ColorNumber(33),
cell.ColorGreen,
cell.ColorRed,
cell.ColorGreen,
cell.ColorRed,
}
textVal := "ArinDash"
step := 0
go util.Periodic(ctx, util.RedrawInterval, func() error {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
capacity := 0
termSize := t.Size()
for {
select {
case <-ticker.C:
if capacity == 0 {
capacity = segmentDisplayWidget.Capacity()
}
if t.Size().Eq(image.Point{}) || !t.Size().Eq(termSize) {
termSize = t.Size()
capacity = segmentDisplayWidget.Capacity()
}
state := TextState(textVal, capacity, step)
var chunks []*segmentdisplay.TextChunk
for i := 0; i < capacity; i++ {
if i >= len(state) {
break
}
color := colors[i%len(colors)]
chunks = append(chunks, segmentdisplay.NewChunk(
string(state[i]),
segmentdisplay.WriteCellOpts(cell.FgColor(color)),
))
}
if len(chunks) == 0 {
continue
}
if err := segmentDisplayWidget.Write(chunks); err != nil {
panic(err)
}
step++
case t := <-options.UpdateText:
textVal = t
segmentDisplayWidget.Reset()
step = 0
case <-ctx.Done():
return nil
}
}
})
return segmentDisplayWidget
}
func TextState(text string, capacity, step int) []rune {
if capacity == 0 {
return nil
}
var state []rune
for i := 0; i < capacity; i++ {
state = append(state, ' ')
}
state = append(state, []rune(text)...)
step = step % len(state)
return append(state[step:], state[:step]...)
}

55
widgets/textInput.go Normal file
View File

@@ -0,0 +1,55 @@
package widgets
import (
"ArinDash/util"
"context"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/textinput"
)
type TextInputOptions struct {
updateText chan<- string
options []textinput.Option
}
var defaultOptions = []textinput.Option{
textinput.Label("Enter Text", cell.FgColor(cell.ColorNumber(33))),
textinput.MaxWidthCells(20),
textinput.PlaceHolder("enter any text"),
}
func TextInput(updateText chan<- string, options ...textinput.Option) TextInputOptions {
widgetOptions["TextInputOptions"] = createTextInput
return TextInputOptions{
options: createOptions(updateText, options),
}
}
func createOptions(updateText chan<- string, options []textinput.Option) []textinput.Option {
result := defaultOptions
if options != nil && len(options) != 0 {
for _, option := range options {
result = append(result, option)
}
}
result = append(result, textinput.ClearOnSubmit())
result = append(result, textinput.OnSubmit(func(text string) error {
updateText <- text
return nil
}))
return result
}
func createTextInput(_ context.Context, _ terminalapi.Terminal, opt interface{}) widgetapi.Widget {
options, ok := opt.(TextInputOptions)
if !ok {
panic("invalid options type")
}
return util.PanicOnErrorWithResult(textinput.New(
options.options...,
))
}

38
widgets/widgets.go Normal file
View File

@@ -0,0 +1,38 @@
package widgets
import (
"context"
"fmt"
"reflect"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
var Get = make(map[string]widgetapi.Widget)
var widgetOptions = make(map[string]func(ctx context.Context, t terminalapi.Terminal, options any) widgetapi.Widget)
type Widget struct {
name string
options interface{}
}
func New(name string, options interface{}) Widget {
return Widget{name, options}
}
func Add(name string, widget widgetapi.Widget) {
Get[name] = widget
}
func Create(ctx context.Context, t terminalapi.Terminal, widgets ...Widget) {
for _, widget := range widgets {
typeName := reflect.TypeOf(widget.options).Name()
factory := widgetOptions[typeName]
if factory == nil {
panic(fmt.Errorf("%s: widget factory not found for type: %s", widget.name, typeName))
}
Add(widget.name, widgetOptions[reflect.TypeOf(widget.options).Name()](ctx, t, widget.options))
}
}

63
widgets/wifiqr.go Normal file
View File

@@ -0,0 +1,63 @@
package widgets
import (
"ArinDash/config"
"ArinDash/util"
"context"
"fmt"
"strings"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type configFile struct {
Wifi wifiConfig
}
type wifiConfig struct {
Auth string
SSID string
Password string
}
const template = "WIFI:T:%s;S:%s;P:%s;;"
type WifiQRCodeOptions struct {
}
func WifiQRCode() WifiQRCodeOptions {
widgetOptions["WifiQRCodeOptions"] = createWifiQRCode
return WifiQRCodeOptions{}
}
func createWifiQRCode(_ context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
cfg := &configFile{}
config.LoadConfig(cfg)
widget := util.PanicOnErrorWithResult(text.New())
util.PanicOnError(widget.Write(QrCodeASCII(fmt.Sprintf(template, cfg.Wifi.Auth, escapeSpecialChars(cfg.Wifi.SSID), escapeSpecialChars(cfg.Wifi.Password)))))
return widget
}
func escapeSpecialChars(s string) string {
return strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
s,
"\\", "\\\\",
),
"\"", "\\\"",
),
",", "\\,",
),
";", "\\;",
),
":", "\\:",
)
}