commit 005551137577929740796b239f33928b61245bd4
parent 0017e12f942c741362ee5305930055a0de8fd494
Author: dwrz <dwrz@dwrz.net>
Date: Fri, 2 Dec 2022 16:20:48 +0000
Add statusbar
Diffstat:
14 files changed, 970 insertions(+), 0 deletions(-)
diff --git a/cmd/statusbar/main.go b/cmd/statusbar/main.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "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")
+
+// TODO: block labels; rendered in case of error.
+func main() {
+ flag.Parse()
+
+ var l = log.New(os.Stderr)
+
+ // Setup workspace and log file.
+ cdir, err := os.UserCacheDir()
+ if err != nil {
+ l.Error.Fatalf(
+ "failed to determine user cache directory: %v", err,
+ )
+ }
+ wdir := filepath.Join(cdir, "statusbar")
+
+ if err := os.MkdirAll(wdir, os.ModeDir|0700); err != nil {
+ l.Error.Fatalf("failed to create tmp dir: %v", err)
+ }
+
+ flog, err := os.Create(wdir + "/log")
+ if err != nil {
+ l.Error.Fatalf("failed to create log file: %v", err)
+ }
+ 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(),
+ )
+
+ 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: "+%d %H:%M %Z",
+ Label: "NYC",
+ Timezone: "America/New_York",
+ }),
+ datetime.New(datetime.Parameters{
+ Format: "+%d %H:%M %Z",
+ Label: "Napoli",
+ Timezone: "Europe/Rome",
+ }),
+ datetime.New(datetime.Parameters{
+ Format: "+%d %H:%M %Z",
+ Label: "上海",
+ Timezone: "Asia/Shanghai",
+ }),
+ datetime.New(datetime.Parameters{
+ Format: dfmt,
+ Timezone: "UTC",
+ }),
+ },
+ Log: log.New(flog),
+ Separator: "┃",
+ })
+
+ // Initial print.
+ fmt.Println(bar.Render())
+ if *once {
+ os.Exit(0)
+ }
+
+ // Main loop.
+ ticker := time.NewTicker(1 * time.Second)
+ for {
+ select {
+ case <-ticker.C:
+ // now := time.Now()
+ fmt.Println(bar.Render())
+ // fmt.Println(time.Since(now))
+ }
+ }
+}
diff --git a/pkg/statusbar/cpu/cpu.go b/pkg/statusbar/cpu/cpu.go
@@ -0,0 +1,33 @@
+package cpu
+
+import (
+ "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() (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
+ }
+}
diff --git a/pkg/statusbar/datetime/datetime.go b/pkg/statusbar/datetime/datetime.go
@@ -0,0 +1,53 @@
+package datetime
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+type Block struct {
+ format string
+ label string
+ timezone string
+}
+
+type Parameters struct {
+ Format string
+ Label string
+ Timezone string
+}
+
+func New(p Parameters) *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() (string, error) {
+ cmd := exec.Command("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
+ }
+}
diff --git a/pkg/statusbar/disk/disk.go b/pkg/statusbar/disk/disk.go
@@ -0,0 +1,60 @@
+package disk
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+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() (string, error) {
+ out, err := exec.Command("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 i, m := range b.mounts {
+ if s, exists := mounts[m]; exists {
+ output.WriteString(s)
+ if i < len(b.mounts)-1 {
+ output.WriteRune(' ')
+ }
+ }
+ }
+
+ return output.String(), nil
+}
diff --git a/pkg/statusbar/eth/eth.go b/pkg/statusbar/eth/eth.go
@@ -0,0 +1,70 @@
+package eth
+
+import (
+ "fmt"
+ "net"
+)
+
+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() (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 "", 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(" "), 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
+}
diff --git a/pkg/statusbar/light/light.go b/pkg/statusbar/light/light.go
@@ -0,0 +1,30 @@
+package light
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+type Block struct{}
+
+func New() *Block {
+ return &Block{}
+}
+
+func (b *Block) Name() string {
+ return "light"
+}
+
+func (b *Block) Render() (string, error) {
+ out, err := exec.Command("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
+ }
+}
diff --git a/pkg/statusbar/memory/memory.go b/pkg/statusbar/memory/memory.go
@@ -0,0 +1,73 @@
+package memory
+
+import (
+ "fmt"
+ "os/exec"
+ "strconv"
+ "strings"
+)
+
+type Block struct{}
+
+func New() *Block {
+ return &Block{}
+}
+
+func (b *Block) Name() string {
+ return "memory"
+}
+
+func (b *Block) Render() (string, error) {
+ var output strings.Builder
+
+ out, err := exec.Command("free", "-m").Output()
+ if err != nil {
+ return "", fmt.Errorf("failed to exec: %v", err)
+ }
+
+ for _, l := range strings.Split(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,
+ )
+ }
+
+ 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,
+ )
+ }
+
+ fmt.Fprintf(&output, " %.0f%%", (used/total)*100)
+ }
+ }
+
+ return output.String(), nil
+}
diff --git a/pkg/statusbar/mic/mic.go b/pkg/statusbar/mic/mic.go
@@ -0,0 +1,32 @@
+package mic
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+type Block struct{}
+
+func New() *Block {
+ return &Block{}
+}
+
+func (b *Block) Name() string {
+ return "mic"
+}
+
+func (b *Block) Render() (string, error) {
+ out, err := exec.Command(
+ "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 fmt.Sprintf(""), nil
+ }
+
+ return fmt.Sprintf(" OPEN"), nil
+}
diff --git a/pkg/statusbar/power/power.go b/pkg/statusbar/power/power.go
@@ -0,0 +1,165 @@
+package power
+
+import (
+ "bufio"
+ "fmt"
+ "math"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// 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() (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())
+ }
+ }
+ sort.Slice(supplies, func(i, j int) bool {
+ return supplies[i] < supplies[j]
+ })
+
+ 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,
+ )
+ }
+ defer f.Close()
+
+ switch {
+ case s == "AC":
+ sections = append(sections, outputAC(f))
+ case strings.Contains(s, "BAT"):
+ sections = append(sections, outputBAT(f))
+ }
+ }
+
+ 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),
+ )
+}
diff --git a/pkg/statusbar/statusbar.go b/pkg/statusbar/statusbar.go
@@ -0,0 +1,78 @@
+package statusbar
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "code.dwrz.net/src/pkg/log"
+)
+
+type Block interface {
+ Name() string
+ Render() (string, error)
+}
+
+type Parameters struct {
+ Blocks []Block
+ Log *log.Logger
+ Separator string
+}
+
+type StatusBar struct {
+ b *strings.Builder
+ blocks []Block
+ l *log.Logger
+ sep string
+}
+
+func (s *StatusBar) Render() string {
+ defer s.b.Reset()
+
+ fmt.Fprintf(s.b, "%s ", s.sep)
+
+ var (
+ outputs = make([]string, len(s.blocks))
+ wg sync.WaitGroup
+ )
+
+ wg.Add(len(s.blocks))
+
+ for i, b := range s.blocks {
+ go func(i int, b Block) {
+ defer wg.Done()
+
+ text, err := b.Render()
+ if err != nil {
+ s.l.Error.Printf(
+ "failed to render %s: %v",
+ b.Name(), 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,
+ }
+}
diff --git a/pkg/statusbar/temp/temp.go b/pkg/statusbar/temp/temp.go
@@ -0,0 +1,45 @@
+package temp
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+)
+
+type Block struct{}
+
+func New() *Block {
+ return &Block{}
+}
+
+func (b *Block) Name() string {
+ return "temp"
+}
+
+func (b *Block) Render() (string, error) {
+ out, err := exec.Command("sensors", "-j").Output()
+ if err != nil {
+ return "", fmt.Errorf("exec sensors failed: %v", err)
+ }
+
+ 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)
+ }
+
+ return fmt.Sprintf(
+ " %.0f℃ %.0f℃",
+ sensors.Thinkpad.CPU.Temp, sensors.NVME.Composite.Temp,
+ ), nil
+}
diff --git a/pkg/statusbar/volume/volume.go b/pkg/statusbar/volume/volume.go
@@ -0,0 +1,43 @@
+package volume
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+type Block struct{}
+
+func New() *Block {
+ return &Block{}
+}
+
+func (b *Block) Name() string {
+ return "volume"
+}
+
+func (b *Block) Render() (string, error) {
+ out, err := exec.Command(
+ "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 fmt.Sprintf(""), nil
+ }
+
+ out, err = exec.Command(
+ "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 fmt.Sprintf(" "), nil
+ } else {
+ return fmt.Sprintf(" %s", fields[4]), nil
+ }
+}
diff --git a/pkg/statusbar/wlan/wlan.go b/pkg/statusbar/wlan/wlan.go
@@ -0,0 +1,55 @@
+package wlan
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+type Block struct{}
+
+func New() *Block {
+ return &Block{}
+}
+
+func (b *Block) Name() string {
+ return "wlan"
+}
+
+// TODO: signal strength icon.
+func (b *Block) Render() (string, error) {
+ out, err := exec.Command("iwctl", "station", "wlan0", "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
+}
diff --git a/pkg/statusbar/wwan/wwan.go b/pkg/statusbar/wwan/wwan.go
@@ -0,0 +1,116 @@
+package wwan
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "net"
+ "os/exec"
+ "path"
+ "strings"
+)
+
+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.
+// TODO: get IP address (net.Interfaces).
+func (b *Block) Render() (string, error) {
+ out, err := exec.Command("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 output: %v", err)
+ }
+ modem := path.Base(fields[0])
+
+ out, err = exec.Command(
+ "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
+}