// 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" "barista.run" "barista.run/bar" "barista.run/base/click" "barista.run/base/watchers/netlink" "barista.run/colors" "barista.run/format" "barista.run/group/collapsing" "barista.run/modules/battery" "barista.run/modules/clock" "barista.run/modules/cputemp" "barista.run/modules/github" "barista.run/modules/media" "barista.run/modules/meminfo" "barista.run/modules/netspeed" "barista.run/modules/sysinfo" "barista.run/modules/volume" "barista.run/modules/volume/alsa" "barista.run/modules/weather" "barista.run/modules/weather/openweathermap" "barista.run/oauth" "barista.run/outputs" "barista.run/pango" "barista.run/pango/icons/fontawesome" "barista.run/pango/icons/material" "barista.run/pango/icons/mdi" "barista.run/pango/icons/typicons" "barista.run/testing/httpcache" colorful "github.com/lucasb-eyer/go-colorful" "github.com/martinlindhe/unit" keyring "github.com/zalando/go-keyring" ) func init() { http.DefaultTransport = httpcache.Wrap(http.DefaultTransport) } 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, )) }