capture_workspace.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5
6	"github.com/bnema/neferwl/internal/ports"
7)
8
9// captureWorkspace is the workspace a capture session renders off screen
10// while it is not the one on screen. Its windows keep a real layout in the
11// workspace viewport: they are sized (configure Captured) and drawn into
12// Scene.CaptureScene, but they are hidden on the physical layout, so they
13// take no pointer input and no physical presentation feedback.
14//
15// Only the core goroutine touches it. The index map and its storage are
16// reused by every publish (cleared, never reallocated once warm).
17type captureWorkspace struct {
18	sc         *screen
19	ws         *Workspace // nil: no workspace is captured off screen
20	frame      Rect       // ws.Output: the viewport, in monitor coordinates
21	placements []Placement
22	index      map[WindowID]int
23}
24
25// active reports whether a hidden workspace is captured.
26func (cw *captureWorkspace) active() bool { return cw != nil && cw.ws != nil }
27
28// reset forgets the captured workspace, keeping the storage.
29func (cw *captureWorkspace) reset() {
30	cw.sc, cw.ws, cw.frame = nil, nil, Rect{}
31	cw.placements = nil
32	clear(cw.index)
33}
34
35// load lays out ws in its own viewport. ws.Layout allocates the placements
36// (the baseline of the existing layout); the index reuses its map.
37func (cw *captureWorkspace) load(sc *screen, ws *Workspace) {
38	cw.sc, cw.ws, cw.frame = sc, ws, ws.Output
39	cw.placements = ws.Layout()
40	if cw.index == nil {
41		cw.index = make(map[WindowID]int, len(cw.placements))
42	}
43	clear(cw.index)
44	for i := range cw.placements {
45		cw.index[cw.placements[i].ID] = i
46	}
47}
48
49// placement is the real placement of a window of the captured workspace.
50func (cw *captureWorkspace) placement(id WindowID) *Placement {
51	if !cw.active() {
52		return nil
53	}
54	if i, ok := cw.index[id]; ok {
55		return &cw.placements[i]
56	}
57	return nil
58}
59
60// captureTrack follows the session view of this publish: it lays out the
61// captured workspace when it is not drawn as itself (another workspace is
62// on screen, or the overview shows it as a preview) and forgets it
63// otherwise. It runs
64// right after captureEvaluate, before the popups and configures that use it.
65func (c *Core) captureTrack(view *capView) {
66	// Tracking follows the decided view, not whether frames were taken: while
67	// the captured workspace slides in its own viewport its windows stay
68	// captured (not suspended) and its popups stay.
69	if view == nil || view.hidden == nil {
70		c.configures.cw.reset()
71		return
72	}
73	c.configures.cw.load(view.hiddenScr, view.hidden)
74}
75
76// captureConfigure picks how a physical placement p on sc is configured:
77// for a window of the captured workspace it returns the real placement and
78// the target sized for it, else nil and t unchanged. Pass both to
79// configures.nextWithCapture.
80func (c *Core) captureConfigure(sc *screen, p Placement, t configureTarget) (*Placement, configureTarget) {
81	cw := &c.configures.cw
82	if !cw.active() || cw.sc != sc {
83		return nil, t
84	}
85	cp := cw.placement(p.ID)
86	if cp == nil {
87		return nil, t
88	}
89	t.captureArea = cw.frame
90	if !p.Hidden {
91		// An overview preview keeps its real size and physical state.
92		return cp, t
93	}
94	t.view = whole(cw.frame) // the capture image has no panels
95	t.focused = false
96	t.client, t.imposed = Rect{}, false
97	if !cp.Hidden {
98		t.client, t.imposed = c.clientRect(*cp), cw.ws.imposedFloat(cp.ID)
99	}
100	return cp, t
101}
102
103// captureRect is where a window of the captured workspace, or a popup
104// hanging from one, is: its client area in monitor coordinates. Unlike
105// windowRect it knows hidden workspaces; it never serves input. A stale
106// captured workspace (its screen or workspace gone) has no geometry.
107func (c *Core) captureRect(id WindowID) (*screen, Rect, bool) {
108	cw := &c.configures.cw
109	if !cw.active() || !slices.Contains(c.screens, cw.sc) || !cw.sc.mon.has(cw.ws) {
110		return nil, Rect{}, false
111	}
112	if p := c.popups[id]; p != nil {
113		sc, pr, ok := c.captureRect(p.parent)
114		if !ok {
115			return nil, Rect{}, false
116		}
117		if cp := cw.placement(c.popupRoot(id)); cp != nil && cp.Peek {
118			return nil, Rect{}, false
119		}
120		return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true
121	}
122	cp := cw.placement(id)
123	if cp == nil || cp.Hidden || !cp.Rect.Overlaps(cw.frame) {
124		return nil, Rect{}, false
125	}
126	return cw.sc, c.clientRect(*cp), true
127}
128
129// popupRect is windowRect that also knows the captured workspace; physical
130// tells which one answered (a single physical lookup).
131func (c *Core) popupRect(id WindowID) (sc *screen, r Rect, physical, ok bool) {
132	if sc, r, ok := c.windowRect(id); ok {
133		return sc, r, true, true
134	}
135	sc, r, ok = c.captureRect(id)
136	return sc, r, false, ok
137}
138
139// captureScene is the captured workspace drawn for capture only: a scene the
140// size of its viewport, whose windows and lines are rebased on the viewport
141// origin. It has no layers and no capture of its own. nil without a
142// captured workspace.
143func (c *Core) captureScene(seq uint64) *ports.Scene {
144	cw := &c.configures.cw
145	if !cw.active() {
146		return nil
147	}
148	f := cw.frame
149	// The capture image has no panels: all of it is seen.
150	view := whole(f)
151	rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r }
152	s := &ports.Scene{
153		Security: c.security,
154		Output:   cw.sc.name(), Seq: seq, OutputWidth: f.W, OutputHeight: f.H, WorkspaceClip: Rect{W: f.W, H: f.H},
155		Scale: cw.sc.scale, Background: c.cfg.Background.Color,
156		Border:  ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive},
157		Windows: make([]ports.SceneWindow, 0, len(cw.placements)),
158		Dim:     floatDim(cw.placements, view, c.cfg.Floating.Dim),
159	}
160	// A hidden workspace lights nothing: its focus is not shown.
161	for _, p := range cw.placements {
162		w := ports.SceneWindow{ID: p.ID, Focused: false, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview}
163		if !p.Hidden {
164			w.Rect = rebase(p.Rect)
165		}
166		w.Dim = c.peekDim(p)
167		s.Windows = append(s.Windows, w)
168	}
169	s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), view, false)
170	for i := range s.Separators {
171		s.Separators[i].Rect = rebase(s.Separators[i].Rect)
172	}
173	for _, id := range c.popupOrder {
174		p := c.popups[id]
175		if p == nil || !p.mapped {
176			continue
177		}
178		if _, r, ok := c.captureRect(id); ok {
179			s.Windows = append(s.Windows, ports.SceneWindow{ID: id, Rect: rebase(r), Popup: true})
180		}
181	}
182	return s
183}

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