feat/data-dnd e05cb15

spawn_placement.go RawHistoryFolder

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}

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