v0.2.0 80c61e3

move_pane_postcommit.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"errors"
6	"os"
7
8	"github.com/bnema/vev/internal/domain"
9	"github.com/bnema/vev/internal/ports"
10)
11
12// movePanePostcommitPlan is captured while the move's architecture and owner
13// fences still protect publication. It contains only immutable pointers,
14// generations, and exact attachment tokens needed by fallible follow-up work.
15func firstMovePane(panes []*pane) *pane {
16	if len(panes) == 0 {
17		return nil
18	}
19	return panes[0]
20}
21
22type movePanePostcommitPlan struct {
23	source            *session
24	destination       *session
25	sourceTab         *tab
26	destinationTab    *tab
27	movedPane         *pane
28	movedPanes        []*pane
29	sourceAttachments []*attachedClient
30	syncCleanup       syncTimerCleanup
31	frozenEffects     frozenAttachmentEffectGates
32	effectsFrozen     bool
33	unlockDispatch    func()
34	reservation       *moveLifecycleReservation
35	oldTabCancel      context.CancelFunc
36
37	sourceTabRemoved bool
38	sourceEmpty      bool
39
40	retiredParked            []parkedAttachmentRetirement
41	retiredAttachments       []detachedAttachmentSnapshot
42	sourceMetadata           domain.CatalogueMetadataUpdate
43	sourceMetadataValid      bool
44	destinationMetadata      domain.CatalogueMetadataUpdate
45	destinationMetadataValid bool
46}
47
48// execute runs only after every resize-owner fence and architecture lock is
49// released. Generation-bound tokens make stale effects drop rather than route
50// back to the retired source.
51func (p movePanePostcommitPlan) execute(d *Daemon) {
52	if p.oldTabCancel != nil {
53		p.oldTabCancel()
54	} else if p.sourceTabRemoved && p.sourceTab != nil && p.sourceTab.cancel != nil {
55		p.sourceTab.cancel()
56	}
57	panes := p.movedPanes
58	if len(panes) == 0 && p.movedPane != nil {
59		panes = []*pane{p.movedPane}
60	}
61	for _, cleanupClient := range p.sourceAttachments {
62		for _, movedPane := range panes {
63			if cleanupClient.overlays != nil {
64				cleanupClient.overlays.clearCopyModeForPane(movedPane)
65			}
66			cleanupClient.pruneCaptureFrames(movedPane)
67		}
68	}
69
70	// Snapshot dirtiness is admitted while lifecycle reservations still pin
71	// both sessions. Repository publication remains outside every lock below.
72	markSnapshotDirty(p.destination)
73	if !p.sourceEmpty {
74		markSnapshotDirty(p.source)
75	}
76	p.syncCleanup.finish()
77	if p.effectsFrozen {
78		p.frozenEffects.unfreeze()
79	}
80	p.unlockDispatch()
81	p.reservation.Release()
82
83	// Repair attachment-local stable targets after shared topology publication
84	// and after the move dispatch locks have been released.
85	p.source.repairAttachmentViews()
86	if p.destination != p.source {
87		p.destination.repairAttachmentViews()
88	}
89
90	if !moveSessionRetired(d, p.source) {
91		sourceLayoutTab := p.sourceTab
92		if p.sourceTabRemoved {
93			sourceLayoutTab = p.source.firstTab()
94		}
95		if sourceLayoutTab != nil && sourceLayoutTab != p.destinationTab {
96			d.applyTabLayout(p.source, sourceLayoutTab)
97		}
98	}
99	d.applyTabLayout(p.destination, p.destinationTab)
100	if p.sourceMetadataValid && !p.sourceEmpty {
101		d.markCatalogueDirty(p.sourceMetadata)
102	}
103	if p.destinationMetadataValid {
104		d.markCatalogueDirty(p.destinationMetadata)
105	}
106	for _, ac := range p.destination.snapshotAttachments() {
107		d.invalidateRender(p.destination, ac, true, "move.go")
108	}
109	if !p.sourceEmpty && p.source != p.destination {
110		for _, ac := range p.source.snapshotAttachments() {
111			d.invalidateRender(p.source, ac, true, "move.go")
112		}
113	}
114	if p.sourceEmpty {
115		retireEmptySessionAfterMove(d, p.source)
116	}
117	for _, attachment := range p.retiredAttachments {
118		d.unregisterPreview(attachment.ac)
119		attachment.ac.clearCaptureFrames()
120		d.notifyDetachedSnapshotAsync(attachment, ports.ReasonSessionKilled)
121	}
122	for _, retirement := range p.retiredParked {
123		if retirement.parked == nil || retirement.parked.ac == nil {
124			continue
125		}
126		d.unregisterPreview(retirement.parked.ac)
127		retirement.parked.ac.clearCaptureFrames()
128	}
129	d.finishParkedAttachmentRetirements(p.retiredParked)
130	if p.sourceEmpty && p.sourceMetadataValid && d.persistEnabled {
131		if err := d.beginSnapshotPurge(p.source.name, p.source.incarnation); err == nil {
132			if err := d.finishSnapshotPurge(d.serveCtx, p.source.name, p.source.incarnation); err != nil {
133				d.log.Warn("moving final pane source purge failed", "err", err, "session", p.source.name)
134			}
135		}
136	}
137}
138
139func moveSessionRetired(d *Daemon, sess *session) bool {
140	if d == nil || sess == nil {
141		return true
142	}
143	d.mu.Lock()
144	_, live := d.sessions[sess.id]
145	d.mu.Unlock()
146	return !live
147}
148
149// retireEmptySessionAfterMove completes only source-owned lifecycle work. The
150// moved pane has already been removed from source.tabs, so stopInMemoryLifecycle
151// cannot cancel or close the transferred PTY.
152func retireEmptySessionAfterMove(d *Daemon, sess *session) {
153	if d == nil || sess == nil {
154		return
155	}
156	sess.stopInMemoryLifecycle()
157	if coordinator := sess.renderCoordinator(); coordinator != nil {
158		coordinator.beginSessionTeardown().finish()
159		coordinator.waitForTimerWorkers()
160	}
161	sess.mu.Lock()
162	clipFiles := append([]string(nil), sess.clipFiles...)
163	sess.clipFiles = nil
164	sess.mu.Unlock()
165	for _, path := range clipFiles {
166		if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
167			d.log.Warn("removing clipboard temp file after move failed", "err", err, "path", path)
168		}
169	}
170}

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 12

Tags 11

↑↓ moveenter openesc close