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