diff --git a/.gitignore b/.gitignore index daf913b..e0839be 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ _testmain.go *.exe *.test *.prof +*.log +.idea diff --git a/config.json b/config.json new file mode 100644 index 0000000..4ad767e --- /dev/null +++ b/config.json @@ -0,0 +1,75 @@ +{ + "defaults": { + "color": "#ffffff" + }, + "blocks": [{ + "module": "ExternalCmd", + "label":"", + "config": { + "command": "apt-get --just-print upgrade |grep Inst | wc -l" + }, + "interval": 60, + "info": { + "border_bottom": 2, + "border": "#ff00ff" + } + },{ + "module": "VolumeInfo", + "label":"V:", + "interval": 1, + "info": { + "border_bottom": 2, + "border": "#ff00ff" + } + },{ + "module": "CpuInfo", + "label": "", + "interval": 2, + "info": { + "border_bottom": 2, + "border": "#ffffff" + } + },{ + "module": "MemInfo", + "label": "", + "interval": 5, + "info": { + "border_bottom": 2, + "border": "#00ff00" + } + },{ + "module": "DiskUsage", + "label": "", + "interval": 10, + "info": { + "border_bottom": 2, + "border": "#ffff00" + } + },{ + "module": "ExternalCmd", + "label":"", + "config": { + "command": "uptime |sed 's/,//g' |awk '{ print $3 }'" + }, + "interval": 60, + "info": { + "border_bottom": 2, + "border": "#909090" + } + },{ + "module": "DateTime", + "label": "", + "interval": 5, + "info": { + "border_bottom": 2, + "border": "#ffffff" + } + },{ + "module": "ExternalCmd", + "label": "", + "config": { + "on_click": "shutdown -h -t now" + }, + "interval": 0 + }] +} \ No newline at end of file diff --git a/gobar/bar.go b/gobar/bar.go new file mode 100644 index 0000000..a493e4b --- /dev/null +++ b/gobar/bar.go @@ -0,0 +1,149 @@ +package gobar + +import ( + "encoding/json" + "fmt" + "strings" + "log" + "time" + "bufio" + "os" +) + +// Header i3 header +type Header struct { + Version int `json:"version"` + ClickEvents bool `json:"click_events"` + // StopSignal int `json:"stop_signal"` + // ContinueSignal int `json:"cont_signal"` +} + +type Bar struct { + modules []Block + logger *log.Logger + updateChannel chan UpdateChannelMsg + stop chan bool +} + +type ClickMessage struct { + Name string `json:"name,omitempty"` + Instance string `json:"instance,omitempty"` + Button int `json:"button"` + X int `json:"x"` + Y int `json:"y"` +} + +func (cm *ClickMessage) isMatch(block Block) bool { + return block.Info.Name == cm.Name && block.Info.Instance == cm.Instance +} + +func (bar *Bar) Start() { + header := Header{ + Version: 1, + ClickEvents: true, + // StopSignal: 20, // SIGHUP + // ContinueSignal: 19, // SIGCONT + } + headerJSON, _ := json.Marshal(header) + fmt.Println(string(headerJSON)) + fmt.Println("[[]") + bar.stop = make(chan bool, 3) + bar.ReStart() +} + +func (bar *Bar) ReStart() { + bar.stop = make(chan bool, 3) + go bar.update() + go bar.printItems() + go bar.handleClick() +} + +func (bar *Bar) Stop() { + bar.stop <- true + bar.stop <- true + bar.stop <- true +} + +func (bar *Bar) update() { + for { + select { + case <-bar.stop: + bar.logger.Println("update") + return + case m := <-bar.updateChannel: + bar.modules[m.ID].Info = m.Info + } + } +} + +func (bar *Bar) handleClick() { + for { + select { + case <-bar.stop: + bar.logger.Println("handleClick") + return + default: + bio := bufio.NewReader(os.Stdin) + line, _, err := bio.ReadLine() + if err != nil { + continue + } + if len(line) == 0 { + continue + } + + var clickMessage ClickMessage + + if line[0] == ',' { + line = line[1:] + } + + err = json.Unmarshal(line, &clickMessage) + if err == nil { + bar.logger.Printf("Click: line: %s, cm:%+v", string(line), clickMessage) + for _, module := range bar.modules { + if clickMessage.isMatch(module) { + bar.logger.Println("Click: handled") + err := module.HandleClick(clickMessage) + if err != nil { + bar.logger.Println("Click handle error: %s", err.Error()) + } + } + } + } + time.Sleep(1) + } + } +} + +func (bar *Bar) printItems() { + for { + select { + case <-bar.stop: + bar.logger.Println("printItems") + return + default: + var infoArray []string + var minInterval int64 + for _, item := range bar.modules { + item.Info.FullText = item.Label + " " + item.Info.FullText + item.Info.ShortText = item.Label + " " + item.Info.ShortText + + info, err := json.Marshal(item.Info) + if err != nil { + bar.logger.Printf("ERROR: %q", err) + } else { + infoArray = append(infoArray, string(info)) + } + if minInterval == 0 || (item.Interval > 0 && item.Interval < minInterval) { + minInterval = item.Interval + } + } + fmt.Println(",[", strings.Join(infoArray, ",\n"), "]") + if minInterval == 0 { + break; + } + time.Sleep(time.Duration(minInterval) * time.Second) + } + } +} diff --git a/gobar/block.go b/gobar/block.go new file mode 100644 index 0000000..38dc073 --- /dev/null +++ b/gobar/block.go @@ -0,0 +1,176 @@ +package gobar + +import ( + "fmt" + "log" + "reflect" + "time" + "encoding/json" +) + +type BlockAlign string + +const ( + AlignCenter BlockAlign = "center" + AlignRight BlockAlign = "right" + AlignLeft BlockAlign = "left" +) + +func (ba *BlockAlign) UnmarshalJSON(data []byte) error { + var align string + if err := json.Unmarshal(data, &align); err != nil { + return err + } + switch align { + case string(AlignCenter): + *ba = AlignCenter + return nil + case string(AlignRight): + *ba = AlignRight + return nil + case string(AlignLeft): + *ba = AlignLeft + return nil + } + return &json.UnsupportedValueError{ + Value: reflect.ValueOf(align), + Str: string(align), + } +} +func (ba *BlockAlign) MarshalJSON() ([]byte, error) { + return json.Marshal(string(*ba)) +} + +type BlockMarkup string + +const ( + MarkupNone BlockMarkup = "none" + MarkupPango BlockMarkup = "pango" +) + +func (bm *BlockMarkup) UnmarshalJSON(data []byte) error { + var markup string + if err := json.Unmarshal(data, &markup); err != nil { + return err + } + switch markup { + case string(MarkupNone): + *bm = MarkupNone + return nil + case string(MarkupPango): + *bm = MarkupPango + return nil + } + + return &json.UnsupportedValueError{ + Value: reflect.ValueOf(markup), + Str: string(markup), + } +} +func (bm *BlockMarkup) MarshalJSON() ([]byte, error) { + return json.Marshal(string(*bm)) +} + + +type BlockInfo struct { + FullText string `json:"full_text"` + ShortText string `json:"short_text,omitempty"` + TextColor string `json:"color,omitempty"` + BackgroundColor string `json:"background,omitempty"` + BorderColor string `json:"border,omitempty"` + MinWidth int `json:"min_width,omitempty"` + Align BlockAlign `json:"align,omitempty"` + Name string `json:"name"` + Instance string `json:"instance,omitempty"` + IsUrgent bool `json:"urgent,omitempty"` + HasSeparator bool `json:"separator,omitempty"` + SeparatorBlockWidth int `json:"separator_block_width,omitempty"` + Markup BlockMarkup `json:"markup,omitempty"` + BorderTop int `json:"border_top"` + BorderBottom int `json:"border_bottom"` + BorderLeft int `json:"border_left"` + BorderRight int `json:"border_right"` +} + +type Config map[string]interface{} + +type ModuleInterface interface { + InitModule(config Config) error + UpdateInfo(info BlockInfo) BlockInfo + HandleClick(cm ClickMessage) error +} + +// Block i3 item +type Block struct { + ModuleName string `json:"module"` + Label string `json:"label"` + Interval int64 `json:"interval"` + Info BlockInfo `json:"info,omitempty"` + Config Config `json:"config,omitempty"` + module ModuleInterface + lastUpdate int64 +} + +type UpdateChannelMsg struct { + ID int + Info BlockInfo +} + +func (block *Block) CreateModule(id int,logger *log.Logger) (err error) { + var ok bool + if name, ok := typeRegistry[block.ModuleName]; ok { + v := reflect.New(name) + if block.module, ok = v.Interface().(ModuleInterface); ok { + err = block.module.InitModule(block.Config) + } else { + err = fmt.Errorf("Cannot create instance of `%s`", name) + } + } else { + err = fmt.Errorf("Module not found: `%s`", block.ModuleName) + } + block.Info.Instance = fmt.Sprintf("id_%d", id) + if block.Info.Name == "" { + block.Info.Name = block.ModuleName + } + + if err != nil { + block.Label = "ERR: " + block.Info = BlockInfo{ + TextColor : "#FF0000", + FullText : err.Error(), + Name: "StaticText", + } + block.Config = Config{} + v := reflect.New(typeRegistry["StaticText"]) + if block.module, ok = v.Interface().(ModuleInterface); ok { + block.module.InitModule(block.Config) + } + } + return err +} + +func (block Block) Start(ID int, updateChannel chan<- UpdateChannelMsg) { + for { + newInfo := block.module.UpdateInfo(block.Info) + m := UpdateChannelMsg{ + ID: ID, + Info: newInfo, + } + updateChannel <- m + block.lastUpdate = time.Now().Unix() + if block.Interval == 0 { + break; + } + time.Sleep(time.Duration(block.Interval) * time.Second) + } +} + +func (block Block) HandleClick(cm ClickMessage) error { + return block.module.HandleClick(cm) +} + +var typeRegistry = make(map[string]reflect.Type) + +func AddModule(name string, module reflect.Type) { + typeRegistry[name] = module +} diff --git a/gobar/configuration.go b/gobar/configuration.go new file mode 100644 index 0000000..5a2388d --- /dev/null +++ b/gobar/configuration.go @@ -0,0 +1,64 @@ +package gobar + +import ( + "log" + "reflect" +) + +type Configuration struct { + Defaults *BlockInfo `json:"defaults,omitempty"` + Blocks []Block `json:"blocks"` +} + +var defaults reflect.Value + +func (config *Configuration) CreateBar(logger *log.Logger) *Bar { + //fmt.Printf("%+v\n\n", reflect.TypeOf(config.Defaults).Elem().Field(0)) + updateChannel := make(chan UpdateChannelMsg) + defaults = reflect.ValueOf(config.Defaults).Elem() + for i := range config.Blocks { + mapDefaults(&config.Blocks[i].Info) + err := config.Blocks[i].CreateModule(i, logger) + if err == nil { + go config.Blocks[i].Start(i, updateChannel) + } else { + logger.Printf("Error: %q\n", err) + } + } + return &Bar{ + modules: config.Blocks, + logger: logger, + updateChannel: updateChannel, + } +} + +func mapDefaults(blockInfo *BlockInfo) { + info := reflect.ValueOf(blockInfo).Elem() + + for i, n := 0, defaults.NumField(); i < n; i++ { + src := defaults.Field(i) + dst := info.Field(i) + if !isEmptyValue(src) && isEmptyValue(dst) && dst.CanSet() { + dst.Set(src) + } + } +} + +// From src/pkg/encoding/json. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f332771 --- /dev/null +++ b/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/signal" + "syscall" + "reflect" + + "github.com/Ak-Army/i3barfeeder/gobar" + "github.com/Ak-Army/i3barfeeder/modules" +) + +func loadConfig(path string) (barConfig gobar.Configuration, err error) { + barConfig = gobar.Configuration{} + text, err := ioutil.ReadFile(path) + if err != nil { + return + } + err = json.Unmarshal(text, &barConfig) + return +} + +func checkErr(err error, msg string) { + if err != nil { + fmt.Fprintf(os.Stderr, "%s:%q", msg, err) + os.Exit(2) + } +} + +func initModules() { + gobar.AddModule("StaticText", reflect.TypeOf(modules.StaticText{})) + gobar.AddModule("DateTime", reflect.TypeOf(modules.DateTime{})) + gobar.AddModule("ExternalCmd", reflect.TypeOf(modules.ExternalCmd{})) + gobar.AddModule("DiskUsage", reflect.TypeOf(modules.DiskUsage{})) + gobar.AddModule("MemInfo", reflect.TypeOf(modules.MemInfo{})) + gobar.AddModule("CpuInfo", reflect.TypeOf(modules.CpuInfo{})) + gobar.AddModule("VolumeInfo", reflect.TypeOf(modules.VolumeInfo{})) +} + +func main() { + var logPath, configPath string + flag.StringVar(&logPath, "log", "/dev/null", "Log path. Default: /dev/null") + flag.StringVar(&logPath, "l", "/dev/null", "Log file to use. Default: /dev/null") + flag.StringVar(&configPath, "config", "", "Configuration path.") + flag.StringVar(&configPath, "c", "", "Configuration path (in JSON).") + + flag.Parse() + + logfile, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) + checkErr(err, "Log file not opened") + defer logfile.Close() + logger := log.New(logfile, "gobar:", log.Lshortfile|log.LstdFlags) + + logger.Println("Start") + initModules() + logger.Printf("Loading configuration from: %s", configPath) + config, err := loadConfig(configPath) + checkErr(err, "Config file not opened") + logger.Printf("bar items: %+v", config.Blocks) + fmt.Printf("%+v", config.Defaults) + + bar := config.CreateBar(logger) + bar.Start() + sigHandler(bar, logger) + logger.Println("End") + os.Exit(0) + defer func () { + if r := recover(); r != nil { + logger.Printf("Unhandled panic: %v", r) + } + }() +} + +func sigHandler(bar *gobar.Bar, logger *log.Logger) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs) + for { + sig := <-sigs + logger.Printf("Received signal: %q", sig) + switch sig { + /*case syscall.SIGSTOP: + bar.Stop() + case syscall.SIGCONT: + bar.Stop() + bar.ReStart()*/ + case syscall.SIGINT: + return + } + } +} diff --git a/modules/cpuinfo.go b/modules/cpuinfo.go new file mode 100644 index 0000000..8e0e465 --- /dev/null +++ b/modules/cpuinfo.go @@ -0,0 +1,70 @@ +package modules + +import ( + "fmt" + "reflect" + "strings" + + "github.com/Ak-Army/i3barfeeder/gobar" + "strconv" + "os/exec" +) + +type CpuInfo struct { + gobar.ModuleInterface + path string + barConfig barConfig +} + +var prevTotal, prevIdle uint64 + +func (module *CpuInfo) InitModule(config gobar.Config) error { + module.path = keyExists(config, "path", reflect.String, "/").(string) + module.barConfig.barSize = keyExists(config, "barSize", reflect.Int, 10).(int) + module.barConfig.barFull = keyExists(config, "barFull", reflect.String, "■").(string) + module.barConfig.barEmpty = keyExists(config, "barEmpty", reflect.String, "□").(string) + + return nil +} + +func (module CpuInfo) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + cpuUsage := module.CpuInfo() + info.ShortText = fmt.Sprintf("%d %s", int(cpuUsage), "%") + info.FullText = makeBar(cpuUsage, module.barConfig) + return info +} +func (module CpuInfo) HandleClick(cm gobar.ClickMessage) error { + split := strings.Split("gnome-system-monitor -p", " ") + return exec.Command(split[0], split[1:]...).Start() +} + +func (module CpuInfo) CpuInfo() (cpuUsage float64) { + // Return the percent utilization of the CPU. + var idle, total uint64 + callback := func(line string) bool { + fields := strings.Fields(line) + if fields[0] == "cpu" { + numFields := len(fields) + for i := 1; i < numFields; i++ { + val, _ := strconv.ParseUint(fields[i], 10, 64) + total += val + if i == 4 { + idle = val + } + } + return false + } + return true + } + readLines("/proc/stat", callback) + + if prevIdle > 0 { + idleTicks := float64(idle - prevIdle) + totalTicks := float64(total - prevTotal) + cpuUsage = 100 * (totalTicks - idleTicks) / totalTicks + } + prevIdle = idle + prevTotal = total + return +} + diff --git a/modules/datetime.go b/modules/datetime.go new file mode 100644 index 0000000..af48a8c --- /dev/null +++ b/modules/datetime.go @@ -0,0 +1,45 @@ +package modules + +import ( + "time" + "fmt" + + "github.com/Ak-Army/i3barfeeder/gobar" +) + +type DateTime struct { + gobar.ModuleInterface + format string + location *time.Location +} + +func (module *DateTime) InitModule(config gobar.Config) error { + if format, ok := config["format"].(string); ok { + module.format = format + } else { + module.format = "2006-01-02 15:04:05" + } + if location, ok := config["location"].(string); ok { + zone, err := time.LoadLocation(location) + if err != nil { + return fmt.Errorf("Timezone not found: `%s", location) + } else { + module.location = zone + } + } + return nil +} + +func (module DateTime) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + var now time.Time + now = time.Now() + if module.location != nil { + now = now.In(module.location) + } + + info.FullText = now.Format(module.format) + return info +} +func (slot DateTime) HandleClick(cm gobar.ClickMessage) error { + return nil +} diff --git a/modules/diskusage.go b/modules/diskusage.go new file mode 100644 index 0000000..31c7d18 --- /dev/null +++ b/modules/diskusage.go @@ -0,0 +1,47 @@ +package modules + +import ( + "syscall" + "fmt" + "reflect" + + "github.com/Ak-Army/i3barfeeder/gobar" + "strings" + "os/exec" +) + +type DiskUsage struct { + gobar.ModuleInterface + path string + barConfig barConfig +} + +func (module *DiskUsage) InitModule(config gobar.Config) error { + module.path = keyExists(config, "path", reflect.String, "/").(string) + module.barConfig.barSize = keyExists(config, "barSize", reflect.Int, 10).(int) + module.barConfig.barFull = keyExists(config, "barFull", reflect.String, "■").(string) + module.barConfig.barEmpty = keyExists(config, "barEmpty", reflect.String, "□").(string) + + return nil +} + +func (module DiskUsage) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + free, total := module.diskUsage() + freePercent := 100 * (free / total) + info.ShortText = fmt.Sprintf("%d %s", int(freePercent), "%") + info.FullText = makeBar(freePercent, module.barConfig) + return info +} +func (module DiskUsage) HandleClick(cm gobar.ClickMessage) error { + split := strings.Split("gnome-system-monitor -f", " ") + return exec.Command(split[0], split[1:]...).Start() +} + +func (module DiskUsage) diskUsage() (free float64, total float64) { + // Return bytes free and total bytes. + buf := new(syscall.Statfs_t) + syscall.Statfs(module.path, buf) + free = float64(buf.Bsize) * float64(buf.Bfree) + total = float64(buf.Bsize) * float64(buf.Blocks) + return free, total +} \ No newline at end of file diff --git a/modules/externalcmd.go b/modules/externalcmd.go new file mode 100644 index 0000000..d4d66b4 --- /dev/null +++ b/modules/externalcmd.go @@ -0,0 +1,47 @@ +package modules + +import ( + "os/exec" + "strings" + + "github.com/Ak-Army/i3barfeeder/gobar" +) + +type ExternalCmd struct { + gobar.ModuleInterface + command string + onClick *exec.Cmd +} + +func (module *ExternalCmd) InitModule(config gobar.Config) error { + if command, ok := config["command"].(string); ok { + module.command = command + } + + if command, ok := config["on_click"].(string); ok { + split := strings.Split(command, " ") + module.onClick = exec.Command(split[0], split[1:]...) + } + return nil +} + +func (module ExternalCmd) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + if module.command != "" { + out, err := exec.Command("sh", "-c", module.command).Output() + if err != nil { + info.FullText = err.Error() + info.TextColor = "#FF2222" + } else { + info.FullText = strings.TrimSpace(string(out)) + } + } + return info +} + +func (module ExternalCmd) HandleClick(cm gobar.ClickMessage) error { + if module.onClick != nil { + return module.onClick.Start() + } + return nil +} + diff --git a/modules/meminfo.go b/modules/meminfo.go new file mode 100644 index 0000000..f022fe4 --- /dev/null +++ b/modules/meminfo.go @@ -0,0 +1,58 @@ +package modules + +import ( + "fmt" + "reflect" + "strings" + + "github.com/Ak-Army/i3barfeeder/gobar" + "os/exec" +) + +type MemInfo struct { + gobar.ModuleInterface + path string + barConfig barConfig +} + +func (module *MemInfo) InitModule(config gobar.Config) error { + module.path = keyExists(config, "path", reflect.String, "/").(string) + module.barConfig.barSize = keyExists(config, "barSize", reflect.Int, 10).(int) + module.barConfig.barFull = keyExists(config, "barFull", reflect.String, "■").(string) + module.barConfig.barEmpty = keyExists(config, "barEmpty", reflect.String, "□").(string) + + return nil +} + +func (module MemInfo) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + free, total := module.memInfo() + freePercent := 100 - 100 * (free / total) + info.ShortText = fmt.Sprintf("%d %s", int(freePercent), "%") + info.FullText = makeBar(freePercent, module.barConfig) + return info +} +func (module MemInfo) HandleClick(cm gobar.ClickMessage) error { + split := strings.Split("gnome-system-monitor -r", " ") + return exec.Command(split[0], split[1:]...).Start() +} + +func (module MemInfo) memInfo() (float64, float64) { + mem := map[string]float64{ + "MemTotal": 0, + "MemFree": 0, + "Buffers": 0, + "Cached": 0, + } + callback := func(line string) bool { + fields := strings.Split(line, ":") + if _, ok := mem[fields[0]]; ok { + var val float64 + fmt.Sscanf(fields[1], "%f", &val) + mem[fields[0]] = val * 1024 + } + return true + } + readLines("/proc/meminfo", callback) + return mem["MemFree"] + mem["Buffers"] + mem["Cached"], mem["MemTotal"] +} + diff --git a/modules/statictext.go b/modules/statictext.go new file mode 100644 index 0000000..4d4fae2 --- /dev/null +++ b/modules/statictext.go @@ -0,0 +1,21 @@ +package modules + +import ( + "github.com/Ak-Army/i3barfeeder/gobar" +) + +type StaticText struct { + gobar.ModuleInterface +} + +func (slot *StaticText) InitModule(config gobar.Config) error { + return nil +} + +func (slot StaticText) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + return info +} + +func (slot StaticText) HandleClick(cm gobar.ClickMessage) error { + return nil +} diff --git a/modules/utils.go b/modules/utils.go new file mode 100644 index 0000000..93bc098 --- /dev/null +++ b/modules/utils.go @@ -0,0 +1,54 @@ +package modules + +import ( + "reflect" + "bytes" + "os" + "fmt" + "bufio" + "io" +) + +type barConfig struct { + barSize int + barFull string + barEmpty string +} + +func keyExists(config map[string]interface{}, key string, keyType reflect.Kind, defaultValue interface{}) interface{} { + if value, ok := config[key]; ok && reflect.TypeOf(config[key]).Kind() == keyType { + return value + } + return defaultValue +} + +func makeBar(freePercent float64, barConfig barConfig) string { + var bar bytes.Buffer + cutoff := int(freePercent * .01 * float64(barConfig.barSize)) + for i := 0; i < barConfig.barSize; i += 1 { + if i < cutoff { + bar.WriteString(barConfig.barFull) + } else { + bar.WriteString(barConfig.barEmpty) + } + } + return bar.String() +} + + +func readLines(fileName string, callback func(string) bool) { + fin, err := os.Open(fileName) + if err != nil { + fmt.Fprintf(os.Stderr, "The file %s does not exist!\n", fileName) + return + } + defer fin.Close() + + reader := bufio.NewReader(fin) + for line, _, err := reader.ReadLine(); err != io.EOF; line, _, err = reader.ReadLine() { + if !callback(string(line)) { + break + } + } +} + diff --git a/modules/volumeInfo.go b/modules/volumeInfo.go new file mode 100644 index 0000000..1f956bf --- /dev/null +++ b/modules/volumeInfo.go @@ -0,0 +1,110 @@ +package modules + +import ( + "fmt" + "reflect" + "strings" + "os/exec" + "regexp" + "strconv" + + "github.com/Ak-Army/i3barfeeder/gobar" +) + +type VolumeInfo struct { + gobar.ModuleInterface + path string + barConfig barConfig + mixer string + sControl string + step int + regex *regexp.Regexp +} + + +func (module *VolumeInfo) InitModule(config gobar.Config) error { + module.path = keyExists(config, "path", reflect.String, "/").(string) + + module.barConfig.barSize = keyExists(config, "barSize", reflect.Int, 10).(int) + module.barConfig.barFull = keyExists(config, "barFull", reflect.String, "■").(string) + module.barConfig.barEmpty = keyExists(config, "barEmpty", reflect.String, "□").(string) + + module.mixer = keyExists(config, "mixer", reflect.String, "default").(string) + module.step = keyExists(config, "step", reflect.Int, 1).(int) + if sControl, ok := config["scontrol"].(string); ok { + module.sControl = sControl; + } else { + sControl, err := exec.Command("sh", "-c", "amixer -D default scontrols").Output() + if err == nil { + regex, _ := regexp.Compile(`'(\w+)',0`) + module.sControl = regex. FindStringSubmatch(string(sControl))[1] + } else { + return fmt.Errorf("Cant find scontrol for mixer: %s, error: %s", module.mixer, err) + } + } + regex, err := regexp.Compile(`(\d+) \[(\d+)%\].*\[(\w+)\]`) + if err != nil { + return fmt.Errorf("Regex error: %s", err) + } + module.regex = regex + + return nil +} + +func (module VolumeInfo) UpdateInfo(info gobar.BlockInfo) gobar.BlockInfo { + out, err := exec.Command("sh", "-c", "amixer -D "+module.mixer+" get "+module.sControl).Output() + if err == nil { + volumes := module.regex.FindStringSubmatch(string(out)) + if len(volumes) == 0 || volumes[3] == "off" { + info.FullText = makeBar(float64(0), module.barConfig) + } else { + currentVolume, err := strconv.ParseFloat(module.regex.FindStringSubmatch(string(out))[2], 64) + if err == nil { + info.FullText = makeBar(float64(currentVolume), module.barConfig) + } + } + } + + if err != nil { + info.FullText = err.Error() + info.TextColor = "#FF2222" + } + + return info +} +func (module VolumeInfo) HandleClick(cm gobar.ClickMessage) error { + var cmd string + switch cm.Button { + case 3: //right click, mute/unmute + cmd = fmt.Sprintf("amixer -D %s sset %s toggle", module.mixer, module.sControl) + case 4: //scroll up, increase + cmd = fmt.Sprintf("amixer -D %s sset %s %d%%+ unmute", module.mixer, module.sControl, module.step) + case 5: //scroll down, decrease + cmd = fmt.Sprintf("amixer -D %s sset %s %d%%- unmute", module.mixer, module.sControl, module.step) + } + if cmd != "" { + return exec.Command("sh", "-c", cmd).Start() + } + return nil +} + +func (module VolumeInfo) VolumeInfo() (float64, float64) { + mem := map[string]float64{ + "MemTotal": 0, + "MemFree": 0, + "Buffers": 0, + "Cached": 0, + } + callback := func(line string) bool { + fields := strings.Split(line, ":") + if _, ok := mem[fields[0]]; ok { + var val float64 + fmt.Sscanf(fields[1], "%f", &val) + mem[fields[0]] = val * 1024 + } + return true + } + readLines("/proc/VolumeInfo", callback) + return mem["MemFree"] + mem["Buffers"] + mem["Cached"], mem["MemTotal"] +} +