├── .gitignore ├── LICENSE ├── README.md ├── config.example.yml ├── core ├── defaults.go ├── formatter.go └── init.go ├── main.go ├── plugins ├── interface.go └── suspend │ ├── suspend.go │ └── suspend_test.go └── utils ├── pattern.go └── suspend_monitor.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Plotnikov Anton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | YetAnotherGoStatus 2 | ================== 3 | 4 | This program provides a simple configurable satatusline generator. It passes 5 | the formatted status line each time when callbacks from plugging is received. 6 | 7 | Unlike `conky`, `yags` prints satatusline only when status really changed and 8 | not overloads disk with useless executions. By the way it is possible to 9 | configure plugins to implement the conky behavior of execution any command 10 | with constant pause. 11 | 12 | Now it uses brand new golang's plugin system powered by `plugin` package. That 13 | mean, that this program would only work with go version newer or equal 1.8. 14 | 15 | ## Installation 16 | 17 | `go get github.com/pltanton/yags` 18 | 19 | ## Usage 20 | 21 | If you use `conky` or `dzen` you could pass `yags` output to them through pipe, 22 | for example: 23 | 24 | ``` 25 | yags /path/to/config.yml | dzen2 -x '866' -w '496' -ta 'r' 26 | 27 | ``` 28 | 29 | ## Plugins 30 | 31 | _Plugins_ is a go modules which provides callbacks when related to them action 32 | is appears and provides formatted result of that action to `yags` output. There 33 | are several implemented plugins: 34 | 35 | - [x] [battery](https://github.com/pltanton/yags-battery) -- uses upower dbus 36 | messages to monitor battery device 37 | - [x] [kbdd](https://github.com/pltanton/yags-kbdd) -- uses `kbdd` daemon to 38 | watch for keyboard layout 39 | - [x] [timer](https://github.com/pltanton/yags-timer) -- conky like plugin to 40 | execute any shell command with pause it includes predefined plugins for 41 | **WiFi** and **time** monitoring 42 | - [x] [maildir](https://github.com/pltanton/yags-maildir) -- monitors _new_ 43 | maildir dirrectory changing for new mails 44 | - [x] [volume](https://github.com/pltanton/yags-volume) -- uses alsalib to 45 | monitor volume changing and `pamixer` program to fetch an volume and a mute 46 | state, would be overwritten with pulselib in future 47 | - [ ] cpu -- alias for timer for cpu monitoring 48 | - [ ] ram -- alias for timer for ram monitoring 49 | 50 | ### How to use 51 | 52 | After you download a plugin via `go get` or any other way, you should build it 53 | as go plugin by `go build -buildmode=plugin` as documented in official golang 54 | [docpage](https://tip.golang.org/pkg/plugin/). As a result of building you 55 | should have a binary file ended by `.so` in working directory. Then you should 56 | pass path to that file by `path` plugin's configuration attribute (you can see 57 | it in [`config.example.yml`](https://github.com/pltanton/yags/blob/master/config.example.yml)). 58 | 59 | ### How to implement custom plugin 60 | 61 | TBD. 62 | 63 | ## Configuration 64 | 65 | You can configure `yags` by an any configuration file format, which 66 | [viper](https://github.com/spf13/viper) 67 | supports and pass configuration file path as a first argument to `yags` 68 | command. 69 | 70 | Exapmle of configuration you can find in root of this repository or in my 71 | [dotfiles](https://github.com/pltanton/dotfiles/tree/master/config/yags) 72 | repository. 73 | 74 | At the root of configuration file it few basic configuration fields: 75 | 76 | | Key | Description | Default value | 77 | | --- | --- | --- | 78 | | varSeps | symbols pair to wrap variables | {} | 79 | | format | string which would be formatted, you should use wrapped plugins names by `varSeps` to display plugin's output. Only if plugin passed to `format` it would be triggered. | | 80 | | plugins | subtree of configuration, where each plugin should be described | | 81 | 82 | ### Plugins 83 | 84 | Each plugin in `plugin` section should contain `path` key to specify go's 85 | plugin path to include. 86 | 87 | `YAGS` will pass configuration subtree related to `plugin` into plugin, for 88 | information about configuration plugin in specific plugin page. 89 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | # vim: ts=2:sw=2 2 | 3 | # varSeps: "{}" # default 4 | format: "{volume} {wifi} {bat1} {time} {layout} {gmail}" 5 | plugins: 6 | volume: 7 | path: "$GOPATH/src/github.com/pltanton/yags-volume/volume.so" 8 | sink: "/org/pulseaudio/core1/sink0" 9 | high: " {vol}%" 10 | medium: " {vol}%" 11 | low: " {vol}%" 12 | muted: " {vol}%" 13 | bat1: 14 | path: "$GOPATH/src/github.com/pltanton/yags-battery/battery.so" 15 | name: "BAT1" 16 | high: " {lvl}%" 17 | medium: " {lvl}%" 18 | low: " {lvl}%" 19 | empty: " {lvl}%" 20 | ac: " {lvl}%" 21 | time: 22 | path: "$GOPATH/src/github.com/pltanton/yags-timer/time/time.so" 23 | # timeFormat: "Jan 2 15:04:05" # default 24 | # pause: 1000 # default 25 | layout: 26 | path: "$GOPATH/src/github.com/pltanton/yags-kbdd/kbdd.so" 27 | names: ["en", "ru"] 28 | wifi: 29 | path: "$GOPATH/src/github.com/pltanton/yags-timer/wifi/wifi.so" 30 | connected: " {lvl}%" 31 | disconnected: " N/A" 32 | interface: "wlp4s0" 33 | pause: 2000 34 | gmail: 35 | path: "$GOPATH/src/github.com/pltanton/yags-maildir/maildir.so" 36 | dir: "/home/anton/mail/gmail/INBOX/" 37 | empty: " {new}" 38 | filled: " {new}" 39 | -------------------------------------------------------------------------------- /core/defaults.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "github.com/spf13/viper" 4 | 5 | func setDefaults() { 6 | viper.SetDefault("varSeps", "{}") 7 | } 8 | -------------------------------------------------------------------------------- /core/formatter.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | 6 | "github.com/pltanton/yags/utils" 7 | ) 8 | 9 | func formatOutput() string { 10 | formatString := viper.GetString("format") 11 | for _, name := range pluginsNames { 12 | formatString = utils.ReplaceVar( 13 | formatString, 14 | name, 15 | values[name], 16 | ) 17 | } 18 | return formatString 19 | } 20 | -------------------------------------------------------------------------------- /core/init.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "plugin" 8 | "reflect" 9 | 10 | "github.com/pltanton/yags/plugins" 11 | "github.com/pltanton/yags/utils" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | var pluginsNames []string 16 | var pluginsInstances []plugins.Plugin 17 | var cases []reflect.SelectCase 18 | var values map[string]string 19 | 20 | func initPlugins() { 21 | values = make(map[string]string) 22 | pluginsNames = utils.GetVars(viper.GetString("format")) 23 | pluginsInstances = make([]plugins.Plugin, len(pluginsNames)) 24 | 25 | for i, name := range pluginsNames { 26 | path := viper.GetString("plugins." + name + ".path") 27 | path, _ = filepath.Abs(os.ExpandEnv(path)) 28 | 29 | p, err := plugin.Open(path) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Println("here") 35 | 36 | pluginNewSym, err := p.Lookup("New") 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | pluginNew := pluginNewSym.(func(conf *viper.Viper) plugins.Plugin) 42 | 43 | pluginConfig := viper.Sub("plugins." + name) 44 | pluginInstance := pluginNew(pluginConfig) 45 | 46 | pluginsInstances[i] = pluginInstance 47 | 48 | go pluginInstance.StartMonitor() 49 | } 50 | } 51 | 52 | func initCases() { 53 | cases = make([]reflect.SelectCase, len(pluginsInstances)) 54 | for i, pluginInstance := range pluginsInstances { 55 | cases[i] = reflect.SelectCase{ 56 | Dir: reflect.SelectRecv, 57 | Chan: reflect.ValueOf(pluginInstance.Chan()), 58 | } 59 | } 60 | } 61 | 62 | func listen() { 63 | for { 64 | chosen, value, _ := reflect.Select(cases) 65 | values[pluginsNames[chosen]] = value.String() 66 | fmt.Println(formatOutput()) 67 | } 68 | } 69 | 70 | func Init() { 71 | setDefaults() 72 | initPlugins() 73 | initCases() 74 | listen() 75 | } 76 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/viper" 8 | 9 | "github.com/pltanton/yags/core" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) != 2 { 14 | panic(fmt.Errorf("You should specify the config file as only one argument")) 15 | } 16 | viper.SetConfigFile(os.Args[1]) 17 | if err := viper.ReadInConfig(); err != nil { 18 | panic(fmt.Errorf("fatal error config file: %s", err)) 19 | } 20 | 21 | core.Init() 22 | } 23 | -------------------------------------------------------------------------------- /plugins/interface.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | type Plugin interface { 4 | StartMonitor() 5 | Chan() chan string 6 | } 7 | -------------------------------------------------------------------------------- /plugins/suspend/suspend.go: -------------------------------------------------------------------------------- 1 | package suspend 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/godbus/dbus" 8 | ) 9 | 10 | var instance chan *dbus.Signal 11 | var once = sync.Once{} 12 | 13 | var subscribers = make([]chan *dbus.Signal, 0) 14 | 15 | func monitorSuspend() { 16 | fmt.Println(subscribers) 17 | for { 18 | signalValue := <-instance 19 | for _, subscriber := range subscribers { 20 | subscriber <- signalValue 21 | } 22 | } 23 | } 24 | 25 | func startMonitorIfNeed() { 26 | once.Do(func() { 27 | if instance == nil { 28 | conn, err := dbus.SystemBus() 29 | if err != nil { 30 | panic(fmt.Errorf("cannot connect dbus session: %s", err.Error())) 31 | } 32 | 33 | arg := fmt.Sprintf( 34 | "type='signal',interface='%s',member='%s',sender='%s'", 35 | "org.freedesktop.login1.Manager", 36 | "PrepareForSleep", 37 | "org.freedesktop.login1", 38 | ) 39 | 40 | conn.BusObject().Call( 41 | "org.freedesktop.DBus.AddMatch", 42 | 0, 43 | arg, 44 | ) 45 | 46 | instance = make(chan *dbus.Signal, 1) 47 | conn.Signal(instance) 48 | 49 | go monitorSuspend() 50 | } 51 | }) 52 | } 53 | 54 | func Subscribe(subscriber chan *dbus.Signal) { 55 | startMonitorIfNeed() 56 | subscribers = append(subscribers, subscriber) 57 | } 58 | -------------------------------------------------------------------------------- /plugins/suspend/suspend_test.go: -------------------------------------------------------------------------------- 1 | package suspend 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestInit(t *testing.T) { 9 | startMonitorIfNeed() 10 | fmt.Println(instance) 11 | startMonitorIfNeed() 12 | fmt.Println(instance) 13 | } 14 | -------------------------------------------------------------------------------- /utils/pattern.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // ReplaceVar returns a copy of the string s with replaced newValue substring, 12 | // surrounded by the separators defined in config as varSeps, by newValue. 13 | func ReplaceVar(s, varName, newValue string) string { 14 | varSeps := viper.GetString("varSeps") 15 | oldValue := fmt.Sprintf("%c%s%c", varSeps[0], varName, varSeps[1]) 16 | return strings.Replace(s, oldValue, newValue, -1) 17 | } 18 | 19 | // GetVars returns the variables containing in the stirng s 20 | func GetVars(s string) (result []string) { 21 | seps := viper.GetString("varSeps") 22 | regex := regexp.MustCompile(fmt.Sprintf(`%c([[:word:]]+)%c`, seps[0], seps[1])) 23 | allSubmatches := regex.FindAllStringSubmatch(s, -1) 24 | for _, submatch := range allSubmatches { 25 | result = append(result, submatch[1]) 26 | } 27 | return 28 | } 29 | 30 | // Contains return true if key is inside slice 31 | func Contains(key string, arr *[]string) bool { 32 | for _, val := range *arr { 33 | if key == val { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /utils/suspend_monitor.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/godbus/dbus" 7 | ) 8 | 9 | // GetResumeDbusConn returns DBUS connection to resume from suspend signal 10 | func GetResumeDbusConn() *dbus.Conn { 11 | conn, err := dbus.SystemBus() 12 | if err != nil { 13 | panic(fmt.Errorf("cannot connect dbus session: %s", err.Error())) 14 | } 15 | 16 | arg := fmt.Sprintf( 17 | "type='signal',interface='%s',member='%s',sender='%s'", 18 | "org.freedesktop.login1.Manager", 19 | "PrepareForSleep", 20 | "org.freedesktop.login1", 21 | ) 22 | 23 | conn.BusObject().Call( 24 | "org.freedesktop.DBus.AddMatch", 25 | 0, 26 | arg, 27 | ) 28 | 29 | return conn 30 | } 31 | --------------------------------------------------------------------------------