Add click event handling

Signed-off-by: AKP <tom@tdpain.net>
This commit is contained in:
akp 2022-05-27 16:19:49 +01:00
parent 43ce8474cb
commit 21e9e1188e
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
3 changed files with 87 additions and 10 deletions

View file

@ -4,12 +4,13 @@
---
This is a i3wm status bar that's built as a toy project. It's pretty basic, doesn't have many features and isn't very configurable unless you want to edit the source and recompile it.
This is a i3wm status bar that's built as a toy project. ~~It's~~ **It was** pretty basic (I've since added click event support), doesn't have many features and isn't very configurable unless you want to edit the source and recompile it.
This interacts with i3 using the [i3bar input protocol](https://i3wm.org/docs/i3bar-protocol.html). i3 versions earlier than v4.3 are not supported.
### Features
* Supports click events
* SIGUSR1 forces a refresh
* It has colours
* Sometimes it breaks

View file

@ -25,7 +25,7 @@ func main() {
Filename: logFileName,
MaxSize: 1, // MB
MaxAge: 14, // days
})).Level(zerolog.ErrorLevel)
})).Level(zerolog.DebugLevel)
if err := run(); err != nil {
log.Fatal().Err(err).Msg("unhandled error")
@ -33,7 +33,7 @@ func main() {
}
func run() error {
b := i3bar.New(os.Stdout, time.Second*5, syscall.SIGUSR1)
b := i3bar.New(os.Stdout, os.Stdin, time.Second*5, syscall.SIGUSR1)
if err := b.Initialise(); err != nil {
return err
}

View file

@ -1,6 +1,8 @@
package i3bar
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
@ -14,17 +16,20 @@ import (
type I3bar struct {
writer io.Writer
reader io.Reader
updateInterval time.Duration
updateSignal syscall.Signal
registeredGenerators []BlockGenerator
registeredConsumers []ClickEventConsumer
hasInitialised bool
hasSentFirstLine bool
}
func New(writer io.Writer, updateInterval time.Duration, updateSignal syscall.Signal) *I3bar {
func New(writer io.Writer, reader io.Reader, updateInterval time.Duration, updateSignal syscall.Signal) *I3bar {
return &I3bar{
writer: writer,
reader: reader,
updateInterval: updateInterval,
updateSignal: updateSignal,
}
@ -32,13 +37,13 @@ func New(writer io.Writer, updateInterval time.Duration, updateSignal syscall.Si
func (b *I3bar) Initialise() error {
capabilities, err := json.Marshal(map[string]any{
"version": 1,
"version": 1,
"click_events": true,
})
if err != nil {
return err
}
if _, err := b.writer.Write(append(capabilities, '\n')); err != nil {
return err
}
@ -95,8 +100,15 @@ func (b *I3bar) Emit(generators []BlockGenerator) error {
return nil
}
// RegisterBlockGenerator registers a block generator with the status bar. This
// function should not be called after StartLoop is called.
func (b *I3bar) RegisterBlockGenerator(bg ...BlockGenerator) {
b.registeredGenerators = append(b.registeredGenerators, bg...)
for _, generator := range bg {
if g, ok := generator.(ClickEventConsumer); ok {
b.registeredConsumers = append(b.registeredConsumers, g)
}
}
}
func (b *I3bar) StartLoop() error {
@ -110,15 +122,50 @@ func (b *I3bar) StartLoop() error {
sigUpdate := make(chan os.Signal, 1)
signal.Notify(sigUpdate, os.Signal(b.updateSignal))
go b.consumerLoop(func() {
sigUpdate <- b.updateSignal
})
for {
select {
case <-sigUpdate:
if err := b.Emit(b.registeredGenerators); err != nil {
return err
log.Error().Err(err).Msg("could not emit registered generators")
}
case <-ticker.C:
if err := b.Emit(b.registeredGenerators); err != nil {
return err
log.Error().Err(err).Msg("could not emit registered generators")
}
}
}
}
func (b *I3bar) consumerLoop(requestBarRefresh func()) {
r := bufio.NewReader(b.reader)
for {
inputBytes, err := r.ReadBytes('\n')
if err != nil {
log.Error().Err(err).Msg("could not read from input reader")
continue
}
// "ReadBytes reads until the first occurrence of delim in the input,
// returning a slice containing the data up to and including the
// delimiter."
inputBytes = inputBytes[:len(inputBytes)-1]
// try and parse inputted JSON
event := new(ClickEvent)
if err := json.Unmarshal(bytes.Trim(inputBytes, ","), event); err != nil {
continue // idk what this could be but it's not relevant so BYE!
}
for _, consumer := range b.registeredConsumers {
consumerName, consumerInstance := consumer.GetNameAndInstance()
if consumerName == event.Name && (consumerName == "" || consumerInstance == event.Instance) {
if consumer.OnClick(event) {
requestBarRefresh()
}
}
}
}
@ -144,7 +191,36 @@ type Block struct {
Markup string `json:"markup,omitempty"`
}
type BlockGenerator interface {
Block(*ColorSet) (*Block, error)
type ProvidesNameAndInstance interface {
GetNameAndInstance() (name, instance string)
}
type BlockGenerator interface {
ProvidesNameAndInstance
Block(*ColorSet) (*Block, error)
}
type ClickEvent struct {
Name string `json:"name"`
Instance string `json:"instance"`
Button int `json:"button"`
Modifiers []string `json:"modifiers"`
X int `json:"x"`
Y int `json:"y"`
RelativeX int `json:"relative_x"`
RelativeY int `json:"relative_y"`
OutputX int `json:"output_x"`
OutputY int `json:"output_y"`
Width int `json:"width"`
Height int `json:"height"`
}
type ClickEventConsumer interface {
ProvidesNameAndInstance
// OnClick is called when a new ClickEvent is recieved with the
// corresponding name and instance is recieved. If OnClick returns true, a
// refresh of the entire statusbar will be performed.
//
// OnClick must not modify the ClickEvent as it may be reused elsewhere.
OnClick(*ClickEvent) (shouldRefresh bool)
}