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

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 23

Tags 10

↑↓ moveenter openesc close