v0.3.0 99dc266

move_pane_candidate.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5	"fmt"
6	"math"
7
8	"github.com/bnema/vev/internal/domain"
9	"github.com/bnema/vev/internal/usecase/layout"
10)
11
12// movePaneCandidate contains only unpublished topology and identity changes.
13// The caller must revalidate the live tabs before committing these values.
14type movePaneCandidate struct {
15	pane *pane
16
17	sourceID        layout.PaneID
18	destinationID   layout.PaneID
19	removeSourceTab bool
20
21	sourceTree            *layout.Tree
22	sourcePlacements      []layout.Placement
23	destinationTree       *layout.Tree
24	destinationPlacements []layout.Placement
25	destinationNextPaneID int
26	sourceGeneration      uint64
27	destinationGeneration uint64
28}
29
30// prepareMovePaneCandidate builds both affected layouts without publishing any
31// tab or pane state. The caller is responsible for holding and later
32// revalidating the source and destination tab state.
33func prepareMovePaneCandidate(source, destination *tab, moved *pane) (*movePaneCandidate, error) {
34	if source == nil || destination == nil || source == destination || moved == nil ||
35		source.tree == nil || source.tree.Root == nil || source.panes[moved.id] != moved ||
36		!layout.ContainsLeaf(source.tree.Root, moved.id) || destination.tree == nil || destination.tree.Root == nil ||
37		destination.panes[destination.tree.Focus] == nil || !layout.ContainsLeaf(destination.tree.Root, destination.tree.Focus) {
38		return nil, errMovePaneInvalid
39	}
40
41	candidate := &movePaneCandidate{
42		pane:                  moved,
43		sourceID:              moved.id,
44		sourceGeneration:      source.layoutGeneration,
45		destinationGeneration: destination.layoutGeneration,
46	}
47
48	sourceLeaves := layout.LeafIDs(source.tree.Root)
49	if len(sourceLeaves) == 1 {
50		switch source.floating.state {
51		case floatingWarming:
52			return nil, errMoveFloatingWarming
53		case floatingHidden, floatingVisible:
54			return nil, errMoveFinalSourceFloating
55		}
56		candidate.removeSourceTab = true
57	} else {
58		candidate.sourceTree = source.tree.Clone()
59		if err := candidate.sourceTree.Close(moved.id); err != nil {
60			return nil, errMovePaneInvalid
61		}
62		var ok bool
63		candidate.sourcePlacements, ok = layout.Solve(candidate.sourceTree.Root, tabArea(source))
64		if !ok || candidate.sourceTree.Focus == "" || !layout.ContainsLeaf(candidate.sourceTree.Root, candidate.sourceTree.Focus) {
65			return nil, errMoveTooSmall
66		}
67	}
68
69	var err error
70	candidate.destinationID, candidate.destinationNextPaneID, err = allocateMovePaneID(destination, moved.id)
71	if err != nil {
72		return nil, err
73	}
74	candidate.destinationTree = destination.tree.Clone()
75	area := tabArea(destination)
76	if err := candidate.destinationTree.Split(destination.tree.Focus, layout.Right, true, candidate.destinationID, area); err != nil {
77		if errors.Is(err, layout.ErrTooSmall) {
78			return nil, errMoveTooSmall
79		}
80		return nil, errMovePaneInvalid
81	}
82	var ok bool
83	candidate.destinationPlacements, ok = layout.Solve(candidate.destinationTree.Root, area)
84	if !ok {
85		return nil, errMoveTooSmall
86	}
87	return candidate, nil
88}
89
90func allocateMovePaneID(destination *tab, sourceID layout.PaneID) (layout.PaneID, int, error) {
91	next := max(destination.nextPaneID, 1)
92	for {
93		id := layout.PaneID(fmt.Sprintf("pane-%d", next))
94		_, mapped := destination.panes[id]
95		available := id != sourceID && !mapped && !layout.ContainsLeaf(destination.tree.Root, id)
96		if available {
97			if next == math.MaxInt {
98				return id, next, nil
99			}
100			return id, next + 1, nil
101		}
102		if next == math.MaxInt {
103			return "", 0, errMovePaneIDExhausted
104		}
105		next++
106	}
107}
108
109func tabArea(tb *tab) domain.Rect {
110	return domain.Rect{Width: tb.size.Cols, Height: tb.size.Rows}
111}

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