mirror of
https://codeberg.org/frosty/modbot.git
synced 2024-09-19 03:36:35 -04:00
collection of changes
updated default config with go functions changed reader function implementations to use only a single function capture outputs in byte arrays and store outputs as byte arrays instead of strings add date reader move some main function code to separate functions move flags parsing to main
This commit is contained in:
parent
8c316dee38
commit
9ec3e94ae5
25
config.go
25
config.go
|
@ -23,30 +23,33 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
delim = "] ["
|
delim = []byte("] [")
|
||||||
prefix = "["
|
prefix = []byte("[")
|
||||||
suffix = "]"
|
suffix = []byte("]")
|
||||||
)
|
)
|
||||||
|
|
||||||
var modules = []Module{
|
var modules = []Module{
|
||||||
{
|
{
|
||||||
Func: readers.ReadExec("statusbar cpu"),
|
Func: readers.ReadCpuUsage(),
|
||||||
Interval: 5 * time.Second,
|
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,
|
Signal: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Func: readers.ReadExec("statusbar battery"),
|
Func: readers.ReadDate("15:04:05"),
|
||||||
Interval: 60 * time.Second,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Func: readers.ReadExec("statusbar date"),
|
|
||||||
Interval: 1 * time.Second,
|
Interval: 1 * time.Second,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Func: readers.ReadExec("statusbar loadavg"),
|
Func: readers.ReadLoad(),
|
||||||
Interval: 5 * time.Second,
|
Interval: 5 * time.Second,
|
||||||
|
Template: "{{.OneMinute}}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,62 +116,58 @@ func BatteryTechnologyFromStr(technologyStr string) BatteryTechnology {
|
||||||
return TechnologyUnknown
|
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) {
|
func ReadBattery(batteryName string) func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,33 +25,29 @@ import (
|
||||||
|
|
||||||
type CpuTemperatureInfo float32
|
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) {
|
func ReadCpuTemperature(hwmonName, tempName string) func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,56 +34,52 @@ func (cu CpuUsageInfo) String() string {
|
||||||
return fmt.Sprintf("%d%%", uint(cu.UsagePercent))
|
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) {
|
func ReadCpuUsage() func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
lib/readers/date.go
Normal file
30
lib/readers/date.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// modbot is a system information agregator
|
||||||
|
// Copyright (C) 2024 frosty <inthishouseofcards@gmail.com>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,34 +25,31 @@ import (
|
||||||
|
|
||||||
type ExecInfo string
|
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) {
|
func ReadExec(command string) func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,36 +33,32 @@ func (l LoadInfo) String() string {
|
||||||
return fmt.Sprintf("%v %v %v", l.OneMinute, l.FiveMinute, l.FifteenMinute)
|
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) {
|
func ReadLoad() func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,61 +37,57 @@ type MemoryInfo struct {
|
||||||
UsedPretty string
|
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) {
|
func ReadMemory() func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,50 +29,46 @@ type OsInfo struct {
|
||||||
Version string
|
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) {
|
func ReadOs() func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,36 +65,32 @@ func (u UptimeInfo) String() string {
|
||||||
return builder.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) {
|
func ReadUptime() func() (interface{}, error) {
|
||||||
return 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
214
main.go
214
main.go
|
@ -36,10 +36,11 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
updateChan = make(chan int)
|
updateChan = make(chan int)
|
||||||
moduleOutputs = make([]string, len(modules))
|
moduleOutputs = make([][]byte, len(modules))
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
mutex sync.Mutex
|
sigChan = make(chan os.Signal, 1024)
|
||||||
lastOutput string
|
signalMap = make(map[os.Signal][]*Module)
|
||||||
|
|
||||||
// X connection data
|
// X connection data
|
||||||
x *xgb.Conn
|
x *xgb.Conn
|
||||||
|
@ -62,130 +63,149 @@ type Module struct {
|
||||||
|
|
||||||
func (m *Module) Run() {
|
func (m *Module) Run() {
|
||||||
if m.pos < 0 || m.pos >= len(modules) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
|
||||||
info, err := m.Func()
|
info, err := m.Func()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
moduleOutputs[m.pos] = "failed"
|
output.WriteString("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()
|
|
||||||
} else {
|
} else {
|
||||||
// Use the output as is
|
if m.Template != "" {
|
||||||
output = fmt.Sprintf("%v", info)
|
// 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()
|
mutex.Lock()
|
||||||
moduleOutputs[m.pos] = output
|
moduleOutputs[m.pos] = output.Bytes()
|
||||||
updateChan <- 1
|
updateChan <- 1
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlags() Flags {
|
func (m *Module) Init(pos int) {
|
||||||
var flags Flags
|
m.pos = pos
|
||||||
flag.BoolVar(&flags.SetXRootName, "x", false, "set x root window name")
|
|
||||||
|
|
||||||
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() {
|
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 {
|
if flags.SetXRootName {
|
||||||
var err error
|
var err error
|
||||||
x, err = xgb.NewConn()
|
x, root, err = grabXRootWindow()
|
||||||
if err != nil {
|
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
|
// Initialize modules
|
||||||
for i := range modules {
|
for i := range modules {
|
||||||
go func(m *Module, i int) {
|
go modules[i].Init(i)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update output on difference
|
// Monitor changes to the combined output
|
||||||
go func() {
|
go monitorUpdates(flags.SetXRootName)
|
||||||
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()
|
|
||||||
|
|
||||||
// Output to either X root window name or stdout based on flags
|
// Handle signals sent for modules
|
||||||
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
|
|
||||||
for sig := range sigChan {
|
for sig := range sigChan {
|
||||||
go func(sig *os.Signal) {
|
go handleSignal(sig)
|
||||||
ms := signalMap[*sig]
|
|
||||||
for _, m := range ms {
|
|
||||||
go m.Run()
|
|
||||||
}
|
|
||||||
}(&sig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue