1package core
2
3import (
4 "crypto/rand"
5 "slices"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// spawnPlacement owns pending SlotEnv claims. Slot and terminal retry decisions
12// remain with their respective policies; only this registry mints and claims tokens.
13type spawnPlacement struct {
14 pending map[string]spawnTarget
15 slots map[slotKey]string
16}
17
18// spawnTarget identifies the destination of one pending spawn.
19type spawnTarget struct {
20 slot slotKey
21 terminal *Workspace
22 at time.Time
23}
24
25func newSpawnPlacement() spawnPlacement {
26 return spawnPlacement{pending: make(map[string]spawnTarget), slots: make(map[slotKey]string)}
27}
28
29// request mints a token and records its pending destination.
30func (p *spawnPlacement) request(argv []string, target spawnTarget) ports.SpawnRequest {
31 token := rand.Text()
32 p.pending[token] = target
33 if target.terminal == nil {
34 p.slots[target.slot] = token
35 }
36 return ports.SpawnRequest{Argv: slices.Clone(argv), Env: []string{ports.SlotEnv + "=" + token}}
37}
38
39// dropSlot invalidates the pending token of a slot.
40func (p *spawnPlacement) dropSlot(key slotKey) {
41 if token := p.slots[key]; token != "" {
42 delete(p.pending, token)
43 delete(p.slots, key)
44 }
45}
46
47// slotPending reports whether a slot still awaits a window.
48func (p *spawnPlacement) slotPending(key slotKey) bool { return p.slots[key] != "" }
49
50// anyPending reports whether any spawn still awaits a window.
51func (p *spawnPlacement) anyPending() bool { return len(p.pending) != 0 }
52
53// terminalPending reports whether a workspace awaits an automatic terminal.
54func (p *spawnPlacement) terminalPending(w *Workspace) bool {
55 for _, target := range p.pending {
56 if target.terminal == w {
57 return true
58 }
59 }
60 return false
61}
62
63// expireTerminals drops terminal claims older than the retry interval.
64func (p *spawnPlacement) expireTerminals(now time.Time) {
65 for token, target := range p.pending {
66 if target.terminal != nil && now.Sub(target.at) > termRetry {
67 delete(p.pending, token)
68 }
69 }
70}
71
72// place claims a token once and puts its window in the target workspace.
73// Without a slot, a window rule (rules.go) may choose its place; otherwise it
74// opens on the focused screen's workspace.
75// Untagged windows return before any map lookup or allocation.
76func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
77 if v.Floating {
78 if s, _ := c.screenOf(v.ID); s != nil {
79 return
80 }
81 // A dialog opens on its parent's workspace, over it, fullscreen too.
82 if ps, pw := c.screenOf(v.Parent); v.Parent != 0 && pw != nil {
83 pw.AddDialog(v.ID, v.Parent, v.Width, v.Height)
84 pw.SetModal(v.ID, v.Modal)
85 ps.mon.normalize()
86 return
87 }
88 // Rules come after the dialog: it stays over its parent.
89 if eff, ok := c.ruleFor(v); ok {
90 c.placeByRule(v, eff)
91 return
92 }
93 c.cur().mon.AddFloating(v.ID, v.Width, v.Height)
94 return
95 }
96 if v.Slot != "" {
97 if target, ok := p.pending[v.Slot]; ok {
98 if target.terminal != nil {
99 delete(p.pending, v.Slot)
100 for _, s := range c.screens {
101 if s.mon.has(target.terminal) {
102 target.terminal.AddWindow(v.ID)
103 s.mon.normalize()
104 return
105 }
106 }
107 } else if st := c.slots[target.slot]; st != nil && st.window == 0 {
108 if sc, w := c.byName(target.slot.workspace); w != nil {
109 p.dropSlot(target.slot)
110 st.window = v.ID
111 w.AddSlotWindow(v.ID, target.slot.index, st.cells)
112 sc.mon.normalize()
113 return
114 }
115 }
116 }
117 }
118 if s, _ := c.screenOf(v.ID); s == nil {
119 if eff, ok := c.ruleFor(v); ok {
120 c.placeByRule(v, eff)
121 return
122 }
123 c.cur().mon.AddWindow(v.ID)
124 }
125}