first commit

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

110
.gitignore vendored Normal file
View File

@ -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

146
apis/docker/docker.go Normal file
View File

@ -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
}

44
apis/pihole/info.go Normal file
View File

@ -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
}

100
apis/pihole/pihole.go Normal file
View File

@ -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,
}
}

41
apis/pihole/stats.go Normal file
View File

@ -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
}

21
config/config.go Normal file
View File

@ -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
}

10
config_template.toml Normal file
View File

@ -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"

48
go.mod Normal file
View File

@ -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
)

156
go.sum Normal file
View File

@ -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=

103
main.go Normal file
View File

@ -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)))
}

53
util/util.go Normal file
View File

@ -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
}

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,
"\\", "\\\\",
),
"\"", "\\\"",
),
",", "\\,",
),
";", "\\;",
),
":", "\\:",
)
}