1// Package bar connects the Wayland layer surface, the Vulkan renderer and the
2// module layout.
3//
4// Everything runs on one goroutine: the loop in Run waits for a Wayland
5// wake-up or a module frame, calls Dispatch, and every Handler method,
6// renderer call and Present happens inside it. Modules only publish frames
7// through a channel.
8package bar
9
10import (
11 "context"
12 "errors"
13 "fmt"
14 "log/slog"
15 "math"
16 "os"
17 "path/filepath"
18 "slices"
19 "strings"
20 "syscall"
21 "time"
22
23 "github.com/bnema/neferclient"
24
25 "github.com/bnema/neferbar/internal/config"
26 "github.com/bnema/neferbar/internal/fswatch"
27 "github.com/bnema/neferbar/internal/glyph"
28 "github.com/bnema/neferbar/internal/gpu"
29 "github.com/bnema/neferbar/internal/layout"
30 "github.com/bnema/neferbar/internal/module"
31 "github.com/bnema/neferbar/internal/syncobj"
32 "github.com/bnema/neferbar/internal/theme"
33)
34
35const acquireTimelineID = 1 << 32
36
37// Stats counts what the bar did; read it only from the loop goroutine or after
38// Run returns.
39type Stats struct {
40 Presented uint64 // frames handed to the compositor
41 Rebuilds uint64 // renderer rebuilds (scale or size changes)
42 Recreated uint64 // layer surface recreations
43 NoSlot uint64 // draws skipped because every image was held
44 Dirty uint64 // module wake-ups that changed the row
45 ModuleWake uint64
46}
47
48// Bar is the running application.
49type Bar struct {
50 cfg config.Config
51 log *slog.Logger
52 display string
53 fg, bg [3]uint8
54
55 conn *neferclient.Conn
56 surf *neferclient.Surface
57 sid neferclient.SurfaceID
58
59 configured bool
60 canPresent bool
61 dirty bool
62
63 // font
64 fonts glyph.Paths
65 face *glyph.Face
66 facePx float64
67
68 dev *gpu.Device
69 node *syncobj.Node
70 fb *neferclient.Feedback
71 rend *gpu.Renderer
72 mod gpu.Modifier
73
74 haveAcquire bool
75 frame gpu.Frame
76 watched [gpu.SlotCount]int // watched eventfds, -1 when none
77
78 runners []runner
79 mods []*module.Module // the modules of runners, in config order
80 runCtx context.Context
81 cancelRun context.CancelFunc
82 lay *layout.Layout
83 modCh chan struct{}
84 cfgPath string
85 cfgCh chan config.Config
86
87 // scriptCh carries the directory of a script that changed on disk.
88 scriptCh chan string
89 scriptCancel context.CancelFunc
90 stale map[string]bool // directories whose modules restart at the next syncModules
91
92 theme theme.Theme
93 themeCh chan struct{}
94 themeCancel context.CancelFunc
95 pal [16][3]uint8
96 env []string // theme colors for the scripts
97
98 // settling is true between a scale or size change and the moment both the
99 // new scale and the new logical width have arrived. They come as separate
100 // events, in either order; building a renderer from half of them presents a
101 // buffer of the wrong size, and some compositors keep that size.
102 settling bool
103 settleC <-chan time.Time
104
105 // fresh is true from the moment a layer surface is created until a renderer
106 // exists for it. A new surface reports scale 1 until the compositor says
107 // otherwise, so its first geometry is not trusted until the events settle.
108 fresh bool
109 // asked is the last (height, scale) a surface was recreated for. Asking for
110 // the same pair twice means the compositor will not give that height, and
111 // the bar takes what it gets instead of looping.
112 askedH int32
113 askedScale float64
114
115 // mappedW is the logical width the current layer surface was first drawn at.
116 // It resets to 0 when the surface is recreated.
117 mappedW int32
118 Stats Stats
119 fatal error
120 closed bool
121}
122
123// New builds a bar from a validated config. cfgPath is the file the config came
124// from: relative theme paths are read from its directory, and the bar reloads
125// it when it changes (empty: no reload). display names the Wayland socket;
126// empty uses $WAYLAND_DISPLAY.
127func New(cfg config.Config, cfgPath string, log *slog.Logger, display string) (*Bar, error) {
128 b := &Bar{cfg: cfg, cfgPath: cfgPath, log: log, display: display, modCh: make(chan struct{}, 1), cfgCh: make(chan config.Config, 1),
129 themeCh: make(chan struct{}, 1), scriptCh: make(chan string, 16)}
130 var err error
131 if err = b.loadTheme(); err != nil {
132 return nil, err
133 }
134 if b.fonts, err = findFonts(cfg.Bar.Font, log); err != nil {
135 return nil, err
136 }
137 for i := range b.watched {
138 b.watched[i] = -1
139 }
140 return b, nil
141}
142
143// barColors picks the bar's own colors: the settings win, otherwise the theme
144// foreground, and a background one step toward the foreground so the bar
145// stands out from a terminal window of the same theme.
146func barColors(cfg config.Config, th theme.Theme) (fg, bg [3]uint8) {
147 fg, bg = th.Foreground, theme.Mix(th.Background, th.Foreground, 0.07)
148 if c, err := config.ParseColor(cfg.Bar.Foreground); err == nil && cfg.Bar.Foreground != "" {
149 fg = c
150 }
151 if c, err := config.ParseColor(cfg.Bar.Background); err == nil && cfg.Bar.Background != "" {
152 bg = c
153 }
154 return fg, bg
155}
156
157// loadTheme resolves bar.theme and applies it. When the theme cannot be read
158// the built-in colors are used, and the problem is logged.
159func (b *Bar) loadTheme() error {
160 base := filepath.Dir(b.cfgPath)
161 if b.cfgPath == "" {
162 base = "."
163 }
164 th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv())
165 if err != nil {
166 b.log.Warn("using the built-in colors: "+err.Error(), "theme", b.cfg.Bar.Theme)
167 } else {
168 b.log.Info("theme", "source", th.Source)
169 }
170 b.setTheme(th)
171 return nil
172}
173
174// setTheme makes th the colors of the bar and of its scripts.
175func (b *Bar) setTheme(th theme.Theme) {
176 b.theme = th
177 b.fg, b.bg = barColors(b.cfg, th)
178 b.pal = th.Palette
179 b.env = th.EnvVars(b.bg, b.fg, b.cfg.Bar.Accent)
180 if b.lay != nil {
181 b.lay.SetColors(b.fg, b.bg, b.pal)
182 }
183 b.dirty = true
184 b.watchTheme()
185}
186
187// watchTheme watches every file the theme was read from, so changing the
188// terminal's theme changes the bar.
189func (b *Bar) watchTheme() {
190 if b.themeCancel != nil {
191 b.themeCancel()
192 }
193 if b.runCtx == nil {
194 return // Run has not started; it calls this again
195 }
196 ctx, cancel := context.WithCancel(b.runCtx)
197 b.themeCancel = cancel
198 for _, f := range b.theme.Files {
199 go func() {
200 if err := fswatch.Watch(ctx, f, 150*time.Millisecond, b.themeCh); err != nil {
201 b.log.Warn("cannot watch a theme file; edits to it need a restart", "file", f, "err", err)
202 }
203 }()
204 }
205}
206
207// watchScripts watches the directory of every module's script, so editing a
208// script, or a helper it sources from the same directory, restarts the modules
209// that live there. It runs again whenever the set of modules changes.
210func (b *Bar) watchScripts() {
211 if b.scriptCancel != nil {
212 b.scriptCancel()
213 }
214 if b.runCtx == nil {
215 return // Run has not started; it calls syncModules, which calls this
216 }
217 ctx, cancel := context.WithCancel(b.runCtx)
218 b.scriptCancel = cancel
219 seen := map[string]bool{}
220 for _, r := range b.runners {
221 dir := module.ScriptDir(r.cfg.Exec)
222 if dir == "" || seen[dir] {
223 continue
224 }
225 seen[dir] = true
226 go func() {
227 changed := make(chan struct{}, 1)
228 go func() {
229 if err := fswatch.WatchDir(ctx, dir, 150*time.Millisecond, changed); err != nil {
230 b.log.Warn("cannot watch a script directory; edits to its scripts need a restart", "dir", dir, "err", err)
231 }
232 }()
233 for {
234 select {
235 case <-ctx.Done():
236 return
237 case <-changed:
238 select {
239 case b.scriptCh <- dir:
240 case <-ctx.Done():
241 return
242 }
243 }
244 }
245 }()
246 }
247}
248
249// restartScripts restarts the modules whose script is in dir.
250func (b *Bar) restartScripts(dir string) {
251 b.log.Info("script changed; restarting its modules", "dir", dir)
252 if b.stale == nil {
253 b.stale = map[string]bool{}
254 }
255 b.stale[dir] = true
256 b.syncModules(b.cfg.Module)
257}
258
259// reloadTheme re-reads the theme after one of its files changed.
260func (b *Bar) reloadTheme() {
261 base := filepath.Dir(b.cfgPath)
262 if b.cfgPath == "" {
263 base = "."
264 }
265 th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv())
266 if err != nil {
267 b.log.Warn("theme reload failed; keeping the current colors", "err", err)
268 return
269 }
270 if th.Equal(b.theme) {
271 // Same colors, but the files that make them up may have changed.
272 if !slices.Equal(th.Files, b.theme.Files) {
273 b.theme.Files = th.Files
274 b.watchTheme()
275 }
276 return
277 }
278 b.log.Info("theme changed", "source", th.Source)
279 b.setTheme(th)
280 b.syncModules(b.cfg.Module) // scripts get the new colors in their environment
281}
282
283// runner is one started module and how to stop it.
284type runner struct {
285 cfg config.Module
286 env []string
287 m *module.Module
288 cancel context.CancelFunc
289}
290
291var zones = map[string]module.Zone{"left": module.Left, "center": module.Center, "right": module.Right}
292
293// syncModules makes the running modules match want. A module with the same
294// name, zone and command keeps running; every other one is stopped, and a new
295// or changed one is started.
296func (b *Bar) syncModules(want []config.Module) {
297 old := make(map[string]runner, len(b.runners))
298 for _, r := range b.runners {
299 old[r.cfg.Name] = r
300 }
301 next := make([]runner, 0, len(want))
302 for _, w := range want {
303 if r, ok := old[w.Name]; ok {
304 delete(old, w.Name)
305 if r.cfg == w && slices.Equal(r.env, b.env) && !b.stale[module.ScriptDir(w.Exec)] {
306 next = append(next, r)
307 continue
308 }
309 r.cancel()
310 }
311 ctx, cancel := context.WithCancel(b.runCtx)
312 m := module.New(w.Name, zones[w.Zone], w.Exec, b.modCh, b.log)
313 m.Env = b.env
314 m.Start(ctx)
315 next = append(next, runner{cfg: w, env: b.env, m: m, cancel: cancel})
316 }
317 for _, r := range old {
318 r.cancel()
319 }
320 b.runners = next
321 clear(b.stale)
322 b.watchScripts()
323 b.mods = b.mods[:0]
324 for _, r := range next {
325 b.mods = append(b.mods, r.m)
326 }
327 if b.lay != nil {
328 b.lay.SetModules(b.mods)
329 }
330}
331
332// applyConfig switches to next. Nothing changes if it cannot be applied.
333func (b *Bar) applyConfig(next config.Config) {
334 old := b.cfg
335 var err error
336 fontChanged := next.Bar.Font != old.Bar.Font
337 fonts := b.fonts
338 if fontChanged {
339 if fonts, err = findFonts(next.Bar.Font, b.log); err != nil {
340 b.log.Warn("config not applied", "err", err)
341 return
342 }
343 }
344 if next.Bar.Output != old.Bar.Output {
345 b.log.Warn("bar.output changed: restart the bar to move it", "from", old.Bar.Output, "to", next.Bar.Output)
346 next.Bar.Output = old.Bar.Output
347 }
348 b.cfg = next
349 b.fonts = fonts
350 if err = b.loadTheme(); err != nil { // the theme setting or the colors may have changed
351 b.log.Warn("theme not applied", "err", err)
352 }
353 b.syncModules(next.Module)
354 b.dirty = true
355 if next.Bar.Position != old.Bar.Position && b.surf != nil {
356 // The edge is part of how a layer surface is created: make a new one.
357 b.log.Info("moving the bar", "to", next.Bar.Position)
358 b.dropRenderer()
359 _, h, _ := b.surf.Size()
360 if err = b.surf.Close(); err != nil {
361 b.fail(err)
362 return
363 }
364 if err = b.createSurface(h); err != nil {
365 b.fail(err)
366 return
367 }
368 b.Stats.Recreated++
369 }
370 if fontChanged || next.Bar.Size != old.Bar.Size || next.Bar.Scale != old.Bar.Scale {
371 b.facePx = -1 // reconcile rebuilds the font, and the surface if its height changed
372 b.reconcile()
373 }
374 b.log.Info("config reloaded")
375}
376
377// Run connects to the compositor and serves until ctx ends or the surface is
378// closed. It returns the first fatal error.
379func (b *Bar) Run(ctx context.Context) (err error) {
380 b.conn, err = neferclient.Connect(ctx, b.display)
381 if err != nil {
382 return connectError(b.display, err)
383 }
384 defer func() {
385 err = errors.Join(err, b.shutdown())
386 b.log.Info("stats", "presented", b.Stats.Presented, "rebuilds", b.Stats.Rebuilds,
387 "recreated", b.Stats.Recreated, "no_slot", b.Stats.NoSlot, "dirty", b.Stats.Dirty, "module_wakes", b.Stats.ModuleWake)
388 }()
389 if err = b.createSurface(estimateHeight(b.cfg.Bar.Size * b.cfg.Bar.Scale)); err != nil {
390 return err
391 }
392 runCtx, cancel := context.WithCancel(ctx)
393 defer cancel()
394 b.runCtx, b.cancelRun = runCtx, cancel
395 b.watchTheme()
396 b.syncModules(b.cfg.Module)
397 if b.cfgPath != "" {
398 go func() {
399 if err := config.Watch(runCtx, b.cfgPath, b.cfg, b.cfgCh, b.log); err != nil {
400 b.log.Warn("config reload is off", "err", err)
401 }
402 }()
403 }
404 for !b.closed {
405 select {
406 case <-ctx.Done():
407 return nil
408 case <-b.conn.Wake():
409 if err = b.conn.Dispatch(b); err != nil {
410 return fmt.Errorf("dispatch: %w", err)
411 }
412 case <-b.modCh:
413 b.Stats.ModuleWake++
414 case next := <-b.cfgCh:
415 b.applyConfig(next)
416 case <-b.themeCh:
417 b.reloadTheme()
418 case dir := <-b.scriptCh:
419 b.restartScripts(dir)
420 case <-b.settleC:
421 b.settleC, b.settling = nil, false
422 b.reconcile()
423 }
424 if b.fatal != nil {
425 return b.fatal
426 }
427 if err = b.step(); err != nil {
428 return err
429 }
430 }
431 return nil
432}
433
434// findFonts resolves the four font files of a family and warns when
435// fontconfig had to substitute another family.
436func findFonts(family string, log *slog.Logger) (glyph.Paths, error) {
437 p, err := glyph.FindFonts(family)
438 if err != nil {
439 return p, err
440 }
441 if got := glyph.FamilyOf(family); got != "" && !strings.EqualFold(got, family) && !strings.HasPrefix(strings.ToLower(family), strings.ToLower(got)) {
442 log.Warn("font not found, fontconfig substituted another family: icons may be missing", "wanted", family, "got", got, "file", p[0])
443 }
444 return p, nil
445}
446
447// socketPath mirrors how neferclient resolves the socket, for error messages.
448func socketPath(display string) string {
449 if display == "" {
450 display = os.Getenv("WAYLAND_DISPLAY")
451 }
452 if display == "" {
453 display = "wayland-0"
454 }
455 if filepath.IsAbs(display) {
456 return display
457 }
458 return filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), display)
459}
460
461// connectError turns a failed dial into one actionable sentence.
462func connectError(display string, err error) error {
463 path := socketPath(display)
464 switch {
465 case errors.Is(err, os.ErrNotExist):
466 return fmt.Errorf("no Wayland socket at %s: is the compositor running? Set WAYLAND_DISPLAY, NEFERBAR_DISPLAY or -display", path)
467 case errors.Is(err, syscall.ECONNREFUSED):
468 return fmt.Errorf("the Wayland socket %s exists but refuses connections: stale socket or compositor not ready", path)
469 case errors.Is(err, os.ErrPermission):
470 return fmt.Errorf("not allowed to open the Wayland socket %s", path)
471 }
472 return err
473}
474
475// estimateHeight is the first request, before the real cell height is known.
476func estimateHeight(px float64) int32 { return int32(math.Ceil(px * 1.5)) }
477
478// edge is the layer-shell anchor for a bar.position value.
479func edge(position string) neferclient.Anchor {
480 if position == "bottom" {
481 return neferclient.AnchorBottom
482 }
483 return neferclient.AnchorTop
484}
485
486func (b *Bar) createSurface(h int32) error {
487 surf, err := b.conn.NewLayerSurface(neferclient.LayerConfig{
488 Output: b.cfg.Bar.Output,
489 Namespace: "neferbar",
490 Level: neferclient.LayerTop,
491 Anchors: edge(b.cfg.Bar.Position) | neferclient.AnchorLeft | neferclient.AnchorRight,
492 ExclusiveZone: h,
493 Height: h,
494 InputRects: []neferclient.Rect{}, // read only: click-through
495 })
496 if err != nil {
497 return fmt.Errorf("layer surface: %w", err)
498 }
499 b.surf, b.sid = surf, surf.ID()
500 b.mappedW = 0
501 b.fresh = true
502 b.configured, b.canPresent = false, false
503 return nil
504}
505
506// step reparses changed modules and draws when something is ready.
507func (b *Bar) step() error {
508 if b.lay != nil && b.lay.Update() {
509 b.dirty = true
510 b.Stats.Dirty++
511 }
512 return b.draw()
513}
514
515func (b *Bar) draw() error {
516 if !b.dirty || !b.canPresent || b.rend == nil || b.settling {
517 return nil
518 }
519 ok, err := b.rend.Draw(b.lay.Compose(), b.bg, &b.frame)
520 if err != nil {
521 return fmt.Errorf("draw: %w", err)
522 }
523 if !ok {
524 b.Stats.NoSlot++ // Released retries
525 return nil
526 }
527 if err = b.present(); err != nil {
528 return err
529 }
530 b.dirty, b.canPresent = false, false
531 b.Stats.Presented++
532 return nil
533}
534
535func (b *Bar) present() error {
536 f := &b.frame
537 if f.NewBuffer {
538 img := f.Image
539 buf := neferclient.Buffer{
540 Width: img.Width, Height: img.Height, FourCC: gpu.DRMFormatXRGB8888, Modifier: img.Modifier, PlaneCount: 1,
541 }
542 buf.Planes[0] = neferclient.Plane{FD: img.FD, Offset: img.Offset, Stride: img.Stride}
543 if err := b.surf.ImportBuffer(uint64(f.Slot), &buf); err != nil {
544 return fmt.Errorf("import buffer: %w", err)
545 }
546 if err := b.surf.ImportTimeline(uint64(f.Slot), f.ReleaseTimeline); err != nil {
547 return fmt.Errorf("import release timeline: %w", err)
548 }
549 if err := b.conn.WatchFD(f.ReleaseEventFD, uint64(f.Slot)); err != nil {
550 return fmt.Errorf("watch release fd: %w", err)
551 }
552 b.watched[f.Slot] = f.ReleaseEventFD
553 }
554 if !b.haveAcquire {
555 if err := b.surf.ImportTimeline(acquireTimelineID, b.rend.AcquireTimelineFD()); err != nil {
556 return fmt.Errorf("import acquire timeline: %w", err)
557 }
558 b.haveAcquire = true
559 }
560 err := b.surf.Present(&neferclient.Present{
561 Buffer: uint64(f.Slot),
562 AcquireTimeline: acquireTimelineID,
563 ReleaseTimeline: uint64(f.Slot),
564 AcquirePoint: f.AcquirePoint,
565 ReleasePoint: f.ReleasePoint,
566 Opaque: true,
567 })
568 if err != nil {
569 return fmt.Errorf("present: %w", err)
570 }
571 return nil
572}
573
574// reconcile brings the font, the layer surface height and the renderer in
575// line with the current scale and feedback. It runs on every event that can
576// change them and does nothing when everything already matches.
577func (b *Bar) reconcile() {
578 if !b.configured || b.fb == nil {
579 return
580 }
581 if b.dev == nil {
582 dev, err := gpu.Open(b.fb.MainDevice)
583 if err != nil {
584 b.fail(err)
585 return
586 }
587 node, err := syncobj.Open(filepath.Join("/dev/dri", fmt.Sprintf("renderD%d", dev.RenderMinor)))
588 if err != nil {
589 dev.Close()
590 b.fail(err)
591 return
592 }
593 b.dev, b.node = dev, node
594 }
595 w, h, scale := b.surf.Size()
596 px := b.cfg.Bar.Size * b.cfg.Bar.Scale * scale
597 if b.face == nil || b.facePx != px {
598 face, err := glyph.Load(b.fonts, px)
599 if err != nil {
600 b.fail(err)
601 return
602 }
603 b.dropRenderer()
604 b.face.Close()
605 b.face, b.facePx = face, px
606 }
607 // The bar is one cell high: logical height = ceil(cell / scale).
608 wantH := int32(math.Ceil(float64(b.face.CellH)/scale - 1e-9))
609 if h != wantH && b.askedH == wantH && b.askedScale == scale {
610 // Already asked for exactly this and got something else: the compositor
611 // clamps the height. Use what we have rather than asking forever.
612 b.log.Warn("the compositor gave another bar height than asked; using it", "asked", wantH, "got", h)
613 wantH = h
614 }
615 if h != wantH {
616 b.askedH, b.askedScale = wantH, scale
617 b.log.Info("recreating layer surface", "height", wantH, "scale", scale)
618 b.dropRenderer()
619 if err := b.surf.Close(); err != nil {
620 b.fail(err)
621 return
622 }
623 if err := b.createSurface(wantH); err != nil {
624 b.fail(err)
625 return
626 }
627 b.Stats.Recreated++
628 return
629 }
630 pw, ph, err := b.surf.PhysicalSize()
631 if err != nil {
632 b.fail(err)
633 return
634 }
635 if b.rend != nil && b.rend.W == pw && b.rend.H == ph {
636 return
637 }
638 if b.mappedW != 0 && w != b.mappedW {
639 // The logical width changed (a scale change on the output). NeferWL
640 // places a layer surface from the size it had when it was mapped and
641 // does not re-read it when only the buffer and viewport change, so the
642 // old width would stay: centered in a narrower output, the left part cut
643 // off. A new surface is mapped fresh and placed from its real size.
644 b.log.Info("recreating layer surface: the output width changed", "from", b.mappedW, "to", w)
645 b.dropRenderer()
646 if err := b.surf.Close(); err != nil {
647 b.fail(err)
648 return
649 }
650 if err := b.createSurface(h); err != nil {
651 b.fail(err)
652 return
653 }
654 b.Stats.Recreated++
655 return
656 }
657 b.dropRenderer()
658 if mod, err := b.chooseModifier(); err != nil {
659 b.fail(err)
660 return
661 } else {
662 b.mod = mod
663 }
664 rend, err := gpu.NewRenderer(b.dev, b.node, b.mod, b.face, pw, ph)
665 if err != nil {
666 b.fail(err)
667 return
668 }
669 b.rend, b.mappedW = rend, w
670 b.fresh = false
671 b.Stats.Rebuilds++
672 if b.lay == nil {
673 b.lay = layout.New(rend.Cols(), b.fg, b.bg, b.pal, b.mods)
674 } else {
675 b.lay.Resize(rend.Cols())
676 }
677 b.lay.Update()
678 b.dirty = true
679 b.log.Info("renderer ready", "px", pw, "py", ph, "scale", scale, "cell", fmt.Sprintf("%dx%d", b.face.CellW, b.face.CellH), "cols", rend.Cols())
680}
681
682func (b *Bar) chooseModifier() (gpu.Modifier, error) {
683 supported, err := b.dev.ExportableModifiers()
684 if err != nil {
685 return 0, err
686 }
687 pairs := make([]gpu.FormatPair, len(b.fb.Formats))
688 for i, f := range b.fb.Formats {
689 pairs[i] = gpu.FormatPair{FourCC: f.FourCC, Modifier: f.Modifier}
690 }
691 return gpu.ChooseModifier(pairs, supported)
692}
693
694// dropRenderer retires the renderer and every wl object that refers to its
695// images.
696func (b *Bar) dropRenderer() {
697 if b.rend == nil {
698 return
699 }
700 for i, fd := range b.watched {
701 if fd >= 0 {
702 _ = b.conn.UnwatchFD(fd)
703 b.watched[i] = -1
704 }
705 }
706 if err := b.surf.DestroyImports(); err != nil {
707 b.log.Warn("destroy imports", "err", err)
708 }
709 b.haveAcquire = false
710 b.rend.Close()
711 b.rend = nil
712}
713
714func (b *Bar) fail(err error) {
715 if b.fatal == nil {
716 b.fatal = err
717 }
718}
719
720func (b *Bar) shutdown() error {
721 var errs []error
722 // Stop the scripts first: cancel asks them to end, Wait gives them time to,
723 // so none outlives the bar.
724 if b.cancelRun != nil {
725 b.cancelRun()
726 }
727 for _, r := range b.runners {
728 r.cancel()
729 }
730 for _, r := range b.runners {
731 r.m.Wait(3 * time.Second)
732 }
733 b.dropRenderer()
734 b.face.Close()
735 if b.node != nil {
736 errs = append(errs, b.node.Close())
737 }
738 b.dev.Close()
739 if b.conn != nil {
740 errs = append(errs, b.conn.Close())
741 }
742 return errors.Join(errs...)
743}
744
745// settleDelay is how long the bar waits for the second half of a scale or size
746// change before it rebuilds.
747const settleDelay = 60 * time.Millisecond
748
749// geometryChanged is called when the compositor reports a new scale or size.
750// The first report builds at once, because there is nothing on screen yet.
751// Later ones wait for the dust to settle, and nothing is drawn meanwhile.
752func (b *Bar) geometryChanged() {
753 if b.rend == nil && !b.fresh {
754 b.reconcile()
755 return
756 }
757 b.settling = true
758 b.settleC = time.After(settleDelay)
759}
760
761// Handler methods. Events of a replaced surface are ignored.
762
763// Configure marks the surface presentable and rebuilds as needed.
764func (b *Bar) Configure(id neferclient.SurfaceID, _, _ int32) {
765 if id != b.sid {
766 return
767 }
768 if !b.configured {
769 b.canPresent = true
770 }
771 b.configured = true
772 b.geometryChanged()
773}
774
775// Scale follows the monitor's preferred scale.
776func (b *Bar) Scale(id neferclient.SurfaceID, _ float64) {
777 if id == b.sid {
778 b.geometryChanged()
779 }
780}
781
782// FeedbackDone records the compositor's formats; a changed main device is not
783// followed.
784func (b *Bar) FeedbackDone(id neferclient.SurfaceID) {
785 if id != b.sid {
786 return
787 }
788 fb := b.surf.Feedback()
789 if b.fb != nil && b.fb.MainDevice != fb.MainDevice {
790 b.log.Warn("dmabuf main device changed; restart the bar to follow it")
791 return
792 }
793 if b.fb.Equal(fb) {
794 return
795 }
796 b.fb = fb.Clone()
797 b.dropRenderer() // formats changed: images must be rebuilt
798 b.reconcile()
799}
800
801// Frame allows the next Present.
802func (b *Bar) Frame(id neferclient.SurfaceID) {
803 if id == b.sid {
804 b.canPresent = true
805 }
806}
807
808// Closed ends the loop when the compositor closes the surface.
809func (b *Bar) Closed(id neferclient.SurfaceID) {
810 if id == b.sid {
811 b.closed = true
812 }
813}
814
815// FDReady hands a slot back once the compositor released it.
816func (b *Bar) FDReady(id uint64) {
817 if b.rend == nil {
818 return
819 }
820 if err := b.rend.Released(int(id)); err != nil {
821 b.fail(err)
822 }
823}
824
825// Error logs non-fatal connection errors.
826func (b *Bar) Error(err error) { b.log.Warn("wayland", "err", err) }
827
828// Unused events.
829func (b *Bar) OutputAdded(*neferclient.Output) {}
830func (b *Bar) OutputRemoved(uint32) {}
831func (b *Bar) Locked() {}
832func (b *Bar) LockFinished() {}
833func (b *Bar) Pointer(*neferclient.PointerEvent) {}
834func (b *Bar) Key(*neferclient.KeyEvent) {}
835func (b *Bar) KeyboardFocus(neferclient.SurfaceID, bool) {}
836func (b *Bar) SecretChanged(int) {}