fix(bar): stop scripts and their children on exit, read relative themes from the config dir, and stop recreate loops
- commit
- 6a789b466bc4b7aaf18c3e7ec539f9daee313f21
- author
- brice · · 2026-10-05 16:00 UTC
- parent
- 44d4d49
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +67 -24
M cmd/neferbar/main.go +6 -2
| @@ -49,11 +49,10 @@ func run(ctx context.Context, log *slog.Logger, cfgPath, display, pprofAddr stri | |||
| 49 | 49 | return err | |
| 50 | 50 | } | |
| 51 | 51 | } | |
| 52 | - | b, err := bar.New(cfg, log, display) | |
| 52 | + | b, err := bar.New(cfg, cfgPath, log, display) | |
| 53 | 53 | if err != nil { | |
| 54 | 54 | return err | |
| 55 | 55 | } | |
| 56 | - | b.WatchConfig(cfgPath) | |
| 57 | 56 | if memStats > 0 { | |
| 58 | 57 | go logMemStats(ctx, log, memStats) | |
| 59 | 58 | } | |
| @@ -73,6 +72,11 @@ func servePprof(ctx context.Context, log *slog.Logger, addr string) error { | |||
| 73 | 72 | if err != nil { | |
| 74 | 73 | return fmt.Errorf("pprof listen: %w", err) | |
| 75 | 74 | } | |
| 75 | + | // "localhost" is resolved by the system: trust the address actually bound. | |
| 76 | + | if tcp, ok := ln.Addr().(*net.TCPAddr); !ok || !tcp.IP.IsLoopback() { | |
| 77 | + | _ = ln.Close() | |
| 78 | + | return fmt.Errorf("pprof address %q is not loopback; refusing to serve profiles on it", addr) | |
| 79 | + | } | |
| 76 | 80 | mux := http.NewServeMux() | |
| 77 | 81 | mux.HandleFunc("/debug/pprof/", pprof.Index) | |
| 78 | 82 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) | |
| @@ -49,11 +49,10 @@ func run(ctx context.Context, log *slog.Logger, cfgPath, display, pprofAddr stri | |||||
| 49 | return err | 49 | return err | ||
| 50 | } | 50 | } | ||
| 51 | } | 51 | } | ||
| 52 | - | b, err := bar.New(cfg, log, display) | 52 | + | b, err := bar.New(cfg, cfgPath, log, display) |
| 53 | if err != nil { | 53 | if err != nil { | ||
| 54 | return err | 54 | return err | ||
| 55 | } | 55 | } | ||
| 56 | - | b.WatchConfig(cfgPath) | |||
| 57 | if memStats > 0 { | 56 | if memStats > 0 { | ||
| 58 | go logMemStats(ctx, log, memStats) | 57 | go logMemStats(ctx, log, memStats) | ||
| 59 | } | 58 | } | ||
| @@ -73,6 +72,11 @@ func servePprof(ctx context.Context, log *slog.Logger, addr string) error { | |||||
| 73 | if err != nil { | 72 | if err != nil { | ||
| 74 | return fmt.Errorf("pprof listen: %w", err) | 73 | return fmt.Errorf("pprof listen: %w", err) | ||
| 75 | } | 74 | } | ||
| 75 | + | // "localhost" is resolved by the system: trust the address actually bound. | |||
| 76 | + | if tcp, ok := ln.Addr().(*net.TCPAddr); !ok || !tcp.IP.IsLoopback() { | |||
| 77 | + | _ = ln.Close() | |||
| 78 | + | return fmt.Errorf("pprof address %q is not loopback; refusing to serve profiles on it", addr) | |||
| 79 | + | } | |||
| 76 | mux := http.NewServeMux() | 80 | mux := http.NewServeMux() | ||
| 77 | mux.HandleFunc("/debug/pprof/", pprof.Index) | 81 | mux.HandleFunc("/debug/pprof/", pprof.Index) | ||
| 78 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) | 82 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) | ||
M internal/bar/bar.go +58 -20
| @@ -75,13 +75,14 @@ type Bar struct { | |||
| 75 | 75 | frame gpu.Frame | |
| 76 | 76 | watched [gpu.SlotCount]int // watched eventfds, -1 when none | |
| 77 | 77 | ||
| 78 | - | runners []runner | |
| 79 | - | mods []*module.Module // the modules of runners, in config order | |
| 80 | - | runCtx context.Context | |
| 81 | - | lay *layout.Layout | |
| 82 | - | modCh chan struct{} | |
| 83 | - | cfgPath string | |
| 84 | - | cfgCh chan config.Config | |
| 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 | |
| 85 | 86 | ||
| 86 | 87 | theme theme.Theme | |
| 87 | 88 | themeCh chan struct{} | |
| @@ -96,6 +97,16 @@ type Bar struct { | |||
| 96 | 97 | settling bool | |
| 97 | 98 | settleC <-chan time.Time | |
| 98 | 99 | ||
| 100 | + | // fresh is true from the moment a layer surface is created until a renderer | |
| 101 | + | // exists for it. A new surface reports scale 1 until the compositor says | |
| 102 | + | // otherwise, so its first geometry is not trusted until the events settle. | |
| 103 | + | fresh bool | |
| 104 | + | // asked is the last (height, scale) a surface was recreated for. Asking for | |
| 105 | + | // the same pair twice means the compositor will not give that height, and | |
| 106 | + | // the bar takes what it gets instead of looping. | |
| 107 | + | askedH int32 | |
| 108 | + | askedScale float64 | |
| 109 | + | ||
| 99 | 110 | // mappedW is the logical width the current layer surface was first drawn at. | |
| 100 | 111 | // It resets to 0 when the surface is recreated. | |
| 101 | 112 | mappedW int32 | |
| @@ -104,10 +115,12 @@ type Bar struct { | |||
| 104 | 115 | closed bool | |
| 105 | 116 | } | |
| 106 | 117 | ||
| 107 | - | // New builds a bar from a validated config. | |
| 108 | - | // display names the Wayland socket; empty uses $WAYLAND_DISPLAY. | |
| 109 | - | func New(cfg config.Config, log *slog.Logger, display string) (*Bar, error) { | |
| 110 | - | b := &Bar{cfg: cfg, log: log, display: display, modCh: make(chan struct{}, 1), cfgCh: make(chan config.Config, 1), | |
| 118 | + | // New builds a bar from a validated config. cfgPath is the file the config came | |
| 119 | + | // from: relative theme paths are read from its directory, and the bar reloads | |
| 120 | + | // it when it changes (empty: no reload). display names the Wayland socket; | |
| 121 | + | // empty uses $WAYLAND_DISPLAY. | |
| 122 | + | func New(cfg config.Config, cfgPath string, log *slog.Logger, display string) (*Bar, error) { | |
| 123 | + | b := &Bar{cfg: cfg, cfgPath: cfgPath, log: log, display: display, modCh: make(chan struct{}, 1), cfgCh: make(chan config.Config, 1), | |
| 111 | 124 | themeCh: make(chan struct{}, 1)} | |
| 112 | 125 | var err error | |
| 113 | 126 | if err = b.loadTheme(); err != nil { | |
| @@ -145,7 +158,7 @@ func (b *Bar) loadTheme() error { | |||
| 145 | 158 | } | |
| 146 | 159 | th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv()) | |
| 147 | 160 | if err != nil { | |
| 148 | - | b.log.Warn("theme not found; using the built-in colors", "theme", b.cfg.Bar.Theme, "err", err) | |
| 161 | + | b.log.Warn("using the built-in colors: "+err.Error(), "theme", b.cfg.Bar.Theme) | |
| 149 | 162 | } else { | |
| 150 | 163 | b.log.Info("theme", "source", th.Source) | |
| 151 | 164 | } | |
| @@ -178,7 +191,11 @@ func (b *Bar) watchTheme() { | |||
| 178 | 191 | ctx, cancel := context.WithCancel(b.runCtx) | |
| 179 | 192 | b.themeCancel = cancel | |
| 180 | 193 | for _, f := range b.theme.Files { | |
| 181 | - | go func() { _ = fswatch.Watch(ctx, f, 150*time.Millisecond, b.themeCh) }() | |
| 194 | + | go func() { | |
| 195 | + | if err := fswatch.Watch(ctx, f, 150*time.Millisecond, b.themeCh); err != nil { | |
| 196 | + | b.log.Warn("cannot watch a theme file; edits to it need a restart", "file", f, "err", err) | |
| 197 | + | } | |
| 198 | + | }() | |
| 182 | 199 | } | |
| 183 | 200 | } | |
| 184 | 201 | ||
| @@ -194,6 +211,11 @@ func (b *Bar) reloadTheme() { | |||
| 194 | 211 | return | |
| 195 | 212 | } | |
| 196 | 213 | if th.Equal(b.theme) { | |
| 214 | + | // Same colors, but the files that make them up may have changed. | |
| 215 | + | if !slices.Equal(th.Files, b.theme.Files) { | |
| 216 | + | b.theme.Files = th.Files | |
| 217 | + | b.watchTheme() | |
| 218 | + | } | |
| 197 | 219 | return | |
| 198 | 220 | } | |
| 199 | 221 | b.log.Info("theme changed", "source", th.Source) | |
| @@ -201,10 +223,6 @@ func (b *Bar) reloadTheme() { | |||
| 201 | 223 | b.syncModules(b.cfg.Module) // scripts get the new colors in their environment | |
| 202 | 224 | } | |
| 203 | 225 | ||
| 204 | - | // WatchConfig makes Run reload the config file at path when it changes. Call | |
| 205 | - | // it before Run. | |
| 206 | - | func (b *Bar) WatchConfig(path string) { b.cfgPath = path } | |
| 207 | - | ||
| 208 | 226 | // runner is one started module and how to stop it. | |
| 209 | 227 | type runner struct { | |
| 210 | 228 | cfg config.Module | |
| @@ -236,7 +254,7 @@ func (b *Bar) syncModules(want []config.Module) { | |||
| 236 | 254 | ctx, cancel := context.WithCancel(b.runCtx) | |
| 237 | 255 | m := module.New(w.Name, zones[w.Zone], w.Exec, b.modCh, b.log) | |
| 238 | 256 | m.Env = b.env | |
| 239 | - | go m.Run(ctx) | |
| 257 | + | m.Start(ctx) | |
| 240 | 258 | next = append(next, runner{cfg: w, env: b.env, m: m, cancel: cancel}) | |
| 241 | 259 | } | |
| 242 | 260 | for _, r := range old { | |
| @@ -299,7 +317,7 @@ func (b *Bar) Run(ctx context.Context) (err error) { | |||
| 299 | 317 | } | |
| 300 | 318 | runCtx, cancel := context.WithCancel(ctx) | |
| 301 | 319 | defer cancel() | |
| 302 | - | b.runCtx = runCtx | |
| 320 | + | b.runCtx, b.cancelRun = runCtx, cancel | |
| 303 | 321 | b.watchTheme() | |
| 304 | 322 | b.syncModules(b.cfg.Module) | |
| 305 | 323 | if b.cfgPath != "" { | |
| @@ -396,6 +414,7 @@ func (b *Bar) createSurface(h int32) error { | |||
| 396 | 414 | } | |
| 397 | 415 | b.surf, b.sid = surf, surf.ID() | |
| 398 | 416 | b.mappedW = 0 | |
| 417 | + | b.fresh = true | |
| 399 | 418 | b.configured, b.canPresent = false, false | |
| 400 | 419 | return nil | |
| 401 | 420 | } | |
| @@ -503,7 +522,14 @@ func (b *Bar) reconcile() { | |||
| 503 | 522 | } | |
| 504 | 523 | // The bar is one cell high: logical height = ceil(cell / scale). | |
| 505 | 524 | wantH := int32(math.Ceil(float64(b.face.CellH)/scale - 1e-9)) | |
| 525 | + | if h != wantH && b.askedH == wantH && b.askedScale == scale { | |
| 526 | + | // Already asked for exactly this and got something else: the compositor | |
| 527 | + | // clamps the height. Use what we have rather than asking forever. | |
| 528 | + | b.log.Warn("the compositor gave another bar height than asked; using it", "asked", wantH, "got", h) | |
| 529 | + | wantH = h | |
| 530 | + | } | |
| 506 | 531 | if h != wantH { | |
| 532 | + | b.askedH, b.askedScale = wantH, scale | |
| 507 | 533 | b.log.Info("recreating layer surface", "height", wantH, "scale", scale) | |
| 508 | 534 | b.dropRenderer() | |
| 509 | 535 | if err := b.surf.Close(); err != nil { | |
| @@ -557,6 +583,7 @@ func (b *Bar) reconcile() { | |||
| 557 | 583 | return | |
| 558 | 584 | } | |
| 559 | 585 | b.rend, b.mappedW = rend, w | |
| 586 | + | b.fresh = false | |
| 560 | 587 | b.Stats.Rebuilds++ | |
| 561 | 588 | if b.lay == nil { | |
| 562 | 589 | b.lay = layout.New(rend.Cols(), b.fg, b.bg, b.pal, b.mods) | |
| @@ -608,6 +635,17 @@ func (b *Bar) fail(err error) { | |||
| 608 | 635 | ||
| 609 | 636 | func (b *Bar) shutdown() error { | |
| 610 | 637 | var errs []error | |
| 638 | + | // Stop the scripts first: cancel asks them to end, Wait gives them time to, | |
| 639 | + | // so none outlives the bar. | |
| 640 | + | if b.cancelRun != nil { | |
| 641 | + | b.cancelRun() | |
| 642 | + | } | |
| 643 | + | for _, r := range b.runners { | |
| 644 | + | r.cancel() | |
| 645 | + | } | |
| 646 | + | for _, r := range b.runners { | |
| 647 | + | r.m.Wait(3 * time.Second) | |
| 648 | + | } | |
| 611 | 649 | b.dropRenderer() | |
| 612 | 650 | b.face.Close() | |
| 613 | 651 | if b.node != nil { | |
| @@ -628,7 +666,7 @@ const settleDelay = 60 * time.Millisecond | |||
| 628 | 666 | // The first report builds at once, because there is nothing on screen yet. | |
| 629 | 667 | // Later ones wait for the dust to settle, and nothing is drawn meanwhile. | |
| 630 | 668 | func (b *Bar) geometryChanged() { | |
| 631 | - | if b.rend == nil { | |
| 669 | + | if b.rend == nil && !b.fresh { | |
| 632 | 670 | b.reconcile() | |
| 633 | 671 | return | |
| 634 | 672 | } | |
| @@ -75,13 +75,14 @@ type Bar struct { | |||||
| 75 | frame gpu.Frame | 75 | frame gpu.Frame | ||
| 76 | watched [gpu.SlotCount]int // watched eventfds, -1 when none | 76 | watched [gpu.SlotCount]int // watched eventfds, -1 when none | ||
| 77 | 77 | ||||
| 78 | - | runners []runner | 78 | + | runners []runner |
| 79 | - | mods []*module.Module // the modules of runners, in config order | 79 | + | mods []*module.Module // the modules of runners, in config order |
| 80 | - | runCtx context.Context | 80 | + | runCtx context.Context |
| 81 | - | lay *layout.Layout | 81 | + | cancelRun context.CancelFunc |
| 82 | - | modCh chan struct{} | 82 | + | lay *layout.Layout |
| 83 | - | cfgPath string | 83 | + | modCh chan struct{} |
| 84 | - | cfgCh chan config.Config | 84 | + | cfgPath string |
| 85 | + | cfgCh chan config.Config | |||
| 85 | 86 | ||||
| 86 | theme theme.Theme | 87 | theme theme.Theme | ||
| 87 | themeCh chan struct{} | 88 | themeCh chan struct{} | ||
| @@ -96,6 +97,16 @@ type Bar struct { | |||||
| 96 | settling bool | 97 | settling bool | ||
| 97 | settleC <-chan time.Time | 98 | settleC <-chan time.Time | ||
| 98 | 99 | ||||
| 100 | + | // fresh is true from the moment a layer surface is created until a renderer | |||
| 101 | + | // exists for it. A new surface reports scale 1 until the compositor says | |||
| 102 | + | // otherwise, so its first geometry is not trusted until the events settle. | |||
| 103 | + | fresh bool | |||
| 104 | + | // asked is the last (height, scale) a surface was recreated for. Asking for | |||
| 105 | + | // the same pair twice means the compositor will not give that height, and | |||
| 106 | + | // the bar takes what it gets instead of looping. | |||
| 107 | + | askedH int32 | |||
| 108 | + | askedScale float64 | |||
| 109 | + | ||||
| 99 | // mappedW is the logical width the current layer surface was first drawn at. | 110 | // mappedW is the logical width the current layer surface was first drawn at. | ||
| 100 | // It resets to 0 when the surface is recreated. | 111 | // It resets to 0 when the surface is recreated. | ||
| 101 | mappedW int32 | 112 | mappedW int32 | ||
| @@ -104,10 +115,12 @@ type Bar struct { | |||||
| 104 | closed bool | 115 | closed bool | ||
| 105 | } | 116 | } | ||
| 106 | 117 | ||||
| 107 | - | // New builds a bar from a validated config. | 118 | + | // New builds a bar from a validated config. cfgPath is the file the config came |
| 108 | - | // display names the Wayland socket; empty uses $WAYLAND_DISPLAY. | 119 | + | // from: relative theme paths are read from its directory, and the bar reloads |
| 109 | - | func New(cfg config.Config, log *slog.Logger, display string) (*Bar, error) { | 120 | + | // it when it changes (empty: no reload). display names the Wayland socket; |
| 110 | - | b := &Bar{cfg: cfg, log: log, display: display, modCh: make(chan struct{}, 1), cfgCh: make(chan config.Config, 1), | 121 | + | // empty uses $WAYLAND_DISPLAY. |
| 122 | + | func New(cfg config.Config, cfgPath string, log *slog.Logger, display string) (*Bar, error) { | |||
| 123 | + | b := &Bar{cfg: cfg, cfgPath: cfgPath, log: log, display: display, modCh: make(chan struct{}, 1), cfgCh: make(chan config.Config, 1), | |||
| 111 | themeCh: make(chan struct{}, 1)} | 124 | themeCh: make(chan struct{}, 1)} | ||
| 112 | var err error | 125 | var err error | ||
| 113 | if err = b.loadTheme(); err != nil { | 126 | if err = b.loadTheme(); err != nil { | ||
| @@ -145,7 +158,7 @@ func (b *Bar) loadTheme() error { | |||||
| 145 | } | 158 | } | ||
| 146 | th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv()) | 159 | th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv()) | ||
| 147 | if err != nil { | 160 | if err != nil { | ||
| 148 | - | b.log.Warn("theme not found; using the built-in colors", "theme", b.cfg.Bar.Theme, "err", err) | 161 | + | b.log.Warn("using the built-in colors: "+err.Error(), "theme", b.cfg.Bar.Theme) |
| 149 | } else { | 162 | } else { | ||
| 150 | b.log.Info("theme", "source", th.Source) | 163 | b.log.Info("theme", "source", th.Source) | ||
| 151 | } | 164 | } | ||
| @@ -178,7 +191,11 @@ func (b *Bar) watchTheme() { | |||||
| 178 | ctx, cancel := context.WithCancel(b.runCtx) | 191 | ctx, cancel := context.WithCancel(b.runCtx) | ||
| 179 | b.themeCancel = cancel | 192 | b.themeCancel = cancel | ||
| 180 | for _, f := range b.theme.Files { | 193 | for _, f := range b.theme.Files { | ||
| 181 | - | go func() { _ = fswatch.Watch(ctx, f, 150*time.Millisecond, b.themeCh) }() | 194 | + | go func() { |
| 195 | + | if err := fswatch.Watch(ctx, f, 150*time.Millisecond, b.themeCh); err != nil { | |||
| 196 | + | b.log.Warn("cannot watch a theme file; edits to it need a restart", "file", f, "err", err) | |||
| 197 | + | } | |||
| 198 | + | }() | |||
| 182 | } | 199 | } | ||
| 183 | } | 200 | } | ||
| 184 | 201 | ||||
| @@ -194,6 +211,11 @@ func (b *Bar) reloadTheme() { | |||||
| 194 | return | 211 | return | ||
| 195 | } | 212 | } | ||
| 196 | if th.Equal(b.theme) { | 213 | if th.Equal(b.theme) { | ||
| 214 | + | // Same colors, but the files that make them up may have changed. | |||
| 215 | + | if !slices.Equal(th.Files, b.theme.Files) { | |||
| 216 | + | b.theme.Files = th.Files | |||
| 217 | + | b.watchTheme() | |||
| 218 | + | } | |||
| 197 | return | 219 | return | ||
| 198 | } | 220 | } | ||
| 199 | b.log.Info("theme changed", "source", th.Source) | 221 | b.log.Info("theme changed", "source", th.Source) | ||
| @@ -201,10 +223,6 @@ func (b *Bar) reloadTheme() { | |||||
| 201 | b.syncModules(b.cfg.Module) // scripts get the new colors in their environment | 223 | b.syncModules(b.cfg.Module) // scripts get the new colors in their environment | ||
| 202 | } | 224 | } | ||
| 203 | 225 | ||||
| 204 | - | // WatchConfig makes Run reload the config file at path when it changes. Call | |||
| 205 | - | // it before Run. | |||
| 206 | - | func (b *Bar) WatchConfig(path string) { b.cfgPath = path } | |||
| 207 | - | ||||
| 208 | // runner is one started module and how to stop it. | 226 | // runner is one started module and how to stop it. | ||
| 209 | type runner struct { | 227 | type runner struct { | ||
| 210 | cfg config.Module | 228 | cfg config.Module | ||
| @@ -236,7 +254,7 @@ func (b *Bar) syncModules(want []config.Module) { | |||||
| 236 | ctx, cancel := context.WithCancel(b.runCtx) | 254 | ctx, cancel := context.WithCancel(b.runCtx) | ||
| 237 | m := module.New(w.Name, zones[w.Zone], w.Exec, b.modCh, b.log) | 255 | m := module.New(w.Name, zones[w.Zone], w.Exec, b.modCh, b.log) | ||
| 238 | m.Env = b.env | 256 | m.Env = b.env | ||
| 239 | - | go m.Run(ctx) | 257 | + | m.Start(ctx) |
| 240 | next = append(next, runner{cfg: w, env: b.env, m: m, cancel: cancel}) | 258 | next = append(next, runner{cfg: w, env: b.env, m: m, cancel: cancel}) | ||
| 241 | } | 259 | } | ||
| 242 | for _, r := range old { | 260 | for _, r := range old { | ||
| @@ -299,7 +317,7 @@ func (b *Bar) Run(ctx context.Context) (err error) { | |||||
| 299 | } | 317 | } | ||
| 300 | runCtx, cancel := context.WithCancel(ctx) | 318 | runCtx, cancel := context.WithCancel(ctx) | ||
| 301 | defer cancel() | 319 | defer cancel() | ||
| 302 | - | b.runCtx = runCtx | 320 | + | b.runCtx, b.cancelRun = runCtx, cancel |
| 303 | b.watchTheme() | 321 | b.watchTheme() | ||
| 304 | b.syncModules(b.cfg.Module) | 322 | b.syncModules(b.cfg.Module) | ||
| 305 | if b.cfgPath != "" { | 323 | if b.cfgPath != "" { | ||
| @@ -396,6 +414,7 @@ func (b *Bar) createSurface(h int32) error { | |||||
| 396 | } | 414 | } | ||
| 397 | b.surf, b.sid = surf, surf.ID() | 415 | b.surf, b.sid = surf, surf.ID() | ||
| 398 | b.mappedW = 0 | 416 | b.mappedW = 0 | ||
| 417 | + | b.fresh = true | |||
| 399 | b.configured, b.canPresent = false, false | 418 | b.configured, b.canPresent = false, false | ||
| 400 | return nil | 419 | return nil | ||
| 401 | } | 420 | } | ||
| @@ -503,7 +522,14 @@ func (b *Bar) reconcile() { | |||||
| 503 | } | 522 | } | ||
| 504 | // The bar is one cell high: logical height = ceil(cell / scale). | 523 | // The bar is one cell high: logical height = ceil(cell / scale). | ||
| 505 | wantH := int32(math.Ceil(float64(b.face.CellH)/scale - 1e-9)) | 524 | wantH := int32(math.Ceil(float64(b.face.CellH)/scale - 1e-9)) | ||
| 525 | + | if h != wantH && b.askedH == wantH && b.askedScale == scale { | |||
| 526 | + | // Already asked for exactly this and got something else: the compositor | |||
| 527 | + | // clamps the height. Use what we have rather than asking forever. | |||
| 528 | + | b.log.Warn("the compositor gave another bar height than asked; using it", "asked", wantH, "got", h) | |||
| 529 | + | wantH = h | |||
| 530 | + | } | |||
| 506 | if h != wantH { | 531 | if h != wantH { | ||
| 532 | + | b.askedH, b.askedScale = wantH, scale | |||
| 507 | b.log.Info("recreating layer surface", "height", wantH, "scale", scale) | 533 | b.log.Info("recreating layer surface", "height", wantH, "scale", scale) | ||
| 508 | b.dropRenderer() | 534 | b.dropRenderer() | ||
| 509 | if err := b.surf.Close(); err != nil { | 535 | if err := b.surf.Close(); err != nil { | ||
| @@ -557,6 +583,7 @@ func (b *Bar) reconcile() { | |||||
| 557 | return | 583 | return | ||
| 558 | } | 584 | } | ||
| 559 | b.rend, b.mappedW = rend, w | 585 | b.rend, b.mappedW = rend, w | ||
| 586 | + | b.fresh = false | |||
| 560 | b.Stats.Rebuilds++ | 587 | b.Stats.Rebuilds++ | ||
| 561 | if b.lay == nil { | 588 | if b.lay == nil { | ||
| 562 | b.lay = layout.New(rend.Cols(), b.fg, b.bg, b.pal, b.mods) | 589 | b.lay = layout.New(rend.Cols(), b.fg, b.bg, b.pal, b.mods) | ||
| @@ -608,6 +635,17 @@ func (b *Bar) fail(err error) { | |||||
| 608 | 635 | ||||
| 609 | func (b *Bar) shutdown() error { | 636 | func (b *Bar) shutdown() error { | ||
| 610 | var errs []error | 637 | var errs []error | ||
| 638 | + | // Stop the scripts first: cancel asks them to end, Wait gives them time to, | |||
| 639 | + | // so none outlives the bar. | |||
| 640 | + | if b.cancelRun != nil { | |||
| 641 | + | b.cancelRun() | |||
| 642 | + | } | |||
| 643 | + | for _, r := range b.runners { | |||
| 644 | + | r.cancel() | |||
| 645 | + | } | |||
| 646 | + | for _, r := range b.runners { | |||
| 647 | + | r.m.Wait(3 * time.Second) | |||
| 648 | + | } | |||
| 611 | b.dropRenderer() | 649 | b.dropRenderer() | ||
| 612 | b.face.Close() | 650 | b.face.Close() | ||
| 613 | if b.node != nil { | 651 | if b.node != nil { | ||
| @@ -628,7 +666,7 @@ const settleDelay = 60 * time.Millisecond | |||||
| 628 | // The first report builds at once, because there is nothing on screen yet. | 666 | // The first report builds at once, because there is nothing on screen yet. | ||
| 629 | // Later ones wait for the dust to settle, and nothing is drawn meanwhile. | 667 | // Later ones wait for the dust to settle, and nothing is drawn meanwhile. | ||
| 630 | func (b *Bar) geometryChanged() { | 668 | func (b *Bar) geometryChanged() { | ||
| 631 | - | if b.rend == nil { | 669 | + | if b.rend == nil && !b.fresh { |
| 632 | b.reconcile() | 670 | b.reconcile() | ||
| 633 | return | 671 | return | ||
| 634 | } | 672 | } | ||
M scripts/headless.sh +3 -2
| @@ -1,19 +1,20 @@ | |||
| 1 | 1 | #!/bin/bash | |
| 2 | 2 | # Run neferbar against a nested headless NeferWL with a clean environment. | |
| 3 | 3 | # usage: scripts/headless.sh <config> <seconds> [neferbar args...] | |
| 4 | - | # env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots), | |
| 4 | + | # env: TERMINAL and NEFERWL_STATE are passed through; SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots), | |
| 5 | 5 | # DISPLAY_VAR=NEFERBAR_DISPLAY (pass the socket that way), NEFERBAR=binary, LOGLINES=n (compositor log lines to show). | |
| 6 | 6 | set -eu | |
| 7 | 7 | cfg=$1; secs=$2; shift 2 | |
| 8 | 8 | out=${OUT:-/tmp/neferbar-shots} | |
| 9 | 9 | root=$(mktemp -d) | |
| 10 | 10 | mkdir -p "$root"/{run,config/neferwl,data,state} "$out" | |
| 11 | + | if [ -n "${TERMINAL:-}" ]; then ln -s "$HOME/.config/$(basename "${TERMINAL%% *}")" "$root/config/$(basename "${TERMINAL%% *}")" 2>/dev/null; fi | |
| 11 | 12 | chmod 700 "$root/run" | |
| 12 | 13 | rm -f "$out"/*.png | |
| 13 | 14 | if [ -n "${SCALE:-}" ]; then | |
| 14 | 15 | printf 'output.HEADLESS-1.scale = %s\n' "$SCALE" >"$root/config/neferwl/config" | |
| 15 | 16 | fi | |
| 16 | - | clean=(env -i PATH="$PATH" HOME="$root" XDG_RUNTIME_DIR="$root/run" XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state" ${NEFERWL_STATE:+NEFERWL_STATE="$NEFERWL_STATE"}) | |
| 17 | + | clean=(env -i PATH="$PATH" HOME="$root" XDG_RUNTIME_DIR="$root/run" XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state" ${NEFERWL_STATE:+NEFERWL_STATE="$NEFERWL_STATE"} ${TERMINAL:+TERMINAL="$TERMINAL"}) | |
| 17 | 18 | "${clean[@]}" neferwl --backend=headless --no-terminal --no-xwayland --size "${SIZE:-800x200}" --timeout "$((secs + 6))s" --screenshot "$out" >"$root/neferwl.log" 2>&1 & | |
| 18 | 19 | comp=$! | |
| 19 | 20 | trap 'kill $comp 2>/dev/null || true; wait $comp 2>/dev/null || true' EXIT | |
| @@ -1,19 +1,20 @@ | |||||
| 1 | #!/bin/bash | 1 | #!/bin/bash | ||
| 2 | # Run neferbar against a nested headless NeferWL with a clean environment. | 2 | # Run neferbar against a nested headless NeferWL with a clean environment. | ||
| 3 | # usage: scripts/headless.sh <config> <seconds> [neferbar args...] | 3 | # usage: scripts/headless.sh <config> <seconds> [neferbar args...] | ||
| 4 | - | # env: SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots), | 4 | + | # env: TERMINAL and NEFERWL_STATE are passed through; SIZE=WxH (output size), SCALE=1.5 (output scale), OUT=dir (screenshots), |
| 5 | # DISPLAY_VAR=NEFERBAR_DISPLAY (pass the socket that way), NEFERBAR=binary, LOGLINES=n (compositor log lines to show). | 5 | # DISPLAY_VAR=NEFERBAR_DISPLAY (pass the socket that way), NEFERBAR=binary, LOGLINES=n (compositor log lines to show). | ||
| 6 | set -eu | 6 | set -eu | ||
| 7 | cfg=$1; secs=$2; shift 2 | 7 | cfg=$1; secs=$2; shift 2 | ||
| 8 | out=${OUT:-/tmp/neferbar-shots} | 8 | out=${OUT:-/tmp/neferbar-shots} | ||
| 9 | root=$(mktemp -d) | 9 | root=$(mktemp -d) | ||
| 10 | mkdir -p "$root"/{run,config/neferwl,data,state} "$out" | 10 | mkdir -p "$root"/{run,config/neferwl,data,state} "$out" | ||
| 11 | + | if [ -n "${TERMINAL:-}" ]; then ln -s "$HOME/.config/$(basename "${TERMINAL%% *}")" "$root/config/$(basename "${TERMINAL%% *}")" 2>/dev/null; fi | |||
| 11 | chmod 700 "$root/run" | 12 | chmod 700 "$root/run" | ||
| 12 | rm -f "$out"/*.png | 13 | rm -f "$out"/*.png | ||
| 13 | if [ -n "${SCALE:-}" ]; then | 14 | if [ -n "${SCALE:-}" ]; then | ||
| 14 | printf 'output.HEADLESS-1.scale = %s\n' "$SCALE" >"$root/config/neferwl/config" | 15 | printf 'output.HEADLESS-1.scale = %s\n' "$SCALE" >"$root/config/neferwl/config" | ||
| 15 | fi | 16 | fi | ||
| 16 | - | clean=(env -i PATH="$PATH" HOME="$root" XDG_RUNTIME_DIR="$root/run" XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state" ${NEFERWL_STATE:+NEFERWL_STATE="$NEFERWL_STATE"}) | 17 | + | clean=(env -i PATH="$PATH" HOME="$root" XDG_RUNTIME_DIR="$root/run" XDG_CONFIG_HOME="$root/config" XDG_DATA_HOME="$root/data" XDG_STATE_HOME="$root/state" ${NEFERWL_STATE:+NEFERWL_STATE="$NEFERWL_STATE"} ${TERMINAL:+TERMINAL="$TERMINAL"}) |
| 17 | "${clean[@]}" neferwl --backend=headless --no-terminal --no-xwayland --size "${SIZE:-800x200}" --timeout "$((secs + 6))s" --screenshot "$out" >"$root/neferwl.log" 2>&1 & | 18 | "${clean[@]}" neferwl --backend=headless --no-terminal --no-xwayland --size "${SIZE:-800x200}" --timeout "$((secs + 6))s" --screenshot "$out" >"$root/neferwl.log" 2>&1 & | ||
| 18 | comp=$! | 19 | comp=$! | ||
| 19 | trap 'kill $comp 2>/dev/null || true; wait $comp 2>/dev/null || true' EXIT | 20 | trap 'kill $comp 2>/dev/null || true; wait $comp 2>/dev/null || true' EXIT | ||