soumya92/barista

View on GitHub
samples/simple/simple.go

Summary

Maintainability
D
2 days
Test Coverage
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// simple demonstrates a simpler i3bar built using barista.
// Serves as a good starting point for building custom bars.
package main

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "os/user"
    "path/filepath"
    "time"

    "github.com/soumya92/barista"
    "github.com/soumya92/barista/bar"
    "github.com/soumya92/barista/base/click"
    "github.com/soumya92/barista/base/watchers/netlink"
    "github.com/soumya92/barista/colors"
    "github.com/soumya92/barista/format"
    "github.com/soumya92/barista/group/collapsing"
    "github.com/soumya92/barista/modules/battery"
    "github.com/soumya92/barista/modules/clock"
    "github.com/soumya92/barista/modules/cputemp"
    "github.com/soumya92/barista/modules/github"
    "github.com/soumya92/barista/modules/media"
    "github.com/soumya92/barista/modules/meminfo"
    "github.com/soumya92/barista/modules/netspeed"
    "github.com/soumya92/barista/modules/sysinfo"
    "github.com/soumya92/barista/modules/volume"
    "github.com/soumya92/barista/modules/volume/alsa"
    "github.com/soumya92/barista/modules/weather"
    "github.com/soumya92/barista/modules/weather/openweathermap"
    "github.com/soumya92/barista/oauth"
    "github.com/soumya92/barista/outputs"
    "github.com/soumya92/barista/pango"
    "github.com/soumya92/barista/pango/icons/fontawesome"
    "github.com/soumya92/barista/pango/icons/material"
    "github.com/soumya92/barista/pango/icons/mdi"
    "github.com/soumya92/barista/pango/icons/typicons"

    colorful "github.com/lucasb-eyer/go-colorful"
    "github.com/martinlindhe/unit"
    keyring "github.com/zalando/go-keyring"
)

var spacer = pango.Text(" ").XXSmall()

func truncate(in string, l int) string {
    if len([]rune(in)) <= l {
        return in
    }
    return string([]rune(in)[:l-1]) + "⋯"
}

func hms(d time.Duration) (h int, m int, s int) {
    h = int(d.Hours())
    m = int(d.Minutes()) % 60
    s = int(d.Seconds()) % 60
    return
}

func formatMediaTime(d time.Duration) string {
    h, m, s := hms(d)
    if h > 0 {
        return fmt.Sprintf("%d:%02d:%02d", h, m, s)
    }
    return fmt.Sprintf("%d:%02d", m, s)
}

func mediaFormatFunc(m media.Info) bar.Output {
    if m.PlaybackStatus == media.Stopped || m.PlaybackStatus == media.Disconnected {
        return nil
    }
    artist := truncate(m.Artist, 20)
    title := truncate(m.Title, 40-len(artist))
    if len(title) < 20 {
        artist = truncate(m.Artist, 40-len(title))
    }
    iconAndPosition := pango.Icon("fa-music").Color(colors.Hex("#f70"))
    if m.PlaybackStatus == media.Playing {
        iconAndPosition.Append(
            spacer, pango.Textf("%s/%s",
                formatMediaTime(m.Position()),
                formatMediaTime(m.Length)),
        )
    }
    return outputs.Pango(iconAndPosition, spacer, title, " - ", artist)
}

var startTaskManager = click.RunLeft("xfce4-taskmanager")

func home(path string) string {
    usr, err := user.Current()
    if err != nil {
        panic(err)
    }
    return filepath.Join(usr.HomeDir, path)
}

type freegeoipResponse struct {
    Lat float64 `json:"latitude"`
    Lng float64 `json:"longitude"`
}

func whereami() (lat float64, lng float64, err error) {
    resp, err := http.Get("https://freegeoip.app/json/")
    if err != nil {
        return 0, 0, err
    }
    var res freegeoipResponse
    err = json.NewDecoder(resp.Body).Decode(&res)
    if err != nil {
        return 0, 0, err
    }
    return res.Lat, res.Lng, nil
}

type autoWeatherProvider struct{}

func (a autoWeatherProvider) GetWeather() (weather.Weather, error) {
    lat, lng, err := whereami()
    if err != nil {
        return weather.Weather{}, err
    }
    return openweathermap.
        New("%%OWM_API_KEY%%").
        Coords(lat, lng).
        GetWeather()
}

func setupOauthEncryption() error {
    const service = "barista-sample-bar"
    var username string
    if u, err := user.Current(); err == nil {
        username = u.Username
    } else {
        username = fmt.Sprintf("user-%d", os.Getuid())
    }
    var secretBytes []byte
    // IMPORTANT: The oauth tokens used by some modules are very sensitive, so
    // we encrypt them with a random key and store that random key using
    // libsecret (gnome-keyring or equivalent). If no secret provider is
    // available, there is no way to store tokens (since the version of
    // sample-bar used for setup-oauth will have a different key from the one
    // running in i3bar). See also https://github.com/zalando/go-keyring#linux.
    secret, err := keyring.Get(service, username)
    if err == nil {
        secretBytes, err = base64.RawURLEncoding.DecodeString(secret)
    }
    if err != nil {
        secretBytes = make([]byte, 64)
        _, err := rand.Read(secretBytes)
        if err != nil {
            return err
        }
        secret = base64.RawURLEncoding.EncodeToString(secretBytes)
        keyring.Set(service, username, secret)
    }
    oauth.SetEncryptionKey(secretBytes)
    return nil
}

var gsuiteOauthConfig = []byte(`{"installed": {
    "client_id":"%%GOOGLE_CLIENT_ID%%",
    "project_id":"i3-barista",
    "auth_uri":"https://accounts.google.com/o/oauth2/auth",
    "token_uri":"https://www.googleapis.com/oauth2/v3/token",
    "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
    "client_secret":"%%GOOGLE_CLIENT_SECRET%%",
    "redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
}}`)

func main() {
    material.Load(home("Github/material-design-icons"))
    mdi.Load(home("Github/MaterialDesign-Webfont"))
    typicons.Load(home("Github/typicons.font"))
    fontawesome.Load(home("Github/Font-Awesome"))

    colors.LoadBarConfig()
    bg := colors.Scheme("background")
    fg := colors.Scheme("statusline")
    if fg != nil && bg != nil {
        iconColor := fg.Colorful().BlendHcl(bg.Colorful(), 0.5).Clamped()
        colors.Set("dim-icon", iconColor)
        _, _, v := fg.Colorful().Hsv()
        if v < 0.3 {
            v = 0.3
        }
        colors.Set("bad", colorful.Hcl(40, 1.0, v).Clamped())
        colors.Set("degraded", colorful.Hcl(90, 1.0, v).Clamped())
        colors.Set("good", colorful.Hcl(120, 1.0, v).Clamped())
    }

    if err := setupOauthEncryption(); err != nil {
        panic(fmt.Sprintf("Could not setup oauth token encryption: %v", err))
    }

    localtime := clock.Local().
        Output(time.Second, func(now time.Time) bar.Output {
            return outputs.Pango(
                pango.Icon("material-today").Color(colors.Scheme("dim-icon")),
                now.Format("Mon Jan 2 "),
                pango.Icon("material-access-time").Color(colors.Scheme("dim-icon")),
                now.Format("15:04:05"),
            ).OnClick(click.RunLeft("gsimplecal"))
        })

    // Weather information comes from OpenWeatherMap.
    // https://openweathermap.org/api.
    wthr := weather.New(autoWeatherProvider{}).Output(func(w weather.Weather) bar.Output {
        iconName := ""
        switch w.Condition {
        case weather.Thunderstorm,
            weather.TropicalStorm,
            weather.Hurricane:
            iconName = "stormy"
        case weather.Drizzle,
            weather.Hail:
            iconName = "shower"
        case weather.Rain:
            iconName = "downpour"
        case weather.Snow,
            weather.Sleet:
            iconName = "snow"
        case weather.Mist,
            weather.Smoke,
            weather.Whirls,
            weather.Haze,
            weather.Fog:
            iconName = "windy-cloudy"
        case weather.Clear:
            if !w.Sunset.IsZero() && time.Now().After(w.Sunset) {
                iconName = "night"
            } else {
                iconName = "sunny"
            }
        case weather.PartlyCloudy:
            iconName = "partly-sunny"
        case weather.Cloudy, weather.Overcast:
            iconName = "cloudy"
        case weather.Tornado,
            weather.Windy:
            iconName = "windy"
        }
        if iconName == "" {
            iconName = "warning-outline"
        } else {
            iconName = "weather-" + iconName
        }
        return outputs.Pango(
            pango.Icon("typecn-"+iconName), spacer,
            pango.Textf("%.1f℃", w.Temperature.Celsius()),
            pango.Textf(" (provided by %s)", w.Attribution).XSmall(),
        )
    })

    buildBattOutput := func(i battery.Info, disp *pango.Node) *bar.Segment {
        if i.Status == battery.Disconnected || i.Status == battery.Unknown {
            return nil
        }
        iconName := "battery"
        if i.Status == battery.Charging {
            iconName += "-charging"
        }
        tenth := i.RemainingPct() / 10
        switch {
        case tenth == 0:
            iconName += "-outline"
        case tenth < 10:
            iconName += fmt.Sprintf("-%d0", tenth)
        }
        out := outputs.Pango(pango.Icon("mdi-"+iconName), disp)
        switch {
        case i.RemainingPct() <= 5:
            out.Urgent(true)
        case i.RemainingPct() <= 15:
            out.Color(colors.Scheme("bad"))
        case i.RemainingPct() <= 25:
            out.Color(colors.Scheme("degraded"))
        }
        return out
    }
    var showBattPct, showBattTime func(battery.Info) bar.Output

    batt := battery.All()
    showBattPct = func(i battery.Info) bar.Output {
        out := buildBattOutput(i, pango.Textf("%d%%", i.RemainingPct()))
        if out == nil {
            return nil
        }
        return out.OnClick(click.Left(func() {
            batt.Output(showBattTime)
        }))
    }
    showBattTime = func(i battery.Info) bar.Output {
        rem := i.RemainingTime()
        out := buildBattOutput(i, pango.Textf(
            "%d:%02d", int(rem.Hours()), int(rem.Minutes())%60))
        if out == nil {
            return nil
        }
        return out.OnClick(click.Left(func() {
            batt.Output(showBattPct)
        }))
    }
    batt.Output(showBattPct)

    vol := volume.New(alsa.DefaultMixer()).Output(func(v volume.Volume) bar.Output {
        if v.Mute {
            return outputs.
                Pango(pango.Icon("fa-volume-mute"), spacer, "MUT").
                Color(colors.Scheme("degraded"))
        }
        iconName := "off"
        pct := v.Pct()
        if pct > 66 {
            iconName = "up"
        } else if pct > 33 {
            iconName = "down"
        }
        return outputs.Pango(
            pango.Icon("fa-volume-"+iconName),
            spacer,
            pango.Textf("%2d%%", pct),
        )
    })

    loadAvg := sysinfo.New().Output(func(s sysinfo.Info) bar.Output {
        out := outputs.Textf("%0.2f %0.2f", s.Loads[0], s.Loads[2])
        // Load averages are unusually high for a few minutes after boot.
        if s.Uptime < 10*time.Minute {
            // so don't add colours until 10 minutes after system start.
            return out
        }
        switch {
        case s.Loads[0] > 128, s.Loads[2] > 64:
            out.Urgent(true)
        case s.Loads[0] > 64, s.Loads[2] > 32:
            out.Color(colors.Scheme("bad"))
        case s.Loads[0] > 32, s.Loads[2] > 16:
            out.Color(colors.Scheme("degraded"))
        }
        out.OnClick(startTaskManager)
        return out
    })

    freeMem := meminfo.New().Output(func(m meminfo.Info) bar.Output {
        out := outputs.Pango(pango.Icon("material-memory"), format.IBytesize(m.Available()))
        freeGigs := m.Available().Gigabytes()
        switch {
        case freeGigs < 0.5:
            out.Urgent(true)
        case freeGigs < 1:
            out.Color(colors.Scheme("bad"))
        case freeGigs < 2:
            out.Color(colors.Scheme("degraded"))
        case freeGigs > 12:
            out.Color(colors.Scheme("good"))
        }
        out.OnClick(startTaskManager)
        return out
    })

    temp := cputemp.New().
        RefreshInterval(2 * time.Second).
        Output(func(temp unit.Temperature) bar.Output {
            out := outputs.Pango(
                pango.Icon("mdi-fan"), spacer,
                pango.Textf("%2d℃", int(temp.Celsius())),
            )
            switch {
            case temp.Celsius() > 90:
                out.Urgent(true)
            case temp.Celsius() > 70:
                out.Color(colors.Scheme("bad"))
            case temp.Celsius() > 60:
                out.Color(colors.Scheme("degraded"))
            }
            return out
        })

    sub := netlink.Any()
    iface := sub.Get().Name
    sub.Unsubscribe()
    net := netspeed.New(iface).
        RefreshInterval(2 * time.Second).
        Output(func(s netspeed.Speeds) bar.Output {
            return outputs.Pango(
                pango.Icon("fa-upload"), spacer, pango.Textf("%7s", format.Byterate(s.Tx)),
                pango.Text(" ").Small(),
                pango.Icon("fa-download"), spacer, pango.Textf("%7s", format.Byterate(s.Rx)),
            )
        })

    rhythmbox := media.New("rhythmbox").Output(mediaFormatFunc)

    grp, _ := collapsing.Group(net, temp, freeMem, loadAvg)

    ghNotify := github.New("%%GITHUB_CLIENT_ID%%", "%%GITHUB_CLIENT_SECRET%%").
        Output(func(n github.Notifications) bar.Output {
            if n.Total() == 0 {
                return nil
            }
            out := outputs.Group(
                pango.Icon("fab-github").
                    Concat(spacer).
                    ConcatTextf("%d", n.Total()))
            mentions := n["mention"] + n["team_mention"]
            if mentions > 0 {
                out.Append(spacer)
                out.Append(outputs.Pango(
                    pango.Icon("mdi-bell").
                        ConcatTextf("%d", mentions)).
                    Urgent(true))
            }
            return out.Glue().OnClick(
                click.RunLeft("xdg-open", "https://github.com/notifications"))
        })

    panic(barista.Run(
        rhythmbox,
        grp,
        ghNotify,
        vol,
        batt,
        wthr,
        localtime,
    ))
}