monitor.go RawHistoryFolder

1package core
2
3import (
4	"iter"
5	"slices"
6)
7
8// Monitor owns an ordered list of workspaces for one output (ADR 011). Numbers
9// are positions: Cmd+N targets Workspaces[N-1]. An empty workspace always sits
10// below the last one, and an empty unnamed workspace is removed once left.
11// Output-wide settings apply to every workspace.
12//
13// Named workspaces come from config and are never removed while configured.
14// Named workspaces are outside normal numbered up/down navigation. Their
15// `workspace <name>` binds and the overview can show them.
16type Monitor struct {
17	// Name is the connector; Key identifies the physical monitor (make,
18	// model and serial, else the connector). Workspaces remember the Key of
19	// their home monitor.
20	Name, Key  string
21	Workspaces []*Workspace
22	Active     int
23	hidden     []*Workspace
24	shown      *Workspace // hidden workspace on screen, nil for Workspaces[Active]
25	back       *Workspace // where a named workspace toggle returns to
26	template   Workspace
27	// slideBuf and slideShown are slideLayout's scratch (the neighbor's
28	// layout and its placements by window), reused and never handed out.
29	slideBuf   []Placement
30	slideShown map[WindowID]Placement
31	nextID     *uint64 // shared by the monitors of one Core; owner goroutine only
32	named      []NamedWorkspace
33	// followMove shows the target workspace after a move to it.
34	followMove bool
35	// stashCapture puts new windows in the shown stash (stash.capture).
36	stashCapture bool
37	// switchView slides the view between numbered workspaces: the view is
38	// at Active+switchView.off, in workspaces.
39	switchView slide
40	// switchList is the numbered list a landing slide measures from (the
41	// one its swipe began on); switchView.off is then from the current
42	// workspace's place in it.
43	switchList []*Workspace
44	// ov holds the overview selection and its Escape snapshot.
45	ov overviewState
46	// overviewOpens counts the overview's openings: a swipe sliding when
47	// one happens is dropped (gesture.go).
48	overviewOpens int
49}
50
51// NamedWorkspace configures a named workspace. Zero MaxColumns and an empty
52// Overflow follow the monitor defaults.
53type NamedWorkspace struct {
54	Name string
55	// Monitor is the home monitor (connector or key); "" follows the focus.
56	Monitor    string
57	MaxColumns int
58	Overflow   Overflow
59	// Size overrides the logical width and height; a zero side inherits
60	// the monitor and the scale is always the monitor's.
61	Size [2]int
62}
63
64func NewMonitor() *Monitor { return newMonitor("", "") }
65
66func newMonitor(name, key string) *Monitor {
67	return newMonitorWithIDs(name, key, new(uint64))
68}
69
70func newMonitorWithIDs(name, key string, nextID *uint64) *Monitor {
71	m := &Monitor{Name: name, Key: key, nextID: nextID}
72	m.normalize()
73	return m
74}
75
76// matches reports whether home designates this monitor, by key or connector.
77func (m *Monitor) matches(home string) bool {
78	return home != "" && (home == m.Key || home == m.Name)
79}
80
81// all yields every workspace, numbered then hidden, without copying.
82// Callers must not add or remove workspaces while ranging.
83func (m *Monitor) all() iter.Seq[*Workspace] {
84	return func(yield func(*Workspace) bool) {
85		for _, w := range m.Workspaces {
86			if !yield(w) {
87				return
88			}
89		}
90		for _, w := range m.hidden {
91			if !yield(w) {
92				return
93			}
94		}
95	}
96}
97
98// isHidden reports whether w is in the hidden list.
99func (m *Monitor) isHidden(w *Workspace) bool { return indexOf(m.hidden, w) >= 0 }
100
101// take removes w from the monitor. The monitor keeps a workspace on screen:
102// the one at the same position, or the numbered active one.
103func (m *Monitor) take(w *Workspace) {
104	if m.back == w {
105		m.back = nil
106	}
107	if i := indexOf(m.hidden, w); i >= 0 {
108		m.hidden = append(m.hidden[:i], m.hidden[i+1:]...)
109		if m.shown == w {
110			m.shown = nil
111		}
112		m.normalize()
113		return
114	}
115	if i := indexOf(m.Workspaces, w); i >= 0 {
116		m.removeNumbered(i)
117		m.normalize()
118	}
119}
120
121// adopt adds a workspace from another monitor: hidden, or numbered at pos
122// (clamped above the trailing empty workspace). It takes this monitor's
123// output-wide settings and does not change what is on screen.
124func (m *Monitor) adopt(w *Workspace, hidden bool, pos int) {
125	w.presets = append([]Width(nil), m.template.presets...)
126	w.Gaps = m.template.Gaps
127	w.border = m.template.border
128	w.stashWidth, w.stashGap = m.template.stashWidth, m.template.stashGap
129	w.SetOutput(m.template.Output.W, m.template.Output.H)
130	w.SetUsable(m.template.Usable)
131	if hidden {
132		m.hidden = append(m.hidden, w)
133	} else {
134		// normalize keeps a trailing empty workspace: stay above it.
135		at := min(max(pos, 0), len(m.Workspaces)-1)
136		m.Workspaces = slices.Insert(m.Workspaces, at, w)
137		if at <= m.Active {
138			m.Active++
139		}
140	}
141	m.applyNamed()
142	m.normalize()
143}
144
145// useSpecs sets the overrides of every named workspace, including those
146// that live on other monitors and may move here.
147func (m *Monitor) useSpecs(all []NamedWorkspace) {
148	m.named = append([]NamedWorkspace(nil), all...)
149	m.applyNamed()
150}
151
152// Current is the workspace on screen.
153func (m *Monitor) Current() *Workspace {
154	if m.shown != nil {
155		return m.shown
156	}
157	return m.Workspaces[m.Active]
158}
159
160func (m *Monitor) newWorkspace() *Workspace {
161	w := m.template
162	w.colBuf, w.rowBuf, w.tileBuf, w.zoomBuf = nil, nil, nil, nil
163	w.presets = append([]Width(nil), m.template.presets...)
164	w.Columns, w.Floats, w.maximized, w.floatFocus, w.home = nil, nil, nil, false, ""
165	w.overviewAfter = nil
166	w.Stash, w.stashAt, w.stashFocus, w.stashHidden, w.hiddenFullscreen = nil, 0, false, false, 0
167	(*m.nextID)++
168	w.ID = *m.nextID
169	return &w
170}
171
172// normalize keeps one trailing empty unnamed workspace and drops other empty
173// unnamed ones that are not active. The active workspace never changes.
174func (m *Monitor) normalize() {
175	spare := func(i int) bool { return m.Workspaces[i].empty() && m.Workspaces[i].Name == "" }
176	for i := len(m.Workspaces) - 2; i >= 0; i-- {
177		if spare(i) && i != m.Active {
178			m.Workspaces = append(m.Workspaces[:i], m.Workspaces[i+1:]...)
179			if i < m.Active {
180				m.Active--
181			}
182		}
183	}
184	for n := len(m.Workspaces); n >= 2 && spare(n-1) && spare(n-2) && m.Active != n-1; n-- {
185		m.Workspaces = m.Workspaces[:n-1]
186	}
187	if n := len(m.Workspaces); n == 0 || !spare(n-1) {
188		m.Workspaces = append(m.Workspaces, m.newWorkspace())
189	}
190	for _, w := range m.hidden {
191		if indexOf(m.Workspaces, w.overviewAfter) < 0 {
192			w.overviewAfter = nil
193		}
194	}
195}
196
197// Focus switches to the numbered workspace at index i, clamped to the list.
198func (m *Monitor) Focus(i int) {
199	i = min(max(i, 0), len(m.Workspaces)-1)
200	m.stopSwitch()
201	m.shown = nil
202	m.Active = i
203	m.normalize()
204}
205
206// MoveWorkspace swaps the active numbered workspace with its neighbor up
207// (dir -1) or down (dir 1); the view follows it. It never passes the
208// trailing empty workspace.
209func (m *Monitor) MoveWorkspace(dir int) {
210	to := m.Active + dir
211	if m.shown != nil || (dir != -1 && dir != 1) || to < 0 || to >= len(m.Workspaces)-1 || m.Active >= len(m.Workspaces)-1 {
212		return
213	}
214	m.Workspaces[m.Active], m.Workspaces[to] = m.Workspaces[to], m.Workspaces[m.Active]
215	m.stopSwitch()
216	m.Active = to
217	m.normalize()
218}
219
220// FocusNumber switches to workspace n (1-based); past the end means the last.
221func (m *Monitor) FocusNumber(n int) { m.Focus(n - 1) }
222
223// show puts w on screen, numbered or hidden.
224func (m *Monitor) show(w *Workspace) {
225	if i := indexOf(m.Workspaces, w); i >= 0 {
226		m.Focus(i)
227		return
228	}
229	m.stopSwitch()
230	m.shown = w
231	m.normalize()
232}
233
234// ToggleNamed shows the named workspace, or returns to the previous one when
235// it is already on screen. Unknown names do nothing.
236func (m *Monitor) ToggleNamed(name string) {
237	target := m.byName(name)
238	switch cur := m.Current(); {
239	case target == nil:
240	case target != cur:
241		anchor := cur
242		if m.isHidden(cur) {
243			anchor = cur.overviewAfter
244		}
245		if indexOf(m.Workspaces, anchor) < 0 {
246			anchor = m.Workspaces[m.Active]
247		}
248		target.overviewAfter = anchor
249		m.back = cur
250		m.show(target)
251	case m.back != nil && m.has(m.back):
252		back := m.back
253		m.back = nil
254		m.show(back)
255	case m.shown != nil:
256		m.back = nil
257		m.shown = nil
258		m.normalize()
259	}
260}
261
262// byName returns the configured workspace with that name, or nil.
263func (m *Monitor) byName(name string) *Workspace {
264	for w := range m.all() {
265		if w.Name == name {
266			return w
267		}
268	}
269	return nil
270}
271
272func indexOf(list []*Workspace, w *Workspace) int {
273	for i, v := range list {
274		if v == w {
275			return i
276		}
277	}
278	return -1
279}
280
281// has reports whether w still belongs to the monitor.
282func (m *Monitor) has(w *Workspace) bool {
283	return indexOf(m.Workspaces, w) >= 0 || indexOf(m.hidden, w) >= 0
284}
285
286// find returns the workspace holding the window; i is -1 for hidden ones.
287func (m *Monitor) find(id WindowID) (*Workspace, int) {
288	for i, w := range m.Workspaces {
289		if w.has(id) {
290			return w, i
291		}
292	}
293	for _, w := range m.hidden {
294		if w.has(id) {
295			return w, -1
296		}
297	}
298	return nil, -1
299}
300
301// AddWindow places a new window on the workspace on screen; with
302// stash.capture, in its stash when shown.
303func (m *Monitor) AddWindow(id WindowID) {
304	if w, _ := m.find(id); w != nil {
305		return
306	}
307	if w := m.Current(); !m.stashCapture || !w.captureStash(id) {
308		w.AddWindow(id)
309	}
310	m.normalize()
311}
312
313// AddFloating adds a floating window to the active workspace.
314func (m *Monitor) AddFloating(id WindowID, width, height int) {
315	if w, _ := m.find(id); w != nil {
316		return
317	}
318	m.Current().AddFloating(id, width, height)
319	m.normalize()
320}
321
322// RemoveWindow drops the window wherever it is; focus stays on the active
323// workspace.
324func (m *Monitor) RemoveWindow(id WindowID) {
325	if w, _ := m.find(id); w != nil {
326		w.RemoveWindow(id)
327		m.normalize()
328	}
329}
330
331// SetFullscreen applies a client request, in place. A client request never
332// moves the user (ADR 011): the view and the focus stay where they are.
333func (m *Monitor) SetFullscreen(id WindowID, on bool) {
334	if w, _ := m.find(id); w != nil {
335		w.SetFullscreen(id, on)
336	}
337}
338
339// ToggleFullscreen is the bind: the focused window goes fullscreen in place,
340// or leaves it. Focus moves leave it too (Workspace.leaveCover).
341func (m *Monitor) ToggleFullscreen() { m.Current().ToggleFullscreen() }
342
343// MoveToWorkspace moves the focused column (or only the focused window) to
344// numbered workspace index i. A floating window always moves alone. Focus
345// stays here unless followMove is set (ADR 017).
346func (m *Monitor) MoveToWorkspace(i int, column bool) {
347	i = min(max(i, 0), len(m.Workspaces)-1)
348	cur, to := m.Current(), m.Workspaces[i]
349	id, ok := cur.Focused()
350	if !ok || to == cur {
351		return
352	}
353	if f := cur.floatIndex(id); f >= 0 {
354		fl := cur.Floats[f]
355		if fl.free && fl.back != nil {
356			// Its column place is on this workspace.
357			fl = fl.rehome()
358		}
359		cur.RemoveWindow(id)
360		to.AddFloating(id, fl.W, fl.H)
361		to.Floats[len(to.Floats)-1] = fl
362		// As receive: a fullscreen window would hide it.
363		to.Activate(id)
364	} else if f := cur.stashIndex(id); f >= 0 {
365		// It joins the end of the stash there, shown and selected.
366		fl := cur.Stash[f].rehome()
367		cur.RemoveWindow(id)
368		to.addStash(fl)
369		to.Activate(id)
370	} else {
371		col := Column{Windows: []WindowID{id}}
372		ok := true
373		if column {
374			col, ok = cur.takeColumn()
375		} else {
376			_, ok = cur.takeWindow()
377		}
378		if !ok {
379			return
380		}
381		to.receive(col, -1)
382	}
383	if m.followMove {
384		m.Active = i
385		m.shown = nil
386	}
387	m.normalize()
388}
389
390// receive adds a column moved in from another workspace and focuses it: at
391// index at, or where a new window goes when at < 0. A fullscreen window
392// there is left, so the moved window is seen.
393func (w *Workspace) receive(col Column, at int) {
394	id := col.Windows[col.Focus]
395	switch {
396	case at < 0:
397		w.addColumn(col)
398	default:
399		w.insertColumn(at, col)
400	}
401	w.Activate(id)
402}
403
404// Focused is the focused window of the workspace on screen; in the
405// overview, the selected stash card when it is one.
406func (m *Monitor) Focused() (WindowID, bool) {
407	if id := m.card(); id != 0 {
408		return id, true
409	}
410	return m.Current().Focused()
411}
412
413// Layout places every window: the workspace on screen laid out, others hidden.
414// The slice is fresh, the caller's to keep; the per-frame paths use layoutInto.
415func (m *Monitor) Layout() []Placement { return m.layoutInto(nil) }
416
417// layoutInto is Layout built in dst[:0]: the result aliases dst's storage,
418// so a reused dst's previous result is overwritten and only a caller done
419// with it may pass it. It is never the monitor's own scratch.
420func (m *Monitor) layoutInto(dst []Placement) []Placement {
421	if m.ov.open {
422		return m.overviewLayout(dst[:0])
423	}
424	result := dst[:0]
425	cur := m.Current()
426	for w := range m.all() {
427		if w == cur {
428			result = w.appendLayout(result)
429			continue
430		}
431		result = w.appendHidden(result, true)
432	}
433	return m.slideLayout(result)
434}
435
436// Output is the whole logical monitor, whatever the workspaces' sizes.
437func (m *Monitor) Output() Rect { return m.template.Output }
438
439// Frame is where the workspace on screen draws and takes input, in monitor
440// coordinates: its viewport, or the whole monitor in the overview. Slides
441// involving a sized workspace remain settled inside the current frame.
442func (m *Monitor) Frame() Rect {
443	if m.ov.open {
444		return m.Output()
445	}
446	return m.Current().Output
447}
448
449// frameHas reports whether the output-local point is inside the frame.
450func (m *Monitor) frameHas(x, y float64) bool {
451	f := m.Frame()
452	return x >= float64(f.X) && x < float64(f.X+f.W) && y >= float64(f.Y) && y < float64(f.Y+f.H)
453}
454
455func (m *Monitor) each(f func(*Workspace)) {
456	f(&m.template)
457	for _, w := range m.Workspaces {
458		f(w)
459	}
460	for _, w := range m.hidden {
461		f(w)
462	}
463}
464func (m *Monitor) SetOutput(width, height int) {
465	m.each(func(w *Workspace) { w.SetOutput(width, height) })
466}
467func (m *Monitor) SetUsable(r Rect) { m.each(func(w *Workspace) { w.SetUsable(r) }) }
468func (m *Monitor) SetGaps(g int)    { m.each(func(w *Workspace) { w.SetGaps(g) }) }
469func (m *Monitor) SetBorder(b int) {
470	m.each(func(w *Workspace) { w.border = max(b, 0); w.reconcileFloats() })
471}
472
473// SetStash sets the width of a stashed window (stash.width; 0 is the
474// default) and the space between it and its neighbors (stash.gap), in
475// percent of the usable width.
476func (m *Monitor) SetStash(width, gap int) {
477	if width != 0 {
478		width = min(max(width, 10), 90)
479	}
480	m.each(func(w *Workspace) { w.stashWidth, w.stashGap = width, min(max(gap, 0), 10) })
481}
482
483// SetStashCapture makes new windows join the shown stash (stash.capture).
484func (m *Monitor) SetStashCapture(on bool) { m.stashCapture = on }
485
486func (m *Monitor) SetPresets(v []Width) { m.each(func(w *Workspace) { w.SetPresets(v) }) }
487
488// SetFollowMove makes moves to another workspace show it (focus.follow-move).
489func (m *Monitor) SetFollowMove(on bool) { m.followMove = on }
490
491// SetMaxColumns sets the default; named workspaces may override it.
492func (m *Monitor) SetMaxColumns(n int) {
493	m.template.MaxColumns = n
494	m.applyNamed()
495}
496
497// SetOverflow sets the default; named workspaces may override it.
498func (m *Monitor) SetOverflow(o Overflow) {
499	m.template.Overflow = o
500	m.applyNamed()
501}
502
503// applyNamed gives every workspace the defaults or its named overrides.
504func (m *Monitor) applyNamed() {
505	specs := map[string]NamedWorkspace{}
506	for _, s := range m.named {
507		specs[s.Name] = s
508	}
509	m.each(func(w *Workspace) {
510		s := specs[w.Name]
511		w.Overflow = m.template.Overflow
512		if s.Overflow != "" {
513			w.Overflow = s.Overflow
514		}
515		w.SetMaxColumns(m.template.MaxColumns)
516		if s.MaxColumns > 0 {
517			w.SetMaxColumns(s.MaxColumns)
518		}
519		// Named workspaces take their configured size; any other keeps its own.
520		if w.Name != "" {
521			w.SetSize(s.Size[0], s.Size[1])
522		}
523	})
524}
525
526// SetNamed applies bind-only named workspaces from config. Removed named
527// workspaces hand their windows to the active numbered workspace.
528func (m *Monitor) SetNamed(specs []NamedWorkspace) {
529	cur := m.Current()
530	want := map[string]NamedWorkspace{}
531	for _, s := range specs {
532		want[s.Name] = s
533	}
534	byName := map[string]*Workspace{}
535	for _, w := range m.Workspaces {
536		if _, ok := want[w.Name]; w.Name != "" && !ok {
537			w.Name = ""
538			w.SetSize(0, 0)
539		}
540		if w.Name != "" {
541			byName[w.Name] = w
542		}
543	}
544	var hidden []*Workspace
545	for _, w := range m.hidden {
546		if _, ok := want[w.Name]; ok {
547			byName[w.Name] = w
548			hidden = append(hidden, w)
549			continue
550		}
551		if w == cur {
552			cur = nil
553		}
554		for _, c := range w.Columns {
555			for _, id := range c.Windows {
556				m.Workspaces[m.Active].AddWindow(id)
557			}
558		}
559		for _, f := range w.Floats {
560			if f.free && f.back != nil {
561				f = f.rehome()
562			}
563			m.Workspaces[m.Active].AddFloating(f.ID, f.W, f.H)
564			m.Workspaces[m.Active].Floats[len(m.Workspaces[m.Active].Floats)-1] = f
565		}
566		m.Workspaces[m.Active].reconcileFloats()
567		for _, f := range w.Stash {
568			m.Workspaces[m.Active].Stash = append(m.Workspaces[m.Active].Stash, f.rehome())
569		}
570	}
571	m.hidden = hidden
572	m.named = append([]NamedWorkspace(nil), specs...)
573	var nextHidden []*Workspace
574	for _, s := range specs {
575		w := byName[s.Name]
576		if w == nil {
577			w = m.newWorkspace()
578			w.Name = s.Name
579		}
580		if i := indexOf(m.Workspaces, w); i >= 0 {
581			m.removeNumbered(i)
582		}
583		nextHidden = append(nextHidden, w)
584	}
585	m.hidden = nextHidden
586	if m.back != nil && !m.has(m.back) {
587		m.back = nil
588	}
589	m.shown = nil
590	switch {
591	case cur == nil:
592	case indexOf(m.hidden, cur) >= 0:
593		m.shown = cur
594	case indexOf(m.Workspaces, cur) >= 0:
595		m.Active = indexOf(m.Workspaces, cur)
596	}
597	m.applyNamed()
598	m.normalize()
599}
600
601// removeNumbered drops Workspaces[i] and keeps Active on the same workspace
602// when possible.
603func (m *Monitor) removeNumbered(i int) {
604	m.Workspaces = append(m.Workspaces[:i], m.Workspaces[i+1:]...)
605	if i < m.Active || m.Active >= len(m.Workspaces) {
606		m.Active = max(m.Active-1, 0)
607	}
608	if len(m.Workspaces) == 0 {
609		m.Workspaces = append(m.Workspaces, m.newWorkspace())
610	}
611}

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 26

Tags 10

↑↓ moveenter openesc close