merge: column-edge (last column stays, review fixes for outputs and terminal)
6 files · +75 -27
M internal/core/action.go +5 -0
| @@ -124,6 +124,11 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 124 | 124 | } | |
| 125 | 125 | edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1) | |
| 126 | 126 | if i := c.neighbor(dir); edge && i >= 0 { | |
| 127 | + | // The last column stays: a workspace on screen is never | |
| 128 | + | // empty, and moving it would only spawn a new terminal. | |
| 129 | + | if len(w.Columns) < 2 && c.keepsTerminal() { | |
| 130 | + | return Effect{} | |
| 131 | + | } | |
| 127 | 132 | col, ok := w.takeColumn() | |
| 128 | 133 | if !ok { | |
| 129 | 134 | return Effect{} | |
| @@ -124,6 +124,11 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 124 | } | 124 | } | ||
| 125 | edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1) | 125 | edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1) | ||
| 126 | if i := c.neighbor(dir); edge && i >= 0 { | 126 | if i := c.neighbor(dir); edge && i >= 0 { | ||
| 127 | + | // The last column stays: a workspace on screen is never | |||
| 128 | + | // empty, and moving it would only spawn a new terminal. | |||
| 129 | + | if len(w.Columns) < 2 && c.keepsTerminal() { | |||
| 130 | + | return Effect{} | |||
| 131 | + | } | |||
| 127 | col, ok := w.takeColumn() | 132 | col, ok := w.takeColumn() | ||
| 128 | if !ok { | 133 | if !ok { | ||
| 129 | return Effect{} | 134 | return Effect{} | ||
M internal/core/core.go +10 -4
| @@ -49,6 +49,7 @@ type Core struct { | |||
| 49 | 49 | grab WindowID | |
| 50 | 50 | buttons map[uint32]bool | |
| 51 | 51 | cursorX, cursorY float64 // global, logical | |
| 52 | + | pointerOutput string // output under the pointer at the last motion | |
| 52 | 53 | seq uint64 | |
| 53 | 54 | // layerChanged is set once layer state arrives from wayland. | |
| 54 | 55 | layerChanged bool | |
| @@ -549,10 +550,15 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 549 | 550 | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | |
| 550 | 551 | // The focused screen follows the pointer, so new windows | |
| 551 | 552 | // and launchers open where the user is. | |
| 552 | - | if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.cur().name() { | |
| 553 | - | c.focusScreen = c.screenIndex(o.Info.Name) | |
| 554 | - | if err := c.publish(ctx); err != nil { | |
| 555 | - | return nil | |
| 553 | + | // Only a pointer entering another output switches: keyboard | |
| 554 | + | // moves to another screen stick until then. Not mid-drag. | |
| 555 | + | if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 { | |
| 556 | + | c.pointerOutput = o.Info.Name | |
| 557 | + | if o.Info.Name != c.cur().name() { | |
| 558 | + | c.focusScreen = c.screenIndex(o.Info.Name) | |
| 559 | + | if err := c.publish(ctx); err != nil { | |
| 560 | + | return nil | |
| 561 | + | } | |
| 556 | 562 | } | |
| 557 | 563 | } | |
| 558 | 564 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| @@ -49,6 +49,7 @@ type Core struct { | |||||
| 49 | grab WindowID | 49 | grab WindowID | ||
| 50 | buttons map[uint32]bool | 50 | buttons map[uint32]bool | ||
| 51 | cursorX, cursorY float64 // global, logical | 51 | cursorX, cursorY float64 // global, logical | ||
| 52 | + | pointerOutput string // output under the pointer at the last motion | |||
| 52 | seq uint64 | 53 | seq uint64 | ||
| 53 | // layerChanged is set once layer state arrives from wayland. | 54 | // layerChanged is set once layer state arrives from wayland. | ||
| 54 | layerChanged bool | 55 | layerChanged bool | ||
| @@ -549,10 +550,15 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 549 | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | 550 | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | ||
| 550 | // The focused screen follows the pointer, so new windows | 551 | // The focused screen follows the pointer, so new windows | ||
| 551 | // and launchers open where the user is. | 552 | // and launchers open where the user is. | ||
| 552 | - | if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.cur().name() { | 553 | + | // Only a pointer entering another output switches: keyboard |
| 553 | - | c.focusScreen = c.screenIndex(o.Info.Name) | 554 | + | // moves to another screen stick until then. Not mid-drag. |
| 554 | - | if err := c.publish(ctx); err != nil { | 555 | + | if o, ok := c.layout().At(c.cursorX, c.cursorY); ok && o.Info.Name != c.pointerOutput && c.grab == 0 { |
| 555 | - | return nil | 556 | + | c.pointerOutput = o.Info.Name |
| 557 | + | if o.Info.Name != c.cur().name() { | |||
| 558 | + | c.focusScreen = c.screenIndex(o.Info.Name) | |||
| 559 | + | if err := c.publish(ctx); err != nil { | |||
| 560 | + | return nil | |||
| 561 | + | } | |||
| 556 | } | 562 | } | ||
| 557 | } | 563 | } | ||
| 558 | id, x, y := c.hit(c.cursorX, c.cursorY) | 564 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
M internal/core/layout.go +3 -0
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "slices" | |
| 8 | 8 | "strconv" | |
| 9 | 9 | "strings" | |
| 10 | + | "time" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | type WindowID = ports.WindowID | |
| @@ -125,6 +126,8 @@ type Workspace struct { | |||
| 125 | 126 | // its position there, where it returns. | |
| 126 | 127 | home string | |
| 127 | 128 | homePos int | |
| 129 | + | // termAt is when core last spawned a terminal for this workspace. | |
| 130 | + | termAt time.Time | |
| 128 | 131 | } | |
| 129 | 132 | ||
| 130 | 133 | func (w *Workspace) empty() bool { return len(w.Columns) == 0 } | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "slices" | 7 | "slices" | ||
| 8 | "strconv" | 8 | "strconv" | ||
| 9 | "strings" | 9 | "strings" | ||
| 10 | + | "time" | |||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| 12 | type WindowID = ports.WindowID | 13 | type WindowID = ports.WindowID | ||
| @@ -125,6 +126,8 @@ type Workspace struct { | |||||
| 125 | // its position there, where it returns. | 126 | // its position there, where it returns. | ||
| 126 | home string | 127 | home string | ||
| 127 | homePos int | 128 | homePos int | ||
| 129 | + | // termAt is when core last spawned a terminal for this workspace. | |||
| 130 | + | termAt time.Time | |||
| 128 | } | 131 | } | ||
| 129 | 132 | ||||
| 130 | func (w *Workspace) empty() bool { return len(w.Columns) == 0 } | 133 | func (w *Workspace) empty() bool { return len(w.Columns) == 0 } | ||
M internal/core/screens.go +2 -1
| @@ -99,7 +99,7 @@ func (c *Core) byName(name string) (*screen, *Workspace) { | |||
| 99 | 99 | return nil, nil | |
| 100 | 100 | } | |
| 101 | 101 | ||
| 102 | - | // configScale is output.<name>.scale, else 1. | |
| 102 | + | // isPrimary reports output.<name>.primary. | |
| 103 | 103 | func (c *Core) isPrimary(name string) bool { | |
| 104 | 104 | for _, o := range c.cfg.Outputs { | |
| 105 | 105 | if o.Name == name && o.Primary { | |
| @@ -121,6 +121,7 @@ func (c *Core) anyWindow() bool { | |||
| 121 | 121 | return false | |
| 122 | 122 | } | |
| 123 | 123 | ||
| 124 | + | // configScale is output.<name>.scale, else 1. | |
| 124 | 125 | func (c *Core) configScale(name string) float64 { | |
| 125 | 126 | v := 1.0 | |
| 126 | 127 | for _, o := range c.cfg.Outputs { | |
| @@ -99,7 +99,7 @@ func (c *Core) byName(name string) (*screen, *Workspace) { | |||||
| 99 | return nil, nil | 99 | return nil, nil | ||
| 100 | } | 100 | } | ||
| 101 | 101 | ||||
| 102 | - | // configScale is output.<name>.scale, else 1. | 102 | + | // isPrimary reports output.<name>.primary. |
| 103 | func (c *Core) isPrimary(name string) bool { | 103 | func (c *Core) isPrimary(name string) bool { | ||
| 104 | for _, o := range c.cfg.Outputs { | 104 | for _, o := range c.cfg.Outputs { | ||
| 105 | if o.Name == name && o.Primary { | 105 | if o.Name == name && o.Primary { | ||
| @@ -121,6 +121,7 @@ func (c *Core) anyWindow() bool { | |||||
| 121 | return false | 121 | return false | ||
| 122 | } | 122 | } | ||
| 123 | 123 | ||||
| 124 | + | // configScale is output.<name>.scale, else 1. | |||
| 124 | func (c *Core) configScale(name string) float64 { | 125 | func (c *Core) configScale(name string) float64 { | ||
| 125 | v := 1.0 | 126 | v := 1.0 | ||
| 126 | for _, o := range c.cfg.Outputs { | 127 | for _, o := range c.cfg.Outputs { | ||
M internal/core/screens_test.go +33 -6
| @@ -361,7 +361,8 @@ func TestMoveColumnCrossesOutputs(t *testing.T) { | |||
| 361 | 361 | if out := lastOutputs(t, r.commands); out.Focused != "DP-2" { | |
| 362 | 362 | t.Fatal(out.Focused) | |
| 363 | 363 | } | |
| 364 | - | // Back left: it lands on the right side of DP-1. | |
| 364 | + | // Back left (no terminal kept here, so a last column may move): it | |
| 365 | + | // lands on the right side of DP-1. | |
| 365 | 366 | set = r.key(t, "Left", ports.ModAlt|ports.ModShift) | |
| 366 | 367 | if got := shown(set)["DP-1"]; len(got) != 2 { | |
| 367 | 368 | t.Fatal(got) | |
| @@ -408,14 +409,40 @@ func TestEmptyWorkspaceGetsTerminal(t *testing.T) { | |||
| 408 | 409 | if got := shown(set); len(got["DP-1"]) != 1 || len(got["DP-2"]) != 1 || got["DP-1"][0] == got["DP-2"][0] { | |
| 409 | 410 | t.Fatal(got) | |
| 410 | 411 | } | |
| 411 | - | // Closing the last window spawns a new terminal; nothing while pending. | |
| 412 | + | // A terminal that exits right after mapping is not respawned at once | |
| 413 | + | // (no loop for a broken terminal command). | |
| 412 | 414 | r.client <- ports.WindowUnmapped{ID: 2} | |
| 413 | 415 | receive(t, r.scenes) | |
| 414 | - | if v := receive(t, r.spawn); v.Argv[0] != "foot" { | |
| 415 | - | t.Fatal(v) | |
| 416 | - | } | |
| 417 | 416 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | |
| 418 | 417 | if len(r.spawn) != 0 { | |
| 419 | - | t.Fatal("spawned twice") | |
| 418 | + | t.Fatal("respawned at once") | |
| 419 | + | } | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | func TestLastColumnStaysOnItsOutput(t *testing.T) { | |
| 423 | + | r := startRig(t, true, nil, left, right) | |
| 424 | + | r.mapWindow(t, 1) | |
| 425 | + | set := r.key(t, "Right", ports.ModAlt|ports.ModShift) | |
| 426 | + | if got := shown(set); len(got["DP-1"]) != 1 || len(got["DP-2"]) != 0 { | |
| 427 | + | t.Fatal(got) | |
| 428 | + | } | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | func TestKeyboardScreenFocusSticksUntilPointerLeaves(t *testing.T) { | |
| 432 | + | r := startMulti(t, nil, left, right) | |
| 433 | + | r.mapWindow(t, 1) | |
| 434 | + | r.input <- ports.PointerMotion{X: 10, Y: 10} | |
| 435 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 436 | + | // Jitter on DP-1 keeps DP-2 focused: the next window opens there. | |
| 437 | + | r.input <- ports.PointerMotion{X: 11, Y: 10} | |
| 438 | + | if got := shown(r.mapWindow(t, 2)); len(got["DP-2"]) != 1 { | |
| 439 | + | t.Fatal(got) | |
| 440 | + | } | |
| 441 | + | // Entering DP-1 again focuses it. | |
| 442 | + | r.input <- ports.PointerMotion{X: 250, Y: 10} | |
| 443 | + | r.input <- ports.PointerMotion{X: 10, Y: 10} | |
| 444 | + | receive(t, r.scenes) | |
| 445 | + | if got := shown(r.mapWindow(t, 3)); len(got["DP-1"]) != 2 { | |
| 446 | + | t.Fatal(got) | |
| 420 | 447 | } | |
| 421 | 448 | } | |
| @@ -361,7 +361,8 @@ func TestMoveColumnCrossesOutputs(t *testing.T) { | |||||
| 361 | if out := lastOutputs(t, r.commands); out.Focused != "DP-2" { | 361 | if out := lastOutputs(t, r.commands); out.Focused != "DP-2" { | ||
| 362 | t.Fatal(out.Focused) | 362 | t.Fatal(out.Focused) | ||
| 363 | } | 363 | } | ||
| 364 | - | // Back left: it lands on the right side of DP-1. | 364 | + | // Back left (no terminal kept here, so a last column may move): it |
| 365 | + | // lands on the right side of DP-1. | |||
| 365 | set = r.key(t, "Left", ports.ModAlt|ports.ModShift) | 366 | set = r.key(t, "Left", ports.ModAlt|ports.ModShift) | ||
| 366 | if got := shown(set)["DP-1"]; len(got) != 2 { | 367 | if got := shown(set)["DP-1"]; len(got) != 2 { | ||
| 367 | t.Fatal(got) | 368 | t.Fatal(got) | ||
| @@ -408,14 +409,40 @@ func TestEmptyWorkspaceGetsTerminal(t *testing.T) { | |||||
| 408 | if got := shown(set); len(got["DP-1"]) != 1 || len(got["DP-2"]) != 1 || got["DP-1"][0] == got["DP-2"][0] { | 409 | if got := shown(set); len(got["DP-1"]) != 1 || len(got["DP-2"]) != 1 || got["DP-1"][0] == got["DP-2"][0] { | ||
| 409 | t.Fatal(got) | 410 | t.Fatal(got) | ||
| 410 | } | 411 | } | ||
| 411 | - | // Closing the last window spawns a new terminal; nothing while pending. | 412 | + | // A terminal that exits right after mapping is not respawned at once |
| 413 | + | // (no loop for a broken terminal command). | |||
| 412 | r.client <- ports.WindowUnmapped{ID: 2} | 414 | r.client <- ports.WindowUnmapped{ID: 2} | ||
| 413 | receive(t, r.scenes) | 415 | receive(t, r.scenes) | ||
| 414 | - | if v := receive(t, r.spawn); v.Argv[0] != "foot" { | |||
| 415 | - | t.Fatal(v) | |||
| 416 | - | } | |||
| 417 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | 416 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | ||
| 418 | if len(r.spawn) != 0 { | 417 | if len(r.spawn) != 0 { | ||
| 419 | - | t.Fatal("spawned twice") | 418 | + | t.Fatal("respawned at once") |
| 419 | + | } | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | func TestLastColumnStaysOnItsOutput(t *testing.T) { | |||
| 423 | + | r := startRig(t, true, nil, left, right) | |||
| 424 | + | r.mapWindow(t, 1) | |||
| 425 | + | set := r.key(t, "Right", ports.ModAlt|ports.ModShift) | |||
| 426 | + | if got := shown(set); len(got["DP-1"]) != 1 || len(got["DP-2"]) != 0 { | |||
| 427 | + | t.Fatal(got) | |||
| 428 | + | } | |||
| 429 | + | } | |||
| 430 | + | ||||
| 431 | + | func TestKeyboardScreenFocusSticksUntilPointerLeaves(t *testing.T) { | |||
| 432 | + | r := startMulti(t, nil, left, right) | |||
| 433 | + | r.mapWindow(t, 1) | |||
| 434 | + | r.input <- ports.PointerMotion{X: 10, Y: 10} | |||
| 435 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |||
| 436 | + | // Jitter on DP-1 keeps DP-2 focused: the next window opens there. | |||
| 437 | + | r.input <- ports.PointerMotion{X: 11, Y: 10} | |||
| 438 | + | if got := shown(r.mapWindow(t, 2)); len(got["DP-2"]) != 1 { | |||
| 439 | + | t.Fatal(got) | |||
| 440 | + | } | |||
| 441 | + | // Entering DP-1 again focuses it. | |||
| 442 | + | r.input <- ports.PointerMotion{X: 250, Y: 10} | |||
| 443 | + | r.input <- ports.PointerMotion{X: 10, Y: 10} | |||
| 444 | + | receive(t, r.scenes) | |||
| 445 | + | if got := shown(r.mapWindow(t, 3)); len(got["DP-1"]) != 2 { | |||
| 446 | + | t.Fatal(got) | |||
| 420 | } | 447 | } | ||
| 421 | } | 448 | } | ||
M internal/core/terminal.go +22 -16
| @@ -12,8 +12,8 @@ import ( | |||
| 12 | 12 | // one is, core spawns the configured terminal with a SlotEnv token, and the | |
| 13 | 13 | // window carrying it fills that workspace, wherever the focus is by then. | |
| 14 | 14 | ||
| 15 | - | // termRetry is how long a terminal may take to map before another is spawned | |
| 16 | - | // (the first one failed, or the terminal command is broken). | |
| 15 | + | // termRetry is the least time between two terminals for one workspace: a | |
| 16 | + | // terminal that fails, or maps and exits at once, is not respawned in a loop. | |
| 17 | 17 | const termRetry = 5 * time.Second | |
| 18 | 18 | ||
| 19 | 19 | type termSpawn struct { | |
| @@ -21,39 +21,45 @@ type termSpawn struct { | |||
| 21 | 21 | at time.Time | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | + | // keepsTerminal reports whether empty workspaces get a terminal. | |
| 25 | + | func (c *Core) keepsTerminal() bool { return c.ch.Terminal && len(c.cfg.Terminal.Command) > 0 } | |
| 26 | + | ||
| 24 | 27 | // fillEmpty returns the terminals to spawn for empty workspaces on screen. | |
| 25 | 28 | // Workspaces with slots are left to them. | |
| 26 | 29 | func (c *Core) fillEmpty() []ports.SpawnRequest { | |
| 27 | - | if !c.ch.Terminal || len(c.cfg.Terminal.Command) == 0 { | |
| 30 | + | now := time.Now() | |
| 31 | + | // Expired spawns go first, whatever is on screen: pending tokens keep | |
| 32 | + | // wayland reading /proc on every map. | |
| 33 | + | for token, t := range c.terms { | |
| 34 | + | if now.Sub(t.at) > termRetry { | |
| 35 | + | delete(c.terms, token) | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | if !c.keepsTerminal() { | |
| 28 | 39 | return nil | |
| 29 | 40 | } | |
| 30 | - | now := time.Now() | |
| 31 | 41 | var reqs []ports.SpawnRequest | |
| 32 | 42 | for _, s := range c.screens { | |
| 33 | 43 | w := s.mon.Current() | |
| 34 | - | if s.name() == "" || !w.empty() || c.hasSlots(w) || c.termPending(w, now) { | |
| 44 | + | if s.name() == "" || !w.empty() || c.hasSlots(w) || c.termPending(w) || now.Sub(w.termAt) < termRetry { | |
| 35 | 45 | continue | |
| 36 | 46 | } | |
| 37 | 47 | token := rand.Text() | |
| 48 | + | w.termAt = now | |
| 38 | 49 | c.terms[token] = &termSpawn{w: w, at: now} | |
| 39 | 50 | reqs = append(reqs, ports.SpawnRequest{Argv: slices.Clone(c.cfg.Terminal.Command), Env: []string{ports.SlotEnv + "=" + token}}) | |
| 40 | 51 | } | |
| 41 | 52 | return reqs | |
| 42 | 53 | } | |
| 43 | 54 | ||
| 44 | - | // termPending reports whether a recent terminal spawn waits for w; it drops | |
| 45 | - | // expired ones. | |
| 46 | - | func (c *Core) termPending(w *Workspace, now time.Time) bool { | |
| 47 | - | found := false | |
| 48 | - | for token, t := range c.terms { | |
| 49 | - | switch { | |
| 50 | - | case now.Sub(t.at) > termRetry: | |
| 51 | - | delete(c.terms, token) | |
| 52 | - | case t.w == w: | |
| 53 | - | found = true | |
| 55 | + | // termPending reports whether a terminal spawn waits for w. | |
| 56 | + | func (c *Core) termPending(w *Workspace) bool { | |
| 57 | + | for _, t := range c.terms { | |
| 58 | + | if t.w == w { | |
| 59 | + | return true | |
| 54 | 60 | } | |
| 55 | 61 | } | |
| 56 | - | return found | |
| 62 | + | return false | |
| 57 | 63 | } | |
| 58 | 64 | ||
| 59 | 65 | func (c *Core) hasSlots(w *Workspace) bool { | |
| @@ -12,8 +12,8 @@ import ( | |||||
| 12 | // one is, core spawns the configured terminal with a SlotEnv token, and the | 12 | // one is, core spawns the configured terminal with a SlotEnv token, and the | ||
| 13 | // window carrying it fills that workspace, wherever the focus is by then. | 13 | // window carrying it fills that workspace, wherever the focus is by then. | ||
| 14 | 14 | ||||
| 15 | - | // termRetry is how long a terminal may take to map before another is spawned | 15 | + | // termRetry is the least time between two terminals for one workspace: a |
| 16 | - | // (the first one failed, or the terminal command is broken). | 16 | + | // terminal that fails, or maps and exits at once, is not respawned in a loop. |
| 17 | const termRetry = 5 * time.Second | 17 | const termRetry = 5 * time.Second | ||
| 18 | 18 | ||||
| 19 | type termSpawn struct { | 19 | type termSpawn struct { | ||
| @@ -21,39 +21,45 @@ type termSpawn struct { | |||||
| 21 | at time.Time | 21 | at time.Time | ||
| 22 | } | 22 | } | ||
| 23 | 23 | ||||
| 24 | + | // keepsTerminal reports whether empty workspaces get a terminal. | |||
| 25 | + | func (c *Core) keepsTerminal() bool { return c.ch.Terminal && len(c.cfg.Terminal.Command) > 0 } | |||
| 26 | + | ||||
| 24 | // fillEmpty returns the terminals to spawn for empty workspaces on screen. | 27 | // fillEmpty returns the terminals to spawn for empty workspaces on screen. | ||
| 25 | // Workspaces with slots are left to them. | 28 | // Workspaces with slots are left to them. | ||
| 26 | func (c *Core) fillEmpty() []ports.SpawnRequest { | 29 | func (c *Core) fillEmpty() []ports.SpawnRequest { | ||
| 27 | - | if !c.ch.Terminal || len(c.cfg.Terminal.Command) == 0 { | 30 | + | now := time.Now() |
| 31 | + | // Expired spawns go first, whatever is on screen: pending tokens keep | |||
| 32 | + | // wayland reading /proc on every map. | |||
| 33 | + | for token, t := range c.terms { | |||
| 34 | + | if now.Sub(t.at) > termRetry { | |||
| 35 | + | delete(c.terms, token) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | if !c.keepsTerminal() { | |||
| 28 | return nil | 39 | return nil | ||
| 29 | } | 40 | } | ||
| 30 | - | now := time.Now() | |||
| 31 | var reqs []ports.SpawnRequest | 41 | var reqs []ports.SpawnRequest | ||
| 32 | for _, s := range c.screens { | 42 | for _, s := range c.screens { | ||
| 33 | w := s.mon.Current() | 43 | w := s.mon.Current() | ||
| 34 | - | if s.name() == "" || !w.empty() || c.hasSlots(w) || c.termPending(w, now) { | 44 | + | if s.name() == "" || !w.empty() || c.hasSlots(w) || c.termPending(w) || now.Sub(w.termAt) < termRetry { |
| 35 | continue | 45 | continue | ||
| 36 | } | 46 | } | ||
| 37 | token := rand.Text() | 47 | token := rand.Text() | ||
| 48 | + | w.termAt = now | |||
| 38 | c.terms[token] = &termSpawn{w: w, at: now} | 49 | c.terms[token] = &termSpawn{w: w, at: now} | ||
| 39 | reqs = append(reqs, ports.SpawnRequest{Argv: slices.Clone(c.cfg.Terminal.Command), Env: []string{ports.SlotEnv + "=" + token}}) | 50 | reqs = append(reqs, ports.SpawnRequest{Argv: slices.Clone(c.cfg.Terminal.Command), Env: []string{ports.SlotEnv + "=" + token}}) | ||
| 40 | } | 51 | } | ||
| 41 | return reqs | 52 | return reqs | ||
| 42 | } | 53 | } | ||
| 43 | 54 | ||||
| 44 | - | // termPending reports whether a recent terminal spawn waits for w; it drops | 55 | + | // termPending reports whether a terminal spawn waits for w. |
| 45 | - | // expired ones. | 56 | + | func (c *Core) termPending(w *Workspace) bool { |
| 46 | - | func (c *Core) termPending(w *Workspace, now time.Time) bool { | 57 | + | for _, t := range c.terms { |
| 47 | - | found := false | 58 | + | if t.w == w { |
| 48 | - | for token, t := range c.terms { | 59 | + | return true |
| 49 | - | switch { | |||
| 50 | - | case now.Sub(t.at) > termRetry: | |||
| 51 | - | delete(c.terms, token) | |||
| 52 | - | case t.w == w: | |||
| 53 | - | found = true | |||
| 54 | } | 60 | } | ||
| 55 | } | 61 | } | ||
| 56 | - | return found | 62 | + | return false |
| 57 | } | 63 | } | ||
| 58 | 64 | ||||
| 59 | func (c *Core) hasSlots(w *Workspace) bool { | 65 | func (c *Core) hasSlots(w *Workspace) bool { | ||