~rkintzi/statusbar

ba7fc174d8f2d2f326370e07a915f077f0ed0728 — RadosÅ‚aw Kintzi 10 months ago e2a1b0e
Add common options to each widget.
M README.md => README.md +44 -3
@@ 20,6 20,7 @@ By default, the program reads its configuration from the
for further customization:

```
- widget: DefaultConfig
- widget: Window
  excludeAppIDs: ["foot"]
  titlePrefix: "\uf2a3 "


@@ 27,9 28,11 @@ for further customization:
- widget: ExternalProgram
  command: /usr/bin/i3status
- widget: SystemMonitor
  format: "\ue8b0 {cpu}% \ue8b2 {mem}% \ue8b3 {load}%"
  format: "\ue8b0 {cpu}% \ue8b2 {mem}%  \ue8b3 {load}%"
  separator: false
  separatorBlockWidth: 0
- widget: TempMonitor
  format: "\ue8a2 {temp}\u00B0C \ue8a1 {fan}"
  format: " \ue8a2 {temp}\u00B0C \ue8a1 {fan}"
- widget: Volume
  scrollDownCommand: volumeup
  scrollUpCommand: volumedown


@@ 65,6 68,13 @@ bar {

Currently, the statusbar provides three widgets, and more will be added soon.

### DefaultConfig

The DefaultConfig is not a real widget. Instead, all the configuration options
given in this section of the config file are used to initialize widgets.
In the example above, no options were given, but you users can use each option
described in the "Common options" section below. 

### BatteryMonitor

The BatteryMonitor widget is used to display the status and level of the


@@ 209,7 219,7 @@ device. It offers the following options for customization:
The window widget displays the title of the focused window. It provides the
following options:

- `titlePrefix` - a string that is added before the title (default: "\uf2a3 ")
- `titlePrefix` - a string that is added before the title (default: "")
- `maxTitleLength` - if the length of the title exceeds the specified value,
  it is trimmed and an ellipsis symbol is added (default: 80, set to -1 to
  disable trimming)


@@ 221,6 231,37 @@ following options:
  This option allows you to specify a list of application IDs that will not be
  appended.

### Common options

Options described in this section can be set for every widget, as well
as in the special widget DefaultConfig (described above). This options are
also described in the [swaybar-protocol(7)](https://man.archlinux.org/man/swaybar-protocol.7).

- `color` - Sets the text color of the widget using #rrggbbaa or #rrggbb
  notation.
- `background` - Sets the background color of the widget.
- `border` - Sets the border color for the widget.
- `borderTop` - Sets the height in pixels of the top border. The default value
  is 1.
- `borderBottom` - Sets the height in pixels of the bottom border. The default
  value is 1.
- `borderLeft` - Sets the width in pixels of the left border. The default
  value is 1.
- `borderRight` - Sets the width in pixels of the right border. The default
  value is 1.
- `minWidth` - Sets the minimum width to use for the widget. The width can be
  specified in pixels or as a string to allow for dynamic calculation based on
  the width of the text.
- `separator` - Specifies whether the bar separator should be drawn after the
  widget. Refer to [sway-bar(5)](https://man.archlinux.org/man/sway-bar.5) for
  more information on how to set the separator text.
- `separatorBlockWidth` - Sets the amount of pixels to leave blank after the
  block. The separator text will be displayed centered in this gap. The
  default value is 9 pixels.
- `align` - Specifies how the text should be aligned inside the block if it
  does not span the full width. The options are left (default), right, or
  center.

## statusbar - The Library

If there is a widget that you believe is missing from the project, you can

M configuration.go => configuration.go +6 -1
@@ 105,7 105,12 @@ func (c *Configuration) parseWidgetConfig(n *yaml.Node) error {
	if err != nil {
		return err
	}
	if cons, ok := c.cons[rec.Widget]; !ok {
	if rec.Widget == "DefaultConfig" {
		err := n.Decode(&c.common)
		if err != nil {
			return err
		}
	} else if cons, ok := c.cons[rec.Widget]; !ok {
		return fmt.Errorf("unknown widget: %s", rec.Widget)
	} else {
		widget := cons.Construct(c.common)

M configuration_test.go => configuration_test.go +12 -7
@@ 4,7 4,6 @@ import (
	"bytes"
	"testing"

	"github.com/google/go-cmp/cmp"
	"gobytes.dev/statusbar"
	"gobytes.dev/statusbar/widgets"
)


@@ 13,23 12,29 @@ func cmpExternalProgram(w1, w2 *widgets.ExternalProgram) bool {
	return w1.Command == w2.Command
}

func cmpDateTime(w1, w2 *widgets.DateTime) bool {
	return w1 == w2
}

func TestConfiguration(t *testing.T) {
	config := `- widget: ExternalProgram
  command: some-command
- widget: DateTime
  dateFormat: date-format
  timeFormat: time-format
  format: format
`
	c := statusbar.Registry{}
	c := statusbar.Configuration{}
	err := c.Parse(bytes.NewBufferString(config))
	requireNoErr(t, err)
	expWidgets := []statusbar.Widget{
		&widgets.ExternalProgram{Command: "some-command"},
		&widgets.DateTime{DateFormat: "date-format", TimeFormat: "time-format"},
		&widgets.DateTime{Format: "format"},
	}
	ws := c.Widgets()
	if !cmp.Equal(ws, expWidgets, cmp.Comparer(cmpExternalProgram)) {
		t.Fatalf(cmp.Diff(ws, expWidgets))
	if ws[0].(*widgets.ExternalProgram).Command != expWidgets[0].(*widgets.ExternalProgram).Command {
		t.Errorf("Unexpected widget: %+v", ws[0])
	}
	if ws[1].(*widgets.DateTime).Format != expWidgets[1].(*widgets.DateTime).Format {
		t.Errorf("Unexpected widget: %+v", ws[1])
	}
}


M swaybarproto.go => swaybarproto.go +90 -16
@@ 1,5 1,12 @@
package statusbar

import (
	"encoding/json"
	"strconv"

	"gopkg.in/yaml.v3"
)

type Header struct {
	Version     int  `json:"version"`
	ClickEvents bool `json:"click_events"`


@@ 8,24 15,39 @@ type Header struct {
}

type Block struct {
	FullText            string `json:"full_text,omitempty"`
	ShortText           string `json:"short_text,omitempty"`
	Color               string `json:"color,omitempty"`
	Background          string `json:"background,omitempty"`
	Border              string `json:"border,omitempty"`
	BorderTop           int    `json:"border_top,omitempty"`
	BorterBottom        int    `json:"border_bottom,omitempty"`
	BorderLeft          int    `json:"border_left,omitempty"`
	BorderRight         int    `json:"border_right,omitempty"`
	MinWidth            int    `json:"min_width,omitempty"`
	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"`
	Markup              string `json:"markup,omitempty"`
	FullText            string    `json:"full_text,omitempty"`
	ShortText           string    `json:"short_text,omitempty"`
	Color               string    `json:"color,omitempty"`
	Background          string    `json:"background,omitempty"`
	Border              string    `json:"border,omitempty"`
	BorderTop           *int      `json:"border_top,omitempty"`
	BorderBottom        *int      `json:"border_bottom,omitempty"`
	BorderLeft          *int      `json:"border_left,omitempty"`
	BorderRight         *int      `json:"border_right,omitempty"`
	MinWidth            *MinWidth `json:"min_width,omitempty"`
	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"`
	Markup              string    `json:"markup,omitempty"`
}

func (b *Block) SetBorder(top, right, bottom, left int) {
	b.BorderTop = &top
	b.BorderBottom = &bottom
	b.BorderRight = &left
	b.BorderRight = &right
}

func (b *Block) SetSeparator(show bool, width int) {
	b.Separator = &show
	b.SeparatorBlockWidth = &width
}

func (b *Block) SetMinWidthInt(i int)       { b.MinWidth = &MinWidth{i: i} }
func (b *Block) SetMinWidthString(s string) { b.MinWidth = &MinWidth{s: s} }

type Event struct {
	Name      string `json:"name"`
	Instance  string `json:"instance"`


@@ 38,3 60,55 @@ type Event struct {
	Width     int    `json:"width"`
	Height    int    `json:"height"`
}

type MinWidth struct {
	s string
	i int
}

func (w *MinWidth) UnmarshalJSON(bs []byte) error {
	mw := MinWidth{}
	if bs[0] == '"' {
		err := json.Unmarshal(bs, &mw.s)
		if err != nil {
			return err
		}
	} else {
		err := json.Unmarshal(bs, &mw.i)
		if err != nil {
			return err
		}
	}
	*w = mw
	return nil
}

func (w MinWidth) MarshalJSON() ([]byte, error) {
	if w.s != "" {
		return json.Marshal(w.s)
	} else {
		return json.Marshal(w.i)
	}
}

func (w *MinWidth) UnmarshalYAML(n *yaml.Node) error {
	mw := MinWidth{}
	err := n.Decode(&mw.s)
	if err != nil {
		return err
	}
	if i, err := strconv.Atoi(mw.s); err == nil {
		mw.s = ""
		mw.i = i
	}
	*w = mw
	return err
}

func (w MinWidth) MarshalYAML() (interface{}, error) {
	if w.s != "" {
		return w.s, nil
	} else {
		return w.i, nil
	}
}

A swaybarproto_test.go => swaybarproto_test.go +39 -0
@@ 0,0 1,39 @@
package statusbar

import (
	"encoding/json"
	"testing"
)

func TestBlockUnMarshaling(t *testing.T) {
	var (
		jsonStr = `[{"min_width":10},{"min_width":"10"},{"border":"color"}]`
		blocks  []Block
		exp     = []Block{
			{MinWidth: &MinWidth{i: 10}},
			{MinWidth: &MinWidth{s: "10"}},
			{Border: "color"},
		}
	)
	err := json.Unmarshal([]byte(jsonStr), &blocks)
	if err != nil {
		t.Fatalf("Unexpected error: %s", err)
	}
	if len(blocks) != len(exp) {
		t.Errorf("Unexpected block deserialized from json: %+v", blocks)
	}
	for i := range exp {
		if blocks[i].MinWidth == nil && exp[i].MinWidth != nil {
			t.Errorf("Unexpected %d-th block deserialized from json: %+v", i, blocks[i])
		} else if blocks[i].MinWidth != nil && exp[i].MinWidth == nil {
			t.Errorf("Unexpected %d-th block deserialized from json: %+v", i, blocks[i])
		} else if blocks[i].MinWidth != nil && *blocks[i].MinWidth != *exp[i].MinWidth {
			t.Errorf("Unexpected %d-th block deserialized from json: %+v", i, blocks[i])
		}
		blocks[i].MinWidth = nil
		exp[i].MinWidth = nil
		if blocks[i] != exp[i] {
			t.Errorf("Unexpected %d-th block deserialized from json: %+v", i, blocks[i])
		}
	}
}

M widget.go => widget.go +18 -9
@@ 1,17 1,22 @@
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"`
	Color               string    `yaml:"color,omitempty"`
	Background          string    `yaml:"background,omitempty"`
	Border              string    `yaml:"border"`
	BorderTop           *int      `yaml:"borderTop"`
	BorterBottom        *int      `yaml:"borderBottom"`
	BorderLeft          *int      `yaml:"borderLeft"`
	BorderRight         *int      `yaml:"borderRight"`
	MinWidth            *MinWidth `yaml:"minWidth"`
	Separator           *bool     `yaml:"separator"`
	SeparatorBlockWidth *int      `yaml:"separatorBlockWidth"`
	Align               string    `yaml:"align,omitempty"`
}

func (p *BaseWidget) setDefault(c BaseWidget) {
	p.Color = c.Color
	p.Background = c.Background
	p.Border = c.Border
	p.BorderTop = c.BorderTop
	p.BorterBottom = c.BorterBottom


@@ 20,17 25,21 @@ func (p *BaseWidget) setDefault(c BaseWidget) {
	p.MinWidth = c.MinWidth
	p.Separator = c.Separator
	p.SeparatorBlockWidth = c.SeparatorBlockWidth
	p.Align = c.Align
}

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

M widgets/batmon.go => widgets/batmon.go +9 -11
@@ 64,7 64,8 @@ type BatteryMonitor struct {
	chargingIcons    []rune
	dischargingIcons []rune
	idx              []int
	baseWidget       `yaml:",inline"`

	baseWidget `yaml:",inline"`
}

func (w *BatteryMonitor) Run(ctx context.Context, update statusbar.UpdateFunc) {


@@ 123,16 124,13 @@ func (w *BatteryMonitor) emitStatus(update statusbar.UpdateFunc) {
	}
	percent := chargeNow * 100 / chargeFull
	fullText := w.sprintf(status, percent)
	update([]statusbar.Block{
		{
			FullText: fullText,
			Align:    "left",
			Name:     "batmon",
			Instance: "0",
			Color:    w.colors.Get(float64(percent)),
			Markup:   "pango",
		},
	})
	bl := w.Block()
	bl.FullText = fullText
	bl.Name = "batmon"
	bl.Instance = "0"
	bl.Color = w.colors.Get(float64(percent))
	bl.Markup = "pango"
	update([]statusbar.Block{bl})
	if status == "Discharging" && percent < w.CriticalLevel && !w.wasNotified {
		w.notify()
		w.wasNotified = true

M widgets/date.go => widgets/date.go +9 -34
@@ 9,15 9,13 @@ import (
)

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

	baseWidget
	baseWidget `yaml:",inline"`
}

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


@@ 34,35 32,12 @@ func (w *DateTime) Run(ctx context.Context, fn statusbar.UpdateFunc) {

func (w *DateTime) publish(update statusbar.UpdateFunc, now time.Time) {
	now = now.In(time.Local)
	blocks := make([]statusbar.Block, 0, 3)
	if w.Format != "" {
		blocks = append(blocks, statusbar.Block{
			FullText: now.Format(w.Format),
			Align:    "left",
			Name:     "datetime",
			Instance: "0",
			Markup:   "pango",
		})
	}
	if w.DateFormat != "" {
		blocks = append(blocks, statusbar.Block{
			FullText: now.Format(w.DateFormat),
			Align:    "left",
			Name:     "date",
			Instance: "0",
			Markup:   "pango",
		})
	}
	if w.TimeFormat != "" {
		blocks = append(blocks, statusbar.Block{
			FullText: now.Format(w.TimeFormat),
			Align:    "left",
			Name:     "time",
			Instance: "0",
			Markup:   "pango",
		})
	}
	update(blocks)
	b := w.Block()
	b.FullText = now.Format(w.Format)
	b.Name = "datetime"
	b.Instance = "0"
	b.Markup = "pango"
	update([]statusbar.Block{b})
}

func init() {

M widgets/sysmon.go => widgets/sysmon.go +0 -1
@@ 58,7 58,6 @@ func (w *SystemMonitor) emitStatus(ctx context.Context) {
	status := w.format.Format(cpu[0], mem.UsedPercent, load.Load1)
	b := w.Block()
	b.FullText = status
	b.Align = "left"
	b.Name = "sysmon"
	b.Instance = "0"
	b.Markup = "pango"

M widgets/temp.go => widgets/temp.go +8 -13
@@ 17,14 17,12 @@ type TemperatureMonitor struct {
	TempInput string `yaml:"tempInput"`
	FanInput  string `yaml:"fanInput"`

	format Format
	status []statusbar.Block
	baseWidget
	format     Format
	baseWidget `yaml:",inline"`
}

func (w *TemperatureMonitor) Run(ctx context.Context, update statusbar.UpdateFunc) {
	w.init()
	w.status = make([]statusbar.Block, 0, 2)
	w.emitStatus(update)
	t := time.NewTicker(1 * time.Second)
	for {


@@ 51,7 49,6 @@ func (w *TemperatureMonitor) readInput(input string) (int, error) {
}

func (w *TemperatureMonitor) emitStatus(update statusbar.UpdateFunc) {
	w.status = w.status[:0]
	temp, err := w.readInput(w.TempInput)
	if err != nil {
		w.errorf(err.Error())


@@ 62,14 59,12 @@ func (w *TemperatureMonitor) emitStatus(update statusbar.UpdateFunc) {
		w.errorf(err.Error())
		fan = 0
	}
	w.status = append(w.status, statusbar.Block{
		FullText: w.format.Format(temp/1000, fan),
		Align:    "left",
		Name:     "batmon",
		Instance: "0",
		Markup:   "pango",
	})
	update(w.status)
	b := w.Block()
	b.FullText = w.format.Format(temp/1000, fan)
	b.Name = "batmon"
	b.Instance = "0"
	b.Markup = "pango"
	update([]statusbar.Block{b})
}

func (w *TemperatureMonitor) init() {

M widgets/volume.go => widgets/volume.go +7 -10
@@ 35,7 35,7 @@ type Volume struct {
	rc      io.ReadCloser
	procErr error

	baseWidget
	baseWidget `yaml:",inline"`
}

func (w *Volume) Run(ctx context.Context, update statusbar.UpdateFunc) {


@@ 119,15 119,12 @@ func (w *Volume) emitVol(vol int, mute bool, update statusbar.UpdateFunc) {
		icon = w.MuteIcon
	}
	status := w.fmt.Format(icon, vol)
	update([]statusbar.Block{
		{
			FullText: status,
			Align:    "left",
			Name:     "volmon",
			Instance: "0",
			Markup:   "pango",
		},
	})
	b := w.Block()
	b.FullText = status
	b.Name = "volmon"
	b.Instance = "0"
	b.Markup = "pango"
	update([]statusbar.Block{b})
}

func (w *Volume) init() {

M widgets/window.go => widgets/window.go +8 -10
@@ 21,7 21,8 @@ type Window struct {
	events        chan *swayipc.WindowEvent
	conn          *swayipc.Conn
	update        statusbar.UpdateFunc
	baseWidget

	baseWidget `yaml:",inline"`
}

func (w *Window) Run(ctx context.Context, update statusbar.UpdateFunc) {


@@ 188,15 189,12 @@ func (w *Window) parseNode(c *swayipc.Node) string {
}

func (w *Window) printTitle(title string) {
	w.update([]statusbar.Block{
		{
			FullText: title,
			Align:    "left",
			Name:     "window",
			Instance: "0",
			Markup:   "pango",
		},
	})
	b := w.Block()
	b.FullText = title
	b.Name = "window"
	b.Instance = "0"
	b.Markup = "pango"
	w.update([]statusbar.Block{b})
}

func (w *Window) init() {