aboutsummaryrefslogtreecommitdiff
path: root/internal/i3status
diff options
context:
space:
mode:
Diffstat (limited to 'internal/i3status')
-rw-r--r--internal/i3status/battery.go75
-rw-r--r--internal/i3status/main.go169
-rw-r--r--internal/i3status/net.go61
3 files changed, 305 insertions, 0 deletions
diff --git a/internal/i3status/battery.go b/internal/i3status/battery.go
new file mode 100644
index 0000000..e41f189
--- /dev/null
+++ b/internal/i3status/battery.go
@@ -0,0 +1,75 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package i3status
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strconv"
+ "strings"
+)
+
+const (
+ batteryFullPath = "/sys/class/power_supply/BAT0/energy_full"
+ batteryNowPath = "/sys/class/power_supply/BAT0/energy_now"
+ powerOnlinePath = "/sys/class/power_supply/AC0/online"
+)
+
+func getBatteryStatus() []Block {
+ //TODO: What's with /sys/class/power_supply/BAT0/uevent? Can this be used to
+ //remove all the polling overhead here?
+
+ energyFull, err := readNumberFromFile(batteryFullPath)
+ if err != nil {
+ return nil
+ }
+ energyNow, err := readNumberFromFile(batteryNowPath)
+ if err != nil {
+ return nil
+ }
+ powerOnline, err := readNumberFromFile(powerOnlinePath)
+ if err != nil {
+ return nil
+ }
+
+ energyPerc := energyNow * 100 / energyFull
+ charging := powerOnline > 0
+ color := "#AAAA00"
+ if charging {
+ color = "#00AA00"
+ } else if energyPerc < 10 {
+ color = "#AA0000"
+ }
+
+ return section("bat", Block{
+ Name: "battery",
+ Position: PositionBattery,
+ FullText: fmt.Sprintf("%d%%", energyPerc),
+ Urgent: energyPerc < 10 && !charging,
+ Color: color,
+ })
+}
+
+func readNumberFromFile(path string) (int64, error) {
+ buf, err := ioutil.ReadFile(path)
+ if err != nil {
+ return 0, err
+ }
+ return strconv.ParseInt(strings.TrimSpace(string(buf)), 0, 64)
+}
diff --git a/internal/i3status/main.go b/internal/i3status/main.go
new file mode 100644
index 0000000..4af726a
--- /dev/null
+++ b/internal/i3status/main.go
@@ -0,0 +1,169 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package i3status
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "sort"
+ "time"
+)
+
+//Exec executes the i3status applet and returns an exit code (0 for
+//success, >0 for error).
+func Exec(args []string) int {
+ //start protocol (we handle errors here for once because if stdout is
+ //working, we have nothing left to do)
+ _, err := os.Stdout.Write([]byte("{\"version\":1}\n[\n"))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, err.Error())
+ return 1
+ }
+
+ //main loop
+ currentBlocks := make(map[string][]Block)
+ for {
+ //prepare clock (this is inline instead of split into a separate function
+ //because the wallclock also drives the main loop's clock, see below)
+ now := time.Now()
+ currentBlocks["clock"] = []Block{
+ {
+ Name: "clock",
+ Instance: "date",
+ Position: PositionClock,
+ FullText: now.Format("2006-01-02"),
+ ShortText: " ",
+ Color: "#AAAAAA",
+ SeparatorBlockWidth: 6,
+ },
+ {
+ Name: "clock",
+ Instance: "time",
+ Position: PositionClock,
+ FullText: now.Format("15:04:05"),
+ },
+ }
+
+ //prepare other blocks
+ currentBlocks["battery"] = getBatteryStatus()
+ currentBlocks["network"] = getNetworkStatus()
+
+ //put blocks in rendering order
+ var allBlocks []Block
+ for _, blocks := range currentBlocks {
+ allBlocks = append(allBlocks, blocks...)
+ }
+ sort.Sort(byPositionAndInstance(allBlocks))
+
+ //write output
+ buf, err := json.Marshal(allBlocks)
+ if err == nil {
+ os.Stdout.Write(append(buf, ',', '\n'))
+ } else {
+ //this should not happen, but if it does, fall back to just encoding the
+ //error string (which always works, otherwise wtf)
+ buf, _ = json.Marshal(err.Error())
+ fmt.Printf(
+ `[{"name":"error","urgent":true,"color":"#FF0000","full_text":%s}],`,
+ string(buf),
+ )
+ }
+
+ //sleep until the next second starts, so that the clock is always on time
+ nsec := 1000000000 - now.Nanosecond()
+ time.Sleep(time.Duration(nsec) * time.Nanosecond)
+ }
+
+ return 0
+}
+
+//Block is a block of text as used in the i3status protocol.
+//(Not all attributes are represented.)
+type Block struct {
+ Position Position `json:"-"`
+ Name string `json:"name"` //REQUIRED
+ Instance string `json:"instance,omitempty"`
+ FullText string `json:"full_text"` //REQUIRED
+ ShortText string `json:"short_text"`
+ Color string `json:"color,omitempty"` //CSS hex syntax, e.g. #123456
+ BackgroundColor string `json:"background,omitempty"`
+ MinWidth uint `json:"min_width,omitempty"`
+ Alignment Alignment `json:"align,omitempty"` //only plausible with MinWidth
+ Urgent bool `json:"urgent,omitempty"`
+ Separator bool `json:"separator"`
+ SeparatorBlockWidth int `json:"separator_block_width,omitempty"`
+}
+
+type byPositionAndInstance []Block
+
+func (b byPositionAndInstance) Len() int { return len(b) }
+func (b byPositionAndInstance) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byPositionAndInstance) Less(i, j int) bool {
+ if b[i].Position == b[j].Position {
+ if b[i].Instance == "_caption" {
+ return true
+ }
+ if b[j].Instance == "_caption" {
+ return false
+ }
+ return b[i].Instance < b[j].Instance
+ }
+ return b[i].Position < b[j].Position
+}
+
+//Alignment is the alignment of a Block.
+type Alignment string
+
+//Acceptable values for Alignment.
+const (
+ AlignmentLeft Alignment = "left"
+ AlignmentCenter Alignment = "center"
+ AlignmentRight Alignment = "right"
+)
+
+//Position defines how blocks are ordered.
+type Position int
+
+//Acceptable values for Position, from left to right.
+const (
+ PositionNone Position = iota
+ PositionNetwork
+ PositionBattery
+ PositionClock
+)
+
+//Order the given blocks byPositionAndInstance, then add a separator to the last one, then add a caption block of the same style in front.
+func section(caption string, blocks ...Block) []Block {
+ if len(blocks) == 0 {
+ return nil
+ }
+ sort.Sort(byPositionAndInstance(blocks))
+ last := len(blocks) - 1
+ blocks[last].Separator = true
+ blocks[last].SeparatorBlockWidth = 15
+
+ return append([]Block{{
+ Name: blocks[0].Name,
+ Position: blocks[0].Position,
+ Instance: "_caption",
+ FullText: caption,
+ Color: blocks[0].Color,
+ }}, blocks...)
+}
diff --git a/internal/i3status/net.go b/internal/i3status/net.go
new file mode 100644
index 0000000..7367b28
--- /dev/null
+++ b/internal/i3status/net.go
@@ -0,0 +1,61 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package i3status
+
+import (
+ "net"
+ "regexp"
+ "strings"
+)
+
+var networkPortRx = regexp.MustCompile(`:\d+$`)
+var networkMaskRx = regexp.MustCompile(`/\d+$`)
+
+func getNetworkStatus() []Block {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return nil
+ }
+ addrStrs := make([]string, 0, len(addrs))
+ for _, addr := range addrs {
+ str := addr.String()
+ //remove uninteresting parts
+ str = networkPortRx.ReplaceAllString(str, "")
+ str = networkMaskRx.ReplaceAllString(str, "")
+ //ignore IPv6 for now
+ if strings.ContainsRune(str, ':') {
+ continue
+ }
+ //ignore uninteresting addrs
+ if strings.HasPrefix(str, "127.") {
+ continue
+ }
+ addrStrs = append(addrStrs, str)
+ }
+
+ if len(addrStrs) == 0 {
+ return nil
+ }
+ return section("ip", Block{
+ Name: "network",
+ Position: PositionNetwork,
+ FullText: strings.Join(addrStrs, " "),
+ Color: "#00AAAA",
+ })
+}