diff --git a/config.go b/config.go index 5ea791b..06706a1 100644 --- a/config.go +++ b/config.go @@ -23,30 +23,33 @@ import ( ) var ( - delim = "] [" - prefix = "[" - suffix = "]" + delim = []byte("] [") + prefix = []byte("[") + suffix = []byte("]") ) var modules = []Module{ { - Func: readers.ReadExec("statusbar cpu"), + Func: readers.ReadCpuUsage(), Interval: 5 * time.Second, + Template: `CPU {{printf "%.0f" .UsagePercent}}%`, }, { - Func: readers.ReadExec("statusbar volume"), + Func: readers.ReadBattery("BAT1"), + Interval: 60 * time.Second, + Template: "BAT {{.Capacity}}%", + }, + { + Func: readers.ReadExec("monitors volume"), Signal: 1, }, { - Func: readers.ReadExec("statusbar battery"), - Interval: 60 * time.Second, - }, - { - Func: readers.ReadExec("statusbar date"), + Func: readers.ReadDate("15:04:05"), Interval: 1 * time.Second, }, { - Func: readers.ReadExec("statusbar loadavg"), + Func: readers.ReadLoad(), Interval: 5 * time.Second, + Template: "{{.OneMinute}}", }, } diff --git a/lib/readers/battery.go b/lib/readers/battery.go index 02e1c26..6d8d403 100644 --- a/lib/readers/battery.go +++ b/lib/readers/battery.go @@ -116,62 +116,58 @@ func BatteryTechnologyFromStr(technologyStr string) BatteryTechnology { return TechnologyUnknown } -func readBattery(batteryName string) (interface{}, error) { - capacityPath := fmt.Sprintf("/sys/class/power_supply/%s/capacity", batteryName) - statusPath := fmt.Sprintf("/sys/class/power_supply/%s/status", batteryName) - technologyPath := fmt.Sprintf("/sys/class/power_supply/%s/technology", batteryName) - - capacityFile, err := os.Open(capacityPath) - if err != nil { - return BatteryInfo{}, fmt.Errorf("failed to open %s: %w", capacityPath, err) - } - defer capacityFile.Close() - - capacityScanner := bufio.NewScanner(capacityFile) - if !capacityScanner.Scan() { - return BatteryInfo{}, fmt.Errorf("failed to read from %s: %w", capacityPath, capacityScanner.Err()) - } - - statusFile, err := os.Open(statusPath) - if err != nil { - return BatteryInfo{}, fmt.Errorf("failed to open %s: %w", statusPath, err) - } - defer statusFile.Close() - - statusScanner := bufio.NewScanner(statusFile) - if !statusScanner.Scan() { - return BatteryInfo{}, fmt.Errorf("failed to read from %s: %w", statusPath, statusScanner.Err()) - } - - technologyFile, err := os.Open(technologyPath) - if err != nil { - return BatteryInfo{}, fmt.Errorf("failed to open %s: %w", technologyPath, err) - } - defer technologyFile.Close() - - technologyScanner := bufio.NewScanner(technologyFile) - if !technologyScanner.Scan() { - return BatteryInfo{}, fmt.Errorf("failed to read from %s: %w", technologyPath, technologyScanner.Err()) - } - - batteryCapacityStr := capacityScanner.Text() - batteryStatus := statusScanner.Text() - batteryTechnology := technologyScanner.Text() - - batteryCapacity, err := strconv.ParseUint(batteryCapacityStr, 10, 8) - if err != nil { - return BatteryInfo{}, fmt.Errorf("failed to parse capacity from %s: %w", capacityPath, err) - } - - return BatteryInfo{ - Capacity: uint8(batteryCapacity), - Status: BatteryStatusFromStr(batteryStatus), - Technology: BatteryTechnologyFromStr(batteryTechnology), - }, nil -} - func ReadBattery(batteryName string) func() (interface{}, error) { return func() (interface{}, error) { - return readBattery(batteryName) + capacityPath := fmt.Sprintf("/sys/class/power_supply/%s/capacity", batteryName) + statusPath := fmt.Sprintf("/sys/class/power_supply/%s/status", batteryName) + technologyPath := fmt.Sprintf("/sys/class/power_supply/%s/technology", batteryName) + + capacityFile, err := os.Open(capacityPath) + if err != nil { + return BatteryInfo{}, fmt.Errorf("failed to open %s: %w", capacityPath, err) + } + defer capacityFile.Close() + + capacityScanner := bufio.NewScanner(capacityFile) + if !capacityScanner.Scan() { + return BatteryInfo{}, fmt.Errorf("failed to read from %s: %w", capacityPath, capacityScanner.Err()) + } + + statusFile, err := os.Open(statusPath) + if err != nil { + return BatteryInfo{}, fmt.Errorf("failed to open %s: %w", statusPath, err) + } + defer statusFile.Close() + + statusScanner := bufio.NewScanner(statusFile) + if !statusScanner.Scan() { + return BatteryInfo{}, fmt.Errorf("failed to read from %s: %w", statusPath, statusScanner.Err()) + } + + technologyFile, err := os.Open(technologyPath) + if err != nil { + return BatteryInfo{}, fmt.Errorf("failed to open %s: %w", technologyPath, err) + } + defer technologyFile.Close() + + technologyScanner := bufio.NewScanner(technologyFile) + if !technologyScanner.Scan() { + return BatteryInfo{}, fmt.Errorf("failed to read from %s: %w", technologyPath, technologyScanner.Err()) + } + + batteryCapacityStr := capacityScanner.Text() + batteryStatus := statusScanner.Text() + batteryTechnology := technologyScanner.Text() + + batteryCapacity, err := strconv.ParseUint(batteryCapacityStr, 10, 8) + if err != nil { + return BatteryInfo{}, fmt.Errorf("failed to parse capacity from %s: %w", capacityPath, err) + } + + return BatteryInfo{ + Capacity: uint8(batteryCapacity), + Status: BatteryStatusFromStr(batteryStatus), + Technology: BatteryTechnologyFromStr(batteryTechnology), + }, nil } } diff --git a/lib/readers/cpu_temperature.go b/lib/readers/cpu_temperature.go index ad98f06..01bd731 100644 --- a/lib/readers/cpu_temperature.go +++ b/lib/readers/cpu_temperature.go @@ -25,33 +25,29 @@ import ( type CpuTemperatureInfo float32 -func readCpuTemperature(hwmonName, tempName string) (interface{}, error) { - tempPath := fmt.Sprintf("/sys/class/hwmon/%s/%s_input", hwmonName, tempName) - - file, err := os.Open(tempPath) - if err != nil { - return 0, fmt.Errorf("failed to open %s: %w", tempPath, err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - if !scanner.Scan() { - return 0, fmt.Errorf("failed to read from %s: %w", tempPath, scanner.Err()) - } - - line := scanner.Text() - cpuTemperatureMdeg, err := strconv.ParseUint(line, 10, 32) - if err != nil { - return 0, fmt.Errorf("failed to parse cpu temperature from %s: %w", tempPath, err) - } - - cpuTemperature := float32(cpuTemperatureMdeg) / 1000 - - return CpuTemperatureInfo(cpuTemperature), nil -} - func ReadCpuTemperature(hwmonName, tempName string) func() (interface{}, error) { return func() (interface{}, error) { - return readCpuTemperature(hwmonName, tempName) + tempPath := fmt.Sprintf("/sys/class/hwmon/%s/%s_input", hwmonName, tempName) + + file, err := os.Open(tempPath) + if err != nil { + return 0, fmt.Errorf("failed to open %s: %w", tempPath, err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + return 0, fmt.Errorf("failed to read from %s: %w", tempPath, scanner.Err()) + } + + line := scanner.Text() + cpuTemperatureMdeg, err := strconv.ParseUint(line, 10, 32) + if err != nil { + return 0, fmt.Errorf("failed to parse cpu temperature from %s: %w", tempPath, err) + } + + cpuTemperature := float32(cpuTemperatureMdeg) / 1000 + + return CpuTemperatureInfo(cpuTemperature), nil } } diff --git a/lib/readers/cpu_usage.go b/lib/readers/cpu_usage.go index 43d72e5..22417c3 100644 --- a/lib/readers/cpu_usage.go +++ b/lib/readers/cpu_usage.go @@ -34,56 +34,52 @@ func (cu CpuUsageInfo) String() string { return fmt.Sprintf("%d%%", uint(cu.UsagePercent)) } -func readCpuUsage() (interface{}, error) { - file, err := os.Open("/proc/stat") - if err != nil { - return CpuUsageInfo{}, err - } - defer file.Close() - - var cpuInUse, cpuTotal uint64 - scanner := bufio.NewScanner(file) - - scanner.Scan() - for scanner.Scan() { - line := scanner.Text() - - if !strings.HasPrefix(line, "cpu") { - break - } - - var cpuN string - var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIoWait, cpuIrq, cpuSoftIrq uint64 - - _, err := fmt.Sscanf(line, "cpu%s %d %d %d %d %d %d %d", - &cpuN, &cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIoWait, &cpuIrq, &cpuSoftIrq) - if err != nil { - return CpuUsageInfo{}, fmt.Errorf("failed to parse CPU stats: %w", err) - } - - inUse := cpuUser + cpuNice + cpuSystem - total := inUse + cpuIdle + cpuIoWait + cpuIrq + cpuSoftIrq - - cpuInUse += inUse - cpuTotal += total - } - - if err := scanner.Err(); err != nil { - return CpuUsageInfo{}, err - } - if cpuTotal == 0 { - return CpuUsageInfo{}, errors.New("no CPU stats found") - } - - return CpuUsageInfo{ - InUse: cpuInUse, - Total: cpuTotal, - UsagePercent: float64(cpuInUse) * 100 / float64(cpuTotal), - }, nil -} - func ReadCpuUsage() func() (interface{}, error) { return func() (interface{}, error) { - return readCpuUsage() + file, err := os.Open("/proc/stat") + if err != nil { + return CpuUsageInfo{}, err + } + defer file.Close() + + var cpuInUse, cpuTotal uint64 + scanner := bufio.NewScanner(file) + + scanner.Scan() + for scanner.Scan() { + line := scanner.Text() + + if !strings.HasPrefix(line, "cpu") { + break + } + + var cpuN string + var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIoWait, cpuIrq, cpuSoftIrq uint64 + + _, err := fmt.Sscanf(line, "cpu%s %d %d %d %d %d %d %d", + &cpuN, &cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIoWait, &cpuIrq, &cpuSoftIrq) + if err != nil { + return CpuUsageInfo{}, fmt.Errorf("failed to parse CPU stats: %w", err) + } + + inUse := cpuUser + cpuNice + cpuSystem + total := inUse + cpuIdle + cpuIoWait + cpuIrq + cpuSoftIrq + + cpuInUse += inUse + cpuTotal += total + } + + if err := scanner.Err(); err != nil { + return CpuUsageInfo{}, err + } + if cpuTotal == 0 { + return CpuUsageInfo{}, errors.New("no CPU stats found") + } + + return CpuUsageInfo{ + InUse: cpuInUse, + Total: cpuTotal, + UsagePercent: float64(cpuInUse) * 100 / float64(cpuTotal), + }, nil } } diff --git a/lib/readers/date.go b/lib/readers/date.go new file mode 100644 index 0000000..053b997 --- /dev/null +++ b/lib/readers/date.go @@ -0,0 +1,30 @@ +// modbot is a system information agregator +// Copyright (C) 2024 frosty +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package readers + +import "time" + +type DateInfo string + +func ReadDate(format string) func() (interface{}, error) { + return func() (interface{}, error) { + date := time.Now() + + // Reference: https://gosamples.dev/date-time-format-cheatsheet/ + return date.Format(format), nil + } +} diff --git a/lib/readers/exec.go b/lib/readers/exec.go index d30c162..d005629 100644 --- a/lib/readers/exec.go +++ b/lib/readers/exec.go @@ -25,34 +25,31 @@ import ( type ExecInfo string -func readExec(command string) (interface{}, error) { - args := []string{"sh", "-c", command} - var stdout, stderr bytes.Buffer - - cmd := exec.Command(args[0], args[1:]...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return ExecInfo(""), err - } - - if cmd.ProcessState.ExitCode() != 0 { - return ExecInfo(""), errors.New("returned non-zero exit code") - } - - outputLines := strings.Split(stdout.String(), "\n") - if len(outputLines) == 0 { - return ExecInfo(""), nil - } - - outputString := outputLines[0] - return ExecInfo(outputString), nil - -} - func ReadExec(command string) func() (interface{}, error) { return func() (interface{}, error) { - return readExec(command) + args := []string{"sh", "-c", command} + var stdout, stderr bytes.Buffer + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return ExecInfo(""), err + } + if cmd.ProcessState.ExitCode() != 0 { + return ExecInfo(""), errors.New("returned non-zero exit code") + } + if len(stderr.String()) > 0 { + return ExecInfo(""), errors.New("data in stderr found") + } + + outputLines := strings.Split(stdout.String(), "\n") + if len(outputLines) == 0 { + return ExecInfo(""), nil + } + + outputString := outputLines[0] + return ExecInfo(outputString), nil } } diff --git a/lib/readers/load.go b/lib/readers/load.go index 5db316b..fcec368 100644 --- a/lib/readers/load.go +++ b/lib/readers/load.go @@ -33,36 +33,32 @@ func (l LoadInfo) String() string { return fmt.Sprintf("%v %v %v", l.OneMinute, l.FiveMinute, l.FifteenMinute) } -func readLoad() (interface{}, error) { - const loadPath = "/proc/loadavg" - - file, err := os.Open(loadPath) - if err != nil { - return LoadInfo{}, fmt.Errorf("failed to open %s: %w", loadPath, err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - scanner.Scan() - if err := scanner.Err(); err != nil { - return LoadInfo{}, fmt.Errorf("failed to read from %s: %w", loadPath, err) - } - - line := scanner.Text() - fields := strings.Fields(line) - if len(fields) < 3 { - return LoadInfo{}, fmt.Errorf("unexpected format in %s: %s", loadPath, line) - } - - return LoadInfo{ - OneMinute: fields[0], - FiveMinute: fields[1], - FifteenMinute: fields[2], - }, nil -} - func ReadLoad() func() (interface{}, error) { return func() (interface{}, error) { - return readLoad() + const loadPath = "/proc/loadavg" + + file, err := os.Open(loadPath) + if err != nil { + return LoadInfo{}, fmt.Errorf("failed to open %s: %w", loadPath, err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Scan() + if err := scanner.Err(); err != nil { + return LoadInfo{}, fmt.Errorf("failed to read from %s: %w", loadPath, err) + } + + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 3 { + return LoadInfo{}, fmt.Errorf("unexpected format in %s: %s", loadPath, line) + } + + return LoadInfo{ + OneMinute: fields[0], + FiveMinute: fields[1], + FifteenMinute: fields[2], + }, nil } } diff --git a/lib/readers/memory.go b/lib/readers/memory.go index 34daf47..fca3c43 100644 --- a/lib/readers/memory.go +++ b/lib/readers/memory.go @@ -37,61 +37,57 @@ type MemoryInfo struct { UsedPretty string } -func readMemory() (interface{}, error) { - file, err := os.Open("/proc/meminfo") - if err != nil { - return MemoryInfo{}, err - } - defer file.Close() - - var memTotal, memAvailable, memUsed uint64 - scanner := bufio.NewScanner(file) - - for scanner.Scan() { - line := scanner.Text() - columns := strings.Fields(line) - - if len(columns) < 2 { - continue - } - - value, err := strconv.ParseUint(columns[1], 10, 64) - if err != nil { - return MemoryInfo{}, fmt.Errorf("failed to parse memory value: %w", err) - } - - switch { - case strings.HasPrefix(line, "MemTotal:"): - memTotal = value - case strings.HasPrefix(line, "MemAvailable:"): - memAvailable = value - } - } - - if err := scanner.Err(); err != nil { - return MemoryInfo{}, err - } - if memTotal == 0 { - return MemoryInfo{}, errors.New("missing MemTotal") - } - if memAvailable == 0 { - return MemoryInfo{}, errors.New("missing MemAvailable") - } - - memUsed = memTotal - memAvailable - return MemoryInfo{ - Total: memTotal, - Available: memAvailable, - Used: memUsed, - - TotalPretty: ui.PrettifyKib(memTotal, 2), - AvailablePretty: ui.PrettifyKib(memAvailable, 2), - UsedPretty: ui.PrettifyKib(memUsed, 2), - }, nil -} - func ReadMemory() func() (interface{}, error) { return func() (interface{}, error) { - return readMemory() + file, err := os.Open("/proc/meminfo") + if err != nil { + return MemoryInfo{}, err + } + defer file.Close() + + var memTotal, memAvailable, memUsed uint64 + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + columns := strings.Fields(line) + + if len(columns) < 2 { + continue + } + + value, err := strconv.ParseUint(columns[1], 10, 64) + if err != nil { + return MemoryInfo{}, fmt.Errorf("failed to parse memory value: %w", err) + } + + switch { + case strings.HasPrefix(line, "MemTotal:"): + memTotal = value + case strings.HasPrefix(line, "MemAvailable:"): + memAvailable = value + } + } + + if err := scanner.Err(); err != nil { + return MemoryInfo{}, err + } + if memTotal == 0 { + return MemoryInfo{}, errors.New("missing MemTotal") + } + if memAvailable == 0 { + return MemoryInfo{}, errors.New("missing MemAvailable") + } + + memUsed = memTotal - memAvailable + return MemoryInfo{ + Total: memTotal, + Available: memAvailable, + Used: memUsed, + + TotalPretty: ui.PrettifyKib(memTotal, 2), + AvailablePretty: ui.PrettifyKib(memAvailable, 2), + UsedPretty: ui.PrettifyKib(memUsed, 2), + }, nil } } diff --git a/lib/readers/os.go b/lib/readers/os.go index b1c1692..09b3adf 100644 --- a/lib/readers/os.go +++ b/lib/readers/os.go @@ -29,50 +29,46 @@ type OsInfo struct { Version string } -func readOs() (interface{}, error) { - const osPath = "/lib/os-release" - - file, err := os.Open(osPath) - if err != nil { - return OsInfo{}, fmt.Errorf("failed to open %s: %w", osPath, err) - } - defer file.Close() - - var osName, osPrettyName, osVersion string - scanner := bufio.NewScanner(file) - - for scanner.Scan() { - line := scanner.Text() - line = strings.ReplaceAll(line, "\"", "") - fields := strings.SplitN(line, "=", 2) - - if len(fields) < 2 { - return OsInfo{}, fmt.Errorf("unexpected format in %s: %s", osPath, line) - } - - switch { - case strings.HasPrefix(line, "NAME"): - osName = fields[1] - case strings.HasPrefix(line, "PRETTY_NAME"): - osPrettyName = fields[1] - case strings.HasPrefix(line, "VERSION_ID"): - osVersion = fields[1] - } - } - - if err := scanner.Err(); err != nil { - return OsInfo{}, fmt.Errorf("failed to read from %s: %w", osPath, err) - } - - return OsInfo{ - Name: osName, - PrettyName: osPrettyName, - Version: osVersion, - }, nil -} - func ReadOs() func() (interface{}, error) { return func() (interface{}, error) { - return readOs() + const osPath = "/lib/os-release" + + file, err := os.Open(osPath) + if err != nil { + return OsInfo{}, fmt.Errorf("failed to open %s: %w", osPath, err) + } + defer file.Close() + + var osName, osPrettyName, osVersion string + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + line = strings.ReplaceAll(line, "\"", "") + fields := strings.SplitN(line, "=", 2) + + if len(fields) < 2 { + return OsInfo{}, fmt.Errorf("unexpected format in %s: %s", osPath, line) + } + + switch { + case strings.HasPrefix(line, "NAME"): + osName = fields[1] + case strings.HasPrefix(line, "PRETTY_NAME"): + osPrettyName = fields[1] + case strings.HasPrefix(line, "VERSION_ID"): + osVersion = fields[1] + } + } + + if err := scanner.Err(); err != nil { + return OsInfo{}, fmt.Errorf("failed to read from %s: %w", osPath, err) + } + + return OsInfo{ + Name: osName, + PrettyName: osPrettyName, + Version: osVersion, + }, nil } } diff --git a/lib/readers/uptime.go b/lib/readers/uptime.go index 0265ee6..4fe5989 100644 --- a/lib/readers/uptime.go +++ b/lib/readers/uptime.go @@ -65,36 +65,32 @@ func (u UptimeInfo) String() string { return builder.String() } -func readUptime() (interface{}, error) { - const uptimePath = "/proc/uptime" - - file, err := os.Open(uptimePath) - if err != nil { - return 0, fmt.Errorf("failed to open %s: %w", uptimePath, err) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - if !scanner.Scan() { - return 0, fmt.Errorf("failed to read from %s: %w", uptimePath, scanner.Err()) - } - - line := scanner.Text() - fields := strings.SplitN(line, ".", 2) - if len(fields) < 1 { - return 0, fmt.Errorf("unexpected format in %s: %s", uptimePath, line) - } - - uptime, err := strconv.ParseUint(fields[0], 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse uptime from %s: %w", uptimePath, err) - } - - return UptimeInfo(uptime), nil -} - func ReadUptime() func() (interface{}, error) { return func() (interface{}, error) { - return readUptime() + const uptimePath = "/proc/uptime" + + file, err := os.Open(uptimePath) + if err != nil { + return 0, fmt.Errorf("failed to open %s: %w", uptimePath, err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + return 0, fmt.Errorf("failed to read from %s: %w", uptimePath, scanner.Err()) + } + + line := scanner.Text() + fields := strings.SplitN(line, ".", 2) + if len(fields) < 1 { + return 0, fmt.Errorf("unexpected format in %s: %s", uptimePath, line) + } + + uptime, err := strconv.ParseUint(fields[0], 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse uptime from %s: %w", uptimePath, err) + } + + return UptimeInfo(uptime), nil } } diff --git a/main.go b/main.go index e21c252..ec4b5a8 100644 --- a/main.go +++ b/main.go @@ -36,10 +36,11 @@ import ( var ( updateChan = make(chan int) - moduleOutputs = make([]string, len(modules)) + moduleOutputs = make([][]byte, len(modules)) + mutex sync.Mutex - mutex sync.Mutex - lastOutput string + sigChan = make(chan os.Signal, 1024) + signalMap = make(map[os.Signal][]*Module) // X connection data x *xgb.Conn @@ -62,130 +63,149 @@ type Module struct { func (m *Module) Run() { if m.pos < 0 || m.pos >= len(modules) { - log.Printf("invalid module index %d\n", m.pos) + log.Printf("invalid module index: %d\n", m.pos) return } + var output bytes.Buffer + info, err := m.Func() if err != nil { - moduleOutputs[m.pos] = "failed" - return - } - - var output string - if m.Template != "" { - // Parse the output and apply the provided template - tmpl, err := template.New("module").Parse(m.Template) - if err != nil { - log.Printf("template parsing error: %v\n", err) - return - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, info); err != nil { - log.Printf("template execution error: %v\n", err) - return - } - - output = buf.String() + output.WriteString("failed") } else { - // Use the output as is - output = fmt.Sprintf("%v", info) + if m.Template != "" { + // Parse the output and apply the provided template + tmpl, err := template.New("module").Parse(m.Template) + if err != nil { + log.Printf("template parsing error: %v\n", err) + return + } + + if err := tmpl.Execute(&output, info); err != nil { + log.Printf("template execution error: %v\n", err) + return + } + } else { + // Use the output as is + fmt.Fprintf(&output, "%v", info) + } } mutex.Lock() - moduleOutputs[m.pos] = output + moduleOutputs[m.pos] = output.Bytes() updateChan <- 1 mutex.Unlock() } -func parseFlags() Flags { - var flags Flags - flag.BoolVar(&flags.SetXRootName, "x", false, "set x root window name") +func (m *Module) Init(pos int) { + m.pos = pos - flag.Parse() + if m.Signal != 0 { + sig := syscall.Signal(34 + m.Signal) + if _, exists := signalMap[sig]; !exists { + signal.Notify(sigChan, sig) + } + signalMap[sig] = append(signalMap[sig], m) + } - return flags + m.Run() + if m.Interval > 0 { + for { + time.Sleep(m.Interval) + m.Run() + } + } +} + +func grabXRootWindow() (*xgb.Conn, xproto.Window, error) { + conn, err := xgb.NewConn() + if err != nil { + return nil, 0, err + } + + root := xproto.Setup(conn).DefaultScreen(conn).Root + if conn == nil { + return nil, 0, fmt.Errorf("failed to create X connection") + } + + return conn, root, nil +} + +func createOutput(b *bytes.Buffer) { + b.Write(prefix) + first := true + for _, output := range moduleOutputs { + if output == nil { + continue + } + if !first { + b.Write(delim) + } else { + first = false + } + b.Write(output) + } + b.Write(suffix) +} + +func monitorUpdates(setXRootName bool) { + var lastOutput []byte + var combinedOutput bytes.Buffer + + for range updateChan { + mutex.Lock() + combinedOutput.Reset() + createOutput(&combinedOutput) + mutex.Unlock() + + combinedOutputBytes := combinedOutput.Bytes() + + if !bytes.Equal(combinedOutputBytes, lastOutput) { + if setXRootName { + // Set X root window name + xproto.ChangeProperty(x, xproto.PropModeReplace, root, xproto.AtomWmName, xproto.AtomString, 8, uint32(len(combinedOutputBytes)), combinedOutputBytes) + } else { + // Send to stdout + fmt.Printf("%s\n", combinedOutputBytes) + } + lastOutput = append([]byte(nil), combinedOutputBytes...) + } + } +} + +func handleSignal(sig os.Signal) { + ms := signalMap[sig] + for _, m := range ms { + go m.Run() + } } func main() { - flags := parseFlags() + // Parse flags + var flags Flags + flag.BoolVar(&flags.SetXRootName, "x", false, "set x root window name") + flag.Parse() - // Connect to X and get the root window if requested + // Grab X root window if requested if flags.SetXRootName { var err error - x, err = xgb.NewConn() + x, root, err = grabXRootWindow() if err != nil { - log.Fatalf("X connection failed: %s\n", err.Error()) + log.Fatalf("error grabbing X root window: %v\n", err) } - root = xproto.Setup(x).DefaultScreen(x).Root + defer x.Close() } - sigChan := make(chan os.Signal, 1024) - signalMap := make(map[os.Signal][]*Module) - // Initialize modules for i := range modules { - go func(m *Module, i int) { - m.pos = i - - if m.Signal != 0 { - sig := syscall.Signal(34 + m.Signal) - if _, exists := signalMap[sig]; !exists { - signal.Notify(sigChan, sig) - } - signalMap[sig] = append(signalMap[sig], m) - } - - m.Run() - if m.Interval > 0 { - for { - time.Sleep(m.Interval) - m.Run() - } - } - }(&modules[i], i) + go modules[i].Init(i) } - // Update output on difference - go func() { - for range updateChan { - mutex.Lock() - var combinedOutput string - for i, output := range moduleOutputs { - if output == "" { - continue - } - if i > 0 { - combinedOutput += delim - } - combinedOutput += output - } - combinedOutput = prefix + combinedOutput + suffix - mutex.Unlock() + // Monitor changes to the combined output + go monitorUpdates(flags.SetXRootName) - // Output to either X root window name or stdout based on flags - if combinedOutput != lastOutput { - if flags.SetXRootName { - // Set the X root window name - outputBytes := []byte(combinedOutput) - xproto.ChangeProperty(x, xproto.PropModeReplace, root, xproto.AtomWmName, xproto.AtomString, 8, uint32(len(outputBytes)), outputBytes) - } else { - // Print to stdout - fmt.Printf("%v\n", combinedOutput) - } - lastOutput = combinedOutput - } - } - }() - - // Handle module signals + // Handle signals sent for modules for sig := range sigChan { - go func(sig *os.Signal) { - ms := signalMap[*sig] - for _, m := range ms { - go m.Run() - } - }(&sig) + go handleSignal(sig) } }