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// spawnKind tells what a pending spawn is destined for.
19type spawnKind uint8
20
21const (
22 // spawnSlot fills a slot; the zero value.
23 spawnSlot spawnKind = iota
24 // spawnTerminal is an automatic terminal for an empty workspace.
25 spawnTerminal
26 // spawnStash is a window opened straight into the stash of ws.
27 spawnStash
28)
29
30// spawnTarget identifies the destination of one pending spawn. slot is set
31// for spawnSlot; ws and at for the workspace-bound kinds.
32type spawnTarget struct {
33 kind spawnKind
34 slot slotKey
35 ws *Workspace
36 at time.Time
37}
38
39func newSpawnPlacement() spawnPlacement {
40 return spawnPlacement{pending: make(map[string]spawnTarget), slots: make(map[slotKey]string)}
41}
42
43// request mints a token and records its pending destination.
44func (p *spawnPlacement) request(argv []string, target spawnTarget) ports.SpawnRequest {
45 token := rand.Text()
46 p.pending[token] = target
47 if target.kind == spawnSlot {
48 p.slots[target.slot] = token
49 }
50 return ports.SpawnRequest{Argv: slices.Clone(argv), Env: []string{ports.SlotEnv + "=" + token}}
51}
52
53// dropSlot invalidates the pending token of a slot.
54func (p *spawnPlacement) dropSlot(key slotKey) {
55 if token := p.slots[key]; token != "" {
56 delete(p.pending, token)
57 delete(p.slots, key)
58 }
59}
60
61// slotPending reports whether a slot still awaits a window.
62func (p *spawnPlacement) slotPending(key slotKey) bool { return p.slots[key] != "" }
63
64// anyPending reports whether any spawn still awaits a window.
65func (p *spawnPlacement) anyPending() bool { return len(p.pending) != 0 }
66
67// workspacePending reports whether a workspace awaits a spawn of the given kind.
68func (p *spawnPlacement) workspacePending(w *Workspace, kind spawnKind) bool {
69 for _, target := range p.pending {
70 if target.kind == kind && target.ws == w {
71 return true
72 }
73 }
74 return false
75}
76
77// expireWorkspaceClaims drops workspace-bound claims older than the retry interval.
78func (p *spawnPlacement) expireWorkspaceClaims(now time.Time) {
79 for token, target := range p.pending {
80 if target.kind != spawnSlot && now.Sub(target.at) > termRetry {
81 delete(p.pending, token)
82 }
83 }
84}
85
86// place claims a token once and puts its window in the target workspace.
87// Without a slot, a window rule (rules.go) may choose its place; otherwise it
88// opens on the focused screen's workspace.
89// Untagged windows return before any map lookup or allocation.
90func (p *spawnPlacement) place(c *Core, v ports.WindowMapped) {
91 if v.Floating {
92 if s, _ := c.screenOf(v.ID); s != nil {
93 return
94 }
95 // A dialog opens on its parent's workspace, over it, fullscreen too.
96 if ps, pw := c.screenOf(v.Parent); v.Parent != 0 && pw != nil {
97 pw.AddDialog(v.ID, v.Parent, v.Width, v.Height)
98 pw.SetModal(v.ID, v.Modal)
99 ps.mon.normalize()
100 return
101 }
102 // Rules come after the dialog: it stays over its parent.
103 if eff, ok := c.ruleFor(v); ok {
104 c.placeByRule(v, eff)
105 return
106 }
107 c.cur().mon.AddFloating(v.ID, v.Width, v.Height)
108 return
109 }
110 if v.Slot != "" {
111 // A claim no window answered in time no longer places one.
112 p.expireWorkspaceClaims(c.now())
113 if target, ok := p.pending[v.Slot]; ok {
114 switch target.kind {
115 case spawnTerminal, spawnStash:
116 delete(p.pending, v.Slot)
117 for _, s := range c.screens {
118 if s.mon.has(target.ws) {
119 if target.kind == spawnStash {
120 target.ws.stashSpawned(v.ID)
121 } else {
122 target.ws.AddWindow(v.ID)
123 }
124 s.mon.normalize()
125 return
126 }
127 }
128 case spawnSlot:
129 if st := c.slots[target.slot]; st != nil && st.window == 0 {
130 if sc, w := c.byName(target.slot.workspace); w != nil {
131 p.dropSlot(target.slot)
132 st.window = v.ID
133 w.AddSlotWindow(v.ID, target.slot.index, st.cells)
134 sc.mon.normalize()
135 return
136 }
137 }
138 }
139 }
140 }
141 if s, _ := c.screenOf(v.ID); s == nil {
142 if eff, ok := c.ruleFor(v); ok {
143 c.placeByRule(v, eff)
144 return
145 }
146 c.cur().mon.AddWindow(v.ID)
147 }
148}