Add statusbar packages
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package cpu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const path = "/proc/loadavg"
|
||||
|
||||
type Block struct{}
|
||||
|
||||
func New() *Block {
|
||||
return &Block{}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "cpu"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read %s: %v", path, err)
|
||||
}
|
||||
|
||||
if fields := strings.Fields(string(out)); len(fields) >= 1 {
|
||||
return fmt.Sprintf(" %s/%d", fields[0], runtime.NumCPU()), nil
|
||||
} else {
|
||||
return fmt.Sprintf(" /%d", runtime.NumCPU()), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package datetime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Block 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 Config) *Block {
|
||||
if p.Timezone == "" {
|
||||
p.Timezone = "UTC"
|
||||
}
|
||||
|
||||
return &Block{
|
||||
format: p.Format,
|
||||
label: p.Label,
|
||||
timezone: p.Timezone,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "datetime"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "date", b.format)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", b.timezone))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to exec: %v", err)
|
||||
}
|
||||
|
||||
if b.label != "" {
|
||||
return fmt.Sprintf(
|
||||
"%s %s", b.label, strings.TrimSpace(string(out)),
|
||||
), nil
|
||||
} else {
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config holds the configuration for the disk block.
|
||||
type Config struct {
|
||||
Mounts []string `json:"mounts"`
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
mounts []string
|
||||
}
|
||||
|
||||
func New(mounts ...string) *Block {
|
||||
return &Block{mounts: mounts}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "disk"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(ctx, "df", "-h").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec df failed: %v", err)
|
||||
}
|
||||
|
||||
var mounts = map[string]string{}
|
||||
for i, l := range strings.Split(string(out), "\n") {
|
||||
if i == 0 || len(l) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(l)
|
||||
if len(fields) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
var icon rune = ''
|
||||
switch fields[5] {
|
||||
case "/":
|
||||
icon = '/'
|
||||
case "/home":
|
||||
icon = ''
|
||||
}
|
||||
|
||||
mounts[fields[5]] = fmt.Sprintf("%c %s", icon, fields[4])
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
for _, m := range b.mounts {
|
||||
if s, exists := mounts[m]; exists {
|
||||
if output.Len() > 0 {
|
||||
output.WriteRune(' ')
|
||||
}
|
||||
output.WriteString(s)
|
||||
}
|
||||
}
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package eth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Iface string `json:"iface"`
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
iface string
|
||||
}
|
||||
|
||||
func New(iface string) *Block {
|
||||
return &Block{iface: iface}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "eth"
|
||||
}
|
||||
|
||||
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 " ", nil
|
||||
}
|
||||
return "", fmt.Errorf(
|
||||
"failed to get interface %s: %v", b.iface, err,
|
||||
)
|
||||
}
|
||||
|
||||
var ip4, ip6 string
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get addresses: %v", err)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
a := addr.String()
|
||||
if isIPv4(a) {
|
||||
ip4 = a
|
||||
continue
|
||||
} else {
|
||||
ip6 = a
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case ip4 == "" && ip6 == "":
|
||||
return " ", nil
|
||||
case ip4 != "" && ip6 == "":
|
||||
return fmt.Sprintf(" %s", ip4), nil
|
||||
case ip4 == "" && ip6 != "":
|
||||
return fmt.Sprintf(" %s", ip6), nil
|
||||
default:
|
||||
return fmt.Sprintf(" %s %s", ip4, ip6), nil
|
||||
}
|
||||
}
|
||||
|
||||
func isIPv4(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '.':
|
||||
return true
|
||||
case ':':
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package light
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Block struct{}
|
||||
|
||||
func New() *Block {
|
||||
return &Block{}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "light"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(ctx, "brightnessctl", "-m").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to exec: %v", err)
|
||||
}
|
||||
|
||||
if fields := strings.Split(string(out), ","); len(fields) >= 4 {
|
||||
return fmt.Sprintf(" %s", fields[3]), nil
|
||||
} else {
|
||||
return " ", nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Block struct{}
|
||||
|
||||
func New() *Block {
|
||||
return &Block{}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "memory"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
var output strings.Builder
|
||||
|
||||
out, err := exec.CommandContext(ctx, "free", "-m").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to exec: %v", err)
|
||||
}
|
||||
|
||||
for l := range strings.SplitSeq(string(out), "\n") {
|
||||
if len(l) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(l)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch fields[0] {
|
||||
case "Mem:":
|
||||
total, err := strconv.ParseFloat(fields[1], 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to parse total memory: %v", err,
|
||||
)
|
||||
}
|
||||
used, err := strconv.ParseFloat(fields[2], 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to parse used memory: %v", err,
|
||||
)
|
||||
}
|
||||
if total > 0 {
|
||||
fmt.Fprintf(&output, " %.0f%%", (used/total)*100)
|
||||
}
|
||||
case "Swap:":
|
||||
total, err := strconv.ParseFloat(fields[1], 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to parse total memory: %v", err,
|
||||
)
|
||||
}
|
||||
used, err := strconv.ParseFloat(fields[2], 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to parse used memory: %v", err,
|
||||
)
|
||||
}
|
||||
if total > 0 {
|
||||
if output.Len() > 0 {
|
||||
output.WriteRune(' ')
|
||||
}
|
||||
fmt.Fprintf(&output, " %.0f%%", (used/total)*100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package mic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Block struct{}
|
||||
|
||||
func New() *Block {
|
||||
return &Block{}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "mic"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(
|
||||
ctx, "pactl", "get-source-mute", "@DEFAULT_SOURCE@",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec pactl failed: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(string(out), "yes") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return " OPEN", nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package mlight
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// VCPBrightnessCode is the VCP code for monitor brightness.
|
||||
VCPBrightnessCode = "10"
|
||||
)
|
||||
|
||||
// Block implements the statusbar.Block interface for monitor brightness.
|
||||
type Block struct{}
|
||||
|
||||
// New returns a new mlight Block.
|
||||
func New() *Block {
|
||||
return &Block{}
|
||||
}
|
||||
|
||||
// Name returns the name of the block.
|
||||
func (b *Block) Name() string {
|
||||
return "mlight"
|
||||
}
|
||||
|
||||
// Render displays the current monitor brightness.
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(
|
||||
ctx,
|
||||
"ddcutil",
|
||||
"--terse",
|
||||
"getvcp",
|
||||
VCPBrightnessCode,
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to exec ddcutil: %v", err)
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(out))
|
||||
if len(fields) < 4 {
|
||||
return " ", nil
|
||||
}
|
||||
|
||||
val, err := strconv.Atoi(fields[3])
|
||||
if err != nil {
|
||||
return " ", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" %d%%", val), nil
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package power
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"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"
|
||||
|
||||
type Block struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func New(path string) *Block {
|
||||
return &Block{path: path}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "power"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
// Get the power supplies.
|
||||
var supplies []string
|
||||
|
||||
files, err := os.ReadDir(b.path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read dir %s: %v", b.path, err)
|
||||
}
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
if name == "AC" || strings.Contains(name, "BAT") {
|
||||
supplies = append(supplies, f.Name())
|
||||
}
|
||||
}
|
||||
slices.Sort(supplies)
|
||||
|
||||
var sections = []string{}
|
||||
for _, s := range supplies {
|
||||
path := fmt.Sprintf("%s/%s/uevent", b.path, s)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to open %s: %v", path, err,
|
||||
)
|
||||
}
|
||||
|
||||
switch {
|
||||
case s == "AC":
|
||||
sections = append(sections, outputAC(f))
|
||||
case strings.Contains(s, "BAT"):
|
||||
sections = append(sections, outputBAT(f))
|
||||
}
|
||||
|
||||
f.Close()
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
for i, s := range sections {
|
||||
if i > 0 {
|
||||
output.WriteRune(' ')
|
||||
output.WriteRune(' ')
|
||||
}
|
||||
output.WriteString(s)
|
||||
}
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
func outputAC(f *os.File) string {
|
||||
var scanner = bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
k, v, found := strings.Cut(scanner.Text(), "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if k == "POWER_SUPPLY_ONLINE" {
|
||||
online, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if online {
|
||||
return ""
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func outputBAT(f *os.File) string {
|
||||
// Compile the stats.
|
||||
var (
|
||||
stats = map[string]string{}
|
||||
scanner = bufio.NewScanner(f)
|
||||
)
|
||||
for scanner.Scan() {
|
||||
k, v, found := strings.Cut(scanner.Text(), "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
// Exit early if the battery is not present.
|
||||
if k == "POWER_SUPPLY_PRESENT" && v == "0" {
|
||||
return ""
|
||||
}
|
||||
|
||||
stats[k] = v
|
||||
}
|
||||
|
||||
// Assemble output.
|
||||
var icon rune
|
||||
switch stats["POWER_SUPPLY_STATUS"] {
|
||||
case "Charging":
|
||||
icon = ''
|
||||
case "Discharging":
|
||||
icon = ''
|
||||
case "Full":
|
||||
icon = ''
|
||||
default:
|
||||
icon = ''
|
||||
}
|
||||
|
||||
// Get capacity.
|
||||
var capacity string
|
||||
if s, exists := stats["POWER_SUPPLY_CAPACITY"]; exists {
|
||||
capacity = s + "%"
|
||||
}
|
||||
|
||||
// Get remaining.
|
||||
pn, _ := strconv.ParseFloat(stats["POWER_SUPPLY_POWER_NOW"], 64)
|
||||
en, _ := strconv.ParseFloat(stats["POWER_SUPPLY_ENERGY_NOW"], 64)
|
||||
if r := remaining(en, pn); r != "" {
|
||||
return fmt.Sprintf("%c %s %s", icon, capacity, r)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%c %s", icon, capacity)
|
||||
}
|
||||
|
||||
func remaining(energy, power float64) string {
|
||||
if energy == 0 || power == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Calculate the remaining hours.
|
||||
var hours = energy / power
|
||||
if hours == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%d:%02d",
|
||||
int(hours),
|
||||
int((hours-math.Floor(hours))*60),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package statusbar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Block interface {
|
||||
Name() string
|
||||
Render(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type Parameters struct {
|
||||
Blocks []Block
|
||||
Log *slog.Logger
|
||||
Separator string
|
||||
}
|
||||
|
||||
type StatusBar struct {
|
||||
b *strings.Builder
|
||||
blocks []Block
|
||||
l *slog.Logger
|
||||
sep string
|
||||
}
|
||||
|
||||
func (s *StatusBar) Render(ctx context.Context) string {
|
||||
defer s.b.Reset()
|
||||
|
||||
fmt.Fprintf(s.b, "%s ", s.sep)
|
||||
|
||||
var (
|
||||
timeout, cancel = context.WithTimeout(ctx, time.Second)
|
||||
outputs = make([]string, len(s.blocks))
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
wg.Add(len(s.blocks))
|
||||
|
||||
for i, b := range s.blocks {
|
||||
go func(i int, b Block) {
|
||||
defer wg.Done()
|
||||
|
||||
text, err := b.Render(timeout)
|
||||
if err != nil {
|
||||
s.l.ErrorContext(
|
||||
timeout,
|
||||
"failed to render block",
|
||||
slog.String("name", b.Name()),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
outputs[i] = ""
|
||||
} else {
|
||||
outputs[i] = text
|
||||
}
|
||||
}(i, b)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for i, o := range outputs {
|
||||
s.b.WriteString(o)
|
||||
|
||||
if i < len(outputs)-1 {
|
||||
fmt.Fprintf(s.b, " %s ", s.sep)
|
||||
}
|
||||
}
|
||||
|
||||
return s.b.String()
|
||||
}
|
||||
|
||||
func New(p Parameters) *StatusBar {
|
||||
return &StatusBar{
|
||||
b: &strings.Builder{},
|
||||
blocks: p.Blocks,
|
||||
l: p.Log,
|
||||
sep: p.Separator,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package temp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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(c Config) *Block {
|
||||
return &Block{config: c}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "temp"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(ctx, "sensors", "-j").Output()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 temp, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Block struct{}
|
||||
|
||||
func New() *Block {
|
||||
return &Block{}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "volume"
|
||||
}
|
||||
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(
|
||||
ctx, "pactl", "get-sink-mute", "@DEFAULT_SINK@",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec pactl failed: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(string(out), "yes") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
out, err = exec.CommandContext(
|
||||
ctx, "pactl", "get-sink-volume", "@DEFAULT_SINK@",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec pactl failed: %v", err)
|
||||
}
|
||||
|
||||
if fields := strings.Fields(string(out)); len(fields) < 5 {
|
||||
return " ", nil
|
||||
} else {
|
||||
return fmt.Sprintf(" %s", fields[4]), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package wlan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config holds the configuration for the wlan block.
|
||||
type Config struct {
|
||||
Iface string `json:"iface"`
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
iface string
|
||||
}
|
||||
|
||||
func New(iface string) *Block {
|
||||
return &Block{iface: iface}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "wlan"
|
||||
}
|
||||
|
||||
// TODO: signal strength icon.
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(
|
||||
ctx, "iwctl", "station", b.iface, "show",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec iwctl failed: %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
scanner = bufio.NewScanner(bytes.NewReader(out))
|
||||
|
||||
state, network, ip, rssi string
|
||||
)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case fields[0] == "State":
|
||||
state = fields[1]
|
||||
case fields[0] == "Connected":
|
||||
network = fields[2]
|
||||
case fields[0] == "IPv4":
|
||||
ip = fields[2]
|
||||
case fields[0] == "RSSI":
|
||||
rssi = fields[1] + fields[2]
|
||||
}
|
||||
}
|
||||
|
||||
if state == "disconnected" {
|
||||
return " ", nil
|
||||
}
|
||||
return fmt.Sprintf(" %s %s %s", network, ip, rssi), nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package wwan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Iface string `json:"iface"`
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
iface string
|
||||
}
|
||||
|
||||
func New(iface string) *Block {
|
||||
return &Block{iface: iface}
|
||||
}
|
||||
|
||||
func (b *Block) Name() string {
|
||||
return "wwan"
|
||||
}
|
||||
|
||||
// TODO: signal strength icon.
|
||||
func (b *Block) Render(ctx context.Context) (string, error) {
|
||||
out, err := exec.CommandContext(ctx, "mmcli", "--list-modems").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec mmcli failed: %v", err)
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(out))
|
||||
if len(fields) == 0 {
|
||||
return "", fmt.Errorf("unexpected mmcli output: %q", string(out))
|
||||
}
|
||||
modem := path.Base(fields[0])
|
||||
|
||||
out, err = exec.CommandContext(
|
||||
ctx, "mmcli", "-m", modem, "--output-keyvalue",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec mmcli failed: %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
scanner = bufio.NewScanner(bytes.NewReader(out))
|
||||
|
||||
state, signal string
|
||||
)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case fields[0] == "modem.generic.state":
|
||||
state = fields[2]
|
||||
case fields[0] == "modem.generic.signal-quality.value":
|
||||
signal = fields[2]
|
||||
}
|
||||
}
|
||||
|
||||
if state == "disabled" {
|
||||
return " ", nil
|
||||
}
|
||||
|
||||
// Get the IP address.
|
||||
iface, err := net.InterfaceByName(b.iface)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to get interface %s: %v", b.iface, err,
|
||||
)
|
||||
}
|
||||
|
||||
var ip4, ip6 string
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get addresses: %v", err)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
a := addr.String()
|
||||
if isIPv4(a) {
|
||||
ip4 = a
|
||||
continue
|
||||
} else {
|
||||
ip6 = a
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case ip4 == "" && ip6 == "":
|
||||
return fmt.Sprintf(" %s%%", signal), nil
|
||||
case ip4 != "" && ip6 == "":
|
||||
return fmt.Sprintf(" %s %s%%", ip4, signal), nil
|
||||
case ip4 == "" && ip6 != "":
|
||||
return fmt.Sprintf(" %s %s%%", ip6, signal), nil
|
||||
default:
|
||||
return fmt.Sprintf(" %s %s %s%%", ip4, ip6, signal), nil
|
||||
}
|
||||
}
|
||||
|
||||
func isIPv4(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '.':
|
||||
return true
|
||||
case ':':
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user