From 658ab8e96156b2b7c57e1239f8051f5ddde4440f Mon Sep 17 00:00:00 2001 From: Arindy Date: Mon, 22 Dec 2025 18:49:05 +0100 Subject: [PATCH] first commit --- .gitignore | 110 +++++++++++++++++++++++++++ apis/docker/docker.go | 146 +++++++++++++++++++++++++++++++++++ apis/pihole/info.go | 44 +++++++++++ apis/pihole/pihole.go | 100 ++++++++++++++++++++++++ apis/pihole/stats.go | 41 ++++++++++ config/config.go | 21 +++++ config_template.toml | 10 +++ go.mod | 48 ++++++++++++ go.sum | 156 ++++++++++++++++++++++++++++++++++++++ main.go | 103 +++++++++++++++++++++++++ util/util.go | 53 +++++++++++++ widgets/clock.go | 57 ++++++++++++++ widgets/dockerlist.go | 97 ++++++++++++++++++++++++ widgets/piholestats.go | 48 ++++++++++++ widgets/qrcode.go | 54 +++++++++++++ widgets/segmentDisplay.go | 112 +++++++++++++++++++++++++++ widgets/textInput.go | 55 ++++++++++++++ widgets/widgets.go | 38 ++++++++++ widgets/wifiqr.go | 63 +++++++++++++++ 19 files changed, 1356 insertions(+) create mode 100644 .gitignore create mode 100644 apis/docker/docker.go create mode 100644 apis/pihole/info.go create mode 100644 apis/pihole/pihole.go create mode 100644 apis/pihole/stats.go create mode 100644 config/config.go create mode 100644 config_template.toml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 util/util.go create mode 100644 widgets/clock.go create mode 100644 widgets/dockerlist.go create mode 100644 widgets/piholestats.go create mode 100644 widgets/qrcode.go create mode 100644 widgets/segmentDisplay.go create mode 100644 widgets/textInput.go create mode 100644 widgets/widgets.go create mode 100644 widgets/wifiqr.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de1a653 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +*.idea + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +*.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +config.toml +old diff --git a/apis/docker/docker.go b/apis/docker/docker.go new file mode 100644 index 0000000..1d1b38f --- /dev/null +++ b/apis/docker/docker.go @@ -0,0 +1,146 @@ +package docker + +import ( + "ArinDash/util" + "context" + "strings" + + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" +) + +type ContainerMetrics struct { + ID string + Name string + Image string + Status string + CPUPercent float64 + MemUsage uint64 + MemLimit uint64 + MemPercent float64 + NetRx uint64 + NetTx uint64 + BlockRead uint64 + BlockWrite uint64 + PidsCurrent uint64 +} + +func newDockerClient() (*client.Client, error) { + return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) +} + +func FetchDockerMetrics(ctx context.Context) ([]ContainerMetrics, error) { + cli, err := newDockerClient() + if err != nil { + return nil, err + } + defer cli.Close() + + _, err = cli.Ping(ctx) + if err != nil { + return nil, err + } + + containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: true}) + if err != nil { + return nil, err + } + + out := make([]ContainerMetrics, 0, len(containers)) + for _, c := range containers { + m := ContainerMetrics{ + ID: c.ID, + Image: c.Image, + Name: containerName(c.Names), + Status: c.Status, + } + + stats, err := cli.ContainerStats(ctx, c.ID, false) + if err != nil { + continue + } + func() { + defer stats.Body.Close() + var sj containertypes.StatsResponse + if err := util.DecodeJSON(stats.Body, &sj); err != nil { + return + } + m.CPUPercent = cpuPercentFromStats(sj) + m.MemUsage, m.MemLimit, m.MemPercent = memoryFromStats(sj) + m.NetRx, m.NetTx = networkTotals(sj) + m.BlockRead, m.BlockWrite = blockIOTotals(sj) + m.PidsCurrent = sj.PidsStats.Current + }() + + out = append(out, m) + } + return out, nil +} + +func containerName(names []string) string { + for _, n := range names { + n = strings.TrimSpace(n) + n = strings.TrimPrefix(n, "/") + if n != "" { + return n + } + } + return "" +} + +func cpuPercentFromStats(s containertypes.StatsResponse) float64 { + // Protect against zero or missing data. + if s.PreCPUStats.SystemUsage == 0 || s.CPUStats.SystemUsage == 0 { + return 0 + } + cpuDelta := float64(s.CPUStats.CPUUsage.TotalUsage - s.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(s.CPUStats.SystemUsage - s.PreCPUStats.SystemUsage) + if systemDelta <= 0 || cpuDelta < 0 { + return 0 + } + // Number of CPUs available to the container. + numCPU := float64(len(s.CPUStats.CPUUsage.PercpuUsage)) + if numCPU == 0 { + numCPU = 1 + } + return (cpuDelta / systemDelta) * numCPU * 100.0 +} + +func memoryFromStats(s containertypes.StatsResponse) (usage, limit uint64, percent float64) { + usage = s.MemoryStats.Usage + limit = s.MemoryStats.Limit + // Optionally discount cached memory if stats present. + if stats := s.MemoryStats.Stats; stats != nil { + // The common Linux approach: usage - cache + if cache, ok := stats["cache"]; ok && cache <= usage { + usage -= cache + } + } + if limit > 0 { + percent = (float64(usage) / float64(limit)) * 100 + } + return +} + +func networkTotals(s containertypes.StatsResponse) (rx, tx uint64) { + if s.Networks == nil { + return 0, 0 + } + for _, v := range s.Networks { + rx += v.RxBytes + tx += v.TxBytes + } + return rx, tx +} + +func blockIOTotals(s containertypes.StatsResponse) (read, write uint64) { + for _, e := range s.BlkioStats.IoServiceBytesRecursive { + switch strings.ToLower(e.Op) { + case "read": + read += e.Value + case "write": + write += e.Value + } + } + return read, write +} diff --git a/apis/pihole/info.go b/apis/pihole/info.go new file mode 100644 index 0000000..dc8ee5a --- /dev/null +++ b/apis/pihole/info.go @@ -0,0 +1,44 @@ +package pihole + +import ( + "encoding/json" + "log" +) + +type Version struct { + Version Versions `json:"version"` +} + +type Versions struct { + Core ModuleVersion `json:"core"` + Web ModuleVersion `json:"web"` + FTL ModuleVersion `json:"FTL"` + Docker DockerVersion `json:"docker"` +} + +type ModuleVersion struct { + Local LocalVersion `json:"local"` + Remote LocalVersion `json:"remote"` +} + +type LocalVersion struct { + Branch string `json:"branch"` + Version string `json:"version"` + Hash string `json:"hash"` + Date string `json:"date"` +} + +type DockerVersion struct { + Local string `json:"local"` + Remote string `json:"remote"` +} + +func (ph *PiHConnector) Version() Version { + version := &Version{} + + err := json.Unmarshal(ph.get("info/version"), version) + if err != nil { + log.Fatal(err) + } + return *version +} diff --git a/apis/pihole/pihole.go b/apis/pihole/pihole.go new file mode 100644 index 0000000..93f3a65 --- /dev/null +++ b/apis/pihole/pihole.go @@ -0,0 +1,100 @@ +package pihole + +import ( + "ArinDash/config" + "encoding/json" + "io" + "log" + "net/http" + "strings" +) + +type configFile struct { + Pihole piholeConfig +} + +type piholeConfig struct { + Host string + Password string +} + +type PiHConnector struct { + Host string + Session PiHSession +} + +type PiHAuth struct { + Session PiHSession `json:"session"` +} + +type PiHSession struct { + SID string `json:"sid"` + CSRF string `json:"csrf"` +} + +func (ph *PiHConnector) get(endpoint string) []byte { + return ph.do("GET", endpoint, nil) +} + +func (ph *PiHConnector) post(endpoint string, body io.Reader) []byte { + return ph.do("POST", endpoint, body) +} + +func (ph *PiHConnector) do(method string, endpoint string, body io.Reader) []byte { + var requestString = "http://" + ph.Host + "/api/" + endpoint + + client := &http.Client{} + req, err := http.NewRequest(method, requestString, body) + + if err != nil { + log.Fatal(err) + } + + req.Header.Add("X-FTL-SID", ph.Session.SID) + req.Header.Add("X-FTL-CSRF", ph.Session.CSRF) + + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + return respBody +} + +func Connect() PiHConnector { + cfg := &configFile{} + config.LoadConfig(cfg) + client := &http.Client{} + req, err := http.NewRequest("POST", "http://"+cfg.Pihole.Host+"/api/auth", strings.NewReader("{\"password\": \""+cfg.Pihole.Password+"\"}")) + + if err != nil { + log.Fatal(err) + } + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + s := &PiHAuth{} + + err = json.Unmarshal(respBody, s) + if err != nil { + log.Fatal(err) + } + return PiHConnector{ + Host: cfg.Pihole.Host, + Session: s.Session, + } +} diff --git a/apis/pihole/stats.go b/apis/pihole/stats.go new file mode 100644 index 0000000..ddcde94 --- /dev/null +++ b/apis/pihole/stats.go @@ -0,0 +1,41 @@ +package pihole + +import ( + "encoding/json" + "log" +) + +type Summary struct { + Queries Queries `json:"queries"` + Clients Clients `json:"clients"` + Gravity Gravity `json:"gravity"` +} + +type Queries struct { + Total int64 `json:"total"` + Blocked int64 `json:"blocked"` + PercentBlocked float64 `json:"percent_blocked"` + UniqueDomains int64 `json:"unique_domains"` + Forwarded int64 `json:"forwarded"` + Cached int64 `json:"cached"` +} + +type Clients struct { + Active int64 `json:"active"` + Total int64 `json:"total"` +} + +type Gravity struct { + DomainsBeingBlocked int64 `json:"domains_being_blocked"` + LastUpdated int64 `json:"last_updated"` +} + +func (ph *PiHConnector) Summary() Summary { + summary := &Summary{} + + err := json.Unmarshal(ph.get("stats/summary"), summary) + if err != nil { + log.Fatal(err) + } + return *summary +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..676f28e --- /dev/null +++ b/config/config.go @@ -0,0 +1,21 @@ +package config + +import ( + "os" + + "github.com/pelletier/go-toml/v2" +) + +func LoadConfig(config interface{}) { + if err := toml.Unmarshal(readFile("config.toml"), config); err != nil { + panic(err) + } +} + +func readFile(path string) []byte { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + return data +} diff --git a/config_template.toml b/config_template.toml new file mode 100644 index 0000000..4e5df13 --- /dev/null +++ b/config_template.toml @@ -0,0 +1,10 @@ +#save this file as config.toml after configuring + +[pihole] +Host = "pi.hole" +Password = "generate-an-app-password-in-pi-hole" + +[wifi] +Auth = "WPA" +SSID = "YourSSID" +Password = "YourPassword" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8f3d189 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module ArinDash + +go 1.25.5 + +require ( + github.com/docker/docker v28.5.2+incompatible + github.com/mum4k/termdash v0.20.0 + github.com/pelletier/go-toml/v2 v2.2.4 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.7.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.14.0 // indirect + gotest.tools/v3 v3.5.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..75e65bb --- /dev/null +++ b/go.sum @@ -0,0 +1,156 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= +github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/mum4k/termdash v0.20.0 h1:g6yZvE7VJmuefJmDrSrv5Az8IFTTSCqG0x8xiOMPbyM= +github.com/mum4k/termdash v0.20.0/go.mod h1:/kPwGKcOhLawc2OmWJPLQ5nzR5PmcbiKMcVv9/413b4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7733724 --- /dev/null +++ b/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "ArinDash/util" + "ArinDash/widgets" + "context" + + "github.com/mum4k/termdash" + "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/container" + "github.com/mum4k/termdash/container/grid" + "github.com/mum4k/termdash/keyboard" + "github.com/mum4k/termdash/linestyle" + "github.com/mum4k/termdash/terminal/tcell" + "github.com/mum4k/termdash/terminal/terminalapi" + "github.com/mum4k/termdash/widgets/textinput" +) + +const rootID = "root" + +func quitter(cancel context.CancelFunc) func(k *terminalapi.Keyboard) { + quitter := func(k *terminalapi.Keyboard) { + if k.Key == keyboard.KeyEsc || k.Key == keyboard.KeyCtrlC { + cancel() + } + } + return quitter +} + +func initWidgets(ctx context.Context, term *tcell.Terminal) { + var titleUpdate = make(chan string) + + widgets.Create(ctx, term, + widgets.New("Clock", widgets.Clock()), + widgets.New("ChangeTitle", widgets.TextInput(titleUpdate, textinput.Label("Update Title: "), textinput.PlaceHolder("New Title"))), + widgets.New("Wifi", widgets.WifiQRCode()), + widgets.New("Docker", widgets.DockerList()), + widgets.New("PiHole", widgets.PiholeStats()), + ) +} + +func layout() []container.Option { + builder := grid.New() + builder.Add( + grid.ColWidthFixed(84, + grid.RowHeightFixed(44, + grid.Widget(widgets.Get["Wifi"], + container.BorderTitle("Wifi"), + container.Border(linestyle.Light), + container.BorderColor(cell.ColorWhite)), + ), + grid.RowHeightFixed(1, + grid.Widget(widgets.Get["empty"]), + ), + ), + grid.ColWidthPerc(20, + grid.RowHeightFixed(8, + grid.Widget(widgets.Get["Clock"], + container.BorderTitle("Time"), + container.Border(linestyle.Light), + container.BorderColor(cell.ColorWhite), + ), + ), + grid.RowHeightFixed(8, + grid.Widget(widgets.Get["PiHole"], + container.BorderTitle("pi-hole"), + container.Border(linestyle.Light), + container.BorderColor(cell.ColorWhite), + ), + ), + grid.RowHeightPerc(85, + grid.Widget(widgets.Get["empty"]), + ), + ), + grid.ColWidthPerc(35, + grid.RowHeightPerc(25, + grid.Widget(widgets.Get["empty"]), + ), + ), + grid.ColWidthPerc(35, + grid.RowHeightPerc(25, + grid.Widget(widgets.Get["Docker"], + container.BorderTitle("Docker"), + container.Border(linestyle.Light), + container.BorderColor(cell.ColorWhite), + ), + ), + ), + ) + return util.PanicOnErrorWithResult(builder.Build()) +} + +func main() { + term := util.PanicOnErrorWithResult(tcell.New(tcell.ColorMode(terminalapi.ColorMode256))) + defer term.Close() + ctx, cancel := context.WithCancel(context.Background()) + + initWidgets(ctx, term) + + rootContainer := util.PanicOnErrorWithResult(container.New(term, container.ID(rootID))) + util.PanicOnError(rootContainer.Update(rootID, layout()...)) + util.PanicOnError(termdash.Run(ctx, term, rootContainer, termdash.KeyboardSubscriber(quitter(cancel)), termdash.RedrawInterval(util.RedrawInterval))) +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..b720674 --- /dev/null +++ b/util/util.go @@ -0,0 +1,53 @@ +package util + +import ( + "context" + "encoding/json" + "fmt" + "io" + "time" +) + +const RedrawInterval = 250 * time.Millisecond + +func Periodic(ctx context.Context, interval time.Duration, fn func() error) { + if err := fn(); err != nil { + panic(err) + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := fn(); err != nil { + panic(err) + } + case <-ctx.Done(): + return + } + } +} + +func PanicOnErrorWithResult[E any](result E, err error) E { + if err != nil { + panic(err) + } + return result +} + +func PanicOnError(err error) { + if err != nil { + panic(err) + } +} + +func DecodeJSON(r io.Reader, v any) error { + dec := json.NewDecoder(r) + if err := dec.Decode(v); err != nil { + if err == io.EOF { + return fmt.Errorf("empty stats stream") + } + return err + } + return nil +} diff --git a/widgets/clock.go b/widgets/clock.go new file mode 100644 index 0000000..2694a30 --- /dev/null +++ b/widgets/clock.go @@ -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 +} diff --git a/widgets/dockerlist.go b/widgets/dockerlist.go new file mode 100644 index 0000000..4f9b2dc --- /dev/null +++ b/widgets/dockerlist.go @@ -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 +} diff --git a/widgets/piholestats.go b/widgets/piholestats.go new file mode 100644 index 0000000..23a6411 --- /dev/null +++ b/widgets/piholestats.go @@ -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 +} diff --git a/widgets/qrcode.go b/widgets/qrcode.go new file mode 100644 index 0000000..b7ced26 --- /dev/null +++ b/widgets/qrcode.go @@ -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 +} diff --git a/widgets/segmentDisplay.go b/widgets/segmentDisplay.go new file mode 100644 index 0000000..f52f14e --- /dev/null +++ b/widgets/segmentDisplay.go @@ -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]...) +} diff --git a/widgets/textInput.go b/widgets/textInput.go new file mode 100644 index 0000000..f25f46f --- /dev/null +++ b/widgets/textInput.go @@ -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..., + )) +} diff --git a/widgets/widgets.go b/widgets/widgets.go new file mode 100644 index 0000000..89c8b5a --- /dev/null +++ b/widgets/widgets.go @@ -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)) + } +} diff --git a/widgets/wifiqr.go b/widgets/wifiqr.go new file mode 100644 index 0000000..baf1d15 --- /dev/null +++ b/widgets/wifiqr.go @@ -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, + "\\", "\\\\", + ), + "\"", "\\\"", + ), + ",", "\\,", + ), + ";", "\\;", + ), + ":", "\\:", + ) +}