feat/data-dnd e05cb15

slots.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close