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

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