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 width Width
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 width Width
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 width, err := ParseWidth(s.Width)
49 if err != nil {
50 return nil, fmt.Errorf("workspace %s column %d: %w", w.Name, s.Index, err)
51 }
52 if len(s.Argv) == 0 || s.Index < 1 {
53 return nil, fmt.Errorf("workspace %s column %d: invalid slot", w.Name, s.Index)
54 }
55 specs = append(specs, slotSpec{key: slotKey{w.Name, s.Index}, argv: slices.Clone(s.Argv), width: width})
56 }
57 }
58 return specs, nil
59}
60
61// updateSlots applies the config and returns the slots to spawn: new ones,
62// and empty ones whose command changed. A slot dropped from config keeps its
63// window as a normal column. Call before named workspaces are applied, so a
64// renamed or dropped workspace can still be found.
65func (c *Core) updateSlots(specs []slotSpec) []slotKey {
66 want := map[slotKey]slotSpec{}
67 for _, s := range specs {
68 want[s.key] = s
69 }
70 for key := range c.slots {
71 if _, ok := want[key]; ok {
72 continue
73 }
74 if _, w := c.byName(key.workspace); w != nil {
75 w.unslot(key.index)
76 }
77 c.placement.dropSlot(key)
78 delete(c.slots, key)
79 }
80 var spawn []slotKey
81 for _, s := range specs {
82 st, ok := c.slots[s.key]
83 if !ok {
84 c.slots[s.key] = &slotState{argv: s.argv, width: s.width}
85 spawn = append(spawn, s.key)
86 continue
87 }
88 if st.width != s.width {
89 st.width = s.width
90 if _, w := c.byName(s.key.workspace); w != nil {
91 w.setSlotWidth(s.key.index, s.width)
92 }
93 }
94 if !slices.Equal(st.argv, s.argv) {
95 st.argv = s.argv
96 if st.window == 0 {
97 // A pending spawn ran the old command: its window opens as
98 // a normal one; the new command replaces it.
99 c.placement.dropSlot(s.key)
100 spawn = append(spawn, s.key)
101 }
102 }
103 }
104 return spawn
105}
106
107// spawnSlot starts the slot's command with a fresh token.
108func (c *Core) spawnSlot(key slotKey) ports.SpawnRequest {
109 st := c.slots[key]
110 c.placement.dropSlot(key)
111 st.stale = false
112 return c.placement.request(st.argv, spawnTarget{slot: key})
113}
114
115// releaseSlots empties slots whose window is gone or no longer in its slot
116// column (closed, or moved to another workspace by the user).
117func (c *Core) releaseSlots() {
118 for key, st := range c.slots {
119 if st.window == 0 {
120 continue
121 }
122 _, w := c.byName(key.workspace)
123 if w == nil || !w.inSlot(st.window, key.index) {
124 st.window = 0
125 }
126 }
127}
128
129// refill returns the empty slots of the workspace on screen to spawn. A slot
130// still pending for the second show in a row is respawned: its command
131// failed, exited or handed off to another instance without a window.
132func (c *Core) refill() []slotKey {
133 name := c.cur().mon.Current().Name
134 if name == "" {
135 return nil
136 }
137 var keys []slotKey
138 for key, st := range c.slots {
139 switch {
140 case key.workspace != name || st.window != 0:
141 case c.placement.slotPending(key) && !st.stale:
142 st.stale = true
143 default:
144 c.placement.dropSlot(key)
145 keys = append(keys, key)
146 }
147 }
148 slices.SortFunc(keys, func(a, b slotKey) int { return a.index - b.index })
149 return keys
150}