diff --git a/.gitignore b/.gitignore index a394ecc..3047c72 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -sysinfo +modbot diff --git a/README.md b/README.md index 629c970..1bdbad2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ -# sysinfo +# modbot -sysinfo queries your system for various information and allows you to format it however you'd like. +modbot is a seriously over-engineered system for querying different information about your system. Often used for a status bar like [dzen](https://github.com/robm/dzen) or [lemonbar](https://github.com/LemonBoy/bar). + +Each part of the output is a module, and modbot is an agregator for these modules. You can query different information about your system like load average, CPU temperature, wireless SSID, and even the output of an arbitrary command or file. + +The modules that are in the output are determined at compile time, within the `config.go` file. Don't worry-this process uses a straightforward syntax that doesn't require any programming knowledge. Though, if you're using this, I assume you're smart enough to know that. diff --git a/go.mod b/go.mod index 26348d0..a5da0fe 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module codeberg.org/frosty/sysinfo +module codeberg.org/frosty/modbot go 1.22.4 diff --git a/lib/readers/battery.go b/lib/readers/battery.go new file mode 100644 index 0000000..6754586 --- /dev/null +++ b/lib/readers/battery.go @@ -0,0 +1,171 @@ +// 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 ( + "bufio" + "fmt" + "os" + "strconv" +) + +type BatteryStatus int +type BatteryTechnology int + +const ( + StatusUnknown BatteryStatus = iota + StatusCharging + StatusDischarging + StatusNotCharging + StatusFull +) + +const ( + TechnologyUnknown BatteryTechnology = iota + TechnologyNimh + TechnologyLiion + TechnologyLipoly + TechnologyLife + TechnologyNicd + TechnologyLimn +) + +type BatteryInfo struct { + Capacity uint8 + Status BatteryStatus + Technology BatteryTechnology +} + +func (bs BatteryStatus) String() string { + switch bs { + case StatusCharging: + return "Charging" + case StatusDischarging: + return "Discharging" + case StatusNotCharging: + return "Not charging" + case StatusFull: + return "Full" + default: + return "Unknown" + } +} + +func (bt BatteryTechnology) String() string { + switch bt { + case TechnologyNimh: + return "NiMH" + case TechnologyLiion: + return "Li-ion" + case TechnologyLipoly: + return "Li-poly" + case TechnologyLife: + return "LiFe" + case TechnologyNicd: + return "NiCd" + case TechnologyLimn: + return "LiMn" + default: + return "Unknown" + } +} + +var batteryStatusMap = map[string]BatteryStatus{ + "Unknown": StatusUnknown, + "Charging": StatusCharging, + "Discharging": StatusDischarging, + "Not charging": StatusNotCharging, + "Full": StatusFull, +} + +var batteryTechnologyMap = map[string]BatteryTechnology{ + "Unknown": TechnologyUnknown, + "NiMH": TechnologyNimh, + "Li-ion": TechnologyLiion, + "Li-poly": TechnologyLipoly, + "LiFe": TechnologyLife, + "NiCd": TechnologyNicd, + "LiMn": TechnologyLimn, +} + +func BatteryStatusFromStr(statusStr string) BatteryStatus { + if status, exists := batteryStatusMap[statusStr]; exists { + return status + } + return StatusUnknown +} + +func BatteryTechnologyFromStr(technologyStr string) BatteryTechnology { + if technology, exists := batteryTechnologyMap[technologyStr]; exists { + return technology + } + return TechnologyUnknown +} + +func ReadBattery(batteryName string) (BatteryInfo, 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 +} diff --git a/lib/readers/cpu_temperature.go b/lib/readers/cpu_temperature.go new file mode 100644 index 0000000..d148a8b --- /dev/null +++ b/lib/readers/cpu_temperature.go @@ -0,0 +1,51 @@ +// 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 ( + "bufio" + "fmt" + "os" + "strconv" +) + +type CpuTemperatureInfo float32 + +func ReadCpuTemperature(hwmonName, tempName string) (CpuTemperatureInfo, 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 +} diff --git a/lib/readers/cpu_usage.go b/lib/readers/cpu_usage.go index 87b5272..978b66a 100644 --- a/lib/readers/cpu_usage.go +++ b/lib/readers/cpu_usage.go @@ -1,4 +1,4 @@ -// sysinfo queries information about your system +// modbot is a system information agregator // Copyright (C) 2024 frosty // // This program is free software: you can redistribute it and/or modify diff --git a/lib/readers/disk_io.go b/lib/readers/disk_io.go new file mode 100644 index 0000000..1f09352 --- /dev/null +++ b/lib/readers/disk_io.go @@ -0,0 +1,17 @@ +// 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 diff --git a/lib/readers/disk_usage.go b/lib/readers/disk_usage.go new file mode 100644 index 0000000..1f09352 --- /dev/null +++ b/lib/readers/disk_usage.go @@ -0,0 +1,17 @@ +// 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 diff --git a/lib/readers/load.go b/lib/readers/load.go new file mode 100644 index 0000000..da8f60d --- /dev/null +++ b/lib/readers/load.go @@ -0,0 +1,58 @@ +// 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 ( + "bufio" + "fmt" + "os" + "strings" +) + +type LoadInfo struct { + OneMinute string + FiveMinute string + FifteenMinute string +} + +func ReadLoad() (LoadInfo, 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 +} diff --git a/lib/readers/memory.go b/lib/readers/memory.go index 53bb3fd..fc310e3 100644 --- a/lib/readers/memory.go +++ b/lib/readers/memory.go @@ -1,4 +1,4 @@ -// sysinfo queries information about your system +// modbot is a system information agregator // Copyright (C) 2024 frosty // // This program is free software: you can redistribute it and/or modify diff --git a/lib/readers/os.go b/lib/readers/os.go index 62e7dce..d93c851 100644 --- a/lib/readers/os.go +++ b/lib/readers/os.go @@ -1,4 +1,4 @@ -// sysinfo queries information about your system +// modbot is a system information agregator // Copyright (C) 2024 frosty // // This program is free software: you can redistribute it and/or modify @@ -18,6 +18,7 @@ package readers import ( "bufio" + "fmt" "os" "strings" ) @@ -29,9 +30,11 @@ type OsInfo struct { } func ReadOs() (OsInfo, error) { - file, err := os.Open("/lib/os-release") + const osPath = "/lib/os-release" + + file, err := os.Open(osPath) if err != nil { - return OsInfo{}, err + return OsInfo{}, fmt.Errorf("failed to open %s: %w", osPath, err) } defer file.Close() @@ -41,18 +44,26 @@ func ReadOs() (OsInfo, error) { for scanner.Scan() { line := scanner.Text() line = strings.ReplaceAll(line, "\"", "") - columns := strings.Split(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 = columns[1] - case strings.HasPrefix(line, "PRETTY_NAME="): - osPrettyName = columns[1] - case strings.HasPrefix(line, "VERSION_ID="): - osVersion = columns[1] + 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, diff --git a/lib/readers/uptime.go b/lib/readers/uptime.go new file mode 100644 index 0000000..4e0501c --- /dev/null +++ b/lib/readers/uptime.go @@ -0,0 +1,94 @@ +// 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 ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const ( + SecondsInMinute = 60 + SecondsInHour = 3600 + SecondsInDay = 86400 +) + +type UptimeInfo uint64 + +func (u UptimeInfo) Days() int { + return int(u) / SecondsInDay +} + +func (u UptimeInfo) Hours() int { + return (int(u) % SecondsInDay) / SecondsInHour +} + +func (u UptimeInfo) Minutes() int { + return (int(u) % SecondsInHour) / SecondsInMinute +} + +func (u UptimeInfo) Seconds() int { + return int(u) % SecondsInMinute +} + +func (u UptimeInfo) String() string { + var builder strings.Builder + + if u.Days() > 0 { + builder.WriteString(fmt.Sprintf("%d days, ", u.Days())) + } + if u.Hours() > 0 { + builder.WriteString(fmt.Sprintf("%d hours, ", u.Hours())) + } + if u.Minutes() > 0 { + builder.WriteString(fmt.Sprintf("%d minutes, ", u.Minutes())) + } + builder.WriteString(fmt.Sprintf("%d seconds", u.Seconds())) + + return builder.String() +} + +func ReadUptime() (UptimeInfo, 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 +} diff --git a/lib/ui/formatter.go b/lib/ui/formatter.go index 45d797b..548051f 100644 --- a/lib/ui/formatter.go +++ b/lib/ui/formatter.go @@ -1,4 +1,4 @@ -// sysinfo queries information about your system +// modbot is a system information agregator // Copyright (C) 2024 frosty // // This program is free software: you can redistribute it and/or modify @@ -19,26 +19,29 @@ package ui import "fmt" const ( - kibibyte = 1 - mebibyte = 1024 * kibibyte - gibibyte = 1024 * mebibyte + Kibibyte = 1 + Mebibyte = 1024 * Kibibyte + Gibibyte = 1024 * Mebibyte + Tebibyte = 1024 * Gibibyte ) -func PrettifySize(sizeKibibytes uint64, decimalPlaces uint8) string { +func PrettifyKib(sizeKibibytes uint64, decimalPlaces uint8) string { var size float64 var unit string - if sizeKibibytes < mebibyte { + if sizeKibibytes < Mebibyte { size = float64(sizeKibibytes) unit = "KiB" - } else if sizeKibibytes < gibibyte { - size = float64(sizeKibibytes) / float64(mebibyte) + } else if sizeKibibytes < Gibibyte { + size = float64(sizeKibibytes) / float64(Mebibyte) unit = "MiB" - } else { - size = float64(sizeKibibytes) / float64(gibibyte) + } else if sizeKibibytes < Tebibyte { + size = float64(sizeKibibytes) / float64(Gibibyte) unit = "GiB" + } else { + size = float64(sizeKibibytes) / float64(Tebibyte) + unit = "TiB" } - format := fmt.Sprintf("%%.%df %s", decimalPlaces, unit) - return fmt.Sprintf(format, size) + return fmt.Sprintf(fmt.Sprintf("%%.%df %s", decimalPlaces, unit), size) } diff --git a/main.go b/main.go index 367e721..b327788 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// sysinfo queries information about your system +// modbot is a system information agregator // Copyright (C) 2024 frosty // // This program is free software: you can redistribute it and/or modify @@ -20,13 +20,16 @@ import ( "fmt" "log" - "codeberg.org/frosty/sysinfo/lib/readers" - "codeberg.org/frosty/sysinfo/lib/ui" + "codeberg.org/frosty/modbot/lib/readers" + "codeberg.org/frosty/modbot/lib/ui" ) const ( - memoryDecimalPlaces = 2 - cpuUsageDecimalPlaces = 2 + MemoryDecimalPlaces = 2 + CpuUsageDecimalPlaces = 2 + CpuTemperatureHwmonName = "hwmon6" + CpuTemperatureTempName = "temp1" + BatteryName = "BAT1" ) func main() { @@ -40,11 +43,33 @@ func main() { if err != nil { log.Fatalf("%v\n", err) } - fmt.Printf("RAM: %v / %v\n", ui.PrettifySize(memoryInfo.Used, memoryDecimalPlaces), ui.PrettifySize(memoryInfo.Total, memoryDecimalPlaces)) + fmt.Printf("RAM: %v / %v\n", ui.PrettifyKib(memoryInfo.Used, MemoryDecimalPlaces), ui.PrettifyKib(memoryInfo.Total, MemoryDecimalPlaces)) cpuUsageInfo, err := readers.ReadCpuUsage() if err != nil { log.Fatalf("%v\n", err) } - fmt.Printf("CPU: %.*f%%\n", cpuUsageDecimalPlaces, cpuUsageInfo.UsagePercent) + cpuTemperatureInfo, err := readers.ReadCpuTemperature(CpuTemperatureHwmonName, CpuTemperatureTempName) + if err != nil { + log.Fatalf("%v\n", err) + } + fmt.Printf("CPU: %.1f°C (%.*f%%)\n", cpuTemperatureInfo, CpuUsageDecimalPlaces, cpuUsageInfo.UsagePercent) + + loadInfo, err := readers.ReadLoad() + if err != nil { + log.Fatalf("%v\n", err) + } + fmt.Printf("Load: %v %v %v\n", loadInfo.OneMinute, loadInfo.FiveMinute, loadInfo.FifteenMinute) + + // batteryInfo, err := readers.ReadBattery(BatteryName) + // if err != nil { + // log.Fatalf("%v\n", err) + // } + // fmt.Printf("Battery: %v%% (%v) w/ %v\n", batteryInfo.Capacity, batteryInfo.Status, batteryInfo.Technology) + + uptimeInfo, err := readers.ReadUptime() + if err != nil { + log.Fatalf("%v\n", err) + } + fmt.Printf("Uptime: %v\n", uptimeInfo) }