M go.mod => go.mod +1 -0
@@ 8,6 8,7 @@ require (
github.com/google/go-cmp v0.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/shirou/gopsutil/v3 v3.23.10
+ gobytes.dev/swayipc v0.0.0-20231109133834-244c14b18a41
gopkg.in/yaml.v3 v3.0.1
)
M go.sum => go.sum +2 -0
@@ 39,6 39,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+gobytes.dev/swayipc v0.0.0-20231109133834-244c14b18a41 h1:DqSMIV/EHVurViBC0FEsPcpB/SlNJMnLqdzCihFrG84=
+gobytes.dev/swayipc v0.0.0-20231109133834-244c14b18a41/go.mod h1:Ie3kt/A1HXdk0zFj5BzaYFyKuGcCxbpjgdrnTFY6kaE=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
A widgets/window.go => widgets/window.go +226 -0
@@ 0,0 1,226 @@
+package widgets
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "unicode"
+
+ "gobytes.dev/statusbar"
+ "gobytes.dev/statusbar/internal/registry"
+ "gobytes.dev/swayipc"
+)
+
+type Window struct {
+ TitlePrefix string `yaml:"titlePrefix"`
+ MaxTitleLen int `yaml:"maxTitleLength"`
+ Ellipsis string `yaml:"ellipsis"`
+ ExcludeAppIDs []string `yaml:"excludeAppIDs"`
+ AllowSymbols bool `yaml:"allowSymbols"`
+
+ excludeAppIDs map[string]bool
+ events chan *swayipc.WindowEvent
+ conn *swayipc.Conn
+ update statusbar.UpdateFunc
+ 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()
+ w.init()
+ w.update = update
+ w.events = make(chan *swayipc.WindowEvent)
+ conn, err := swayipc.Connect(ctx)
+ if err != nil {
+ w.errorf(err.Error())
+ return
+ }
+ w.conn = conn
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go w.watchEvents(ctx, wg.Done)
+ conn.RegisterEventHandler(w)
+ _, err = conn.Subscribe(swayipc.WindowEventType, swayipc.WorkspaceEventType)
+ if err != nil {
+ w.errorf(err.Error())
+ cancel()
+ }
+ tree, err := conn.GetTree()
+ if err != nil {
+ w.errorf(err.Error())
+ cancel()
+ }
+ cont := w.findFocused(&tree)
+ if cont != nil {
+ w.events <- &swayipc.WindowEvent{
+ Change: "not-an-event",
+ Container: *cont,
+ }
+ }
+ wg.Wait()
+ <-ctx.Done()
+}
+
+func (w *Window) findFocused(node *swayipc.Node) *swayipc.Node {
+ if (node.Type == "con" || node.Type == "floating_con") && node.Focused {
+ return node
+ }
+ for _, child := range node.Nodes {
+ focused := w.findFocused(&child)
+ if focused != nil {
+ return focused
+ }
+ }
+ return nil
+}
+
+func (w *Window) HandleEvent(ev swayipc.Event) {
+ switch ev := ev.(type) {
+ case *swayipc.WindowEvent:
+ w.events <- ev
+ case *swayipc.WorkspaceEvent:
+ if ev.Change != "init" && ev.Current.Focused {
+ w.events <- &swayipc.WindowEvent{
+ Change: "close",
+ }
+ }
+ }
+}
+
+func (w *Window) watchEvents(ctx context.Context, done func()) {
+ defer done()
+ var hadEvent bool
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case ev := <-w.events:
+ switch ev.Change {
+ case "not-an-event":
+ if hadEvent {
+ continue
+ }
+ w.printTitle(w.parseNode(&ev.Container))
+ continue
+ case "focus":
+ w.printTitle(w.parseNode(&ev.Container))
+ case "title":
+ if ev.Container.Focused {
+ w.printTitle(w.parseNode(&ev.Container))
+ }
+ case "close":
+ w.printTitle("")
+ }
+ hadEvent = true
+ }
+ }
+}
+
+func removeUnclosed(name string) string {
+ count := 0
+ for i := len(name) - 1; i >= 0; i-- {
+ switch name[i] {
+ case '}', ']', ')':
+ count += 1
+ case '{', '[', '(':
+ count -= 1
+ }
+ if count < 0 {
+ name = name[:i]
+ count = 0
+ }
+ }
+ name = strings.TrimRightFunc(name, func(r rune) bool {
+ return (unicode.IsSpace(r) || unicode.IsPunct(r))
+ })
+ return name
+}
+
+func (w *Window) removeAppId(name, appId string) string {
+ name = strings.TrimSpace(name)
+ trimmed := strings.TrimPrefix(name, appId)
+ if trimmed != name {
+ trimmed = strings.TrimLeftFunc(trimmed, func(r rune) bool {
+ return (unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r)) &&
+ (r != '[' && r != '(' && r != '{') &&
+ (r != '~' && r != '/')
+ })
+ }
+ name = trimmed
+ trimmed = strings.TrimSuffix(name, appId)
+ if trimmed != name {
+ trimmed = strings.TrimRightFunc(trimmed, func(r rune) bool {
+ return (unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r)) &&
+ (r != ']' && r != ')' && r != '}') &&
+ (r != '~' && r != '/')
+ })
+ }
+ name = trimmed
+ return name
+}
+
+func (w *Window) parseNode(c *swayipc.Node) string {
+ var (
+ name = c.Name
+ appId = c.AppId
+ suffix string
+ )
+ if appId == "" {
+ appId = c.WindowProperties.Class
+ }
+ name = w.removeAppId(name, appId)
+ if name == "" {
+ suffix = appId
+ } else {
+ if !w.excludeAppIDs[appId] {
+ if !strings.Contains(strings.ToLower(name), strings.ToLower(appId)) {
+ suffix = " - " + appId
+ }
+ }
+ }
+ if w.MaxTitleLen > 0 && len(name)+len(suffix)+len(w.TitlePrefix) > w.MaxTitleLen {
+ maxLen := w.MaxTitleLen - len(suffix) - len(w.TitlePrefix)
+ name = name[0 : maxLen-len(w.Ellipsis)]
+ name = removeUnclosed(name)
+ name += w.Ellipsis
+ }
+ return w.TitlePrefix + name + suffix
+}
+
+func (w *Window) printTitle(title string) {
+ w.update([]statusbar.Block{
+ {
+ FullText: title,
+ Align: "left",
+ Name: "window",
+ Instance: "0",
+ Markup: "pango",
+ },
+ })
+}
+
+func (w *Window) init() {
+ w.prefix = "Windows"
+ w.excludeAppIDs = make(map[string]bool)
+ for _, id := range w.ExcludeAppIDs {
+ w.excludeAppIDs[id] = true
+ }
+ if w.MaxTitleLen == 0 {
+ w.MaxTitleLen = 80
+ }
+ if w.Ellipsis == "" {
+ w.Ellipsis = "\u2026"
+ }
+ if w.TitlePrefix == "" {
+ w.TitlePrefix = "\uf2a3 "
+ }
+}
+
+func init() {
+ registry.RegisterWidget(new(Window))
+}
A widgets/window_test.go => widgets/window_test.go +30 -0
@@ 0,0 1,30 @@
+package widgets
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestWindow(t *testing.T) {
+ tcs := []struct {
+ name string
+ exp string
+ }{
+ {name: "title - appid"},
+ {name: "appid - title"},
+ {name: "appid -- title -- appid"},
+ {name: "appid -- [title] -- appid", exp: "[title]"},
+ }
+ for i, tc := range tcs {
+ t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+ w := Window{}
+ trimmed := w.removeAppId(tc.name, "appid")
+ exp := tc.exp
+ if exp == "" {
+ exp = "title"
+ }
+ o := opts{}
+ o.assertEqual(t, trimmed, exp)
+ })
+ }
+}