7 Commits
v1.4 ... v1.7

Author SHA1 Message Date
eleksir
12f4dc75f0 Fix linter warnings 2025-06-24 16:13:36 +03:00
eleksir
913fcc34ee Fix incorrect display measurement units in swap calculation in mem plugin 2025-06-24 16:12:48 +03:00
eleksir
ac856f4174 Fix empty font name and incorrect font size name in VPN plugin output 2025-06-24 16:11:43 +03:00
eleksir
a5d793926b Fix typo in README.md 2025-06-01 18:59:15 +03:00
eleksir
40a94bd001 Add missing runcommand.go 2025-06-01 18:57:47 +03:00
eleksir
d19836b821 Update README.md 2025-05-19 12:27:56 +03:00
eleksir
32d865e24e Implement cmdrun config parameter 2025-05-19 12:19:25 +03:00
11 changed files with 368 additions and 21 deletions

View File

@@ -4,6 +4,7 @@ BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
BINARY=i3status-go
TEST1=battery-test
TEST2=cmdrun-test
## Use calssic targets where first one is deafult target
all: clean build
@@ -16,15 +17,18 @@ battery-test:
rm -rf ${TEST1}
go build ${BUILDOPTS} -o ${TEST1} ./cmd/${TEST1}
cmdrun-test:
rm -rf ${TEST1}
go build ${BUILDOPTS} -o ${TEST2} ./cmd/${TEST2}
## Remove binary with golang compiler' means
clean:
rm -rf ${BINARY} ${TEST1}
rm -rf ${BINARY} ${TEST1} ${TEST2}
## Misc target, for development purposes. Updates vendored libs, brutal way.
upgrade:
$(RM) -r vendor
go get -u -t -tool ./...
go get -u -t ./...
go mod tidy
go mod vendor

View File

@@ -14,11 +14,12 @@ It is i3bar text generator.
* PulseAudio volume indicator, can adjust master volume too
* Clock
* Cron jobs (intended to use for show periodic desktop notifications but not limited to)
* Show output of one-shot system command
## How to build it
This appliction was tested to compile with go v1.21 but can be built with earlier versions (seems down to 1.18, not
sure).
This application was tested to compile with go v1.24.
All you need is go lang compiler and gnu make utility. Invoke
```bash

View File

@@ -702,6 +702,78 @@
]
},
// Prints stdout of command to panel.
"cmdrun" : {
"enabled": false,
// Delay between two consecutive command batch runs.
"delay": 3,
// command' binary.
"cmd": "/bin/bash",
// Arguments for command binary.
"args": ["-c", "echo 42"],
// If omitted set to default color defined up here.
"color": "#3e78fd",
// If omitted set to default background color defined up here.
"background": "#000000",
// If omitted set to default font defined up here.
"font": "Noto Sans",
// ‘xx-small’, ‘x-small’, ‘small’, ‘medium’, ‘large’, ‘x-large’, ‘xx-large’, or one of relative sizes
// ‘smaller’ or ‘larger’. If omitted set to default font size defined up here.
"font_size": "medium",
// Re-define separator parameters for each command block here.
"separator": {
"left": {
// Set to false if omitted.
"enabled": true,
// If omitted set to default color defined for separator up here.
"color": "#3e78fd",
// If omitted set to default background color defined for separator up here.
"background": "#000000",
// If omitted set to default symbol defined for separator up here.
"symbol": "▶",
// If omitted set to default font defined for separator up here.
"font": "Noto Sans",
// ‘xx-small’, ‘x-small’, ‘small’, ‘medium’, ‘large’, ‘x-large’, ‘xx-large’, or one of relative sizes
// ‘smaller’ or ‘larger’. If omitted set to default font size defined for separator up here.
"font_size": "medium"
},
"right": {
// Set to false if omitted.
"enabled": true,
// If omitted set to default color defined for separator up here.
"color": "#3e78fd",
// If omitted set to default background color defined for separator up here.
"background": "#000000",
// If omitted set to default symbol defined for separator up here.
"symbol": "◀",
// If omitted set to default font defined for separator up here.
"font": "Noto Sans",
// ‘xx-small’, ‘x-small’, ‘small’, ‘medium’, ‘large’, ‘x-large’, ‘xx-large’, or one of relative sizes
// ‘smaller’ or ‘larger’. If omitted set to default font size defined for separator up here.
"font_size": "medium"
}
}
}
// Whether to display Application Buttons.
"app_buttons": {
"enabled": true,

View File

@@ -84,6 +84,14 @@ func main() {
Conf.Clock.FontSize,
)
Conf.Values.RunCommandOutput = fmt.Sprintf(
"<span color='%s' background='%s' font='%s' size='%s'>?</span>",
Conf.CmdRun.Color,
Conf.CmdRun.Background,
Conf.CmdRun.Font,
Conf.CmdRun.FontSize,
)
go Conf.Spawner()
go Conf.ParseStdin()
go Conf.CleanZombies()
@@ -143,6 +151,10 @@ func main() {
go Conf.UpdateVPNStatus()
}
if Conf.CmdRun.Enabled {
go Conf.RunCommand()
}
/*
I3bar documentation pretends that message protocol must be valid json. In practice, we only have to print valid
header, empty json array and (potentially infinite) json lines (line that is valid json by itself) that is
@@ -519,6 +531,50 @@ func main() {
j = append(j, b)
}
if Conf.CmdRun.Enabled {
var b lib.I3BarOutBlock
b.Name = `runcommandoutput`
b.Color = Conf.CmdRun.Color
b.Background = Conf.CmdRun.Background
if Conf.CmdRun.Separator.Left.Enabled {
b.FullText = fmt.Sprintf(
"<span color='%s' background='%s' font='%s' size='%s'>%s</span>",
Conf.CmdRun.Separator.Left.Color,
Conf.CmdRun.Separator.Left.Background,
Conf.CmdRun.Separator.Left.Font,
Conf.CmdRun.Separator.Left.FontSize,
Conf.CmdRun.Separator.Left.Symbol,
)
}
b.FullText += fmt.Sprintf(
"<span color='%s' background='%s' font='%s' size='%s'>%s</span>",
Conf.CmdRun.Color,
Conf.CmdRun.Background,
Conf.CmdRun.Font,
Conf.CmdRun.FontSize,
Conf.Values.RunCommandOutput,
)
if Conf.CmdRun.Separator.Right.Enabled {
b.FullText += fmt.Sprintf(
"<span color='%s' background='%s' font='%s' size='%s'>%s</span>",
Conf.CmdRun.Separator.Right.Color,
Conf.CmdRun.Separator.Right.Background,
Conf.CmdRun.Separator.Right.Font,
Conf.CmdRun.Separator.Right.FontSize,
Conf.CmdRun.Separator.Right.Symbol,
)
}
b.Markup = "pango"
b.Separator = false
j = append(j, b)
}
if Conf.Clock.Enabled {
var b lib.I3BarOutBlock

View File

@@ -1 +1 @@
/home/eleksir/git/i3status-go/cmd/i3status-go/default-config.json
../cmd/i3status-go/default-config.json

View File

@@ -29,7 +29,7 @@ func (c *MyConfig) UpdateBatteryInfo() {
}
var (
batteries []*battery.Battery = []*battery.Battery{}
batteries = []*battery.Battery{}
Batts string
ch int
status string
@@ -52,6 +52,7 @@ func (c *MyConfig) UpdateBatteryInfo() {
if err != nil {
log.Printf("Unable to read file %s: %s", file, err)
continue
}
@@ -65,6 +66,7 @@ func (c *MyConfig) UpdateBatteryInfo() {
if err != nil {
log.Printf("Unable to read file %s: %s", statusFile, err)
continue
}
@@ -91,6 +93,7 @@ func (c *MyConfig) UpdateBatteryInfo() {
if err != nil {
log.Printf("Unable to convert string from file %s to integer", file)
continue
}
@@ -100,9 +103,9 @@ func (c *MyConfig) UpdateBatteryInfo() {
}
}
} else {
batteries, _ = battery.GetAll()
// In theory, this module should give for each entry its separate err, but in practice it gives
// one single err for all entries, so we cannot detemine whist exatly entry errored.
batteries, _ = battery.GetAll()
}
var (

View File

@@ -69,6 +69,7 @@ type MyConfig struct {
La string
PA *p.Client
SoundVolume string
RunCommandOutput string
}
Channels struct {
@@ -231,6 +232,18 @@ type MyConfig struct {
} `json:"tasks,omitempty"`
} `json:"cron,omitempty"`
CmdRun struct {
Enabled bool `json:"enabled,omitempty"`
Color string `json:"color,omitempty"`
Background string `json:"background,omitempty"`
Font string `json:"font,omitempty"`
FontSize string `json:"font_size,omitempty"`
Separator Separator `json:"separator,omitempty"`
Cmd string `json:"cmd,omitempty"`
Delay int `json:"delay,omitempty"`
Args []string `json:"args,omitempty"`
}
AppButtons struct {
Enabled bool `json:"enabled,omitempty"`
Separator Separator `json:"separator,omitempty"`
@@ -1036,7 +1049,7 @@ func ReadConf(defaultConfig []byte) (*MyConfig, error) {
}
if sampleConfig.Vpn.Font == "" {
sampleConfig.Vpn.FontSize = sampleConfig.Font
sampleConfig.Vpn.Font = sampleConfig.Font
}
if sampleConfig.Vpn.FontSize == "" {
@@ -1446,6 +1459,122 @@ func ReadConf(defaultConfig []byte) (*MyConfig, error) {
}
}
// sampleConfig.CmdRun.Enabled will false if not set in config
if sampleConfig.CmdRun.Cmd == "" && sampleConfig.CmdRun.Enabled {
sampleConfig.CmdRun.Enabled = false
log.Print(
"Setting sampleConfig.CmdRun.Enabled to false because sampleConfig.CmdRun.Cmd is not set",
)
}
// sampleConfig.CmdRun.Args can be empty
if sampleConfig.CmdRun.Color == "" {
sampleConfig.CmdRun.Color = sampleConfig.Color
}
if sampleConfig.CmdRun.Background == "" {
sampleConfig.CmdRun.Background = sampleConfig.Background
}
if sampleConfig.CmdRun.Font == "" {
sampleConfig.CmdRun.Font = sampleConfig.Font
}
if sampleConfig.CmdRun.FontSize == "" {
sampleConfig.CmdRun.FontSize = sampleConfig.FontSize
}
if sampleConfig.CmdRun.Separator.Left.Color == "" {
sampleConfig.CmdRun.Separator.Left.Color = sampleConfig.Separator.Left.Color
}
if sampleConfig.CmdRun.Separator.Left.Background == "" {
sampleConfig.CmdRun.Separator.Left.Background = sampleConfig.Separator.Left.Background
}
if sampleConfig.CmdRun.Separator.Left.Symbol == "" {
sampleConfig.CmdRun.Separator.Left.Symbol = sampleConfig.Separator.Left.Symbol
}
if sampleConfig.CmdRun.Separator.Left.Font == "" {
sampleConfig.CmdRun.Separator.Left.Font = sampleConfig.Separator.Left.Font
}
if sampleConfig.CmdRun.Separator.Left.FontSize == "" {
sampleConfig.CmdRun.Separator.Left.FontSize = sampleConfig.Separator.Left.FontSize
} else {
matched, err := regexp.MatchString(
`^(xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger)$`,
sampleConfig.CmdRun.Separator.Left.FontSize,
)
if err != nil {
log.Printf(
"Unable to set sampleConfig.CmdRun.Separator.Left.FontSize: %s, fallback to %s",
err,
sampleConfig.Separator.Left.FontSize,
)
sampleConfig.CmdRun.Separator.Left.FontSize = sampleConfig.Separator.Left.FontSize
}
if !matched {
log.Printf(
"Unable to set sampleConfig.CmdRun.Separator.Left.FontSize, fallback to %s",
sampleConfig.Separator.Left.FontSize,
)
sampleConfig.CmdRun.Separator.Left.FontSize = sampleConfig.Separator.Left.FontSize
}
}
if sampleConfig.CmdRun.Separator.Right.Color == "" {
sampleConfig.CmdRun.Separator.Right.Color = sampleConfig.Separator.Right.Color
}
if sampleConfig.CmdRun.Separator.Right.Background == "" {
sampleConfig.CmdRun.Separator.Right.Background = sampleConfig.Separator.Right.Background
}
if sampleConfig.CmdRun.Separator.Right.Symbol == "" {
sampleConfig.CmdRun.Separator.Right.Symbol = sampleConfig.Separator.Right.Symbol
}
if sampleConfig.CmdRun.Separator.Right.Font == "" {
sampleConfig.CmdRun.Separator.Right.Font = sampleConfig.Separator.Right.Font
}
if sampleConfig.CmdRun.Separator.Right.FontSize == "" {
sampleConfig.CmdRun.Separator.Right.FontSize = sampleConfig.Separator.Right.FontSize
} else {
matched, err := regexp.MatchString(
`^(xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger)$`,
sampleConfig.CmdRun.Separator.Right.FontSize,
)
if err != nil {
log.Printf(
"Unable to set sampleConfig.CmdRun.Separator.Right.FontSize: %s, fallback to %s",
err,
sampleConfig.Separator.Right.FontSize,
)
sampleConfig.CmdRun.Separator.Right.FontSize = sampleConfig.Separator.Right.FontSize
}
if !matched {
log.Printf(
"Unable to set sampleConfig.CmdRun.Separator.Right.FontSize, fallback to %s",
sampleConfig.Separator.Right.FontSize,
)
sampleConfig.CmdRun.Separator.Right.FontSize = sampleConfig.Separator.Right.FontSize
}
}
// sampleConfig.AppButtons.Enabled will false if not set in config
if sampleConfig.AppButtons.Separator.Left.Color == "" {

View File

@@ -51,7 +51,7 @@ func (c *MyConfig) UpdateMemStats() {
if c.Values.Memory.Usedpct != uint64(v.UsedPercent) || c.Values.Memory.Shared != v.Shared/1024/1024 || c.Values.Memory.Swap != v.SwapTotal-v.SwapFree {
c.Values.Memory.Usedpct = uint64(v.UsedPercent)
c.Values.Memory.Shared = v.Shared / 1024 / 1024
c.Values.Memory.Swap = sw.Used
c.Values.Memory.Swap = sw.Used / 1024 / 1024
c.Channels.UpdateReady <- true
}
} else {

View File

@@ -0,0 +1,41 @@
package lib
import (
"log"
"time"
)
// RunCommands gets list of commands runs them sequentally and conacatenates their output.
func (c *MyConfig) RunCommand() {
var (
InitialDelay = 100 * time.Millisecond
LoopIterationDelay = time.Duration(c.CmdRun.Delay) * time.Second
Delay = InitialDelay
ticker = time.NewTicker(Delay)
)
for range ticker.C {
if Delay == InitialDelay {
Delay = LoopIterationDelay
ticker.Reset(Delay)
}
var (
outputString string
command = []string{c.CmdRun.Cmd}
)
if len(c.CmdRun.Args) > 0 {
command = append(command, c.CmdRun.Args...)
}
outputString += RunProcess(command)
log.Println(c.Values.RunCommandOutput)
if c.Values.RunCommandOutput != outputString {
c.Values.RunCommandOutput = outputString
c.Channels.UpdateReady <- true
}
}
}

View File

@@ -1,9 +1,11 @@
package lib
import (
"bytes"
"log"
"os"
"os/exec"
"strings"
"syscall"
"time"
)
@@ -12,6 +14,7 @@ import (
func (c *MyConfig) Spawner() {
for prg := range c.Channels.RunChan {
<-time.After(25 * time.Millisecond)
devnullR, _ := os.Open(os.DevNull)
devnullW, _ := os.OpenFile(os.DevNull, os.O_WRONLY|os.O_APPEND, 0644)
@@ -55,3 +58,41 @@ func (c *MyConfig) CleanZombies() {
time.Sleep(1 * time.Minute)
}
}
// RunProcess runs given command and returns contens of command stdout. On error prints error on stderr and returns
// empty syting.
func RunProcess(command []string) string {
var (
cmd *exec.Cmd
stdout, stderr bytes.Buffer
)
switch len(command) {
case 1:
cmd = exec.Command(command[0], "") //nolint: gosec
default:
cmd = exec.Command(command[0], command[1:]...) //nolint: gosec
}
cmd.Dir = "/"
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
if stdout.String() != "" {
log.Printf("Unable to run '%s': %s, %s", strings.Join(command, " "), err, stdout.String())
} else {
log.Printf("Unable to run '%s': %s", strings.Join(command, " "), err)
}
return ""
}
if stderr.String() != "" {
log.Printf("%s: %s", strings.Join(command, " "), stderr.String())
}
return strings.TrimRight(stdout.String(), "\n")
}

View File

@@ -70,7 +70,7 @@ func (c *MyConfig) UpdateVPNStatus() {
func (c *MyConfig) VPNTCPCheck() bool {
conn, err := net.DialTimeout(
"tcp",
fmt.Sprintf("%s:%d", c.Vpn.TCPCheck.Host, c.Vpn.TCPCheck.Port),
fmt.Sprintf("%s:%d", c.Vpn.TCPCheck.Host, c.Vpn.TCPCheck.Port), //nolint: hostport
time.Second*time.Duration(c.Vpn.TCPCheck.Timeout),
)