From a19aed960ce5594a60ef7bf76bfa76a03b832b5d Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Thu, 21 Feb 2019 15:25:56 +0300 Subject: [PATCH 01/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39eb608..db005d3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Yet Another i3status replacement written in Go. [![Twitter](https://img.shields.io/twitter/url/https/github.com/burik666/yagostatus.svg?style=social)](https://twitter.com/intent/tweet?text=Yet%20Another%20i3status%20replacement%20written%20in%20Go.%0A&url=https%3A%2F%2Fgithub.com%2Fburik666%2Fyagostatus&hashtags=i3,i3wm,i3status,golang) -[![yagostatus](https://gist.githubusercontent.com/burik666/da88db63b9a85863ad585646c03488b8/raw/yagostatus_example.gif)](https://gist.github.com/burik666/da88db63b9a85863ad585646c03488b8) +[![yagostatus.gif](https://raw.githubusercontent.com/wiki/burik666/yagostatus/yagostatus.gif)](https://github.com/burik666/yagostatus/wiki/Conky) ## Features - Instant and independent updating of widgets. From 3989c705052c6f164d73c17481bb946ce72e2440 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Fri, 22 Feb 2019 01:47:52 +0300 Subject: [PATCH 02/34] YaGoStatus v0.4.0 --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index 0be71ea..57c29d1 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package main // Version contains YaGoStatus version. -const Version = "0.3.0" +const Version = "0.4.0" From 0de65e826f30cba19b618db9af6644e225f2892c Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Thu, 28 Feb 2019 07:13:55 +0300 Subject: [PATCH 03/34] Add i3-gaps border_top/bottom/left/right --- ygs/protocol.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ygs/protocol.go b/ygs/protocol.go index 1a98cf1..02db634 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -14,6 +14,10 @@ type I3BarBlock struct { ShortText string `json:"short_text,omitempty"` Color string `json:"color,omitempty"` BorderColor string `json:"border,omitempty"` + BorderTop *uint16 `json:"border_top,omitempty"` + BorderBottom *uint16 `json:"border_bottom,omitempty"` + BorderLeft *uint16 `json:"border_left,omitempty"` + BorderRight *uint16 `json:"border_right,omitempty"` BackgroundColor string `json:"background,omitempty"` Markup string `json:"markup,omitempty"` MinWidth string `json:"min_width,omitempty"` From 38f04f899485083680052ac9d8fbbb2fa715b3d6 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 4 Mar 2019 13:28:41 +0300 Subject: [PATCH 04/34] Add `signal` to `exec` widget --- README.md | 14 +++++++++++ internal/pkg/signals/signals.go | 15 ++++++++++++ widgets/exec.go | 43 ++++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 internal/pkg/signals/signals.go diff --git a/README.md b/README.md index db005d3..092cef5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Yet Another i3status replacement written in Go. - Different widgets on different workspaces. - Templates for widgets outputs. - Update widget via http/websocket requests. +- Update widget by POSIX Real-Time Signals (SIGRTMIN-SIGRTMAX). ## Installation @@ -162,6 +163,11 @@ This widget runs the command at the specified interval. - `command` - Command to execute (via `sh -c`). - `interval` - Update interval in seconds (set 0 to run once at start). - `events_update` - Update widget if an event occurred (default: `false`). +- `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. + +Use pkill to send signals: + + pkill -SIGRTMIN+1 yagostatus ### Widget `wrapper` @@ -199,6 +205,13 @@ Send an empty array to clear: ## Examples ### Volume control +i3 config: +``` +bindsym XF86AudioLowerVolume exec amixer -c 1 -q set Master 1%-; exec pkill -SIGRTMIN+1 yagostatus +bindsym XF86AudioRaiseVolume exec amixer -c 1 -q set Master 1%+; exec pkill -SIGRTMIN+1 yagostatus +bindsym XF86AudioMute exec amixer -q set Master toggle; exec pkill -SIGRTMIN+1 yagostatus +``` + ```yml - widget: exec command: | @@ -210,6 +223,7 @@ Send an empty array to clear: echo -e '[{"full_text": "\xF0\x9F\x94\x8A '${res[0]}'", "color": "'$color'"}]' interval: 0 + signal: 1 events_update: true events: - button: 1 diff --git a/internal/pkg/signals/signals.go b/internal/pkg/signals/signals.go new file mode 100644 index 0000000..805effc --- /dev/null +++ b/internal/pkg/signals/signals.go @@ -0,0 +1,15 @@ +package signals + +// #include +import "C" + +// SIGRTMIN signal +var SIGRTMIN int + +// SIGRTMAX signal +var SIGRTMAX int + +func init() { + SIGRTMIN = int(C.int(C.SIGRTMIN)) + SIGRTMAX = int(C.int(C.SIGRTMAX)) +} diff --git a/widgets/exec.go b/widgets/exec.go index 69884a6..7626727 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -2,14 +2,17 @@ package widgets import ( "encoding/json" - "errors" - "log" "os" "os/exec" + "os/signal" "strings" + "syscall" "time" + "github.com/burik666/yagostatus/internal/pkg/signals" "github.com/burik666/yagostatus/ygs" + + "github.com/pkg/errors" ) // ExecWidget implements the exec widget. @@ -17,6 +20,7 @@ type ExecWidget struct { command string interval time.Duration eventsUpdate bool + signal os.Signal c chan<- []ygs.I3BarBlock } @@ -43,6 +47,14 @@ func NewExecWidget(params map[string]interface{}) (ygs.Widget, error) { w.eventsUpdate = false } + v, ok = params["signal"] + if ok { + sig := v.(int) + if sig < 0 || signals.SIGRTMIN+sig > signals.SIGRTMAX { + return nil, errors.Errorf("signal should be between 0 AND %d", signals.SIGRTMAX-signals.SIGRTMIN) + } + w.signal = syscall.Signal(signals.SIGRTMIN + sig) + } return w, nil } @@ -57,7 +69,6 @@ func (w *ExecWidget) exec() error { var blocks []ygs.I3BarBlock err = json.Unmarshal(output, &blocks) if err != nil { - log.Printf("Failed to parse output: %s", err) blocks = append(blocks, ygs.I3BarBlock{FullText: strings.Trim(string(output), "\n ")}) } w.c <- blocks @@ -68,13 +79,33 @@ func (w *ExecWidget) exec() error { // Run starts the main loop. func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { w.c = c - if w.interval == 0 { + if w.interval == 0 && w.signal == nil { return w.exec() } - ticker := time.NewTicker(w.interval) + upd := make(chan struct{}, 1) + + if w.signal != nil { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, w.signal) + go (func() { + for { + <-sigc + upd <- struct{}{} + } + })() + } + if w.interval > 0 { + ticker := time.NewTicker(w.interval) + go (func() { + for { + <-ticker.C + upd <- struct{}{} + } + })() + } - for ; true; <-ticker.C { + for ; true; <-upd { err := w.exec() if err != nil { return err From 9c59c9d0f09f14733a737810f30192b9d0256f05 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Tue, 5 Mar 2019 09:08:48 +0300 Subject: [PATCH 05/34] Refactoring and improvements --- README.md | 8 +- go.mod | 7 +- go.sum | 14 +-- internal/pkg/config/config.go | 39 ++++++-- main.go | 5 +- widgets/blank.go | 13 +-- widgets/clock.go | 44 +++++---- widgets/exec.go | 87 +++++++++--------- widgets/http.go | 89 +++++++++++------- widgets/static.go | 26 ++++-- widgets/wrapper.go | 32 +++++-- yagostatus.go | 164 +++++++++++++++++++--------------- ygs/protocol.go | 61 +++++++++++++ ygs/utils.go | 61 ++++++++++--- ygs/widget.go | 62 ------------- 15 files changed, 417 insertions(+), 295 deletions(-) diff --git a/README.md b/README.md index 092cef5..6afa975 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,7 @@ Yet Another i3status replacement written in Go. go get github.com/burik666/yagostatus cp $GOPATH/src/github.com/burik666/yagostatus/yagostatus.yml ~/.config/i3/yagostatus.yml -Get the absolute path to the yagostatus binary: - - $ echo $GOPATH/bin/yagostatus - /home/burik/go/bin/yagostatus - - -Replace `status_command` to `~/go/bin/yagostatus` in your i3 config file. +Replace `status_command` to `~/go/bin/yagostatus --config ~/.config/i3/yagostatus.yml` in your i3 config file. ### Troubleshooting Yagostatus outputs error messages in stderr, you can log them by redirecting stderr to a file. diff --git a/go.mod b/go.mod index 270d591..174adc3 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/burik666/yagostatus +go 1.12 + require ( - github.com/pkg/errors v0.8.1 go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd - golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc - gopkg.in/yaml.v2 v2.2.1 + golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index eb17012..85f3391 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,16 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOC github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e h1:4ZrkT/RzpnROylmoQL57iVUL57wGKTR5O6KpVnbm2tA= github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd h1:PVrHRo3MP4x/+tbG01rmFnp1sJ1GC7laHJ56OedXYp0= go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd/go.mod h1:7w17+r1F28Yxb1pHu8SxARJo+RGvJCAGOEf+GLw+2aQ= -golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQIXlnN3/yXxbT6/awxI= -golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU= +golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 9e84eee..1cfd255 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -2,12 +2,13 @@ package config import ( "encoding/json" + "errors" + "fmt" "io/ioutil" "strings" "github.com/burik666/yagostatus/ygs" - "github.com/pkg/errors" "gopkg.in/yaml.v2" ) @@ -62,7 +63,7 @@ func (e WidgetEventConfig) Validate() error { } } if !found { - return errors.Errorf("Unknown '%s' modifier", mod) + return fmt.Errorf("Unknown '%s' modifier", mod) } } return nil @@ -79,19 +80,15 @@ func LoadFile(filename string) (*Config, error) { // Parse parses config. func Parse(data []byte) (*Config, error) { - var raw struct { Widgets []map[string]interface{} `yaml:"widgets"` } config := Config{} if err := yaml.Unmarshal(data, &config); err != nil { - return nil, err - } - - if err := yaml.Unmarshal(data, &raw); err != nil { - return nil, err + return nil, trimYamlErr(err, false) } + yaml.Unmarshal(data, &raw) for widgetIndex := range config.Widgets { widget := &config.Widgets[widgetIndex] @@ -104,9 +101,24 @@ func Parse(data []byte) (*Config, error) { } } + tmp, _ := yaml.Marshal(params["events"]) + if err := yaml.UnmarshalStrict(tmp, &widget.Events); err != nil { + name, params := ygs.ErrorWidget(trimYamlErr(err, true).Error()) + *widget = WidgetConfig{ + Name: name, + Params: params, + } + continue + } + widget.Params = params if err := widget.Validate(); err != nil { - return nil, err + name, params := ygs.ErrorWidget(trimYamlErr(err, true).Error()) + *widget = WidgetConfig{ + Name: name, + Params: params, + } + continue } delete(params, "widget") @@ -116,3 +128,12 @@ func Parse(data []byte) (*Config, error) { } return &config, nil } + +func trimYamlErr(err error, trimLineN bool) error { + msg := strings.TrimPrefix(err.Error(), "yaml: unmarshal errors:\n ") + if trimLineN { + msg = strings.TrimPrefix(msg, "line ") + msg = strings.TrimLeft(msg, "1234567890: ") + } + return errors.New(msg) +} diff --git a/main.go b/main.go index 0359d5b..763bba4 100644 --- a/main.go +++ b/main.go @@ -63,10 +63,12 @@ func main() { log.Fatalf("Failed to parse builtin config: %s", err) } } + if cfgError != nil { + cfg = &config.Config{} + } } else { cfg, cfgError = config.LoadFile(configFile) if cfgError != nil { - log.Printf("Failed to load config file: %s", cfgError) cfg = &config.Config{} } } @@ -76,6 +78,7 @@ func main() { log.Fatalf("Failed to create yagostatus instance: %s", err) } if cfgError != nil { + log.Printf("Failed to load config: %s", cfgError) yaGoStatus.errorWidget(cfgError.Error()) } diff --git a/widgets/blank.go b/widgets/blank.go index 37b13d5..700df84 100644 --- a/widgets/blank.go +++ b/widgets/blank.go @@ -5,11 +5,18 @@ import ( "github.com/burik666/yagostatus/ygs" ) +// BlankWidgetParams are widget parameters. +type BlankWidgetParams struct{} + // BlankWidget is a widgets template. type BlankWidget struct{} +func init() { + ygs.RegisterWidget("blank", NewBlankWidget, BlankWidgetParams{}) +} + // NewBlankWidget returns a new BlankWidget. -func NewBlankWidget(params map[string]interface{}) (ygs.Widget, error) { +func NewBlankWidget(params interface{}) (ygs.Widget, error) { return &BlankWidget{}, nil } @@ -23,7 +30,3 @@ func (w *BlankWidget) Event(event ygs.I3BarClickEvent) {} // Stop shutdowns the widget. func (w *BlankWidget) Stop() {} - -func init() { - ygs.RegisterWidget("blank", NewBlankWidget) -} diff --git a/widgets/clock.go b/widgets/clock.go index 32ea8ed..0a84550 100644 --- a/widgets/clock.go +++ b/widgets/clock.go @@ -1,49 +1,49 @@ package widgets import ( - "errors" "time" "github.com/burik666/yagostatus/ygs" ) +// ClockWidgetParams are widget parameters. +type ClockWidgetParams struct { + Interval uint + Format string +} + // ClockWidget implements a clock. type ClockWidget struct { - format string - interval time.Duration + params ClockWidgetParams } -// NewClockWidget returns a new ClockWidget. -func NewClockWidget(params map[string]interface{}) (ygs.Widget, error) { - w := &ClockWidget{} - - v, ok := params["format"] - if !ok { - return nil, errors.New("missing 'format' setting") - } - w.format = v.(string) +func init() { + ygs.RegisterWidget("clock", NewClockWidget, ClockWidgetParams{ + Interval: 1, + Format: "Jan _2 Mon 15:04:05", + }) +} - v, ok = params["interval"] - if ok { - w.interval = time.Duration(v.(int)) * time.Second - } else { - w.interval = time.Second +// NewClockWidget returns a new ClockWidget. +func NewClockWidget(params interface{}) (ygs.Widget, error) { + w := &ClockWidget{ + params: params.(ClockWidgetParams), } return w, nil } // Run starts the main loop. func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error { - ticker := time.NewTicker(w.interval) + ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) res := []ygs.I3BarBlock{ ygs.I3BarBlock{}, } - res[0].FullText = time.Now().Format(w.format) + res[0].FullText = time.Now().Format(w.params.Format) c <- res for { select { case t := <-ticker.C: - res[0].FullText = t.Format(w.format) + res[0].FullText = t.Format(w.params.Format) c <- res } } @@ -54,7 +54,3 @@ func (w *ClockWidget) Event(event ygs.I3BarClickEvent) {} // Stop shutdowns the widget. func (w *ClockWidget) Stop() {} - -func init() { - ygs.RegisterWidget("clock", NewClockWidget) -} diff --git a/widgets/exec.go b/widgets/exec.go index 7626727..34bd9cc 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -2,6 +2,8 @@ package widgets import ( "encoding/json" + "errors" + "fmt" "os" "os/exec" "os/signal" @@ -11,47 +13,43 @@ import ( "github.com/burik666/yagostatus/internal/pkg/signals" "github.com/burik666/yagostatus/ygs" - - "github.com/pkg/errors" ) +// ExecWidgetParams are widget parameters. +type ExecWidgetParams struct { + Command string + Interval uint + EventsUpdate bool `yaml:"events_update"` + Signal *int +} + // ExecWidget implements the exec widget. type ExecWidget struct { - command string - interval time.Duration - eventsUpdate bool - signal os.Signal - c chan<- []ygs.I3BarBlock -} + params ExecWidgetParams -// NewExecWidget returns a new ExecWidget. -func NewExecWidget(params map[string]interface{}) (ygs.Widget, error) { - w := &ExecWidget{} + signal os.Signal + c chan<- []ygs.I3BarBlock + upd chan struct{} +} - v, ok := params["command"] - if !ok { - return nil, errors.New("missing 'command' setting") - } - w.command = v.(string) +func init() { + ygs.RegisterWidget("exec", NewExecWidget, ExecWidgetParams{}) +} - v, ok = params["interval"] - if !ok { - return nil, errors.New("missing 'interval' setting") +// NewExecWidget returns a new ExecWidget. +func NewExecWidget(params interface{}) (ygs.Widget, error) { + w := &ExecWidget{ + params: params.(ExecWidgetParams), } - w.interval = time.Second * time.Duration(v.(int)) - v, ok = params["events_update"] - if ok { - w.eventsUpdate = v.(bool) - } else { - w.eventsUpdate = false + if len(w.params.Command) == 0 { + return nil, errors.New("missing 'command' setting") } - v, ok = params["signal"] - if ok { - sig := v.(int) + if w.params.Signal != nil { + sig := *w.params.Signal if sig < 0 || signals.SIGRTMIN+sig > signals.SIGRTMAX { - return nil, errors.Errorf("signal should be between 0 AND %d", signals.SIGRTMAX-signals.SIGRTMIN) + return nil, fmt.Errorf("signal should be between 0 AND %d", signals.SIGRTMAX-signals.SIGRTMIN) } w.signal = syscall.Signal(signals.SIGRTMIN + sig) } @@ -59,7 +57,7 @@ func NewExecWidget(params map[string]interface{}) (ygs.Widget, error) { } func (w *ExecWidget) exec() error { - cmd := exec.Command("sh", "-c", w.command) + cmd := exec.Command("sh", "-c", w.params.Command) cmd.Stderr = os.Stderr output, err := cmd.Output() if err != nil { @@ -79,11 +77,11 @@ func (w *ExecWidget) exec() error { // Run starts the main loop. func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { w.c = c - if w.interval == 0 && w.signal == nil { + if w.params.Interval == 0 && w.signal == nil { return w.exec() } - upd := make(chan struct{}, 1) + w.upd = make(chan struct{}, 1) if w.signal != nil { sigc := make(chan os.Signal, 1) @@ -91,24 +89,29 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { go (func() { for { <-sigc - upd <- struct{}{} + w.upd <- struct{}{} } })() } - if w.interval > 0 { - ticker := time.NewTicker(w.interval) + if w.params.Interval > 0 { + ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) go (func() { for { <-ticker.C - upd <- struct{}{} + w.upd <- struct{}{} } })() } - for ; true; <-upd { + for ; true; <-w.upd { err := w.exec() if err != nil { - return err + w.c <- []ygs.I3BarBlock{ + ygs.I3BarBlock{ + FullText: err.Error(), + Color: "#ff0000", + }, + } } } return nil @@ -116,14 +119,10 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *ExecWidget) Event(event ygs.I3BarClickEvent) { - if w.eventsUpdate { - w.exec() + if w.params.EventsUpdate { + w.upd <- struct{}{} } } // Stop shutdowns the widget. func (w *ExecWidget) Stop() {} - -func init() { - ygs.RegisterWidget("exec", NewExecWidget) -} diff --git a/widgets/http.go b/widgets/http.go index 4a01a4c..6fefcad 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -14,56 +14,83 @@ import ( "golang.org/x/net/websocket" ) +// HTTPWidgetParams are widget parameters. +type HTTPWidgetParams struct { + Listen string + Path string +} + // HTTPWidget implements the http server widget. type HTTPWidget struct { - c chan<- []ygs.I3BarBlock - conn *websocket.Conn - listen string - path string + params HTTPWidgetParams + + httpServer *http.Server + conn *websocket.Conn + c chan<- []ygs.I3BarBlock +} + +type httpInstance struct { + mux *http.ServeMux + paths map[string]struct{} +} + +var instances map[string]*httpInstance + +func init() { + ygs.RegisterWidget("http", NewHTTPWidget, HTTPWidgetParams{}) } // NewHTTPWidget returns a new HTTPWidget. -func NewHTTPWidget(params map[string]interface{}) (ygs.Widget, error) { - w := &HTTPWidget{} - v, ok := params["listen"] - if !ok { - return nil, errors.New("missing 'listen' setting") +func NewHTTPWidget(params interface{}) (ygs.Widget, error) { + w := &HTTPWidget{ + params: params.(HTTPWidgetParams), } - w.listen = v.(string) - v, ok = params["path"] - if !ok { + if len(w.params.Listen) == 0 { + return nil, errors.New("missing 'listen' param") + } + + if len(w.params.Path) == 0 { return nil, errors.New("missing 'path' setting") } - w.path = v.(string) - if serveMuxes == nil { - serveMuxes = make(map[string]*http.ServeMux, 1) + if instances == nil { + instances = make(map[string]*httpInstance, 1) + } + + if instance, ok := instances[w.params.Listen]; ok { + if _, ok := instance.paths[w.params.Path]; ok { + return nil, fmt.Errorf("path '%s' already in use", w.params.Path) + } + instance.mux.HandleFunc(w.params.Path, w.httpHandler) + instance.paths[w.params.Path] = struct{}{} + } else { + instance := &httpInstance{ + mux: http.NewServeMux(), + paths: make(map[string]struct{}, 1), + } + instance.mux.HandleFunc(w.params.Path, w.httpHandler) + instance.paths[w.params.Path] = struct{}{} + instances[w.params.Listen] = instance + + w.httpServer = &http.Server{ + Addr: w.params.Listen, + Handler: instance.mux, + } + } return w, nil } -var serveMuxes map[string]*http.ServeMux - // Run starts the main loop. func (w *HTTPWidget) Run(c chan<- []ygs.I3BarBlock) error { w.c = c - - mux, ok := serveMuxes[w.listen] - if ok { - mux.HandleFunc(w.path, w.httpHandler) + if w.httpServer == nil { return nil } - mux = http.NewServeMux() - mux.HandleFunc(w.path, w.httpHandler) - httpServer := &http.Server{ - Addr: w.listen, - Handler: mux, - } - serveMuxes[w.listen] = mux - return httpServer.ListenAndServe() + return w.httpServer.ListenAndServe() } // Event processes the widget events. @@ -124,7 +151,3 @@ func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { // Stop shutdowns the widget. func (w *HTTPWidget) Stop() {} - -func init() { - ygs.RegisterWidget("http", NewHTTPWidget) -} diff --git a/widgets/static.go b/widgets/static.go index b80e34d..c1d9d92 100644 --- a/widgets/static.go +++ b/widgets/static.go @@ -7,21 +7,33 @@ import ( "github.com/burik666/yagostatus/ygs" ) +// StaticWidgetParams are widget parameters. +type StaticWidgetParams struct { + Blocks string +} + // StaticWidget implements a static widget. type StaticWidget struct { + params StaticWidgetParams + blocks []ygs.I3BarBlock } +func init() { + ygs.RegisterWidget("static", NewStaticWidget, StaticWidgetParams{}) +} + // NewStaticWidget returns a new StaticWidget. -func NewStaticWidget(params map[string]interface{}) (ygs.Widget, error) { - w := &StaticWidget{} +func NewStaticWidget(params interface{}) (ygs.Widget, error) { + w := &StaticWidget{ + params: params.(StaticWidgetParams), + } - v, ok := params["blocks"] - if !ok { + if len(w.params.Blocks) == 0 { return nil, errors.New("missing 'blocks' setting") } - if err := json.Unmarshal([]byte(v.(string)), &w.blocks); err != nil { + if err := json.Unmarshal([]byte(w.params.Blocks), &w.blocks); err != nil { return nil, err } @@ -39,7 +51,3 @@ func (w *StaticWidget) Event(event ygs.I3BarClickEvent) {} // Stop shutdowns the widget. func (w *StaticWidget) Stop() {} - -func init() { - ygs.RegisterWidget("static", NewStaticWidget) -} diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 3f62f59..eea3e93 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -8,28 +8,41 @@ import ( "os" "os/exec" "regexp" + "syscall" "github.com/burik666/yagostatus/ygs" ) +// WrapperWidgetParams are widget parameters. +type WrapperWidgetParams struct { + Command string +} + // WrapperWidget implements the wrapper of other status commands. type WrapperWidget struct { + params WrapperWidgetParams + stdin io.WriteCloser cmd *exec.Cmd command string args []string } +func init() { + ygs.RegisterWidget("wrapper", NewWrapperWidget, WrapperWidgetParams{}) +} + // NewWrapperWidget returns a new WrapperWidget. -func NewWrapperWidget(params map[string]interface{}) (ygs.Widget, error) { - w := &WrapperWidget{} +func NewWrapperWidget(params interface{}) (ygs.Widget, error) { + w := &WrapperWidget{ + params: params.(WrapperWidgetParams), + } - v, ok := params["command"] - if !ok { + if len(w.params.Command) == 0 { return nil, errors.New("missing 'command' setting") } r := regexp.MustCompile("'.+'|\".+\"|\\S+") - m := r.FindAllString(v.(string), -1) + m := r.FindAllString(w.params.Command, -1) w.command = m[0] w.args = m[1:] @@ -97,8 +110,9 @@ func (w *WrapperWidget) Event(event ygs.I3BarClickEvent) { } // Stop shutdowns the widget. -func (w *WrapperWidget) Stop() {} - -func init() { - ygs.RegisterWidget("wrapper", NewWrapperWidget) +func (w *WrapperWidget) Stop() { + if w.cmd != nil && w.cmd.Process != nil { + w.cmd.Process.Signal(syscall.SIGHUP) + w.cmd.Process.Wait() + } } diff --git a/yagostatus.go b/yagostatus.go index b217358..091e546 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -9,6 +9,7 @@ import ( "log" "os" "os/exec" + "runtime/debug" "strings" "sync" @@ -35,35 +36,33 @@ type YaGoStatus struct { func NewYaGoStatus(cfg config.Config) (*YaGoStatus, error) { status := &YaGoStatus{} for _, w := range cfg.Widgets { - widget, err := ygs.NewWidget(w.Name, w.Params) - if err != nil { - status.errorWidget(err.Error()) - continue - } - - status.AddWidget(widget, w) + (func() { + defer (func() { + if r := recover(); r != nil { + log.Printf("NewWidget is panicking: %s", r) + debug.PrintStack() + status.errorWidget("Widget is panicking") + } + })() + widget, err := ygs.NewWidget(w.Name, w.Params) + if err != nil { + log.Printf("Failed to create widget: %s", err) + status.errorWidget(err.Error()) + return + } + status.AddWidget(widget, w) + })() } return status, nil } func (status *YaGoStatus) errorWidget(text string) { - log.Print(text) - blocks, _ := json.Marshal([]ygs.I3BarBlock{ - ygs.I3BarBlock{ - FullText: text, - Color: "#ff0000", - }, - }) - - widget, err := ygs.NewWidget("static", map[string]interface{}{ - "blocks": string(blocks), - }) + errWidget, err := ygs.NewWidget(ygs.ErrorWidget(text)) if err != nil { - log.Fatalf("Failed to create error widget: %s", err) + panic(err) } - - status.AddWidget(widget, config.WidgetConfig{}) + status.AddWidget(errWidget, config.WidgetConfig{}) } // AddWidget adds widget to statusbar. @@ -74,13 +73,26 @@ func (status *YaGoStatus) AddWidget(widget ygs.Widget, config config.WidgetConfi } func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, event ygs.I3BarClickEvent) error { - defer status.widgets[widgetIndex].Event(event) - for _, we := range status.widgetsConfig[widgetIndex].Events { - if (we.Button == 0 || we.Button == event.Button) && - (we.Name == "" || we.Name == event.Name) && - (we.Instance == "" || we.Instance == event.Instance) && - checkModifiers(we.Modifiers, event.Modifiers) { - cmd := exec.Command("sh", "-c", we.Command) + (func() { + defer (func() { + if r := recover(); r != nil { + log.Printf("Widget event is panicking: %s", r) + debug.PrintStack() + status.widgetsOutput[widgetIndex] = []ygs.I3BarBlock{ygs.I3BarBlock{ + FullText: "Widget event is panicking", + Color: "#ff0000", + }} + } + })() + status.widgets[widgetIndex].Event(event) + })() + + for _, widgetEvent := range status.widgetsConfig[widgetIndex].Events { + if (widgetEvent.Button == 0 || widgetEvent.Button == event.Button) && + (widgetEvent.Name == "" || widgetEvent.Name == event.Name) && + (widgetEvent.Instance == "" || widgetEvent.Instance == event.Instance) && + checkModifiers(widgetEvent.Modifiers, event.Modifiers) { + cmd := exec.Command("sh", "-c", widgetEvent.Command) cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), fmt.Sprintf("I3_%s=%s", "NAME", event.Name), @@ -107,26 +119,35 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, if err != nil { return err } - if we.Output { + + if widgetEvent.Output { var blocks []ygs.I3BarBlock - if err := json.Unmarshal(cmdOutput, &blocks); err == nil { - for bi := range blocks { - block := &blocks[bi] - mergeBlocks(block, status.widgetsConfig[widgetIndex].Template) - block.Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, block.Name) - block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", widgetIndex, outputIndex, block.Instance) - } - status.widgetsOutput[widgetIndex] = blocks - } else { - status.widgetsOutput[widgetIndex][outputIndex].FullText = strings.Trim(string(cmdOutput), "\n\r") + if err := json.Unmarshal(cmdOutput, &blocks); err != nil { + blocks = append(blocks, + ygs.I3BarBlock{ + FullText: strings.Trim(string(cmdOutput), "\n\r"), + }, + ) } - status.upd <- widgetIndex + status.addWidgetOutput(widgetIndex, blocks) } } } return nil } +func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlock) { + status.widgetsOutput[widgetIndex] = make([]ygs.I3BarBlock, len(blocks)) + for blockIndex := range blocks { + block := blocks[blockIndex] + mergeBlocks(&block, status.widgetsConfig[widgetIndex].Template) + block.Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, block.Name) + block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", widgetIndex, blockIndex, block.Instance) + status.widgetsOutput[widgetIndex][blockIndex] = block + } + status.upd <- widgetIndex +} + func (status *YaGoStatus) eventReader() { reader := bufio.NewReader(os.Stdin) for { @@ -160,8 +181,8 @@ func (status *YaGoStatus) eventReader() { Name: event.Name, Instance: event.Instance, } - break } + break } } } @@ -171,39 +192,40 @@ func (status *YaGoStatus) eventReader() { // Run starts the main loop. func (status *YaGoStatus) Run() { status.upd = make(chan int) + status.updateWorkspaces() + go (func() { + recv := i3.Subscribe(i3.WorkspaceEventType) + for recv.Next() { + e := recv.Event().(*i3.WorkspaceEvent) + if e.Change == "empty" { + continue + } + status.updateWorkspaces() + status.upd <- -1 + } + })() for widgetIndex, widget := range status.widgets { c := make(chan []ygs.I3BarBlock) go func(widgetIndex int, c chan []ygs.I3BarBlock) { for { select { case out := <-c: - output := make([]ygs.I3BarBlock, len(out)) - copy(output, out) - for outputIndex := range output { - mergeBlocks(&output[outputIndex], status.widgetsConfig[widgetIndex].Template) - output[outputIndex].Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, output[outputIndex].Name) - output[outputIndex].Instance = fmt.Sprintf("yagostatus-%d-%d-%s", widgetIndex, outputIndex, output[outputIndex].Instance) - } - status.widgetsOutput[widgetIndex] = output - status.upd <- widgetIndex + status.addWidgetOutput(widgetIndex, out) } } }(widgetIndex, c) - status.updateWorkspaces() - go (func() { - recv := i3.Subscribe(i3.WorkspaceEventType) - for recv.Next() { - e := recv.Event().(*i3.WorkspaceEvent) - if e.Change == "empty" { - continue - } - status.updateWorkspaces() - status.upd <- -1 - } - })() - go func(widget ygs.Widget, c chan []ygs.I3BarBlock) { + defer (func() { + if r := recover(); r != nil { + c <- []ygs.I3BarBlock{ygs.I3BarBlock{ + FullText: "Widget is panicking", + Color: "#ff0000", + }} + log.Printf("Widget is panicking: %s", r) + debug.PrintStack() + } + })() if err := widget.Run(c); err != nil { log.Print(err) c <- []ygs.I3BarBlock{ygs.I3BarBlock{ @@ -245,8 +267,14 @@ func (status *YaGoStatus) Stop() { for _, widget := range status.widgets { wg.Add(1) go func(widget ygs.Widget) { + defer wg.Done() + defer (func() { + if r := recover(); r != nil { + log.Printf("Widget is panicking: %s", r) + debug.PrintStack() + } + })() widget.Stop() - wg.Done() }(widget) } wg.Wait() @@ -268,14 +296,8 @@ func (status *YaGoStatus) updateWorkspaces() { } func mergeBlocks(b *ygs.I3BarBlock, tpl ygs.I3BarBlock) { - var resmap map[string]interface{} - jb, _ := json.Marshal(*b) - jtpl, _ := json.Marshal(tpl) - json.Unmarshal(jtpl, &resmap) - json.Unmarshal(jb, &resmap) - - jb, _ = json.Marshal(resmap) + *b = tpl json.Unmarshal(jb, b) } diff --git a/ygs/protocol.go b/ygs/protocol.go index 02db634..a005145 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -1,5 +1,10 @@ package ygs +import ( + "bytes" + "encoding/json" +) + // I3BarHeader represents the header of an i3bar message. type I3BarHeader struct { Version uint8 `json:"version"` @@ -43,3 +48,59 @@ type I3BarClickEvent struct { Height uint16 `json:"height"` Modifiers []string `json:"modifiers"` } + +// UnmarshalJSON unmarshals json with custom keys (with _ prefix). +func (b *I3BarBlock) UnmarshalJSON(data []byte) error { + type dataWrapped I3BarBlock + + wr := dataWrapped(*b) + + if err := json.Unmarshal(data, &wr); err != nil { + return err + } + + if err := json.Unmarshal(data, &wr.Custom); err != nil { + return err + } + + for k := range wr.Custom { + if k[0] != '_' { + delete(wr.Custom, k) + } + } + + *b = I3BarBlock(wr) + + return nil +} + +// MarshalJSON marshals json with custom keys (with _ prefix). +func (b I3BarBlock) MarshalJSON() ([]byte, error) { + type dataWrapped I3BarBlock + var wd dataWrapped + wd = dataWrapped(b) + + if len(wd.Custom) == 0 { + buf := &bytes.Buffer{} + encoder := json.NewEncoder(buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(wd) + return buf.Bytes(), err + } + + var resmap map[string]interface{} + + var tmp []byte + + tmp, _ = json.Marshal(wd) + json.Unmarshal(tmp, &resmap) + + tmp, _ = json.Marshal(wd.Custom) + json.Unmarshal(tmp, &resmap) + + buf := &bytes.Buffer{} + encoder := json.NewEncoder(buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(resmap) + return buf.Bytes(), err +} diff --git a/ygs/utils.go b/ygs/utils.go index fb3bdff..e546e0d 100644 --- a/ygs/utils.go +++ b/ygs/utils.go @@ -1,28 +1,67 @@ package ygs import ( - "log" + "encoding/json" + "fmt" + "reflect" - "github.com/pkg/errors" + "gopkg.in/yaml.v2" ) -type newWidgetFunc = func(map[string]interface{}) (Widget, error) +type newWidgetFunc = func(interface{}) (Widget, error) -var registeredWidgets = make(map[string]newWidgetFunc) +type widget struct { + newFunc newWidgetFunc + defaultParams interface{} +} + +var registeredWidgets = make(map[string]widget) // RegisterWidget registers widget. -func RegisterWidget(name string, newFunc newWidgetFunc) { +func RegisterWidget(name string, newFunc newWidgetFunc, defaultParams interface{}) { if _, ok := registeredWidgets[name]; ok { - log.Fatalf("Widget '%s' already registered", name) + panic(fmt.Sprintf("Widget '%s' already registered", name)) + } + def := reflect.ValueOf(defaultParams) + if def.Kind() != reflect.Struct { + panic("defaultParams should be a struct") + } + registeredWidgets[name] = widget{ + newFunc: newFunc, + defaultParams: defaultParams, } - registeredWidgets[name] = newFunc } // NewWidget creates new widget by name. -func NewWidget(name string, params widgetParams) (Widget, error) { - newFunc, ok := registeredWidgets[name] +func NewWidget(name string, rawParams map[string]interface{}) (Widget, error) { + widget, ok := registeredWidgets[name] if !ok { - return nil, errors.Errorf("Widget '%s' not found", name) + return nil, fmt.Errorf("Widget '%s' not found", name) + } + + def := reflect.ValueOf(widget.defaultParams) + + params := reflect.New(def.Type()) + params.Elem().Set(def) + + b, _ := yaml.Marshal(rawParams) + if err := yaml.UnmarshalStrict(b, params.Interface()); err != nil { + return nil, err + } + + return widget.newFunc(params.Elem().Interface()) +} + +// ErrorWidget creates new widget with error message. +func ErrorWidget(text string) (string, map[string]interface{}) { + blocks, _ := json.Marshal([]I3BarBlock{ + I3BarBlock{ + FullText: text, + Color: "#ff0000", + }, + }) + + return "static", map[string]interface{}{ + "blocks": string(blocks), } - return newFunc(params) } diff --git a/ygs/widget.go b/ygs/widget.go index d9b9754..f86b584 100644 --- a/ygs/widget.go +++ b/ygs/widget.go @@ -1,71 +1,9 @@ // Package ygs contains the YaGoStatus structures. package ygs -import ( - "bytes" - "encoding/json" -) - -type widgetParams = map[string]interface{} - // Widget represents a widget struct. type Widget interface { Run(chan<- []I3BarBlock) error Event(I3BarClickEvent) Stop() } - -// UnmarshalJSON unmarshals json with custom keys (with _ prefix). -func (b *I3BarBlock) UnmarshalJSON(data []byte) error { - type dataWrapped I3BarBlock - - var wr dataWrapped - - if err := json.Unmarshal(data, &wr); err != nil { - return err - } - - if err := json.Unmarshal(data, &wr.Custom); err != nil { - return err - } - for k := range wr.Custom { - if k[0] != '_' { - delete(wr.Custom, k) - } - } - - *b = I3BarBlock(wr) - - return nil -} - -// MarshalJSON marshals json with custom keys (with _ prefix). -func (b I3BarBlock) MarshalJSON() ([]byte, error) { - type dataWrapped I3BarBlock - var wd dataWrapped - wd = dataWrapped(b) - - if len(wd.Custom) == 0 { - buf := &bytes.Buffer{} - encoder := json.NewEncoder(buf) - encoder.SetEscapeHTML(false) - err := encoder.Encode(wd) - return buf.Bytes(), err - } - - var resmap map[string]interface{} - - var tmp []byte - - tmp, _ = json.Marshal(wd) - json.Unmarshal(tmp, &resmap) - - tmp, _ = json.Marshal(wd.Custom) - json.Unmarshal(tmp, &resmap) - - buf := &bytes.Buffer{} - encoder := json.NewEncoder(buf) - encoder.SetEscapeHTML(false) - err := encoder.Encode(resmap) - return buf.Bytes(), err -} From 213a4f1cec8818d1f29522b333b4c4648ae4fc2f Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Thu, 21 Mar 2019 04:59:53 +0300 Subject: [PATCH 06/34] Add executor (with autodetect output formats) Removed output parameter Added output_format parameter --- README.md | 3 +- internal/pkg/config/config.go | 22 ++-- internal/pkg/executor/executor.go | 160 ++++++++++++++++++++++++++++++ widgets/exec.go | 19 +--- widgets/wrapper.go | 80 ++++----------- yagostatus.go | 35 +++---- ygs/protocol.go | 37 +++++-- 7 files changed, 238 insertions(+), 118 deletions(-) create mode 100644 internal/pkg/executor/executor.go diff --git a/README.md b/README.md index 6afa975..959e657 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Example: * `command` - Command to execute (via `sh -c`). Сlick_event json will be written to stdin. Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_X`, `$I3_Y`, `$I3_RELATIVE_X`, `$I3_RELATIVE_Y`, `$I3_WIDTH`, `$I3_HEIGHT`, `$I3_MODIFIERS`. - * `output` - If `true` widget text will be replaced with the command output (default: `false`). + * `output_format` - The command output format (none, text, json, auto) (default: `none`). * `name` - Filter by `name` for widgets with multiple blocks (default: empty). * `instance` - Filter by `instance` for widgets with multiple blocks (default: empty). @@ -157,6 +157,7 @@ This widget runs the command at the specified interval. - `command` - Command to execute (via `sh -c`). - `interval` - Update interval in seconds (set 0 to run once at start). - `events_update` - Update widget if an event occurred (default: `false`). +- `output_format` - The command output format (none, text, json, auto) (default: `auto`). - `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. Use pkill to send signals: diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 1cfd255..0c1067b 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "strings" + "github.com/burik666/yagostatus/internal/pkg/executor" "github.com/burik666/yagostatus/ygs" "gopkg.in/yaml.v2" @@ -32,8 +33,8 @@ func (c WidgetConfig) Validate() error { if c.Name == "" { return errors.New("Missing widget name") } - for _, e := range c.Events { - if err := e.Validate(); err != nil { + for ei := range c.Events { + if err := c.Events[ei].Validate(); err != nil { return err } } @@ -42,16 +43,16 @@ func (c WidgetConfig) Validate() error { // WidgetEventConfig represents a widget events. type WidgetEventConfig struct { - Command string `yaml:"command"` - Button uint8 `yaml:"button"` - Modifiers []string `yaml:"modifiers,omitempty"` - Name string `yaml:"name,omitempty"` - Instance string `yaml:"instance,omitempty"` - Output bool `yaml:"output,omitempty"` + Command string `yaml:"command"` + Button uint8 `yaml:"button"` + Modifiers []string `yaml:"modifiers,omitempty"` + Name string `yaml:"name,omitempty"` + Instance string `yaml:"instance,omitempty"` + OutputFormat executor.OutputFormat `yaml:"output_format,omitempty"` } // Validate checks event parameters. -func (e WidgetEventConfig) Validate() error { +func (e *WidgetEventConfig) Validate() error { var availableWidgetEventModifiers = [...]string{"Shift", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"} for _, mod := range e.Modifiers { found := false @@ -66,6 +67,9 @@ func (e WidgetEventConfig) Validate() error { return fmt.Errorf("Unknown '%s' modifier", mod) } } + if e.OutputFormat == "" { + e.OutputFormat = executor.OutputFormatNone + } return nil } diff --git a/internal/pkg/executor/executor.go b/internal/pkg/executor/executor.go new file mode 100644 index 0000000..cd550e0 --- /dev/null +++ b/internal/pkg/executor/executor.go @@ -0,0 +1,160 @@ +package executor + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strings" + + "github.com/burik666/yagostatus/ygs" +) + +type OutputFormat string + +const ( + OutputFormatAuto OutputFormat = "auto" + OutputFormatNone OutputFormat = "none" + OutputFormatText OutputFormat = "text" + OutputFormatJSON OutputFormat = "json" +) + +type Executor struct { + cmd *exec.Cmd +} + +func Exec(command string, args ...string) (*Executor, error) { + r := regexp.MustCompile("'.+'|\".+\"|\\S+") + m := r.FindAllString(command, -1) + name := m[0] + args = append(m[1:], args...) + + e := &Executor{} + + e.cmd = exec.Command(name, args...) + e.cmd.Stderr = os.Stderr + e.cmd.Env = os.Environ() + + return e, nil +} + +func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { + stdout, err := e.cmd.StdoutPipe() + if err != nil { + return err + } + defer stdout.Close() + + if err := e.cmd.Start(); err != nil { + return err + } + defer e.Wait() + + if format == OutputFormatNone { + return nil + } + buf := &bufferCloser{} + outreader := io.TeeReader(stdout, buf) + + decoder := json.NewDecoder(outreader) + + isJSON := false + var firstMessage interface{} + + err = decoder.Decode(&firstMessage) + + switch firstMessage.(type) { + case map[string]interface{}: + isJSON = true + case []interface{}: + isJSON = true + } + if (err != nil) && format == OutputFormatJSON { + buf.Close() + return err + } + + if err != nil || !isJSON || format == OutputFormatText { + io.Copy(ioutil.Discard, outreader) + if buf.Len() > 0 { + c <- []ygs.I3BarBlock{ + ygs.I3BarBlock{ + FullText: strings.Trim(string(buf.Bytes()), "\n "), + }, + } + } + buf.Close() + return nil + } + buf.Close() + + firstMessageData, _ := json.Marshal(firstMessage) + + headerDecoder := json.NewDecoder(bytes.NewBuffer(firstMessageData)) + headerDecoder.DisallowUnknownFields() + + var header ygs.I3BarHeader + if err := headerDecoder.Decode(&header); err == nil { + decoder.Token() + } else { + var blocks []ygs.I3BarBlock + if err := json.Unmarshal(firstMessageData, &blocks); err != nil { + return err + } + c <- blocks + } + for { + var blocks []ygs.I3BarBlock + if err := decoder.Decode(&blocks); err != nil { + if err != io.EOF { + return err + } + return nil + } + c <- blocks + } + return nil +} + +func (e *Executor) Stdin() (io.WriteCloser, error) { + return e.cmd.StdinPipe() +} + +func (e *Executor) AddEnv(env ...string) { + e.cmd.Env = append(e.cmd.Env, env...) +} + +func (e *Executor) Wait() error { + if e.cmd != nil { + return e.cmd.Wait() + } + return nil +} + +func (e *Executor) Signal(sig os.Signal) error { + if e.cmd != nil && e.cmd.Process != nil { + return e.cmd.Process.Signal(sig) + } + return nil +} + +type bufferCloser struct { + bytes.Buffer + stoped bool +} + +func (b *bufferCloser) Write(p []byte) (n int, err error) { + if b.stoped { + return len(p), nil + } + return b.Buffer.Write(p) +} + +func (b *bufferCloser) Close() error { + b.stoped = true + b.Reset() + return nil +} diff --git a/widgets/exec.go b/widgets/exec.go index 34bd9cc..369663f 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -1,16 +1,14 @@ package widgets import ( - "encoding/json" "errors" "fmt" "os" - "os/exec" "os/signal" - "strings" "syscall" "time" + "github.com/burik666/yagostatus/internal/pkg/executor" "github.com/burik666/yagostatus/internal/pkg/signals" "github.com/burik666/yagostatus/ygs" ) @@ -21,6 +19,7 @@ type ExecWidgetParams struct { Interval uint EventsUpdate bool `yaml:"events_update"` Signal *int + OutputFormat executor.OutputFormat `yaml:"output_format"` } // ExecWidget implements the exec widget. @@ -57,21 +56,11 @@ func NewExecWidget(params interface{}) (ygs.Widget, error) { } func (w *ExecWidget) exec() error { - cmd := exec.Command("sh", "-c", w.params.Command) - cmd.Stderr = os.Stderr - output, err := cmd.Output() + exc, err := executor.Exec("sh", "-c", w.params.Command) if err != nil { return err } - - var blocks []ygs.I3BarBlock - err = json.Unmarshal(output, &blocks) - if err != nil { - blocks = append(blocks, ygs.I3BarBlock{FullText: strings.Trim(string(output), "\n ")}) - } - w.c <- blocks - return nil - + return exc.Run(w.c, w.params.OutputFormat) } // Run starts the main loop. diff --git a/widgets/wrapper.go b/widgets/wrapper.go index eea3e93..0b80e18 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -1,15 +1,12 @@ package widgets import ( - "bufio" "encoding/json" "errors" "io" - "os" - "os/exec" - "regexp" "syscall" + "github.com/burik666/yagostatus/internal/pkg/executor" "github.com/burik666/yagostatus/ygs" ) @@ -22,10 +19,8 @@ type WrapperWidgetParams struct { type WrapperWidget struct { params WrapperWidgetParams - stdin io.WriteCloser - cmd *exec.Cmd - command string - args []string + exc *executor.Executor + stdin io.WriteCloser } func init() { @@ -41,78 +36,39 @@ func NewWrapperWidget(params interface{}) (ygs.Widget, error) { if len(w.params.Command) == 0 { return nil, errors.New("missing 'command' setting") } - r := regexp.MustCompile("'.+'|\".+\"|\\S+") - m := r.FindAllString(w.params.Command, -1) - w.command = m[0] - w.args = m[1:] return w, nil } // Run starts the main loop. func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { - w.cmd = exec.Command(w.command, w.args...) - w.cmd.Stderr = os.Stderr - stdout, err := w.cmd.StdoutPipe() + var err error + w.exc, err = executor.Exec(w.params.Command) if err != nil { - return err + return nil } - - w.stdin, err = w.cmd.StdinPipe() + w.stdin, err = w.exc.Stdin() if err != nil { - return err - } - - if err := w.cmd.Start(); err != nil { - return err + return nil } + defer w.stdin.Close() w.stdin.Write([]byte("[")) - - reader := bufio.NewReader(stdout) - decoder := json.NewDecoder(reader) - - var firstMessage interface{} - if err := decoder.Decode(&firstMessage); err != nil { - return err - } - firstMessageData, _ := json.Marshal(firstMessage) - - var header ygs.I3BarHeader - if err := json.Unmarshal(firstMessageData, &header); err == nil { - decoder.Token() - } else { - var blocks []ygs.I3BarBlock - if err := json.Unmarshal(firstMessageData, &blocks); err != nil { - return err - } - c <- blocks - } - - for { - var blocks []ygs.I3BarBlock - err := decoder.Decode(&blocks) - if err != nil { - if err == io.EOF { - break - } - return err - } - c <- blocks - } - return w.cmd.Wait() + return w.exc.Run(c, executor.OutputFormatJSON) } // Event processes the widget events. func (w *WrapperWidget) Event(event ygs.I3BarClickEvent) { - msg, _ := json.Marshal(event) - w.stdin.Write(msg) - w.stdin.Write([]byte(",\n")) + if w.stdin != nil { + msg, _ := json.Marshal(event) + w.stdin.Write(msg) + w.stdin.Write([]byte(",\n")) + } } // Stop shutdowns the widget. func (w *WrapperWidget) Stop() { - if w.cmd != nil && w.cmd.Process != nil { - w.cmd.Process.Signal(syscall.SIGHUP) - w.cmd.Process.Wait() + if w.exc != nil { + w.exc.Signal(syscall.SIGHUP) + w.exc.Wait() } } diff --git a/yagostatus.go b/yagostatus.go index 091e546..999de41 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -8,12 +8,12 @@ import ( "io" "log" "os" - "os/exec" "runtime/debug" "strings" "sync" "github.com/burik666/yagostatus/internal/pkg/config" + "github.com/burik666/yagostatus/internal/pkg/executor" _ "github.com/burik666/yagostatus/widgets" "github.com/burik666/yagostatus/ygs" @@ -25,6 +25,7 @@ type YaGoStatus struct { widgets []ygs.Widget widgetsOutput [][]ygs.I3BarBlock widgetsConfig []config.WidgetConfig + widgetChans []chan []ygs.I3BarBlock upd chan int @@ -92,9 +93,12 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, (widgetEvent.Name == "" || widgetEvent.Name == event.Name) && (widgetEvent.Instance == "" || widgetEvent.Instance == event.Instance) && checkModifiers(widgetEvent.Modifiers, event.Modifiers) { - cmd := exec.Command("sh", "-c", widgetEvent.Command) - cmd.Stderr = os.Stderr - cmd.Env = append(os.Environ(), + + exc, err := executor.Exec("sh", "-c", widgetEvent.Command) + if err != nil { + return err + } + exc.AddEnv( fmt.Sprintf("I3_%s=%s", "NAME", event.Name), fmt.Sprintf("I3_%s=%s", "INSTANCE", event.Instance), fmt.Sprintf("I3_%s=%d", "BUTTON", event.Button), @@ -106,31 +110,19 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, fmt.Sprintf("I3_%s=%d", "HEIGHT", event.Height), fmt.Sprintf("I3_%s=%s", "MODIFIERS", strings.Join(event.Modifiers, ",")), ) - cmdStdin, err := cmd.StdinPipe() + stdin, err := exc.Stdin() if err != nil { return err } eventJSON, _ := json.Marshal(event) - cmdStdin.Write(eventJSON) - cmdStdin.Write([]byte("\n")) - cmdStdin.Close() + stdin.Write(eventJSON) + stdin.Write([]byte("\n")) + stdin.Close() - cmdOutput, err := cmd.Output() + err = exc.Run(status.widgetChans[widgetIndex], widgetEvent.OutputFormat) if err != nil { return err } - - if widgetEvent.Output { - var blocks []ygs.I3BarBlock - if err := json.Unmarshal(cmdOutput, &blocks); err != nil { - blocks = append(blocks, - ygs.I3BarBlock{ - FullText: strings.Trim(string(cmdOutput), "\n\r"), - }, - ) - } - status.addWidgetOutput(widgetIndex, blocks) - } } } return nil @@ -206,6 +198,7 @@ func (status *YaGoStatus) Run() { })() for widgetIndex, widget := range status.widgets { c := make(chan []ygs.I3BarBlock) + status.widgetChans = append(status.widgetChans, c) go func(widgetIndex int, c chan []ygs.I3BarBlock) { for { select { diff --git a/ygs/protocol.go b/ygs/protocol.go index a005145..094dd8b 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -51,22 +51,39 @@ type I3BarClickEvent struct { // UnmarshalJSON unmarshals json with custom keys (with _ prefix). func (b *I3BarBlock) UnmarshalJSON(data []byte) error { - type dataWrapped I3BarBlock - - wr := dataWrapped(*b) + var resmap map[string]interface{} - if err := json.Unmarshal(data, &wr); err != nil { + if err := json.Unmarshal(data, &resmap); err != nil { return err } - if err := json.Unmarshal(data, &wr.Custom); err != nil { - return err - } + type dataWrapped I3BarBlock + wr := dataWrapped(*b) - for k := range wr.Custom { - if k[0] != '_' { - delete(wr.Custom, k) + for k, v := range resmap { + if len(k) == 0 { + delete(resmap, k) + continue } + + if k[0] == '_' { + if wr.Custom == nil { + wr.Custom = make(map[string]interface{}) + } + wr.Custom[k] = v + delete(resmap, k) + } + } + + buf := &bytes.Buffer{} + + enc := json.NewEncoder(buf) + enc.Encode(resmap) + + decoder := json.NewDecoder(buf) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&wr); err != nil { + return err } *b = I3BarBlock(wr) From 870c610b9b9f66a87d833f0b595d7d763450398d Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 13 Apr 2019 01:55:55 +0300 Subject: [PATCH 07/34] Add current blocks to widget.event --- widgets/blank.go | 2 +- widgets/clock.go | 2 +- widgets/exec.go | 2 +- widgets/http.go | 2 +- widgets/static.go | 2 +- widgets/wrapper.go | 2 +- yagostatus.go | 2 +- ygs/widget.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/widgets/blank.go b/widgets/blank.go index 700df84..8751d53 100644 --- a/widgets/blank.go +++ b/widgets/blank.go @@ -26,7 +26,7 @@ func (w *BlankWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *BlankWidget) Event(event ygs.I3BarClickEvent) {} +func (w *BlankWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} // Stop shutdowns the widget. func (w *BlankWidget) Stop() {} diff --git a/widgets/clock.go b/widgets/clock.go index 0a84550..2f95b45 100644 --- a/widgets/clock.go +++ b/widgets/clock.go @@ -50,7 +50,7 @@ func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *ClockWidget) Event(event ygs.I3BarClickEvent) {} +func (w *ClockWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} // Stop shutdowns the widget. func (w *ClockWidget) Stop() {} diff --git a/widgets/exec.go b/widgets/exec.go index 369663f..c55e0c4 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -107,7 +107,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *ExecWidget) Event(event ygs.I3BarClickEvent) { +func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { if w.params.EventsUpdate { w.upd <- struct{}{} } diff --git a/widgets/http.go b/widgets/http.go index 6fefcad..f1fbccf 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -94,7 +94,7 @@ func (w *HTTPWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *HTTPWidget) Event(event ygs.I3BarClickEvent) { +func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { if w.conn != nil { websocket.JSON.Send(w.conn, event) } diff --git a/widgets/static.go b/widgets/static.go index c1d9d92..c31d9df 100644 --- a/widgets/static.go +++ b/widgets/static.go @@ -47,7 +47,7 @@ func (w *StaticWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *StaticWidget) Event(event ygs.I3BarClickEvent) {} +func (w *StaticWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} // Stop shutdowns the widget. func (w *StaticWidget) Stop() {} diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 0b80e18..3cedbe9 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -57,7 +57,7 @@ func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *WrapperWidget) Event(event ygs.I3BarClickEvent) { +func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { if w.stdin != nil { msg, _ := json.Marshal(event) w.stdin.Write(msg) diff --git a/yagostatus.go b/yagostatus.go index 999de41..0206e42 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -85,7 +85,7 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, }} } })() - status.widgets[widgetIndex].Event(event) + status.widgets[widgetIndex].Event(event, status.widgetsOutput[widgetIndex]) })() for _, widgetEvent := range status.widgetsConfig[widgetIndex].Events { diff --git a/ygs/widget.go b/ygs/widget.go index f86b584..d653763 100644 --- a/ygs/widget.go +++ b/ygs/widget.go @@ -4,6 +4,6 @@ package ygs // Widget represents a widget struct. type Widget interface { Run(chan<- []I3BarBlock) error - Event(I3BarClickEvent) + Event(I3BarClickEvent, []I3BarBlock) Stop() } From 0630084995688854c39381df1cdf9a2b02ed9f3b Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 13 Apr 2019 04:05:38 +0300 Subject: [PATCH 08/34] Add custom fields to the exec widget and events --- README.md | 35 ++++++++++++++++++++++++++++++++++- widgets/exec.go | 46 +++++++++++++++++++++++++++++++++++++++++----- yagostatus.go | 26 +++++++++++++++----------- 3 files changed, 90 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 959e657..0221063 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Example: * `modifiers` - List of X11 modifiers condition. * `command` - Command to execute (via `sh -c`). Сlick_event json will be written to stdin. - Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_X`, `$I3_Y`, `$I3_RELATIVE_X`, `$I3_RELATIVE_Y`, `$I3_WIDTH`, `$I3_HEIGHT`, `$I3_MODIFIERS`. + Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_X`, `$I3_Y`, `$I3_RELATIVE_X`, `$I3_RELATIVE_Y`, `$I3_WIDTH`, `$I3_HEIGHT`, `$I3_MODIFIERS`. `$I3__` prefix for custom fields. * `output_format` - The command output format (none, text, json, auto) (default: `none`). * `name` - Filter by `name` for widgets with multiple blocks (default: empty). * `instance` - Filter by `instance` for widgets with multiple blocks (default: empty). @@ -160,6 +160,8 @@ This widget runs the command at the specified interval. - `output_format` - The command output format (none, text, json, auto) (default: `auto`). - `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. +Custom fields are available as ENV variables with the prefix `$I3__`. + Use pkill to send signals: pkill -SIGRTMIN+1 yagostatus @@ -199,6 +201,37 @@ Send an empty array to clear: ## Examples +### Counter + +This example shows how you can use custom fields. + +- Left mouse button - increment +- Right mouse button - decrement +- Middle mouse button - reset + +```yml + - widget: static + blocks: > + [ + { + "full_text":"COUNTER" + } + ] + events: + - command: | + printf '[{"full_text":"Counter: %d", "_count":%d}]' $((I3__COUNT + 1)) $((I3__COUNT + 1)) + output_format: json + button: 1 + - command: | + printf '[{"full_text":"Counter: %d", "_count":%d}]' $((I3__COUNT - 1)) $((I3__COUNT - 1)) + output_format: json + button: 3 + - command: | + printf '[{"full_text":"Counter: 0", "_count":0}]' + output_format: json + button: 2 +``` + ### Volume control i3 config: ``` diff --git a/widgets/exec.go b/widgets/exec.go index c55e0c4..35c4dd8 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -1,10 +1,12 @@ package widgets import ( + "encoding/json" "errors" "fmt" "os" "os/signal" + "strings" "syscall" "time" @@ -26,9 +28,10 @@ type ExecWidgetParams struct { type ExecWidget struct { params ExecWidgetParams - signal os.Signal - c chan<- []ygs.I3BarBlock - upd chan struct{} + signal os.Signal + c chan<- []ygs.I3BarBlock + upd chan struct{} + customfields map[string]interface{} } func init() { @@ -60,7 +63,29 @@ func (w *ExecWidget) exec() error { if err != nil { return err } - return exc.Run(w.c, w.params.OutputFormat) + + for k, v := range w.customfields { + vst, _ := json.Marshal(v) + exc.AddEnv( + fmt.Sprintf("I3_%s=%s", strings.ToUpper(k), vst), + ) + + } + + c := make(chan []ygs.I3BarBlock) + go (func() { + for { + blocks, ok := <-c + if !ok { + break + } + w.c <- blocks + w.setCustomFields(blocks) + } + })() + err = exc.Run(c, w.params.OutputFormat) + close(c) + return err } // Run starts the main loop. @@ -95,7 +120,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { for ; true; <-w.upd { err := w.exec() if err != nil { - w.c <- []ygs.I3BarBlock{ + c <- []ygs.I3BarBlock{ ygs.I3BarBlock{ FullText: err.Error(), Color: "#ff0000", @@ -108,6 +133,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { + w.setCustomFields(blocks) if w.params.EventsUpdate { w.upd <- struct{}{} } @@ -115,3 +141,13 @@ func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { // Stop shutdowns the widget. func (w *ExecWidget) Stop() {} + +func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { + customfields := make(map[string]interface{}) + for _, block := range blocks { + for k, v := range block.Custom { + customfields[k] = v + } + } + w.customfields = customfields +} diff --git a/yagostatus.go b/yagostatus.go index 0206e42..6f6962e 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -74,17 +74,15 @@ func (status *YaGoStatus) AddWidget(widget ygs.Widget, config config.WidgetConfi } func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, event ygs.I3BarClickEvent) error { - (func() { - defer (func() { - if r := recover(); r != nil { - log.Printf("Widget event is panicking: %s", r) - debug.PrintStack() - status.widgetsOutput[widgetIndex] = []ygs.I3BarBlock{ygs.I3BarBlock{ - FullText: "Widget event is panicking", - Color: "#ff0000", - }} - } - })() + defer (func() { + if r := recover(); r != nil { + log.Printf("Widget event is panicking: %s", r) + debug.PrintStack() + status.widgetsOutput[widgetIndex] = []ygs.I3BarBlock{ygs.I3BarBlock{ + FullText: "Widget event is panicking", + Color: "#ff0000", + }} + } status.widgets[widgetIndex].Event(event, status.widgetsOutput[widgetIndex]) })() @@ -110,6 +108,12 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, fmt.Sprintf("I3_%s=%d", "HEIGHT", event.Height), fmt.Sprintf("I3_%s=%s", "MODIFIERS", strings.Join(event.Modifiers, ",")), ) + for k, v := range status.widgetsOutput[widgetIndex][outputIndex].Custom { + vst, _ := json.Marshal(v) + exc.AddEnv( + fmt.Sprintf("I3_%s=%s", strings.ToUpper(k), vst), + ) + } stdin, err := exc.Stdin() if err != nil { return err From 58ce4d5a72274d29a83e6c5ddf9d59956180d1da Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 2 Nov 2019 00:00:27 +0300 Subject: [PATCH 09/34] Add stop_signal/cont_signal support --- README.md | 2 +- internal/pkg/config/config.go | 8 ++++ internal/pkg/executor/executor.go | 23 +++++++++-- main.go | 24 +++++++++--- widgets/blank.go | 8 +++- widgets/clock.go | 8 +++- widgets/exec.go | 19 ++++++++-- widgets/http.go | 8 +++- widgets/static.go | 8 +++- widgets/wrapper.go | 51 +++++++++++++++++++++---- yagostatus.go | 63 ++++++++++++++++++++++++------- ygs/widget.go | 2 + 12 files changed, 184 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 0221063..0e73361 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Replace `status_command` to `~/go/bin/yagostatus --config ~/.config/i3/yagostatu ### Troubleshooting Yagostatus outputs error messages in stderr, you can log them by redirecting stderr to a file. -`status_command ~/go/bin/yagostatus --config ~/.config/i3/yagostatus.yml 2> /tmp/yagostatus.log` +`status_command exec ~/go/bin/yagostatus --config ~/.config/i3/yagostatus.yml 2> /tmp/yagostatus.log` ## Configuration diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 0c1067b..14fe0eb 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "strings" + "syscall" "github.com/burik666/yagostatus/internal/pkg/executor" "github.com/burik666/yagostatus/ygs" @@ -15,6 +16,10 @@ import ( // Config represents the main configuration. type Config struct { + Signals struct { + StopSignal syscall.Signal `yaml:"stop"` + ContSignal syscall.Signal `yaml:"cont"` + } `yaml:"signals"` Widgets []WidgetConfig `yaml:"widgets"` } @@ -89,6 +94,9 @@ func Parse(data []byte) (*Config, error) { } config := Config{} + config.Signals.StopSignal = syscall.SIGUSR1 + config.Signals.ContSignal = syscall.SIGCONT + if err := yaml.Unmarshal(data, &config); err != nil { return nil, trimYamlErr(err, false) } diff --git a/internal/pkg/executor/executor.go b/internal/pkg/executor/executor.go index cd550e0..e542095 100644 --- a/internal/pkg/executor/executor.go +++ b/internal/pkg/executor/executor.go @@ -9,6 +9,7 @@ import ( "os/exec" "regexp" "strings" + "syscall" "github.com/burik666/yagostatus/ygs" ) @@ -23,7 +24,8 @@ const ( ) type Executor struct { - cmd *exec.Cmd + cmd *exec.Cmd + header *ygs.I3BarHeader } func Exec(command string, args ...string) (*Executor, error) { @@ -37,6 +39,10 @@ func Exec(command string, args ...string) (*Executor, error) { e.cmd = exec.Command(name, args...) e.cmd.Stderr = os.Stderr e.cmd.Env = os.Environ() + e.cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: 0, + } return e, nil } @@ -98,6 +104,7 @@ func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { var header ygs.I3BarHeader if err := headerDecoder.Decode(&header); err == nil { + e.header = &header decoder.Token() } else { var blocks []ygs.I3BarBlock @@ -109,10 +116,10 @@ func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { for { var blocks []ygs.I3BarBlock if err := decoder.Decode(&blocks); err != nil { - if err != io.EOF { - return err + if err == io.EOF { + return nil } - return nil + return err } c <- blocks } @@ -141,6 +148,14 @@ func (e *Executor) Signal(sig os.Signal) error { return nil } +func (e *Executor) ProcessState() *os.ProcessState { + return e.cmd.ProcessState +} + +func (e *Executor) I3BarHeader() *ygs.I3BarHeader { + return e.header +} + type bufferCloser struct { bytes.Buffer stoped bool diff --git a/main.go b/main.go index 763bba4..1a2fdaa 100644 --- a/main.go +++ b/main.go @@ -82,14 +82,28 @@ func main() { yaGoStatus.errorWidget(cfgError.Error()) } - stopsignals := make(chan os.Signal, 1) - signal.Notify(stopsignals, syscall.SIGINT, syscall.SIGTERM) + stopContSignals := make(chan os.Signal, 1) + signal.Notify(stopContSignals, cfg.Signals.StopSignal, cfg.Signals.ContSignal) + go func() { + for { + sig := <-stopContSignals + switch sig { + case cfg.Signals.StopSignal: + yaGoStatus.Stop() + case cfg.Signals.ContSignal: + yaGoStatus.Continue() + } + } + }() + + shutdownsignals := make(chan os.Signal, 1) + signal.Notify(shutdownsignals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) go func() { yaGoStatus.Run() - stopsignals <- syscall.SIGTERM + shutdownsignals <- syscall.SIGTERM }() - <-stopsignals - yaGoStatus.Stop() + <-shutdownsignals + yaGoStatus.Shutdown() } diff --git a/widgets/blank.go b/widgets/blank.go index 8751d53..9a34629 100644 --- a/widgets/blank.go +++ b/widgets/blank.go @@ -28,5 +28,11 @@ func (w *BlankWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *BlankWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} -// Stop shutdowns the widget. +// Stop stops the widdget. func (w *BlankWidget) Stop() {} + +// Continue continues the widdget. +func (w *BlankWidget) Continue() {} + +// Shutdown shutdowns the widget. +func (w *BlankWidget) Shutdown() {} diff --git a/widgets/clock.go b/widgets/clock.go index 2f95b45..7ece834 100644 --- a/widgets/clock.go +++ b/widgets/clock.go @@ -52,5 +52,11 @@ func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *ClockWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} -// Stop shutdowns the widget. +// Stop stops the widdget. func (w *ClockWidget) Stop() {} + +// Continue continues the widdget. +func (w *ClockWidget) Continue() {} + +// Shutdown shutdowns the widget. +func (w *ClockWidget) Shutdown() {} diff --git a/widgets/exec.go b/widgets/exec.go index 35c4dd8..053d4c8 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -32,6 +32,7 @@ type ExecWidget struct { c chan<- []ygs.I3BarBlock upd chan struct{} customfields map[string]interface{} + ticker *time.Ticker } func init() { @@ -48,6 +49,10 @@ func NewExecWidget(params interface{}) (ygs.Widget, error) { return nil, errors.New("missing 'command' setting") } + if w.params.Interval > 0 { + w.ticker = time.NewTicker(time.Duration(w.params.Interval) * time.Second) + } + if w.params.Signal != nil { sig := *w.params.Signal if sig < 0 || signals.SIGRTMIN+sig > signals.SIGRTMAX { @@ -107,11 +112,11 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { } })() } - if w.params.Interval > 0 { - ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) + + if w.ticker != nil { go (func() { for { - <-ticker.C + <-w.ticker.C w.upd <- struct{}{} } })() @@ -139,9 +144,15 @@ func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { } } -// Stop shutdowns the widget. +// Stop stops the widdget. func (w *ExecWidget) Stop() {} +// Continue continues the widdget. +func (w *ExecWidget) Continue() {} + +// Shutdown shutdowns the widget. +func (w *ExecWidget) Shutdown() {} + func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { customfields := make(map[string]interface{}) for _, block := range blocks { diff --git a/widgets/http.go b/widgets/http.go index f1fbccf..913ec2f 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -149,5 +149,11 @@ func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { ws.Close() } -// Stop shutdowns the widget. +// Stop stops the widdget. func (w *HTTPWidget) Stop() {} + +// Continue continues the widdget. +func (w *HTTPWidget) Continue() {} + +// Shutdown shutdowns the widget. +func (w *HTTPWidget) Shutdown() {} diff --git a/widgets/static.go b/widgets/static.go index c31d9df..da6d675 100644 --- a/widgets/static.go +++ b/widgets/static.go @@ -49,5 +49,11 @@ func (w *StaticWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *StaticWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} -// Stop shutdowns the widget. +// Stop stops the widdget. func (w *StaticWidget) Stop() {} + +// Continue continues the widdget. +func (w *StaticWidget) Continue() {} + +// Shutdown shutdowns the widget. +func (w *StaticWidget) Shutdown() {} diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 3cedbe9..28dfabd 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -3,6 +3,7 @@ package widgets import ( "encoding/json" "errors" + "fmt" "io" "syscall" @@ -37,23 +38,35 @@ func NewWrapperWidget(params interface{}) (ygs.Widget, error) { return nil, errors.New("missing 'command' setting") } + var err error + w.exc, err = executor.Exec(w.params.Command) + if err != nil { + return nil, err + } + return w, nil } // Run starts the main loop. func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { var err error - w.exc, err = executor.Exec(w.params.Command) - if err != nil { - return nil - } + w.stdin, err = w.exc.Stdin() if err != nil { - return nil + return err } + defer w.stdin.Close() + w.stdin.Write([]byte("[")) - return w.exc.Run(c, executor.OutputFormatJSON) + err = w.exc.Run(c, executor.OutputFormatJSON) + if err == nil { + err = errors.New("process exited unexpectedly") + if state := w.exc.ProcessState(); state != nil { + return fmt.Errorf("%w: %s", err, state.String()) + } + } + return err } // Event processes the widget events. @@ -65,10 +78,32 @@ func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock } } -// Stop shutdowns the widget. +// Stop stops the widdget. func (w *WrapperWidget) Stop() { + if header := w.exc.I3BarHeader(); header != nil { + if header.StopSignal != 0 { + w.exc.Signal(syscall.Signal(header.StopSignal)) + return + } + } + w.exc.Signal(syscall.SIGSTOP) +} + +// Continue continues the widdget. +func (w *WrapperWidget) Continue() { + if header := w.exc.I3BarHeader(); header != nil { + if header.ContSignal != 0 { + w.exc.Signal(syscall.Signal(header.ContSignal)) + return + } + } + w.exc.Signal(syscall.SIGCONT) +} + +// Shutdown shutdowns the widget. +func (w *WrapperWidget) Shutdown() { if w.exc != nil { - w.exc.Signal(syscall.SIGHUP) + w.exc.Signal(syscall.SIGTERM) w.exc.Wait() } } diff --git a/yagostatus.go b/yagostatus.go index 6f6962e..3c5a200 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "bytes" "encoding/json" "fmt" "io" @@ -31,11 +30,13 @@ type YaGoStatus struct { workspaces []i3.Workspace visibleWorkspaces []string + + cfg config.Config } // NewYaGoStatus returns a new YaGoStatus instance. func NewYaGoStatus(cfg config.Config) (*YaGoStatus, error) { - status := &YaGoStatus{} + status := &YaGoStatus{cfg: cfg} for _, w := range cfg.Widgets { (func() { defer (func() { @@ -188,8 +189,8 @@ func (status *YaGoStatus) eventReader() { // Run starts the main loop. func (status *YaGoStatus) Run() { status.upd = make(chan int) - status.updateWorkspaces() go (func() { + status.updateWorkspaces() recv := i3.Subscribe(i3.WorkspaceEventType) for recv.Next() { e := recv.Event().(*i3.WorkspaceEvent) @@ -233,12 +234,18 @@ func (status *YaGoStatus) Run() { }(widget, c) } - fmt.Print("{\"version\":1, \"click_events\": true}\n[\n[]") + encoder := json.NewEncoder(os.Stdout) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + + encoder.Encode(ygs.I3BarHeader{ + Version: 1, + ClickEvents: true, + StopSignal: int(status.cfg.Signals.StopSignal), + ContSignal: int(status.cfg.Signals.ContSignal), + }) + fmt.Print("\n[\n[]") go func() { - buf := &bytes.Buffer{} - encoder := json.NewEncoder(buf) - encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") for { select { case <-status.upd: @@ -248,18 +255,16 @@ func (status *YaGoStatus) Run() { result = append(result, widgetOutput...) } } - buf.Reset() - encoder.Encode(result) fmt.Print(",") - fmt.Print(string(buf.Bytes())) + encoder.Encode(result) } } }() status.eventReader() } -// Stop shutdowns widgets and main loop. -func (status *YaGoStatus) Stop() { +// Shutdown shutdowns widgets and main loop. +func (status *YaGoStatus) Shutdown() { var wg sync.WaitGroup for _, widget := range status.widgets { wg.Add(1) @@ -271,12 +276,42 @@ func (status *YaGoStatus) Stop() { debug.PrintStack() } })() - widget.Stop() + widget.Shutdown() }(widget) } wg.Wait() } +// Stop stops widgets and main loop. +func (status *YaGoStatus) Stop() { + for _, widget := range status.widgets { + go func(widget ygs.Widget) { + defer (func() { + if r := recover(); r != nil { + log.Printf("Widget is panicking: %s", r) + debug.PrintStack() + } + })() + widget.Stop() + }(widget) + } +} + +// Continue continues widgets and main loop. +func (status *YaGoStatus) Continue() { + for _, widget := range status.widgets { + go func(widget ygs.Widget) { + defer (func() { + if r := recover(); r != nil { + log.Printf("Widget is panicking: %s", r) + debug.PrintStack() + } + })() + widget.Continue() + }(widget) + } +} + func (status *YaGoStatus) updateWorkspaces() { var err error status.workspaces, err = i3.GetWorkspaces() diff --git a/ygs/widget.go b/ygs/widget.go index d653763..de54c2c 100644 --- a/ygs/widget.go +++ b/ygs/widget.go @@ -6,4 +6,6 @@ type Widget interface { Run(chan<- []I3BarBlock) error Event(I3BarClickEvent, []I3BarBlock) Stop() + Continue() + Shutdown() } From e2b79c12777c5d6aa1744481845502708968dfc6 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 2 Nov 2019 01:07:27 +0300 Subject: [PATCH 10/34] Fix: write events only if click_events is true (wrapper) --- widgets/wrapper.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 28dfabd..393ce1d 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -22,6 +22,8 @@ type WrapperWidget struct { exc *executor.Executor stdin io.WriteCloser + + eventBracketWritten bool } func init() { @@ -58,7 +60,6 @@ func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { defer w.stdin.Close() - w.stdin.Write([]byte("[")) err = w.exc.Run(c, executor.OutputFormatJSON) if err == nil { err = errors.New("process exited unexpectedly") @@ -71,7 +72,15 @@ func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { - if w.stdin != nil { + if w.stdin == nil { + return + } + + if header := w.exc.I3BarHeader(); header != nil && header.ClickEvents { + if !w.eventBracketWritten { + w.eventBracketWritten = true + w.stdin.Write([]byte("[")) + } msg, _ := json.Marshal(event) w.stdin.Write(msg) w.stdin.Write([]byte(",\n")) From a331878abe036f07f372324269b2201877ccf812 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 2 Nov 2019 01:31:09 +0300 Subject: [PATCH 11/34] Add error return --- main.go | 8 ++++++-- widgets/blank.go | 16 ++++++++++++---- widgets/clock.go | 14 ++------------ widgets/exec.go | 14 ++++---------- widgets/http.go | 17 +++++------------ widgets/static.go | 14 ++------------ widgets/wrapper.go | 29 ++++++++++++++++------------- yagostatus.go | 28 +++++++++++++++++++--------- ygs/widget.go | 8 ++++---- 9 files changed, 70 insertions(+), 78 deletions(-) diff --git a/main.go b/main.go index 1a2fdaa..8127bdd 100644 --- a/main.go +++ b/main.go @@ -100,10 +100,14 @@ func main() { signal.Notify(shutdownsignals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) go func() { - yaGoStatus.Run() + if err := yaGoStatus.Run(); err != nil { + log.Printf("Failed to run yagostatus: %s", err) + } shutdownsignals <- syscall.SIGTERM }() <-shutdownsignals - yaGoStatus.Shutdown() + if err := yaGoStatus.Shutdown(); err != nil { + log.Printf("Failed to shutdown yagostatus: %s", err) + } } diff --git a/widgets/blank.go b/widgets/blank.go index 9a34629..56bd125 100644 --- a/widgets/blank.go +++ b/widgets/blank.go @@ -26,13 +26,21 @@ func (w *BlankWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *BlankWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} +func (w *BlankWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { + return nil +} // Stop stops the widdget. -func (w *BlankWidget) Stop() {} +func (w *BlankWidget) Stop() error { + return nil +} // Continue continues the widdget. -func (w *BlankWidget) Continue() {} +func (w *BlankWidget) Continue() error { + return nil +} // Shutdown shutdowns the widget. -func (w *BlankWidget) Shutdown() {} +func (w *BlankWidget) Shutdown() error { + return nil +} diff --git a/widgets/clock.go b/widgets/clock.go index 7ece834..e62efda 100644 --- a/widgets/clock.go +++ b/widgets/clock.go @@ -14,6 +14,8 @@ type ClockWidgetParams struct { // ClockWidget implements a clock. type ClockWidget struct { + BlankWidget + params ClockWidgetParams } @@ -48,15 +50,3 @@ func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error { } } } - -// Event processes the widget events. -func (w *ClockWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} - -// Stop stops the widdget. -func (w *ClockWidget) Stop() {} - -// Continue continues the widdget. -func (w *ClockWidget) Continue() {} - -// Shutdown shutdowns the widget. -func (w *ClockWidget) Shutdown() {} diff --git a/widgets/exec.go b/widgets/exec.go index 053d4c8..090db6d 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -26,6 +26,8 @@ type ExecWidgetParams struct { // ExecWidget implements the exec widget. type ExecWidget struct { + BlankWidget + params ExecWidgetParams signal os.Signal @@ -137,22 +139,14 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { +func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { w.setCustomFields(blocks) if w.params.EventsUpdate { w.upd <- struct{}{} } + return nil } -// Stop stops the widdget. -func (w *ExecWidget) Stop() {} - -// Continue continues the widdget. -func (w *ExecWidget) Continue() {} - -// Shutdown shutdowns the widget. -func (w *ExecWidget) Shutdown() {} - func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { customfields := make(map[string]interface{}) for _, block := range blocks { diff --git a/widgets/http.go b/widgets/http.go index 913ec2f..2c356f9 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -22,6 +22,8 @@ type HTTPWidgetParams struct { // HTTPWidget implements the http server widget. type HTTPWidget struct { + BlankWidget + params HTTPWidgetParams httpServer *http.Server @@ -94,11 +96,11 @@ func (w *HTTPWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { +func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { if w.conn != nil { - websocket.JSON.Send(w.conn, event) + return websocket.JSON.Send(w.conn, event) } - + return nil } func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Request) { @@ -148,12 +150,3 @@ func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { } ws.Close() } - -// Stop stops the widdget. -func (w *HTTPWidget) Stop() {} - -// Continue continues the widdget. -func (w *HTTPWidget) Continue() {} - -// Shutdown shutdowns the widget. -func (w *HTTPWidget) Shutdown() {} diff --git a/widgets/static.go b/widgets/static.go index da6d675..b415814 100644 --- a/widgets/static.go +++ b/widgets/static.go @@ -14,6 +14,8 @@ type StaticWidgetParams struct { // StaticWidget implements a static widget. type StaticWidget struct { + BlankWidget + params StaticWidgetParams blocks []ygs.I3BarBlock @@ -45,15 +47,3 @@ func (w *StaticWidget) Run(c chan<- []ygs.I3BarBlock) error { c <- w.blocks return nil } - -// Event processes the widget events. -func (w *StaticWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) {} - -// Stop stops the widdget. -func (w *StaticWidget) Stop() {} - -// Continue continues the widdget. -func (w *StaticWidget) Continue() {} - -// Shutdown shutdowns the widget. -func (w *StaticWidget) Shutdown() {} diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 393ce1d..ba3ba4e 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -71,9 +71,9 @@ func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { } // Event processes the widget events. -func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) { +func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { if w.stdin == nil { - return + return nil } if header := w.exc.I3BarHeader(); header != nil && header.ClickEvents { @@ -85,34 +85,37 @@ func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock w.stdin.Write(msg) w.stdin.Write([]byte(",\n")) } + return nil } // Stop stops the widdget. -func (w *WrapperWidget) Stop() { +func (w *WrapperWidget) Stop() error { if header := w.exc.I3BarHeader(); header != nil { if header.StopSignal != 0 { - w.exc.Signal(syscall.Signal(header.StopSignal)) - return + return w.exc.Signal(syscall.Signal(header.StopSignal)) } } - w.exc.Signal(syscall.SIGSTOP) + return w.exc.Signal(syscall.SIGSTOP) } // Continue continues the widdget. -func (w *WrapperWidget) Continue() { +func (w *WrapperWidget) Continue() error { if header := w.exc.I3BarHeader(); header != nil { if header.ContSignal != 0 { - w.exc.Signal(syscall.Signal(header.ContSignal)) - return + return w.exc.Signal(syscall.Signal(header.ContSignal)) } } - w.exc.Signal(syscall.SIGCONT) + return w.exc.Signal(syscall.SIGCONT) } // Shutdown shutdowns the widget. -func (w *WrapperWidget) Shutdown() { +func (w *WrapperWidget) Shutdown() error { if w.exc != nil { - w.exc.Signal(syscall.SIGTERM) - w.exc.Wait() + err := w.exc.Signal(syscall.SIGTERM) + if err != nil { + return err + } + return w.exc.Wait() } + return nil } diff --git a/yagostatus.go b/yagostatus.go index 3c5a200..2673a20 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -84,7 +84,9 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, Color: "#ff0000", }} } - status.widgets[widgetIndex].Event(event, status.widgetsOutput[widgetIndex]) + if err := status.widgets[widgetIndex].Event(event, status.widgetsOutput[widgetIndex]); err != nil { + log.Printf("Failed to process widget event: %s", err) + } })() for _, widgetEvent := range status.widgetsConfig[widgetIndex].Events { @@ -145,13 +147,13 @@ func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlo status.upd <- widgetIndex } -func (status *YaGoStatus) eventReader() { +func (status *YaGoStatus) eventReader() error { reader := bufio.NewReader(os.Stdin) for { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { - log.Fatal(err) + return err } break } @@ -184,10 +186,11 @@ func (status *YaGoStatus) eventReader() { } } } + return nil } // Run starts the main loop. -func (status *YaGoStatus) Run() { +func (status *YaGoStatus) Run() error { status.upd = make(chan int) go (func() { status.updateWorkspaces() @@ -260,11 +263,11 @@ func (status *YaGoStatus) Run() { } } }() - status.eventReader() + return status.eventReader() } // Shutdown shutdowns widgets and main loop. -func (status *YaGoStatus) Shutdown() { +func (status *YaGoStatus) Shutdown() error { var wg sync.WaitGroup for _, widget := range status.widgets { wg.Add(1) @@ -276,10 +279,13 @@ func (status *YaGoStatus) Shutdown() { debug.PrintStack() } })() - widget.Shutdown() + if err := widget.Shutdown(); err != nil { + log.Printf("Failed to shutdown widget: %s", err) + } }(widget) } wg.Wait() + return nil } // Stop stops widgets and main loop. @@ -292,7 +298,9 @@ func (status *YaGoStatus) Stop() { debug.PrintStack() } })() - widget.Stop() + if err := widget.Stop(); err != nil { + log.Printf("Failed to stop widget: %s", err) + } }(widget) } } @@ -307,7 +315,9 @@ func (status *YaGoStatus) Continue() { debug.PrintStack() } })() - widget.Continue() + if err := widget.Continue(); err != nil { + log.Printf("Failed to continue widget: %s", err) + } }(widget) } } diff --git a/ygs/widget.go b/ygs/widget.go index de54c2c..2ebdd4d 100644 --- a/ygs/widget.go +++ b/ygs/widget.go @@ -4,8 +4,8 @@ package ygs // Widget represents a widget struct. type Widget interface { Run(chan<- []I3BarBlock) error - Event(I3BarClickEvent, []I3BarBlock) - Stop() - Continue() - Shutdown() + Event(I3BarClickEvent, []I3BarBlock) error + Stop() error + Continue() error + Shutdown() error } From 60d35ac5f87357767543cd880dd967836ddcf63e Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 2 Nov 2019 02:47:43 +0300 Subject: [PATCH 12/34] Fix: err checking, style (by golang-lint) --- internal/pkg/config/config.go | 38 +++++++-- internal/pkg/executor/executor.go | 40 ++++++--- main.go | 8 ++ widgets/clock.go | 18 +++-- widgets/exec.go | 17 +++- widgets/http.go | 24 +++++- widgets/static.go | 2 +- widgets/wrapper.go | 32 ++++++-- yagostatus.go | 129 +++++++++++++++++++++++------- ygs/protocol.go | 24 ++++-- ygs/utils.go | 28 +++++-- 11 files changed, 280 insertions(+), 80 deletions(-) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 14fe0eb..f701ae4 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "log" "strings" "syscall" @@ -36,13 +37,15 @@ type WidgetConfig struct { // Validate checks widget configuration. func (c WidgetConfig) Validate() error { if c.Name == "" { - return errors.New("Missing widget name") + return errors.New("missing widget name") } + for ei := range c.Events { if err := c.Events[ei].Validate(); err != nil { return err } } + return nil } @@ -59,22 +62,27 @@ type WidgetEventConfig struct { // Validate checks event parameters. func (e *WidgetEventConfig) Validate() error { var availableWidgetEventModifiers = [...]string{"Shift", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"} + for _, mod := range e.Modifiers { found := false mod = strings.TrimLeft(mod, "!") + for _, m := range availableWidgetEventModifiers { if mod == m { found = true break } } + if !found { - return fmt.Errorf("Unknown '%s' modifier", mod) + return fmt.Errorf("unknown '%s' modifier", mod) } } + if e.OutputFormat == "" { e.OutputFormat = executor.OutputFormatNone } + return nil } @@ -84,6 +92,7 @@ func LoadFile(filename string) (*Config, error) { if err != nil { return nil, err } + return Parse(data) } @@ -100,7 +109,10 @@ func Parse(data []byte) (*Config, error) { if err := yaml.Unmarshal(data, &config); err != nil { return nil, trimYamlErr(err, false) } - yaml.Unmarshal(data, &raw) + + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil, trimYamlErr(err, false) + } for widgetIndex := range config.Widgets { widget := &config.Widgets[widgetIndex] @@ -109,17 +121,30 @@ func Parse(data []byte) (*Config, error) { tpl, ok := params["template"] if ok { if err := json.Unmarshal([]byte(tpl.(string)), &widget.Template); err != nil { - return nil, err + name, params := ygs.ErrorWidget(err.Error()) + *widget = WidgetConfig{ + Name: name, + Params: params, + } + + log.Printf("template error: %s", err) + + continue } } - tmp, _ := yaml.Marshal(params["events"]) + tmp, err := yaml.Marshal(params["events"]) + if err != nil { + return nil, err + } + if err := yaml.UnmarshalStrict(tmp, &widget.Events); err != nil { name, params := ygs.ErrorWidget(trimYamlErr(err, true).Error()) *widget = WidgetConfig{ Name: name, Params: params, } + continue } @@ -130,6 +155,7 @@ func Parse(data []byte) (*Config, error) { Name: name, Params: params, } + continue } @@ -138,6 +164,7 @@ func Parse(data []byte) (*Config, error) { delete(params, "template") delete(params, "events") } + return &config, nil } @@ -147,5 +174,6 @@ func trimYamlErr(err error, trimLineN bool) error { msg = strings.TrimPrefix(msg, "line ") msg = strings.TrimLeft(msg, "1234567890: ") } + return errors.New(msg) } diff --git a/internal/pkg/executor/executor.go b/internal/pkg/executor/executor.go index e542095..2ca1ab0 100644 --- a/internal/pkg/executor/executor.go +++ b/internal/pkg/executor/executor.go @@ -52,49 +52,60 @@ func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { if err != nil { return err } + defer stdout.Close() if err := e.cmd.Start(); err != nil { return err } + defer e.Wait() if format == OutputFormatNone { return nil } + buf := &bufferCloser{} outreader := io.TeeReader(stdout, buf) decoder := json.NewDecoder(outreader) - isJSON := false var firstMessage interface{} err = decoder.Decode(&firstMessage) + if (err != nil) && format == OutputFormatJSON { + buf.Close() + return err + } + + isJSON := false switch firstMessage.(type) { case map[string]interface{}: isJSON = true case []interface{}: isJSON = true } - if (err != nil) && format == OutputFormatJSON { - buf.Close() - return err - } if err != nil || !isJSON || format == OutputFormatText { - io.Copy(ioutil.Discard, outreader) + _, err := io.Copy(ioutil.Discard, outreader) + if err != nil { + return err + } + if buf.Len() > 0 { c <- []ygs.I3BarBlock{ - ygs.I3BarBlock{ - FullText: strings.Trim(string(buf.Bytes()), "\n "), + { + FullText: strings.Trim(buf.String(), "\n "), }, } } + buf.Close() + return nil } + buf.Close() firstMessageData, _ := json.Marshal(firstMessage) @@ -105,7 +116,11 @@ func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { var header ygs.I3BarHeader if err := headerDecoder.Decode(&header); err == nil { e.header = &header - decoder.Token() + + _, err := decoder.Token() + if err != nil { + return err + } } else { var blocks []ygs.I3BarBlock if err := json.Unmarshal(firstMessageData, &blocks); err != nil { @@ -113,17 +128,18 @@ func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { } c <- blocks } + for { var blocks []ygs.I3BarBlock if err := decoder.Decode(&blocks); err != nil { if err == io.EOF { return nil } + return err } c <- blocks } - return nil } func (e *Executor) Stdin() (io.WriteCloser, error) { @@ -138,6 +154,7 @@ func (e *Executor) Wait() error { if e.cmd != nil { return e.cmd.Wait() } + return nil } @@ -145,6 +162,7 @@ func (e *Executor) Signal(sig os.Signal) error { if e.cmd != nil && e.cmd.Process != nil { return e.cmd.Process.Signal(sig) } + return nil } @@ -165,11 +183,13 @@ func (b *bufferCloser) Write(p []byte) (n int, err error) { if b.stoped { return len(p), nil } + return b.Buffer.Write(p) } func (b *bufferCloser) Close() error { b.stoped = true b.Reset() + return nil } diff --git a/main.go b/main.go index 8127bdd..6f7f335 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,9 @@ func main() { log.SetFlags(log.Ldate + log.Ltime + log.Lshortfile) var configFile string + flag.StringVar(&configFile, "config", "", `config file (default "yagostatus.yml")`) + versionFlag := flag.Bool("version", false, "print version information and exit") flag.Parse() @@ -52,17 +54,20 @@ func main() { } var cfg *config.Config + var cfgError, err error if configFile == "" { cfg, cfgError = config.LoadFile("yagostatus.yml") if os.IsNotExist(cfgError) { cfgError = nil + cfg, err = config.Parse(builtinConfig) if err != nil { log.Fatalf("Failed to parse builtin config: %s", err) } } + if cfgError != nil { cfg = &config.Config{} } @@ -77,6 +82,7 @@ func main() { if err != nil { log.Fatalf("Failed to create yagostatus instance: %s", err) } + if cfgError != nil { log.Printf("Failed to load config: %s", cfgError) yaGoStatus.errorWidget(cfgError.Error()) @@ -84,6 +90,7 @@ func main() { stopContSignals := make(chan os.Signal, 1) signal.Notify(stopContSignals, cfg.Signals.StopSignal, cfg.Signals.ContSignal) + go func() { for { sig := <-stopContSignals @@ -107,6 +114,7 @@ func main() { }() <-shutdownsignals + if err := yaGoStatus.Shutdown(); err != nil { log.Printf("Failed to shutdown yagostatus: %s", err) } diff --git a/widgets/clock.go b/widgets/clock.go index e62efda..98d79f7 100644 --- a/widgets/clock.go +++ b/widgets/clock.go @@ -31,22 +31,24 @@ func NewClockWidget(params interface{}) (ygs.Widget, error) { w := &ClockWidget{ params: params.(ClockWidgetParams), } + return w, nil } // Run starts the main loop. func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error { - ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) res := []ygs.I3BarBlock{ - ygs.I3BarBlock{}, + {}, } res[0].FullText = time.Now().Format(w.params.Format) + c <- res - for { - select { - case t := <-ticker.C: - res[0].FullText = t.Format(w.params.Format) - c <- res - } + + ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) + for t := range ticker.C { + res[0].FullText = t.Format(w.params.Format) + c <- res } + + return nil } diff --git a/widgets/exec.go b/widgets/exec.go index 090db6d..b6b9dbb 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -48,7 +48,7 @@ func NewExecWidget(params interface{}) (ygs.Widget, error) { } if len(w.params.Command) == 0 { - return nil, errors.New("missing 'command' setting") + return nil, errors.New("missing 'command'") } if w.params.Interval > 0 { @@ -60,8 +60,10 @@ func NewExecWidget(params interface{}) (ygs.Widget, error) { if sig < 0 || signals.SIGRTMIN+sig > signals.SIGRTMAX { return nil, fmt.Errorf("signal should be between 0 AND %d", signals.SIGRTMAX-signals.SIGRTMIN) } + w.signal = syscall.Signal(signals.SIGRTMIN + sig) } + return w, nil } @@ -76,10 +78,10 @@ func (w *ExecWidget) exec() error { exc.AddEnv( fmt.Sprintf("I3_%s=%s", strings.ToUpper(k), vst), ) - } c := make(chan []ygs.I3BarBlock) + go (func() { for { blocks, ok := <-c @@ -90,8 +92,11 @@ func (w *ExecWidget) exec() error { w.setCustomFields(blocks) } })() + err = exc.Run(c, w.params.OutputFormat) + close(c) + return err } @@ -107,6 +112,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { if w.signal != nil { sigc := make(chan os.Signal, 1) signal.Notify(sigc, w.signal) + go (func() { for { <-sigc @@ -128,31 +134,36 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { err := w.exec() if err != nil { c <- []ygs.I3BarBlock{ - ygs.I3BarBlock{ + { FullText: err.Error(), Color: "#ff0000", }, } } } + return nil } // Event processes the widget events. func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { w.setCustomFields(blocks) + if w.params.EventsUpdate { w.upd <- struct{}{} } + return nil } func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { customfields := make(map[string]interface{}) + for _, block := range blocks { for k, v := range block.Custom { customfields[k] = v } } + w.customfields = customfields } diff --git a/widgets/http.go b/widgets/http.go index 2c356f9..ec1387b 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -49,11 +49,11 @@ func NewHTTPWidget(params interface{}) (ygs.Widget, error) { } if len(w.params.Listen) == 0 { - return nil, errors.New("missing 'listen' param") + return nil, errors.New("missing 'listen'") } if len(w.params.Path) == 0 { - return nil, errors.New("missing 'path' setting") + return nil, errors.New("missing 'path'") } if instances == nil { @@ -64,6 +64,7 @@ func NewHTTPWidget(params interface{}) (ygs.Widget, error) { if _, ok := instance.paths[w.params.Path]; ok { return nil, fmt.Errorf("path '%s' already in use", w.params.Path) } + instance.mux.HandleFunc(w.params.Path, w.httpHandler) instance.paths[w.params.Path] = struct{}{} } else { @@ -79,7 +80,6 @@ func NewHTTPWidget(params interface{}) (ygs.Widget, error) { Addr: w.params.Listen, Handler: instance.mux, } - } return w, nil @@ -100,6 +100,7 @@ func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) e if w.conn != nil { return websocket.JSON.Send(w.conn, event) } + return nil } @@ -107,30 +108,41 @@ func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Req if request.Method == "GET" { ws := websocket.Handler(w.wsHandler) ws.ServeHTTP(response, request) + return } + if request.Method == "POST" { body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("%s", err) } + var messages []ygs.I3BarBlock if err := json.Unmarshal(body, &messages); err != nil { log.Printf("%s", err) response.WriteHeader(http.StatusBadRequest) fmt.Fprintf(response, "%s", err) } + w.c <- messages + return } response.WriteHeader(http.StatusBadRequest) - response.Write([]byte("Bad request method, allow GET for websocket and POST for HTTP update")) + + _, err := response.Write([]byte("bad request method, allow GET for websocket and POST for HTTP update")) + if err != nil { + log.Printf("failed to write response: %s", err) + } } func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { var messages []ygs.I3BarBlock + w.conn = ws + for { if err := websocket.JSON.Receive(ws, &messages); err != nil { if err == io.EOF { @@ -138,15 +150,19 @@ func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { w.c <- nil w.conn = nil } + break } + log.Printf("%s", err) } if w.conn != ws { break } + w.c <- messages } + ws.Close() } diff --git a/widgets/static.go b/widgets/static.go index b415814..413e501 100644 --- a/widgets/static.go +++ b/widgets/static.go @@ -32,7 +32,7 @@ func NewStaticWidget(params interface{}) (ygs.Widget, error) { } if len(w.params.Blocks) == 0 { - return nil, errors.New("missing 'blocks' setting") + return nil, errors.New("missing 'blocks'") } if err := json.Unmarshal([]byte(w.params.Blocks), &w.blocks); err != nil { diff --git a/widgets/wrapper.go b/widgets/wrapper.go index ba3ba4e..7c372c2 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -37,15 +37,16 @@ func NewWrapperWidget(params interface{}) (ygs.Widget, error) { } if len(w.params.Command) == 0 { - return nil, errors.New("missing 'command' setting") + return nil, errors.New("missing 'command'") } - var err error - w.exc, err = executor.Exec(w.params.Command) + exc, err := executor.Exec(w.params.Command) if err != nil { return nil, err } + w.exc = exc + return w, nil } @@ -63,10 +64,12 @@ func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { err = w.exc.Run(c, executor.OutputFormatJSON) if err == nil { err = errors.New("process exited unexpectedly") + if state := w.exc.ProcessState(); state != nil { return fmt.Errorf("%w: %s", err, state.String()) } } + return err } @@ -79,12 +82,23 @@ func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock if header := w.exc.I3BarHeader(); header != nil && header.ClickEvents { if !w.eventBracketWritten { w.eventBracketWritten = true - w.stdin.Write([]byte("[")) + if _, err := w.stdin.Write([]byte("[")); err != nil { + return err + } + } + + msg, err := json.Marshal(event) + if err != nil { + return err + } + + msg = append(msg, []byte(",\n")...) + + if _, err := w.stdin.Write(msg); err != nil { + return err } - msg, _ := json.Marshal(event) - w.stdin.Write(msg) - w.stdin.Write([]byte(",\n")) } + return nil } @@ -95,6 +109,7 @@ func (w *WrapperWidget) Stop() error { return w.exc.Signal(syscall.Signal(header.StopSignal)) } } + return w.exc.Signal(syscall.SIGSTOP) } @@ -105,6 +120,7 @@ func (w *WrapperWidget) Continue() error { return w.exc.Signal(syscall.Signal(header.ContSignal)) } } + return w.exc.Signal(syscall.SIGCONT) } @@ -115,7 +131,9 @@ func (w *WrapperWidget) Shutdown() error { if err != nil { return err } + return w.exc.Wait() } + return nil } diff --git a/yagostatus.go b/yagostatus.go index 2673a20..422d899 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -37,6 +37,7 @@ type YaGoStatus struct { // NewYaGoStatus returns a new YaGoStatus instance. func NewYaGoStatus(cfg config.Config) (*YaGoStatus, error) { status := &YaGoStatus{cfg: cfg} + for _, w := range cfg.Widgets { (func() { defer (func() { @@ -46,12 +47,15 @@ func NewYaGoStatus(cfg config.Config) (*YaGoStatus, error) { status.errorWidget("Widget is panicking") } })() + widget, err := ygs.NewWidget(w.Name, w.Params) if err != nil { log.Printf("Failed to create widget: %s", err) status.errorWidget(err.Error()) + return } + status.AddWidget(widget, w) })() } @@ -64,6 +68,7 @@ func (status *YaGoStatus) errorWidget(text string) { if err != nil { panic(err) } + status.AddWidget(errWidget, config.WidgetConfig{}) } @@ -79,11 +84,12 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, if r := recover(); r != nil { log.Printf("Widget event is panicking: %s", r) debug.PrintStack() - status.widgetsOutput[widgetIndex] = []ygs.I3BarBlock{ygs.I3BarBlock{ + status.widgetsOutput[widgetIndex] = []ygs.I3BarBlock{{ FullText: "Widget event is panicking", Color: "#ff0000", }} } + if err := status.widgets[widgetIndex].Event(event, status.widgetsOutput[widgetIndex]); err != nil { log.Printf("Failed to process widget event: %s", err) } @@ -94,11 +100,11 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, (widgetEvent.Name == "" || widgetEvent.Name == event.Name) && (widgetEvent.Instance == "" || widgetEvent.Instance == event.Instance) && checkModifiers(widgetEvent.Modifiers, event.Modifiers) { - exc, err := executor.Exec("sh", "-c", widgetEvent.Command) if err != nil { return err } + exc.AddEnv( fmt.Sprintf("I3_%s=%s", "NAME", event.Name), fmt.Sprintf("I3_%s=%s", "INSTANCE", event.Instance), @@ -111,20 +117,35 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, fmt.Sprintf("I3_%s=%d", "HEIGHT", event.Height), fmt.Sprintf("I3_%s=%s", "MODIFIERS", strings.Join(event.Modifiers, ",")), ) + for k, v := range status.widgetsOutput[widgetIndex][outputIndex].Custom { vst, _ := json.Marshal(v) exc.AddEnv( fmt.Sprintf("I3_%s=%s", strings.ToUpper(k), vst), ) } + stdin, err := exc.Stdin() if err != nil { return err } - eventJSON, _ := json.Marshal(event) - stdin.Write(eventJSON) - stdin.Write([]byte("\n")) - stdin.Close() + + eventJSON, err := json.Marshal(event) + if err != nil { + return err + } + + eventJSON = append(eventJSON, []byte("\n")...) + + _, err = stdin.Write(eventJSON) + if err != nil { + return err + } + + err = stdin.Close() + if err != nil { + return err + } err = exc.Run(status.widgetChans[widgetIndex], widgetEvent.OutputFormat) if err != nil { @@ -132,48 +153,63 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, } } } + return nil } func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlock) { status.widgetsOutput[widgetIndex] = make([]ygs.I3BarBlock, len(blocks)) + for blockIndex := range blocks { block := blocks[blockIndex] - mergeBlocks(&block, status.widgetsConfig[widgetIndex].Template) + if err := mergeBlocks(&block, status.widgetsConfig[widgetIndex].Template); err != nil { + log.Printf("Failed to merge blocks: %s", err) + } + block.Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, block.Name) block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", widgetIndex, blockIndex, block.Instance) + status.widgetsOutput[widgetIndex][blockIndex] = block } + status.upd <- widgetIndex } func (status *YaGoStatus) eventReader() error { reader := bufio.NewReader(os.Stdin) + for { line, err := reader.ReadString('\n') if err != nil { if err != io.EOF { return err } + break } + line = strings.Trim(line, "[], \n") if line == "" { continue } + var event ygs.I3BarClickEvent if err := json.Unmarshal([]byte(line), &event); err != nil { log.Printf("%s (%s)", err, line) + continue } + for widgetIndex, widgetOutputs := range status.widgetsOutput { for outputIndex, output := range widgetOutputs { if (event.Name != "" && event.Name == output.Name) && (event.Instance != "" && event.Instance == output.Instance) { e := event e.Name = strings.Join(strings.Split(e.Name, "-")[2:], "-") e.Instance = strings.Join(strings.Split(e.Instance, "-")[3:], "-") + if err := status.processWidgetEvents(widgetIndex, outputIndex, e); err != nil { log.Print(err) + status.widgetsOutput[widgetIndex][outputIndex] = ygs.I3BarBlock{ FullText: fmt.Sprintf("Event error: %s", err.Error()), Color: "#ff0000", @@ -181,17 +217,20 @@ func (status *YaGoStatus) eventReader() error { Instance: event.Instance, } } + break } } } } + return nil } // Run starts the main loop. func (status *YaGoStatus) Run() error { status.upd = make(chan int) + go (func() { status.updateWorkspaces() recv := i3.Subscribe(i3.WorkspaceEventType) @@ -204,22 +243,21 @@ func (status *YaGoStatus) Run() error { status.upd <- -1 } })() + for widgetIndex, widget := range status.widgets { c := make(chan []ygs.I3BarBlock) status.widgetChans = append(status.widgetChans, c) + go func(widgetIndex int, c chan []ygs.I3BarBlock) { - for { - select { - case out := <-c: - status.addWidgetOutput(widgetIndex, out) - } + for out := range c { + status.addWidgetOutput(widgetIndex, out) } }(widgetIndex, c) go func(widget ygs.Widget, c chan []ygs.I3BarBlock) { defer (func() { if r := recover(); r != nil { - c <- []ygs.I3BarBlock{ygs.I3BarBlock{ + c <- []ygs.I3BarBlock{{ FullText: "Widget is panicking", Color: "#ff0000", }} @@ -229,7 +267,7 @@ func (status *YaGoStatus) Run() error { })() if err := widget.Run(c); err != nil { log.Print(err) - c <- []ygs.I3BarBlock{ygs.I3BarBlock{ + c <- []ygs.I3BarBlock{{ FullText: err.Error(), Color: "#ff0000", }} @@ -241,36 +279,43 @@ func (status *YaGoStatus) Run() error { encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") - encoder.Encode(ygs.I3BarHeader{ + if err := encoder.Encode(ygs.I3BarHeader{ Version: 1, ClickEvents: true, StopSignal: int(status.cfg.Signals.StopSignal), ContSignal: int(status.cfg.Signals.ContSignal), - }) + }); err != nil { + log.Printf("Failed to encode I3BarHeader: %s", err) + } + fmt.Print("\n[\n[]") + go func() { - for { - select { - case <-status.upd: - var result []ygs.I3BarBlock - for widgetIndex, widgetOutput := range status.widgetsOutput { - if checkWorkspaceConditions(status.widgetsConfig[widgetIndex].Workspaces, status.visibleWorkspaces) { - result = append(result, widgetOutput...) - } + for range status.upd { + var result []ygs.I3BarBlock + for widgetIndex, widgetOutput := range status.widgetsOutput { + if checkWorkspaceConditions(status.widgetsConfig[widgetIndex].Workspaces, status.visibleWorkspaces) { + result = append(result, widgetOutput...) } - fmt.Print(",") - encoder.Encode(result) + } + fmt.Print(",") + err := encoder.Encode(result) + if err != nil { + log.Printf("Failed to encode result: %s", err) } } }() + return status.eventReader() } // Shutdown shutdowns widgets and main loop. func (status *YaGoStatus) Shutdown() error { var wg sync.WaitGroup + for _, widget := range status.widgets { wg.Add(1) + go func(widget ygs.Widget) { defer wg.Done() defer (func() { @@ -284,7 +329,9 @@ func (status *YaGoStatus) Shutdown() error { } }(widget) } + wg.Wait() + return nil } @@ -324,43 +371,58 @@ func (status *YaGoStatus) Continue() { func (status *YaGoStatus) updateWorkspaces() { var err error + status.workspaces, err = i3.GetWorkspaces() + if err != nil { log.Printf("Failed to get workspaces: %s", err) } + var vw []string + for i := range status.workspaces { if status.workspaces[i].Visible { vw = append(vw, status.workspaces[i].Name) } } + status.visibleWorkspaces = vw } -func mergeBlocks(b *ygs.I3BarBlock, tpl ygs.I3BarBlock) { - jb, _ := json.Marshal(*b) +func mergeBlocks(b *ygs.I3BarBlock, tpl ygs.I3BarBlock) error { + jb, err := json.Marshal(*b) + if err != nil { + return err + } + *b = tpl - json.Unmarshal(jb, b) + + return json.Unmarshal(jb, b) } func checkModifiers(conditions []string, values []string) bool { for _, c := range conditions { isNegative := c[0] == '!' c = strings.TrimLeft(c, "!") + found := false + for _, v := range values { if c == v { found = true break } } + if found && isNegative { return false } - if (!found) && !isNegative { + + if !found && !isNegative { return false } } + return true } @@ -368,23 +430,30 @@ func checkWorkspaceConditions(conditions []string, values []string) bool { if len(conditions) == 0 { return true } + pass := 0 + for _, c := range conditions { isNegative := c[0] == '!' c = strings.TrimLeft(c, "!") + found := false + for _, v := range values { if c == v { found = true break } } + if found && !isNegative { return true } + if !found && isNegative { pass++ } } + return len(conditions) == pass } diff --git a/ygs/protocol.go b/ygs/protocol.go index 094dd8b..25c8616 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -49,6 +49,8 @@ type I3BarClickEvent struct { Modifiers []string `json:"modifiers"` } +type dataWrapped I3BarBlock + // UnmarshalJSON unmarshals json with custom keys (with _ prefix). func (b *I3BarBlock) UnmarshalJSON(data []byte) error { var resmap map[string]interface{} @@ -57,7 +59,6 @@ func (b *I3BarBlock) UnmarshalJSON(data []byte) error { return err } - type dataWrapped I3BarBlock wr := dataWrapped(*b) for k, v := range resmap { @@ -70,7 +71,9 @@ func (b *I3BarBlock) UnmarshalJSON(data []byte) error { if wr.Custom == nil { wr.Custom = make(map[string]interface{}) } + wr.Custom[k] = v + delete(resmap, k) } } @@ -78,10 +81,13 @@ func (b *I3BarBlock) UnmarshalJSON(data []byte) error { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) - enc.Encode(resmap) + if err := enc.Encode(resmap); err != nil { + return err + } decoder := json.NewDecoder(buf) decoder.DisallowUnknownFields() + if err := decoder.Decode(&wr); err != nil { return err } @@ -93,15 +99,14 @@ func (b *I3BarBlock) UnmarshalJSON(data []byte) error { // MarshalJSON marshals json with custom keys (with _ prefix). func (b I3BarBlock) MarshalJSON() ([]byte, error) { - type dataWrapped I3BarBlock - var wd dataWrapped - wd = dataWrapped(b) + wd := dataWrapped(b) if len(wd.Custom) == 0 { buf := &bytes.Buffer{} encoder := json.NewEncoder(buf) encoder.SetEscapeHTML(false) err := encoder.Encode(wd) + return buf.Bytes(), err } @@ -110,14 +115,19 @@ func (b I3BarBlock) MarshalJSON() ([]byte, error) { var tmp []byte tmp, _ = json.Marshal(wd) - json.Unmarshal(tmp, &resmap) + if err := json.Unmarshal(tmp, &resmap); err != nil { + return nil, err + } tmp, _ = json.Marshal(wd.Custom) - json.Unmarshal(tmp, &resmap) + if err := json.Unmarshal(tmp, &resmap); err != nil { + return nil, err + } buf := &bytes.Buffer{} encoder := json.NewEncoder(buf) encoder.SetEscapeHTML(false) err := encoder.Encode(resmap) + return buf.Bytes(), err } diff --git a/ygs/utils.go b/ygs/utils.go index e546e0d..2c3cce2 100644 --- a/ygs/utils.go +++ b/ygs/utils.go @@ -2,8 +2,10 @@ package ygs import ( "encoding/json" + "errors" "fmt" "reflect" + "strings" "gopkg.in/yaml.v2" ) @@ -20,12 +22,14 @@ var registeredWidgets = make(map[string]widget) // RegisterWidget registers widget. func RegisterWidget(name string, newFunc newWidgetFunc, defaultParams interface{}) { if _, ok := registeredWidgets[name]; ok { - panic(fmt.Sprintf("Widget '%s' already registered", name)) + panic(fmt.Sprintf("widget '%s' already registered", name)) } + def := reflect.ValueOf(defaultParams) if def.Kind() != reflect.Struct { panic("defaultParams should be a struct") } + registeredWidgets[name] = widget{ newFunc: newFunc, defaultParams: defaultParams, @@ -36,7 +40,7 @@ func RegisterWidget(name string, newFunc newWidgetFunc, defaultParams interface{ func NewWidget(name string, rawParams map[string]interface{}) (Widget, error) { widget, ok := registeredWidgets[name] if !ok { - return nil, fmt.Errorf("Widget '%s' not found", name) + return nil, fmt.Errorf("widget '%s' not found", name) } def := reflect.ValueOf(widget.defaultParams) @@ -44,18 +48,22 @@ func NewWidget(name string, rawParams map[string]interface{}) (Widget, error) { params := reflect.New(def.Type()) params.Elem().Set(def) - b, _ := yaml.Marshal(rawParams) - if err := yaml.UnmarshalStrict(b, params.Interface()); err != nil { + b, err := yaml.Marshal(rawParams) + if err != nil { return nil, err } + if err := yaml.UnmarshalStrict(b, params.Interface()); err != nil { + return nil, trimYamlErr(err, true) + } + return widget.newFunc(params.Elem().Interface()) } // ErrorWidget creates new widget with error message. func ErrorWidget(text string) (string, map[string]interface{}) { blocks, _ := json.Marshal([]I3BarBlock{ - I3BarBlock{ + { FullText: text, Color: "#ff0000", }, @@ -65,3 +73,13 @@ func ErrorWidget(text string) (string, map[string]interface{}) { "blocks": string(blocks), } } + +func trimYamlErr(err error, trimLineN bool) error { + msg := strings.TrimPrefix(err.Error(), "yaml: unmarshal errors:\n ") + if trimLineN { + msg = strings.TrimPrefix(msg, "line ") + msg = strings.TrimLeft(msg, "1234567890: ") + } + + return errors.New(msg) +} From d49f63a8d7a87b2cf6a48430d915f7b4cb583822 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 2 Nov 2019 06:15:34 +0300 Subject: [PATCH 13/34] Add retry, infintiy loop to exec widget --- README.md | 3 +- widgets/exec.go | 97 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 0e73361..05d8de1 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,8 @@ The clock widget returns the current time in the specified format. This widget runs the command at the specified interval. - `command` - Command to execute (via `sh -c`). -- `interval` - Update interval in seconds (set 0 to run once at start). +- `interval` - Update interval in seconds (`0` to run once at start; `-1` for loop without delay; default: `0`). +- `retry` - Retry interval in seconds if command failed (default: none). - `events_update` - Update widget if an event occurred (default: `false`). - `output_format` - The command output format (none, text, json, auto) (default: `auto`). - `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. diff --git a/widgets/exec.go b/widgets/exec.go index b6b9dbb..ee5b006 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "strings" + "sync" "syscall" "time" @@ -18,7 +19,8 @@ import ( // ExecWidgetParams are widget parameters. type ExecWidgetParams struct { Command string - Interval uint + Interval int + Retry *int EventsUpdate bool `yaml:"events_update"` Signal *int OutputFormat executor.OutputFormat `yaml:"output_format"` @@ -34,7 +36,9 @@ type ExecWidget struct { c chan<- []ygs.I3BarBlock upd chan struct{} customfields map[string]interface{} - ticker *time.Ticker + tickerC *chan struct{} + + outputWG sync.WaitGroup } func init() { @@ -51,8 +55,11 @@ func NewExecWidget(params interface{}) (ygs.Widget, error) { return nil, errors.New("missing 'command'") } - if w.params.Interval > 0 { - w.ticker = time.NewTicker(time.Duration(w.params.Interval) * time.Second) + if w.params.Retry != nil && + *w.params.Retry > 0 && + w.params.Interval > 0 && + *w.params.Retry >= w.params.Interval { + return nil, errors.New("restart value should be less than interval") } if w.params.Signal != nil { @@ -64,6 +71,9 @@ func NewExecWidget(params interface{}) (ygs.Widget, error) { w.signal = syscall.Signal(signals.SIGRTMIN + sig) } + w.upd = make(chan struct{}, 1) + w.upd <- struct{}{} + return w, nil } @@ -82,11 +92,16 @@ func (w *ExecWidget) exec() error { c := make(chan []ygs.I3BarBlock) + defer close(c) + + w.outputWG.Add(1) go (func() { + defer w.outputWG.Done() + for { blocks, ok := <-c if !ok { - break + return } w.c <- blocks w.setCustomFields(blocks) @@ -94,8 +109,19 @@ func (w *ExecWidget) exec() error { })() err = exc.Run(c, w.params.OutputFormat) + if err == nil { + if state := exc.ProcessState(); state != nil && state.ExitCode() != 0 { + if w.params.Retry != nil { + go (func() { + time.Sleep(time.Second * time.Duration(*w.params.Retry)) + w.upd <- struct{}{} + w.resetTicker() + })() + } - close(c) + return fmt.Errorf("process exited unexpectedly: %s", state.String()) + } + } return err } @@ -103,42 +129,43 @@ func (w *ExecWidget) exec() error { // Run starts the main loop. func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { w.c = c - if w.params.Interval == 0 && w.signal == nil { + if w.params.Interval == 0 && w.signal == nil && w.params.Retry == nil { return w.exec() } - w.upd = make(chan struct{}, 1) - - if w.signal != nil { - sigc := make(chan os.Signal, 1) - signal.Notify(sigc, w.signal) + if w.params.Interval > 0 { + w.resetTicker() + } + if w.params.Interval == -1 { go (func() { for { - <-sigc w.upd <- struct{}{} } })() } - if w.ticker != nil { + if w.signal != nil { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, w.signal) + go (func() { for { - <-w.ticker.C + <-sigc w.upd <- struct{}{} } })() } - for ; true; <-w.upd { + for range w.upd { err := w.exec() if err != nil { - c <- []ygs.I3BarBlock{ - { - FullText: err.Error(), - Color: "#ff0000", - }, - } + w.outputWG.Wait() + + c <- []ygs.I3BarBlock{{ + FullText: err.Error(), + Color: "#ff0000", + }} } } @@ -167,3 +194,29 @@ func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { w.customfields = customfields } + +func (w *ExecWidget) resetTicker() { + if w.tickerC != nil { + *w.tickerC <- struct{}{} + } + + if w.params.Interval > 0 { + tickerC := make(chan struct{}, 1) + w.tickerC = &tickerC + + go (func() { + ticker := time.NewTicker(time.Duration(w.params.Interval) * time.Second) + + defer ticker.Stop() + + for { + select { + case <-tickerC: + return + case <-ticker.C: + w.upd <- struct{}{} + } + } + })() + } +} From 45ccca704812819df4b27dcbfb85485d4389da13 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sat, 2 Nov 2019 06:28:31 +0300 Subject: [PATCH 14/34] Remove codacy, twitter badges --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 05d8de1..abec66a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ Yet Another i3status replacement written in Go. [![GitHub release](https://img.shields.io/github/release/burik666/yagostatus.svg)](https://github.com/burik666/yagostatus) [![GitHub license](https://img.shields.io/github/license/burik666/yagostatus.svg)](https://github.com/burik666/yagostatus/blob/master/LICENSE) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/fb1e1cbd0987425783c6ae30d5c5a833)](https://app.codacy.com/app/burik666/yagostatus?utm_source=github.com&utm_medium=referral&utm_content=burik666/yagostatus&utm_campaign=badger) -[![Twitter](https://img.shields.io/twitter/url/https/github.com/burik666/yagostatus.svg?style=social)](https://twitter.com/intent/tweet?text=Yet%20Another%20i3status%20replacement%20written%20in%20Go.%0A&url=https%3A%2F%2Fgithub.com%2Fburik666%2Fyagostatus&hashtags=i3,i3wm,i3status,golang) [![yagostatus.gif](https://raw.githubusercontent.com/wiki/burik666/yagostatus/yagostatus.gif)](https://github.com/burik666/yagostatus/wiki/Conky) From 49b5b73b22c556502b4abc3d78d5d1a8ccb334ba Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Thu, 7 Nov 2019 22:13:11 +0300 Subject: [PATCH 15/34] Add silent parameter to `exec` widget --- README.md | 1 + widgets/exec.go | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index abec66a..65701f6 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ This widget runs the command at the specified interval. - `command` - Command to execute (via `sh -c`). - `interval` - Update interval in seconds (`0` to run once at start; `-1` for loop without delay; default: `0`). - `retry` - Retry interval in seconds if command failed (default: none). +- `silent` - Don't show error widget if command failed (default: `false`). - `events_update` - Update widget if an event occurred (default: `false`). - `output_format` - The command output format (none, text, json, auto) (default: `auto`). - `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. diff --git a/widgets/exec.go b/widgets/exec.go index ee5b006..6278910 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "os" "os/signal" "strings" @@ -21,6 +22,7 @@ type ExecWidgetParams struct { Command string Interval int Retry *int + Silent bool EventsUpdate bool `yaml:"events_update"` Signal *int OutputFormat executor.OutputFormat `yaml:"output_format"` @@ -130,7 +132,16 @@ func (w *ExecWidget) exec() error { func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { w.c = c if w.params.Interval == 0 && w.signal == nil && w.params.Retry == nil { - return w.exec() + err := w.exec() + if w.params.Silent { + if err != nil { + log.Print(err) + } + + return nil + } + + return err } if w.params.Interval > 0 { @@ -160,12 +171,16 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { for range w.upd { err := w.exec() if err != nil { - w.outputWG.Wait() + if !w.params.Silent { + w.outputWG.Wait() + + c <- []ygs.I3BarBlock{{ + FullText: err.Error(), + Color: "#ff0000", + }} + } - c <- []ygs.I3BarBlock{{ - FullText: err.Error(), - Color: "#ff0000", - }} + log.Print(err) } } From 50c2ea531dea075414ec65a4417aa0387d1d7c9e Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 00:48:50 +0300 Subject: [PATCH 16/34] Fix custom env variables name (I3__) --- widgets/exec.go | 3 +-- yagostatus.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/widgets/exec.go b/widgets/exec.go index 6278910..7adc374 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -7,7 +7,6 @@ import ( "log" "os" "os/signal" - "strings" "sync" "syscall" "time" @@ -88,7 +87,7 @@ func (w *ExecWidget) exec() error { for k, v := range w.customfields { vst, _ := json.Marshal(v) exc.AddEnv( - fmt.Sprintf("I3_%s=%s", strings.ToUpper(k), vst), + fmt.Sprintf("I3_%s=%s", k, vst), ) } diff --git a/yagostatus.go b/yagostatus.go index 422d899..ffd67bb 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -121,7 +121,7 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, for k, v := range status.widgetsOutput[widgetIndex][outputIndex].Custom { vst, _ := json.Marshal(v) exc.AddEnv( - fmt.Sprintf("I3_%s=%s", strings.ToUpper(k), vst), + fmt.Sprintf("I3_%s=%s", k, vst), ) } From d367e0bee6143962931d958b2955cdb80f884797 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Sun, 1 Dec 2019 16:20:47 +0300 Subject: [PATCH 17/34] Fix atomic widget update --- yagostatus.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yagostatus.go b/yagostatus.go index ffd67bb..c3e6f23 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -158,7 +158,7 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, } func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlock) { - status.widgetsOutput[widgetIndex] = make([]ygs.I3BarBlock, len(blocks)) + output := make([]ygs.I3BarBlock, len(blocks)) for blockIndex := range blocks { block := blocks[blockIndex] @@ -169,9 +169,11 @@ func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlo block.Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, block.Name) block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", widgetIndex, blockIndex, block.Instance) - status.widgetsOutput[widgetIndex][blockIndex] = block + output[blockIndex] = block } + status.widgetsOutput[widgetIndex] = output + status.upd <- widgetIndex } From f359f54db1d59dd4c67e544dc4ca18c144608fa4 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 05:56:54 +0300 Subject: [PATCH 18/34] Add unix socket support (http widget) --- README.md | 11 +++++-- go.mod | 2 +- widgets/http.go | 82 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 65701f6..0c6b8fb 100644 --- a/README.md +++ b/README.md @@ -186,17 +186,22 @@ The static widget renders the blocks. Useful for labels and buttons. The http widget starts http server and accept HTTP or Websocket requests. -- `listen` - Address and port for binding (example: `localhost:9900`). +- `network` - `tcp` or `unix` (default `tcp`). +- `listen` - Hostname and port or path to the socket file to bind (example: `localhost:9900`, `/tmp/yagostatus.sock`). - `path` - Path for receiving requests (example: `/mystatus/`). Must be unique for multiple widgets with same `listen`. For example, you can update the widget with the following command: - curl http://localhost:9900/mystatus/ -d '[{"full_text": "hello"}, {"full_text": "world"}]' + curl http://localhost:9900/mystatus/ -d '[{"full_text": "hello"}, {"full_text": "world"}]' Send an empty array to clear: - curl http://localhost:9900/mystatus/ -d '[]' + curl http://localhost:9900/mystatus/ -d '[]' + +Unix socket: + + curl --unix-socket /tmp/yagostatus.sock localhost/mystatus/ -d '[{"full_text": "hello"}]' ## Examples diff --git a/go.mod b/go.mod index 174adc3..721cfef 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/burik666/yagostatus -go 1.12 +go 1.13 require ( go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd diff --git a/widgets/http.go b/widgets/http.go index ec1387b..8bbf638 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -1,12 +1,14 @@ package widgets import ( + "context" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" + "net" "net/http" "github.com/burik666/yagostatus/ygs" @@ -16,8 +18,9 @@ import ( // HTTPWidgetParams are widget parameters. type HTTPWidgetParams struct { - Listen string - Path string + Network string + Listen string + Path string } // HTTPWidget implements the http server widget. @@ -26,20 +29,26 @@ type HTTPWidget struct { params HTTPWidgetParams - httpServer *http.Server - conn *websocket.Conn - c chan<- []ygs.I3BarBlock + conn *websocket.Conn + c chan<- []ygs.I3BarBlock + instance *httpInstance } type httpInstance struct { - mux *http.ServeMux - paths map[string]struct{} + l net.Listener + server *http.Server + mux *http.ServeMux + paths map[string]struct{} } var instances map[string]*httpInstance func init() { - ygs.RegisterWidget("http", NewHTTPWidget, HTTPWidgetParams{}) + ygs.RegisterWidget("http", NewHTTPWidget, HTTPWidgetParams{ + Network: "tcp", + }) + + instances = make(map[string]*httpInstance, 1) } // NewHTTPWidget returns a new HTTPWidget. @@ -56,43 +65,58 @@ func NewHTTPWidget(params interface{}) (ygs.Widget, error) { return nil, errors.New("missing 'path'") } - if instances == nil { - instances = make(map[string]*httpInstance, 1) + if w.params.Network != "tcp" && w.params.Network != "unix" { + return nil, errors.New("invalid 'net' (may be 'tcp' or 'unix')") } - if instance, ok := instances[w.params.Listen]; ok { + instanceKey := w.params.Listen + instance, ok := instances[instanceKey] + if ok { if _, ok := instance.paths[w.params.Path]; ok { return nil, fmt.Errorf("path '%s' already in use", w.params.Path) } - - instance.mux.HandleFunc(w.params.Path, w.httpHandler) - instance.paths[w.params.Path] = struct{}{} } else { - instance := &httpInstance{ - mux: http.NewServeMux(), + mux := http.NewServeMux() + instance = &httpInstance{ + mux: mux, paths: make(map[string]struct{}, 1), + server: &http.Server{ + Addr: w.params.Listen, + Handler: mux, + }, } - instance.mux.HandleFunc(w.params.Path, w.httpHandler) - instance.paths[w.params.Path] = struct{}{} - instances[w.params.Listen] = instance - w.httpServer = &http.Server{ - Addr: w.params.Listen, - Handler: instance.mux, - } + instances[w.params.Listen] = instance + w.instance = instance } + instance.mux.HandleFunc(w.params.Path, w.httpHandler) + instance.paths[instanceKey] = struct{}{} + return w, nil } // Run starts the main loop. func (w *HTTPWidget) Run(c chan<- []ygs.I3BarBlock) error { w.c = c - if w.httpServer == nil { + + if w.instance == nil { return nil } - return w.httpServer.ListenAndServe() + l, err := net.Listen(w.params.Network, w.params.Listen) + if err != nil { + return err + } + + w.instance.l = l + + err = w.instance.server.Serve(l) + if errors.Is(err, http.ErrServerClosed) { + return nil + } + + return err } // Event processes the widget events. @@ -104,6 +128,14 @@ func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) e return nil } +func (w *HTTPWidget) Shutdown() error { + if w.instance == nil { + return nil + } + + return w.instance.server.Shutdown(context.Background()) +} + func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Request) { if request.Method == "GET" { ws := websocket.Handler(w.wsHandler) From a5280ba598fc6c578b81f323b44b9b50072978fb Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 17:02:42 +0300 Subject: [PATCH 19/34] Improve widgets json parser --- widgets/exec.go | 8 +-- yagostatus.go | 3 +- ygs/parser.go | 158 ++++++++++++++++++++++++++++++++++++++++++++++++ ygs/protocol.go | 83 +++++++------------------ ygs/vary.go | 31 ++++++++++ 5 files changed, 214 insertions(+), 69 deletions(-) create mode 100644 ygs/parser.go create mode 100644 ygs/vary.go diff --git a/widgets/exec.go b/widgets/exec.go index 7adc374..ddb13ff 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -1,7 +1,6 @@ package widgets import ( - "encoding/json" "errors" "fmt" "log" @@ -36,7 +35,7 @@ type ExecWidget struct { signal os.Signal c chan<- []ygs.I3BarBlock upd chan struct{} - customfields map[string]interface{} + customfields map[string]ygs.Vary tickerC *chan struct{} outputWG sync.WaitGroup @@ -85,9 +84,8 @@ func (w *ExecWidget) exec() error { } for k, v := range w.customfields { - vst, _ := json.Marshal(v) exc.AddEnv( - fmt.Sprintf("I3_%s=%s", k, vst), + fmt.Sprintf("I3_%s=%s", k, v), ) } @@ -198,7 +196,7 @@ func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) e } func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { - customfields := make(map[string]interface{}) + customfields := make(map[string]ygs.Vary) for _, block := range blocks { for k, v := range block.Custom { diff --git a/yagostatus.go b/yagostatus.go index c3e6f23..4a82f60 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -119,9 +119,8 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, ) for k, v := range status.widgetsOutput[widgetIndex][outputIndex].Custom { - vst, _ := json.Marshal(v) exc.AddEnv( - fmt.Sprintf("I3_%s=%s", k, vst), + fmt.Sprintf("I3_%s=%s", k, v), ) } diff --git a/ygs/parser.go b/ygs/parser.go new file mode 100644 index 0000000..8912212 --- /dev/null +++ b/ygs/parser.go @@ -0,0 +1,158 @@ +package ygs + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" +) + +func (b *I3BarBlock) FromJSON(data []byte, strict bool) error { + type dataWrapped I3BarBlock + + var block dataWrapped + + // copy + tmp, err := json.Marshal(b) + if err != nil { + return err + } + + if err := json.Unmarshal(tmp, &block); err != nil { + return err + } + + if block.Custom == nil { + block.Custom = make(map[string]Vary) + } + + if err := parseBlock(&block, block.Custom, data, strict); err != nil { + return err + } + + *b = I3BarBlock(block) + + return nil +} + +func (b *I3BarBlock) ToVaryMap() map[string]Vary { + tmp, _ := json.Marshal(b) + + varyMap := make(map[string]Vary) + + json.Unmarshal(tmp, &varyMap) + + return varyMap +} + +func parseBlock(block interface{}, custom map[string]Vary, data []byte, strict bool) error { + var jfields map[string]Vary + + if err := json.Unmarshal(data, &jfields); err != nil { + return err + } + + val := reflect.ValueOf(block).Elem() + fieldsByJSONTag := make(map[string]reflect.Value) + + for i := 0; i < val.NumField(); i++ { + typeField := val.Type().Field(i) + if tag, ok := typeField.Tag.Lookup("json"); ok { + tagName := strings.Split(tag, ",")[0] + if tagName == "-" || tagName == "" { + continue + } + + fieldsByJSONTag[tagName] = val.Field(i) + } + } + + for k, v := range jfields { + f, ok := fieldsByJSONTag[k] + if !ok { + if len(k) == 0 { + continue + } + + if strict && k[0] != byte('_') { + return fmt.Errorf("uknown field: %s", k) + } + + custom[k] = v + + continue + } + + var val reflect.Value + + if f.Type().Kind() == reflect.Ptr { + val = reflect.New(f.Type().Elem()) + } else { + val = reflect.New(f.Type()).Elem() + } + + sv := string(v) + + switch reflect.Indirect(val).Kind() { + case reflect.String: + s, err := strconv.Unquote(sv) + if strict && err != nil { + return fmt.Errorf("invalid value for %s (string): %s", k, sv) + } else { + if err != nil { + s = sv + } + } + + reflect.Indirect(val).SetString(s) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := sv + if !strict { + s = strings.Trim(sv, "\"") + } + + if n, err := strconv.ParseInt(s, 10, 64); err == nil { + reflect.Indirect(val).SetInt(n) + } else { + return fmt.Errorf("invalid value for %s (int): %s", k, sv) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s := sv + if !strict { + s = strings.Trim(sv, "\"") + } + + if n, err := strconv.ParseUint(s, 10, 64); err == nil { + reflect.Indirect(val).SetUint(n) + } else { + return fmt.Errorf("invalid value for %s (uint): %s", k, sv) + } + + case reflect.Bool: + if strict { + switch sv { + case "true": + reflect.Indirect(val).SetBool(true) + case "false": + reflect.Indirect(val).SetBool(false) + default: + return fmt.Errorf("invalid value for %s: %s", k, sv) + } + } else { + s := strings.Trim(strings.ToLower(sv), "\"") + if s == "false" || s == "0" || s == "f" { + reflect.Indirect(val).SetBool(false) + } else { + reflect.Indirect(val).SetBool(true) + } + } + default: + panic("unsuported type") + } + + f.Set(val) + } + + return nil +} diff --git a/ygs/protocol.go b/ygs/protocol.go index 25c8616..0b20ae0 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -15,24 +15,24 @@ type I3BarHeader struct { // I3BarBlock represents a block of i3bar message. type I3BarBlock struct { - FullText string `json:"full_text"` - ShortText string `json:"short_text,omitempty"` - Color string `json:"color,omitempty"` - BorderColor string `json:"border,omitempty"` - BorderTop *uint16 `json:"border_top,omitempty"` - BorderBottom *uint16 `json:"border_bottom,omitempty"` - BorderLeft *uint16 `json:"border_left,omitempty"` - BorderRight *uint16 `json:"border_right,omitempty"` - BackgroundColor string `json:"background,omitempty"` - Markup string `json:"markup,omitempty"` - MinWidth string `json:"min_width,omitempty"` - Align string `json:"align,omitempty"` - Name string `json:"name,omitempty"` - Instance string `json:"instance,omitempty"` - Urgent bool `json:"urgent,omitempty"` - Separator *bool `json:"separator,omitempty"` - SeparatorBlockWidth uint16 `json:"separator_block_width,omitempty"` - Custom map[string]interface{} `json:"-"` + FullText string `json:"full_text"` + ShortText string `json:"short_text,omitempty"` + Color string `json:"color,omitempty"` + BorderColor string `json:"border,omitempty"` + BorderTop *uint16 `json:"border_top,omitempty"` + BorderBottom *uint16 `json:"border_bottom,omitempty"` + BorderLeft *uint16 `json:"border_left,omitempty"` + BorderRight *uint16 `json:"border_right,omitempty"` + BackgroundColor string `json:"background,omitempty"` + Markup string `json:"markup,omitempty"` + MinWidth string `json:"min_width,omitempty"` + Align string `json:"align,omitempty"` + Name string `json:"name,omitempty"` + Instance string `json:"instance,omitempty"` + Urgent bool `json:"urgent,omitempty"` + Separator *bool `json:"separator,omitempty"` + SeparatorBlockWidth uint16 `json:"separator_block_width,omitempty"` + Custom map[string]Vary `json:"-"` } // I3BarClickEvent represents a user click event message. @@ -49,56 +49,15 @@ type I3BarClickEvent struct { Modifiers []string `json:"modifiers"` } -type dataWrapped I3BarBlock - // UnmarshalJSON unmarshals json with custom keys (with _ prefix). func (b *I3BarBlock) UnmarshalJSON(data []byte) error { - var resmap map[string]interface{} - - if err := json.Unmarshal(data, &resmap); err != nil { - return err - } - - wr := dataWrapped(*b) - - for k, v := range resmap { - if len(k) == 0 { - delete(resmap, k) - continue - } - - if k[0] == '_' { - if wr.Custom == nil { - wr.Custom = make(map[string]interface{}) - } - - wr.Custom[k] = v - - delete(resmap, k) - } - } - - buf := &bytes.Buffer{} - - enc := json.NewEncoder(buf) - if err := enc.Encode(resmap); err != nil { - return err - } - - decoder := json.NewDecoder(buf) - decoder.DisallowUnknownFields() - - if err := decoder.Decode(&wr); err != nil { - return err - } - - *b = I3BarBlock(wr) - - return nil + return b.FromJSON(data, true) } // MarshalJSON marshals json with custom keys (with _ prefix). func (b I3BarBlock) MarshalJSON() ([]byte, error) { + type dataWrapped I3BarBlock + wd := dataWrapped(b) if len(wd.Custom) == 0 { diff --git a/ygs/vary.go b/ygs/vary.go new file mode 100644 index 0000000..4c6a4d2 --- /dev/null +++ b/ygs/vary.go @@ -0,0 +1,31 @@ +package ygs + +import ( + "errors" + "strconv" +) + +type Vary []byte + +func (v Vary) MarshalJSON() ([]byte, error) { + if v == nil { + return []byte("null"), nil + } + return v, nil +} + +func (v *Vary) UnmarshalJSON(data []byte) error { + if v == nil { + return errors.New("ygs.Vary: UnmarshalJSON on nil pointer") + } + *v = append((*v)[0:0], data...) + return nil +} + +func (v Vary) String() string { + s, err := strconv.Unquote(string(v)) + if err != nil { + return string(v) + } + return s +} From 4ccf3c748e3e585c8d8684fd2287f4ae27000296 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 16:58:26 +0300 Subject: [PATCH 20/34] Add include support in config --- internal/pkg/config/config.go | 157 ++-------------------------- internal/pkg/config/parser.go | 190 ++++++++++++++++++++++++++++++++++ yagostatus.go | 10 +- ygs/utils.go | 29 ++++-- ygs/widget_config.go | 33 ++++++ ygs/widget_event_config.go | 43 ++++++++ 6 files changed, 301 insertions(+), 161 deletions(-) create mode 100644 internal/pkg/config/parser.go create mode 100644 ygs/widget_config.go create mode 100644 ygs/widget_event_config.go diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index f701ae4..93a80c1 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -1,18 +1,12 @@ package config import ( - "encoding/json" - "errors" - "fmt" "io/ioutil" - "log" - "strings" + "os" + "path/filepath" "syscall" - "github.com/burik666/yagostatus/internal/pkg/executor" "github.com/burik666/yagostatus/ygs" - - "gopkg.in/yaml.v2" ) // Config represents the main configuration. @@ -21,69 +15,7 @@ type Config struct { StopSignal syscall.Signal `yaml:"stop"` ContSignal syscall.Signal `yaml:"cont"` } `yaml:"signals"` - Widgets []WidgetConfig `yaml:"widgets"` -} - -// WidgetConfig represents a widget configuration. -type WidgetConfig struct { - Name string `yaml:"widget"` - Workspaces []string `yaml:"workspaces"` - Template ygs.I3BarBlock `yaml:"-"` - Events []WidgetEventConfig `yaml:"events"` - - Params map[string]interface{} -} - -// Validate checks widget configuration. -func (c WidgetConfig) Validate() error { - if c.Name == "" { - return errors.New("missing widget name") - } - - for ei := range c.Events { - if err := c.Events[ei].Validate(); err != nil { - return err - } - } - - return nil -} - -// WidgetEventConfig represents a widget events. -type WidgetEventConfig struct { - Command string `yaml:"command"` - Button uint8 `yaml:"button"` - Modifiers []string `yaml:"modifiers,omitempty"` - Name string `yaml:"name,omitempty"` - Instance string `yaml:"instance,omitempty"` - OutputFormat executor.OutputFormat `yaml:"output_format,omitempty"` -} - -// Validate checks event parameters. -func (e *WidgetEventConfig) Validate() error { - var availableWidgetEventModifiers = [...]string{"Shift", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"} - - for _, mod := range e.Modifiers { - found := false - mod = strings.TrimLeft(mod, "!") - - for _, m := range availableWidgetEventModifiers { - if mod == m { - found = true - break - } - } - - if !found { - return fmt.Errorf("unknown '%s' modifier", mod) - } - } - - if e.OutputFormat == "" { - e.OutputFormat = executor.OutputFormatNone - } - - return nil + Widgets []ygs.WidgetConfig `yaml:"widgets"` } // LoadFile loads and parses config from file. @@ -93,87 +25,14 @@ func LoadFile(filename string) (*Config, error) { return nil, err } - return Parse(data) + return parse(data, filepath.Dir(filename)) } // Parse parses config. func Parse(data []byte) (*Config, error) { - var raw struct { - Widgets []map[string]interface{} `yaml:"widgets"` - } - - config := Config{} - config.Signals.StopSignal = syscall.SIGUSR1 - config.Signals.ContSignal = syscall.SIGCONT - - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, trimYamlErr(err, false) - } - - if err := yaml.Unmarshal(data, &raw); err != nil { - return nil, trimYamlErr(err, false) - } - - for widgetIndex := range config.Widgets { - widget := &config.Widgets[widgetIndex] - params := raw.Widgets[widgetIndex] - - tpl, ok := params["template"] - if ok { - if err := json.Unmarshal([]byte(tpl.(string)), &widget.Template); err != nil { - name, params := ygs.ErrorWidget(err.Error()) - *widget = WidgetConfig{ - Name: name, - Params: params, - } - - log.Printf("template error: %s", err) - - continue - } - } - - tmp, err := yaml.Marshal(params["events"]) - if err != nil { - return nil, err - } - - if err := yaml.UnmarshalStrict(tmp, &widget.Events); err != nil { - name, params := ygs.ErrorWidget(trimYamlErr(err, true).Error()) - *widget = WidgetConfig{ - Name: name, - Params: params, - } - - continue - } - - widget.Params = params - if err := widget.Validate(); err != nil { - name, params := ygs.ErrorWidget(trimYamlErr(err, true).Error()) - *widget = WidgetConfig{ - Name: name, - Params: params, - } - - continue - } - - delete(params, "widget") - delete(params, "workspaces") - delete(params, "template") - delete(params, "events") - } - - return &config, nil -} - -func trimYamlErr(err error, trimLineN bool) error { - msg := strings.TrimPrefix(err.Error(), "yaml: unmarshal errors:\n ") - if trimLineN { - msg = strings.TrimPrefix(msg, "line ") - msg = strings.TrimLeft(msg, "1234567890: ") + wd, err := os.Getwd() + if err != nil { + return nil, err } - - return errors.New(msg) + return parse(data, wd) } diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go new file mode 100644 index 0000000..6dde906 --- /dev/null +++ b/internal/pkg/config/parser.go @@ -0,0 +1,190 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "path/filepath" + "reflect" + "strings" + "syscall" + + "github.com/burik666/yagostatus/ygs" + + "gopkg.in/yaml.v2" +) + +func parse(data []byte, workdir string) (*Config, error) { + + config := Config{} + config.Signals.StopSignal = syscall.SIGUSR1 + config.Signals.ContSignal = syscall.SIGCONT + + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, trimYamlErr(err, false) + } + +WIDGET: + for widgetIndex := 0; widgetIndex < len(config.Widgets); widgetIndex++ { + widget := &config.Widgets[widgetIndex] + + params := make(map[string]interface{}) + for k, v := range config.Widgets[widgetIndex].Params { + params[strings.ToLower(k)] = v + } + + config.Widgets[widgetIndex].Params = params + + if widget.WorkDir == "" { + widget.WorkDir = workdir + } + + if len(widget.Name) > 0 && widget.Name[0] == '$' { + for i := range widget.IncludeStack { + if widget.Name == widget.IncludeStack[i] { + stack := append(widget.IncludeStack, widget.Name) + + setError(widget, fmt.Errorf("recursive include: '%s'", strings.Join(stack, " -> ")), false) + + continue WIDGET + } + } + + wd := workdir + + if widget.WorkDir != "" { + wd = widget.WorkDir + } + + filename := wd + "/" + widget.Name[1:] + data, err := ioutil.ReadFile(filename) + if err != nil { + setError(widget, err, false) + + continue WIDGET + } + + dict := make(map[string]string, len(params)) + for k, v := range params { + vb, err := json.Marshal(v) + if err != nil { + setError(widget, err, false) + + continue WIDGET + } + + var vraw ygs.Vary + + err = json.Unmarshal(vb, &vraw) + if err != nil { + setError(widget, err, true) + + continue WIDGET + } + + dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n") + } + + var snipWidgetsConfig []ygs.WidgetConfig + if err := yaml.Unmarshal(data, &snipWidgetsConfig); err != nil { + setError(widget, err, false) + + continue WIDGET + } + + v := reflect.ValueOf(snipWidgetsConfig) + replaceRecursive(&v, dict) + + wd = filepath.Dir(filename) + for i := range snipWidgetsConfig { + snipWidgetsConfig[i].WorkDir = wd + snipWidgetsConfig[i].IncludeStack = append(widget.IncludeStack, widget.Name) + } + + i := widgetIndex + config.Widgets = append(config.Widgets[:i], config.Widgets[i+1:]...) + config.Widgets = append(config.Widgets[:i], append(snipWidgetsConfig, config.Widgets[i:]...)...) + + widgetIndex-- + + continue WIDGET + } + + if tpl, ok := params["template"]; ok { + if err := json.Unmarshal([]byte(tpl.(string)), &widget.Template); err != nil { + setError(widget, err, false) + + log.Printf("template error: %s", err) + + continue WIDGET + } + + delete(params, "template") + } + + if err := widget.Validate(); err != nil { + setError(widget, err, true) + + continue WIDGET + } + } + + return &config, nil +} + +func setError(widget *ygs.WidgetConfig, err error, trimLineN bool) { + *widget = ygs.ErrorWidget(trimYamlErr(err, trimLineN).Error()) +} + +func trimYamlErr(err error, trimLineN bool) error { + msg := strings.TrimPrefix(err.Error(), "yaml: ") + msg = strings.TrimPrefix(msg, "unmarshal errors:\n ") + if trimLineN { + msg = strings.TrimPrefix(msg, "line ") + msg = strings.TrimLeft(msg, "1234567890: ") + } + + return errors.New(msg) +} + +func replaceRecursive(v *reflect.Value, dict map[string]string) { + vv := *v + for vv.Kind() == reflect.Ptr || vv.Kind() == reflect.Interface { + vv = vv.Elem() + } + + switch vv.Kind() { + case reflect.Slice, reflect.Array: + for i := 0; i < vv.Len(); i++ { + vi := vv.Index(i) + replaceRecursive(&vi, dict) + } + case reflect.Map: + for _, i := range vv.MapKeys() { + vm := vv.MapIndex(i) + replaceRecursive(&vm, dict) + vv.SetMapIndex(i, vm) + } + case reflect.Struct: + t := vv.Type() + for i := 0; i < t.NumField(); i++ { + vf := v.Field(i) + replaceRecursive(&vf, dict) + } + case reflect.String: + st := vv.String() + for s, r := range dict { + st = strings.ReplaceAll(st, s, r) + } + + if vv.CanSet() { + vv.SetString(st) + } else { + vn := reflect.New(vv.Type()).Elem() + vn.SetString(st) + *v = vn + } + } +} diff --git a/yagostatus.go b/yagostatus.go index 4a82f60..9177dea 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -23,7 +23,7 @@ import ( type YaGoStatus struct { widgets []ygs.Widget widgetsOutput [][]ygs.I3BarBlock - widgetsConfig []config.WidgetConfig + widgetsConfig []ygs.WidgetConfig widgetChans []chan []ygs.I3BarBlock upd chan int @@ -48,7 +48,7 @@ func NewYaGoStatus(cfg config.Config) (*YaGoStatus, error) { } })() - widget, err := ygs.NewWidget(w.Name, w.Params) + widget, err := ygs.NewWidget(w) if err != nil { log.Printf("Failed to create widget: %s", err) status.errorWidget(err.Error()) @@ -69,11 +69,11 @@ func (status *YaGoStatus) errorWidget(text string) { panic(err) } - status.AddWidget(errWidget, config.WidgetConfig{}) + status.AddWidget(errWidget, ygs.WidgetConfig{}) } // AddWidget adds widget to statusbar. -func (status *YaGoStatus) AddWidget(widget ygs.Widget, config config.WidgetConfig) { +func (status *YaGoStatus) AddWidget(widget ygs.Widget, config ygs.WidgetConfig) { status.widgets = append(status.widgets, widget) status.widgetsOutput = append(status.widgetsOutput, nil) status.widgetsConfig = append(status.widgetsConfig, config) @@ -146,7 +146,7 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, return err } - err = exc.Run(status.widgetChans[widgetIndex], widgetEvent.OutputFormat) + err = exc.Run(status.widgetChans[widgetIndex], executor.OutputFormat(widgetEvent.OutputFormat)) if err != nil { return err } diff --git a/ygs/utils.go b/ygs/utils.go index 2c3cce2..5d0806f 100644 --- a/ygs/utils.go +++ b/ygs/utils.go @@ -37,7 +37,8 @@ func RegisterWidget(name string, newFunc newWidgetFunc, defaultParams interface{ } // NewWidget creates new widget by name. -func NewWidget(name string, rawParams map[string]interface{}) (Widget, error) { +func NewWidget(widgetConfig WidgetConfig) (Widget, error) { + name := widgetConfig.Name widget, ok := registeredWidgets[name] if !ok { return nil, fmt.Errorf("widget '%s' not found", name) @@ -46,9 +47,10 @@ func NewWidget(name string, rawParams map[string]interface{}) (Widget, error) { def := reflect.ValueOf(widget.defaultParams) params := reflect.New(def.Type()) - params.Elem().Set(def) + pe := params.Elem() + pe.Set(def) - b, err := yaml.Marshal(rawParams) + b, err := yaml.Marshal(widgetConfig.Params) if err != nil { return nil, err } @@ -57,11 +59,20 @@ func NewWidget(name string, rawParams map[string]interface{}) (Widget, error) { return nil, trimYamlErr(err, true) } - return widget.newFunc(params.Elem().Interface()) + if _, ok := widgetConfig.Params["workdir"]; !ok { + for i := 0; i < pe.NumField(); i++ { + fn := pe.Type().Field(i).Name + if strings.ToLower(fn) == "workdir" { + pe.Field(i).SetString(widgetConfig.WorkDir) + } + } + } + + return widget.newFunc(pe.Interface()) } // ErrorWidget creates new widget with error message. -func ErrorWidget(text string) (string, map[string]interface{}) { +func ErrorWidget(text string) WidgetConfig { blocks, _ := json.Marshal([]I3BarBlock{ { FullText: text, @@ -69,9 +80,13 @@ func ErrorWidget(text string) (string, map[string]interface{}) { }, }) - return "static", map[string]interface{}{ - "blocks": string(blocks), + return WidgetConfig{ + Name: "static", + Params: map[string]interface{}{ + "blocks": string(blocks), + }, } + } func trimYamlErr(err error, trimLineN bool) error { diff --git a/ygs/widget_config.go b/ygs/widget_config.go new file mode 100644 index 0000000..759cfa6 --- /dev/null +++ b/ygs/widget_config.go @@ -0,0 +1,33 @@ +package ygs + +import ( + "errors" +) + +// WidgetConfig represents a widget configuration. +type WidgetConfig struct { + Name string `yaml:"widget"` + Workspaces []string `yaml:"workspaces"` + Template I3BarBlock `yaml:"-"` + Events []WidgetEventConfig `yaml:"events"` + WorkDir string `yaml:"-"` + + Params map[string]interface{} `yaml:",inline"` + + IncludeStack []string `yaml:"-"` +} + +// Validate checks widget configuration. +func (c WidgetConfig) Validate() error { + if c.Name == "" { + return errors.New("missing widget name") + } + + for ei := range c.Events { + if err := c.Events[ei].Validate(); err != nil { + return err + } + } + + return nil +} diff --git a/ygs/widget_event_config.go b/ygs/widget_event_config.go new file mode 100644 index 0000000..a6c2903 --- /dev/null +++ b/ygs/widget_event_config.go @@ -0,0 +1,43 @@ +package ygs + +import ( + "fmt" + "strings" +) + +// WidgetEventConfig represents a widget events. +type WidgetEventConfig struct { + Command string `yaml:"command"` + Button uint8 `yaml:"button"` + Modifiers []string `yaml:"modifiers,omitempty"` + Name string `yaml:"name,omitempty"` + Instance string `yaml:"instance,omitempty"` + OutputFormat string `yaml:"output_format,omitempty"` +} + +// Validate checks event parameters. +func (e *WidgetEventConfig) Validate() error { + var availableWidgetEventModifiers = [...]string{"Shift", "Control", "Mod1", "Mod2", "Mod3", "Mod4", "Mod5"} + + for _, mod := range e.Modifiers { + found := false + mod = strings.TrimLeft(mod, "!") + + for _, m := range availableWidgetEventModifiers { + if mod == m { + found = true + break + } + } + + if !found { + return fmt.Errorf("unknown '%s' modifier", mod) + } + } + + if e.OutputFormat == "" { + e.OutputFormat = "none" + } + + return nil +} From 82da64a7d7265ec0a02d47a83b68e04f46981d4f Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 19:15:19 +0300 Subject: [PATCH 21/34] Add support for multiple websocket clients --- widgets/http.go | 76 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/widgets/http.go b/widgets/http.go index 8bbf638..0bdd7b4 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -10,6 +10,7 @@ import ( "log" "net" "net/http" + "sync" "github.com/burik666/yagostatus/ygs" @@ -29,9 +30,11 @@ type HTTPWidget struct { params HTTPWidgetParams - conn *websocket.Conn c chan<- []ygs.I3BarBlock instance *httpInstance + + clients map[*websocket.Conn]chan interface{} + cm sync.RWMutex } type httpInstance struct { @@ -93,6 +96,7 @@ func NewHTTPWidget(params interface{}) (ygs.Widget, error) { instance.mux.HandleFunc(w.params.Path, w.httpHandler) instance.paths[instanceKey] = struct{}{} + w.clients = make(map[*websocket.Conn]chan interface{}) return w, nil } @@ -121,11 +125,7 @@ func (w *HTTPWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { - if w.conn != nil { - return websocket.JSON.Send(w.conn, event) - } - - return nil + return w.broadcast(event) } func (w *HTTPWidget) Shutdown() error { @@ -138,8 +138,14 @@ func (w *HTTPWidget) Shutdown() error { func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Request) { if request.Method == "GET" { - ws := websocket.Handler(w.wsHandler) - ws.ServeHTTP(response, request) + serv := websocket.Server{ + Handshake: func(cfg *websocket.Config, r *http.Request) error { + return nil + }, + Handler: w.wsHandler, + } + + serv.ServeHTTP(response, request) return } @@ -171,30 +177,56 @@ func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Req } func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { - var messages []ygs.I3BarBlock + defer ws.Close() - w.conn = ws + ch := make(chan interface{}) - for { - if err := websocket.JSON.Receive(ws, &messages); err != nil { - if err == io.EOF { - if w.conn == ws { - w.c <- nil - w.conn = nil - } + w.cm.RLock() + w.clients[ws] = ch + w.cm.RUnlock() - break + var blocks []ygs.I3BarBlock + + go func() { + for { + msg, ok := <-ch + if !ok { + return } - log.Printf("%s", err) + if err := websocket.JSON.Send(ws, msg); err != nil { + log.Printf("failed to send msg: %s", err) + } } + }() - if w.conn != ws { + for { + if err := websocket.JSON.Receive(ws, &blocks); err != nil { + if err == io.EOF { + break + } + + log.Printf("invalid message: %s", err) break } - w.c <- messages + w.c <- blocks } - ws.Close() + w.cm.Lock() + delete(w.clients, ws) + w.cm.Unlock() + + close(ch) +} + +func (w *HTTPWidget) broadcast(msg interface{}) error { + w.cm.RLock() + defer w.cm.RUnlock() + + for _, ch := range w.clients { + ch <- msg + } + + return nil } From 58f5fcd16da11ba31d083c1adb6f77d746621920 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 19:59:42 +0300 Subject: [PATCH 22/34] Add omit empty full_text --- ygs/protocol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ygs/protocol.go b/ygs/protocol.go index 0b20ae0..8ea3654 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -15,7 +15,7 @@ type I3BarHeader struct { // I3BarBlock represents a block of i3bar message. type I3BarBlock struct { - FullText string `json:"full_text"` + FullText string `json:"full_text,omitempty"` ShortText string `json:"short_text,omitempty"` Color string `json:"color,omitempty"` BorderColor string `json:"border,omitempty"` From 5995c4e4c378d0c451993fdc8afb13b0a057c302 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 21:53:25 +0300 Subject: [PATCH 23/34] Add multiblock templates add template support for nested widgets. --- README.md | 33 +++++++++++++---------- internal/pkg/config/parser.go | 50 ++++++++++++++++++++++++++--------- main.go | 6 ++--- yagostatus.go | 21 ++++++--------- yagostatus.yml | 6 ++--- ygs/protocol.go | 7 +++++ ygs/widget_config.go | 2 +- 7 files changed, 78 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 0c6b8fb..0f7c44a 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,12 @@ widgets: - widget: clock format: Jan _2 Mon 15:04:05 # https://golang.org/pkg/time/#Time.Format - template: > - { + templates: > + [{ "color": "#ffffff", "separator": true, "separator_block_width": 20 - } + }] ``` ## Widgets @@ -93,7 +93,7 @@ Example: ] ``` -- `template` - The template that is applied to the output of the widget. +- `templates` - The templates that apply to widget blocks. - `events` - List of commands to be executed on user actions. * `button` - X11 button ID (0 for any, 1 to 3 for left/middle/right mouse button. 4/5 for mouse wheel up/down. Default: `0`). * `modifiers` - List of X11 modifiers condition. @@ -118,10 +118,15 @@ Example: "name": "ch" } ] - template: > - { - "color": "#0000ff" - } + templates: > + [ + { + "color": "#ff8000" + }, + { + "color": "#ff3030" + } + ] events: - button: 1 command: /usr/bin/firefox @@ -268,12 +273,12 @@ bindsym XF86AudioMute exec amixer -q set Master toggle; exec pkill -SIGRTMIN+1 y - button: 5 command: amixer -q set Master 3%- - template: > - { + templates: > + [{ "markup": "pango", "separator": true, "separator_block_width": 20 - } + }] ``` ### Weather @@ -296,11 +301,11 @@ Requires [jq](https://stedolan.github.com/jq/) for json parsing. - widget: exec command: curl -s 'http://api.openweathermap.org/data/2.5/weather?q=London,uk&units=metric&appid='|jq .main.temp interval: 300 - template: > - { + templates: > + [{ "separator": true, "separator_block_width": 20 - } + }] ``` ### Conky diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index 6dde906..1189aa5 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io/ioutil" - "log" "path/filepath" "reflect" "strings" @@ -41,6 +40,42 @@ WIDGET: widget.WorkDir = workdir } + // for backward compatibility + if itpl, ok := params["template"]; ok { + tpl, ok := itpl.(string) + if !ok { + setError(widget, fmt.Errorf("invalid template"), false) + continue WIDGET + } + + widget.Templates = append(widget.Templates, ygs.I3BarBlock{}) + if err := json.Unmarshal([]byte(tpl), &widget.Templates[0]); err != nil { + setError(widget, err, false) + + continue WIDGET + } + + delete(params, "template") + } + + if itpls, ok := params["templates"]; ok { + tpls, ok := itpls.(string) + if !ok { + setError(widget, fmt.Errorf("invalid templates"), false) + continue WIDGET + } + + if err := json.Unmarshal([]byte(tpls), &widget.Templates); err != nil { + setError(widget, err, false) + + continue WIDGET + } + + delete(params, "templates") + } + + tpls, _ := json.Marshal(widget.Templates) + if len(widget.Name) > 0 && widget.Name[0] == '$' { for i := range widget.IncludeStack { if widget.Name == widget.IncludeStack[i] { @@ -101,6 +136,7 @@ WIDGET: for i := range snipWidgetsConfig { snipWidgetsConfig[i].WorkDir = wd snipWidgetsConfig[i].IncludeStack = append(widget.IncludeStack, widget.Name) + json.Unmarshal(tpls, &snipWidgetsConfig[i].Templates) } i := widgetIndex @@ -112,18 +148,6 @@ WIDGET: continue WIDGET } - if tpl, ok := params["template"]; ok { - if err := json.Unmarshal([]byte(tpl.(string)), &widget.Template); err != nil { - setError(widget, err, false) - - log.Printf("template error: %s", err) - - continue WIDGET - } - - delete(params, "template") - } - if err := widget.Validate(); err != nil { setError(widget, err, true) diff --git a/main.go b/main.go index 6f7f335..8b09b7b 100644 --- a/main.go +++ b/main.go @@ -29,12 +29,12 @@ widgets: command: /usr/bin/i3status - widget: clock format: Jan _2 Mon 15:04:05 # https://golang.org/pkg/time/#Time.Format - template: > - { + templates: > + [{ "color": "#ffffff", "separator": true, "separator_block_width": 20 - } + }] `) func main() { diff --git a/yagostatus.go b/yagostatus.go index 9177dea..6bbc34e 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -159,10 +159,16 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlock) { output := make([]ygs.I3BarBlock, len(blocks)) + tplc := len(status.widgetsConfig[widgetIndex].Templates) for blockIndex := range blocks { block := blocks[blockIndex] - if err := mergeBlocks(&block, status.widgetsConfig[widgetIndex].Template); err != nil { - log.Printf("Failed to merge blocks: %s", err) + + if tplc == 1 { + block.Apply(status.widgetsConfig[widgetIndex].Templates[0]) + } else { + if blockIndex < tplc { + block.Apply(status.widgetsConfig[widgetIndex].Templates[blockIndex]) + } } block.Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, block.Name) @@ -390,17 +396,6 @@ func (status *YaGoStatus) updateWorkspaces() { status.visibleWorkspaces = vw } -func mergeBlocks(b *ygs.I3BarBlock, tpl ygs.I3BarBlock) error { - jb, err := json.Marshal(*b) - if err != nil { - return err - } - - *b = tpl - - return json.Unmarshal(jb, b) -} - func checkModifiers(conditions []string, values []string) bool { for _, c := range conditions { isNegative := c[0] == '!' diff --git a/yagostatus.yml b/yagostatus.yml index d643c31..5dee489 100644 --- a/yagostatus.yml +++ b/yagostatus.yml @@ -16,9 +16,9 @@ widgets: - widget: clock format: Jan _2 Mon 15:04:05 # https://golang.org/pkg/time/#Time.Format - template: > - { + templates: > + [{ "color": "#ffffff", "separator": true, "separator_block_width": 20 - } + }] diff --git a/ygs/protocol.go b/ygs/protocol.go index 8ea3654..c6b7bca 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -90,3 +90,10 @@ func (b I3BarBlock) MarshalJSON() ([]byte, error) { return buf.Bytes(), err } + +func (b *I3BarBlock) Apply(tpl I3BarBlock) { + jb, _ := json.Marshal(b) + *b = tpl + + json.Unmarshal(jb, b) +} diff --git a/ygs/widget_config.go b/ygs/widget_config.go index 759cfa6..c97ae88 100644 --- a/ygs/widget_config.go +++ b/ygs/widget_config.go @@ -8,7 +8,7 @@ import ( type WidgetConfig struct { Name string `yaml:"widget"` Workspaces []string `yaml:"workspaces"` - Template I3BarBlock `yaml:"-"` + Templates []I3BarBlock `yaml:"-"` Events []WidgetEventConfig `yaml:"events"` WorkDir string `yaml:"-"` From 3d1cd06e71842919fed77b876674b734c8548b73 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Mon, 2 Dec 2019 22:25:38 +0300 Subject: [PATCH 24/34] Add override events --- README.md | 1 + internal/pkg/config/parser.go | 28 ++++++++++++++++++++++++++++ ygs/widget_event_config.go | 1 + 3 files changed, 30 insertions(+) diff --git a/README.md b/README.md index 0f7c44a..209021d 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Example: * `output_format` - The command output format (none, text, json, auto) (default: `none`). * `name` - Filter by `name` for widgets with multiple blocks (default: empty). * `instance` - Filter by `instance` for widgets with multiple blocks (default: empty). + * `override` - If `true`, previously defined events with the same `button`, `modifier`, `name` and `instance` will be ignored (default: `false`) Example: ```yml diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index 1189aa5..9b2695e 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "path/filepath" "reflect" + "sort" "strings" "syscall" @@ -137,6 +138,33 @@ WIDGET: snipWidgetsConfig[i].WorkDir = wd snipWidgetsConfig[i].IncludeStack = append(widget.IncludeStack, widget.Name) json.Unmarshal(tpls, &snipWidgetsConfig[i].Templates) + + snipEvents := snipWidgetsConfig[i].Events + for _, e := range widget.Events { + if e.Override { + sort.Strings(e.Modifiers) + + ne := make([]ygs.WidgetEventConfig, 0, len(snipEvents)) + + for _, se := range snipEvents { + sort.Strings(se.Modifiers) + + if e.Button == se.Button && + e.Name == se.Name && + e.Instance == se.Instance && + reflect.DeepEqual(e.Modifiers, se.Modifiers) { + + continue + } + + ne = append(ne, se) + } + snipEvents = append(ne, e) + } else { + snipEvents = append(snipEvents, e) + } + } + snipWidgetsConfig[i].Events = snipEvents } i := widgetIndex diff --git a/ygs/widget_event_config.go b/ygs/widget_event_config.go index a6c2903..6ce8cfc 100644 --- a/ygs/widget_event_config.go +++ b/ygs/widget_event_config.go @@ -13,6 +13,7 @@ type WidgetEventConfig struct { Name string `yaml:"name,omitempty"` Instance string `yaml:"instance,omitempty"` OutputFormat string `yaml:"output_format,omitempty"` + Override bool `yaml:"override"` } // Validate checks event parameters. From 3dac37166aef73e487c139982238a9b1fd904ec0 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Tue, 3 Dec 2019 04:07:22 +0300 Subject: [PATCH 25/34] Add workdir to events, exec, wrapper --- README.md | 3 +++ internal/pkg/config/parser.go | 13 +++++++++++++ internal/pkg/executor/executor.go | 6 ++++++ widgets/exec.go | 3 +++ widgets/wrapper.go | 3 +++ yagostatus.go | 2 ++ ygs/widget_event_config.go | 1 + 7 files changed, 31 insertions(+) diff --git a/README.md b/README.md index 209021d..c868904 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Example: * `command` - Command to execute (via `sh -c`). Сlick_event json will be written to stdin. Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_X`, `$I3_Y`, `$I3_RELATIVE_X`, `$I3_RELATIVE_Y`, `$I3_WIDTH`, `$I3_HEIGHT`, `$I3_MODIFIERS`. `$I3__` prefix for custom fields. + * `workdir` - Set a working directory. * `output_format` - The command output format (none, text, json, auto) (default: `none`). * `name` - Filter by `name` for widgets with multiple blocks (default: empty). * `instance` - Filter by `instance` for widgets with multiple blocks (default: empty). @@ -159,6 +160,7 @@ The clock widget returns the current time in the specified format. This widget runs the command at the specified interval. - `command` - Command to execute (via `sh -c`). +- `workdir` - Set a working directory. - `interval` - Update interval in seconds (`0` to run once at start; `-1` for loop without delay; default: `0`). - `retry` - Retry interval in seconds if command failed (default: none). - `silent` - Don't show error widget if command failed (default: `false`). @@ -179,6 +181,7 @@ The wrapper widget starts the command and proxy received blocks (and click_event See: https://i3wm.org/docs/i3bar-protocol.html - `command` - Command to execute. +- `workdir` - Set a working directory. ### Widget `static` diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index 9b2695e..a26ce43 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -41,6 +41,12 @@ WIDGET: widget.WorkDir = workdir } + for i := range widget.Events { + if widget.Events[i].WorkDir == "" { + widget.Events[i].WorkDir = workdir + } + } + // for backward compatibility if itpl, ok := params["template"]; ok { tpl, ok := itpl.(string) @@ -140,6 +146,12 @@ WIDGET: json.Unmarshal(tpls, &snipWidgetsConfig[i].Templates) snipEvents := snipWidgetsConfig[i].Events + for i := range snipEvents { + if snipEvents[i].WorkDir == "" { + snipEvents[i].WorkDir = wd + } + } + for _, e := range widget.Events { if e.Override { sort.Strings(e.Modifiers) @@ -164,6 +176,7 @@ WIDGET: snipEvents = append(snipEvents, e) } } + snipWidgetsConfig[i].Events = snipEvents } diff --git a/internal/pkg/executor/executor.go b/internal/pkg/executor/executor.go index 2ca1ab0..e7515b6 100644 --- a/internal/pkg/executor/executor.go +++ b/internal/pkg/executor/executor.go @@ -47,6 +47,12 @@ func Exec(command string, args ...string) (*Executor, error) { return e, nil } +func (e *Executor) SetWD(wd string) { + if e.cmd != nil { + e.cmd.Dir = wd + } +} + func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { stdout, err := e.cmd.StdoutPipe() if err != nil { diff --git a/widgets/exec.go b/widgets/exec.go index ddb13ff..4c268f9 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -24,6 +24,7 @@ type ExecWidgetParams struct { EventsUpdate bool `yaml:"events_update"` Signal *int OutputFormat executor.OutputFormat `yaml:"output_format"` + WorkDir string } // ExecWidget implements the exec widget. @@ -83,6 +84,8 @@ func (w *ExecWidget) exec() error { return err } + exc.SetWD(w.params.WorkDir) + for k, v := range w.customfields { exc.AddEnv( fmt.Sprintf("I3_%s=%s", k, v), diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 7c372c2..140e238 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -14,6 +14,7 @@ import ( // WrapperWidgetParams are widget parameters. type WrapperWidgetParams struct { Command string + WorkDir string } // WrapperWidget implements the wrapper of other status commands. @@ -45,6 +46,8 @@ func NewWrapperWidget(params interface{}) (ygs.Widget, error) { return nil, err } + exc.SetWD(w.params.WorkDir) + w.exc = exc return w, nil diff --git a/yagostatus.go b/yagostatus.go index 6bbc34e..4d99b6f 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -105,6 +105,8 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, return err } + exc.SetWD(widgetEvent.WorkDir) + exc.AddEnv( fmt.Sprintf("I3_%s=%s", "NAME", event.Name), fmt.Sprintf("I3_%s=%s", "INSTANCE", event.Instance), diff --git a/ygs/widget_event_config.go b/ygs/widget_event_config.go index 6ce8cfc..3ef9545 100644 --- a/ygs/widget_event_config.go +++ b/ygs/widget_event_config.go @@ -14,6 +14,7 @@ type WidgetEventConfig struct { Instance string `yaml:"instance,omitempty"` OutputFormat string `yaml:"output_format,omitempty"` Override bool `yaml:"override"` + WorkDir string `yaml:"workdir"` } // Validate checks event parameters. From f2ead3ea59ecc8e29161d0eb509fedf89523c5d0 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Tue, 3 Dec 2019 07:14:21 +0300 Subject: [PATCH 26/34] Add snippets support absolute path --- internal/pkg/config/parser.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index a26ce43..e0cad1c 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -100,7 +100,11 @@ WIDGET: wd = widget.WorkDir } - filename := wd + "/" + widget.Name[1:] + filename := widget.Name[1:] + if !filepath.IsAbs(filename) { + filename = wd + "/" + filename + } + data, err := ioutil.ReadFile(filename) if err != nil { setError(widget, err, false) From f7129acc3b935804bc69ff7b6a04bfab11da9eb5 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Tue, 3 Dec 2019 08:08:24 +0300 Subject: [PATCH 27/34] Update README.md --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index c868904..88a9a9e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Yet Another i3status replacement written in Go. - Shell scripting widgets and events handlers. - Wrapping other status programs (i3status, py3status, conky, etc.). - Different widgets on different workspaces. +- Snippets. - Templates for widgets outputs. - Update widget via http/websocket requests. - Update widget by POSIX Real-Time Signals (SIGRTMIN-SIGRTMAX). @@ -146,6 +147,29 @@ Example: name: ch ``` +### Snippets + +Yagostatus supports the inclusion of snippets from files. +```yml + - widget: $ygs/snip.yaml + msg: hello world + color: #00ff00 +``` + +`ygs/snip.yaml`: +```yml + - widget: static + blocks: > + [ + { + "full_text": "message: ${msg}", + "color": "${color}" + } + ] +``` + +`ygs/snip.yaml` - relative path from the current file. + ### Widget `clock` From b0d6d1c7118b42c0f6ac677b30d66e352e114817 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Wed, 4 Dec 2019 07:29:37 +0300 Subject: [PATCH 28/34] Add current block fields as ENV variables --- README.md | 6 ++++-- widgets/exec.go | 34 ++++++++++++++++------------------ yagostatus.go | 9 ++++----- ygs/protocol.go | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 88a9a9e..4cb08ad 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,8 @@ Example: * `modifiers` - List of X11 modifiers condition. * `command` - Command to execute (via `sh -c`). Сlick_event json will be written to stdin. - Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_X`, `$I3_Y`, `$I3_RELATIVE_X`, `$I3_RELATIVE_Y`, `$I3_WIDTH`, `$I3_HEIGHT`, `$I3_MODIFIERS`. `$I3__` prefix for custom fields. + Also env variables are available: `$I3_NAME`, `$I3_INSTANCE`, `$I3_BUTTON`, `$I3_MODIFIERS`, `$I3_X`, `$I3_Y`, `$I3_RELATIVE_X`, `$I3_RELATIVE_Y`, `$I3_WIDTH`, `$I3_HEIGHT`, `$I3_MODIFIERS`. + The clicked widget fields are available as ENV variables with the prefix `I3_` (example:` $ I3_full_text`). * `workdir` - Set a working directory. * `output_format` - The command output format (none, text, json, auto) (default: `none`). * `name` - Filter by `name` for widgets with multiple blocks (default: empty). @@ -192,7 +193,8 @@ This widget runs the command at the specified interval. - `output_format` - The command output format (none, text, json, auto) (default: `auto`). - `signal` - SIGRTMIN offset to update widget. Should be between 0 and `SIGRTMIN`-`SIGRTMAX`. -Custom fields are available as ENV variables with the prefix `$I3__`. +The current widget fields are available as ENV variables with the prefix `I3_` (example: `$I3_full_text`). +For widgets with multiple blocks, an suffix with an index will be added. (example: `$I3_full_text`, `$I3_full_text_1`, `$I3_full_text_2`, etc.) Use pkill to send signals: diff --git a/widgets/exec.go b/widgets/exec.go index 4c268f9..93c6efd 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -33,11 +33,11 @@ type ExecWidget struct { params ExecWidgetParams - signal os.Signal - c chan<- []ygs.I3BarBlock - upd chan struct{} - customfields map[string]ygs.Vary - tickerC *chan struct{} + signal os.Signal + c chan<- []ygs.I3BarBlock + upd chan struct{} + tickerC *chan struct{} + env []string outputWG sync.WaitGroup } @@ -86,11 +86,7 @@ func (w *ExecWidget) exec() error { exc.SetWD(w.params.WorkDir) - for k, v := range w.customfields { - exc.AddEnv( - fmt.Sprintf("I3_%s=%s", k, v), - ) - } + exc.AddEnv(w.env...) c := make(chan []ygs.I3BarBlock) @@ -106,7 +102,7 @@ func (w *ExecWidget) exec() error { return } w.c <- blocks - w.setCustomFields(blocks) + w.setEnv(blocks) } })() @@ -189,7 +185,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { // Event processes the widget events. func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error { - w.setCustomFields(blocks) + w.setEnv(blocks) if w.params.EventsUpdate { w.upd <- struct{}{} @@ -198,16 +194,18 @@ func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) e return nil } -func (w *ExecWidget) setCustomFields(blocks []ygs.I3BarBlock) { - customfields := make(map[string]ygs.Vary) +func (w *ExecWidget) setEnv(blocks []ygs.I3BarBlock) { + env := make([]string, 0) - for _, block := range blocks { - for k, v := range block.Custom { - customfields[k] = v + for i, block := range blocks { + suffix := "" + if i > 0 { + suffix = fmt.Sprintf("_%d", i) } + env = append(env, block.Env(suffix)...) } - w.customfields = customfields + w.env = env } func (w *ExecWidget) resetTicker() { diff --git a/yagostatus.go b/yagostatus.go index 4d99b6f..449f435 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -120,12 +120,11 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, fmt.Sprintf("I3_%s=%s", "MODIFIERS", strings.Join(event.Modifiers, ",")), ) - for k, v := range status.widgetsOutput[widgetIndex][outputIndex].Custom { - exc.AddEnv( - fmt.Sprintf("I3_%s=%s", k, v), - ) - } + block := status.widgetsOutput[widgetIndex][outputIndex] + block.Name = event.Name + block.Instance = event.Instance + exc.AddEnv(block.Env("")...) stdin, err := exc.Stdin() if err != nil { return err diff --git a/ygs/protocol.go b/ygs/protocol.go index c6b7bca..e797537 100644 --- a/ygs/protocol.go +++ b/ygs/protocol.go @@ -3,6 +3,7 @@ package ygs import ( "bytes" "encoding/json" + "fmt" ) // I3BarHeader represents the header of an i3bar message. @@ -97,3 +98,21 @@ func (b *I3BarBlock) Apply(tpl I3BarBlock) { json.Unmarshal(jb, b) } + +func (b I3BarBlock) Env(suffix string) []string { + env := make([]string, 0) + for k, v := range b.Custom { + env = append(env, fmt.Sprintf("I3_%s%s=%s", k, suffix, v)) + } + + ob, _ := json.Marshal(b) + + var rawOutput map[string]Vary + json.Unmarshal(ob, &rawOutput) + + for k, v := range rawOutput { + env = append(env, fmt.Sprintf("I3_%s%s=%s", k, suffix, v.String())) + } + + return env +} From b371b6cd6bff0e8699fa14d80939e7ce447d700c Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Wed, 4 Dec 2019 08:29:24 +0300 Subject: [PATCH 29/34] Fix int snippets variables --- internal/pkg/config/parser.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index e0cad1c..276a91a 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "sort" + "strconv" "strings" "syscall" @@ -248,6 +249,13 @@ func replaceRecursive(v *reflect.Value, dict map[string]string) { st = strings.ReplaceAll(st, s, r) } + if n, err := strconv.ParseInt(st, 10, 64); err == nil { + vi := reflect.New(reflect.ValueOf(n).Type()).Elem() + vi.SetInt(n) + *v = vi + return + } + if vv.CanSet() { vv.SetString(st) } else { From 2a0baf210cff64a27d58dcd19c3c108deced8321 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Thu, 20 Feb 2020 04:43:04 +0300 Subject: [PATCH 30/34] Add logger --- internal/pkg/config/config.go | 6 +- internal/pkg/config/parser.go | 21 ++- internal/pkg/executor/executor.go | 21 ++- internal/pkg/logger/looger.go | 47 +++++++ main.go | 24 ++-- widgets/blank.go | 3 +- widgets/clock.go | 3 +- widgets/exec.go | 13 +- widgets/http.go | 17 ++- widgets/static.go | 3 +- widgets/wrapper.go | 8 +- yagostatus.go | 216 ++++++++++++++++-------------- ygs/utils.go | 8 +- ygs/widget_config.go | 2 + 14 files changed, 246 insertions(+), 146 deletions(-) create mode 100644 internal/pkg/logger/looger.go diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 93a80c1..60d4e2a 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -25,14 +25,14 @@ func LoadFile(filename string) (*Config, error) { return nil, err } - return parse(data, filepath.Dir(filename)) + return parse(data, filepath.Dir(filename), filepath.Base(filename)) } // Parse parses config. -func Parse(data []byte) (*Config, error) { +func Parse(data []byte, source string) (*Config, error) { wd, err := os.Getwd() if err != nil { return nil, err } - return parse(data, wd) + return parse(data, wd, source) } diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index 276a91a..2b17399 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -17,7 +17,7 @@ import ( "gopkg.in/yaml.v2" ) -func parse(data []byte, workdir string) (*Config, error) { +func parse(data []byte, workdir string, source string) (*Config, error) { config := Config{} config.Signals.StopSignal = syscall.SIGUSR1 @@ -27,16 +27,21 @@ func parse(data []byte, workdir string) (*Config, error) { return nil, trimYamlErr(err, false) } + for wi := range config.Widgets { + config.Widgets[wi].File = source + config.Widgets[wi].Index = wi + } + WIDGET: - for widgetIndex := 0; widgetIndex < len(config.Widgets); widgetIndex++ { - widget := &config.Widgets[widgetIndex] + for wi := 0; wi < len(config.Widgets); wi++ { + widget := &config.Widgets[wi] params := make(map[string]interface{}) - for k, v := range config.Widgets[widgetIndex].Params { + for k, v := range config.Widgets[wi].Params { params[strings.ToLower(k)] = v } - config.Widgets[widgetIndex].Params = params + config.Widgets[wi].Params = params if widget.WorkDir == "" { widget.WorkDir = workdir @@ -147,6 +152,8 @@ WIDGET: wd = filepath.Dir(filename) for i := range snipWidgetsConfig { snipWidgetsConfig[i].WorkDir = wd + snipWidgetsConfig[i].File = filename + snipWidgetsConfig[i].Index = i snipWidgetsConfig[i].IncludeStack = append(widget.IncludeStack, widget.Name) json.Unmarshal(tpls, &snipWidgetsConfig[i].Templates) @@ -185,11 +192,11 @@ WIDGET: snipWidgetsConfig[i].Events = snipEvents } - i := widgetIndex + i := wi config.Widgets = append(config.Widgets[:i], config.Widgets[i+1:]...) config.Widgets = append(config.Widgets[:i], append(snipWidgetsConfig, config.Widgets[i:]...)...) - widgetIndex-- + wi-- continue WIDGET } diff --git a/internal/pkg/executor/executor.go b/internal/pkg/executor/executor.go index e7515b6..dce241d 100644 --- a/internal/pkg/executor/executor.go +++ b/internal/pkg/executor/executor.go @@ -1,6 +1,7 @@ package executor import ( + "bufio" "bytes" "encoding/json" "io" @@ -11,6 +12,7 @@ import ( "strings" "syscall" + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/ygs" ) @@ -37,7 +39,6 @@ func Exec(command string, args ...string) (*Executor, error) { e := &Executor{} e.cmd = exec.Command(name, args...) - e.cmd.Stderr = os.Stderr e.cmd.Env = os.Environ() e.cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, @@ -53,7 +54,21 @@ func (e *Executor) SetWD(wd string) { } } -func (e *Executor) Run(c chan<- []ygs.I3BarBlock, format OutputFormat) error { +func (e *Executor) Run(logger logger.Logger, c chan<- []ygs.I3BarBlock, format OutputFormat) error { + stderr, err := e.cmd.StderrPipe() + if err != nil { + return err + } + + go (func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + logger.Errorf("(stderr) %s", scanner.Text()) + } + })() + + defer stderr.Close() + stdout, err := e.cmd.StdoutPipe() if err != nil { return err @@ -157,7 +172,7 @@ func (e *Executor) AddEnv(env ...string) { } func (e *Executor) Wait() error { - if e.cmd != nil { + if e.cmd != nil && e.cmd.Process != nil { return e.cmd.Wait() } diff --git a/internal/pkg/logger/looger.go b/internal/pkg/logger/looger.go new file mode 100644 index 0000000..ad65d9b --- /dev/null +++ b/internal/pkg/logger/looger.go @@ -0,0 +1,47 @@ +package logger + +import ( + "fmt" + "log" + "os" +) + +type Logger interface { + Infof(format string, v ...interface{}) + Errorf(format string, v ...interface{}) + Debugf(format string, v ...interface{}) + WithPrefix(prefix string) Logger +} + +func New(flags int) Logger { + return &stdLogger{ + std: log.New(os.Stderr, "", flags), + } +} + +type stdLogger struct { + std *log.Logger + prefix string +} + +func (l stdLogger) Outputf(calldepth int, subprefix string, format string, v ...interface{}) { + st := l.prefix + subprefix + fmt.Sprintf(format, v...) + l.std.Output(calldepth+1, st) +} + +func (l stdLogger) Infof(format string, v ...interface{}) { + l.Outputf(2, "INFO ", format, v...) +} + +func (l stdLogger) Errorf(format string, v ...interface{}) { + l.Outputf(2, "ERROR ", format, v...) +} + +func (l stdLogger) Debugf(format string, v ...interface{}) { + l.Outputf(2, "DEBUG", format, v...) +} + +func (l stdLogger) WithPrefix(prefix string) Logger { + l.prefix = prefix + " " + return &l +} diff --git a/main.go b/main.go index 8b09b7b..40385cb 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,13 @@ package main import ( "flag" - "fmt" "log" "os" "os/signal" "syscall" "github.com/burik666/yagostatus/internal/pkg/config" + "github.com/burik666/yagostatus/internal/pkg/logger" ) var builtinConfig = []byte(` @@ -38,7 +38,7 @@ widgets: `) func main() { - log.SetFlags(log.Ldate + log.Ltime + log.Lshortfile) + logger := logger.New(log.Ldate + log.Ltime + log.Lshortfile) var configFile string @@ -49,7 +49,7 @@ func main() { flag.Parse() if *versionFlag { - fmt.Printf("YaGoStatus %s\n", Version) + logger.Infof("YaGoStatus %s", Version) return } @@ -62,9 +62,10 @@ func main() { if os.IsNotExist(cfgError) { cfgError = nil - cfg, err = config.Parse(builtinConfig) + cfg, err = config.Parse(builtinConfig, "builtin") if err != nil { - log.Fatalf("Failed to parse builtin config: %s", err) + logger.Errorf("Failed to parse builtin config: %s", err) + os.Exit(1) } } @@ -78,13 +79,14 @@ func main() { } } - yaGoStatus, err := NewYaGoStatus(*cfg) + yaGoStatus, err := NewYaGoStatus(*cfg, logger) if err != nil { - log.Fatalf("Failed to create yagostatus instance: %s", err) + logger.Errorf("Failed to create yagostatus instance: %s", err) + os.Exit(1) } if cfgError != nil { - log.Printf("Failed to load config: %s", cfgError) + logger.Errorf("Failed to load config: %s", cfgError) yaGoStatus.errorWidget(cfgError.Error()) } @@ -108,7 +110,7 @@ func main() { go func() { if err := yaGoStatus.Run(); err != nil { - log.Printf("Failed to run yagostatus: %s", err) + logger.Errorf("Failed to run yagostatus: %s", err) } shutdownsignals <- syscall.SIGTERM }() @@ -116,6 +118,8 @@ func main() { <-shutdownsignals if err := yaGoStatus.Shutdown(); err != nil { - log.Printf("Failed to shutdown yagostatus: %s", err) + logger.Errorf("Failed to shutdown yagostatus: %s", err) } + + logger.Infof("exit") } diff --git a/widgets/blank.go b/widgets/blank.go index 56bd125..8bb237a 100644 --- a/widgets/blank.go +++ b/widgets/blank.go @@ -2,6 +2,7 @@ package widgets import ( + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/ygs" ) @@ -16,7 +17,7 @@ func init() { } // NewBlankWidget returns a new BlankWidget. -func NewBlankWidget(params interface{}) (ygs.Widget, error) { +func NewBlankWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) { return &BlankWidget{}, nil } diff --git a/widgets/clock.go b/widgets/clock.go index 98d79f7..6ac4f3a 100644 --- a/widgets/clock.go +++ b/widgets/clock.go @@ -3,6 +3,7 @@ package widgets import ( "time" + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/ygs" ) @@ -27,7 +28,7 @@ func init() { } // NewClockWidget returns a new ClockWidget. -func NewClockWidget(params interface{}) (ygs.Widget, error) { +func NewClockWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) { w := &ClockWidget{ params: params.(ClockWidgetParams), } diff --git a/widgets/exec.go b/widgets/exec.go index 93c6efd..01dc47a 100644 --- a/widgets/exec.go +++ b/widgets/exec.go @@ -3,7 +3,6 @@ package widgets import ( "errors" "fmt" - "log" "os" "os/signal" "sync" @@ -11,6 +10,7 @@ import ( "time" "github.com/burik666/yagostatus/internal/pkg/executor" + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/internal/pkg/signals" "github.com/burik666/yagostatus/ygs" ) @@ -33,6 +33,8 @@ type ExecWidget struct { params ExecWidgetParams + logger logger.Logger + signal os.Signal c chan<- []ygs.I3BarBlock upd chan struct{} @@ -47,9 +49,10 @@ func init() { } // NewExecWidget returns a new ExecWidget. -func NewExecWidget(params interface{}) (ygs.Widget, error) { +func NewExecWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) { w := &ExecWidget{ params: params.(ExecWidgetParams), + logger: wlogger, } if len(w.params.Command) == 0 { @@ -106,7 +109,7 @@ func (w *ExecWidget) exec() error { } })() - err = exc.Run(c, w.params.OutputFormat) + err = exc.Run(w.logger, c, w.params.OutputFormat) if err == nil { if state := exc.ProcessState(); state != nil && state.ExitCode() != 0 { if w.params.Retry != nil { @@ -131,7 +134,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { err := w.exec() if w.params.Silent { if err != nil { - log.Print(err) + w.logger.Errorf("exec failed: %s", err) } return nil @@ -176,7 +179,7 @@ func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error { }} } - log.Print(err) + w.logger.Errorf("exec failed: %s", err) } } diff --git a/widgets/http.go b/widgets/http.go index 0bdd7b4..39cdd22 100644 --- a/widgets/http.go +++ b/widgets/http.go @@ -7,11 +7,11 @@ import ( "fmt" "io" "io/ioutil" - "log" "net" "net/http" "sync" + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/ygs" "golang.org/x/net/websocket" @@ -30,6 +30,8 @@ type HTTPWidget struct { params HTTPWidgetParams + logger logger.Logger + c chan<- []ygs.I3BarBlock instance *httpInstance @@ -55,9 +57,10 @@ func init() { } // NewHTTPWidget returns a new HTTPWidget. -func NewHTTPWidget(params interface{}) (ygs.Widget, error) { +func NewHTTPWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) { w := &HTTPWidget{ params: params.(HTTPWidgetParams), + logger: wlogger, } if len(w.params.Listen) == 0 { @@ -153,12 +156,12 @@ func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Req if request.Method == "POST" { body, err := ioutil.ReadAll(request.Body) if err != nil { - log.Printf("%s", err) + w.logger.Errorf("%s", err) } var messages []ygs.I3BarBlock if err := json.Unmarshal(body, &messages); err != nil { - log.Printf("%s", err) + w.logger.Errorf("%s", err) response.WriteHeader(http.StatusBadRequest) fmt.Fprintf(response, "%s", err) } @@ -172,7 +175,7 @@ func (w *HTTPWidget) httpHandler(response http.ResponseWriter, request *http.Req _, err := response.Write([]byte("bad request method, allow GET for websocket and POST for HTTP update")) if err != nil { - log.Printf("failed to write response: %s", err) + w.logger.Errorf("failed to write response: %s", err) } } @@ -195,7 +198,7 @@ func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { } if err := websocket.JSON.Send(ws, msg); err != nil { - log.Printf("failed to send msg: %s", err) + w.logger.Errorf("failed to send msg: %s", err) } } }() @@ -206,7 +209,7 @@ func (w *HTTPWidget) wsHandler(ws *websocket.Conn) { break } - log.Printf("invalid message: %s", err) + w.logger.Errorf("invalid message: %s", err) break } diff --git a/widgets/static.go b/widgets/static.go index 413e501..55d3aa3 100644 --- a/widgets/static.go +++ b/widgets/static.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/ygs" ) @@ -26,7 +27,7 @@ func init() { } // NewStaticWidget returns a new StaticWidget. -func NewStaticWidget(params interface{}) (ygs.Widget, error) { +func NewStaticWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) { w := &StaticWidget{ params: params.(StaticWidgetParams), } diff --git a/widgets/wrapper.go b/widgets/wrapper.go index 140e238..9ec5195 100644 --- a/widgets/wrapper.go +++ b/widgets/wrapper.go @@ -8,6 +8,7 @@ import ( "syscall" "github.com/burik666/yagostatus/internal/pkg/executor" + "github.com/burik666/yagostatus/internal/pkg/logger" "github.com/burik666/yagostatus/ygs" ) @@ -21,6 +22,8 @@ type WrapperWidgetParams struct { type WrapperWidget struct { params WrapperWidgetParams + logger logger.Logger + exc *executor.Executor stdin io.WriteCloser @@ -32,9 +35,10 @@ func init() { } // NewWrapperWidget returns a new WrapperWidget. -func NewWrapperWidget(params interface{}) (ygs.Widget, error) { +func NewWrapperWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) { w := &WrapperWidget{ params: params.(WrapperWidgetParams), + logger: wlogger, } if len(w.params.Command) == 0 { @@ -64,7 +68,7 @@ func (w *WrapperWidget) Run(c chan<- []ygs.I3BarBlock) error { defer w.stdin.Close() - err = w.exc.Run(c, executor.OutputFormatJSON) + err = w.exc.Run(w.logger, c, executor.OutputFormatJSON) if err == nil { err = errors.New("process exited unexpectedly") diff --git a/yagostatus.go b/yagostatus.go index 449f435..869dff7 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "os" "runtime/debug" "strings" @@ -13,18 +12,24 @@ import ( "github.com/burik666/yagostatus/internal/pkg/config" "github.com/burik666/yagostatus/internal/pkg/executor" + "github.com/burik666/yagostatus/internal/pkg/logger" _ "github.com/burik666/yagostatus/widgets" "github.com/burik666/yagostatus/ygs" "go.i3wm.org/i3" ) +type widgetContainer struct { + instance ygs.Widget + output []ygs.I3BarBlock + config ygs.WidgetConfig + ch chan []ygs.I3BarBlock + logger logger.Logger +} + // YaGoStatus is the main struct. type YaGoStatus struct { - widgets []ygs.Widget - widgetsOutput [][]ygs.I3BarBlock - widgetsConfig []ygs.WidgetConfig - widgetChans []chan []ygs.I3BarBlock + widgets []widgetContainer upd chan int @@ -32,70 +37,74 @@ type YaGoStatus struct { visibleWorkspaces []string cfg config.Config + + logger logger.Logger } // NewYaGoStatus returns a new YaGoStatus instance. -func NewYaGoStatus(cfg config.Config) (*YaGoStatus, error) { - status := &YaGoStatus{cfg: cfg} - - for _, w := range cfg.Widgets { - (func() { - defer (func() { - if r := recover(); r != nil { - log.Printf("NewWidget is panicking: %s", r) - debug.PrintStack() - status.errorWidget("Widget is panicking") - } - })() - - widget, err := ygs.NewWidget(w) - if err != nil { - log.Printf("Failed to create widget: %s", err) - status.errorWidget(err.Error()) - - return - } +func NewYaGoStatus(cfg config.Config, l logger.Logger) (*YaGoStatus, error) { + status := &YaGoStatus{ + cfg: cfg, + logger: l, + } - status.AddWidget(widget, w) - })() + for wi := range cfg.Widgets { + status.addWidget(cfg.Widgets[wi]) } return status, nil } func (status *YaGoStatus) errorWidget(text string) { - errWidget, err := ygs.NewWidget(ygs.ErrorWidget(text)) - if err != nil { - panic(err) - } - - status.AddWidget(errWidget, ygs.WidgetConfig{}) + status.addWidget(ygs.ErrorWidget(text)) } -// AddWidget adds widget to statusbar. -func (status *YaGoStatus) AddWidget(widget ygs.Widget, config ygs.WidgetConfig) { - status.widgets = append(status.widgets, widget) - status.widgetsOutput = append(status.widgetsOutput, nil) - status.widgetsConfig = append(status.widgetsConfig, config) +func (status *YaGoStatus) addWidget(wcfg ygs.WidgetConfig) { + wlogger := status.logger.WithPrefix(fmt.Sprintf("[%s#%d]", wcfg.File, wcfg.Index+1)) + + (func() { + defer (func() { + if r := recover(); r != nil { + wlogger.Errorf("NewWidget panic: %s", r) + debug.PrintStack() + status.errorWidget("widget panic") + } + })() + + widget, err := ygs.NewWidget(wcfg, wlogger) + if err != nil { + wlogger.Errorf("Failed to create widget: %s", err) + status.errorWidget(err.Error()) + + return + } + + status.widgets = append(status.widgets, widgetContainer{ + instance: widget, + config: wcfg, + ch: make(chan []ygs.I3BarBlock), + logger: wlogger, + }) + })() } -func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, event ygs.I3BarClickEvent) error { +func (status *YaGoStatus) processWidgetEvents(wi int, outputIndex int, event ygs.I3BarClickEvent) error { defer (func() { if r := recover(); r != nil { - log.Printf("Widget event is panicking: %s", r) + status.widgets[wi].logger.Errorf("widget event panic: %s", r) debug.PrintStack() - status.widgetsOutput[widgetIndex] = []ygs.I3BarBlock{{ - FullText: "Widget event is panicking", + status.widgets[wi].output = []ygs.I3BarBlock{{ + FullText: "widget panic", Color: "#ff0000", }} } - if err := status.widgets[widgetIndex].Event(event, status.widgetsOutput[widgetIndex]); err != nil { - log.Printf("Failed to process widget event: %s", err) + if err := status.widgets[wi].instance.Event(event, status.widgets[wi].output); err != nil { + status.widgets[wi].logger.Errorf("Failed to process widget event: %s", err) } })() - for _, widgetEvent := range status.widgetsConfig[widgetIndex].Events { + for _, widgetEvent := range status.widgets[wi].config.Events { if (widgetEvent.Button == 0 || widgetEvent.Button == event.Button) && (widgetEvent.Name == "" || widgetEvent.Name == event.Name) && (widgetEvent.Instance == "" || widgetEvent.Instance == event.Instance) && @@ -120,7 +129,7 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, fmt.Sprintf("I3_%s=%s", "MODIFIERS", strings.Join(event.Modifiers, ",")), ) - block := status.widgetsOutput[widgetIndex][outputIndex] + block := status.widgets[wi].output[outputIndex] block.Name = event.Name block.Instance = event.Instance @@ -147,7 +156,11 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, return err } - err = exc.Run(status.widgetChans[widgetIndex], executor.OutputFormat(widgetEvent.OutputFormat)) + err = exc.Run( + status.widgets[wi].logger, + status.widgets[wi].ch, + executor.OutputFormat(widgetEvent.OutputFormat), + ) if err != nil { return err } @@ -157,30 +170,30 @@ func (status *YaGoStatus) processWidgetEvents(widgetIndex int, outputIndex int, return nil } -func (status *YaGoStatus) addWidgetOutput(widgetIndex int, blocks []ygs.I3BarBlock) { +func (status *YaGoStatus) addWidgetOutput(wi int, blocks []ygs.I3BarBlock) { output := make([]ygs.I3BarBlock, len(blocks)) - tplc := len(status.widgetsConfig[widgetIndex].Templates) + tplc := len(status.widgets[wi].config.Templates) for blockIndex := range blocks { block := blocks[blockIndex] if tplc == 1 { - block.Apply(status.widgetsConfig[widgetIndex].Templates[0]) + block.Apply(status.widgets[wi].config.Templates[0]) } else { if blockIndex < tplc { - block.Apply(status.widgetsConfig[widgetIndex].Templates[blockIndex]) + block.Apply(status.widgets[wi].config.Templates[blockIndex]) } } - block.Name = fmt.Sprintf("yagostatus-%d-%s", widgetIndex, block.Name) - block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", widgetIndex, blockIndex, block.Instance) + block.Name = fmt.Sprintf("yagostatus-%d-%s", wi, block.Name) + block.Instance = fmt.Sprintf("yagostatus-%d-%d-%s", wi, blockIndex, block.Instance) output[blockIndex] = block } - status.widgetsOutput[widgetIndex] = output + status.widgets[wi].output = output - status.upd <- widgetIndex + status.upd <- wi } func (status *YaGoStatus) eventReader() error { @@ -203,23 +216,23 @@ func (status *YaGoStatus) eventReader() error { var event ygs.I3BarClickEvent if err := json.Unmarshal([]byte(line), &event); err != nil { - log.Printf("%s (%s)", err, line) + status.logger.Errorf("%s (%s)", err, line) continue } - for widgetIndex, widgetOutputs := range status.widgetsOutput { - for outputIndex, output := range widgetOutputs { + for wi := range status.widgets { + for outputIndex, output := range status.widgets[wi].output { if (event.Name != "" && event.Name == output.Name) && (event.Instance != "" && event.Instance == output.Instance) { e := event e.Name = strings.Join(strings.Split(e.Name, "-")[2:], "-") e.Instance = strings.Join(strings.Split(e.Instance, "-")[3:], "-") - if err := status.processWidgetEvents(widgetIndex, outputIndex, e); err != nil { - log.Print(err) + if err := status.processWidgetEvents(wi, outputIndex, e); err != nil { + status.widgets[wi].logger.Errorf("event error: %s", err) - status.widgetsOutput[widgetIndex][outputIndex] = ygs.I3BarBlock{ - FullText: fmt.Sprintf("Event error: %s", err.Error()), + status.widgets[wi].output[outputIndex] = ygs.I3BarBlock{ + FullText: fmt.Sprintf("event error: %s", err.Error()), Color: "#ff0000", Name: event.Name, Instance: event.Instance, @@ -252,35 +265,32 @@ func (status *YaGoStatus) Run() error { } })() - for widgetIndex, widget := range status.widgets { - c := make(chan []ygs.I3BarBlock) - status.widgetChans = append(status.widgetChans, c) - - go func(widgetIndex int, c chan []ygs.I3BarBlock) { - for out := range c { - status.addWidgetOutput(widgetIndex, out) + for wi := range status.widgets { + go func(wi int) { + for out := range status.widgets[wi].ch { + status.addWidgetOutput(wi, out) } - }(widgetIndex, c) + }(wi) - go func(widget ygs.Widget, c chan []ygs.I3BarBlock) { + go func(wi int) { defer (func() { if r := recover(); r != nil { - c <- []ygs.I3BarBlock{{ - FullText: "Widget is panicking", + status.widgets[wi].logger.Errorf("widget panic: %s", r) + debug.PrintStack() + status.widgets[wi].ch <- []ygs.I3BarBlock{{ + FullText: "widget panic", Color: "#ff0000", }} - log.Printf("Widget is panicking: %s", r) - debug.PrintStack() } })() - if err := widget.Run(c); err != nil { - log.Print(err) - c <- []ygs.I3BarBlock{{ + if err := status.widgets[wi].instance.Run(status.widgets[wi].ch); err != nil { + status.widgets[wi].logger.Errorf("Widget done: %s", err) + status.widgets[wi].ch <- []ygs.I3BarBlock{{ FullText: err.Error(), Color: "#ff0000", }} } - }(widget, c) + }(wi) } encoder := json.NewEncoder(os.Stdout) @@ -293,7 +303,7 @@ func (status *YaGoStatus) Run() error { StopSignal: int(status.cfg.Signals.StopSignal), ContSignal: int(status.cfg.Signals.ContSignal), }); err != nil { - log.Printf("Failed to encode I3BarHeader: %s", err) + status.logger.Errorf("Failed to encode I3BarHeader: %s", err) } fmt.Print("\n[\n[]") @@ -301,15 +311,15 @@ func (status *YaGoStatus) Run() error { go func() { for range status.upd { var result []ygs.I3BarBlock - for widgetIndex, widgetOutput := range status.widgetsOutput { - if checkWorkspaceConditions(status.widgetsConfig[widgetIndex].Workspaces, status.visibleWorkspaces) { - result = append(result, widgetOutput...) + for wi := range status.widgets { + if checkWorkspaceConditions(status.widgets[wi].config.Workspaces, status.visibleWorkspaces) { + result = append(result, status.widgets[wi].output...) } } fmt.Print(",") err := encoder.Encode(result) if err != nil { - log.Printf("Failed to encode result: %s", err) + status.logger.Errorf("Failed to encode result: %s", err) } } }() @@ -321,21 +331,21 @@ func (status *YaGoStatus) Run() error { func (status *YaGoStatus) Shutdown() error { var wg sync.WaitGroup - for _, widget := range status.widgets { + for wi := range status.widgets { wg.Add(1) - go func(widget ygs.Widget) { + go func(wi int) { defer wg.Done() defer (func() { if r := recover(); r != nil { - log.Printf("Widget is panicking: %s", r) + status.widgets[wi].logger.Errorf("widget panic: %s", r) debug.PrintStack() } })() - if err := widget.Shutdown(); err != nil { - log.Printf("Failed to shutdown widget: %s", err) + if err := status.widgets[wi].instance.Shutdown(); err != nil { + status.widgets[wi].logger.Errorf("Failed to shutdown widget: %s", err) } - }(widget) + }(wi) } wg.Wait() @@ -345,35 +355,35 @@ func (status *YaGoStatus) Shutdown() error { // Stop stops widgets and main loop. func (status *YaGoStatus) Stop() { - for _, widget := range status.widgets { - go func(widget ygs.Widget) { + for wi := range status.widgets { + go func(wi int) { defer (func() { if r := recover(); r != nil { - log.Printf("Widget is panicking: %s", r) + status.widgets[wi].logger.Errorf("widget panic: %s", r) debug.PrintStack() } })() - if err := widget.Stop(); err != nil { - log.Printf("Failed to stop widget: %s", err) + if err := status.widgets[wi].instance.Stop(); err != nil { + status.widgets[wi].logger.Errorf("Failed to stop widget: %s", err) } - }(widget) + }(wi) } } // Continue continues widgets and main loop. func (status *YaGoStatus) Continue() { - for _, widget := range status.widgets { - go func(widget ygs.Widget) { + for wi := range status.widgets { + go func(wi int) { defer (func() { if r := recover(); r != nil { - log.Printf("Widget is panicking: %s", r) + status.widgets[wi].logger.Errorf("widget panic: %s", r) debug.PrintStack() } })() - if err := widget.Continue(); err != nil { - log.Printf("Failed to continue widget: %s", err) + if err := status.widgets[wi].instance.Continue(); err != nil { + status.widgets[wi].logger.Errorf("Failed to continue widget: %s", err) } - }(widget) + }(wi) } } @@ -383,7 +393,7 @@ func (status *YaGoStatus) updateWorkspaces() { status.workspaces, err = i3.GetWorkspaces() if err != nil { - log.Printf("Failed to get workspaces: %s", err) + status.logger.Errorf("Failed to get workspaces: %s", err) } var vw []string diff --git a/ygs/utils.go b/ygs/utils.go index 5d0806f..f779ad0 100644 --- a/ygs/utils.go +++ b/ygs/utils.go @@ -7,10 +7,11 @@ import ( "reflect" "strings" + "github.com/burik666/yagostatus/internal/pkg/logger" "gopkg.in/yaml.v2" ) -type newWidgetFunc = func(interface{}) (Widget, error) +type newWidgetFunc = func(interface{}, logger.Logger) (Widget, error) type widget struct { newFunc newWidgetFunc @@ -37,7 +38,7 @@ func RegisterWidget(name string, newFunc newWidgetFunc, defaultParams interface{ } // NewWidget creates new widget by name. -func NewWidget(widgetConfig WidgetConfig) (Widget, error) { +func NewWidget(widgetConfig WidgetConfig, wlogger logger.Logger) (Widget, error) { name := widgetConfig.Name widget, ok := registeredWidgets[name] if !ok { @@ -68,7 +69,7 @@ func NewWidget(widgetConfig WidgetConfig) (Widget, error) { } } - return widget.newFunc(pe.Interface()) + return widget.newFunc(pe.Interface(), wlogger) } // ErrorWidget creates new widget with error message. @@ -85,6 +86,7 @@ func ErrorWidget(text string) WidgetConfig { Params: map[string]interface{}{ "blocks": string(blocks), }, + File: "bultin", } } diff --git a/ygs/widget_config.go b/ygs/widget_config.go index c97ae88..1dd32e4 100644 --- a/ygs/widget_config.go +++ b/ygs/widget_config.go @@ -11,6 +11,8 @@ type WidgetConfig struct { Templates []I3BarBlock `yaml:"-"` Events []WidgetEventConfig `yaml:"events"` WorkDir string `yaml:"-"` + Index int `yaml:"-"` + File string `yaml:"-"` Params map[string]interface{} `yaml:",inline"` From c84d1335a7813e7d39780815b749e66b01f7071e Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Fri, 15 May 2020 14:06:34 +0300 Subject: [PATCH 31/34] Upd go.mod, go.sum, go.i3wm.org/i3/v4 --- go.mod | 10 ++++++---- go.sum | 23 ++++++++++++----------- yagostatus.go | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 721cfef..00c75d5 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/burik666/yagostatus -go 1.13 +go 1.14 require ( - go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd - golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 - gopkg.in/yaml.v2 v2.2.2 + github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843 // indirect + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect + go.i3wm.org/i3/v4 v4.18.0 + golang.org/x/net v0.0.0-20200513185701-a91f0712d120 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index 85f3391..09729c7 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,18 @@ -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e h1:4ZrkT/RzpnROylmoQL57iVUL57wGKTR5O6KpVnbm2tA= +github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843 h1:3iF31c7rp7nGZVDv7YQ+VxOgpipVfPKotLXykjZmwM8= +github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd h1:PVrHRo3MP4x/+tbG01rmFnp1sJ1GC7laHJ56OedXYp0= -go.i3wm.org/i3 v0.0.0-20181105220049-e2468ef5e1cd/go.mod h1:7w17+r1F28Yxb1pHu8SxARJo+RGvJCAGOEf+GLw+2aQ= +go.i3wm.org/i3/v4 v4.18.0 h1:yV9SCpYeyTqEuele2bw7rLmEfrGCRlid9enTqLjPuG0= +go.i3wm.org/i3/v4 v4.18.0/go.mod h1:FN6trxp2TXmfzQVa6JSKq5IJx/XUJsnPFKAz15cBWtw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU= -golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/yagostatus.go b/yagostatus.go index 869dff7..25d8cb1 100644 --- a/yagostatus.go +++ b/yagostatus.go @@ -16,7 +16,7 @@ import ( _ "github.com/burik666/yagostatus/widgets" "github.com/burik666/yagostatus/ygs" - "go.i3wm.org/i3" + "go.i3wm.org/i3/v4" ) type widgetContainer struct { From 8e543efc720626235d194eaa82e32ae27aa20083 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Fri, 15 May 2020 16:19:02 +0300 Subject: [PATCH 32/34] Add variables --- README.md | 10 +- internal/pkg/config/config.go | 9 +- internal/pkg/config/parser.go | 206 ++++++++++++++++++++-------------- ygs/widget_config.go | 1 + 4 files changed, 135 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 4cb08ad..c311020 100644 --- a/README.md +++ b/README.md @@ -152,13 +152,17 @@ Example: Yagostatus supports the inclusion of snippets from files. ```yml - - widget: $ygs/snip.yaml + - widget: $ygs-snippets/snip.yaml msg: hello world color: #00ff00 ``` -`ygs/snip.yaml`: +`ygs-snippets/snip.yaml`: ```yml +variables: + msg: "default messsage" + color: #ffffff +widgets: - widget: static blocks: > [ @@ -169,7 +173,7 @@ Yagostatus supports the inclusion of snippets from files. ] ``` -`ygs/snip.yaml` - relative path from the current file. +`ygs-snippets/snip.yaml` - relative path from the current file. ### Widget `clock` diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 60d4e2a..b6484f9 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -15,7 +15,14 @@ type Config struct { StopSignal syscall.Signal `yaml:"stop"` ContSignal syscall.Signal `yaml:"cont"` } `yaml:"signals"` - Widgets []ygs.WidgetConfig `yaml:"widgets"` + Variables map[string]interface{} `yaml:"variables"` + Widgets []ygs.WidgetConfig `yaml:"widgets"` +} + +// SnippetConfig represents the snippet configuration +type SnippetConfig struct { + Variables map[string]interface{} `yaml:"variables"` + Widgets []ygs.WidgetConfig `yaml:"widgets"` } // LoadFile loads and parses config from file. diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go index 2b17399..0ea0c73 100644 --- a/internal/pkg/config/parser.go +++ b/internal/pkg/config/parser.go @@ -32,6 +32,26 @@ func parse(data []byte, workdir string, source string) (*Config, error) { config.Widgets[wi].Index = wi } + dict := make(map[string]string, len(config.Variables)) + for k, v := range config.Variables { + vb, err := json.Marshal(v) + if err != nil { + return nil, err + } + + var vraw ygs.Vary + + err = json.Unmarshal(vb, &vraw) + if err != nil { + return nil, err + } + + dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n") + } + + v := reflect.ValueOf(config.Widgets) + replaceRecursive(&v, dict) + WIDGET: for wi := 0; wi < len(config.Widgets); wi++ { widget := &config.Widgets[wi] @@ -58,6 +78,7 @@ WIDGET: tpl, ok := itpl.(string) if !ok { setError(widget, fmt.Errorf("invalid template"), false) + continue WIDGET } @@ -75,6 +96,7 @@ WIDGET: tpls, ok := itpls.(string) if !ok { setError(widget, fmt.Errorf("invalid templates"), false) + continue WIDGET } @@ -87,128 +109,138 @@ WIDGET: delete(params, "templates") } - tpls, _ := json.Marshal(widget.Templates) + ok, err := parseSnippet(&config, wi, params) + if err != nil { + setError(widget, err, false) - if len(widget.Name) > 0 && widget.Name[0] == '$' { - for i := range widget.IncludeStack { - if widget.Name == widget.IncludeStack[i] { - stack := append(widget.IncludeStack, widget.Name) + continue WIDGET + } - setError(widget, fmt.Errorf("recursive include: '%s'", strings.Join(stack, " -> ")), false) + if ok { + wi-- + continue WIDGET + } - continue WIDGET - } - } + if err := widget.Validate(); err != nil { + setError(widget, err, true) - wd := workdir + continue WIDGET + } + } - if widget.WorkDir != "" { - wd = widget.WorkDir - } + return &config, nil +} - filename := widget.Name[1:] - if !filepath.IsAbs(filename) { - filename = wd + "/" + filename - } +func parseSnippet(config *Config, wi int, params map[string]interface{}) (bool, error) { + widget := config.Widgets[wi] - data, err := ioutil.ReadFile(filename) - if err != nil { - setError(widget, err, false) + if len(widget.Name) > 0 && widget.Name[0] == '$' { + for i := range widget.IncludeStack { + if widget.Name == widget.IncludeStack[i] { + stack := append(widget.IncludeStack, widget.Name) - continue WIDGET + return false, fmt.Errorf("recursive include: '%s'", strings.Join(stack, " -> ")) } + } - dict := make(map[string]string, len(params)) - for k, v := range params { - vb, err := json.Marshal(v) - if err != nil { - setError(widget, err, false) + wd := widget.WorkDir - continue WIDGET - } + filename := widget.Name[1:] + if !filepath.IsAbs(filename) { + filename = wd + "/" + filename + } - var vraw ygs.Vary + data, err := ioutil.ReadFile(filename) + if err != nil { + return false, err + } - err = json.Unmarshal(vb, &vraw) - if err != nil { - setError(widget, err, true) + var snippetConfig SnippetConfig + if err := yaml.Unmarshal(data, &snippetConfig); err != nil { + return false, err + } - continue WIDGET - } + for k, v := range snippetConfig.Variables { + if _, ok := params[k]; !ok { + params[k] = v + } + } - dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n") + dict := make(map[string]string, len(params)) + for k, v := range params { + if _, ok := snippetConfig.Variables[k]; !ok { + return false, fmt.Errorf("unknown variable '%s'", k) } - var snipWidgetsConfig []ygs.WidgetConfig - if err := yaml.Unmarshal(data, &snipWidgetsConfig); err != nil { - setError(widget, err, false) + vb, err := json.Marshal(v) + if err != nil { + return false, err + } - continue WIDGET + var vraw ygs.Vary + + err = json.Unmarshal(vb, &vraw) + if err != nil { + return false, err } - v := reflect.ValueOf(snipWidgetsConfig) - replaceRecursive(&v, dict) - - wd = filepath.Dir(filename) - for i := range snipWidgetsConfig { - snipWidgetsConfig[i].WorkDir = wd - snipWidgetsConfig[i].File = filename - snipWidgetsConfig[i].Index = i - snipWidgetsConfig[i].IncludeStack = append(widget.IncludeStack, widget.Name) - json.Unmarshal(tpls, &snipWidgetsConfig[i].Templates) - - snipEvents := snipWidgetsConfig[i].Events - for i := range snipEvents { - if snipEvents[i].WorkDir == "" { - snipEvents[i].WorkDir = wd - } - } + dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n") + } + + v := reflect.ValueOf(snippetConfig.Widgets) + replaceRecursive(&v, dict) - for _, e := range widget.Events { - if e.Override { - sort.Strings(e.Modifiers) + tpls, _ := json.Marshal(widget.Templates) + + wd = filepath.Dir(filename) + for i := range snippetConfig.Widgets { + snippetConfig.Widgets[i].WorkDir = wd + snippetConfig.Widgets[i].File = filename + snippetConfig.Widgets[i].Index = i + snippetConfig.Widgets[i].IncludeStack = append(widget.IncludeStack, widget.Name) + json.Unmarshal(tpls, &snippetConfig.Widgets[i].Templates) + + snipEvents := snippetConfig.Widgets[i].Events + for i := range snipEvents { + if snipEvents[i].WorkDir == "" { + snipEvents[i].WorkDir = wd + } + } - ne := make([]ygs.WidgetEventConfig, 0, len(snipEvents)) + for _, e := range widget.Events { + if e.Override { + sort.Strings(e.Modifiers) - for _, se := range snipEvents { - sort.Strings(se.Modifiers) + ne := make([]ygs.WidgetEventConfig, 0, len(snipEvents)) - if e.Button == se.Button && - e.Name == se.Name && - e.Instance == se.Instance && - reflect.DeepEqual(e.Modifiers, se.Modifiers) { + for _, se := range snipEvents { + sort.Strings(se.Modifiers) - continue - } + if e.Button == se.Button && + e.Name == se.Name && + e.Instance == se.Instance && + reflect.DeepEqual(e.Modifiers, se.Modifiers) { - ne = append(ne, se) + continue } - snipEvents = append(ne, e) - } else { - snipEvents = append(snipEvents, e) + + ne = append(ne, se) } + snipEvents = append(ne, e) + } else { + snipEvents = append(snipEvents, e) } - - snipWidgetsConfig[i].Events = snipEvents } - i := wi - config.Widgets = append(config.Widgets[:i], config.Widgets[i+1:]...) - config.Widgets = append(config.Widgets[:i], append(snipWidgetsConfig, config.Widgets[i:]...)...) - - wi-- - - continue WIDGET + snippetConfig.Widgets[i].Events = snipEvents } - if err := widget.Validate(); err != nil { - setError(widget, err, true) + config.Widgets = append(config.Widgets[:wi], config.Widgets[wi+1:]...) + config.Widgets = append(config.Widgets[:wi], append(snippetConfig.Widgets, config.Widgets[wi:]...)...) - continue WIDGET - } + return true, nil } - - return &config, nil + return false, nil } func setError(widget *ygs.WidgetConfig, err error, trimLineN bool) { diff --git a/ygs/widget_config.go b/ygs/widget_config.go index 1dd32e4..5df552d 100644 --- a/ygs/widget_config.go +++ b/ygs/widget_config.go @@ -13,6 +13,7 @@ type WidgetConfig struct { WorkDir string `yaml:"-"` Index int `yaml:"-"` File string `yaml:"-"` + Variables map[string]string `yaml:"variables"` Params map[string]interface{} `yaml:",inline"` From d55ee7df938f7f84904073f3ff66bc69da3108d5 Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Fri, 15 May 2020 16:57:38 +0300 Subject: [PATCH 33/34] Add ygs-snippets url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c311020..88f9bcb 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Yet Another i3status replacement written in Go. - Shell scripting widgets and events handlers. - Wrapping other status programs (i3status, py3status, conky, etc.). - Different widgets on different workspaces. -- Snippets. +- [Snippets](https://github.com/burik666/ygs-snippets). - Templates for widgets outputs. - Update widget via http/websocket requests. - Update widget by POSIX Real-Time Signals (SIGRTMIN-SIGRTMAX). From 5a8d2248b223e999fb96b64079c122f6243857ff Mon Sep 17 00:00:00 2001 From: Andrey Burov Date: Fri, 15 May 2020 16:58:26 +0300 Subject: [PATCH 34/34] Upd default config --- README.md | 14 +++++++------- main.go | 2 +- yagostatus.yml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 88f9bcb..0b88299 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ widgets: [{ "color": "#ffffff", "separator": true, - "separator_block_width": 20 + "separator_block_width": 21 }] ``` ## Widgets @@ -311,7 +311,7 @@ bindsym XF86AudioMute exec amixer -q set Master toggle; exec pkill -SIGRTMIN+1 y [{ "markup": "pango", "separator": true, - "separator_block_width": 20 + "separator_block_width": 21 }] ``` @@ -338,7 +338,7 @@ Requires [jq](https://stedolan.github.com/jq/) for json parsing. templates: > [{ "separator": true, - "separator_block_width": 20 + "separator_block_width": 21 }] ``` @@ -394,16 +394,16 @@ conky.text = [[ { ${lua_parse cpu cpu1} , "min_width": "100%", "align": "right", "separator": false }, { ${lua_parse cpu cpu2} , "min_width": "100%", "align": "right", "separator": false }, { ${lua_parse cpu cpu3} , "min_width": "100%", "align": "right", "separator": false }, -{ ${lua_parse cpu cpu4} , "min_width": "100%", "align": "right", "separator": true, "separator_block_width":20 }, +{ ${lua_parse cpu cpu4} , "min_width": "100%", "align": "right", "separator": true, "separator_block_width":21 }, { "full_text": "RAM:", "color": "\#2e9ef4", "separator": false }, -{ "full_text": "${mem} / ${memeasyfree}", "color": ${if_match ${memperc}<80}"\#ffffff"${else}"\#ff0000"${endif}, "separator": true, "separator_block_width":20 }, +{ "full_text": "${mem} / ${memeasyfree}", "color": ${if_match ${memperc}<80}"\#ffffff"${else}"\#ff0000"${endif}, "separator": true, "separator_block_width":21 }, { "full_text": "sda:", "color": "\#2e9ef4", "separator": false }, -{ "full_text": "▼ ${diskio_read sda} ▲ ${diskio_write sda}", "color": "\#ffffff", "separator": true, "separator_block_width":20 }, +{ "full_text": "▼ ${diskio_read sda} ▲ ${diskio_write sda}", "color": "\#ffffff", "separator": true, "separator_block_width":21 }, { "full_text": "eth0:", "color": "\#2e9ef4", "separator": false }, -{ "full_text": "▼ ${downspeed eth0} ▲ ${upspeed eth0}", "color": "\#ffffff", "separator": true, "separator_block_width":20 } +{ "full_text": "▼ ${downspeed eth0} ▲ ${upspeed eth0}", "color": "\#ffffff", "separator": true, "separator_block_width":21 } ] diff --git a/main.go b/main.go index 40385cb..bb2422a 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ widgets: [{ "color": "#ffffff", "separator": true, - "separator_block_width": 20 + "separator_block_width": 21 }] `) diff --git a/yagostatus.yml b/yagostatus.yml index 5dee489..15d6cc9 100644 --- a/yagostatus.yml +++ b/yagostatus.yml @@ -20,5 +20,5 @@ widgets: [{ "color": "#ffffff", "separator": true, - "separator_block_width": 20 + "separator_block_width": 21 }]