diff --git a/README.md b/README.md
index db005d3..0b88299 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,6 @@ Yet Another i3status replacement written in Go.
[](https://github.com/burik666/yagostatus)
[](https://github.com/burik666/yagostatus/blob/master/LICENSE)
-[](https://app.codacy.com/app/burik666/yagostatus?utm_source=github.com&utm_medium=referral&utm_content=burik666/yagostatus&utm_campaign=badger)
-[](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)
[](https://github.com/burik666/yagostatus/wiki/Conky)
@@ -15,26 +13,22 @@ 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](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).
## Installation
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.
-`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
@@ -60,12 +54,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
- }
+ "separator_block_width": 21
+ }]
```
## Widgets
@@ -100,16 +94,19 @@ 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.
* `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`).
+ 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).
* `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
@@ -125,10 +122,15 @@ Example:
"name": "ch"
}
]
- template: >
- {
- "color": "#0000ff"
- }
+ templates: >
+ [
+ {
+ "color": "#ff8000"
+ },
+ {
+ "color": "#ff3030"
+ }
+ ]
events:
- button: 1
command: /usr/bin/firefox
@@ -146,6 +148,33 @@ Example:
name: ch
```
+### Snippets
+
+Yagostatus supports the inclusion of snippets from files.
+```yml
+ - widget: $ygs-snippets/snip.yaml
+ msg: hello world
+ color: #00ff00
+```
+
+`ygs-snippets/snip.yaml`:
+```yml
+variables:
+ msg: "default messsage"
+ color: #ffffff
+widgets:
+ - widget: static
+ blocks: >
+ [
+ {
+ "full_text": "message: ${msg}",
+ "color": "${color}"
+ }
+ ]
+```
+
+`ygs-snippets/snip.yaml` - relative path from the current file.
+
### Widget `clock`
@@ -160,8 +189,20 @@ 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).
+- `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`).
- `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`.
+
+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:
+
+ pkill -SIGRTMIN+1 yagostatus
### Widget `wrapper`
@@ -170,6 +211,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`
@@ -183,22 +225,65 @@ 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
+### 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:
+```
+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 +295,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
@@ -221,12 +307,12 @@ Send an empty array to clear:
- button: 5
command: amixer -q set Master 3%-
- template: >
- {
+ templates: >
+ [{
"markup": "pango",
"separator": true,
- "separator_block_width": 20
- }
+ "separator_block_width": 21
+ }]
```
### Weather
@@ -249,11 +335,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
- }
+ "separator_block_width": 21
+ }]
```
### Conky
@@ -308,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/go.mod b/go.mod
index 270d591..00c75d5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,11 @@
module github.com/burik666/yagostatus
+go 1.14
+
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
+ 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 eb17012..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/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=
-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=
+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-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 9e84eee..b6484f9 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -1,71 +1,28 @@
package config
import (
- "encoding/json"
"io/ioutil"
- "strings"
+ "os"
+ "path/filepath"
+ "syscall"
"github.com/burik666/yagostatus/ygs"
-
- "github.com/pkg/errors"
- "gopkg.in/yaml.v2"
)
// Config represents the main configuration.
type Config struct {
- 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 _, e := range c.Events {
- if err := e.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"`
- Output bool `yaml:"output,omitempty"`
+ Signals struct {
+ StopSignal syscall.Signal `yaml:"stop"`
+ ContSignal syscall.Signal `yaml:"cont"`
+ } `yaml:"signals"`
+ Variables map[string]interface{} `yaml:"variables"`
+ Widgets []ygs.WidgetConfig `yaml:"widgets"`
}
-// 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 errors.Errorf("Unknown '%s' modifier", mod)
- }
- }
- return nil
+// 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.
@@ -74,45 +31,15 @@ func LoadFile(filename string) (*Config, error) {
if err != nil {
return nil, err
}
- return Parse(data)
+
+ return parse(data, filepath.Dir(filename), filepath.Base(filename))
}
// 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 {
+func Parse(data []byte, source string) (*Config, error) {
+ wd, err := os.Getwd()
+ if err != nil {
return nil, err
}
-
- 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 {
- return nil, err
- }
- }
-
- widget.Params = params
- if err := widget.Validate(); err != nil {
- return nil, err
- }
-
- delete(params, "widget")
- delete(params, "workspaces")
- delete(params, "template")
- delete(params, "events")
- }
- return &config, nil
+ return parse(data, wd, source)
}
diff --git a/internal/pkg/config/parser.go b/internal/pkg/config/parser.go
new file mode 100644
index 0000000..0ea0c73
--- /dev/null
+++ b/internal/pkg/config/parser.go
@@ -0,0 +1,306 @@
+package config
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "syscall"
+
+ "github.com/burik666/yagostatus/ygs"
+
+ "gopkg.in/yaml.v2"
+)
+
+func parse(data []byte, workdir string, source 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)
+ }
+
+ for wi := range config.Widgets {
+ config.Widgets[wi].File = source
+ 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]
+
+ params := make(map[string]interface{})
+ for k, v := range config.Widgets[wi].Params {
+ params[strings.ToLower(k)] = v
+ }
+
+ config.Widgets[wi].Params = params
+
+ if widget.WorkDir == "" {
+ 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)
+ 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")
+ }
+
+ ok, err := parseSnippet(&config, wi, params)
+ if err != nil {
+ setError(widget, err, false)
+
+ continue WIDGET
+ }
+
+ if ok {
+ wi--
+ continue WIDGET
+ }
+
+ if err := widget.Validate(); err != nil {
+ setError(widget, err, true)
+
+ continue WIDGET
+ }
+ }
+
+ return &config, nil
+}
+
+func parseSnippet(config *Config, wi int, params map[string]interface{}) (bool, error) {
+ widget := config.Widgets[wi]
+
+ 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)
+
+ return false, fmt.Errorf("recursive include: '%s'", strings.Join(stack, " -> "))
+ }
+ }
+
+ wd := widget.WorkDir
+
+ filename := widget.Name[1:]
+ if !filepath.IsAbs(filename) {
+ filename = wd + "/" + filename
+ }
+
+ data, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return false, err
+ }
+
+ var snippetConfig SnippetConfig
+ if err := yaml.Unmarshal(data, &snippetConfig); err != nil {
+ return false, err
+ }
+
+ for k, v := range snippetConfig.Variables {
+ if _, ok := params[k]; !ok {
+ params[k] = v
+ }
+ }
+
+ 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)
+ }
+
+ vb, err := json.Marshal(v)
+ if err != nil {
+ return false, err
+ }
+
+ var vraw ygs.Vary
+
+ err = json.Unmarshal(vb, &vraw)
+ if err != nil {
+ return false, err
+ }
+
+ dict[fmt.Sprintf("${%s}", k)] = strings.TrimRight(vraw.String(), "\n")
+ }
+
+ v := reflect.ValueOf(snippetConfig.Widgets)
+ replaceRecursive(&v, dict)
+
+ 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
+ }
+ }
+
+ 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)
+ }
+ }
+
+ snippetConfig.Widgets[i].Events = snipEvents
+ }
+
+ config.Widgets = append(config.Widgets[:wi], config.Widgets[wi+1:]...)
+ config.Widgets = append(config.Widgets[:wi], append(snippetConfig.Widgets, config.Widgets[wi:]...)...)
+
+ return true, nil
+ }
+ return false, 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 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 {
+ vn := reflect.New(vv.Type()).Elem()
+ vn.SetString(st)
+ *v = vn
+ }
+ }
+}
diff --git a/internal/pkg/executor/executor.go b/internal/pkg/executor/executor.go
new file mode 100644
index 0000000..dce241d
--- /dev/null
+++ b/internal/pkg/executor/executor.go
@@ -0,0 +1,216 @@
+package executor
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "regexp"
+ "strings"
+ "syscall"
+
+ "github.com/burik666/yagostatus/internal/pkg/logger"
+ "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
+ header *ygs.I3BarHeader
+}
+
+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.Env = os.Environ()
+ e.cmd.SysProcAttr = &syscall.SysProcAttr{
+ Setpgid: true,
+ Pgid: 0,
+ }
+
+ return e, nil
+}
+
+func (e *Executor) SetWD(wd string) {
+ if e.cmd != nil {
+ e.cmd.Dir = wd
+ }
+}
+
+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
+ }
+
+ 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)
+
+ 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 || !isJSON || format == OutputFormatText {
+ _, err := io.Copy(ioutil.Discard, outreader)
+ if err != nil {
+ return err
+ }
+
+ if buf.Len() > 0 {
+ c <- []ygs.I3BarBlock{
+ {
+ FullText: strings.Trim(buf.String(), "\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 {
+ e.header = &header
+
+ _, err := decoder.Token()
+ if err != nil {
+ return err
+ }
+ } 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 nil
+ }
+
+ return err
+ }
+ c <- blocks
+ }
+}
+
+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 && e.cmd.Process != 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
+}
+
+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
+}
+
+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/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/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/main.go b/main.go
index 0359d5b..bb2422a 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(`
@@ -29,64 +29,97 @@ 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
- }
+ "separator_block_width": 21
+ }]
`)
func main() {
- log.SetFlags(log.Ldate + log.Ltime + log.Lshortfile)
+ logger := logger.New(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()
if *versionFlag {
- fmt.Printf("YaGoStatus %s\n", Version)
+ logger.Infof("YaGoStatus %s", Version)
return
}
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)
+
+ 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)
}
}
+
+ 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{}
}
}
- 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 {
+ logger.Errorf("Failed to load config: %s", cfgError)
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() {
- yaGoStatus.Run()
- stopsignals <- syscall.SIGTERM
+ 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() {
+ if err := yaGoStatus.Run(); err != nil {
+ logger.Errorf("Failed to run yagostatus: %s", err)
+ }
+ shutdownsignals <- syscall.SIGTERM
}()
- <-stopsignals
- yaGoStatus.Stop()
+ <-shutdownsignals
+
+ if err := yaGoStatus.Shutdown(); err != nil {
+ logger.Errorf("Failed to shutdown yagostatus: %s", err)
+ }
+
+ logger.Infof("exit")
}
diff --git a/widgets/blank.go b/widgets/blank.go
index 37b13d5..8bb237a 100644
--- a/widgets/blank.go
+++ b/widgets/blank.go
@@ -2,14 +2,22 @@
package widgets
import (
+ "github.com/burik666/yagostatus/internal/pkg/logger"
"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{}, wlogger logger.Logger) (ygs.Widget, error) {
return &BlankWidget{}, nil
}
@@ -19,11 +27,21 @@ 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) error {
+ return nil
+}
-// Stop shutdowns the widget.
-func (w *BlankWidget) Stop() {}
+// Stop stops the widdget.
+func (w *BlankWidget) Stop() error {
+ return nil
+}
-func init() {
- ygs.RegisterWidget("blank", NewBlankWidget)
+// Continue continues the widdget.
+func (w *BlankWidget) Continue() error {
+ return nil
+}
+
+// Shutdown shutdowns the widget.
+func (w *BlankWidget) Shutdown() error {
+ return nil
}
diff --git a/widgets/clock.go b/widgets/clock.go
index 32ea8ed..6ac4f3a 100644
--- a/widgets/clock.go
+++ b/widgets/clock.go
@@ -1,60 +1,55 @@
package widgets
import (
- "errors"
"time"
+ "github.com/burik666/yagostatus/internal/pkg/logger"
"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
+ BlankWidget
+
+ params ClockWidgetParams
}
-// NewClockWidget returns a new ClockWidget.
-func NewClockWidget(params map[string]interface{}) (ygs.Widget, error) {
- w := &ClockWidget{}
+func init() {
+ ygs.RegisterWidget("clock", NewClockWidget, ClockWidgetParams{
+ Interval: 1,
+ Format: "Jan _2 Mon 15:04:05",
+ })
+}
- v, ok := params["format"]
- if !ok {
- return nil, errors.New("missing 'format' setting")
+// NewClockWidget returns a new ClockWidget.
+func NewClockWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) {
+ w := &ClockWidget{
+ params: params.(ClockWidgetParams),
}
- w.format = v.(string)
- v, ok = params["interval"]
- if ok {
- w.interval = time.Duration(v.(int)) * time.Second
- } else {
- w.interval = time.Second
- }
return w, nil
}
// Run starts the main loop.
func (w *ClockWidget) Run(c chan<- []ygs.I3BarBlock) error {
- ticker := time.NewTicker(w.interval)
res := []ygs.I3BarBlock{
- ygs.I3BarBlock{},
- }
- res[0].FullText = time.Now().Format(w.format)
- c <- res
- for {
- select {
- case t := <-ticker.C:
- res[0].FullText = t.Format(w.format)
- c <- res
- }
+ {},
}
-}
+ res[0].FullText = time.Now().Format(w.params.Format)
-// Event processes the widget events.
-func (w *ClockWidget) Event(event ygs.I3BarClickEvent) {}
+ c <- res
-// Stop shutdowns the widget.
-func (w *ClockWidget) Stop() {}
+ 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
+ }
-func init() {
- ygs.RegisterWidget("clock", NewClockWidget)
+ return nil
}
diff --git a/widgets/exec.go b/widgets/exec.go
index 69884a6..01dc47a 100644
--- a/widgets/exec.go
+++ b/widgets/exec.go
@@ -1,98 +1,238 @@
package widgets
import (
- "encoding/json"
"errors"
- "log"
+ "fmt"
"os"
- "os/exec"
- "strings"
+ "os/signal"
+ "sync"
+ "syscall"
"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"
)
+// ExecWidgetParams are widget parameters.
+type ExecWidgetParams struct {
+ Command string
+ Interval int
+ Retry *int
+ Silent bool
+ EventsUpdate bool `yaml:"events_update"`
+ Signal *int
+ OutputFormat executor.OutputFormat `yaml:"output_format"`
+ WorkDir string
+}
+
// ExecWidget implements the exec widget.
type ExecWidget struct {
- command string
- interval time.Duration
- eventsUpdate bool
- c chan<- []ygs.I3BarBlock
+ BlankWidget
+
+ params ExecWidgetParams
+
+ logger logger.Logger
+
+ signal os.Signal
+ c chan<- []ygs.I3BarBlock
+ upd chan struct{}
+ tickerC *chan struct{}
+ env []string
+
+ outputWG sync.WaitGroup
+}
+
+func init() {
+ ygs.RegisterWidget("exec", NewExecWidget, ExecWidgetParams{})
}
// NewExecWidget returns a new ExecWidget.
-func NewExecWidget(params map[string]interface{}) (ygs.Widget, error) {
- w := &ExecWidget{}
+func NewExecWidget(params interface{}, wlogger logger.Logger) (ygs.Widget, error) {
+ w := &ExecWidget{
+ params: params.(ExecWidgetParams),
+ logger: wlogger,
+ }
- v, ok := params["command"]
- if !ok {
- return nil, errors.New("missing 'command' setting")
+ if len(w.params.Command) == 0 {
+ return nil, errors.New("missing 'command'")
}
- w.command = v.(string)
- v, ok = params["interval"]
- if !ok {
- return nil, errors.New("missing 'interval' setting")
+ 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")
}
- w.interval = time.Second * time.Duration(v.(int))
- v, ok = params["events_update"]
- if ok {
- w.eventsUpdate = v.(bool)
- } else {
- w.eventsUpdate = false
+ if w.params.Signal != nil {
+ sig := *w.params.Signal
+ 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)
}
+ w.upd = make(chan struct{}, 1)
+ w.upd <- struct{}{}
+
return w, nil
}
func (w *ExecWidget) exec() error {
- cmd := exec.Command("sh", "-c", w.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 {
- log.Printf("Failed to parse output: %s", err)
- blocks = append(blocks, ygs.I3BarBlock{FullText: strings.Trim(string(output), "\n ")})
+ exc.SetWD(w.params.WorkDir)
+
+ exc.AddEnv(w.env...)
+
+ c := make(chan []ygs.I3BarBlock)
+
+ defer close(c)
+
+ w.outputWG.Add(1)
+ go (func() {
+ defer w.outputWG.Done()
+
+ for {
+ blocks, ok := <-c
+ if !ok {
+ return
+ }
+ w.c <- blocks
+ w.setEnv(blocks)
+ }
+ })()
+
+ 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 {
+ go (func() {
+ time.Sleep(time.Second * time.Duration(*w.params.Retry))
+ w.upd <- struct{}{}
+ w.resetTicker()
+ })()
+ }
+
+ return fmt.Errorf("process exited unexpectedly: %s", state.String())
+ }
}
- w.c <- blocks
- return nil
+ return err
}
// Run starts the main loop.
func (w *ExecWidget) Run(c chan<- []ygs.I3BarBlock) error {
w.c = c
- if w.interval == 0 {
- return w.exec()
+ if w.params.Interval == 0 && w.signal == nil && w.params.Retry == nil {
+ err := w.exec()
+ if w.params.Silent {
+ if err != nil {
+ w.logger.Errorf("exec failed: %s", err)
+ }
+
+ return nil
+ }
+
+ return err
+ }
+
+ if w.params.Interval > 0 {
+ w.resetTicker()
}
- ticker := time.NewTicker(w.interval)
+ if w.params.Interval == -1 {
+ go (func() {
+ for {
+ w.upd <- struct{}{}
+ }
+ })()
+ }
+
+ if w.signal != nil {
+ sigc := make(chan os.Signal, 1)
+ signal.Notify(sigc, w.signal)
+
+ go (func() {
+ for {
+ <-sigc
+ w.upd <- struct{}{}
+ }
+ })()
+ }
- for ; true; <-ticker.C {
+ for range w.upd {
err := w.exec()
if err != nil {
- return err
+ if !w.params.Silent {
+ w.outputWG.Wait()
+
+ c <- []ygs.I3BarBlock{{
+ FullText: err.Error(),
+ Color: "#ff0000",
+ }}
+ }
+
+ w.logger.Errorf("exec failed: %s", err)
}
}
+
return nil
}
// Event processes the widget events.
-func (w *ExecWidget) Event(event ygs.I3BarClickEvent) {
- if w.eventsUpdate {
- w.exec()
+func (w *ExecWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error {
+ w.setEnv(blocks)
+
+ if w.params.EventsUpdate {
+ w.upd <- struct{}{}
}
+
+ return nil
}
-// Stop shutdowns the widget.
-func (w *ExecWidget) Stop() {}
+func (w *ExecWidget) setEnv(blocks []ygs.I3BarBlock) {
+ env := make([]string, 0)
-func init() {
- ygs.RegisterWidget("exec", NewExecWidget)
+ for i, block := range blocks {
+ suffix := ""
+ if i > 0 {
+ suffix = fmt.Sprintf("_%d", i)
+ }
+ env = append(env, block.Env(suffix)...)
+ }
+
+ w.env = env
+}
+
+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{}{}
+ }
+ }
+ })()
+ }
}
diff --git a/widgets/http.go b/widgets/http.go
index 4a01a4c..39cdd22 100644
--- a/widgets/http.go
+++ b/widgets/http.go
@@ -1,130 +1,235 @@
package widgets
import (
+ "context"
"encoding/json"
"errors"
"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"
)
+// HTTPWidgetParams are widget parameters.
+type HTTPWidgetParams struct {
+ Network string
+ Listen string
+ Path string
+}
+
// HTTPWidget implements the http server widget.
type HTTPWidget struct {
- c chan<- []ygs.I3BarBlock
- conn *websocket.Conn
- listen string
- path string
+ BlankWidget
+
+ params HTTPWidgetParams
+
+ logger logger.Logger
+
+ c chan<- []ygs.I3BarBlock
+ instance *httpInstance
+
+ clients map[*websocket.Conn]chan interface{}
+ cm sync.RWMutex
+}
+
+type httpInstance 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{
+ Network: "tcp",
+ })
+
+ instances = make(map[string]*httpInstance, 1)
}
// 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{}, wlogger logger.Logger) (ygs.Widget, error) {
+ w := &HTTPWidget{
+ params: params.(HTTPWidgetParams),
+ logger: wlogger,
+ }
+
+ if len(w.params.Listen) == 0 {
+ return nil, errors.New("missing 'listen'")
}
- w.listen = v.(string)
- v, ok = params["path"]
- if !ok {
- return nil, errors.New("missing 'path' setting")
+ if len(w.params.Path) == 0 {
+ return nil, errors.New("missing 'path'")
}
- w.path = v.(string)
- if serveMuxes == nil {
- serveMuxes = make(map[string]*http.ServeMux, 1)
+ if w.params.Network != "tcp" && w.params.Network != "unix" {
+ return nil, errors.New("invalid 'net' (may be 'tcp' or 'unix')")
}
+ 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)
+ }
+ } else {
+ mux := http.NewServeMux()
+ instance = &httpInstance{
+ mux: mux,
+ paths: make(map[string]struct{}, 1),
+ server: &http.Server{
+ Addr: w.params.Listen,
+ Handler: mux,
+ },
+ }
+
+ instances[w.params.Listen] = instance
+ w.instance = instance
+ }
+
+ instance.mux.HandleFunc(w.params.Path, w.httpHandler)
+ instance.paths[instanceKey] = struct{}{}
+
+ w.clients = make(map[*websocket.Conn]chan interface{})
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.instance == nil {
return nil
}
- mux = http.NewServeMux()
- mux.HandleFunc(w.path, w.httpHandler)
- httpServer := &http.Server{
- Addr: w.listen,
- Handler: mux,
+ l, err := net.Listen(w.params.Network, w.params.Listen)
+ if err != nil {
+ return err
}
- serveMuxes[w.listen] = mux
- return httpServer.ListenAndServe()
+
+ 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.
-func (w *HTTPWidget) Event(event ygs.I3BarClickEvent) {
- if w.conn != nil {
- websocket.JSON.Send(w.conn, event)
+func (w *HTTPWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error {
+ return w.broadcast(event)
+}
+
+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)
- 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
}
+
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)
}
+
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 {
+ w.logger.Errorf("failed to write response: %s", err)
+ }
}
func (w *HTTPWidget) wsHandler(ws *websocket.Conn) {
- var messages []ygs.I3BarBlock
- w.conn = ws
+ defer ws.Close()
+
+ ch := make(chan interface{})
+
+ w.cm.RLock()
+ w.clients[ws] = ch
+ w.cm.RUnlock()
+
+ var blocks []ygs.I3BarBlock
+
+ go func() {
+ for {
+ msg, ok := <-ch
+ if !ok {
+ return
+ }
+
+ if err := websocket.JSON.Send(ws, msg); err != nil {
+ w.logger.Errorf("failed to send msg: %s", err)
+ }
+ }
+ }()
+
for {
- if err := websocket.JSON.Receive(ws, &messages); err != nil {
+ if err := websocket.JSON.Receive(ws, &blocks); err != nil {
if err == io.EOF {
- if w.conn == ws {
- w.c <- nil
- w.conn = nil
- }
break
}
- log.Printf("%s", err)
- }
- if w.conn != ws {
+ w.logger.Errorf("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)
}
-// Stop shutdowns the widget.
-func (w *HTTPWidget) Stop() {}
+func (w *HTTPWidget) broadcast(msg interface{}) error {
+ w.cm.RLock()
+ defer w.cm.RUnlock()
-func init() {
- ygs.RegisterWidget("http", NewHTTPWidget)
+ for _, ch := range w.clients {
+ ch <- msg
+ }
+
+ return nil
}
diff --git a/widgets/static.go b/widgets/static.go
index b80e34d..55d3aa3 100644
--- a/widgets/static.go
+++ b/widgets/static.go
@@ -4,24 +4,39 @@ import (
"encoding/json"
"errors"
+ "github.com/burik666/yagostatus/internal/pkg/logger"
"github.com/burik666/yagostatus/ygs"
)
+// StaticWidgetParams are widget parameters.
+type StaticWidgetParams struct {
+ Blocks string
+}
+
// StaticWidget implements a static widget.
type StaticWidget struct {
+ BlankWidget
+
+ 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{}, wlogger logger.Logger) (ygs.Widget, error) {
+ w := &StaticWidget{
+ params: params.(StaticWidgetParams),
+ }
- v, ok := params["blocks"]
- if !ok {
- return nil, errors.New("missing 'blocks' setting")
+ if len(w.params.Blocks) == 0 {
+ return nil, errors.New("missing 'blocks'")
}
- 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
}
@@ -33,13 +48,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) {}
-
-// 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..9ec5195 100644
--- a/widgets/wrapper.go
+++ b/widgets/wrapper.go
@@ -1,104 +1,146 @@
package widgets
import (
- "bufio"
"encoding/json"
"errors"
+ "fmt"
"io"
- "os"
- "os/exec"
- "regexp"
+ "syscall"
+ "github.com/burik666/yagostatus/internal/pkg/executor"
+ "github.com/burik666/yagostatus/internal/pkg/logger"
"github.com/burik666/yagostatus/ygs"
)
+// WrapperWidgetParams are widget parameters.
+type WrapperWidgetParams struct {
+ Command string
+ WorkDir string
+}
+
// WrapperWidget implements the wrapper of other status commands.
type WrapperWidget struct {
- stdin io.WriteCloser
- cmd *exec.Cmd
- command string
- args []string
+ params WrapperWidgetParams
+
+ logger logger.Logger
+
+ exc *executor.Executor
+ stdin io.WriteCloser
+
+ eventBracketWritten bool
+}
+
+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{}, wlogger logger.Logger) (ygs.Widget, error) {
+ w := &WrapperWidget{
+ params: params.(WrapperWidgetParams),
+ logger: wlogger,
+ }
+
+ if len(w.params.Command) == 0 {
+ return nil, errors.New("missing 'command'")
+ }
- v, ok := params["command"]
- if !ok {
- return nil, errors.New("missing 'command' setting")
+ exc, err := executor.Exec(w.params.Command)
+ if err != nil {
+ return nil, err
}
- r := regexp.MustCompile("'.+'|\".+\"|\\S+")
- m := r.FindAllString(v.(string), -1)
- w.command = m[0]
- w.args = m[1:]
+
+ exc.SetWD(w.params.WorkDir)
+
+ w.exc = exc
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()
- if err != nil {
- return err
- }
+ var err error
- 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
+ defer w.stdin.Close()
+
+ err = w.exc.Run(w.logger, 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())
+ }
}
- w.stdin.Write([]byte("["))
- reader := bufio.NewReader(stdout)
- decoder := json.NewDecoder(reader)
+ return err
+}
- var firstMessage interface{}
- if err := decoder.Decode(&firstMessage); err != nil {
- return err
+// Event processes the widget events.
+func (w *WrapperWidget) Event(event ygs.I3BarClickEvent, blocks []ygs.I3BarBlock) error {
+ if w.stdin == nil {
+ return nil
}
- 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
+
+ if header := w.exc.I3BarHeader(); header != nil && header.ClickEvents {
+ if !w.eventBracketWritten {
+ w.eventBracketWritten = true
+ if _, err := w.stdin.Write([]byte("[")); err != nil {
+ return err
+ }
}
- c <- blocks
- }
- for {
- var blocks []ygs.I3BarBlock
- err := decoder.Decode(&blocks)
+ msg, err := json.Marshal(event)
if err != nil {
- if err == io.EOF {
- break
- }
return err
}
- c <- blocks
+
+ msg = append(msg, []byte(",\n")...)
+
+ if _, err := w.stdin.Write(msg); err != nil {
+ return err
+ }
}
- return w.cmd.Wait()
+
+ return nil
}
-// 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"))
+// Stop stops the widdget.
+func (w *WrapperWidget) Stop() error {
+ if header := w.exc.I3BarHeader(); header != nil {
+ if header.StopSignal != 0 {
+ return w.exc.Signal(syscall.Signal(header.StopSignal))
+ }
+ }
+
+ return w.exc.Signal(syscall.SIGSTOP)
}
-// Stop shutdowns the widget.
-func (w *WrapperWidget) Stop() {}
+// Continue continues the widdget.
+func (w *WrapperWidget) Continue() error {
+ if header := w.exc.I3BarHeader(); header != nil {
+ if header.ContSignal != 0 {
+ return w.exc.Signal(syscall.Signal(header.ContSignal))
+ }
+ }
+
+ return w.exc.Signal(syscall.SIGCONT)
+}
-func init() {
- ygs.RegisterWidget("wrapper", NewWrapperWidget)
+// Shutdown shutdowns the widget.
+func (w *WrapperWidget) Shutdown() error {
+ if w.exc != nil {
+ 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 b217358..25d8cb1 100644
--- a/yagostatus.go
+++ b/yagostatus.go
@@ -2,87 +2,121 @@ package main
import (
"bufio"
- "bytes"
"encoding/json"
"fmt"
"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/internal/pkg/logger"
_ "github.com/burik666/yagostatus/widgets"
"github.com/burik666/yagostatus/ygs"
- "go.i3wm.org/i3"
+ "go.i3wm.org/i3/v4"
)
+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 []config.WidgetConfig
+ widgets []widgetContainer
upd chan int
workspaces []i3.Workspace
visibleWorkspaces []string
+
+ cfg config.Config
+
+ logger logger.Logger
}
// NewYaGoStatus returns a new YaGoStatus instance.
-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
- }
+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) {
- 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),
- })
- if err != nil {
- log.Fatalf("Failed to create error widget: %s", err)
- }
-
- status.AddWidget(widget, config.WidgetConfig{})
+ status.addWidget(ygs.ErrorWidget(text))
}
-// AddWidget adds widget to statusbar.
-func (status *YaGoStatus) AddWidget(widget ygs.Widget, config config.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 {
- 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)
- cmd.Stderr = os.Stderr
- cmd.Env = append(os.Environ(),
+func (status *YaGoStatus) processWidgetEvents(wi int, outputIndex int, event ygs.I3BarClickEvent) error {
+ defer (func() {
+ if r := recover(); r != nil {
+ status.widgets[wi].logger.Errorf("widget event panic: %s", r)
+ debug.PrintStack()
+ status.widgets[wi].output = []ygs.I3BarBlock{{
+ FullText: "widget panic",
+ Color: "#ff0000",
+ }}
+ }
+
+ 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.widgets[wi].config.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) {
+ exc, err := executor.Exec("sh", "-c", widgetEvent.Command)
+ if err != nil {
+ return err
+ }
+
+ exc.SetWD(widgetEvent.WorkDir)
+
+ 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),
@@ -94,209 +128,308 @@ 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()
+
+ block := status.widgets[wi].output[outputIndex]
+ block.Name = event.Name
+ block.Instance = event.Instance
+
+ exc.AddEnv(block.Env("")...)
+ stdin, err := exc.Stdin()
if err != nil {
return err
}
- eventJSON, _ := json.Marshal(event)
- cmdStdin.Write(eventJSON)
- cmdStdin.Write([]byte("\n"))
- cmdStdin.Close()
- cmdOutput, err := cmd.Output()
+ eventJSON, err := json.Marshal(event)
if err != nil {
return err
}
- if we.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")
- }
- status.upd <- widgetIndex
+
+ 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.widgets[wi].logger,
+ status.widgets[wi].ch,
+ executor.OutputFormat(widgetEvent.OutputFormat),
+ )
+ if err != nil {
+ return err
}
}
}
+
return nil
}
-func (status *YaGoStatus) eventReader() {
+func (status *YaGoStatus) addWidgetOutput(wi int, blocks []ygs.I3BarBlock) {
+ output := make([]ygs.I3BarBlock, len(blocks))
+
+ tplc := len(status.widgets[wi].config.Templates)
+ for blockIndex := range blocks {
+ block := blocks[blockIndex]
+
+ if tplc == 1 {
+ block.Apply(status.widgets[wi].config.Templates[0])
+ } else {
+ if blockIndex < tplc {
+ block.Apply(status.widgets[wi].config.Templates[blockIndex])
+ }
+ }
+
+ 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.widgets[wi].output = output
+
+ status.upd <- wi
+}
+
+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
}
+
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)
+ 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)
- status.widgetsOutput[widgetIndex][outputIndex] = ygs.I3BarBlock{
- FullText: fmt.Sprintf("Event error: %s", err.Error()),
+
+ if err := status.processWidgetEvents(wi, outputIndex, e); err != nil {
+ status.widgets[wi].logger.Errorf("event error: %s", err)
+
+ status.widgets[wi].output[outputIndex] = ygs.I3BarBlock{
+ FullText: fmt.Sprintf("event error: %s", err.Error()),
Color: "#ff0000",
Name: event.Name,
Instance: event.Instance,
}
- break
}
+
+ break
}
}
}
}
+
+ return nil
}
// Run starts the main loop.
-func (status *YaGoStatus) Run() {
+func (status *YaGoStatus) Run() error {
status.upd = make(chan int)
- 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
- }
- }
- }(widgetIndex, c)
+ go (func() {
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
+ recv := i3.Subscribe(i3.WorkspaceEventType)
+ for recv.Next() {
+ e := recv.Event().(*i3.WorkspaceEvent)
+ if e.Change == "empty" {
+ continue
}
- })()
+ status.updateWorkspaces()
+ status.upd <- -1
+ }
+ })()
+
+ for wi := range status.widgets {
+ go func(wi int) {
+ for out := range status.widgets[wi].ch {
+ status.addWidgetOutput(wi, out)
+ }
+ }(wi)
- go func(widget ygs.Widget, c chan []ygs.I3BarBlock) {
- if err := widget.Run(c); err != nil {
- log.Print(err)
- c <- []ygs.I3BarBlock{ygs.I3BarBlock{
+ go func(wi int) {
+ defer (func() {
+ if r := recover(); r != nil {
+ status.widgets[wi].logger.Errorf("widget panic: %s", r)
+ debug.PrintStack()
+ status.widgets[wi].ch <- []ygs.I3BarBlock{{
+ FullText: "widget panic",
+ Color: "#ff0000",
+ }}
+ }
+ })()
+ 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)
}
- fmt.Print("{\"version\":1, \"click_events\": true}\n[\n[]")
+ encoder := json.NewEncoder(os.Stdout)
+ encoder.SetEscapeHTML(false)
+ encoder.SetIndent("", " ")
+
+ if err := encoder.Encode(ygs.I3BarHeader{
+ Version: 1,
+ ClickEvents: true,
+ StopSignal: int(status.cfg.Signals.StopSignal),
+ ContSignal: int(status.cfg.Signals.ContSignal),
+ }); err != nil {
+ status.logger.Errorf("Failed to encode I3BarHeader: %s", err)
+ }
+
+ fmt.Print("\n[\n[]")
+
go func() {
- buf := &bytes.Buffer{}
- encoder := json.NewEncoder(buf)
- encoder.SetEscapeHTML(false)
- encoder.SetIndent("", " ")
- 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 wi := range status.widgets {
+ if checkWorkspaceConditions(status.widgets[wi].config.Workspaces, status.visibleWorkspaces) {
+ result = append(result, status.widgets[wi].output...)
}
- buf.Reset()
- encoder.Encode(result)
- fmt.Print(",")
- fmt.Print(string(buf.Bytes()))
+ }
+ fmt.Print(",")
+ err := encoder.Encode(result)
+ if err != nil {
+ status.logger.Errorf("Failed to encode result: %s", err)
}
}
}()
- status.eventReader()
+
+ return status.eventReader()
}
-// Stop shutdowns widgets and main loop.
-func (status *YaGoStatus) Stop() {
+// Shutdown shutdowns widgets and main loop.
+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) {
- widget.Stop()
- wg.Done()
- }(widget)
+
+ go func(wi int) {
+ defer wg.Done()
+ defer (func() {
+ if r := recover(); r != nil {
+ status.widgets[wi].logger.Errorf("widget panic: %s", r)
+ debug.PrintStack()
+ }
+ })()
+ if err := status.widgets[wi].instance.Shutdown(); err != nil {
+ status.widgets[wi].logger.Errorf("Failed to shutdown widget: %s", err)
+ }
+ }(wi)
}
+
wg.Wait()
+
+ return nil
+}
+
+// Stop stops widgets and main loop.
+func (status *YaGoStatus) Stop() {
+ for wi := range status.widgets {
+ go func(wi int) {
+ defer (func() {
+ if r := recover(); r != nil {
+ status.widgets[wi].logger.Errorf("widget panic: %s", r)
+ debug.PrintStack()
+ }
+ })()
+ if err := status.widgets[wi].instance.Stop(); err != nil {
+ status.widgets[wi].logger.Errorf("Failed to stop widget: %s", err)
+ }
+ }(wi)
+ }
+}
+
+// Continue continues widgets and main loop.
+func (status *YaGoStatus) Continue() {
+ for wi := range status.widgets {
+ go func(wi int) {
+ defer (func() {
+ if r := recover(); r != nil {
+ status.widgets[wi].logger.Errorf("widget panic: %s", r)
+ debug.PrintStack()
+ }
+ })()
+ if err := status.widgets[wi].instance.Continue(); err != nil {
+ status.widgets[wi].logger.Errorf("Failed to continue widget: %s", err)
+ }
+ }(wi)
+ }
}
func (status *YaGoStatus) updateWorkspaces() {
var err error
+
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
+
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) {
- 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)
- json.Unmarshal(jb, b)
+ status.visibleWorkspaces = vw
}
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
}
@@ -304,23 +437,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/yagostatus.yml b/yagostatus.yml
index d643c31..15d6cc9 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
- }
+ "separator_block_width": 21
+ }]
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 02db634..e797537 100644
--- a/ygs/protocol.go
+++ b/ygs/protocol.go
@@ -1,5 +1,11 @@
package ygs
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
// I3BarHeader represents the header of an i3bar message.
type I3BarHeader struct {
Version uint8 `json:"version"`
@@ -10,24 +16,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,omitempty"`
+ 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.
@@ -43,3 +49,70 @@ 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 {
+ 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 {
+ 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)
+ if err := json.Unmarshal(tmp, &resmap); err != nil {
+ return nil, err
+ }
+
+ tmp, _ = json.Marshal(wd.Custom)
+ 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
+}
+
+func (b *I3BarBlock) Apply(tpl I3BarBlock) {
+ jb, _ := json.Marshal(b)
+ *b = tpl
+
+ 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
+}
diff --git a/ygs/utils.go b/ygs/utils.go
index fb3bdff..f779ad0 100644
--- a/ygs/utils.go
+++ b/ygs/utils.go
@@ -1,28 +1,102 @@
package ygs
import (
- "log"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
- "github.com/pkg/errors"
+ "github.com/burik666/yagostatus/internal/pkg/logger"
+ "gopkg.in/yaml.v2"
)
-type newWidgetFunc = func(map[string]interface{}) (Widget, error)
+type newWidgetFunc = func(interface{}, logger.Logger) (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(widgetConfig WidgetConfig, wlogger logger.Logger) (Widget, error) {
+ name := widgetConfig.Name
+ 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())
+ pe := params.Elem()
+ pe.Set(def)
+
+ b, err := yaml.Marshal(widgetConfig.Params)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := yaml.UnmarshalStrict(b, params.Interface()); err != nil {
+ return nil, trimYamlErr(err, true)
}
- return newFunc(params)
+
+ 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(), wlogger)
+}
+
+// ErrorWidget creates new widget with error message.
+func ErrorWidget(text string) WidgetConfig {
+ blocks, _ := json.Marshal([]I3BarBlock{
+ {
+ FullText: text,
+ Color: "#ff0000",
+ },
+ })
+
+ return WidgetConfig{
+ Name: "static",
+ Params: map[string]interface{}{
+ "blocks": string(blocks),
+ },
+ File: "bultin",
+ }
+
+}
+
+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/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
+}
diff --git a/ygs/widget.go b/ygs/widget.go
index d9b9754..2ebdd4d 100644
--- a/ygs/widget.go
+++ b/ygs/widget.go
@@ -1,71 +1,11 @@
// 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
+ Event(I3BarClickEvent, []I3BarBlock) error
+ Stop() error
+ Continue() error
+ Shutdown() error
}
diff --git a/ygs/widget_config.go b/ygs/widget_config.go
new file mode 100644
index 0000000..5df552d
--- /dev/null
+++ b/ygs/widget_config.go
@@ -0,0 +1,36 @@
+package ygs
+
+import (
+ "errors"
+)
+
+// WidgetConfig represents a widget configuration.
+type WidgetConfig struct {
+ Name string `yaml:"widget"`
+ Workspaces []string `yaml:"workspaces"`
+ Templates []I3BarBlock `yaml:"-"`
+ Events []WidgetEventConfig `yaml:"events"`
+ WorkDir string `yaml:"-"`
+ Index int `yaml:"-"`
+ File string `yaml:"-"`
+ Variables map[string]string `yaml:"variables"`
+
+ 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..3ef9545
--- /dev/null
+++ b/ygs/widget_event_config.go
@@ -0,0 +1,45 @@
+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"`
+ Override bool `yaml:"override"`
+ WorkDir string `yaml:"workdir"`
+}
+
+// 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
+}