commit 740ed3793aca68b7078422edf4e3b3a3d337f50c
parent 0843da2f58feeff6fd5e8121794a31ed8cd66de2
Author: dwrz <dwrz@dwrz.net>
Date: Tue, 14 Oct 2025 16:34:15 +0000
Refactor statusbar to be configurable
Diffstat:
8 files changed, 130 insertions(+), 80 deletions(-)
diff --git a/cmd/statusbar/main.go b/cmd/statusbar/main.go
@@ -8,20 +8,9 @@ import (
"path/filepath"
"time"
+ "code.dwrz.net/src/cmd/statusbar/config"
"code.dwrz.net/src/pkg/log"
"code.dwrz.net/src/pkg/statusbar"
- "code.dwrz.net/src/pkg/statusbar/cpu"
- "code.dwrz.net/src/pkg/statusbar/datetime"
- "code.dwrz.net/src/pkg/statusbar/disk"
- "code.dwrz.net/src/pkg/statusbar/eth"
- "code.dwrz.net/src/pkg/statusbar/light"
- "code.dwrz.net/src/pkg/statusbar/memory"
- "code.dwrz.net/src/pkg/statusbar/mic"
- "code.dwrz.net/src/pkg/statusbar/power"
- "code.dwrz.net/src/pkg/statusbar/temp"
- "code.dwrz.net/src/pkg/statusbar/volume"
- "code.dwrz.net/src/pkg/statusbar/wlan"
- "code.dwrz.net/src/pkg/statusbar/wwan"
)
var once = flag.Bool("once", false, "print output once")
@@ -54,37 +43,15 @@ func main() {
}
defer flog.Close()
- // Prepare the datetime format.
- now := time.Now()
- yearEnd := time.Date(
- now.Year()+1, 1, 1, 0, 0, 0, 0, time.UTC,
- ).AddDate(0, 0, -1)
-
- dfmt := fmt.Sprintf(
- "+%%Y-%%m-%%d %%u/7 %%W/52 %%j/%d %%H:%%M %%Z",
- yearEnd.YearDay(),
- )
-
+ // Load the config and create the blocks.
+ cfg, blocks, err := config.Load()
+ if err != nil {
+ l.Error.Fatalf("failed to load config: %v", err)
+ }
var bar = statusbar.New(statusbar.Parameters{
- Blocks: []statusbar.Block{
- cpu.New(),
- temp.New(),
- memory.New(),
- disk.New("/", "/home"),
- eth.New("eth0"),
- wlan.New(),
- wwan.New("wwan0"),
- power.New(power.Path),
- light.New(),
- volume.New(),
- mic.New(),
- datetime.New(datetime.Parameters{
- Format: dfmt,
- Timezone: "UTC",
- }),
- },
+ Blocks: blocks,
Log: log.New(flog),
- Separator: "┃",
+ Separator: cfg.Separator,
})
// Initial print.
@@ -95,12 +62,9 @@ func main() {
// Main loop.
ticker := time.NewTicker(1 * time.Second)
- for {
- select {
- case <-ticker.C:
- // now := time.Now()
- fmt.Println(bar.Render(ctx))
- // fmt.Println(time.Since(now))
- }
+ for range ticker.C {
+ // now := time.Now()
+ fmt.Println(bar.Render(ctx))
+ // fmt.Println(time.Since(now))
}
}
diff --git a/pkg/statusbar/datetime/datetime.go b/pkg/statusbar/datetime/datetime.go
@@ -13,13 +13,13 @@ type Block struct {
timezone string
}
-type Parameters struct {
- Format string
- Label string
- Timezone string
+type Config struct {
+ Format string `json:"format"`
+ Label string `json:"label"`
+ Timezone string `json:"timezone"`
}
-func New(p Parameters) *Block {
+func New(p Config) *Block {
if p.Timezone == "" {
p.Timezone = "UTC"
}
diff --git a/pkg/statusbar/disk/disk.go b/pkg/statusbar/disk/disk.go
@@ -7,6 +7,11 @@ import (
"strings"
)
+// Config holds the configuration for the disk block.
+type Config struct {
+ Mounts []string `json:"mounts"`
+}
+
type Block struct {
mounts []string
}
diff --git a/pkg/statusbar/eth/eth.go b/pkg/statusbar/eth/eth.go
@@ -6,6 +6,10 @@ import (
"net"
)
+type Config struct {
+ Iface string `json:"iface"`
+}
+
type Block struct {
iface string
}
@@ -22,7 +26,7 @@ func (b *Block) Render(ctx context.Context) (string, error) {
iface, err := net.InterfaceByName(b.iface)
if err != nil {
if err.Error() == "route ip+net: no such network interface" {
- return fmt.Sprintf(" "), nil
+ return " ", nil
}
return "", fmt.Errorf(
"failed to get interface %s: %v", b.iface, err,
@@ -47,7 +51,7 @@ func (b *Block) Render(ctx context.Context) (string, error) {
switch {
case ip4 == "" && ip6 == "":
- return fmt.Sprintf(" "), nil
+ return " ", nil
case ip4 != "" && ip6 == "":
return fmt.Sprintf(" %s", ip4), nil
case ip4 == "" && ip6 != "":
diff --git a/pkg/statusbar/power/power.go b/pkg/statusbar/power/power.go
@@ -11,6 +11,10 @@ import (
"strings"
)
+type Config struct {
+ Path string `json:"path"`
+}
+
// https://www.kernel.org/doc/html/latest/power/power_supply_class.html
const Path = "/sys/class/power_supply"
diff --git a/pkg/statusbar/temp/temp.go b/pkg/statusbar/temp/temp.go
@@ -5,12 +5,26 @@ import (
"encoding/json"
"fmt"
"os/exec"
+ "strings"
)
-type Block struct{}
+// Sensor describes a single temperature sensor to be read.
+type Sensor struct {
+ Label string `json:"label"`
+ Path []string `json:"path"`
+}
+
+// Config holds the configuration for the temp block.
+type Config struct {
+ Sensors []Sensor `json:"sensors"`
+}
+
+type Block struct {
+ config Config
+}
-func New() *Block {
- return &Block{}
+func New(c Config) *Block {
+ return &Block{config: c}
}
func (b *Block) Name() string {
@@ -22,25 +36,73 @@ func (b *Block) Render(ctx context.Context) (string, error) {
if err != nil {
return "", fmt.Errorf("exec sensors failed: %v", err)
}
+ if len(b.config.Sensors) == 0 {
+ return "", nil
+ }
+
+ var parts = make([]string, 0, len(b.config.Sensors))
+ for _, sensor := range b.config.Sensors {
+ temp, err := parse(out, sensor.Path)
+ if err != nil {
+ return "", fmt.Errorf(
+ "failed to get temp for %v: %w",
+ sensor.Path, err,
+ )
+ }
+
+ var renderedSensor string
+ if sensor.Label != "" {
+ renderedSensor = fmt.Sprintf(
+ "%s %.0f℃", sensor.Label, temp,
+ )
+ } else {
+ renderedSensor = fmt.Sprintf("%.0f℃", temp)
+ }
+ parts = append(parts, renderedSensor)
+ }
+
+ return strings.Join(parts, " "), nil
+}
+
+// parse traverses the json data using the given path to find a temperature
+// value.
+func parse(data []byte, path []string) (float64, error) {
+ if len(path) == 0 {
+ return 0, fmt.Errorf("sensor path cannot be empty")
+ }
+
+ // We start with the full JSON output.
+ var current = json.RawMessage(data)
+
+ // For each key in the path, we dive one level deeper into the JSON
+ // structure.
+ for i, key := range path {
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(current, &m); err != nil {
+ return 0, fmt.Errorf(
+ "failed to unmarshal object at path segment %d ('%s'): %w",
+ i, key, err,
+ )
+ }
+
+ var ok bool
+ current, ok = m[key]
+ if !ok {
+ return 0, fmt.Errorf(
+ "key not found at path segment %d ('%s')",
+ i, key,
+ )
+ }
+ }
- var sensors = struct {
- Thinkpad struct {
- CPU struct {
- Temp float64 `json:"temp1_input"`
- } `json:"CPU"`
- } `json:"thinkpad-isa-0000"`
- NVME struct {
- Composite struct {
- Temp float64 `json:"temp1_input"`
- } `json:"Composite"`
- } `json:"nvme-pci-0400"`
- }{}
- if err := json.Unmarshal(out, &sensors); err != nil {
- return "", fmt.Errorf("failed to json unmarshal: %v", err)
+ // After the loop, 'current' holds the raw JSON for the temperature
+ // value.
+ var temp float64
+ if err := json.Unmarshal(current, &temp); err != nil {
+ return 0, fmt.Errorf(
+ "final value at path %v is not a number: %w", path, err,
+ )
}
- return fmt.Sprintf(
- " %.0f℃ %.0f℃",
- sensors.Thinkpad.CPU.Temp, sensors.NVME.Composite.Temp,
- ), nil
+ return temp, nil
}
diff --git a/pkg/statusbar/wlan/wlan.go b/pkg/statusbar/wlan/wlan.go
@@ -9,10 +9,17 @@ import (
"strings"
)
-type Block struct{}
+// Config holds the configuration for the wlan block.
+type Config struct {
+ Iface string `json:"iface"`
+}
+
+type Block struct {
+ iface string
+}
-func New() *Block {
- return &Block{}
+func New(iface string) *Block {
+ return &Block{iface: iface}
}
func (b *Block) Name() string {
@@ -22,7 +29,7 @@ func (b *Block) Name() string {
// TODO: signal strength icon.
func (b *Block) Render(ctx context.Context) (string, error) {
out, err := exec.CommandContext(
- ctx, "iwctl", "station", "wlan0", "show",
+ ctx, "iwctl", "station", b.iface, "show",
).Output()
if err != nil {
return "", fmt.Errorf("exec iwctl failed: %v", err)
diff --git a/pkg/statusbar/wwan/wwan.go b/pkg/statusbar/wwan/wwan.go
@@ -11,6 +11,10 @@ import (
"strings"
)
+type Config struct {
+ Iface string `json:"iface"`
+}
+
type Block struct {
iface string
}