├── .gitignore ├── .rubocop.yml ├── LICENSE ├── README.md ├── bar ├── bar.go ├── segment.go ├── segment_test.go ├── sink.go └── sink_test.go ├── barista.go ├── barista_test.go ├── base ├── click │ ├── buttons.go │ ├── buttons.rb │ ├── click.go │ └── click_test.go ├── notifier │ ├── notifier.go │ └── notifier_test.go ├── value │ ├── value.go │ └── value_test.go └── watchers │ ├── dbus │ ├── dbus.go │ ├── dbus_test.go │ ├── nameowner.go │ ├── nameowner_test.go │ ├── properties.go │ ├── properties_test.go │ ├── testbus.go │ ├── testbus_test.go │ ├── testobject.go │ ├── testobject_test.go │ ├── testservice.go │ └── testservice_test.go │ ├── file │ ├── file.go │ └── file_test.go │ ├── localtz │ ├── localtz.go │ └── localtz_test.go │ └── netlink │ ├── netlink.go │ ├── netlink_test.go │ ├── nl.go │ └── nl_test.go ├── build.sh ├── colors ├── colors.go └── colors_test.go ├── core ├── module.go ├── module_test.go ├── moduleset.go └── moduleset_test.go ├── format ├── bytes.go ├── bytes_test.go ├── siunit.go ├── siunit.rb ├── siunit_test.go ├── units.go └── units_test.go ├── go.mod ├── go.sum ├── group ├── collapsing │ ├── collapsing.go │ └── collapsing_test.go ├── cycling │ ├── cycling.go │ └── cycling_test.go ├── following │ ├── following.go │ └── following_test.go ├── group.go ├── group_test.go ├── modal │ ├── modal.go │ └── modal_test.go └── switching │ ├── switching.go │ └── switching_test.go ├── logging ├── id.go ├── id_test.go ├── logging.go ├── logging_test.go ├── nop.go ├── nop_test.go └── race_test.go ├── modules ├── battery │ ├── battery.go │ └── battery_test.go ├── bluetooth │ ├── adapter.go │ ├── adapter_test.go │ ├── device.go │ └── device_test.go ├── clock │ ├── clock.go │ └── clock_test.go ├── counter │ ├── counter.go │ └── counter_test.go ├── cpuload │ ├── cpuload.go │ └── cpuload_test.go ├── cputemp │ └── cputemp.go ├── diskio │ ├── diskio.go │ └── diskio_test.go ├── diskspace │ ├── diskspace.go │ └── diskspace_test.go ├── funcs │ ├── funcs.go │ └── funcs_test.go ├── github │ ├── github.go │ └── github_test.go ├── gsuite │ ├── calendar │ │ ├── calendar.go │ │ └── calendar_test.go │ └── gmail │ │ ├── gmail.go │ │ └── gmail_test.go ├── hwmon │ └── hwmon.go ├── internal │ └── temperature │ │ ├── hwmon.go │ │ ├── hwmon_test.go │ │ ├── module.go │ │ ├── thermalzone.go │ │ └── thermalzone_test.go ├── media │ ├── info.go │ ├── media.go │ └── media_test.go ├── meminfo │ ├── meminfo.go │ └── meminfo_test.go ├── meta │ ├── multicast │ │ ├── multicast.go │ │ └── multicast_test.go │ ├── reformat │ │ ├── reformat.go │ │ └── reformat_test.go │ ├── slot │ │ ├── slot.go │ │ └── slot_test.go │ └── split │ │ ├── split.go │ │ └── split_test.go ├── netinfo │ ├── netinfo.go │ └── netinfo_test.go ├── netspeed │ ├── netspeed.go │ └── netspeed_test.go ├── shell │ ├── shell.go │ ├── shell_test.go │ ├── tail.go │ └── tail_test.go ├── static │ ├── static.go │ └── static_test.go ├── sysinfo │ ├── sysinfo.go │ └── sysinfo_test.go ├── systemd │ ├── systemd.go │ └── systemd_test.go ├── volume │ ├── alsa │ │ ├── alsa.go │ │ ├── alsa_capi.go │ │ ├── alsa_capi_for_test.go │ │ ├── alsa_test.go │ │ └── capi.rb │ ├── pulseaudio │ │ └── pulse.go │ ├── volume.go │ └── volume_test.go ├── vpn │ ├── vpn.go │ └── vpn_test.go ├── weather │ ├── direction.go │ ├── direction_test.go │ ├── metar │ │ ├── metar.go │ │ ├── metar_test.go │ │ └── testdata │ │ │ ├── bad.xml │ │ │ ├── example.xml │ │ │ └── no-entries.xml │ ├── openweathermap │ │ ├── openweathermap.go │ │ ├── openweathermap_test.go │ │ └── testdata │ │ │ ├── bad.json │ │ │ ├── empty.json │ │ │ └── good.json.tpl │ ├── weather.go │ └── weather_test.go └── wlan │ ├── wlan.go │ └── wlan_test.go ├── oauth ├── crypt.go ├── crypt_test.go ├── oauth.go ├── oauth_test.go └── testdata │ ├── empty.json │ └── simple.json ├── outputs ├── group.go ├── group_test.go ├── outputs.go ├── outputs_test.go ├── timed.go ├── timed_test.go ├── timedelta.go └── timedelta_test.go ├── pango ├── attrs.go ├── icon.go ├── icon_test.go ├── icons │ ├── fontawesome │ │ ├── fontawesome.go │ │ └── fontawesome_test.go │ ├── icons.go │ ├── icons_test.go │ ├── material │ │ ├── material.go │ │ └── material_test.go │ ├── mdi │ │ ├── mdi.go │ │ └── mdi_test.go │ └── typicons │ │ ├── typicons.go │ │ └── typicons_test.go ├── kwattrs.rb ├── kwattrs_size.go ├── kwattrs_stretch.go ├── kwattrs_strikethrough.go ├── kwattrs_style.go ├── kwattrs_underline.go ├── kwattrs_variant.go ├── kwattrs_weight.go ├── pango.go └── pango_test.go ├── rb ├── gofile.rb └── run_cmd.rb ├── samples ├── i3status │ └── i3status.go ├── sample-bar │ ├── cachehttp.go │ └── sample-bar.go ├── simple │ └── simple.go ├── stable-api │ └── stable-api.go └── yubikey │ └── yubikey.go ├── sink ├── sink.go └── sink_test.go ├── test.sh ├── testing ├── bar │ ├── bar.go │ └── bar_test.go ├── capi │ ├── funcs.rb │ ├── generators.rb │ ├── main.rb │ └── types.rb ├── cron │ ├── cron.go │ └── cron_test.go ├── fail │ ├── fail.go │ └── fail_test.go ├── flakes.sh ├── githubfs │ ├── githubfs.go │ └── githubfs_test.go ├── httpcache │ ├── httpcache.go │ ├── httpcache_test.go │ └── testdata │ │ └── debug.tpl ├── httpclient │ ├── httpclient.go │ └── httpclient_test.go ├── httpserver │ ├── httpserver.go │ └── httpserver_test.go ├── mockio │ ├── mockio.go │ └── mockio_test.go ├── module │ ├── module.go │ └── module_test.go ├── notifier │ ├── notifier.go │ └── notifier_test.go ├── output │ ├── output.go │ └── output_test.go └── pango │ ├── pango.go │ └── pango_test.go └── timing ├── internal └── timerfd │ ├── nativeendian.go │ └── timerfd.go ├── misc.go ├── scheduler.go ├── scheduler_test.go ├── schedulerimpl_test.go ├── testmode.go ├── testmode_test.go ├── time_scheduler.go ├── time_scheduler_test.go ├── timerfd_scheduler.go ├── timerfd_scheduler_fallback.go ├── timerfd_scheduler_test.go └── timing.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | coverage.txt 16 | /profiles 17 | c.out 18 | .idea -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | AllCops: 16 | TargetRubyVersion: 2.3 17 | 18 | Style/FrozenStringLiteralComment: 19 | EnforcedStyle: never 20 | 21 | Metrics: 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 16 | Logo 17 | 18 | # Barista 19 | 20 | Barista is an i3 status bar written in golang. 21 | 22 | **This is not an official Google product** 23 | 24 | ## Features 25 | 26 | - Based on push rather than fixed interval polling. This allows immediate updates 27 | for many modules, like volume, media, shell, etc. 28 | 29 | - Produces a single binary via go build. This makes it easy to set up the bar 30 | executable, since no import paths, environment variables, et al. need to be 31 | configured. 32 | 33 | - Good click handlers (especially media and volume), since we can wait for a 34 | command and update the bar immediately rather than waiting for the next 'tick'. 35 | 36 | - Configuration is code, providing oodles of customization options without 37 | needing myriad configuration options in a file somewhere. If/then/else, loops, 38 | functions, variables, and even other go packages can all be used seamlessly. 39 | 40 | ## Usage 41 | 42 | See samples/sample-bar.go for a sample bar. 43 | 44 | To build your own bar, simply create a `package main` go file, 45 | import and configure the modules you wish to use, and call `barista.Run()`. 46 | 47 | To show your bar in i3, set the `status_command` of a `bar { ... }` section 48 | to be the newly built bar binary, e.g. 49 | 50 | ``` 51 | bar { 52 | position top 53 | status_command exec ~/bin/mybar 54 | font pango:DejaVu Sans Mono 10 55 | } 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /bar/sink.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package bar 16 | 17 | // Output updates the module's output on the bar. 18 | func (s Sink) Output(o Output) { 19 | s(o) 20 | } 21 | 22 | // Error is a convenience method that returns false and does nothing 23 | // when given a nil error, and outputs an error segment and returns 24 | // true when a non-nil error is given. 25 | func (s Sink) Error(e error) bool { 26 | if e != nil { 27 | s(ErrorSegment(e)) 28 | return true 29 | } 30 | return false 31 | } 32 | -------------------------------------------------------------------------------- /bar/sink_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package bar 16 | 17 | import ( 18 | "io" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | func TestOutput(t *testing.T) { 25 | var sink Sink 26 | ch := make(chan Output, 1) 27 | sink = func(o Output) { ch <- o } 28 | 29 | sink.Output(TextSegment("foo")) 30 | select { 31 | case out := <-ch: 32 | txt, _ := out.Segments()[0].Content() 33 | require.Equal(t, "foo", txt) 34 | default: 35 | require.Fail(t, "expected output on Output(...)") 36 | } 37 | 38 | sink.Output(nil) 39 | require.Nil(t, <-ch) 40 | } 41 | 42 | func TestError(t *testing.T) { 43 | var sink Sink 44 | ch := make(chan Output, 1) 45 | sink = func(o Output) { ch <- o } 46 | 47 | require.False(t, sink.Error(nil), "nil error returns false") 48 | select { 49 | case <-ch: 50 | require.Fail(t, "Should not send any output on Error(nil)") 51 | default: 52 | // test passed. 53 | } 54 | 55 | require.True(t, sink.Error(io.EOF), "non-nil error returns true") 56 | select { 57 | case out := <-ch: 58 | require.Error(t, out.Segments()[0].GetError(), 59 | "output sent on Error(...) has error segment") 60 | default: 61 | require.Fail(t, "Expected an error output on Error(...)") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /base/click/buttons.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative '../../rb/gofile.rb' 16 | 17 | SCROLLS = %w[Left Right Up Down].freeze 18 | BUTTONS = %w[Left Right Middle Back Forward].freeze 19 | 20 | method_and_btn = BUTTONS.map { |n| [n, "Button#{n}"] } + 21 | SCROLLS.map { |n| ["Scroll#{n}", "Scroll#{n}"] } 22 | 23 | write_go_file('buttons.go') do |out| 24 | out.puts <<~HEADER 25 | package click 26 | 27 | import "github.com/soumya92/barista/bar" 28 | HEADER 29 | 30 | method_and_btn.each do |method, btn| 31 | out.write(<<~METHOD) 32 | 33 | // #{method} creates a click handler that invokes the given function 34 | // when a #{btn} event is received. 35 | func #{method}(do func()) func(bar.Event) { 36 | \treturn #{method}E(DiscardEvent(do)) 37 | } 38 | 39 | // #{method}E wraps the click handler so that it is only triggered by a 40 | // #{btn} event. 41 | func #{method}E(handler func(bar.Event)) func(bar.Event) { 42 | \treturn ButtonE(handler, bar.#{btn}) 43 | } 44 | METHOD 45 | end 46 | method_and_btn.each do |method, btn| 47 | out.write(<<~METHOD) 48 | 49 | // #{method} invokes the given function on #{btn} events. 50 | func (m Map) #{method}(do func()) Map { 51 | \treturn m.#{method}E(DiscardEvent(do)) 52 | } 53 | 54 | // #{method}E sets the click handler for #{btn} events. 55 | func (m Map) #{method}E(handler func(bar.Event)) Map { 56 | \treturn m.Set(bar.#{btn}, handler) 57 | } 58 | METHOD 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /base/notifier/notifier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package notifier provides a channel that can send update notifications. 17 | Specifically, a notifier automatically coalesces multiple notifications 18 | such that if a previous notification is already pending, a new notification 19 | will not be created. This is useful in scenarios like formatting changes, 20 | where if multiple updates come in before the first one is processed, it 21 | is preferable to apply just the final format, ignoring the intermediate ones. 22 | */ 23 | package notifier 24 | 25 | import ( 26 | "sync" 27 | 28 | l "github.com/soumya92/barista/logging" 29 | ) 30 | 31 | // New constructs a new notifier. It returns a func that triggers a notification, 32 | // and a <-chan that consumes these notifications. 33 | func New() (func(), <-chan struct{}) { 34 | ch := make(chan struct{}, 1) 35 | return func() { notify(ch) }, ch 36 | } 37 | 38 | func notify(ch chan<- struct{}) { 39 | l.Fine("Notify %s", l.ID(ch)) 40 | select { 41 | case ch <- struct{}{}: 42 | default: 43 | } 44 | } 45 | 46 | // Source can be used to notify multiple listeners of a signal. It provides both 47 | // one-shot listeners that close the channel on the next signal, and continuous 48 | // listeners that emit a struct{} on every signal (but need to be cleaned up). 49 | type Source struct { 50 | obs []chan struct{} 51 | subs map[<-chan struct{}]func() 52 | mu sync.Mutex 53 | } 54 | 55 | // Next returns a channel that will be closed on the next signal. 56 | func (s *Source) Next() <-chan struct{} { 57 | ch := make(chan struct{}) 58 | s.mu.Lock() 59 | defer s.mu.Unlock() 60 | s.obs = append(s.obs, ch) 61 | return ch 62 | } 63 | 64 | // Subscribe returns a channel that will receive an empty struct{} on the next 65 | // signal, and a func to close the subscription. 66 | func (s *Source) Subscribe() (sub <-chan struct{}, done func()) { 67 | fn, sub := New() 68 | s.mu.Lock() 69 | defer s.mu.Unlock() 70 | if s.subs == nil { 71 | s.subs = map[<-chan struct{}]func(){} 72 | } 73 | s.subs[sub] = fn 74 | return sub, func() { 75 | s.mu.Lock() 76 | defer s.mu.Unlock() 77 | delete(s.subs, sub) 78 | } 79 | } 80 | 81 | // Notify notifies all interested listeners. 82 | func (s *Source) Notify() { 83 | s.mu.Lock() 84 | defer s.mu.Unlock() 85 | for _, o := range s.obs { 86 | close(o) 87 | } 88 | s.obs = nil 89 | for _, f := range s.subs { 90 | f() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /base/watchers/dbus/dbus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package dbus 16 | 17 | import ( 18 | "errors" 19 | "os" 20 | "sync/atomic" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestBusTypes(t *testing.T) { 27 | if os.Getenv("CI") != "true" { 28 | require.NotPanics(t, func() { Session() }, "session bus") 29 | require.NotPanics(t, func() { System() }, "system bus") 30 | } 31 | 32 | // To allow -count > 1 tests. 33 | testBusInstance = atomic.Value{} 34 | 35 | require.Panics(t, func() { Test() }, "test bus before setup") 36 | SetupTestBus() 37 | require.NotPanics(t, func() { Test() }, "test bus after setup") 38 | 39 | require.Panics(t, func() { connect(nil, errors.New("something")) }) 40 | } 41 | 42 | func TestExpandAndShorten(t *testing.T) { 43 | require := require.New(t) 44 | 45 | require.Equal("com.example.service.Method", 46 | expand("com.example.service", "Method")) 47 | require.Equal("com.example.service.Method.SubMethod", 48 | expand("com.example.service", ".Method.SubMethod")) 49 | require.Equal("net.example.service.Method", 50 | expand("com.example.service", "net.example.service.Method")) 51 | 52 | require.Equal("Method", 53 | shorten("com.example.service", "com.example.service.Method")) 54 | require.Equal(".Method.SubMethod", 55 | shorten("com.example.service", "com.example.service.Method.SubMethod")) 56 | require.Equal("net.example.service.Method", 57 | shorten("com.example.service", "net.example.service.Method")) 58 | require.Equal("com.example.service2.Method", 59 | shorten("com.example.service", "com.example.service2.Method")) 60 | } 61 | 62 | func TestMakeDBusName(t *testing.T) { 63 | require := require.New(t) 64 | 65 | require.Equal(dbusName{"com.example.foo", "Service"}, 66 | makeDbusName("com.example.foo.Service")) 67 | require.Equal(dbusName{"com.example", "foo"}, 68 | makeDbusName("com.example.foo")) 69 | require.Equal(dbusName{"com", "example"}, 70 | makeDbusName("com.example")) 71 | require.Equal(dbusName{"", "example"}, 72 | makeDbusName("example")) 73 | 74 | for _, s := range []string{ 75 | "com.example.foo.Service", 76 | "com.example.foo", 77 | "com.example", 78 | } { 79 | require.Equal(s, makeDbusName(s).String(), 80 | "%s -> dbus -> string != %s", s, s) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | # Save keys to ~/.config/barista/keys to avoid leaking them to git. 20 | # Or use your preferred way to set them as environment variables. 21 | CONFIG_DIR="${XDG_CONFIG_HOME:-"$HOME/.config"}" 22 | [ -e "$CONFIG_DIR/barista/keys" ] && . "$CONFIG_DIR/barista/keys" 23 | 24 | # Any KEYS will be inserted into the sample bar by replacing 25 | # '%%$NAME_OF_KEY%%' with the value of $NAME_OF_KEY from the environment. 26 | # e.g. '%%OWM_API_KEY%%' in the go file will be replaced with the value 27 | # of $OWM_API_KEY. 28 | KEYS=( 29 | 'GITHUB_CLIENT_ID' 'GITHUB_CLIENT_SECRET' 30 | 'GOOGLE_CLIENT_ID' 'GOOGLE_CLIENT_SECRET' 31 | 'OWM_API_KEY' 32 | ) 33 | 34 | TARGET_FILE="./samples/sample-bar/sample-bar.go" 35 | 36 | # Save the current sample-bar, so we can revert it after building, to 37 | # prevent accidentally checking in the keys. We can't use git checkout 38 | # because the file could have other modifications that aren't committed. 39 | BACKUP_FILE="$(mktemp)" 40 | cp "$TARGET_FILE" "$BACKUP_FILE" 41 | function restore { 42 | cp "$BACKUP_FILE" "$TARGET_FILE" 43 | rm -f "$BACKUP_FILE" 44 | } 45 | trap restore EXIT 46 | 47 | for KEY in ${KEYS[@]}; do 48 | if [ -n "${!KEY}" ]; then 49 | sed -i "s/%%${KEY}%%/${!KEY}/g" "$TARGET_FILE" 50 | else 51 | echo "Skipping $KEY, value not set" >&2 52 | fi 53 | done 54 | 55 | # Build the sample bar with all the keys set. Pass all arguments to the 56 | # `go build` command, allowing e.g. `./build.sh -o ~/bin/mybar`, or even 57 | # `./build.sh -race -tags debuglog`. 58 | go build "$@" 59 | 60 | -------------------------------------------------------------------------------- /core/moduleset.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package core 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/soumya92/barista/bar" 21 | l "github.com/soumya92/barista/logging" 22 | "github.com/soumya92/barista/sink" 23 | ) 24 | 25 | // ModuleSet is a group of modules. It provides a channel for identifying module 26 | // updates, and methods to get the last output of the set or a specific module. 27 | type ModuleSet struct { 28 | modules []*Module 29 | updateCh chan int 30 | outputs []bar.Segments 31 | outputsMu sync.RWMutex 32 | } 33 | 34 | // NewModuleSet creates a ModuleSet with the given modules. 35 | func NewModuleSet(modules []bar.Module) *ModuleSet { 36 | set := &ModuleSet{ 37 | modules: make([]*Module, len(modules)), 38 | outputs: make([]bar.Segments, len(modules)), 39 | updateCh: make(chan int), 40 | } 41 | for i, m := range modules { 42 | l.Fine("%s added as %s[%d]", l.ID(m), l.ID(set), i) 43 | set.modules[i] = NewModule(m) 44 | } 45 | return set 46 | } 47 | 48 | // Stream starts streaming all modules and returns a channel that receives the 49 | // index of the module any time one updates with new output. 50 | func (m *ModuleSet) Stream() <-chan int { 51 | for i, mod := range m.modules { 52 | go mod.Stream(m.sinkFn(i)) 53 | } 54 | return m.updateCh 55 | } 56 | 57 | func (m *ModuleSet) sinkFn(idx int) bar.Sink { 58 | return sink.Func(func(out bar.Segments) { 59 | l.Fine("%s new output from %s", 60 | l.ID(m), l.ID(m.modules[idx].original)) 61 | m.outputsMu.Lock() 62 | m.outputs[idx] = out 63 | m.outputsMu.Unlock() 64 | m.updateCh <- idx 65 | }) 66 | } 67 | 68 | // Len returns the number of modules in this ModuleSet. 69 | func (m *ModuleSet) Len() int { 70 | return len(m.modules) 71 | } 72 | 73 | // LastOutput returns the last output from the module at a specific position. 74 | // If the module has not yet updated, an empty output will be used. 75 | func (m *ModuleSet) LastOutput(idx int) bar.Segments { 76 | m.outputsMu.RLock() 77 | defer m.outputsMu.RUnlock() 78 | return m.outputs[idx] 79 | } 80 | 81 | // LastOutputs returns the last output from all modules in order. The returned 82 | // slice will have exactly Len() elements, and if a module has not yet updated 83 | // an empty output will be placed in its position. 84 | func (m *ModuleSet) LastOutputs() []bar.Segments { 85 | m.outputsMu.RLock() 86 | defer m.outputsMu.RUnlock() 87 | cp := make([]bar.Segments, len(m.outputs)) 88 | copy(cp, m.outputs) 89 | return cp 90 | } 91 | -------------------------------------------------------------------------------- /core/moduleset_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package core 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/soumya92/barista/bar" 22 | testModule "github.com/soumya92/barista/testing/module" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func nextUpdate(t *testing.T, ch <-chan int, formatAndArgs ...interface{}) int { 28 | select { 29 | case idx := <-ch: 30 | return idx 31 | case <-time.After(time.Second): 32 | require.Fail(t, "No update from moduleset", formatAndArgs...) 33 | } 34 | return -1 35 | } 36 | 37 | func assertNoUpdate(t *testing.T, ch <-chan int, formatAndArgs ...interface{}) { 38 | select { 39 | case <-ch: 40 | require.Fail(t, "Unexpected update from moduleset", formatAndArgs...) 41 | case <-time.After(10 * time.Millisecond): 42 | // test passed. 43 | } 44 | } 45 | 46 | func TestModuleSet(t *testing.T) { 47 | tms := []*testModule.TestModule{ 48 | testModule.New(t), 49 | testModule.New(t), 50 | testModule.New(t), 51 | } 52 | ms := NewModuleSet([]bar.Module{tms[0], tms[1], tms[2]}) 53 | updateCh := ms.Stream() 54 | assertNoUpdate(t, updateCh, "on start") 55 | for _, tm := range tms { 56 | tm.AssertStarted("on moduleset stream") 57 | } 58 | require.Equal(t, 3, ms.Len()) 59 | 60 | tms[1].OutputText("foo") 61 | require.Equal(t, 1, nextUpdate(t, updateCh, "on output"), 62 | "update notification on new output from module") 63 | 64 | tms[0].OutputText("baz") 65 | require.Equal(t, 0, nextUpdate(t, updateCh, "on update"), 66 | "update notification on new output from module") 67 | 68 | require.Empty(t, ms.LastOutput(2), "without any output") 69 | txt, _ := ms.LastOutput(0)[0].Content() 70 | require.Equal(t, "baz", txt) 71 | 72 | out := ms.LastOutputs() 73 | require.Equal(t, 1, len(out[0])) 74 | txt, _ = out[0][0].Content() 75 | require.Equal(t, "baz", txt) 76 | require.Equal(t, 1, len(out[1])) 77 | txt, _ = out[1][0].Content() 78 | require.Equal(t, "foo", txt) 79 | require.Empty(t, out[2]) 80 | } 81 | -------------------------------------------------------------------------------- /format/bytes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/dustin/go-humanize" 21 | "github.com/martinlindhe/unit" 22 | ) 23 | 24 | // Bytesize formats a Datasize in SI units using go-humanize. 25 | // e.g. Bytesize(10 * unit.Megabyte) == "10 MB" 26 | func Bytesize(v unit.Datasize) string { 27 | intval := uint64(v.Bytes()) 28 | return humanize.Bytes(intval) 29 | } 30 | 31 | // IBytesize formats a Datasize in IEC units using go-humanize. 32 | // e.g. IBytesize(10 * unit.Mebibyte) == "10 MiB" 33 | func IBytesize(v unit.Datasize) string { 34 | intval := uint64(v.Bytes()) 35 | return humanize.IBytes(intval) 36 | } 37 | 38 | // Byterate formats a Datarate in SI units using go-humanize. 39 | // e.g. Byterate(10 * unit.MegabytePerSecond) == "10 MB/s" 40 | func Byterate(v unit.Datarate) string { 41 | intval := uint64(v.BytesPerSecond()) 42 | return fmt.Sprintf("%s/s", humanize.Bytes(intval)) 43 | } 44 | 45 | // IByterate formats a Datarate in IEC units using go-humanize. 46 | // e.g. Byterate(10 * unit.MebibytePerSecond) == "10 MiB/s" 47 | func IByterate(v unit.Datarate) string { 48 | intval := uint64(v.BytesPerSecond()) 49 | return fmt.Sprintf("%s/s", humanize.IBytes(intval)) 50 | } 51 | -------------------------------------------------------------------------------- /format/bytes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/martinlindhe/unit" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | func TestDataSize(t *testing.T) { 25 | require := require.New(t) 26 | require.Equal("10 KiB", IBytesize(10*unit.Kibibyte)) 27 | require.Equal("10 kB", Bytesize(10*unit.Kilobyte)) 28 | require.Equal("9.8 KiB", IBytesize(10*unit.Kilobyte)) 29 | } 30 | 31 | func TestDataRate(t *testing.T) { 32 | require := require.New(t) 33 | require.Equal("10 KiB/s", IByterate(10*unit.KibibytePerSecond)) 34 | require.Equal("10 kB/s", Byterate(10*1000*8*unit.BitPerSecond)) 35 | require.Equal("9.8 KiB/s", IByterate(10*1000*8*unit.BitPerSecond)) 36 | } 37 | -------------------------------------------------------------------------------- /format/siunit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by siunit.rb; DO NOT EDIT. 16 | 17 | package format 18 | 19 | import "github.com/martinlindhe/unit" 20 | 21 | // SIUnit formats a unit.Unit value to an appropriately scaled base unit. 22 | // For example, SIUnit(length) is equivalent to SI(length.Meters(), "m"). 23 | // For non-base units (e.g. feet), use SI(length.Feet(), "ft"). 24 | func SIUnit(val interface{}) (Value, bool) { 25 | switch v := val.(type) { 26 | case unit.Acceleration: 27 | return SI(v.MetersPerSecondSquared(), "m/s²"), true 28 | case unit.Angle: 29 | return SI(v.Radians(), "rad"), true 30 | case unit.Area: 31 | return SI(v.SquareMeters(), "m²"), true 32 | case unit.Datarate: 33 | return SI(v.BytesPerSecond(), "B/s"), true 34 | case unit.Datasize: 35 | return SI(v.Bytes(), "B"), true 36 | case unit.ElectricCurrent: 37 | return SI(v.Amperes(), "A"), true 38 | case unit.Energy: 39 | return SI(v.Joules(), "J"), true 40 | case unit.Force: 41 | return SI(v.Newtons(), "N"), true 42 | case unit.Frequency: 43 | return SI(v.Hertz(), "Hz"), true 44 | case unit.Length: 45 | return SI(v.Meters(), "m"), true 46 | case unit.Mass: 47 | return SI(v.Grams(), "g"), true 48 | case unit.Power: 49 | return SI(v.Watts(), "W"), true 50 | case unit.Pressure: 51 | return SI(v.Pascals(), "Pa"), true 52 | case unit.Speed: 53 | return SI(v.MetersPerSecond(), "m/s"), true 54 | case unit.Voltage: 55 | return SI(v.Volts(), "V"), true 56 | case unit.Volume: 57 | return SI(v.CubicMeters(), "m³"), true 58 | case unit.AmountOfSubstance: 59 | return SI(v.Moles(), "mol"), true 60 | case unit.ElectricalConductance: 61 | return SI(v.Siemens(), "S"), true 62 | case unit.ElectricalResistance: 63 | return SI(v.Ohms(), "Ω"), true 64 | case unit.Illuminance: 65 | return SI(v.Lux(), "lx"), true 66 | case unit.LuminousFlux: 67 | return SI(v.Lumen(), "lm"), true 68 | case unit.LuminousIntensity: 69 | return SI(v.Candela(), "cd"), true 70 | } 71 | return Value{}, false 72 | } 73 | -------------------------------------------------------------------------------- /format/siunit.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative '../rb/gofile.rb' 16 | 17 | Unit = Struct.new(:type, :base, :unit) 18 | UNITS = [ 19 | Unit.new('Acceleration', 'MetersPerSecondSquared', 'm/s²'), 20 | Unit.new('Angle', 'Radians', 'rad'), 21 | Unit.new('Area', 'SquareMeters', 'm²'), 22 | Unit.new('Datarate', 'BytesPerSecond', 'B/s'), 23 | Unit.new('Datasize', 'Bytes', 'B'), 24 | Unit.new('ElectricCurrent', 'Amperes', 'A'), 25 | Unit.new('Energy', 'Joules', 'J'), 26 | Unit.new('Force', 'Newtons', 'N'), 27 | Unit.new('Frequency', 'Hertz', 'Hz'), 28 | Unit.new('Length', 'Meters', 'm'), 29 | Unit.new('Mass', 'Grams', 'g'), 30 | Unit.new('Power', 'Watts', 'W'), 31 | Unit.new('Pressure', 'Pascals', 'Pa'), 32 | Unit.new('Speed', 'MetersPerSecond', 'm/s'), 33 | Unit.new('Voltage', 'Volts', 'V'), 34 | Unit.new('Volume', 'CubicMeters', 'm³'), 35 | Unit.new('AmountOfSubstance', 'Moles', 'mol'), 36 | Unit.new('ElectricalConductance', 'Siemens', 'S'), 37 | Unit.new('ElectricalResistance', 'Ohms', 'Ω'), 38 | Unit.new('Illuminance', 'Lux', 'lx'), 39 | Unit.new('LuminousFlux', 'Lumen', 'lm'), 40 | Unit.new('LuminousIntensity', 'Candela', 'cd') 41 | ].freeze 42 | 43 | write_go_file('siunit.go') do |out| 44 | out.write <<~HEADER 45 | package format 46 | 47 | import "github.com/martinlindhe/unit" 48 | 49 | // SIUnit formats a unit.Unit value to an appropriately scaled base unit. 50 | // For example, SIUnit(length) is equivalent to SI(length.Meters(), "m"). 51 | // For non-base units (e.g. feet), use SI(length.Feet(), "ft"). 52 | func SIUnit(val interface{}) (Value, bool) { 53 | \tswitch v := val.(type) { 54 | HEADER 55 | 56 | UNITS.each do |u| 57 | out.write <<~CASE 58 | \tcase unit.#{u.type}: 59 | \t\treturn SI(v.#{u.base}(), "#{u.unit}"), true 60 | CASE 61 | end 62 | 63 | out.write <<~FOOTER 64 | \t} 65 | \treturn Value{}, false 66 | } 67 | FOOTER 68 | end 69 | 70 | write_go_file('siunit_test.go') do |out| 71 | out.write <<~HEADER 72 | package format 73 | 74 | import "github.com/martinlindhe/unit" 75 | 76 | // An example for each unit that can be handled by the SIUint function. This 77 | // is used to test that all declared units in the unit package are handled. 78 | var siUnitsHandled = map[string]interface{}{ 79 | HEADER 80 | UNITS.each do |u| 81 | out.puts "\t\"#{u.type}\": unit.#{u.type}(1)," 82 | end 83 | out.puts '}' 84 | end 85 | -------------------------------------------------------------------------------- /format/siunit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by siunit.rb; DO NOT EDIT. 16 | 17 | package format 18 | 19 | import "github.com/martinlindhe/unit" 20 | 21 | // An example for each unit that can be handled by the SIUint function. This 22 | // is used to test that all declared units in the unit package are handled. 23 | var siUnitsHandled = map[string]interface{}{ 24 | "Acceleration": unit.Acceleration(1), 25 | "Angle": unit.Angle(1), 26 | "Area": unit.Area(1), 27 | "Datarate": unit.Datarate(1), 28 | "Datasize": unit.Datasize(1), 29 | "ElectricCurrent": unit.ElectricCurrent(1), 30 | "Energy": unit.Energy(1), 31 | "Force": unit.Force(1), 32 | "Frequency": unit.Frequency(1), 33 | "Length": unit.Length(1), 34 | "Mass": unit.Mass(1), 35 | "Power": unit.Power(1), 36 | "Pressure": unit.Pressure(1), 37 | "Speed": unit.Speed(1), 38 | "Voltage": unit.Voltage(1), 39 | "Volume": unit.Volume(1), 40 | "AmountOfSubstance": unit.AmountOfSubstance(1), 41 | "ElectricalConductance": unit.ElectricalConductance(1), 42 | "ElectricalResistance": unit.ElectricalResistance(1), 43 | "Illuminance": unit.Illuminance(1), 44 | "LuminousFlux": unit.LuminousFlux(1), 45 | "LuminousIntensity": unit.LuminousIntensity(1), 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/soumya92/barista 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/coreos/go-systemd/v22 v22.5.0 7 | github.com/dustin/go-humanize v1.0.1 8 | github.com/fsnotify/fsnotify v1.7.0 9 | github.com/godbus/dbus/v5 v5.1.0 10 | github.com/jfreymuth/pulse v0.1.1 11 | github.com/lucasb-eyer/go-colorful v1.2.0 12 | github.com/martinlindhe/unit v0.0.0-20220817221856-f7b595b5f97e 13 | github.com/maximbaz/yubikey-touch-detector v0.0.0-20220526224932-a2161c415e60 14 | github.com/spf13/afero v1.11.0 15 | github.com/stretchr/testify v1.9.0 16 | github.com/vishvananda/netlink v1.1.0 17 | github.com/zalando/go-keyring v0.2.5 18 | golang.org/x/crypto v0.24.0 19 | golang.org/x/net v0.26.0 20 | golang.org/x/oauth2 v0.19.0 21 | golang.org/x/sys v0.21.0 22 | golang.org/x/time v0.5.0 23 | google.golang.org/api v0.174.0 24 | gopkg.in/yaml.v2 v2.4.0 25 | ) 26 | 27 | require ( 28 | cloud.google.com/go/auth v0.2.0 // indirect 29 | cloud.google.com/go/auth/oauth2adapt v0.2.0 // indirect 30 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 31 | github.com/alessio/shellescape v1.4.1 // indirect 32 | github.com/danieljoos/wincred v1.2.0 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/esiqveland/notify v0.11.1 // indirect 35 | github.com/felixge/httpsnoop v1.0.4 // indirect 36 | github.com/go-logr/logr v1.4.1 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 39 | github.com/golang/protobuf v1.5.4 // indirect 40 | github.com/google/s2a-go v0.1.7 // indirect 41 | github.com/google/uuid v1.6.0 // indirect 42 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 43 | github.com/googleapis/gax-go/v2 v2.12.3 // indirect 44 | github.com/pmezard/go-difflib v1.0.0 // indirect 45 | github.com/rjeczalik/notify v0.9.2 // indirect 46 | github.com/scylladb/go-set v1.0.2 // indirect 47 | github.com/sirupsen/logrus v1.9.0 // indirect 48 | github.com/vishvananda/netns v0.0.1 // indirect 49 | github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810 // indirect 50 | go.opencensus.io v0.24.0 // indirect 51 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 52 | go.opentelemetry.io/otel v1.24.0 // indirect 53 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 54 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 55 | golang.org/x/text v0.16.0 // indirect 56 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect 57 | google.golang.org/grpc v1.63.0 // indirect 58 | google.golang.org/protobuf v1.33.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /group/collapsing/collapsing_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package collapsing 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | "github.com/soumya92/barista/base/click" 22 | "github.com/soumya92/barista/outputs" 23 | testBar "github.com/soumya92/barista/testing/bar" 24 | testModule "github.com/soumya92/barista/testing/module" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestCollapsing(t *testing.T) { 30 | testBar.New(t) 31 | 32 | tm0 := testModule.New(t) 33 | tm1 := testModule.New(t) 34 | tm2 := testModule.New(t) 35 | 36 | grp, ctrl := Group(tm0, tm1, tm2) 37 | tm0.AssertNotStarted("on group creation") 38 | tm1.AssertNotStarted() 39 | tm2.AssertNotStarted() 40 | 41 | testBar.Run(grp) 42 | tm0.AssertStarted("on stream") 43 | tm1.AssertStarted() 44 | tm2.AssertStarted() 45 | 46 | tm0.OutputText("a") 47 | out := testBar.NextOutput() 48 | out.AssertText([]string{"+"}, "starts collapsed") 49 | 50 | tm1.OutputText("b") 51 | testBar.AssertNoOutput("while collapsed") 52 | require.False(t, ctrl.Expanded()) 53 | 54 | out.At(0).LeftClick() 55 | testBar.NextOutput().AssertText([]string{">", "a", "b", "<"}, 56 | "Expands on click, uses previous output") 57 | 58 | require.True(t, ctrl.Expanded()) 59 | tm2.OutputText("c") 60 | testBar.NextOutput().AssertText([]string{">", "a", "b", "c", "<"}, 61 | "Updates immediately when expanded") 62 | 63 | ctrl.Collapse() 64 | testBar.NextOutput().AssertText([]string{"+"}) 65 | 66 | ctrl.Collapse() 67 | testBar.AssertNoOutput("no change when already collapsed") 68 | 69 | ctrl.Expand() 70 | out = testBar.NextOutput() 71 | out.AssertText([]string{">", "a", "b", "c", "<"}, 72 | "Uses last output when re-expanded") 73 | 74 | out.At(2).LeftClick() 75 | tm1.AssertClicked("after expansion") 76 | 77 | out.At(4).LeftClick() 78 | testBar.NextOutput().AssertText([]string{"+"}) 79 | require.False(t, ctrl.Expanded()) 80 | 81 | ctrl.ButtonFunc(func(c Controller) (start, end bar.Output) { 82 | if c.Expanded() { 83 | return outputs.Text("->").OnClick(click.Left(c.Collapse)), 84 | outputs.Text("<-").OnClick(click.Left(c.Collapse)) 85 | } 86 | return outputs.Text("<->").OnClick(click.Left(c.Expand)), nil 87 | }) 88 | testBar.NextOutput().AssertText([]string{"<->"}, 89 | "On button func change") 90 | 91 | ctrl.Toggle() 92 | testBar.NextOutput().AssertText([]string{"->", "a", "b", "c", "<-"}, 93 | "On expansion with custom button func") 94 | } 95 | -------------------------------------------------------------------------------- /group/cycling/cycling.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cycling provides a group that continuously cycles between 16 | // all modules at a fixed interval. 17 | package cycling 18 | 19 | import ( 20 | "sync" 21 | "time" 22 | 23 | "github.com/soumya92/barista/bar" 24 | "github.com/soumya92/barista/base/notifier" 25 | "github.com/soumya92/barista/group" 26 | l "github.com/soumya92/barista/logging" 27 | "github.com/soumya92/barista/timing" 28 | ) 29 | 30 | // Controller provides an interface to control a collapsing group. 31 | type Controller interface { 32 | SetInterval(time.Duration) 33 | } 34 | 35 | // grouper implements a cycling grouper. 36 | type grouper struct { 37 | current int 38 | count int 39 | scheduler *timing.Scheduler 40 | 41 | sync.Mutex 42 | notifyCh <-chan struct{} 43 | notifyFn func() 44 | } 45 | 46 | // Group returns a new cycling group with the given interval, 47 | // and a linked Controller. 48 | func Group(interval time.Duration, m ...bar.Module) (bar.Module, Controller) { 49 | g := &grouper{count: len(m), scheduler: timing.NewScheduler()} 50 | g.scheduler.Every(interval) 51 | g.notifyFn, g.notifyCh = notifier.New() 52 | go g.cycle() 53 | return group.New(g, m...), g 54 | } 55 | 56 | func (g *grouper) Visible(idx int) bool { return g.current == idx } 57 | 58 | func (g *grouper) Buttons() (start, end bar.Output) { return nil, nil } 59 | 60 | func (g *grouper) Signal() <-chan struct{} { return g.notifyCh } 61 | 62 | func (g *grouper) cycle() { 63 | for range g.scheduler.C { 64 | g.Lock() 65 | l.Fine("%s %d++", l.ID(g), g.current) 66 | g.current = (g.current + 1) % g.count 67 | g.Unlock() 68 | g.notifyFn() 69 | } 70 | } 71 | 72 | func (g *grouper) SetInterval(interval time.Duration) { 73 | g.scheduler.Every(interval) 74 | } 75 | -------------------------------------------------------------------------------- /group/cycling/cycling_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cycling 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | testBar "github.com/soumya92/barista/testing/bar" 22 | testModule "github.com/soumya92/barista/testing/module" 23 | "github.com/soumya92/barista/timing" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestCycling(t *testing.T) { 29 | testBar.New(t) 30 | 31 | tm0 := testModule.New(t) 32 | tm1 := testModule.New(t) 33 | tm2 := testModule.New(t) 34 | 35 | grp, ctrl := Group(time.Second, tm0, tm1, tm2) 36 | tm0.AssertNotStarted("on group creation") 37 | tm1.AssertNotStarted() 38 | tm2.AssertNotStarted() 39 | 40 | testBar.Run(grp) 41 | tm0.AssertStarted("on stream") 42 | tm1.AssertStarted() 43 | tm2.AssertStarted() 44 | 45 | start := timing.Now() 46 | 47 | testBar.NextOutput().AssertEmpty("With no module output") 48 | tm0.OutputText("a") 49 | testBar.NextOutput().AssertText([]string{"a"}, 50 | "on active module update") 51 | require.Equal(t, start, timing.Now(), "First output is immediate") 52 | 53 | testBar.Tick() 54 | testBar.NextOutput().AssertEmpty( 55 | "switched to module with no output") 56 | require.Equal(t, start.Add(time.Second), timing.Now()) 57 | 58 | tm1.OutputText("b") 59 | testBar.NextOutput().AssertText([]string{"b"}, 60 | "on active module update") 61 | require.Equal(t, start.Add(time.Second), timing.Now()) 62 | 63 | tm2.OutputText("c") 64 | testBar.AssertNoOutput("inactive module update") 65 | require.Equal(t, start.Add(time.Second), timing.Now()) 66 | 67 | ctrl.SetInterval(time.Minute) 68 | testBar.Tick() 69 | testBar.NextOutput().AssertText([]string{"c"}, 70 | "switched to module with an update") 71 | require.Equal(t, start.Add(61*time.Second), timing.Now()) 72 | } 73 | -------------------------------------------------------------------------------- /group/following/following.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package following provides a group that always shows the output from 16 | // the most recently updated module in the set. 17 | package following 18 | 19 | import ( 20 | "sync/atomic" 21 | 22 | "github.com/soumya92/barista/bar" 23 | "github.com/soumya92/barista/group" 24 | ) 25 | 26 | // grouper implements a following grouper. 27 | type grouper struct{ current int64 } 28 | 29 | // Group returns a new following group. 30 | func Group(m ...bar.Module) bar.Module { 31 | return group.New(&grouper{}, m...) 32 | } 33 | 34 | func (g *grouper) Visible(idx int) bool { 35 | return atomic.LoadInt64(&g.current) == int64(idx) 36 | } 37 | 38 | func (g *grouper) Updated(idx int) { 39 | atomic.StoreInt64(&g.current, int64(idx)) 40 | } 41 | 42 | func (g *grouper) Buttons() (start, end bar.Output) { return nil, nil } 43 | -------------------------------------------------------------------------------- /group/following/following_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package following 16 | 17 | import ( 18 | "testing" 19 | 20 | testBar "github.com/soumya92/barista/testing/bar" 21 | testModule "github.com/soumya92/barista/testing/module" 22 | ) 23 | 24 | func TestFollowing(t *testing.T) { 25 | testBar.New(t) 26 | 27 | tm0 := testModule.New(t) 28 | tm1 := testModule.New(t) 29 | tm2 := testModule.New(t) 30 | 31 | grp := Group(tm0, tm1, tm2) 32 | tm0.AssertNotStarted("on group creation") 33 | tm1.AssertNotStarted() 34 | tm2.AssertNotStarted() 35 | 36 | testBar.Run(grp) 37 | tm0.AssertStarted("on stream") 38 | tm1.AssertStarted() 39 | tm2.AssertStarted() 40 | 41 | testBar.NextOutput().AssertEmpty("With no module output") 42 | 43 | tm0.OutputText("a") 44 | testBar.NextOutput().AssertText([]string{"a"}, 45 | "on module update") 46 | 47 | tm1.OutputText("b") 48 | testBar.NextOutput().AssertText([]string{"b"}) 49 | 50 | tm2.OutputText("c") 51 | testBar.NextOutput().AssertText([]string{"c"}) 52 | } 53 | -------------------------------------------------------------------------------- /group/switching/switching_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package switching 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | "github.com/soumya92/barista/base/click" 22 | "github.com/soumya92/barista/outputs" 23 | testBar "github.com/soumya92/barista/testing/bar" 24 | testModule "github.com/soumya92/barista/testing/module" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestSwitching(t *testing.T) { 30 | testBar.New(t) 31 | 32 | tm0 := testModule.New(t) 33 | tm1 := testModule.New(t) 34 | tm2 := testModule.New(t) 35 | 36 | grp, ctrl := Group(tm0, tm1, tm2) 37 | tm0.AssertNotStarted("on group creation") 38 | tm1.AssertNotStarted() 39 | tm2.AssertNotStarted() 40 | 41 | testBar.Run(grp) 42 | tm0.AssertStarted("on stream") 43 | tm1.AssertStarted() 44 | tm2.AssertStarted() 45 | 46 | require.Equal(t, 3, ctrl.Count()) 47 | require.Equal(t, 0, ctrl.Current()) 48 | out := testBar.NextOutput() 49 | out.AssertText([]string{">"}, 50 | "with no output from module") 51 | 52 | out.At(0).LeftClick() 53 | testBar.NextOutput().AssertText([]string{"<", ">"}) 54 | require.Equal(t, 1, ctrl.Current()) 55 | 56 | ctrl.Next() 57 | out = testBar.NextOutput() 58 | out.AssertText([]string{"<"}) 59 | 60 | tm1.OutputText("a") 61 | testBar.AssertNoOutput("on hidden module update") 62 | 63 | out.At(0).LeftClick() 64 | testBar.NextOutput().AssertText([]string{"<", "a", ">"}) 65 | 66 | ctrl.ButtonFunc(func(c Controller) (start, end bar.Output) { 67 | return outputs.Text("/*").OnClick(click.Left(c.Previous)), 68 | outputs.Text("*/").OnClick(click.Left(c.Next)) 69 | }) 70 | testBar.NextOutput().AssertText([]string{"/*", "a", "*/"}) 71 | 72 | tm0.OutputText("0") 73 | testBar.AssertNoOutput("on hidden module update") 74 | 75 | ctrl.Show(0) 76 | out = testBar.NextOutput() 77 | out.AssertText([]string{"/*", "0", "*/"}) 78 | out.At(0).LeftClick() 79 | testBar.NextOutput().AssertText([]string{"/*", "*/"}) 80 | require.Equal(t, 2, ctrl.Current(), "wraparound on left") 81 | 82 | ctrl.Next() 83 | testBar.NextOutput().AssertText([]string{"/*", "0", "*/"}) 84 | require.Equal(t, 0, ctrl.Current(), "wraparound on right") 85 | } 86 | -------------------------------------------------------------------------------- /logging/nop_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !baristadebuglog 16 | 17 | package logging 18 | 19 | import ( 20 | "log" 21 | "testing" 22 | 23 | "github.com/soumya92/barista/testing/mockio" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestNopMethods(t *testing.T) { 29 | SetOutput(mockio.Stdout()) 30 | SetFlags(log.Lshortfile) 31 | Log("foo: %d", 42) 32 | Fine("bar: %g", 3.14159) 33 | require.Equal(t, "", ID(4)) 34 | Label(&struct{}{}, "empty") 35 | Labelf(&struct{}{}, "empty: %b", true) 36 | Attach(t, 4, "->int") 37 | Attachf(t, 1.0, "->float:%g", 1.0) 38 | Register(t, "Fail", "FailNow") 39 | } 40 | -------------------------------------------------------------------------------- /modules/bluetooth/adapter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package bluetooth provides modules for watching the status of Bluetooth adapters and devices. 16 | package bluetooth 17 | 18 | import ( 19 | "github.com/soumya92/barista/bar" 20 | "github.com/soumya92/barista/base/value" 21 | "github.com/soumya92/barista/base/watchers/dbus" 22 | ) 23 | 24 | // AdapterModule represents a Bluetooth bar module. 25 | type AdapterModule struct { 26 | adapter string 27 | outputFunc value.Value 28 | } 29 | 30 | // AdapterInfo represents a Bluetooth adapters information. 31 | type AdapterInfo struct { 32 | Name string 33 | Alias string 34 | Address string 35 | Discoverable bool 36 | Pairable bool 37 | Powered bool 38 | Discovering bool 39 | } 40 | 41 | // replaced in tests. 42 | var busType = dbus.System 43 | 44 | // DefaultAdapter constructs an instance of the bluetooth module using the first adapter ("hci0"). 45 | func DefaultAdapter() *AdapterModule { 46 | return Adapter("hci0") 47 | } 48 | 49 | // Adapter constructs an instance of the bluetooth module with the provided device name (ex. "hci1"). 50 | func Adapter(name string) *AdapterModule { 51 | return &AdapterModule{adapter: name} 52 | } 53 | 54 | // Output configures a module to display the output of a user-defined function. 55 | func (bt *AdapterModule) Output(outputFunc func(AdapterInfo) bar.Output) *AdapterModule { 56 | bt.outputFunc.Set(outputFunc) 57 | return bt 58 | } 59 | 60 | // Stream starts the module. 61 | func (bt *AdapterModule) Stream(sink bar.Sink) { 62 | w := dbus.WatchProperties( 63 | busType, 64 | "org.bluez", 65 | "/org/bluez/"+bt.adapter, 66 | "org.bluez.Adapter1", 67 | ). 68 | Add("Name", "Alias", "Address", "Discoverable", "Pairable", "Powered", "Discovering") 69 | defer w.Unsubscribe() 70 | 71 | outputFunc := bt.outputFunc.Get().(func(AdapterInfo) bar.Output) 72 | nextOutputFunc, done := bt.outputFunc.Subscribe() 73 | defer done() 74 | 75 | info := getAdapterInfo(w) 76 | for { 77 | sink.Output(outputFunc(info)) 78 | select { 79 | case <-w.Updates: 80 | info = getAdapterInfo(w) 81 | case <-nextOutputFunc: 82 | outputFunc = bt.outputFunc.Get().(func(AdapterInfo) bar.Output) 83 | } 84 | } 85 | } 86 | 87 | func getAdapterInfo(w *dbus.PropertiesWatcher) AdapterInfo { 88 | i := AdapterInfo{} 89 | props := w.Get() 90 | 91 | if name, ok := props["Name"].(string); ok { 92 | i.Name = name 93 | } 94 | i.Alias, _ = props["Alias"].(string) 95 | i.Address, _ = props["Address"].(string) 96 | i.Discoverable, _ = props["Discoverable"].(bool) 97 | i.Pairable, _ = props["Pairable"].(bool) 98 | i.Powered, _ = props["Powered"].(bool) 99 | i.Discovering, _ = props["Discovering"].(bool) 100 | 101 | return i 102 | } 103 | -------------------------------------------------------------------------------- /modules/bluetooth/adapter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package bluetooth 16 | 17 | import ( 18 | "testing" 19 | 20 | godbus "github.com/godbus/dbus/v5" 21 | 22 | "github.com/soumya92/barista/bar" 23 | "github.com/soumya92/barista/base/watchers/dbus" 24 | "github.com/soumya92/barista/outputs" 25 | testBar "github.com/soumya92/barista/testing/bar" 26 | ) 27 | 28 | func init() { 29 | busType = dbus.Test 30 | } 31 | 32 | func TestAdapter(t *testing.T) { 33 | testBar.New(t) 34 | 35 | adapterName := "hci0" 36 | adapter := setupTestAdapter(adapterName) 37 | adapter.SetProperties(map[string]interface{}{ 38 | "Name": "foo", 39 | "Alias": "foo alias", 40 | "Address": "28:C2:DD:8B:73:8C", 41 | "Discoverable": false, 42 | "Pairable": true, 43 | "Powered": true, 44 | "Discovering": false, 45 | }, dbus.SignalTypeNone) 46 | 47 | btModule := Adapter(adapterName) 48 | btModule.Output(func(i AdapterInfo) bar.Output { 49 | state := "OFF" 50 | if i.Powered { 51 | state = "ON" 52 | } 53 | return outputs.Textf("%s: %s", i.Name, state) 54 | }) 55 | testBar.Run(btModule) 56 | 57 | testBar.LatestOutput().AssertText([]string{ 58 | "foo: ON", 59 | }) 60 | } 61 | 62 | func TestAdapterDisconnect(t *testing.T) { 63 | testBar.New(t) 64 | 65 | adapterName := "hci0" 66 | adapter := setupTestAdapter(adapterName) 67 | adapter.SetProperties(map[string]interface{}{ 68 | "Name": "foo", 69 | "Alias": "foo alias", 70 | "Address": "28:C2:DD:8B:73:8C", 71 | "Discoverable": false, 72 | "Pairable": true, 73 | "Powered": true, 74 | "Discovering": false, 75 | }, dbus.SignalTypeNone) 76 | 77 | btModule := DefaultAdapter() 78 | btModule.Output(func(i AdapterInfo) bar.Output { 79 | state := "OFF" 80 | if i.Powered { 81 | state = "ON" 82 | } 83 | return outputs.Textf("%s", state) 84 | }) 85 | testBar.Run(btModule) 86 | 87 | testBar.LatestOutput().AssertText([]string{ 88 | "ON", 89 | }) 90 | 91 | adapter.SetPropertyForTest("Powered", false, dbus.SignalTypeChanged) 92 | 93 | testBar.LatestOutput().AssertText([]string{ 94 | "OFF", 95 | }) 96 | } 97 | 98 | func setupTestAdapter(adapterName string) *dbus.TestBusObject { 99 | bus := dbus.SetupTestBus() 100 | bluez := bus.RegisterService("org.bluez") 101 | 102 | adapterObjPath := godbus.ObjectPath("/org/bluez/" + adapterName) 103 | adapter := bluez.Object(adapterObjPath, "org.bluez.Adapter1") 104 | 105 | return adapter 106 | } 107 | -------------------------------------------------------------------------------- /modules/counter/counter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package counter demonstrates an extremely simple i3bar module that shows a counter 16 | // which can be chnaged by clicking on it. It showcases the asynchronous nature of 17 | // i3bar modules when written in go. 18 | package counter 19 | 20 | import ( 21 | "github.com/soumya92/barista/bar" 22 | "github.com/soumya92/barista/base/value" 23 | l "github.com/soumya92/barista/logging" 24 | "github.com/soumya92/barista/outputs" 25 | ) 26 | 27 | // Module represents a "counter" module that displays a count 28 | // in the given format, and adjusts the count on click/scroll. 29 | // This module exemplifies the event-based architecture of barista. 30 | type Module struct { 31 | count value.Value // of int 32 | format value.Value // of string 33 | } 34 | 35 | // New constructs a new counter module. 36 | func New(format string) *Module { 37 | m := &Module{} 38 | l.Register(m, "format", "count") 39 | m.count.Set(0) 40 | m.format.Set(format) 41 | return m 42 | } 43 | 44 | // Stream starts the module. 45 | func (m *Module) Stream(s bar.Sink) { 46 | count := m.count.Get().(int) 47 | countSub, done := m.count.Subscribe() 48 | defer done() 49 | format := m.format.Get().(string) 50 | formatSub, done := m.format.Subscribe() 51 | defer done() 52 | for { 53 | s.Output(outputs.Textf(format, count).OnClick(m.click)) 54 | select { 55 | case <-countSub: 56 | count = m.count.Get().(int) 57 | case <-formatSub: 58 | format = m.format.Get().(string) 59 | } 60 | } 61 | } 62 | 63 | // Format sets the output format. 64 | // The given format string will receive the counter value 65 | // as the only argument. 66 | func (m *Module) Format(format string) *Module { 67 | m.format.Set(format) 68 | return m 69 | } 70 | 71 | // Click handles clicks on the module output. 72 | func (m *Module) click(e bar.Event) { 73 | current := m.count.Get().(int) 74 | switch e.Button { 75 | case bar.ButtonLeft, bar.ScrollDown, bar.ScrollLeft, bar.ButtonBack: 76 | current-- 77 | case bar.ButtonRight, bar.ScrollUp, bar.ScrollRight, bar.ButtonForward: 78 | current++ 79 | } 80 | m.count.Set(current) 81 | } 82 | -------------------------------------------------------------------------------- /modules/counter/counter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package counter 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | testBar "github.com/soumya92/barista/testing/bar" 22 | ) 23 | 24 | func TestCounter(t *testing.T) { 25 | ctr := New("C:%d") 26 | testBar.New(t) 27 | testBar.Run(ctr) 28 | 29 | out := testBar.NextOutput() 30 | out.AssertText([]string{"C:0"}, "on start") 31 | 32 | testBar.AssertNoOutput("without any interaction") 33 | 34 | out.At(0).Click(bar.Event{Button: bar.ScrollUp}) 35 | out = testBar.NextOutput() 36 | out.AssertText([]string{"C:1"}, "on click") 37 | 38 | out.At(0).Click(bar.Event{Button: bar.ScrollDown}) 39 | out = testBar.NextOutput() 40 | out.AssertText([]string{"C:0"}, "on click") 41 | 42 | out.At(0).Click(bar.Event{Button: bar.ButtonBack}) 43 | out = testBar.NextOutput() 44 | out.AssertText([]string{"C:-1"}, "on click") 45 | 46 | ctr.Format("=%d=") 47 | out = testBar.NextOutput() 48 | out.AssertText([]string{"=-1="}, "on format change") 49 | 50 | out.At(0).Click(bar.Event{Button: bar.ScrollUp}) 51 | testBar.NextOutput().AssertText( 52 | []string{"=0="}, "on click after format change") 53 | } 54 | -------------------------------------------------------------------------------- /modules/cputemp/cputemp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cputemp implements an i3bar module that shows the CPU temperature. 16 | package cputemp 17 | 18 | import ( 19 | "github.com/soumya92/barista/modules/internal/temperature" 20 | ) 21 | 22 | // Module represents a cputemp bar module. It supports setting the output 23 | // format, click handler, update frequency, and urgency/colour functions. 24 | type Module = temperature.Module 25 | 26 | // Zone constructs an instance of the cputemp module for the specified zone. 27 | // The file /sys/class/thermal//temp should return cpu temp in 1/1000 deg C. 28 | func Zone(thermalZone string) *Module { 29 | return temperature.ThermalZone(thermalZone) 30 | } 31 | 32 | // OfType constructs an instance of the cputemp module for the *first* available 33 | // sensor of the given type. "x86_pkg_temp" usually represents the temperature 34 | // of the actual CPU package, while others may be available depending on the 35 | // system, e.g. "iwlwifi" for wifi, or "acpitz" for the motherboard. 36 | func OfType(typ string) *Module { 37 | return temperature.ThermalOfType(typ) 38 | } 39 | 40 | // New constructs an instance of the cputemp module for zone type "x86_pkg_temp". 41 | // Returns nil of the x86_pkg_temp thermal zone is unavailable. 42 | func New() *Module { 43 | return temperature.NewDefaultThermal() 44 | } 45 | -------------------------------------------------------------------------------- /modules/funcs/funcs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package funcs provides the ability to construct i3bar modules from simple Funcs. 16 | package funcs 17 | 18 | import ( 19 | "github.com/soumya92/barista/base/notifier" 20 | "github.com/soumya92/barista/timing" 21 | "time" 22 | 23 | "github.com/soumya92/barista/bar" 24 | ) 25 | 26 | // Func receives a bar.Sink and uses it for output. 27 | type Func func(bar.Sink) 28 | 29 | // Once constructs a bar module that runs the given function once. 30 | // Useful if the function loops internally. 31 | func Once(f Func) *OnceModule { 32 | return &OnceModule{Func: f} 33 | } 34 | 35 | // OnceModule represents a bar.Module that runs a function once. 36 | // If the function sets an error output, it will be restarted on 37 | // the next click. 38 | type OnceModule struct { 39 | Func 40 | } 41 | 42 | // Stream starts the module. 43 | func (o *OnceModule) Stream(s bar.Sink) { 44 | forever := make(chan struct{}) 45 | o.Func(s) 46 | <-forever 47 | } 48 | 49 | // OnClick constructs a bar module that runs the given function 50 | // when clicked. The function is given a Channel to allow 51 | // multiple outputs (e.g. Loading... Done), and when the function 52 | // returns, the next click will call it again. 53 | func OnClick(f Func) *OnclickModule { 54 | return &OnclickModule{f} 55 | } 56 | 57 | // OnclickModule represents a bar.Module that runs a function and 58 | // marks the module as finished, causing the next click to start the 59 | // module again. 60 | type OnclickModule struct { 61 | Func 62 | } 63 | 64 | // Stream starts the module. 65 | func (o OnclickModule) Stream(s bar.Sink) { 66 | o.Func(s) 67 | } 68 | 69 | // Every constructs a bar module that repeatedly runs the given function. 70 | // Useful if the function needs to poll a resource for output. 71 | func Every(d time.Duration, f Func) *RepeatingModule { 72 | r := &RepeatingModule{fn: f, d: d} 73 | r.scheduler = timing.NewScheduler().Every(d) 74 | r.notifyFn, r.notifyCh = notifier.New() 75 | 76 | return r 77 | } 78 | 79 | // RepeatingModule represents a bar.Module that runs a function at a fixed 80 | // interval (while accounting for bar paused/resumed state). 81 | type RepeatingModule struct { 82 | notifyCh <-chan struct{} 83 | notifyFn func() 84 | fn Func 85 | d time.Duration 86 | scheduler *timing.Scheduler 87 | } 88 | 89 | // Stream starts the module. 90 | func (r *RepeatingModule) Stream(s bar.Sink) { 91 | r.fn(s) 92 | for { 93 | select { 94 | case <-r.notifyCh: 95 | r.scheduler.Every(r.d) 96 | r.fn(s) 97 | case <-r.scheduler.C: 98 | r.fn(s) 99 | } 100 | } 101 | } 102 | 103 | // Refresh forces a refresh of the data being displayed. 104 | func (r *RepeatingModule) Refresh() { 105 | r.notifyFn() 106 | } 107 | -------------------------------------------------------------------------------- /modules/hwmon/hwmon.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package hwmon implements an i3bar module that shows the temperature from /sys/class/hwmon 16 | package hwmon 17 | 18 | import ( 19 | "github.com/soumya92/barista/modules/internal/temperature" 20 | ) 21 | 22 | // Module represents a hwmon bar module. It supports setting the output 23 | // format, click handler, update frequency, and urgency/colour functions. 24 | type Module = temperature.Module 25 | 26 | // OfNameAndLabel finds a sensor given hwmon name and label. 27 | // 28 | // For example, if you have /sys/class/hwmon/hwmon4/name containing 29 | // "k10temp", and /sys/class/hwmon/hwmon4/temp1_label containing 30 | // "Tctl", the former would be name, and the latter would be label. 31 | func OfNameAndLabel(name string, label string) *Module { 32 | return temperature.HwmonOfNameAndLabel(name, label) 33 | } 34 | -------------------------------------------------------------------------------- /modules/internal/temperature/hwmon.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temperature 16 | 17 | import ( 18 | "path/filepath" 19 | "strings" 20 | 21 | "github.com/spf13/afero" 22 | ) 23 | 24 | // HwmonOfNameAndLabel finds a sensor given hwmon name and label. 25 | // 26 | // For example, if you have /sys/class/hwmon/hwmon4/name containing 27 | // "k10temp", and /sys/class/hwmon/hwmon4/temp1_label containing 28 | // "Tctl", the former would be name, and the latter would be label. 29 | func HwmonOfNameAndLabel(name string, label string) *Module { 30 | baseDir := "/sys/class/hwmon" 31 | files, _ := afero.ReadDir(fs, baseDir) 32 | for _, file := range files { 33 | n, _ := afero.ReadFile(fs, filepath.Join(baseDir, file.Name(), "name")) 34 | if strings.TrimSpace(string(n)) == name { 35 | baseDir := filepath.Join(baseDir, file.Name()) 36 | files, _ := afero.ReadDir(fs, filepath.Join(baseDir)) 37 | for _, file := range files { 38 | if strings.HasSuffix(file.Name(), "_label") { 39 | l, _ := afero.ReadFile(fs, filepath.Join(baseDir, file.Name())) 40 | if strings.TrimSpace(string(l)) == label { 41 | filename := file.Name() 42 | filename = strings.TrimSuffix(filename, "_label") + "_input" 43 | return newModule(filepath.Join(baseDir, filename)) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | return newModule("") 50 | } 51 | -------------------------------------------------------------------------------- /modules/internal/temperature/hwmon_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temperature 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/spf13/afero" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestHwmonDetection(t *testing.T) { 26 | fs = afero.NewMemMapFs() 27 | contents := []struct { 28 | path string 29 | data string 30 | }{ 31 | {"/sys/class/hwmon/hwmon0/name", "k10temp\n"}, 32 | {"/sys/class/hwmon/hwmon0/temp1_label", "Tctl\n"}, 33 | {"/sys/class/hwmon/hwmon0/temp1_input", "63500\n"}, 34 | {"/sys/class/hwmon/hwmon1/name", "amdgpu\n"}, 35 | {"/sys/class/hwmon/hwmon1/temp1_label", "edge\n"}, 36 | {"/sys/class/hwmon/hwmon1/temp2_label", "junction\n"}, 37 | {"/sys/class/hwmon/hwmon1/temp3_label", "mem\n"}, 38 | {"/sys/class/hwmon/hwmon1/temp1_input", "56000\n"}, 39 | {"/sys/class/hwmon/hwmon1/temp2_input", "56000\n"}, 40 | {"/sys/class/hwmon/hwmon1/temp3_input", "56000\n"}, 41 | } 42 | for _, content := range contents { 43 | err := afero.WriteFile(fs, content.path, []byte(content.data), 0644) 44 | require.NoError(t, err) 45 | } 46 | // This only tests detection code. 47 | // functional tests are in thermalzone_test.go 48 | var m *Module 49 | m = HwmonOfNameAndLabel("k10temp", "Tctl") 50 | assert.Equal(t, "/sys/class/hwmon/hwmon0/temp1_input", m.thermalFile) 51 | m = HwmonOfNameAndLabel("amdgpu", "junction") 52 | assert.Equal(t, "/sys/class/hwmon/hwmon1/temp2_input", m.thermalFile) 53 | } 54 | -------------------------------------------------------------------------------- /modules/internal/temperature/module.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017, 2022 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temperature 16 | 17 | import ( 18 | "strconv" 19 | "strings" 20 | "time" 21 | 22 | "github.com/soumya92/barista/bar" 23 | "github.com/soumya92/barista/base/value" 24 | l "github.com/soumya92/barista/logging" 25 | "github.com/soumya92/barista/outputs" 26 | "github.com/soumya92/barista/timing" 27 | 28 | "github.com/martinlindhe/unit" 29 | "github.com/spf13/afero" 30 | ) 31 | 32 | // Module represents a cputemp bar module. It supports setting the output 33 | // format, click handler, update frequency, and urgency/colour functions. 34 | type Module struct { 35 | thermalFile string 36 | scheduler *timing.Scheduler 37 | outputFunc value.Value // of func(unit.Temperature) bar.Output 38 | } 39 | 40 | func newModule(thermalFile string) *Module { 41 | m := &Module{ 42 | thermalFile: thermalFile, 43 | scheduler: timing.NewScheduler(), 44 | } 45 | l.Label(m, thermalFile) 46 | l.Register(m, "scheduler", "format") 47 | m.RefreshInterval(3 * time.Second) 48 | // Default output, if no function is specified later. 49 | m.Output(func(t unit.Temperature) bar.Output { 50 | return outputs.Textf("%.1f℃", t.Celsius()) 51 | }) 52 | return m 53 | } 54 | 55 | // Output configures a module to display the output of a user-defined function. 56 | func (m *Module) Output(outputFunc func(unit.Temperature) bar.Output) *Module { 57 | m.outputFunc.Set(outputFunc) 58 | return m 59 | } 60 | 61 | // RefreshInterval configures the polling frequency for cpu temperatures. 62 | // Note: updates might still be less frequent if the temperature does not change. 63 | func (m *Module) RefreshInterval(interval time.Duration) *Module { 64 | m.scheduler.Every(interval) 65 | return m 66 | } 67 | 68 | var fs = afero.NewOsFs() 69 | 70 | // Stream starts the module. 71 | func (m *Module) Stream(s bar.Sink) { 72 | temp, err := getTemperature(m.thermalFile) 73 | outputFunc := m.outputFunc.Get().(func(unit.Temperature) bar.Output) 74 | nextOutputFunc, done := m.outputFunc.Subscribe() 75 | defer done() 76 | for { 77 | if s.Error(err) { 78 | return 79 | } 80 | s.Output(outputFunc(temp)) 81 | select { 82 | case <-m.scheduler.C: 83 | temp, err = getTemperature(m.thermalFile) 84 | case <-nextOutputFunc: 85 | outputFunc = m.outputFunc.Get().(func(unit.Temperature) bar.Output) 86 | } 87 | } 88 | } 89 | 90 | func getTemperature(thermalFile string) (unit.Temperature, error) { 91 | bytes, err := afero.ReadFile(fs, thermalFile) 92 | if err != nil { 93 | return 0, err 94 | } 95 | value := strings.TrimSpace(string(bytes)) 96 | milliC, err := strconv.Atoi(value) 97 | if err != nil { 98 | return 0, err 99 | } 100 | return unit.FromCelsius(float64(milliC) / 1000.0), nil 101 | } 102 | -------------------------------------------------------------------------------- /modules/internal/temperature/thermalzone.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package temperature 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/spf13/afero" 22 | ) 23 | 24 | // ThermalZone constructs an instance of the temperature module for the specified zone. 25 | // The file /sys/class/thermal//temp should return cpu temp in 1/1000 deg C. 26 | func ThermalZone(thermalZone string) *Module { 27 | return newModule(fmt.Sprintf("/sys/class/thermal/%s/temp", thermalZone)) 28 | } 29 | 30 | // ThermalOfType constructs an instance of the cputemp module for the *first* 31 | // available sensor of the given type. "x86_pkg_temp" usually represents the temperature 32 | // of the actual CPU package, while others may be available depending on the 33 | // system, e.g. "iwlwifi" for wifi, or "acpitz" for the motherboard. 34 | func ThermalOfType(typ string) *Module { 35 | files, _ := afero.ReadDir(fs, "/sys/class/thermal") 36 | for _, file := range files { 37 | name := file.Name() 38 | typFile := fmt.Sprintf("/sys/class/thermal/%s/type", name) 39 | typBytes, _ := afero.ReadFile(fs, typFile) 40 | if strings.TrimSpace(string(typBytes)) == typ { 41 | return ThermalZone(name) 42 | } 43 | } 44 | return ThermalZone("") 45 | } 46 | 47 | // NewDefaultThermal constructs an instance of the cputemp module for zone type 48 | // "x86_pkg_temp". Returns nil of the x86_pkg_temp thermal zone is unavailable. 49 | func NewDefaultThermal() *Module { 50 | return ThermalOfType("x86_pkg_temp") 51 | } 52 | -------------------------------------------------------------------------------- /modules/meta/multicast/multicast.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package multicast provides a method to convert any bar.Module into one that 16 | // can be added to the bar multiple times. When combined with group.Simple, this 17 | // allows multiple combinations of modules without creating extra instances. 18 | package multicast 19 | 20 | import ( 21 | "sync" 22 | 23 | "github.com/soumya92/barista/bar" 24 | "github.com/soumya92/barista/base/value" 25 | "github.com/soumya92/barista/core" 26 | "github.com/soumya92/barista/sink" 27 | ) 28 | 29 | type module struct { 30 | *value.Value 31 | start func() // called on Stream(), to ensure backing module is started 32 | } 33 | 34 | // Stream starts the module, and tries to stream the original module as well. 35 | func (m module) Stream(sink bar.Sink) { 36 | go m.start() 37 | for { 38 | next := m.Next() 39 | s, _ := m.Get().(bar.Segments) 40 | sink.Output(s) 41 | <-next 42 | } 43 | } 44 | 45 | // New creates a multicast module that can be added to the bar any number of 46 | // times, and mirrors the output of the original module at each location. 47 | // IMPORTANT: The original module must not be added to the bar. 48 | func New(original bar.Module) bar.Module { 49 | output, sink := sink.Value() 50 | coreModule := core.NewModule(original) 51 | var once sync.Once 52 | start := func() { 53 | once.Do(func() { 54 | coreModule.Stream(sink) 55 | }) 56 | } 57 | return module{output, start} 58 | } 59 | -------------------------------------------------------------------------------- /modules/meta/multicast/multicast_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package multicast 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/outputs" 21 | testBar "github.com/soumya92/barista/testing/bar" 22 | testModule "github.com/soumya92/barista/testing/module" 23 | ) 24 | 25 | func TestMulticast(t *testing.T) { 26 | testBar.New(t) 27 | 28 | original := testModule.New(t) 29 | mcast := New(original) 30 | 31 | original.AssertNotStarted("when wrapped") 32 | testBar.Run(mcast, mcast, mcast) 33 | original.AssertStarted("on stream of multicast modules") 34 | 35 | testBar.LatestOutput().AssertEmpty("On start with no output") 36 | 37 | original.OutputText("foo") 38 | out := testBar.LatestOutput() 39 | out.AssertText([]string{"foo", "foo", "foo"}, 40 | "All copies update with new output") 41 | 42 | out.At(1).LeftClick() 43 | original.AssertClicked("clicked on multicasted output") 44 | 45 | original.Output(outputs.Group( 46 | outputs.Text("test"), outputs.Text("baz"), 47 | )) 48 | testBar.LatestOutput().AssertText( 49 | []string{"test", "baz", "test", "baz", "test", "baz"}, 50 | "multiple segments from original module") 51 | 52 | original.Output(nil) 53 | testBar.LatestOutput().AssertEmpty("empty output from original") 54 | 55 | original.Output(outputs.Errorf("something went wrong")) 56 | testBar.LatestOutput().AssertError("On error in original") 57 | 58 | original.Close() 59 | out = testBar.LatestOutput() 60 | original.AssertNotStarted() 61 | 62 | out.At(0).LeftClick() 63 | original.AssertStarted("On click of multicast output") 64 | } 65 | -------------------------------------------------------------------------------- /modules/meta/slot/slot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package slot provides multiple slots for a single module, allowing it to be 16 | // moved between various positions on the bar. When used carefully, this can be 17 | // useful for conveying limited information by re-ordering modules, but it has 18 | // the potential to become too distracting if overused. 19 | package slot 20 | 21 | import ( 22 | "sync" 23 | 24 | "github.com/soumya92/barista/bar" 25 | "github.com/soumya92/barista/base/value" 26 | "github.com/soumya92/barista/sink" 27 | ) 28 | 29 | // Slotter provides the ability to display the output of a module into named 30 | // slots, and allows changing the active slot at runtime to effectively move 31 | // the module on the bar. 32 | type Slotter struct { 33 | module bar.Module 34 | stream sync.Once 35 | activeSlot value.Value // of string 36 | 37 | sink bar.Sink 38 | lastOutput *value.Value // of bar.Segments 39 | } 40 | 41 | // New creates a slotter for the given module. The module is 'consumed' by 42 | // this operation and should not be used except through slots created from the 43 | // returned Slotter. 44 | func New(m bar.Module) *Slotter { 45 | s := &Slotter{module: m} 46 | s.lastOutput, s.sink = sink.Value() 47 | s.activeSlot.Set("") 48 | return s 49 | } 50 | 51 | // Slot creates a named slot for the module output. 52 | func (s *Slotter) Slot(name string) bar.Module { 53 | return &slotModule{s, name} 54 | } 55 | 56 | // Activate moves the module output to the named slot. 57 | func (s *Slotter) Activate(slotName string) { 58 | s.activeSlot.Set(slotName) 59 | } 60 | 61 | type slotModule struct { 62 | *Slotter 63 | slotName string 64 | } 65 | 66 | func (s *slotModule) Stream(sink bar.Sink) { 67 | go s.stream.Do(func() { s.module.Stream(s.sink) }) 68 | 69 | activeSub, done := s.activeSlot.Subscribe() 70 | defer done() 71 | active := s.activeSlot.Get().(string) 72 | 73 | outputSub, done := s.lastOutput.Subscribe() 74 | defer done() 75 | out := s.lastOutput.Get().(bar.Segments) 76 | 77 | hasOutput := false 78 | outputChanged := true 79 | 80 | for { 81 | if active == s.slotName { 82 | if !hasOutput || outputChanged { 83 | sink(out) 84 | hasOutput = true 85 | } 86 | } else { 87 | if hasOutput { 88 | sink(nil) 89 | hasOutput = false 90 | } 91 | } 92 | outputChanged = false 93 | select { 94 | case <-activeSub: 95 | active = s.activeSlot.Get().(string) 96 | case <-outputSub: 97 | out = s.lastOutput.Get().(bar.Segments) 98 | outputChanged = true 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /modules/meta/slot/slot_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package slot 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/modules/static" 21 | "github.com/soumya92/barista/outputs" 22 | testBar "github.com/soumya92/barista/testing/bar" 23 | testModule "github.com/soumya92/barista/testing/module" 24 | ) 25 | 26 | func TestReformat(t *testing.T) { 27 | testBar.New(t) 28 | original := testModule.New(t) 29 | slotter := New(original) 30 | original.AssertNotStarted("on construction of slotter module") 31 | 32 | slotA := slotter.Slot("A") 33 | slotB := slotter.Slot("B") 34 | slotC := slotter.Slot("C") 35 | testBar.Run( 36 | static.New(outputs.Text("<")), 37 | slotA, 38 | static.New(outputs.Text("-")), 39 | slotB, 40 | static.New(outputs.Text("-")), 41 | slotC, 42 | static.New(outputs.Text(">")), 43 | ) 44 | 45 | testBar.LatestOutput(0, 2, 4, 6).AssertText([]string{"<", "-", "-", ">"}) 46 | original.AssertStarted("on stream of slots") 47 | 48 | original.OutputText("foo") 49 | testBar.AssertNoOutput("not showing in any slot") 50 | 51 | slotter.Activate("A") 52 | testBar.NextOutput("On slot change").AssertText( 53 | []string{"<", "foo", "-", "-", ">"}) 54 | 55 | slotter.Activate("B") 56 | testBar.LatestOutput(1, 3).AssertText( 57 | []string{"<", "-", "foo", "-", ">"}) 58 | 59 | slotter.Activate("C") 60 | out := testBar.LatestOutput(3, 5) 61 | out.AssertText([]string{"<", "-", "-", "foo", ">"}) 62 | 63 | out.At(3).LeftClick() 64 | original.AssertClicked("on click of active slot") 65 | 66 | original.OutputText("new text") 67 | testBar.NextOutput().AssertText([]string{"<", "-", "-", "new text", ">"}) 68 | 69 | original.Output(nil) 70 | testBar.NextOutput().AssertText([]string{"<", "-", "-", ">"}) 71 | 72 | slotter.Activate("A") 73 | testBar.LatestOutput(1, 5).AssertText([]string{"<", "-", "-", ">"}) 74 | 75 | original.OutputText("baz") 76 | testBar.NextOutput().AssertText([]string{"<", "baz", "-", "-", ">"}) 77 | } 78 | -------------------------------------------------------------------------------- /modules/meta/split/split_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package split 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | "github.com/soumya92/barista/outputs" 22 | testBar "github.com/soumya92/barista/testing/bar" 23 | testModule "github.com/soumya92/barista/testing/module" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestSplit(t *testing.T) { 28 | testBar.New(t) 29 | 30 | original := testModule.New(t) 31 | first, rest := New(original, 2) 32 | 33 | // To delineate the portions of the output coming from first and rest. 34 | start := testModule.New(t) 35 | separator := testModule.New(t) 36 | end := testModule.New(t) 37 | 38 | original.AssertNotStarted("on construction of split modules") 39 | testBar.Run(start, rest, separator, first, end) 40 | original.AssertStarted("on stream of split modules") 41 | 42 | start.AssertStarted() 43 | start.Output(outputs.Text("*")) 44 | separator.AssertStarted() 45 | separator.Output(outputs.Text("*")) 46 | end.AssertStarted() 47 | end.Output(outputs.Text("*")) 48 | testBar.LatestOutput(0, 2, 4).Expect("setup") 49 | 50 | original.Output(outputs.Group( 51 | outputs.Text("a"), 52 | outputs.Text("b"), 53 | outputs.Text("c"), 54 | outputs.Text("d"), 55 | outputs.Text("e"), 56 | )) 57 | out := testBar.LatestOutput(1, 3) 58 | out.AssertText([]string{"*", "c", "d", "e", "*", "a", "b", "*"}, 59 | "Segments split up between the two modules") 60 | 61 | out.At(3).LeftClick() 62 | original.AssertClicked("clicked on rest") 63 | 64 | out.At(5).LeftClick() 65 | original.AssertClicked("clicked on first") 66 | 67 | out.At(4).LeftClick() 68 | original.AssertNotClicked("clicked on other") 69 | 70 | original.Output(outputs.Textf("test")) 71 | testBar.LatestOutput(1, 3).AssertText( 72 | []string{"*", "*", "test", "*"}, 73 | "not enough segments for rest") 74 | 75 | original.Output(nil) 76 | testBar.LatestOutput(1, 3).AssertText( 77 | []string{"*", "*", "*"}, 78 | "no segments in original output") 79 | 80 | clickChan := make(chan string, 1) 81 | original.Output(outputs.Group( 82 | outputs.Text("a").OnClick(func(bar.Event) { clickChan <- "a" }), 83 | outputs.Text("b").OnClick(func(bar.Event) { clickChan <- "b" }), 84 | outputs.Text("c").OnClick(func(bar.Event) { clickChan <- "c" }), 85 | outputs.Text("d").OnClick(func(bar.Event) { clickChan <- "d" }), 86 | outputs.Text("e").OnClick(func(bar.Event) { clickChan <- "e" }), 87 | )) 88 | 89 | out = testBar.LatestOutput(1, 3) 90 | out.At(1).LeftClick() 91 | require.Equal(t, "c", <-clickChan) 92 | 93 | out.At(6).LeftClick() 94 | require.Equal(t, "b", <-clickChan) 95 | } 96 | -------------------------------------------------------------------------------- /modules/netinfo/netinfo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package netinfo 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | "github.com/soumya92/barista/base/watchers/netlink" 22 | "github.com/soumya92/barista/outputs" 23 | testBar "github.com/soumya92/barista/testing/bar" 24 | ) 25 | 26 | func TestNetinfo(t *testing.T) { 27 | nlt := netlink.TestMode() 28 | link0 := nlt.AddLink(netlink.Link{Name: "lo0", State: netlink.Up}) 29 | link1 := nlt.AddLink(netlink.Link{Name: "eth0", State: netlink.Down}) 30 | link2 := nlt.AddLink(netlink.Link{Name: "eth1", State: netlink.Down}) 31 | link3 := nlt.AddLink(netlink.Link{Name: "wlan0", State: netlink.NotPresent}) 32 | 33 | testBar.New(t) 34 | n1 := New().Output(func(s State) bar.Output { 35 | if !s.Connected() { 36 | return outputs.Text("No net") 37 | } 38 | return outputs.Text(s.Name) 39 | }) 40 | n2 := Interface("wlan0").Output(func(s State) bar.Output { 41 | if !s.Enabled() { 42 | return nil 43 | } 44 | if s.Connected() { 45 | return outputs.Textf("W:%s", s.Name) 46 | } else if s.Connecting() { 47 | return outputs.Text("W:...") 48 | } else { 49 | return outputs.Text("W:down") 50 | } 51 | }) 52 | n3 := Prefix("eth").Output(func(s State) bar.Output { 53 | if !s.Connected() { 54 | return nil 55 | } 56 | return outputs.Textf("E:%s", s.Name) 57 | }) 58 | n4 := New() 59 | testBar.Run(n1, n2, n3, n4) 60 | 61 | testBar.LatestOutput().AssertText([]string{"lo0", "lo0"}) 62 | 63 | nlt.UpdateLink(link0, netlink.Link{State: netlink.Down}) 64 | testBar.LatestOutput(0, 3).Expect("on link update") 65 | nlt.UpdateLink(link3, netlink.Link{State: netlink.Down}) 66 | testBar.LatestOutput(0, 1, 3).AssertText([]string{"No net", "W:down"}) 67 | 68 | nlt.UpdateLink(link1, netlink.Link{State: netlink.Dormant}) 69 | testBar.LatestOutput(0, 2, 3).Expect("on link update") 70 | nlt.UpdateLink(link2, netlink.Link{State: netlink.Up}) 71 | testBar.LatestOutput(0, 2, 3).Expect("on link update") 72 | 73 | n1.Output(func(s State) bar.Output { 74 | return outputs.Textf("%v", s.State) 75 | }) 76 | testBar.NextOutput().AssertText([]string{"6", "W:down", "E:eth1", "eth1"}) 77 | } 78 | -------------------------------------------------------------------------------- /modules/shell/shell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package shell provides modules to display the output of shell commands. 17 | It supports both long-running commands, where the output is the last line, 18 | e.g. dmesg or tail -f /var/log/some.log, and repeatedly running commands, 19 | e.g. whoami, date +%s. 20 | */ 21 | package shell 22 | 23 | import ( 24 | "os/exec" 25 | "strings" 26 | "time" 27 | 28 | "github.com/soumya92/barista/bar" 29 | "github.com/soumya92/barista/base/notifier" 30 | "github.com/soumya92/barista/base/value" 31 | "github.com/soumya92/barista/outputs" 32 | "github.com/soumya92/barista/timing" 33 | ) 34 | 35 | // Module represents a shell module that updates on a timer or on demand. 36 | type Module struct { 37 | cmd string 38 | args []string 39 | outf value.Value // of func(string) bar.Output 40 | notifyCh <-chan struct{} 41 | notifyFn func() 42 | scheduler *timing.Scheduler 43 | } 44 | 45 | // New constructs a new shell module. 46 | func New(cmd string, args ...string) *Module { 47 | m := &Module{cmd: cmd, args: args} 48 | m.notifyFn, m.notifyCh = notifier.New() 49 | m.scheduler = timing.NewScheduler() 50 | m.outf.Set(func(text string) bar.Output { 51 | return outputs.Text(text) 52 | }) 53 | return m 54 | } 55 | 56 | // Stream starts the module. 57 | func (m *Module) Stream(s bar.Sink) { 58 | out, err := exec.Command(m.cmd, m.args...).Output() 59 | outf := m.outf.Get().(func(string) bar.Output) 60 | for { 61 | if s.Error(err) { 62 | return 63 | } 64 | s.Output(outf(strings.TrimSpace(string(out)))) 65 | select { 66 | case <-m.outf.Next(): 67 | outf = m.outf.Get().(func(string) bar.Output) 68 | case <-m.notifyCh: 69 | out, err = exec.Command(m.cmd, m.args...).Output() 70 | case <-m.scheduler.C: 71 | out, err = exec.Command(m.cmd, m.args...).Output() 72 | } 73 | } 74 | } 75 | 76 | // Output sets the output format. The format func will be passed the entire 77 | // trimmed output from the command once it's done executing. To process output 78 | // by lines, see Tail(). 79 | func (m *Module) Output(format func(string) bar.Output) *Module { 80 | m.outf.Set(format) 81 | return m 82 | } 83 | 84 | // Every sets the refresh interval for the module. The command will be executed 85 | // repeatedly at the given interval, and the output updated. A zero interval 86 | // stops automatic repeats (but Refresh will still work). 87 | func (m *Module) Every(interval time.Duration) *Module { 88 | if interval == 0 { 89 | m.scheduler.Stop() 90 | } else { 91 | m.scheduler.Every(interval) 92 | } 93 | return m 94 | } 95 | 96 | // Refresh executes the command and updates the output. 97 | func (m *Module) Refresh() { 98 | m.notifyFn() 99 | } 100 | -------------------------------------------------------------------------------- /modules/shell/shell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shell 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/soumya92/barista/bar" 22 | "github.com/soumya92/barista/outputs" 23 | testBar "github.com/soumya92/barista/testing/bar" 24 | "github.com/soumya92/barista/timing" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestRepeating(t *testing.T) { 30 | testBar.New(t) 31 | 32 | rep := New("echo", "foo").Every(time.Second) 33 | testBar.Run(rep) 34 | 35 | testBar.NextOutput().AssertText([]string{"foo"}, "on start") 36 | 37 | then := timing.Now() 38 | now := timing.NextTick() 39 | require.InDelta(t, float64(time.Second), float64(now.Sub(then)), 40 | float64(time.Millisecond)) 41 | 42 | testBar.NextOutput().Expect("on tick") 43 | testBar.AssertNoOutput("until tick") 44 | 45 | for i := 0; i < 10; i++ { 46 | testBar.Tick() 47 | testBar.NextOutput().Expect("on tick") 48 | } 49 | 50 | rep.Every(0) 51 | testBar.Tick() 52 | testBar.AssertNoOutput("on zero interval") 53 | 54 | testBar.New(t) 55 | rep = New("this-is-not-a-valid-command", "foo").Every(time.Second) 56 | testBar.Run(rep) 57 | testBar.NextOutput().AssertError("when starting an invalid command") 58 | out := testBar.NextOutput("sets restart handler") 59 | testBar.Tick() 60 | testBar.AssertNoOutput("on next tick with error") 61 | out.At(0).LeftClick() 62 | testBar.NextOutput("clears error segment") 63 | testBar.NextOutput().AssertError("when starting an invalid command") 64 | } 65 | 66 | func TestUpdate(t *testing.T) { 67 | testBar.New(t) 68 | m := New("echo", "bar").Output(func(in string) bar.Output { 69 | return outputs.Textf(">%s<", in) 70 | }) 71 | testBar.Run(m) 72 | testBar.NextOutput().AssertText([]string{">bar<"}, "on start") 73 | testBar.AssertNoOutput("after the first output") 74 | 75 | m.Output(func(in string) bar.Output { 76 | return outputs.Textf("*%s*", in) 77 | }) 78 | testBar.NextOutput("on format change").AssertText([]string{"*bar*"}) 79 | 80 | m.Refresh() 81 | testBar.NextOutput("on refresh").AssertText([]string{"*bar*"}) 82 | } 83 | -------------------------------------------------------------------------------- /modules/shell/tail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shell 16 | 17 | import ( 18 | "bufio" 19 | "os/exec" 20 | "syscall" 21 | 22 | "github.com/soumya92/barista/bar" 23 | "github.com/soumya92/barista/base/value" 24 | "github.com/soumya92/barista/outputs" 25 | ) 26 | 27 | // TailModule represents a bar.Module that displays the last line of output from 28 | // a shell command in the bar. 29 | type TailModule struct { 30 | cmd string 31 | args []string 32 | outf value.Value // of func(string) bar.Output 33 | } 34 | 35 | // Tail constructs a module that displays the last line of output from a long 36 | // running command. 37 | func Tail(cmd string, args ...string) *TailModule { 38 | t := &TailModule{cmd: cmd, args: args} 39 | t.outf.Set(func(text string) bar.Output { 40 | return outputs.Text(text) 41 | }) 42 | return t 43 | } 44 | 45 | // Stream starts the module. 46 | func (m *TailModule) Stream(s bar.Sink) { 47 | cmd := exec.Command(m.cmd, m.args...) 48 | // Prevent SIGUSR for bar pause/resume from propagating to the 49 | // child process. Some commands don't play nice with signals. 50 | cmd.SysProcAttr = &syscall.SysProcAttr{ 51 | Setpgid: true, 52 | Pgid: 0, 53 | } 54 | stdout, err := cmd.StdoutPipe() 55 | if s.Error(err) { 56 | return 57 | } 58 | if s.Error(cmd.Start()) { 59 | return 60 | } 61 | var out *string 62 | outf := m.outf.Get().(func(string) bar.Output) 63 | errChan := make(chan error) 64 | outChan := make(chan string) 65 | go func() { 66 | scanner := bufio.NewScanner(stdout) 67 | for scanner.Scan() { 68 | outChan <- scanner.Text() 69 | } 70 | errChan <- cmd.Wait() 71 | }() 72 | for { 73 | select { 74 | case e := <-errChan: 75 | s.Error(e) 76 | return 77 | case <-m.outf.Next(): 78 | outf = m.outf.Get().(func(string) bar.Output) 79 | case txt := <-outChan: 80 | out = &txt 81 | } 82 | if out != nil { 83 | s.Output(outf(*out)) 84 | } 85 | } 86 | } 87 | 88 | // Output sets the output format for each line of output. 89 | func (m *TailModule) Output(format func(string) bar.Output) *TailModule { 90 | m.outf.Set(format) 91 | return m 92 | } 93 | -------------------------------------------------------------------------------- /modules/shell/tail_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package shell 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | "github.com/soumya92/barista/outputs" 22 | testBar "github.com/soumya92/barista/testing/bar" 23 | ) 24 | 25 | func TestTail(t *testing.T) { 26 | testBar.New(t) 27 | tail := Tail("bash", "-c", "for i in `seq 1 5`; do echo $i; sleep 0.075; done") 28 | testBar.Run(tail) 29 | 30 | for _, i := range []string{"1", "2", "3", "4", "5"} { 31 | testBar.NextOutput().AssertText([]string{i}, i) 32 | } 33 | 34 | testBar.AssertNoOutput("when command terminates normally") 35 | 36 | tail.Output(func(in string) bar.Output { 37 | return outputs.Textf("++%s++", in) 38 | }) 39 | testBar.NextOutput("on format func change").AssertText( 40 | []string{"++5++"}, "applies format func to last output line") 41 | } 42 | 43 | func TestTailExitCode(t *testing.T) { 44 | testBar.New(t) 45 | tail := Tail("bash", "-c", "for i in `seq 1 3`; do echo $i; sleep 0.075; done; exit 1") 46 | testBar.Run(tail) 47 | for _, i := range []string{"1", "2", "3"} { 48 | testBar.NextOutput().AssertText([]string{i}, i) 49 | } 50 | 51 | testBar.NextOutput().AssertError( 52 | "when command terminates with an error") 53 | } 54 | 55 | func TestTailInvalidCommand(t *testing.T) { 56 | testBar.New(t) 57 | tail := Tail("this-is-not-a-valid-command", "--but", "'have'", "-some", "args") 58 | testBar.Run(tail) 59 | testBar.NextOutput().AssertError( 60 | "when starting an invalid command") 61 | } 62 | -------------------------------------------------------------------------------- /modules/static/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package static provides a simple module that shows static content on the bar, 16 | // with methods to set the content. In a pinch, this can be used to create 17 | // buttons, or show additional information by setting the output from within 18 | // a format function. 19 | package static 20 | 21 | import ( 22 | "github.com/soumya92/barista/bar" 23 | "github.com/soumya92/barista/base/value" 24 | ) 25 | 26 | // Module represents a module that displays static content on the bar. 27 | type Module struct { 28 | output value.Value 29 | } 30 | 31 | // Stream starts the module. 32 | func (m *Module) Stream(sink bar.Sink) { 33 | for { 34 | next := m.output.Next() 35 | out, _ := m.output.Get().(bar.Output) 36 | sink.Output(out) 37 | <-next 38 | } 39 | } 40 | 41 | // Set sets the output to display. 42 | func (m *Module) Set(out bar.Output) { 43 | m.output.Set(out) 44 | } 45 | 46 | // Clear sets an empty output. 47 | func (m *Module) Clear() { 48 | m.output.Set(nil) 49 | } 50 | 51 | // New constructs a static module that displays the given output. 52 | func New(initial bar.Output) *Module { 53 | m := new(Module) 54 | m.Set(initial) 55 | return m 56 | } 57 | -------------------------------------------------------------------------------- /modules/static/static_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package static 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/soumya92/barista/bar" 22 | "github.com/soumya92/barista/outputs" 23 | testBar "github.com/soumya92/barista/testing/bar" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestStatic(t *testing.T) { 28 | testBar.New(t) 29 | 30 | s0 := New(nil) 31 | s1 := New(outputs.Text("foo")) 32 | s2 := new(Module) 33 | testBar.Run(s0, s1, s2) 34 | 35 | testBar.LatestOutput().AssertText([]string{"foo"}, "on start") 36 | testBar.AssertNoOutput("no change to static modules") 37 | 38 | s0.Set(outputs.Text("baz")) 39 | testBar.LatestOutput(0).AssertText([]string{"baz", "foo"}) 40 | 41 | s1.Clear() 42 | testBar.LatestOutput(1).AssertText([]string{"baz"}) 43 | 44 | s2.Clear() 45 | testBar.LatestOutput(2).AssertText([]string{"baz"}) 46 | 47 | clickCh := make(chan bool, 1) 48 | s2.Set(outputs.Text("foo").OnClick(func(bar.Event) { clickCh <- true })) 49 | 50 | out := testBar.LatestOutput(2) 51 | out.AssertText([]string{"baz", "foo"}) 52 | 53 | select { 54 | case <-clickCh: 55 | require.Fail(t, "spurious click event") 56 | case <-time.After(10 * time.Millisecond): 57 | // test passed 58 | } 59 | 60 | out.At(0).LeftClick() 61 | select { 62 | case <-clickCh: 63 | require.Fail(t, "spurious click event") 64 | case <-time.After(10 * time.Millisecond): 65 | // test passed 66 | } 67 | 68 | out.At(1).LeftClick() 69 | select { 70 | case v := <-clickCh: 71 | require.True(t, v, "click handler triggered") 72 | case <-time.After(time.Second): 73 | require.Fail(t, "click event not received") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /modules/volume/alsa/capi.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative '../../../testing/capi/main.rb' 16 | 17 | make_capi 'alsa/asoundlib.h', library: 'alsa', pkg_config: 'alsa', go_src_files: ['alsa.go'] 18 | -------------------------------------------------------------------------------- /modules/vpn/vpn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package vpn provides an i3bar module for openvpn information. 16 | package vpn 17 | 18 | import ( 19 | "github.com/soumya92/barista/bar" 20 | "github.com/soumya92/barista/base/value" 21 | "github.com/soumya92/barista/base/watchers/netlink" 22 | l "github.com/soumya92/barista/logging" 23 | "github.com/soumya92/barista/outputs" 24 | ) 25 | 26 | // State represents the vpn state. 27 | type State int 28 | 29 | // Connected returns true if the VPN is connected. 30 | func (s State) Connected() bool { 31 | return s == Connected 32 | } 33 | 34 | // Disconnected returns true if the VPN is off. 35 | func (s State) Disconnected() bool { 36 | return s == Disconnected 37 | } 38 | 39 | // Valid states for the vpn 40 | const ( 41 | Disconnected State = iota 42 | Waiting 43 | Connected 44 | ) 45 | 46 | // Module represents a VPN bar module. 47 | type Module struct { 48 | intf string 49 | outputFunc value.Value // of func(State) bar.Output 50 | } 51 | 52 | // New constructs an instance of the VPN module for the specified interface. 53 | func New(iface string) *Module { 54 | m := &Module{intf: iface} 55 | l.Label(m, iface) 56 | l.Register(m, "outputFunc") 57 | // Default output is just 'VPN' when connected. 58 | m.Output(func(s State) bar.Output { 59 | if s.Connected() { 60 | return outputs.Text("VPN") 61 | } 62 | return nil 63 | }) 64 | return m 65 | } 66 | 67 | // DefaultInterface constructs an instance of the VPN module for "tun0", 68 | // the usual interface for VPNs. 69 | func DefaultInterface() *Module { 70 | return New("tun0") 71 | } 72 | 73 | // Output configures a module to display the output of a user-defined function. 74 | func (m *Module) Output(outputFunc func(State) bar.Output) *Module { 75 | m.outputFunc.Set(outputFunc) 76 | return m 77 | } 78 | 79 | // Stream starts the module. 80 | func (m *Module) Stream(s bar.Sink) { 81 | outputFunc := m.outputFunc.Get().(func(State) bar.Output) 82 | nextOutputFunc, done := m.outputFunc.Subscribe() 83 | defer done() 84 | 85 | linkSub := netlink.ByName(m.intf) 86 | defer linkSub.Unsubscribe() 87 | 88 | state := getState(linkSub.Get().State) 89 | for { 90 | s.Output(outputFunc(state)) 91 | select { 92 | case <-linkSub.C: 93 | state = getState(linkSub.Get().State) 94 | case <-nextOutputFunc: 95 | outputFunc = m.outputFunc.Get().(func(State) bar.Output) 96 | } 97 | } 98 | } 99 | 100 | func getState(state netlink.OperState) State { 101 | switch state { 102 | case netlink.Up: 103 | return Connected 104 | case netlink.Dormant: 105 | return Waiting 106 | default: 107 | return Disconnected 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modules/vpn/vpn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package vpn 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/bar" 21 | "github.com/soumya92/barista/base/watchers/netlink" 22 | "github.com/soumya92/barista/outputs" 23 | testBar "github.com/soumya92/barista/testing/bar" 24 | ) 25 | 26 | func TestVpn(t *testing.T) { 27 | nlt := netlink.TestMode() 28 | link := nlt.AddLink(netlink.Link{Name: "tun0", State: netlink.Down}) 29 | 30 | testBar.New(t) 31 | v := DefaultInterface() 32 | 33 | testBar.Run(v) 34 | testBar.NextOutput().AssertText([]string{}) 35 | 36 | nlt.UpdateLink(link, netlink.Link{Name: "tun0", State: netlink.Up}) 37 | testBar.NextOutput().AssertText([]string{"VPN"}) 38 | 39 | v.Output(func(s State) bar.Output { 40 | switch { 41 | case s.Connected(): 42 | return outputs.Text("VPN!") 43 | case s.Disconnected(): 44 | return nil 45 | default: 46 | return outputs.Text("...") 47 | } 48 | }) 49 | testBar.NextOutput().AssertText([]string{"VPN!"}) 50 | 51 | nlt.UpdateLink(link, netlink.Link{Name: "tun0", State: netlink.Dormant}) 52 | testBar.NextOutput().AssertText([]string{"..."}) 53 | 54 | nlt.UpdateLink(link, netlink.Link{Name: "tun0", State: netlink.Up}) 55 | testBar.NextOutput().AssertText([]string{"VPN!"}) 56 | 57 | v.Output(func(s State) bar.Output { 58 | if s.Disconnected() { 59 | return outputs.Text("NO VPN") 60 | } 61 | return nil 62 | }) 63 | testBar.NextOutput().AssertText([]string{}) 64 | 65 | nlt.RemoveLink(link) 66 | testBar.NextOutput().AssertText([]string{"NO VPN"}) 67 | } 68 | -------------------------------------------------------------------------------- /modules/weather/direction.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package weather 16 | 17 | // Deg returns the direction in meteorological degrees. 18 | func (d Direction) Deg() int { 19 | return int(d) 20 | } 21 | 22 | // Cardinal returns the cardinal direction. 23 | func (d Direction) Cardinal() string { 24 | cardinal := "" 25 | deg := d.Deg() 26 | m := 34 // rounded from (90/4 + 90/8) 27 | // primary cardinal direction first. N, E, S, W. 28 | switch { 29 | case deg < m || deg > 360-m: 30 | cardinal = "N" 31 | case 90-m < deg && deg < 90+m: 32 | cardinal = "E" 33 | case 180-m < deg && deg < 180+m: 34 | cardinal = "S" 35 | case 270-m < deg && deg < 270+m: 36 | cardinal = "W" 37 | } 38 | // Now append the midway points. NE, NW, SE, SW. 39 | switch { 40 | case 45-m < deg && deg < 45+m: 41 | cardinal += "NE" 42 | case 135-m < deg && deg < 135+m: 43 | cardinal += "SE" 44 | case 225-m < deg && deg < 225+m: 45 | cardinal += "SW" 46 | case 315-m < deg && deg < 315+m: 47 | cardinal += "NW" 48 | } 49 | return cardinal 50 | } 51 | -------------------------------------------------------------------------------- /modules/weather/direction_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package weather 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestDirection(t *testing.T) { 24 | for _, c := range []struct { 25 | deg int 26 | card string 27 | }{ 28 | // Basic sanity checks. 29 | {0, "N"}, {90, "E"}, {180, "S"}, {270, "W"}, 30 | {45, "NE"}, {135, "SE"}, {225, "SW"}, {315, "NW"}, 31 | 32 | // All boundary conditions. Should be safe to assume that 33 | // points within are correct. 34 | {349, "N"}, {11, "N"}, 35 | {12, "NNE"}, {33, "NNE"}, 36 | {34, "NE"}, {56, "NE"}, 37 | {57, "ENE"}, {78, "ENE"}, 38 | {79, "E"}, {101, "E"}, 39 | {102, "ESE"}, {123, "ESE"}, 40 | {124, "SE"}, {146, "SE"}, 41 | {147, "SSE"}, {168, "SSE"}, 42 | {169, "S"}, {191, "S"}, 43 | {192, "SSW"}, {213, "SSW"}, 44 | {214, "SW"}, {236, "SW"}, 45 | {237, "WSW"}, {258, "WSW"}, 46 | {259, "W"}, {281, "W"}, 47 | {282, "WNW"}, {303, "WNW"}, 48 | {304, "NW"}, {326, "NW"}, 49 | {327, "NNW"}, {348, "NNW"}, 50 | } { 51 | dir := Direction(c.deg) 52 | require.Equal(t, c.card, dir.Cardinal()) 53 | require.Equal(t, c.deg, dir.Deg()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/weather/metar/metar_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package metar 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/soumya92/barista/modules/weather" 22 | testServer "github.com/soumya92/barista/testing/httpserver" 23 | 24 | "github.com/martinlindhe/unit" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestExample(t *testing.T) { 29 | ts := testServer.New() 30 | defer ts.Close() 31 | 32 | provider := Station("KBFI").StripRemarks().IncludeFlightCat().Build().(*provider) 33 | provider.url = ts.URL + "/static/example.xml" 34 | 35 | wthr, err := provider.GetWeather() 36 | require.NoError(t, err) 37 | require.NotNil(t, wthr) 38 | require.Equal(t, weather.Weather{ 39 | Location: "KBFI", 40 | Condition: weather.Overcast, 41 | Description: "[VFR] KBFI 252053Z 00000KT 10SM BKN090 OVC110 09.4/M03.3 A3013", 42 | Humidity: 0.4070166689067204, 43 | Pressure: 1020.4 * unit.Millibar, 44 | Temperature: unit.FromCelsius(9.4), 45 | CloudCover: 1.0, 46 | Updated: time.Unix(1543179180, 0).In(time.UTC), 47 | Attribution: "NWS", 48 | }, wthr) 49 | 50 | provider.url = ts.URL + "/code/503" 51 | wthr, err = provider.GetWeather() 52 | require.NoError(t, err) 53 | require.NotNil(t, wthr) 54 | require.Equal(t, weather.Weather{ 55 | Location: "KBFI", 56 | Condition: weather.Overcast, 57 | Description: "[VFR] KBFI 252053Z 00000KT 10SM BKN090 OVC110 09.4/M03.3 A3013", 58 | Humidity: 0.4070166689067204, 59 | Pressure: 1020.4 * unit.Millibar, 60 | Temperature: unit.FromCelsius(9.4), 61 | CloudCover: 1.0, 62 | Updated: time.Unix(1543179180, 0).In(time.UTC), 63 | Attribution: "NWS", 64 | }, wthr) 65 | 66 | provider.url = ts.URL + "/code/401" 67 | wthr, err = provider.GetWeather() 68 | require.Error(t, err) 69 | 70 | provider.url = ts.URL + "/static/bad.xml" 71 | wthr, err = provider.GetWeather() 72 | require.Error(t, err) 73 | 74 | provider.url = ts.URL + "/static/no-entries.xml" 75 | wthr, err = provider.GetWeather() 76 | require.Error(t, err) 77 | } 78 | -------------------------------------------------------------------------------- /modules/weather/metar/testdata/bad.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17357439 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /modules/weather/metar/testdata/example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17357439 4 | 5 | 6 | 7 | 8 | 4 9 | 10 | 11 | KBFI 252053Z 00000KT 10SM BKN090 OVC110 09/03 A3013 RMK AO2 RAB37E51 SLP204 P0000 60000 T00940033 56009 12 | KBFI 13 | 2018-11-25T20:53:00Z 14 | 47.55 15 | -122.32 16 | 9.4 17 | -3.3 18 | 0 19 | 0 20 | 10.0 21 | 30.129921 22 | 1020.4 23 | 24 | TRUE 25 | 26 | 27 | 28 | VFR 29 | -0.9 30 | 0.005 31 | 0.005 32 | METAR 33 | 4.0 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /modules/weather/metar/testdata/no-entries.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17357439 4 | 5 | 6 | 7 | 8 | 4 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /modules/weather/openweathermap/testdata/bad.json: -------------------------------------------------------------------------------- 1 | {"coord": 2 | {"lon":145.77,"lat":-16.92}, 3 | "weather":[{"id":901,"main":"condition","description":"description","icon":"04n"}], 4 | "base":, 5 | "main":--- 6 | -------------------------------------------------------------------------------- /modules/weather/openweathermap/testdata/empty.json: -------------------------------------------------------------------------------- 1 | {"weather":[]} 2 | -------------------------------------------------------------------------------- /modules/weather/openweathermap/testdata/good.json.tpl: -------------------------------------------------------------------------------- 1 | {"coord": 2 | {"lon":145.77,"lat":-16.92}, 3 | "weather":[{"id":{{.id}},"main":"{{.cond}}","description":"{{.desc}}","icon":"04n"}], 4 | "base":"cmc stations", 5 | "main":{"temp":293.25,"pressure":1019,"humidity":83,"temp_min":289.82,"temp_max":295.37}, 6 | "wind":{"speed":5.1,"deg":150}, 7 | "clouds":{"all":75}, 8 | "rain":{"3h":3}, 9 | "dt":1435658272, 10 | "sys":{"type":1,"id":8166,"message":0.0166,"country":"AU","sunrise":1435610796,"sunset":1435650870}, 11 | "id":2172797, 12 | "name":"Cairns", 13 | "cod":200} 14 | -------------------------------------------------------------------------------- /modules/weather/weather_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package weather 16 | 17 | import ( 18 | "errors" 19 | "sync" 20 | "testing" 21 | 22 | "github.com/soumya92/barista/bar" 23 | "github.com/soumya92/barista/outputs" 24 | testBar "github.com/soumya92/barista/testing/bar" 25 | 26 | "github.com/martinlindhe/unit" 27 | ) 28 | 29 | type testProvider struct { 30 | sync.RWMutex 31 | Weather 32 | error 33 | } 34 | 35 | func (t *testProvider) GetWeather() (Weather, error) { 36 | t.RLock() 37 | defer t.RUnlock() 38 | return t.Weather, t.error 39 | } 40 | 41 | func TestWeather(t *testing.T) { 42 | testBar.New(t) 43 | p := &testProvider{Weather: Weather{ 44 | Location: "Swallow Falls", 45 | Condition: Cloudy, 46 | Description: "chance of meatballs", 47 | Temperature: unit.FromFahrenheit(72), 48 | Humidity: 0.7, 49 | Attribution: "FLDSMDFR", 50 | }} 51 | w := New(p) 52 | testBar.Run(w) 53 | 54 | testBar.NextOutput().AssertText( 55 | []string{"22.2℃ chance of meatballs (FLDSMDFR)"}, "on start") 56 | 57 | testBar.Tick() 58 | testBar.NextOutput().Expect("on tick") 59 | 60 | w.Output(func(w Weather) bar.Output { 61 | return outputs.Textf("%.0f, by %s", w.Temperature.Fahrenheit(), w.Attribution) 62 | }) 63 | testBar.NextOutput().AssertText([]string{ 64 | "72, by FLDSMDFR"}, "on template change") 65 | 66 | p.Lock() 67 | p.error = errors.New("foo") 68 | p.Unlock() 69 | 70 | testBar.Tick() 71 | testBar.NextOutput().AssertError("on tick with error") 72 | 73 | testBar.Tick() 74 | out := testBar.NextOutput("on tick with error") 75 | 76 | p.Lock() 77 | p.error = nil 78 | out.At(0).LeftClick() 79 | testBar.NextOutput().AssertEmpty("clears error on refresh") 80 | 81 | p.Unlock() 82 | testBar.NextOutput().AssertText([]string{"72, by FLDSMDFR"}) 83 | } 84 | -------------------------------------------------------------------------------- /oauth/testdata/empty.json: -------------------------------------------------------------------------------- 1 | {"Salt":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==","IV":"AAECAwQFBgcICQoLDA0ODw==","Token":"1H/HbuTbnVAnvEJCu1+hJ7vas0gATXDa93M33RVrsYoWpLyX03hQxuzR6RjcidHoAMhD"} 2 | -------------------------------------------------------------------------------- /oauth/testdata/simple.json: -------------------------------------------------------------------------------- 1 | {"Salt":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==","IV":"AAECAwQFBgcICQoLDA0ODw==","Token":"1H/HbuTbnVAnvEJCu1+hJ7ue8AUHVHKRqShnglEp5Mk7aW2zB9LGs5SeAOEzA0vtJiDRTwOq9uSRq+DYqxry4jZ/U0dHq/jCBFjQOvC+Dx+1oc+Qs3Gw"} 2 | -------------------------------------------------------------------------------- /outputs/outputs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package outputs provides helper functions to construct bar.Outputs. 16 | package outputs 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/soumya92/barista/bar" 22 | "github.com/soumya92/barista/format" 23 | "github.com/soumya92/barista/pango" 24 | ) 25 | 26 | // Errorf constructs a bar output that indicates an error, 27 | // using the given format string and arguments. 28 | func Errorf(format string, args ...interface{}) *bar.Segment { 29 | return Error(fmt.Errorf(format, args...)) 30 | } 31 | 32 | // Error constructs a bar output that indicates an error. 33 | func Error(e error) *bar.Segment { 34 | return bar.ErrorSegment(e) 35 | } 36 | 37 | // Textf constructs simple text output from a format string and arguments. 38 | func Textf(format string, args ...interface{}) *bar.Segment { 39 | return Text(fmt.Sprintf(format, args...)) 40 | } 41 | 42 | //Text constructs a simple text output from the given string. 43 | func Text(text string) *bar.Segment { 44 | return bar.TextSegment(text) 45 | } 46 | 47 | // Pango constructs a pango bar segment from a list of pango Nodes and strings. 48 | func Pango(things ...interface{}) *bar.Segment { 49 | nodes := []*pango.Node{} 50 | for _, thing := range things { 51 | switch t := thing.(type) { 52 | case *pango.Node: 53 | nodes = append(nodes, t) 54 | case string: 55 | nodes = append(nodes, pango.Text(t)) 56 | case format.Value: 57 | nodes = append(nodes, pango.Unit(t)) 58 | case format.Values: 59 | nodes = append(nodes, pango.Unit(t...)) 60 | default: 61 | if val, ok := format.Unit(t); ok { 62 | nodes = append(nodes, pango.Unit(val...)) 63 | } else { 64 | nodes = append(nodes, pango.Textf("%v", t)) 65 | } 66 | } 67 | } 68 | return bar.PangoSegment(pango.New(nodes...).String()) 69 | } 70 | 71 | // Group concatenates several outputs into a single SegmentGroup, 72 | // to facilitate easier manipulation of output properties. 73 | // For example, setting a colour or urgency for all segments together. 74 | func Group(outputs ...bar.Output) *SegmentGroup { 75 | group := new(SegmentGroup) 76 | for _, o := range outputs { 77 | group.Append(o) 78 | } 79 | return group 80 | } 81 | -------------------------------------------------------------------------------- /pango/icon.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pango 16 | 17 | import ( 18 | "strings" 19 | 20 | l "github.com/soumya92/barista/logging" 21 | ) 22 | 23 | // IconProvider is an interface for providing pango Icons. 24 | // The function should return a pango node for the given 25 | // icon name, or nil if an icon could not be found. 26 | type IconProvider func(string) *Node 27 | 28 | var iconProviders = map[string]IconProvider{} 29 | 30 | // Icon returns a pango node that displays the given icon. 31 | // The identifier must be of the form $provider-$name, and the returned 32 | // node will render the $name icon using $provider. e.g. "fa-add" will 33 | // render the "add" icon using font awesome. 34 | func Icon(ident string) *Node { 35 | providerAndName := strings.SplitN(ident, "-", 2) 36 | if len(providerAndName) != 2 { 37 | l.Log("Could not identify icon provider in '%s'", ident) 38 | return &Node{} 39 | } 40 | provider := providerAndName[0] 41 | name := providerAndName[1] 42 | if p, ok := iconProviders[provider]; ok { 43 | node := p(name) 44 | if node != nil { 45 | node.attributes["fallback"] = "false" 46 | return New(node) 47 | } 48 | } 49 | return &Node{} 50 | } 51 | 52 | // AddIconProvider adds an icon provider for a given prefix. 53 | // This is intended for use only by the pango/icons package. 54 | // See "pango/icons".LoadFromFile for more details. 55 | func AddIconProvider(name string, provider IconProvider) { 56 | iconProviders[name] = provider 57 | } 58 | -------------------------------------------------------------------------------- /pango/icon_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pango 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/testing/pango" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestNoProviders(t *testing.T) { 26 | iconProviders = map[string]IconProvider{} 27 | require.Empty(t, 28 | Icon("anything-iconname").String(), 29 | "when no providers are added") 30 | 31 | require.Empty(t, 32 | Icon("alert").String(), 33 | "when no providers are added") 34 | } 35 | 36 | func singleIconProvider(name string) IconProvider { 37 | return func(s string) *Node { 38 | if string(s) == name { 39 | return Textf("s:%s", s).Small() 40 | } 41 | return nil 42 | } 43 | } 44 | 45 | func TestProviders(t *testing.T) { 46 | iconProviders = map[string]IconProvider{} 47 | AddIconProvider("t1", singleIconProvider("foo")) 48 | AddIconProvider("t2", singleIconProvider("bar")) 49 | 50 | require.Empty(t, Icon("t0-bar").String(), "non-existent provider") 51 | require.Empty(t, Icon("t1-bar").String(), "non-existent icon") 52 | pango.AssertText(t, "s:bar", Icon("t2-bar").String(), 53 | "provider name is not passed to Icon(...)") 54 | } 55 | -------------------------------------------------------------------------------- /pango/icons/fontawesome/fontawesome.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package fontawesome provides support for FontAwesome Icons 17 | from https://github.com/FortAwesome/Font-Awesome 18 | 19 | It uses metadata/icons.yml to get the list of icons, 20 | and requires fonts/fontawesome-webfont.ttf to be installed. 21 | */ 22 | package fontawesome 23 | 24 | import ( 25 | "fmt" 26 | "path/filepath" 27 | 28 | "github.com/soumya92/barista/pango" 29 | "github.com/soumya92/barista/pango/icons" 30 | 31 | "github.com/spf13/afero" 32 | "gopkg.in/yaml.v2" 33 | ) 34 | 35 | type faMetadata struct { 36 | Code string `yaml:"unicode"` 37 | Styles []string `yaml:"styles"` 38 | } 39 | 40 | var fs = afero.NewOsFs() 41 | 42 | // Load initialises the fontawesome icon provider from the given repo. 43 | func Load(repoPath string) error { 44 | f, err := fs.Open(filepath.Join(repoPath, "metadata/icons.yml")) 45 | if err != nil { 46 | return err 47 | } 48 | defer f.Close() 49 | 50 | // Defaults to solid since that style has the most icons available. 51 | faSolid := icons.NewProvider("fa") 52 | faSolid.Font("Font Awesome 5 Free") 53 | faSolid.AddStyle(func(n *pango.Node) { n.Weight(900) }) 54 | 55 | faBrands := icons.NewProvider("fab") 56 | faBrands.Font("Font Awesome 5 Brands") 57 | 58 | faRegular := icons.NewProvider("far") 59 | faRegular.Font("Font Awesome 5 Free") 60 | 61 | styles := map[string]*icons.Provider{ 62 | "solid": faSolid, 63 | "regular": faRegular, 64 | "brands": faBrands, 65 | } 66 | 67 | var glyphs map[string]faMetadata 68 | err = yaml.NewDecoder(f).Decode(&glyphs) 69 | if err != nil { 70 | return err 71 | } 72 | for name, meta := range glyphs { 73 | for _, style := range meta.Styles { 74 | p, ok := styles[style] 75 | if !ok { 76 | return fmt.Errorf("Unknown FontAwesome style: '%s'", style) 77 | } 78 | err = p.Hex(name, meta.Code) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pango/icons/icons.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package icons provides an interface for using icon fonts in a bar. 17 | To use an icon font: 18 | - Clone a supported repository 19 | - Link the ttf into ~/.fonts 20 | - Load the icon by passing it the path to the repo 21 | - Use icons as pango constructs in your bar 22 | 23 | Compatible icon fonts: 24 | - Material Design Icons (+community fork) 25 | - FontAwesome 26 | - Typicons 27 | 28 | Example usage: 29 | material.Load("/Users/me/Github/google/material-design-icons") 30 | ... 31 | return pango.Icon("material-today").Color(colors.Hex("#ddd")). 32 | Append(pango.Text(now.Sprintf("%H:%M"))) 33 | */ 34 | package icons 35 | 36 | import ( 37 | "strconv" 38 | 39 | "github.com/soumya92/barista/pango" 40 | ) 41 | 42 | // Provider provides pango nodes for icons 43 | type Provider struct { 44 | symbols map[string]string 45 | styles []func(*pango.Node) 46 | } 47 | 48 | // NewProvider creates a new icon provider with the given name, 49 | // registers it with pango.Icon, and returns it so that an appropriate 50 | // Load method can be used. 51 | func NewProvider(name string) *Provider { 52 | p := &Provider{symbols: map[string]string{}} 53 | pango.AddIconProvider(name, p.icon) 54 | return p 55 | } 56 | 57 | // icon creates a pango node that renders the named icon. 58 | func (p *Provider) icon(name string) *pango.Node { 59 | symbol, ok := p.symbols[name] 60 | if !ok { 61 | return nil 62 | } 63 | n := pango.Text(symbol) 64 | for _, s := range p.styles { 65 | s(n) 66 | } 67 | return n 68 | } 69 | 70 | // Hex adds a symbol to the provider where the value is given 71 | // in hex-encoded form. 72 | func (p *Provider) Hex(name, value string) error { 73 | sym, err := SymbolFromHex(value) 74 | if err != nil { 75 | return err 76 | } 77 | p.Symbol(name, sym) 78 | return nil 79 | } 80 | 81 | // Symbol adds a symbol to the provider where the value is the 82 | // symbol/string to use for the icon. 83 | func (p *Provider) Symbol(name, value string) { 84 | p.symbols[name] = value 85 | } 86 | 87 | // Font sets the font set on the returned pango nodes. 88 | func (p *Provider) Font(font string) { 89 | p.AddStyle(func(n *pango.Node) { n.Font(font) }) 90 | } 91 | 92 | // AddStyle sets additional styles on all returned pango nodes. 93 | func (p *Provider) AddStyle(style func(*pango.Node)) { 94 | p.styles = append(p.styles, style) 95 | } 96 | 97 | // SymbolFromHex parses a hex string (e.g. "1F44D") and converts 98 | // it to a string (e.g. "👍"). 99 | func SymbolFromHex(hex string) (string, error) { 100 | intVal, err := strconv.ParseUint(hex, 16, 32) 101 | if err != nil { 102 | return "", err 103 | } 104 | return string(rune(intVal)), nil 105 | } 106 | -------------------------------------------------------------------------------- /pango/icons/icons_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package icons 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/colors" 21 | "github.com/soumya92/barista/pango" 22 | pangoTesting "github.com/soumya92/barista/testing/pango" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestSymbolFromHex(t *testing.T) { 28 | tests := []struct{ input, expected string }{ 29 | {"1F44D", "👍"}, 30 | {"0024", "$"}, 31 | {"20AC", "€"}, 32 | {"10437", "𐐷"}, 33 | {"24B62", "𤭢"}, 34 | } 35 | for _, tc := range tests { 36 | value, err := SymbolFromHex(tc.input) 37 | require.NoError(t, err, "Decoding a valid symbol from hex") 38 | require.Equal(t, tc.expected, value, "Symbol is decoded correctly") 39 | } 40 | 41 | _, err := SymbolFromHex("04xc") 42 | require.Error(t, err, "Error with invalid hex string") 43 | _, err = SymbolFromHex("ffffffffffff") 44 | require.Error(t, err, "Error with out of bounds hex") 45 | } 46 | 47 | func TestIconProvider(t *testing.T) { 48 | p := NewProvider("test") 49 | require.NoError(t, p.Hex("lgtm", "1F44D")) 50 | require.Error(t, p.Hex("not-real", "xx")) 51 | p.Symbol("test", "a") 52 | p.Symbol("ligature-font", "home") 53 | p.Font("testfont") 54 | p.AddStyle(func(n *pango.Node) { n.Weight(200) }) 55 | 56 | tests := []struct{ desc, icon, expected string }{ 57 | {"no output for unknown icon", "unknown", ""}, 58 | {"simple icon", "test", "a"}, 59 | {"emoji", "lgtm", "👍"}, 60 | {"ligature", "ligature-font", "home"}, 61 | } 62 | for _, tc := range tests { 63 | pangoTesting.AssertEqual(t, tc.expected, pango.Icon("test-"+tc.icon).String(), tc.desc) 64 | } 65 | 66 | pangoTesting.AssertEqual(t, 67 | "a", 68 | pango.Icon("test-test").Color(colors.Hex("#f00")).String(), 69 | "Attributes are added to a wrapping ", 70 | ) 71 | 72 | pangoTesting.AssertEqual(t, 73 | `homefoobar`, 75 | pango.Icon("test-ligature-font").Italic().Append(pango.Text("foobar").Bold()).String(), 76 | "Append adds new elements without icon font styling", 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /pango/icons/material/material.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package material provides support for Google's Material Design Icons 17 | from https://github.com/google/material-design-icons 18 | 19 | It uses font/MaterialIcons-Regular.codepoints to get the list of icons, 20 | and requires font/MaterialIcons-Regular.ttf to be installed. 21 | */ 22 | package material 23 | 24 | import ( 25 | "bufio" 26 | "fmt" 27 | "path/filepath" 28 | "strings" 29 | 30 | "github.com/soumya92/barista/pango" 31 | "github.com/soumya92/barista/pango/icons" 32 | 33 | "github.com/spf13/afero" 34 | ) 35 | 36 | var fs = afero.NewOsFs() 37 | 38 | // Load initialises the material design icon provider from the given repo. 39 | func Load(repoPath string) error { 40 | f, err := fs.Open(filepath.Join(repoPath, "font/MaterialIcons-Regular.codepoints")) 41 | if err != nil { 42 | return err 43 | } 44 | defer f.Close() 45 | material := icons.NewProvider("material") 46 | material.Font("Material Icons") 47 | material.AddStyle(func(n *pango.Node) { n.UltraLight().Rise(-4000) }) 48 | s := bufio.NewScanner(f) 49 | s.Split(bufio.ScanLines) 50 | for s.Scan() { 51 | line := strings.TrimSpace(s.Text()) 52 | components := strings.Split(line, " ") 53 | if len(components) != 2 { 54 | return fmt.Errorf("Unexpected line '%s' in 'font/MaterialIcons-Regular.codepoints'", line) 55 | } 56 | // Material Design Icons uses '_', but all other fonts use '-', 57 | // so we'll normalise it here. 58 | name := strings.Replace(components[0], "_", "-", -1) 59 | value := components[1] 60 | err = material.Hex(name, value) 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pango/icons/material/material_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package material 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/pango" 21 | "github.com/soumya92/barista/testing/cron" 22 | "github.com/soumya92/barista/testing/githubfs" 23 | pangoTesting "github.com/soumya92/barista/testing/pango" 24 | 25 | "github.com/spf13/afero" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestInvalid(t *testing.T) { 30 | fs = afero.NewMemMapFs() 31 | require.Error(t, Load("/src/no-such-directory")) 32 | 33 | afero.WriteFile(fs, "/src/material-error-1/font/MaterialIcons-Regular.codepoints", []byte( 34 | `-- Lines in weird formats -- 35 | Empty: 36 | 37 | Valid line: 38 | someIcon 61 39 | otherIcon 62 40 | Invalid codepoint: 41 | badIcon xy`, 42 | ), 0644) 43 | require.Error(t, Load("/src/material-error-1")) 44 | 45 | afero.WriteFile(fs, "/src/material-error-2/iconfont/codepoint", nil, 0644) 46 | require.Error(t, Load("/src/material-error-2")) 47 | 48 | afero.WriteFile(fs, "/src/material-error-3/font/MaterialIcons-Regular.codepoints", []byte( 49 | `someIcon 61 50 | otherIcon 62 51 | badIcon xy`, 52 | ), 0644) 53 | require.Error(t, Load("/src/material-error-3")) 54 | } 55 | 56 | func TestValid(t *testing.T) { 57 | fs = afero.NewMemMapFs() 58 | afero.WriteFile(fs, "/src/material/font/MaterialIcons-Regular.codepoints", []byte( 59 | `someIcon 61 60 | otherIcon 62 61 | thirdIcon 63`, 62 | ), 0644) 63 | require.NoError(t, Load("/src/material")) 64 | pangoTesting.AssertText(t, "a", pango.Icon("material-someIcon").String()) 65 | } 66 | 67 | // TestLive tests that current master branch of the icon font works with 68 | // this package. This test only runs when CI runs tests in 'cron' mode, 69 | // which provides timely notifications of incompatible changes while 70 | // keeping default tests hermetic. 71 | func TestLive(t *testing.T) { 72 | fs = githubfs.New() 73 | cron.Test(t, func() error { 74 | if err := Load("/google/material-design-icons/master"); err != nil { 75 | return err 76 | } 77 | // At least one of these icons should be loaded. 78 | testIcons := pango.New( 79 | pango.Icon("material-face"), 80 | pango.Icon("material-room"), 81 | pango.Icon("material-view-agenda"), 82 | pango.Icon("material-link-off"), 83 | ) 84 | require.NotEmpty(t, testIcons.String(), "No expected icons were loaded") 85 | return nil 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /pango/icons/mdi/mdi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package mdi provides support for "Material Design Icons" from 17 | https://materialdesignicons.com/, a fork and extension of Material. 18 | 19 | It requires cloning the webfont repo 20 | https://github.com/Templarian/MaterialDesign-Webfont, 21 | uses scss/_variables.scss to get the list of icons, 22 | and requires fonts/materialdesignicons-webfont.ttf to be installed. 23 | */ 24 | package mdi 25 | 26 | import ( 27 | "bufio" 28 | "errors" 29 | "fmt" 30 | "path/filepath" 31 | "strings" 32 | "unicode" 33 | 34 | "github.com/soumya92/barista/pango" 35 | "github.com/soumya92/barista/pango/icons" 36 | 37 | "github.com/spf13/afero" 38 | ) 39 | 40 | var fs = afero.NewOsFs() 41 | 42 | // Load initialises the material design (community) icon provider 43 | // from the given repo. 44 | func Load(repoPath string) error { 45 | f, err := fs.Open(filepath.Join(repoPath, "scss/_variables.scss")) 46 | if err != nil { 47 | return err 48 | } 49 | defer f.Close() 50 | mdi := icons.NewProvider("mdi") 51 | mdi.Font("Material Design Icons") 52 | mdi.AddStyle(func(n *pango.Node) { n.UltraLight() }) 53 | started := false 54 | s := bufio.NewScanner(f) 55 | s.Split(bufio.ScanLines) 56 | for s.Scan() { 57 | line := strings.TrimSpace(s.Text()) 58 | if !started { 59 | if strings.Contains(line, "$mdi-icons:") { 60 | started = true 61 | } 62 | continue 63 | } 64 | if line == ");" { 65 | return nil 66 | } 67 | colon := strings.Index(line, ":") 68 | if colon < 0 { 69 | return fmt.Errorf("Unexpected line '%s'", line) 70 | } 71 | name := strings.TrimFunc(line[:colon], func(r rune) bool { 72 | return unicode.IsSpace(r) || r == '"' 73 | }) 74 | value := strings.TrimFunc(line[colon+1:], func(r rune) bool { 75 | return unicode.IsSpace(r) || r == ',' 76 | }) 77 | err = mdi.Hex(name, value) 78 | if err != nil { 79 | return err 80 | } 81 | } 82 | if !started { 83 | return errors.New("Could not find any icons in _variables.scss") 84 | } 85 | return errors.New("Expected ); to end $mdi-icons, got end of file") 86 | } 87 | -------------------------------------------------------------------------------- /pango/icons/typicons/typicons.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package typicons provides support for Typicons 17 | from https://github.com/stephenhutchings/typicons.font 18 | 19 | It uses config.yml to get the list of icons, 20 | and requires src/font/typicons.ttf to be installed. 21 | */ 22 | package typicons 23 | 24 | import ( 25 | "encoding/json" 26 | "path/filepath" 27 | 28 | "github.com/soumya92/barista/pango/icons" 29 | 30 | "github.com/spf13/afero" 31 | ) 32 | 33 | var fs = afero.NewOsFs() 34 | 35 | // Load initialises the typicons icon provider from the given repo. 36 | func Load(repoPath string) error { 37 | f, err := fs.Open(filepath.Join(repoPath, "src/font/typicons.json")) 38 | if err != nil { 39 | return err 40 | } 41 | defer f.Close() 42 | t := icons.NewProvider("typecn") 43 | t.Font("Typicons") 44 | var conf map[string]int 45 | err = json.NewDecoder(f).Decode(&conf) 46 | if err != nil { 47 | return err 48 | } 49 | for name, glyphCode := range conf { 50 | t.Symbol(name, string(rune(glyphCode))) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pango/icons/typicons/typicons_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package typicons 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/soumya92/barista/pango" 21 | "github.com/soumya92/barista/testing/cron" 22 | "github.com/soumya92/barista/testing/githubfs" 23 | pangoTesting "github.com/soumya92/barista/testing/pango" 24 | 25 | "github.com/spf13/afero" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestInvalid(t *testing.T) { 30 | fs = afero.NewMemMapFs() 31 | require.Error(t, Load("/src/no-such-directory")) 32 | 33 | afero.WriteFile(fs, "/src/typicons-error-1/src/font/typicons.json", []byte( 34 | `-- Invalid JSON --`, 35 | ), 0644) 36 | require.Error(t, Load("/src/typicons-error-1")) 37 | 38 | afero.WriteFile(fs, "/src/typicons-error-2/src/font/typicons.json", nil, 0644) 39 | require.Error(t, Load("/src/typicons-error-2")) 40 | 41 | afero.WriteFile(fs, "/src/typicons-error-3/src/font/typicons.json", []byte( 42 | `{ 43 | "someIcon": 97, 44 | "otherIcon": 98, 45 | "thirdIcon": "ghij" 46 | } 47 | `, 48 | ), 0644) 49 | require.Error(t, Load("/src/typicons-error-3")) 50 | } 51 | 52 | func TestValid(t *testing.T) { 53 | fs = afero.NewMemMapFs() 54 | afero.WriteFile(fs, "/src/typicons/src/font/typicons.json", []byte( 55 | `{ 56 | "someIcon": 97, 57 | "otherIcon": 98, 58 | "thirdIcon": 99 59 | } 60 | `, 61 | ), 0644) 62 | require.NoError(t, Load("/src/typicons")) 63 | pangoTesting.AssertText(t, "a", pango.Icon("typecn-someIcon").String()) 64 | pangoTesting.AssertText(t, "b", pango.Icon("typecn-otherIcon").String()) 65 | } 66 | 67 | // TestLive tests that current master branch of the icon font works with 68 | // this package. This test only runs when CI runs tests in 'cron' mode, 69 | // which provides timely notifications of incompatible changes while 70 | // keeping default tests hermetic. 71 | func TestLive(t *testing.T) { 72 | fs = githubfs.New() 73 | cron.Test(t, func() error { 74 | if err := Load("/stephenhutchings/typicons.font/master"); err != nil { 75 | return err 76 | } 77 | // At least one of these icons should be loaded. 78 | testIcons := pango.New( 79 | pango.Icon("typecn-pen"), 80 | pango.Icon("typecn-flag-outline"), 81 | pango.Icon("typecn-plus"), 82 | pango.Icon("typecn-beaker"), 83 | ) 84 | require.NotEmpty(t, testIcons.String(), "No expected icons were loaded") 85 | return nil 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /pango/kwattrs.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'optparse' 16 | require_relative '../rb/gofile.rb' 17 | 18 | attr_name = nil 19 | OptionParser.new do |opts| 20 | opts.on('-n', '--name NAME', 'Attribute name') { |v| attr_name = v } 21 | end.parse! 22 | abort if attr_name.nil? 23 | 24 | write_go_file("kwattrs_#{attr_name}.go") do |out| 25 | out.puts 'package pango' 26 | ARGV.map { |it| it.split ':' }.each do |method_name, value| 27 | value = method_name.downcase if value.nil? 28 | out.write(<<~METHOD) 29 | 30 | // #{method_name} sets the pango #{attr_name} to "#{value}". 31 | func (n *Node) #{method_name}() *Node { 32 | \treturn n.setAttr("#{attr_name}", "#{value}") 33 | } 34 | METHOD 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /pango/kwattrs_size.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // XXSmall sets the pango size to "xx-small". 20 | func (n *Node) XXSmall() *Node { 21 | return n.setAttr("size", "xx-small") 22 | } 23 | 24 | // XSmall sets the pango size to "x-small". 25 | func (n *Node) XSmall() *Node { 26 | return n.setAttr("size", "x-small") 27 | } 28 | 29 | // Small sets the pango size to "small". 30 | func (n *Node) Small() *Node { 31 | return n.setAttr("size", "small") 32 | } 33 | 34 | // Medium sets the pango size to "medium". 35 | func (n *Node) Medium() *Node { 36 | return n.setAttr("size", "medium") 37 | } 38 | 39 | // Large sets the pango size to "large". 40 | func (n *Node) Large() *Node { 41 | return n.setAttr("size", "large") 42 | } 43 | 44 | // XLarge sets the pango size to "x-large". 45 | func (n *Node) XLarge() *Node { 46 | return n.setAttr("size", "x-large") 47 | } 48 | 49 | // XXLarge sets the pango size to "xx-large". 50 | func (n *Node) XXLarge() *Node { 51 | return n.setAttr("size", "xx-large") 52 | } 53 | -------------------------------------------------------------------------------- /pango/kwattrs_stretch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // UltraCondensed sets the pango stretch to "ultracondensed". 20 | func (n *Node) UltraCondensed() *Node { 21 | return n.setAttr("stretch", "ultracondensed") 22 | } 23 | 24 | // ExtraCondensed sets the pango stretch to "extracondensed". 25 | func (n *Node) ExtraCondensed() *Node { 26 | return n.setAttr("stretch", "extracondensed") 27 | } 28 | 29 | // Condensed sets the pango stretch to "condensed". 30 | func (n *Node) Condensed() *Node { 31 | return n.setAttr("stretch", "condensed") 32 | } 33 | 34 | // SemiCondensed sets the pango stretch to "semicondensed". 35 | func (n *Node) SemiCondensed() *Node { 36 | return n.setAttr("stretch", "semicondensed") 37 | } 38 | 39 | // StretchNormal sets the pango stretch to "normal". 40 | func (n *Node) StretchNormal() *Node { 41 | return n.setAttr("stretch", "normal") 42 | } 43 | 44 | // SemiExpanded sets the pango stretch to "semiexpanded". 45 | func (n *Node) SemiExpanded() *Node { 46 | return n.setAttr("stretch", "semiexpanded") 47 | } 48 | 49 | // Expanded sets the pango stretch to "expanded". 50 | func (n *Node) Expanded() *Node { 51 | return n.setAttr("stretch", "expanded") 52 | } 53 | 54 | // ExtraExpanded sets the pango stretch to "extraexpanded". 55 | func (n *Node) ExtraExpanded() *Node { 56 | return n.setAttr("stretch", "extraexpanded") 57 | } 58 | 59 | // UltraExpanded sets the pango stretch to "ultraexpanded". 60 | func (n *Node) UltraExpanded() *Node { 61 | return n.setAttr("stretch", "ultraexpanded") 62 | } 63 | -------------------------------------------------------------------------------- /pango/kwattrs_strikethrough.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // Strikethrough sets the pango strikethrough to "true". 20 | func (n *Node) Strikethrough() *Node { 21 | return n.setAttr("strikethrough", "true") 22 | } 23 | 24 | // NoStrikethrough sets the pango strikethrough to "false". 25 | func (n *Node) NoStrikethrough() *Node { 26 | return n.setAttr("strikethrough", "false") 27 | } 28 | -------------------------------------------------------------------------------- /pango/kwattrs_style.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // StyleNormal sets the pango style to "normal". 20 | func (n *Node) StyleNormal() *Node { 21 | return n.setAttr("style", "normal") 22 | } 23 | 24 | // Oblique sets the pango style to "oblique". 25 | func (n *Node) Oblique() *Node { 26 | return n.setAttr("style", "oblique") 27 | } 28 | 29 | // Italic sets the pango style to "italic". 30 | func (n *Node) Italic() *Node { 31 | return n.setAttr("style", "italic") 32 | } 33 | -------------------------------------------------------------------------------- /pango/kwattrs_underline.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // UnderlineNone sets the pango underline to "none". 20 | func (n *Node) UnderlineNone() *Node { 21 | return n.setAttr("underline", "none") 22 | } 23 | 24 | // UnderlineSingle sets the pango underline to "single". 25 | func (n *Node) UnderlineSingle() *Node { 26 | return n.setAttr("underline", "single") 27 | } 28 | 29 | // UnderlineDouble sets the pango underline to "double". 30 | func (n *Node) UnderlineDouble() *Node { 31 | return n.setAttr("underline", "double") 32 | } 33 | 34 | // UnderlineLow sets the pango underline to "low". 35 | func (n *Node) UnderlineLow() *Node { 36 | return n.setAttr("underline", "low") 37 | } 38 | 39 | // UnderlineError sets the pango underline to "error". 40 | func (n *Node) UnderlineError() *Node { 41 | return n.setAttr("underline", "error") 42 | } 43 | -------------------------------------------------------------------------------- /pango/kwattrs_variant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // VariantNormal sets the pango variant to "normal". 20 | func (n *Node) VariantNormal() *Node { 21 | return n.setAttr("variant", "normal") 22 | } 23 | 24 | // SmallCaps sets the pango variant to "smallcaps". 25 | func (n *Node) SmallCaps() *Node { 26 | return n.setAttr("variant", "smallcaps") 27 | } 28 | -------------------------------------------------------------------------------- /pango/kwattrs_weight.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by kwattrs.rb; DO NOT EDIT. 16 | 17 | package pango 18 | 19 | // UltraLight sets the pango weight to "ultralight". 20 | func (n *Node) UltraLight() *Node { 21 | return n.setAttr("weight", "ultralight") 22 | } 23 | 24 | // Light sets the pango weight to "light". 25 | func (n *Node) Light() *Node { 26 | return n.setAttr("weight", "light") 27 | } 28 | 29 | // WeightNormal sets the pango weight to "normal". 30 | func (n *Node) WeightNormal() *Node { 31 | return n.setAttr("weight", "normal") 32 | } 33 | 34 | // Bold sets the pango weight to "bold". 35 | func (n *Node) Bold() *Node { 36 | return n.setAttr("weight", "bold") 37 | } 38 | 39 | // UltraBold sets the pango weight to "ultrabold". 40 | func (n *Node) UltraBold() *Node { 41 | return n.setAttr("weight", "ultrabold") 42 | } 43 | 44 | // Heavy sets the pango weight to "heavy". 45 | func (n *Node) Heavy() *Node { 46 | return n.setAttr("weight", "heavy") 47 | } 48 | -------------------------------------------------------------------------------- /rb/gofile.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative 'run_cmd.rb' 16 | 17 | COPYRIGHT = File.readlines(__FILE__) 18 | .take_while { |line| line.start_with? '#' } 19 | .map { |comment| comment.sub('#', '//') } 20 | .join 21 | 22 | # Writes the standard copyright and generated file header, then provides a file 23 | # handle for callers to append the actual go code, and finally runs the file 24 | # through goimports to clean up any unused imports and format the code. 25 | def write_go_file(name) 26 | out = File.open(name, 'w') 27 | out.puts COPYRIGHT 28 | out.puts 29 | out.puts "// Code generated by #{File.basename($PROGRAM_NAME)}; DO NOT EDIT." 30 | out.puts 31 | yield out 32 | out.close 33 | run_cmd('goimports', '-w', name) 34 | end 35 | -------------------------------------------------------------------------------- /rb/run_cmd.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'open3' 16 | 17 | def run_cmd(*args) 18 | output, status = Open3.capture2e(*args) 19 | raise "Error executing #{args}: #{status}" unless status.success? 20 | 21 | output 22 | end 23 | -------------------------------------------------------------------------------- /samples/sample-bar/cachehttp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !prod 16 | 17 | // When included in the main build process, this file PERMANENTLY caches all 18 | // HTTP requests. This is useful for quickly prototyping customisations to the 19 | // bar without incurring HTTP costs or consuming quota on remote services. 20 | // The cache is stored in ~/.cache/barista/http (using XDG_CACHE_HOME if set), 21 | // and individual responses can be deleted if a fresher copy is needed. 22 | // Once you are satisfied with the bar, simply omit this file (or build with 23 | // the "prod" tag) to build a production version of the bar with no caching. 24 | 25 | package main 26 | 27 | import "net/http" 28 | import "github.com/soumya92/barista/testing/httpcache" 29 | 30 | func init() { 31 | http.DefaultTransport = httpcache.Wrap(http.DefaultTransport) 32 | } 33 | -------------------------------------------------------------------------------- /samples/stable-api/stable-api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // stable-api demonstrates a bar that exercises barista's stable API. 16 | package main 17 | 18 | import ( 19 | "syscall" 20 | "time" 21 | 22 | "github.com/soumya92/barista" 23 | "github.com/soumya92/barista/bar" 24 | "github.com/soumya92/barista/timing" 25 | 26 | "github.com/dustin/go-humanize" 27 | ) 28 | 29 | type simpleClockModule struct { 30 | format string 31 | interval time.Duration 32 | } 33 | 34 | func (s simpleClockModule) Stream(sink bar.Sink) { 35 | sch := timing.NewScheduler() 36 | for { 37 | now := timing.Now() 38 | sink.Output(bar.TextSegment(now.Format(s.format))) 39 | next := now.Add(s.interval).Truncate(s.interval) 40 | sch.At(next).Tick() 41 | } 42 | } 43 | 44 | type diskSpaceModule string 45 | 46 | func (d diskSpaceModule) Stream(sink bar.Sink) { 47 | sch := timing.NewScheduler().Every(5 * time.Second) 48 | for { 49 | var stat syscall.Statfs_t 50 | err := syscall.Statfs(string(d), &stat) 51 | if sink.Error(err) { 52 | return 53 | } 54 | sink.Output(bar.TextSegment( 55 | humanize.IBytes(stat.Bavail * uint64(stat.Bsize)), 56 | )) 57 | sch.Tick() 58 | } 59 | } 60 | 61 | func main() { 62 | panic(barista.Run( 63 | diskSpaceModule("/home"), 64 | simpleClockModule{"Mon Jan 02", time.Hour}, 65 | simpleClockModule{"15:04:05", time.Second}, 66 | )) 67 | } 68 | -------------------------------------------------------------------------------- /sink/sink.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package sink provides functions to create sinks. 16 | package sink 17 | 18 | import ( 19 | "github.com/soumya92/barista/bar" 20 | "github.com/soumya92/barista/base/value" 21 | ) 22 | 23 | // Func creates a bar.Sink that sends sends output in the form of Segments. 24 | func Func(s func(bar.Segments)) bar.Sink { 25 | return func(o bar.Output) { 26 | if o == nil { 27 | s(nil) 28 | return 29 | } 30 | s(o.Segments()) 31 | } 32 | } 33 | 34 | // New creates a new sink and returns a channel that 35 | // will emit any outputs sent to the sink. 36 | func New() (<-chan bar.Segments, bar.Sink) { 37 | return Buffered(0) 38 | } 39 | 40 | // Buffered creates a new buffered sink. 41 | func Buffered(bufCount int) (<-chan bar.Segments, bar.Sink) { 42 | ch := make(chan bar.Segments, bufCount) 43 | return ch, Func(func(o bar.Segments) { ch <- o }) 44 | } 45 | 46 | // Null returns a sink that swallows any output sent to it. 47 | func Null() bar.Sink { 48 | ch, sink := New() 49 | go func() { 50 | for range ch { 51 | } 52 | }() 53 | return sink 54 | } 55 | 56 | // Value returns a sink that sends output to a base.Value. 57 | func Value() (*value.Value, bar.Sink) { 58 | ch, sink := New() 59 | val := new(value.Value) 60 | go func(ch <-chan bar.Segments, val *value.Value) { 61 | for o := range ch { 62 | val.Set(o) 63 | } 64 | }(ch, val) 65 | val.Set(bar.Segments(nil)) 66 | return val, sink 67 | } 68 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | mkdir -p profiles/ 19 | 20 | # Since quite a few tests have sleeps, running nproc + 2 tests should result in most 21 | # effective parallelisation. 22 | NPAR="$(($(nproc) + 2))" 23 | 24 | # Skip running go vet in cron, since the code hasn't changed. 25 | if [ "$CRON" != "true" ]; then 26 | echo "Vet: go vet" 27 | go vet 28 | fi 29 | 30 | echo "Test: Running $NPAR in parallel" 31 | # Run tests with coverage for all barista packages 32 | go list ./... \ 33 | | grep -v /samples/ \ 34 | | sed "s|_$PWD|.|" \ 35 | | tac \ 36 | | xargs -n1 -P$NPAR -IPKG sh -c \ 37 | 'for try in `seq 1 3`; do 38 | go test \ 39 | -timeout 90s \ 40 | -coverprofile=profiles/$(echo "PKG" | sed -e "s|./||" -e "s|/|_|g").out \ 41 | -covermode=atomic \ 42 | "PKG" \ 43 | && exit 0 44 | done 45 | exit 1' 46 | 47 | echo "Test: Logging with -tags baristadebuglog" 48 | # Debug log tests need the build tag, otherwise the nop versions will be used. 49 | go test -tags baristadebuglog -coverprofile=profiles/logging_real.out -covermode=atomic github.com/soumya92/barista/logging 50 | 51 | # Remove all _capi.go coverage since those will intentionally not be tested. 52 | for profile in profiles/*.out; do 53 | perl -i -ne 'print unless /_capi\.go:/' "$profile" 54 | done 55 | 56 | # Merge all code coverage reports. Doing this here means that after running 57 | # ./test.sh, 58 | # go tool cover -html=c.out 59 | # will show a coverage report instead of complaining about a bad format. 60 | grep -E '^mode: \w+$' "$(find profiles/ -name '*.out' -print -quit)" > c.out 61 | grep -hEv '^(mode: \w+)?$' profiles/*.out >> c.out 62 | rm -rf profiles/ 63 | 64 | echo "Test: Samples" 65 | # Run tests only for samples. 66 | # This is just to make sure that all samples compile. 67 | go test ./samples/... 68 | -------------------------------------------------------------------------------- /testing/capi/funcs.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require_relative 'types.rb' 16 | 17 | Argument = Struct.new(:name, :type) do 18 | def initialize(sig, idx) 19 | type, _, name = sig.rpartition ' ' 20 | if type.empty? || (name == '*') 21 | type = "#{type} #{name}".strip 22 | name = "unnamed_#{idx}" 23 | else 24 | name = "arg_#{name}" 25 | end 26 | if name[-1] == ']' 27 | name, _, count = name[0...-1].rpartition '[' 28 | super(name, CTypePrimAry.new(from_c_type(type), count)) 29 | return 30 | end 31 | super(name, from_c_type(type)) 32 | end 33 | end 34 | 35 | def parse_args(signature) 36 | in_fn_ptr = 0 37 | curr = '' 38 | args = [] 39 | signature.each_char do |c| 40 | if c == '(' 41 | in_fn_ptr += 1 42 | next 43 | end 44 | if in_fn_ptr.positive? 45 | in_fn_ptr -= 1 if c == ')' 46 | curr = "void * funcptr_#{args.size}" if in_fn_ptr.zero? 47 | next 48 | end 49 | if c == ',' 50 | args << curr unless curr.empty? 51 | curr = '' 52 | next 53 | end 54 | curr += c 55 | end 56 | args << curr 57 | args.reject { |arg| arg.strip == 'void' || arg.strip == '...' } 58 | .map.with_index { |arg, idx| Argument.new(arg, idx) } 59 | end 60 | 61 | Prototype = Struct.new(:name, :type, :args) do 62 | def initialize(name, typeref, signature) 63 | name = "_#{name}" if GO_KEYWORDS.include? name 64 | super(name, 65 | from_c_type(typeref.sub('typename:', '').sub('struct:', 'struct ')), 66 | parse_args(signature[1...-1])) 67 | end 68 | 69 | def go_func 70 | arglist = args.map { |a| "#{a.name} #{a.type.gotype}" } 71 | "#{name}(#{arglist.join(', ')}) #{type.gotype}" 72 | end 73 | 74 | def go_type 75 | argtypes = args.map { |a| a.type.gotype.to_s } 76 | "func(#{argtypes.join(', ')}) #{type.gotype}" 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /testing/cron/cron.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package cron provides a function to run a test only in Travis CI cron 17 | runs, and retry the test with increasing delays a few times before 18 | failing the build. 19 | 20 | The primary purpose of this method is to allow cron test that are 21 | non-hermetic and run against live (usually http) endpoints. Since the 22 | live endpoints could occasionally throw errors, there is built-in retry 23 | with delays between attempts. 24 | */ 25 | package cron 26 | 27 | import ( 28 | "os" 29 | "testing" 30 | "time" 31 | 32 | "github.com/stretchr/testify/require" 33 | ) 34 | 35 | var getenv = os.Getenv 36 | var waits = []int{1, 3, 7, 15} 37 | 38 | // Test runs a test if running in the CI's cron mode. It handles retries if the 39 | // test returns an error, but passes through failures to the test suite. This 40 | // allows the test function to retry by returning transient errors, while not 41 | // wasting attempts on non-retryable failures. 42 | func Test(t *testing.T, testFunc func() error) { 43 | if getenv("CRON") != "true" { 44 | t.Skipf("Skipping LiveVersion test in non-cron mode") 45 | } 46 | for _, wait := range waits { 47 | err := testFunc() 48 | if err == nil { 49 | return 50 | } 51 | t.Logf("Waiting %ds due to %v", wait, err) 52 | time.Sleep(time.Duration(wait) * time.Second) 53 | } 54 | require.NoError(t, testFunc(), "On last cron attempt") 55 | } 56 | -------------------------------------------------------------------------------- /testing/cron/cron_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cron 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "testing" 21 | "time" 22 | 23 | "github.com/soumya92/barista/testing/fail" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func init() { 29 | // Not waiting 31 seconds for this test. 30 | waits = []int{0, 1, 2, 3} 31 | } 32 | 33 | func failNTimes(n int) func() error { 34 | count := 0 35 | return func() error { 36 | count++ 37 | if count > n { 38 | return nil 39 | } 40 | return fmt.Errorf("Failing N Times: %d out of %d", count, n) 41 | } 42 | } 43 | 44 | func mockGetenv(cronEnvVar string) func(string) string { 45 | return func(key string) string { 46 | if key == "CRON" { 47 | return cronEnvVar 48 | } 49 | return os.Getenv(key) 50 | } 51 | } 52 | 53 | func TestNotCron(t *testing.T) { 54 | getenv = mockGetenv("false") 55 | Test(t, func() error { 56 | require.Fail(t, "test func called but not a cron build") 57 | return nil 58 | }) 59 | } 60 | 61 | func TestCron(t *testing.T) { 62 | getenv = mockGetenv("true") 63 | 64 | start := time.Now() 65 | fail.AssertFails(t, func(testT *testing.T) { 66 | Test(testT, failNTimes(100)) 67 | }, "More than 4 failures from test function") 68 | end := time.Now() 69 | require.WithinDuration(t, start.Add(6*time.Second), end, time.Second) 70 | 71 | start = time.Now() 72 | Test(t, failNTimes(2)) 73 | end = time.Now() 74 | require.WithinDuration(t, start.Add(1*time.Second), end, time.Second, 75 | "Test should only wait 0+1 seconds (for the 2 failures)") 76 | 77 | called := false 78 | Test(t, func() error { 79 | require.False(t, called, "test func called again after no error") 80 | called = true 81 | return nil 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /testing/fail/fail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fail provides methods to test and verify failing assertions. 16 | package fail 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | // failed returns true if the given function failed the test, 25 | // using the provided fake testing.T instead of a new one, allowing 26 | // re-use between calls. 27 | func failed(fakeT *testing.T, fn func(*testing.T)) bool { 28 | doneCh := make(chan bool) 29 | // Need a separate goroutine in case the test function calls FailNow. 30 | go func() { 31 | defer func() { doneCh <- true }() 32 | fn(fakeT) 33 | }() 34 | <-doneCh 35 | return fakeT.Failed() 36 | } 37 | 38 | // Failed returns true if the given function failed the test. 39 | func Failed(fn func(*testing.T)) bool { 40 | return failed(&testing.T{}, fn) 41 | } 42 | 43 | // AssertFails asserts that the given test function fails the test. 44 | func AssertFails(t *testing.T, fn func(*testing.T), formatAndArgs ...interface{}) { 45 | if !Failed(fn) { 46 | require.Fail(t, "Expected test to fail", formatAndArgs...) 47 | } 48 | } 49 | 50 | // TestSetup represents an already set up test environment, which provides 51 | // a variant on AssertFails that fails the test as normal, but also fails 52 | // the test if the setup function causes test failures. 53 | type TestSetup struct { 54 | setupFailed bool 55 | fakeT *testing.T 56 | } 57 | 58 | // Setup shares the fake testing.T instance between a setup method 59 | // and a test method, providing an AssertFails method that fails the test 60 | // if the setup fails, or if the test method does not. 61 | func Setup(setupFn func(*testing.T)) *TestSetup { 62 | t := &TestSetup{fakeT: &testing.T{}} 63 | t.setupFailed = failed(t.fakeT, setupFn) 64 | return t 65 | } 66 | 67 | // AssertFails asserts that the given test function fails the test, and 68 | // that the setup function used did not cause any test failures. 69 | func (s *TestSetup) AssertFails(t *testing.T, fn func(*testing.T), formatAndArgs ...interface{}) { 70 | if s.setupFailed { 71 | require.Fail(t, "Test failed in setup", formatAndArgs...) 72 | } 73 | if !failed(s.fakeT, fn) { 74 | require.Fail(t, "Expected test to fail", formatAndArgs...) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /testing/fail/fail_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fail 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | "github.com/stretchr/testify/require" 22 | ) 23 | 24 | var failures = []struct { 25 | fn func(*testing.T) 26 | desc string 27 | }{ 28 | {func(t *testing.T) { t.Fail() }, "Fail"}, 29 | {func(t *testing.T) { t.FailNow() }, "FailNow"}, 30 | {func(t *testing.T) { t.Fatal("fatal") }, "Fatal"}, 31 | {func(t *testing.T) { assert.Fail(t, "something") }, "assert.Fail"}, 32 | {func(t *testing.T) { assert.FailNow(t, "error") }, "assert.FailNow"}, 33 | {func(t *testing.T) { require.Fail(t, "required") }, "require.Fail"}, 34 | {func(t *testing.T) { assert.True(t, false) }, "assert.True"}, 35 | {func(t *testing.T) { require.Equal(t, "a", "b") }, "require.Equal"}, 36 | } 37 | 38 | var noFailures = []struct { 39 | fn func(*testing.T) 40 | desc string 41 | }{ 42 | {func(t *testing.T) {}, "Nop"}, 43 | {func(t *testing.T) { t.Log("everything is awesome") }, "Log"}, 44 | {func(t *testing.T) { require.True(t, true) }, "require.True"}, 45 | {func(t *testing.T) { assert.Equal(t, 4, 2+2) }, "assert.Equal"}, 46 | } 47 | 48 | func TestFailures(t *testing.T) { 49 | for _, f := range failures { 50 | AssertFails(t, f.fn, f.desc) 51 | } 52 | } 53 | 54 | func TestNoFailure(t *testing.T) { 55 | for _, f := range noFailures { 56 | assert.False(t, Failed(f.fn), f.desc) 57 | } 58 | } 59 | 60 | func TestAssertionWithNoFailure(t *testing.T) { 61 | for _, f := range noFailures { 62 | AssertFails(t, func(t *testing.T) { 63 | AssertFails(t, f.fn) 64 | }, f.desc) 65 | } 66 | } 67 | 68 | func TestSetupFailures(t *testing.T) { 69 | for _, f := range failures { 70 | for _, ff := range failures { 71 | AssertFails(t, func(t *testing.T) { 72 | Setup(f.fn).AssertFails(t, ff.fn, 73 | "Setup(%s).AssertFails(%s)", f.desc, ff.desc) 74 | }) 75 | } 76 | for _, nf := range noFailures { 77 | AssertFails(t, func(t *testing.T) { 78 | Setup(f.fn).AssertFails(t, nf.fn, 79 | "Setup(%s).AssertFails(%s)", f.desc, nf.desc) 80 | }) 81 | } 82 | } 83 | } 84 | 85 | func TestNoFailureWithSetup(t *testing.T) { 86 | for _, s := range noFailures { 87 | for _, nf := range noFailures { 88 | AssertFails(t, func(t *testing.T) { 89 | Setup(s.fn).AssertFails(t, nf.fn, 90 | "Setup(%s).AssertFails(%s)", s.desc, nf.desc) 91 | }) 92 | } 93 | } 94 | } 95 | 96 | func TestFailureWithSetup(t *testing.T) { 97 | for _, s := range noFailures { 98 | for _, f := range failures { 99 | Setup(s.fn).AssertFails(t, f.fn, 100 | "Setup(%s).AssertFails(%s)", s.desc, f.desc) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /testing/flakes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2018 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This script runs the given test many times, in parallel, with all logging 17 | # enabled, and prints the names of the files containing output from failures. 18 | # 19 | # This is useful for tracking down flaky tests, hence the name. 20 | 21 | # To avoid depending on a second script, use a 'magic' first argument. 22 | if [ "$1" = "BARISTA_FLAKE_TEST" ]; then 23 | # When invoked by xargs using the magic first arg, it simply executes the 24 | # given command, piping the stdout and stderr to a temporary file. 25 | # If the command exits successfully (or ^C), nothing happens, otherwise 26 | # the name of the file is printed. 27 | shift 28 | tmpdir="$1" 29 | shift 30 | outfile="$(mktemp --tmpdir="$tmpdir")" 31 | "$@" >"$outfile" 2>&1 32 | status="$?" 33 | if [ "$status" -eq 0 ] || [ "$status" -eq 127 ]; then 34 | rm -f "$outfile" 35 | else 36 | echo "$outfile" 37 | fi 38 | exit $status 39 | fi 40 | 41 | # When invoked normally, sets up a parallel pipeline using seq | xargs to 42 | # run the given test many times and catching any failures. 43 | 44 | # TODO: Use argument parsing here. 45 | parallel=16 46 | total=96 47 | tmpdir="$(mktemp -d)" 48 | 49 | echo "Saving results to $tmpdir" 50 | seq 1 $total | xargs -n 1 -P $parallel $0 BARISTA_FLAKE_TEST "$tmpdir" go test -v -race -tags debuglog "$@" -- -finelog= 51 | -------------------------------------------------------------------------------- /testing/githubfs/githubfs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package githubfs 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "testing" 21 | "time" 22 | 23 | testServer "github.com/soumya92/barista/testing/httpserver" 24 | 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | func TestName(t *testing.T) { 29 | require.Contains(t, New().Name(), "GitHubFS") 30 | } 31 | 32 | func TestFs(t *testing.T) { 33 | ts := testServer.New() 34 | defer ts.Close() 35 | root = ts.URL 36 | 37 | fs := New() 38 | 39 | _, err := fs.Open("/code/500") 40 | require.Error(t, err) 41 | _, err = fs.OpenFile("/redir", 0, 0444) 42 | require.Error(t, err) 43 | _, err = fs.Stat("/code/403") 44 | require.Error(t, err) 45 | 46 | info, err := fs.Stat("/modtime/1382140800") 47 | require.NoError(t, err) 48 | modTime := time.Date(2013, time.October, 19, 0, 0, 0, 0, time.UTC) 49 | require.WithinDuration(t, modTime, info.ModTime(), time.Minute) 50 | 51 | f, err := fs.Open("/basic/empty") 52 | require.NoError(t, err) 53 | contents, err := ioutil.ReadAll(f) 54 | require.NoError(t, err) 55 | require.Equal(t, []byte{}, contents) 56 | 57 | f, err = fs.OpenFile("/basic/foo", os.O_RDONLY, 0600) 58 | require.NoError(t, err) 59 | contents, err = ioutil.ReadAll(f) 60 | require.NoError(t, err) 61 | require.Equal(t, "bar", string(contents)) 62 | } 63 | -------------------------------------------------------------------------------- /testing/httpcache/httpcache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package httpcache 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "net/http" 21 | "net/url" 22 | "os" 23 | "testing" 24 | 25 | testServer "github.com/soumya92/barista/testing/httpserver" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestCache(t *testing.T) { 30 | server := testServer.New() 31 | defer server.Close() 32 | u, _ := url.Parse(server.URL) 33 | base := sanitizeRe.ReplaceAllString(u.Host, "-") 34 | 35 | dir, err := ioutil.TempDir("", "httpcache") 36 | if err != nil { 37 | t.Fatalf("failed to create test directory: %s", err) 38 | } 39 | defer os.RemoveAll(dir) 40 | 41 | os.Setenv("XDG_CACHE_HOME", dir) 42 | client := &http.Client{Transport: Wrap(http.DefaultTransport)} 43 | 44 | client.Get(server.URL + "/basic/foo") 45 | _, err = os.Stat(fmt.Sprintf("%s/barista/http/%s_basic-foo", dir, base)) 46 | require.NoError(t, err, "simple response cached to correct file") 47 | 48 | client.Get(server.URL + "/code/404") 49 | _, err = os.Stat(fmt.Sprintf("%s/barista/http/%s_code-404", dir, base)) 50 | require.NoError(t, err, "http non-200 responses also cached") 51 | 52 | _, redirectError := client.Get(server.URL + "/redir") 53 | 54 | r, err := client.Get(server.URL + "/tpl/debug?param=foo&bar=baz") 55 | require.NoError(t, err) 56 | body, _ := ioutil.ReadAll(r.Body) 57 | require.Equal(t, "\nbar = baz\nparam = foo\n", string(body)) 58 | 59 | server.Close() 60 | 61 | r, err = client.Get(server.URL + "/basic/foo") 62 | require.NoError(t, err, "simple response cached") 63 | body, _ = ioutil.ReadAll(r.Body) 64 | require.Equal(t, "bar", string(body), "full body cached") 65 | 66 | r, err = client.Get(server.URL + "/tpl/debug?param=other-value") 67 | require.NoError(t, err, "response cached, despite different query params") 68 | body, _ = ioutil.ReadAll(r.Body) 69 | require.Equal(t, "\nbar = baz\nparam = foo\n", string(body), 70 | "body cached, not affected by query parameters") 71 | 72 | _, err = client.Get(server.URL + "/redir") 73 | require.Equal(t, redirectError, err, "redirects also cached") 74 | 75 | r, err = client.Get(server.URL + "/code/404") 76 | require.NoError(t, err, "server stopped, response from cache") 77 | require.Equal(t, r.StatusCode, 404, "status code also cached") 78 | 79 | _, err = client.Get(server.URL + "/code/204") 80 | require.Error(t, err, "no successful cached response, server stopped") 81 | 82 | _, err = os.Stat(fmt.Sprintf("%s/barista/http/%s_code-204", dir, base)) 83 | require.True(t, os.IsNotExist(err), "transport error not cached") 84 | 85 | } 86 | -------------------------------------------------------------------------------- /testing/httpcache/testdata/debug.tpl: -------------------------------------------------------------------------------- 1 | {{range $key, $value := .}} 2 | {{$key}} = {{$value}}{{end}} 3 | -------------------------------------------------------------------------------- /testing/httpclient/httpclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package httpclient provides a testable wrapper around an existing *http.Client. 16 | package httpclient 17 | 18 | import ( 19 | "net/http" 20 | "net/url" 21 | 22 | l "github.com/soumya92/barista/logging" 23 | 24 | "golang.org/x/oauth2" 25 | ) 26 | 27 | type rewritingTransport struct { 28 | newURL *url.URL 29 | transport http.RoundTripper 30 | } 31 | 32 | func (r rewritingTransport) RoundTrip(req *http.Request) (*http.Response, error) { 33 | newReq := &http.Request{} 34 | *newReq = *req 35 | newReq.URL = &url.URL{} 36 | *newReq.URL = *req.URL 37 | newReq.URL.Scheme = r.newURL.Scheme 38 | newReq.URL.Host = r.newURL.Host 39 | return r.transport.RoundTrip(newReq) 40 | } 41 | 42 | // Wrap redirects all calls from the original *http.Client to the given host. 43 | // Typical usage would be httpclient.Wrap(client, server.URL), where server 44 | // is a httptest.Server or equivalent. 45 | func Wrap(client *http.Client, newURL string) { 46 | u, _ := url.Parse(newURL) 47 | client.Transport = rewritingTransport{ 48 | newURL: u, 49 | transport: client.Transport, 50 | } 51 | } 52 | 53 | // FreezeOauthToken sets the client's token source to a static token source that 54 | // always provides the given access token. 55 | func FreezeOauthToken(client *http.Client, accessToken string) { 56 | if t, ok := client.Transport.(*oauth2.Transport); ok { 57 | t.Source = oauth2.StaticTokenSource( 58 | &oauth2.Token{AccessToken: accessToken}) 59 | } else { 60 | l.Log("Client %v does not use an oauth transport (%T)", 61 | client, client.Transport) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /testing/notifier/notifier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package notifier provides assertions that notifier channels (<-chan struct{}) 16 | // received or did not receive a signal. 17 | package notifier 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | var positiveTimeout = 10 * time.Millisecond 27 | var negativeTimeout = 10 * time.Second 28 | 29 | // AssertNotified asserts that the given channel received a notification. 30 | func AssertNotified(t *testing.T, ch <-chan struct{}, formatAndArgs ...interface{}) { 31 | select { 32 | case _, ok := <-ch: 33 | if !ok { 34 | require.Fail(t, "Expected notification but channel was closed", formatAndArgs...) 35 | } 36 | case <-time.After(negativeTimeout): 37 | require.Fail(t, "Expected notification not received", formatAndArgs...) 38 | } 39 | } 40 | 41 | // AssertClosed asserts that the given channel was closed. 42 | func AssertClosed(t *testing.T, ch <-chan struct{}, formatAndArgs ...interface{}) { 43 | select { 44 | case _, ok := <-ch: 45 | if ok { 46 | require.Fail(t, "Expected channel close, received notification", formatAndArgs...) 47 | } 48 | case <-time.After(negativeTimeout): 49 | require.Fail(t, "Channel not closed when expected", formatAndArgs...) 50 | } 51 | } 52 | 53 | // AssertNoUpdate asserts that the given channel was not notified or closed. 54 | func AssertNoUpdate(t *testing.T, ch <-chan struct{}, formatAndArgs ...interface{}) { 55 | select { 56 | case <-ch: 57 | require.Fail(t, "Unexpected notification", formatAndArgs...) 58 | case <-time.After(positiveTimeout): 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /testing/notifier/notifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package notifier 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/soumya92/barista/base/notifier" 22 | "github.com/soumya92/barista/testing/fail" 23 | ) 24 | 25 | func TestAssertions(t *testing.T) { 26 | fn, ch := notifier.New() 27 | 28 | AssertNoUpdate(t, ch, "no updates sent on channel") 29 | fn() 30 | AssertNotified(t, ch, "after update sent on channel") 31 | 32 | sendCh := make(chan struct{}) 33 | ch = sendCh 34 | AssertNoUpdate(t, ch, "on open channel") 35 | close(sendCh) 36 | AssertClosed(t, ch, "after close") 37 | } 38 | 39 | func TestAssertionFailures(t *testing.T) { 40 | defer func(prev time.Duration) { negativeTimeout = prev }(negativeTimeout) 41 | negativeTimeout = 10 * time.Millisecond 42 | 43 | fn, ch := notifier.New() 44 | fail.AssertFails(t, func(t *testing.T) { 45 | AssertNotified(t, ch) 46 | }, "AssertNotified with no updates") 47 | 48 | fail.AssertFails(t, func(t *testing.T) { 49 | AssertClosed(t, ch) 50 | }, "AssertClosed with no updates") 51 | 52 | fn() 53 | fail.AssertFails(t, func(t *testing.T) { 54 | AssertClosed(t, ch) 55 | }, "AssertClosed with non-closing update") 56 | 57 | fn, ch = notifier.New() 58 | fn() 59 | 60 | fail.AssertFails(t, func(t *testing.T) { 61 | AssertNoUpdate(t, ch) 62 | }, "AssertNoUpdate with update") 63 | 64 | sendCh := make(chan struct{}) 65 | ch = sendCh 66 | close(sendCh) 67 | 68 | fail.AssertFails(t, func(t *testing.T) { 69 | AssertNotified(t, ch) 70 | }, "AssertNotified after close") 71 | 72 | fail.AssertFails(t, func(t *testing.T) { 73 | AssertNoUpdate(t, ch) 74 | }, "AssertNoUpdate after close") 75 | } 76 | -------------------------------------------------------------------------------- /testing/pango/pango.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package pango provides provides a method to test markup equality. 17 | It compares to strings that represent pango markup while ignoring 18 | differences in attribute order, escaping, etc. 19 | */ 20 | package pango 21 | 22 | import ( 23 | "fmt" 24 | "strings" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/require" 28 | "golang.org/x/net/html" 29 | ) 30 | 31 | // AssertEqual asserts that the given strings represent equivalent pango markup, 32 | // i.e. the result of their rendering will be the same. 33 | func AssertEqual(t *testing.T, expected, actual string, args ...interface{}) { 34 | expectedR, err := html.Parse(strings.NewReader(expected)) 35 | require.NoError(t, err, args...) 36 | actualR, err := html.Parse(strings.NewReader(actual)) 37 | require.NoError(t, err, args...) 38 | if !equalMarkup(expectedR, actualR) { 39 | require.Fail(t, fmt.Sprintf("%s !~= %s", expected, actual), args...) 40 | } 41 | } 42 | 43 | // AssertText asserts that the markup string has the expected text content. 44 | // Text content ignores any tags and attributes, using only rendered text. 45 | func AssertText(t *testing.T, expected string, markup string, args ...interface{}) { 46 | markupR, err := html.Parse(strings.NewReader(markup)) 47 | require.NoError(t, err, args...) 48 | require.Equal(t, expected, textOf(markupR), args...) 49 | } 50 | 51 | func equalMarkup(a, b *html.Node) bool { 52 | if a == nil && b == nil { 53 | return true 54 | } 55 | if a == nil || b == nil { 56 | return false 57 | } 58 | if a.Data != b.Data { 59 | return false 60 | } 61 | if len(a.Attr) != len(b.Attr) { 62 | return false 63 | } 64 | aAttrMap := map[string]string{} 65 | for _, aAttr := range a.Attr { 66 | aAttrMap[aAttr.Key] = aAttr.Val 67 | } 68 | for _, bAttr := range b.Attr { 69 | if aAttrMap[bAttr.Key] != bAttr.Val { 70 | return false 71 | } 72 | } 73 | if !equalMarkup(a.NextSibling, b.NextSibling) { 74 | return false 75 | } 76 | if !equalMarkup(a.FirstChild, b.FirstChild) { 77 | return false 78 | } 79 | return true 80 | } 81 | 82 | func textOf(n *html.Node) (text string) { 83 | if n == nil { 84 | return text 85 | } 86 | if n.Type == html.TextNode { 87 | text += n.Data 88 | } 89 | text += textOf(n.FirstChild) 90 | text += textOf(n.NextSibling) 91 | return text 92 | } 93 | -------------------------------------------------------------------------------- /timing/internal/timerfd/nativeendian.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package timerfd 16 | 17 | import ( 18 | "encoding/binary" 19 | "unsafe" 20 | ) 21 | 22 | var nativeEndian binary.ByteOrder 23 | 24 | func init() { 25 | i := uint32(1) 26 | b := (*[4]byte)(unsafe.Pointer(&i)) 27 | if b[0] == 1 { 28 | nativeEndian = binary.LittleEndian 29 | } else { 30 | nativeEndian = binary.BigEndian 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /timing/internal/timerfd/timerfd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build linux 16 | 17 | package timerfd 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | 24 | "golang.org/x/sys/unix" 25 | ) 26 | 27 | // ErrTimerfdCancelled is the error returned when timer is cancelled due to 28 | // discontinuous clock change. See man timerfd_settime for details. 29 | var ErrTimerfdCancelled = errors.New("Timerfd was cancelled due to discontinuous change") 30 | 31 | // Timerfd provides higher-level abstraction for Linux-specific timerfd timers. 32 | type Timerfd struct { 33 | fd *os.File 34 | clockid int 35 | } 36 | 37 | func newTimerfd(clockid int) (*Timerfd, error) { 38 | fd, err := unix.TimerfdCreate(clockid, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC) 39 | if err != nil { 40 | return nil, err 41 | } 42 | f := os.NewFile(uintptr(fd), "Timerfd") 43 | return &Timerfd{fd: f, clockid: clockid}, nil 44 | } 45 | 46 | // Settime arms or disarms the timer. See man timerfd_settime for details. 47 | func (t *Timerfd) Settime(newValue *unix.ItimerSpec, oldValue *unix.ItimerSpec, absolute bool, cancelOnSet bool) error { 48 | rawConn, err := t.fd.SyscallConn() 49 | if err != nil { 50 | return err 51 | } 52 | var err2 error 53 | err = rawConn.Control(func(fd uintptr) { 54 | var flags int 55 | if absolute { 56 | flags |= unix.TFD_TIMER_ABSTIME 57 | } 58 | if cancelOnSet { 59 | flags |= unix.TFD_TIMER_CANCEL_ON_SET 60 | } 61 | err2 = unix.TimerfdSettime(int(fd), flags, newValue, oldValue) 62 | }) 63 | if err != nil { 64 | return err 65 | } 66 | return err2 67 | } 68 | 69 | // Wait waits until timer expiration. See man timerfd_create for details. 70 | func (t *Timerfd) Wait() (expirations uint64, err error) { 71 | var buf [8]byte 72 | 73 | n, err := t.fd.Read(buf[:]) 74 | if err != nil { 75 | if pe, ok := err.(*os.PathError); ok { 76 | err = pe.Err 77 | } 78 | if err == unix.ECANCELED { 79 | err = ErrTimerfdCancelled 80 | } 81 | return 0, err 82 | } 83 | if n != 8 { 84 | panic(fmt.Sprintf("Timerfd returned %d bytes (expected 8)", n)) 85 | } 86 | return nativeEndian.Uint64(buf[:]), nil 87 | } 88 | 89 | // GetClockid returns the clock id that this timerfd uses. 90 | func (t *Timerfd) GetClockid() int { 91 | return t.clockid 92 | } 93 | 94 | // Close closes the underlying timerfd descriptor. 95 | func (t *Timerfd) Close() error { 96 | return t.fd.Close() 97 | } 98 | 99 | // NewRealtimeTimerfd returns a Timerfd backed by CLOCK_REALTIME. 100 | func NewRealtimeTimerfd() (*Timerfd, error) { 101 | return newTimerfd(unix.CLOCK_REALTIME) 102 | } 103 | -------------------------------------------------------------------------------- /timing/misc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package timing 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | // nextAlignedExpiration calculates the next expiration time. 22 | // The expiration is first rounded to interval granularity, and then offset is added. 23 | // 24 | // For example, given interval 1h, and offset 15m, the expirations will happen at 25 | // :15 of every hour regardless of the initial time. 26 | func nextAlignedExpiration(initial time.Time, interval time.Duration, offset time.Duration) time.Time { 27 | next := initial.Truncate(interval).Add(offset) 28 | if !next.After(initial) { 29 | next = next.Add(interval) 30 | } 31 | if !next.After(initial) { 32 | panic("nextAlignedExpiration: bug: !next.After(initial)") 33 | } 34 | return next 35 | } 36 | -------------------------------------------------------------------------------- /timing/time_scheduler.go: -------------------------------------------------------------------------------- 1 | package timing 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var _ schedulerImpl = &timeScheduler{} 9 | 10 | // timeScheduler is a scheduler backed by "time" package. 11 | type timeScheduler struct { 12 | mu sync.Mutex 13 | timer *time.Timer 14 | ticker *time.Ticker 15 | quitter chan struct{} 16 | } 17 | 18 | // NewScheduler creates a new scheduler. 19 | // 20 | // The scheduler is backed by "time" package. Its "At" implementation 21 | // is unreliable, as it's unable to take system suspend and time adjustments 22 | // into account. 23 | func NewScheduler() *Scheduler { 24 | if testModeScheduler := maybeNewTestModeScheduler(); testModeScheduler != nil { 25 | return newScheduler(testModeScheduler) 26 | } 27 | return newScheduler(&timeScheduler{}) 28 | } 29 | 30 | // At implements the schedulerImpl interface. 31 | func (s *timeScheduler) At(when time.Time, f func()) { 32 | s.mu.Lock() 33 | defer s.mu.Unlock() 34 | s.stop() 35 | s.timer = time.AfterFunc(when.Sub(Now()), f) 36 | } 37 | 38 | // After implements the schedulerImpl interface. 39 | func (s *timeScheduler) After(delay time.Duration, f func()) { 40 | s.mu.Lock() 41 | defer s.mu.Unlock() 42 | s.stop() 43 | s.timer = time.AfterFunc(delay, f) 44 | } 45 | 46 | // Every implements the schedulerImpl interface. 47 | func (s *timeScheduler) Every(interval time.Duration, f func()) { 48 | s.mu.Lock() 49 | defer s.mu.Unlock() 50 | s.stop() 51 | s.quitter = make(chan struct{}) 52 | s.ticker = time.NewTicker(interval) 53 | go func() { 54 | s.mu.Lock() 55 | ticker := s.ticker 56 | quitter := s.quitter 57 | s.mu.Unlock() 58 | if ticker == nil || quitter == nil { 59 | // Scheduler stopped before goroutine was started. 60 | return 61 | } 62 | for { 63 | select { 64 | case <-ticker.C: 65 | f() 66 | case <-quitter: 67 | return 68 | } 69 | } 70 | }() 71 | } 72 | 73 | // EveryAlign implements the schedulerImpl interface. 74 | func (s *timeScheduler) EveryAlign(interval time.Duration, offset time.Duration, f func()) { 75 | s.mu.Lock() 76 | defer s.mu.Unlock() 77 | s.stop() 78 | quitter := make(chan struct{}) 79 | s.quitter = quitter 80 | go func() { 81 | var timer *time.Timer 82 | for { 83 | now := time.Now() 84 | next := nextAlignedExpiration(now, interval, offset) 85 | delay := next.Sub(now) 86 | if timer == nil { 87 | timer = time.NewTimer(delay) 88 | defer timer.Stop() 89 | } else { 90 | timer.Reset(delay) 91 | } 92 | select { 93 | case <-timer.C: 94 | f() 95 | case <-quitter: 96 | return 97 | } 98 | } 99 | }() 100 | } 101 | 102 | // Stop implements the schedulerImpl interface. 103 | func (s *timeScheduler) Stop() { 104 | s.mu.Lock() 105 | defer s.mu.Unlock() 106 | s.stop() 107 | } 108 | 109 | // Close implements the schedulerImpl interface. 110 | func (s *timeScheduler) Close() { 111 | s.Stop() 112 | } 113 | 114 | func (s *timeScheduler) stop() { 115 | if s.timer != nil { 116 | s.timer.Stop() 117 | s.timer = nil 118 | } 119 | if s.ticker != nil { 120 | s.ticker.Stop() 121 | s.ticker = nil 122 | } 123 | if s.quitter != nil { 124 | close(s.quitter) 125 | s.quitter = nil 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /timing/time_scheduler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package timing 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestTimeSchedulerImpl(t *testing.T) { 22 | testSchedulerImplementation(t, NewScheduler) 23 | } 24 | -------------------------------------------------------------------------------- /timing/timerfd_scheduler_fallback.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !linux 16 | 17 | package timing 18 | 19 | // NewRealtimeScheduler creates a scheduler backed by system real-time clock. 20 | // 21 | // It properly handles system suspend (sleep mode) and time adjustments. For periodic timers, 22 | // it triggers immediately whenever time changes discontinuously. For one-shot timers 23 | // (At and After), it will fire immediately if the time is skipped over 24 | // the set trigger time, and will properly wait for it otherwise. 25 | // 26 | // This scheduler is only properly supported on Linux. On other systems, 27 | // plain scheduler based on "time" package is returned. 28 | // 29 | // In order to clean up resources associated with it, 30 | // remember to call Stop(). 31 | func NewRealtimeScheduler() (*Scheduler, error) { 32 | return NewScheduler(), nil 33 | } 34 | -------------------------------------------------------------------------------- /timing/timerfd_scheduler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build linux 16 | 17 | package timing 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestRealtimeTimerfdSchedulerImpl(t *testing.T) { 24 | create := func() *Scheduler { 25 | s, err := NewRealtimeScheduler() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | return s 30 | } 31 | testSchedulerImplementation(t, create) 32 | } 33 | -------------------------------------------------------------------------------- /timing/timing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* 16 | Package timing provides a testable interface for timing and scheduling. 17 | 18 | This makes it simple to update a module at a fixed interval or 19 | at a fixed point in time. 20 | 21 | Typically, modules will make a scheduler: 22 | mod.sch = timing.NewScheduler() 23 | and use the scheduling calls to control the update timing: 24 | mod.sch.Every(time.Second) 25 | 26 | The Stream() goroutine will then loop over the ticker, and update 27 | the module with fresh information: 28 | for range mod.sch.C { 29 | // update code. 30 | } 31 | 32 | This will automatically suspend processing when the bar is hidden. 33 | 34 | Modules should also use timing.Now() instead of time.Now() to control time 35 | during tests, as well as correctly track the machine's time zone. 36 | */ 37 | package timing 38 | 39 | import ( 40 | "time" 41 | 42 | "github.com/soumya92/barista/base/watchers/localtz" 43 | ) 44 | 45 | // Now returns the current time, in the machine's local time zone. 46 | // 47 | // IMPORTANT: This function differs from time.Now() in that the time zone can 48 | // change between invocations. Use timing.Now().Local() to get a consistent 49 | // zone, however note that the zone will be the machine's zone at the start 50 | // of the bar and may be out of date. 51 | func Now() time.Time { 52 | mu.Lock() 53 | defer mu.Unlock() 54 | var now time.Time 55 | if testMode { 56 | now = testNow() 57 | } else { 58 | now = time.Now() 59 | } 60 | return now.In(localtz.Get()) 61 | } 62 | --------------------------------------------------------------------------------