v0.1.0 d9218d6

screens.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5
6	"github.com/bnema/neferwl/internal/ports"
7)
8
9// screen is one connected output: its monitor tree, scale, layer surfaces and
10// place in the global layout (ADR 011). Outputs are placed left to right, in
11// config order (output.<name>.*), then in connection order.
12type screen struct {
13	info            ports.OutputInfo
14	mon             *Monitor
15	scale, cfgScale float64
16	primary         bool
17	x               int // logical left edge in the global layout
18	layers          []ports.LayerSurface
19	placed          []ports.SceneLayer
20}
21
22func (s *screen) name() string { return s.info.Name }
23
24func (s *screen) placement() ports.OutputPlacement {
25	o := s.mon.Output()
26	return ports.OutputPlacement{Info: s.info, X: s.x, Width: o.W, Height: o.H, Scale: s.scale, Primary: s.primary}
27}
28
29// setScale resizes the logical output; layer placement follows.
30func (s *screen) setScale(v float64) {
31	s.scale = SnapScale(v)
32	s.mon.SetOutput(logical(s.info.Width, s.scale), logical(s.info.Height, s.scale))
33	s.arrange()
34}
35
36// arrange places the layer surfaces and gives workspaces the usable area.
37func (s *screen) arrange() {
38	var usable ports.Rect
39	o := s.mon.Output()
40	s.placed, usable = arrangeLayers(o.W, o.H, s.layers)
41	s.mon.SetUsable(usable)
42}
43
44// order sorts screens by config order, then connection order, and assigns x.
45func (c *Core) order() {
46	rank := func(s *screen) int {
47		for i, o := range c.cfg.Outputs {
48			if o.Name == s.name() {
49				return i
50			}
51		}
52		return len(c.cfg.Outputs)
53	}
54	focused := c.cur()
55	slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) })
56	c.focusScreen = slices.Index(c.screens, focused)
57	x := 0
58	for _, s := range c.screens {
59		s.x = x
60		x += s.mon.Output().W
61	}
62}
63
64// layout lists the connected outputs; the placeholder is not one.
65func (c *Core) layout() ports.Layout {
66	l := make(ports.Layout, 0, len(c.screens))
67	for _, s := range c.screens {
68		if s.name() != "" {
69			l = append(l, s.placement())
70		}
71	}
72	return l
73}
74
75// cur is the focused screen: new windows open there and binds apply to it.
76func (c *Core) cur() *screen { return c.screens[c.focusScreen] }
77
78func (c *Core) screenIndex(name string) int {
79	return slices.IndexFunc(c.screens, func(s *screen) bool { return s.name() == name })
80}
81
82// screenOf returns the screen and workspace holding the window.
83func (c *Core) screenOf(id WindowID) (*screen, *Workspace) {
84	for _, s := range c.screens {
85		if w, _ := s.mon.find(id); w != nil {
86			return s, w
87		}
88	}
89	return nil, nil
90}
91
92// byName finds a named workspace on any screen.
93func (c *Core) byName(name string) (*screen, *Workspace) {
94	for _, s := range c.screens {
95		if w := s.mon.byName(name); w != nil {
96			return s, w
97		}
98	}
99	return nil, nil
100}
101
102// isPrimary reports output.<name>.primary.
103func (c *Core) isPrimary(name string) bool {
104	for _, o := range c.cfg.Outputs {
105		if o.Name == name && o.Primary {
106			return true
107		}
108	}
109	return false
110}
111
112// anyWindow reports whether any workspace holds a window.
113func (c *Core) anyWindow() bool {
114	for _, s := range c.screens {
115		for _, w := range s.mon.all() {
116			if !w.empty() {
117				return true
118			}
119		}
120	}
121	return false
122}
123
124// configScale is output.<name>.scale, else 1.
125func (c *Core) configScale(name string) float64 {
126	v := 1.0
127	for _, o := range c.cfg.Outputs {
128		if o.Name == name && o.Scale != 0 {
129			v = o.Scale
130		}
131	}
132	return SnapScale(v)
133}
134
135// applyConfigScales picks up output.<name>.scale when it changed in the
136// config, replacing any live scale-up/scale-down adjustment.
137func (c *Core) applyConfigScales() {
138	for _, s := range c.screens {
139		if v := c.configScale(s.name()); v != s.cfgScale {
140			s.cfgScale = v
141			s.setScale(v)
142		}
143	}
144	c.order()
145}
146
147// addScreen handles a connected output or a new mode for a known one. The
148// first output takes over the placeholder screen core starts with; a new
149// monitor gets its named workspaces and takes its guests back.
150func (c *Core) addScreen(info ports.OutputInfo) {
151	if i := c.screenIndex(info.Name); i >= 0 {
152		s := c.screens[i]
153		s.info = info
154		s.setScale(s.scale)
155		c.order()
156		return
157	}
158	var s *screen
159	if len(c.screens) == 1 && c.screens[0].name() == "" {
160		s = c.screens[0]
161		s.info = info
162	} else {
163		s = &screen{info: info, mon: newMonitor(info.Name, info.Key())}
164		c.settings(s.mon)
165		c.screens = append(c.screens, s)
166	}
167	s.mon.Name, s.mon.Key = info.Name, info.Key()
168	s.cfgScale = c.configScale(info.Name)
169	s.setScale(s.cfgScale)
170	s.primary = c.isPrimary(info.Name)
171	c.order()
172	// The primary output takes the focus while nothing is open yet
173	// (startup). Later, plugging it in never moves the user.
174	if s.primary && !c.anyWindow() {
175		c.focusScreen = slices.Index(c.screens, s)
176	}
177	c.named()
178	c.settleGuests()
179}
180
181// removeScreen moves every workspace of an unplugged output to the focused
182// screen (another one if it was focused), numbered ones below its numbered
183// workspaces, hidden ones hidden. They remember their home and position and
184// return when it is plugged in again. The last output becomes the
185// placeholder again: it keeps its windows for the next output.
186func (c *Core) removeScreen(name string) {
187	i := c.screenIndex(name)
188	if i < 0 {
189		return
190	}
191	if len(c.screens) == 1 {
192		c.screens[0].info = ports.OutputInfo{}
193		c.screens[0].layers = nil
194		return
195	}
196	gone := c.screens[i]
197	focused := c.cur()
198	c.screens = slices.Delete(c.screens, i, i+1)
199	c.focusScreen = max(slices.Index(c.screens, focused), 0)
200	host := c.cur()
201	for pos, w := range gone.mon.Workspaces {
202		if w.empty() && w.Name == "" {
203			continue
204		}
205		switch {
206		case host.mon.matches(w.home):
207			// A guest from the host goes home.
208			w.home = ""
209			host.mon.adopt(w, false, w.homePos)
210			continue
211		case w.home == "":
212			w.home, w.homePos = gone.info.Key(), pos
213		}
214		host.mon.adopt(w, false, len(host.mon.Workspaces))
215	}
216	for _, w := range gone.mon.hidden {
217		switch {
218		case host.mon.matches(w.home):
219			w.home = ""
220		case w.home == "":
221			w.home = gone.info.Key()
222		}
223		host.mon.adopt(w, true, 0)
224	}
225	c.order()
226}
227
228// settleGuests returns guests to their connected home monitor, at their
229// original positions. A guest on screen stays until the user leaves it
230// (golden rule: automatic events never move the user).
231func (c *Core) settleGuests() {
232	for _, home := range c.screens {
233		type guest struct {
234			w      *Workspace
235			hidden bool
236		}
237		var list []guest
238		for _, host := range c.screens {
239			if host == home {
240				continue
241			}
242			for _, w := range host.mon.all() {
243				if home.mon.matches(w.home) && host.mon.Current() != w {
244					list = append(list, guest{w, host.mon.isHidden(w)})
245					host.mon.take(w)
246				}
247			}
248		}
249		slices.SortStableFunc(list, func(a, b guest) int { return a.w.homePos - b.w.homePos })
250		// A monitor showing an empty workspace (just plugged in) shows
251		// its first returning workspace instead.
252		idle := home.mon.Current().empty() && home.mon.Current().Name == ""
253		for _, g := range list {
254			g.w.home = ""
255			home.mon.adopt(g.w, g.hidden, g.w.homePos)
256			if idle && !g.hidden {
257				home.mon.show(g.w)
258				idle = false
259			}
260		}
261	}
262}
263
264// named applies the named workspaces from config. One that exists stays on
265// its screen; a new one is created on its home monitor, or on the first
266// screen as a guest when its home is not connected.
267func (c *Core) named() {
268	target := map[string]*screen{}
269	for _, spec := range c.specs {
270		if s, _ := c.byName(spec.Name); s != nil {
271			target[spec.Name] = s
272			continue
273		}
274		target[spec.Name] = c.screens[0]
275		for _, s := range c.screens {
276			if s.mon.matches(spec.Monitor) {
277				target[spec.Name] = s
278			}
279		}
280	}
281	for _, s := range c.screens {
282		var mine []NamedWorkspace
283		for _, spec := range c.specs {
284			if target[spec.Name] == s {
285				mine = append(mine, spec)
286			}
287		}
288		s.mon.SetNamed(mine)
289		s.mon.useSpecs(c.specs)
290	}
291	for _, spec := range c.specs {
292		s, w := c.byName(spec.Name)
293		w.home = ""
294		if spec.Monitor != "" && !s.mon.matches(spec.Monitor) {
295			w.home = spec.Monitor
296		}
297	}
298}
299
300// settings applies output-wide config to a monitor.
301func (c *Core) settings(m *Monitor) {
302	m.SetOverflow(c.overflow)
303	m.SetMaxColumns(c.cfg.Layout.MaxColumns)
304	m.SetPresets(c.presets)
305	m.SetGaps(c.cfg.Layout.Gaps)
306	m.SetBorder(c.cfg.Border.Width)
307	m.SetFollowMove(c.cfg.Focus.FollowMove)
308}
309
310// neighbor returns the screen index left (-1) or right (+1) of the focused
311// one, or -1 at the edge.
312func (c *Core) neighbor(dir int) int {
313	i := c.focusScreen + dir
314	if i < 0 || i >= len(c.screens) {
315		return -1
316	}
317	return i
318}
319
320// moveWorkspace moves the workspace on screen to the neighbor screen, where
321// it is appended and shown; it becomes its new home (ADR 011).
322func (c *Core) moveWorkspace(dir int) {
323	to := c.neighbor(dir)
324	if to < 0 {
325		return
326	}
327	from := c.cur()
328	w := from.mon.Current()
329	if w.empty() && w.Name == "" {
330		return
331	}
332	hidden := from.mon.isHidden(w)
333	from.mon.take(w)
334	w.home = ""
335	dst := c.screens[to]
336	dst.mon.adopt(w, hidden, len(dst.mon.Workspaces))
337	dst.mon.show(w)
338	c.focusScreen = to
339}

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