~rkintzi/statusbar

cac14109c4744761a215a2db771a6b3fbc8255be — RadosÅ‚aw Kintzi 9 months ago 484591d
Simplify widgets implementation and registration
M README.md => README.md +33 -29
@@ 242,27 242,20 @@ import (

type StaticText struct {
	Text string `yaml:"textToDisplay"`
	statusbar.BaseWidget
}

func (w *StaticText) WidgetName() string    { return "StaticText" }
func (w *StaticText) New() statusbar.Widget { return new(StaticText) }

func (w *StaticText) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
	defer done()
	update([]statusbar.Block{
		{
			FullText: w.Text,
			Name:     "static",
		},
	})
func (w StaticText) Run(ctx context.Context, update statusbar.UpdateFunc) {
	b := w.Block()
	b.FullText = w.Text
	b.Name = "static"
	update([]statusbar.Block{b})
	<-ctx.Done()
}

func main() {
	cfg := statusbar.Configuration{}
	cfg.RegisterWidget(new(StaticText))
	err := cfg.Run(nil)
	abortOnErr(err)
	statusbar.RegisterWidget[StaticText]("StaticText")
	abortOnErr(statusbar.Run(nil))
}

func abortOnErr(err error) {


@@ 287,26 280,37 @@ forget to update the swaybar configuration (as described in the

### Additional notes

1. Widgets must implement the Widget interface as defined in the statusbar
   library. This means that they must provide three methods, as illustrated in
   the example above.
1. Widgets must implement the Widget interface as defined by the statusbar
   library. This means that they must provide the Run method, as illustrated
   in the example above.

2. Widgets should be structs with a pointer receiver. This is required by the
   configuration parser.
### Additional notes

1. Widgets must implement the Widget interface as defined by the statusbar
   library. This means that they must provide the Run method, as shown in the
   example above.

3. The Run method is executed by a status bar in a dedicated goroutine. It is
   responsible for updating the contents of the status bar blocks. In the
   provided example, the update function is called only once, but usually it
2. Widgets should be structs with a pointer receiver. They should also embed
   the `BaseWidget` struct, as demonstrated above. This allows them to accept
   default configurations and be registered using the
   `statusbar.RegisterWidget` function.

3. The `Run` method is executed by a status bar in a dedicated goroutine. It
   is responsible for updating the contents of the status bar blocks. In the
   provided example, the `update` function is called only once, but usually it
   should be called within the loop whenever a status change is detected. 

4. In general, the Run method should continue running until the context is
4. The `Block` method provided by the `BaseWidget` returns the block that has
   been preinitialized with common configuration options.

5. In general, the Run method should continue running until the context is
   canceled. However, it may exit earlier if necessary. In such cases, the
   status bar will preserve the last version of the widget's blocks. In the
   provided example, the `<-ctx.Done()` statement could be omitted.
   status bar will preserve the last version of the widget's blocks. To remove
   the widget's blocks from the status bar, you can pass `nil` to the `update`
   function. 

5. The Run method must call the done function just before it returns. To
   achieve this, it is suggested to use the `defer done()` statement as the first
   line of code in the method.
6. Note that in the provided example, the `<-ctx.Done()` statement could be
   omitted.

## Contributions


M configuration.go => configuration.go +13 -26
@@ 10,19 10,16 @@ import (
	"path/filepath"

	"github.com/adrg/xdg"
	"gobytes.dev/statusbar/internal"
	"gopkg.in/yaml.v3"
)

var DefaultConfigFile = filepath.Join(xdg.ConfigHome, "statusbar", "config.yaml")

type Constructor interface {
	WidgetName() string
	New() Widget
}
var globalConfiguration = Configuration{}

type Configuration struct {
	cons    map[string]Constructor
	cons    map[string]constructor
	common  BaseWidget
	widgets []Widget
	logger  *slog.Logger
	nodef   bool


@@ 30,13 27,6 @@ type Configuration struct {

func (c *Configuration) NoBuiltinWidgets() { c.nodef = true }

func (c *Configuration) RegisterWidget(cons Constructor) {
	if c.widgets == nil {
		c.cons = make(map[string]Constructor)
	}
	c.cons[cons.WidgetName()] = cons
}

func (c *Configuration) SetLogger(l *slog.Logger) { c.logger = l }

func (c *Configuration) ParseFile(filename string) error {


@@ 101,6 91,12 @@ func (c *Configuration) UnmarshalYAML(n *yaml.Node) error {
	return nil
}

func Run(args []string) error     { return globalConfiguration.Run(args) }
func NoBuiltinWidgets()           { globalConfiguration.NoBuiltinWidgets() }
func SetLogger(l *slog.Logger)    { globalConfiguration.SetLogger(l) }
func Widgets() []Widget           { return globalConfiguration.Widgets() }
func GlobalStatusBar() *StatusBar { return globalConfiguration.StatusBar() }

func (c *Configuration) parseWidgetConfig(n *yaml.Node) error {
	rec := struct {
		Widget string `yaml:"widget"`


@@ 112,7 108,7 @@ func (c *Configuration) parseWidgetConfig(n *yaml.Node) error {
	if cons, ok := c.cons[rec.Widget]; !ok {
		return fmt.Errorf("unknown widget: %s", rec.Widget)
	} else {
		widget := cons.New()
		widget := cons.Construct(c.common)
		err := n.Decode(widget)
		if err != nil {
			return err


@@ 127,20 123,11 @@ func (c *Configuration) builtinWidgets() {
		return
	}
	if c.cons == nil {
		c.cons = make(map[string]Constructor)
		c.cons = make(map[string]constructor)
	}
	for i := range builtinWidgets {
		name := builtinWidgets[i].WidgetName()
	for name, constr := range registry {
		if _, ok := c.cons[name]; !ok {
			c.cons[name] = builtinWidgets[i]
			c.cons[name] = constr
		}
	}
}

var builtinWidgets = []Constructor{}

func init() {
	internal.Register = func(w any) {
		builtinWidgets = append(builtinWidgets, w.(Constructor))
	}
}

M configuration_test.go => configuration_test.go +1 -1
@@ 20,7 20,7 @@ func TestConfiguration(t *testing.T) {
  dateFormat: date-format
  timeFormat: time-format
`
	c := statusbar.Configuration{}
	c := statusbar.Registry{}
	err := c.Parse(bytes.NewBufferString(config))
	requireNoErr(t, err)
	expWidgets := []statusbar.Widget{

A internal/mark.go => internal/mark.go +5 -0
@@ 0,0 1,5 @@
package internal

type Mark struct{}

var M = Mark{}

D internal/registry.go => internal/registry.go +0 -3
@@ 1,3 0,0 @@
package internal

var Register func(any)

D internal/registry/reg.go => internal/registry/reg.go +0 -10
@@ 1,10 0,0 @@
package registry

import (
	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal"
)

func RegisterWidget(w statusbar.Constructor) {
	internal.Register(w)
}

A registry.go => registry.go +42 -0
@@ 0,0 1,42 @@
package statusbar

import "gobytes.dev/statusbar/internal"

type widget[T any] interface {
	*T
	setDefault(c BaseWidget)
	Widget
}

func construct[T any, pT widget[T]](c BaseWidget) Widget {
	var w pT = new(T)
	w.setDefault(c)
	return w
}

type constructor interface {
	Construct(c BaseWidget) Widget
}

type constrFunc func(c BaseWidget) Widget

func (co constrFunc) Construct(c BaseWidget) Widget {
	return co(c)
}

func RegisterWidgetIn[T any, pT widget[T]](c *Configuration, name string) {
	if c.cons == nil {
		c.cons = make(map[string]constructor)
	}
	c.cons[name] = constrFunc(construct[T, pT])
}

var registry = make(map[string]constructor)

func RegisterWidget[T any, pT widget[T]](name string) {
	RegisterWidgetIn[T, pT](&globalConfiguration, name)
}

func RegisterInternalWidget[T any, pT widget[T]](m internal.Mark, name string) {
	registry[name] = constrFunc(construct[T, pT])
}

M statusbar.go => statusbar.go +9 -8
@@ 17,7 17,7 @@ import (
type UpdateFunc func([]Block)

type Widget interface {
	Run(ctx context.Context, fn UpdateFunc, done func())
	Run(ctx context.Context, fn UpdateFunc)
}

type EventHandler interface {


@@ 28,7 28,7 @@ type StatusBar struct {
	LogFile      string
	debug        bool
	errf         io.WriteCloser
	widgets      []*widget
	widgets      []*updater
	index        []int
	blocks       []Block
	headerEmited bool


@@ 41,13 41,13 @@ type StatusBar struct {

func New(ws ...Widget) *StatusBar {
	b := &StatusBar{
		widgets: make([]*widget, len(ws)),
		widgets: make([]*updater, len(ws)),
		index:   make([]int, len(ws)),
		sigs:    make(chan os.Signal, 3),
		enc:     json.NewEncoder(os.Stdout),
	}
	for i, w := range ws {
		widget := &widget{instance: i, widget: w, bar: b}
		widget := &updater{instance: i, widget: w, bar: b}
		b.widgets[i] = widget
		b.index[i] = 0
	}


@@ 259,16 259,17 @@ func (b *StatusBar) signals(ctx context.Context, done func()) {
	}
}

type widget struct {
type updater struct {
	instance int
	widget   Widget
	bar      *StatusBar
}

func (w *widget) update(blocks []Block) {
func (w *updater) update(blocks []Block) {
	w.bar.update(w.instance, blocks)
}

func (w *widget) run(ctx context.Context, done func()) {
	w.widget.Run(ctx, w.update, done)
func (w *updater) run(ctx context.Context, done func()) {
	w.widget.Run(ctx, w.update)
	done()
}

M swaybarproto.go => swaybarproto.go +2 -2
@@ 21,8 21,8 @@ type Block struct {
	Align               string `json:"align,omitempty"`
	Name                string `json:"name,omitempty"`
	Instance            string `json:"instance,omitempty"`
	Separator           bool   `json:"separator,omitempty"`
	SeparatorBlockWidth int    `json:"separator_block_width,omitempty"`
	Separator           *bool  `json:"separator,omitempty"`
	SeparatorBlockWidth *int   `json:"separator_block_width,omitempty"`
	Markup              string `json:"markup,omitempty"`
}


A widget.go => widget.go +36 -0
@@ 0,0 1,36 @@
package statusbar

type BaseWidget struct {
	Border              string `yaml:"border"`
	BorderTop           int    `yaml:"borderTop"`
	BorterBottom        int    `yaml:"borderBottom"`
	BorderLeft          int    `yaml:"borderLeft"`
	BorderRight         int    `yaml:"borderRight"`
	MinWidth            int    `yaml:"minWidth"`
	Separator           *bool  `yaml:"separator"`
	SeparatorBlockWidth *int   `yaml:"separatorBlockWidth"`
}

func (p *BaseWidget) setDefault(c BaseWidget) {
	p.Border = c.Border
	p.BorderTop = c.BorderTop
	p.BorterBottom = c.BorterBottom
	p.BorderLeft = c.BorderLeft
	p.BorderRight = c.BorderRight
	p.MinWidth = c.MinWidth
	p.Separator = c.Separator
	p.SeparatorBlockWidth = c.SeparatorBlockWidth
}

func (p *BaseWidget) Block() Block {
	b := Block{}
	b.Border = p.Border
	b.BorderTop = p.BorderTop
	b.BorterBottom = p.BorterBottom
	b.BorderLeft = p.BorderLeft
	b.BorderRight = p.BorderRight
	b.MinWidth = p.MinWidth
	b.Separator = p.Separator
	b.SeparatorBlockWidth = p.SeparatorBlockWidth
	return b
}

M widgets/base.go => widgets/base.go +4 -0
@@ 3,11 3,15 @@ package widgets
import (
	"fmt"
	"log/slog"

	"gobytes.dev/statusbar"
)

type baseWidget struct {
	logger *slog.Logger
	prefix string

	statusbar.BaseWidget `yaml:",inline"`
}

func (p *baseWidget) format(format string, args ...any) string {

M widgets/batmon.go => widgets/batmon.go +5 -9
@@ 13,7 13,7 @@ import (

	"github.com/godbus/dbus/v5"
	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
)

type notification struct {


@@ 64,15 64,11 @@ type BatteryMonitor struct {
	chargingIcons    []rune
	dischargingIcons []rune
	idx              []int
	baseWidget
	baseWidget       `yaml:",inline"`
}

func (w *BatteryMonitor) WidgetName() string    { return "BatteryMonitor" }
func (w *BatteryMonitor) New() statusbar.Widget { return new(BatteryMonitor) }

func (w *BatteryMonitor) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
	defer done()
	w.baseWidget.prefix = "BatteryMonitor"
func (w *BatteryMonitor) Run(ctx context.Context, update statusbar.UpdateFunc) {
	w.prefix = "BatteryMonitor"
	w.init()
	w.emitStatus(update)
	t := time.NewTicker(1 * time.Second)


@@ 254,5 250,5 @@ func (w *BatteryMonitor) notify() {
}

func init() {
	registry.RegisterWidget(new(BatteryMonitor))
	statusbar.RegisterInternalWidget[BatteryMonitor](internal.M, "BatteryMonitor")
}

M widgets/batmon_test.go => widgets/batmon_test.go +10 -0
@@ 3,6 3,8 @@ package widgets
import (
	"strconv"
	"testing"

	"gopkg.in/yaml.v3"
)

func TestBatteryMonitor(t *testing.T) {


@@ 383,3 385,11 @@ func TestBatteryMonitor(t *testing.T) {
		})
	}
}

func TestBatteryMonitorConfig(t *testing.T) {
	cfg := "separator: true\nborderTop: 10"
	w := BatteryMonitor{}
	yaml.Unmarshal([]byte(cfg), &w)
	o := opts{}
	o.assertEqual(t, w.BorderTop, 10)
}

M widgets/date.go => widgets/date.go +5 -7
@@ 5,20 5,18 @@ import (
	"time"

	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
)

type DateTime struct {
	Format     string `yaml:"format"`
	TimeFormat string `yaml:"timeFormat"`
	DateFormat string `yaml:"dateFormat"`
}

func (w *DateTime) WidgetName() string    { return "DateTime" }
func (w *DateTime) New() statusbar.Widget { return new(DateTime) }
	baseWidget
}

func (w *DateTime) Run(ctx context.Context, fn statusbar.UpdateFunc, done func()) {
	defer done()
func (w *DateTime) Run(ctx context.Context, fn statusbar.UpdateFunc) {
	if w.Format == "" && w.DateFormat == "" && w.TimeFormat == "" {
		w.Format = "2006-01-02 15:04:05"
	}


@@ 68,5 66,5 @@ func (w *DateTime) publish(update statusbar.UpdateFunc, now time.Time) {
}

func init() {
	registry.RegisterWidget(new(DateTime))
	statusbar.RegisterInternalWidget[DateTime](internal.M, "DateTime")
}

M widgets/external.go => widgets/external.go +3 -7
@@ 14,7 14,7 @@ import (
	"syscall"

	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
)

type ExternalProgram struct {


@@ 35,11 35,7 @@ type ExternalProgram struct {
	baseWidget
}

func (p *ExternalProgram) WidgetName() string    { return "ExternalProgram" }
func (w *ExternalProgram) New() statusbar.Widget { return new(ExternalProgram) }

func (p *ExternalProgram) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
	defer done()
func (p *ExternalProgram) Run(ctx context.Context, update statusbar.UpdateFunc) {
	p.baseWidget.prefix = fmt.Sprintf("external %s", p.Command)
	p.sigs = make(chan os.Signal, 2)
	var err error


@@ 169,5 165,5 @@ func (p *ExternalProgram) signals(ctx context.Context, done func()) {
}

func init() {
	registry.RegisterWidget(new(ExternalProgram))
	statusbar.RegisterInternalWidget[ExternalProgram](internal.M, "ExternalProgram")
}

D widgets/init.go => widgets/init.go +0 -1
@@ 1,1 0,0 @@
package widgets

M widgets/sysmon.go => widgets/sysmon.go +14 -20
@@ 8,20 8,19 @@ import (
	"github.com/shirou/gopsutil/v3/load"
	"github.com/shirou/gopsutil/v3/mem"
	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
)

type SystemMonitor struct {
	Format string `yaml:"format"`

	format Format
	update statusbar.UpdateFunc
	idx    []int
	baseWidget
	format     Format
	update     statusbar.UpdateFunc
	idx        []int
	baseWidget `yaml:",inline"`
}

func (w *SystemMonitor) Run(ctx context.Context, b statusbar.UpdateFunc, done func()) {
	defer done()
func (w *SystemMonitor) Run(ctx context.Context, b statusbar.UpdateFunc) {
	w.prefix = "SysMon"
	w.update = b
	if w.Format == "" {


@@ 40,9 39,6 @@ func (w *SystemMonitor) Run(ctx context.Context, b statusbar.UpdateFunc, done fu
	}
}

func (w *SystemMonitor) WidgetName() string    { return "SystemMonitor" }
func (w *SystemMonitor) New() statusbar.Widget { return new(SystemMonitor) }

func (w *SystemMonitor) emitStatus(ctx context.Context) {
	cpu, err := cpu.PercentWithContext(ctx, 0, false)
	if err != nil {


@@ 60,17 56,15 @@ func (w *SystemMonitor) emitStatus(ctx context.Context) {
		return
	}
	status := w.format.Format(cpu[0], mem.UsedPercent, load.Load1)
	w.update([]statusbar.Block{
		{
			FullText: status,
			Align:    "left",
			Name:     "sysmon",
			Instance: "0",
			Markup:   "pango",
		},
	})
	b := w.Block()
	b.FullText = status
	b.Align = "left"
	b.Name = "sysmon"
	b.Instance = "0"
	b.Markup = "pango"
	w.update([]statusbar.Block{b})
}

func init() {
	registry.RegisterWidget(new(SystemMonitor))
	statusbar.RegisterInternalWidget[SystemMonitor](internal.M, "SystemMonitor")
}

M widgets/temp.go => widgets/temp.go +3 -7
@@ 9,7 9,7 @@ import (
	"time"

	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
)

type TemperatureMonitor struct {


@@ 22,11 22,7 @@ type TemperatureMonitor struct {
	baseWidget
}

func (w *TemperatureMonitor) WidgetName() string    { return "TempMonitor" }
func (w *TemperatureMonitor) New() statusbar.Widget { return new(TemperatureMonitor) }

func (w *TemperatureMonitor) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
	defer done()
func (w *TemperatureMonitor) Run(ctx context.Context, update statusbar.UpdateFunc) {
	w.init()
	w.status = make([]statusbar.Block, 0, 2)
	w.emitStatus(update)


@@ 91,5 87,5 @@ func (w *TemperatureMonitor) init() {
}

func init() {
	registry.RegisterWidget(new(TemperatureMonitor))
	statusbar.RegisterInternalWidget[TemperatureMonitor](internal.M, "TempMonitor")
}

M widgets/volume.go => widgets/volume.go +3 -7
@@ 11,7 11,7 @@ import (

	"github.com/google/shlex"
	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
)

type Volume struct {


@@ 38,11 38,7 @@ type Volume struct {
	baseWidget
}

func (w *Volume) WidgetName() string    { return "Volume" }
func (w *Volume) New() statusbar.Widget { return new(Volume) }

func (w *Volume) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
	defer done()
func (w *Volume) Run(ctx context.Context, update statusbar.UpdateFunc) {
	w.init()
	if w.volMonCmd == nil {
		w.errorf("No volMonCommand provided")


@@ 185,5 181,5 @@ func (w *Volume) init() {
}

func init() {
	registry.RegisterWidget(new(Volume))
	statusbar.RegisterInternalWidget[Volume](internal.M, "Volume")
}

M widgets/window.go => widgets/window.go +3 -7
@@ 7,7 7,7 @@ import (
	"unicode"

	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/internal/registry"
	"gobytes.dev/statusbar/internal"
	"gobytes.dev/swayipc"
)



@@ 24,11 24,7 @@ type Window struct {
	baseWidget
}

func (w *Window) WidgetName() string    { return "Window" }
func (w *Window) New() statusbar.Widget { return new(Window) }

func (w *Window) Run(ctx context.Context, update statusbar.UpdateFunc, done func()) {
	defer done()
func (w *Window) Run(ctx context.Context, update statusbar.UpdateFunc) {
	w.init()
	w.update = update
	w.events = make(chan *swayipc.WindowEvent)


@@ 218,5 214,5 @@ func (w *Window) init() {
}

func init() {
	registry.RegisterWidget(new(Window))
	statusbar.RegisterInternalWidget[Window](internal.M, "Window")
}