samples/simple/simple.go
// 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,
))
}