1package core
2
3import (
4 "fmt"
5 "slices"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// Slots of declared workspaces (ADR 011): workspace.<name>.column.N reserves
11// column N for the window of one command. The slot, not the window, has the
12// identity: neferwl spawns the command with SlotEnv set to a random token,
13// and the first window of a client carrying that token fills the slot.
14//
15// Only the token of the pending spawn is accepted: once the slot is filled,
16// or the command changes, other windows with that token (children of the slot
17// app inherit its environment) open as normal windows.
18//
19// A slot is spawned at startup, and again when the user shows its workspace
20// while it is empty. Nothing is relaunched on its own.
21
22type slotKey struct {
23 workspace string
24 index int
25}
26
27type slotState struct {
28 argv []string
29 cells int
30 window WindowID // 0 while empty
31 // stale is set when the workspace is shown while the spawn is pending.
32 // Shown again still pending, the command is deemed failed and respawned;
33 // a slow app therefore gets one show to map its window.
34 stale bool
35}
36
37// slotSpec is one parsed slot of the config.
38type slotSpec struct {
39 key slotKey
40 argv []string
41 cells int
42}
43
44func parseSlots(ws []ports.WorkspaceConfig) ([]slotSpec, error) {
45 var specs []slotSpec
46 for _, w := range ws {
47 for _, s := range w.Slots {
48 if s.Cells < 0 || s.Cells > maxCells {
49 return nil, fmt.Errorf("workspace %s column %d: invalid width %d", w.Name, s.Index, s.Cells)
50 }
51 if len(s.Argv) == 0 || s.Index < 1 {
52 return nil, fmt.Errorf("workspace %s column %d: invalid slot", w.Name, s.Index)
53 }
54 specs = append(specs, slotSpec{key: slotKey{w.Name, s.Index}, argv: slices.Clone(s.Argv), cells: s.Cells})
55 }
56 }
57 return specs, nil
58}
59
60// updateSlots applies the config and returns the slots to spawn: new ones,
61// and empty ones whose command changed. A slot dropped from config keeps its
62// window as a normal column. Call before named workspaces are applied, so a
63// renamed or dropped workspace can still be found.
64func (c *Core) updateSlots(specs []slotSpec) []slotKey {
65 want := map[slotKey]slotSpec{}
66 for _, s := range specs {
67 want[s.key] = s
68 }
69 for key := range c.slots {
70 if _, ok := want[key]; ok {
71 continue
72 }
73 if _, w := c.byName(key.workspace); w != nil {
74 w.unslot(key.index)
75 }
76 c.placement.dropSlot(key)
77 delete(c.slots, key)
78 }
79 var spawn []slotKey
80 for _, s := range specs {
81 st, ok := c.slots[s.key]
82 if !ok {
83 c.slots[s.key] = &slotState{argv: s.argv, cells: s.cells}
84 spawn = append(spawn, s.key)
85 continue
86 }
87 if st.cells != s.cells {
88 st.cells = s.cells
89 if _, w := c.byName(s.key.workspace); w != nil {
90 w.setSlotCells(s.key.index, s.cells)
91 }
92 }
93 if !slices.Equal(st.argv, s.argv) {
94 st.argv = s.argv
95 if st.window == 0 {
96 // A pending spawn ran the old command: its window opens as
97 // a normal one; the new command replaces it.
98 c.placement.dropSlot(s.key)
99 spawn = append(spawn, s.key)
100 }
101 }
102 }
103 return spawn
104}
105
106// spawnSlot starts the slot's command with a fresh token.
107func (c *Core) spawnSlot(key slotKey) ports.SpawnRequest {
108 st := c.slots[key]
109 c.placement.dropSlot(key)
110 st.stale = false
111 return c.placement.request(st.argv, spawnTarget{slot: key})
112}
113
114// releaseSlots empties slots whose window is gone or no longer in its slot
115// column (closed, or moved to another workspace by the user).
116func (c *Core) releaseSlots() {
117 for key, st := range c.slots {
118 if st.window == 0 {
119 continue
120 }
121 _, w := c.byName(key.workspace)
122 if w == nil || !w.inSlot(st.window, key.index) {
123 st.window = 0
124 }
125 }
126}
127
128// refill returns the empty slots of the workspace on screen to spawn. A slot
129// still pending for the second show in a row is respawned: its command
130// failed, exited or handed off to another instance without a window.
131func (c *Core) refill() []slotKey {
132 name := c.cur().mon.Current().Name
133 if name == "" {
134 return nil
135 }
136 var keys []slotKey
137 for key, st := range c.slots {
138 switch {
139 case key.workspace != name || st.window != 0:
140 case c.placement.slotPending(key) && !st.stale:
141 st.stale = true
142 default:
143 c.placement.dropSlot(key)
144 keys = append(keys, key)
145 }
146 }
147 slices.SortFunc(keys, func(a, b slotKey) int { return a.index - b.index })
148 return keys
149}