move_rejection.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/bnema/vev/internal/domain/layout"
8	"github.com/bnema/vev/internal/protocol"
9)
10
11// moveRejectionReason is the daemon API category for a rejected move. It keeps
12// callers independent of topology and layout implementation errors.
13type moveRejectionReason uint8
14
15const (
16	moveRejectionInvalid moveRejectionReason = iota + 1
17	moveRejectionNoDestination
18	moveRejectionFloatingWarming
19	moveRejectionFinalSourceFloating
20	moveRejectionStaleTarget
21	moveRejectionTooSmall
22	moveRejectionPaneIDExhausted
23)
24
25// moveRejection carries a stable category while Unwrap preserves identities
26// relied on by lower-level move tests and callers.
27type moveRejection struct {
28	Reason moveRejectionReason
29	msg    string
30	cause  error
31}
32
33func (e *moveRejection) Error() string { return e.msg }
34func (e *moveRejection) Unwrap() error { return e.cause }
35
36// moveRejectionDescriptor is the canonical presentation for one rejection
37// reason. Palette and control adapters only wrap these values for their
38// respective transports.
39type moveRejectionDescriptor struct {
40	Reason          moveRejectionReason
41	PaletteCode     domain.NoticeCode
42	PaletteSeverity domain.NoticeSeverity
43	PaletteText     string
44	CommandCode     uint16
45	CommandText     string
46}
47
48func moveRejectionDescriptorFor(err error) (moveRejectionDescriptor, bool) {
49	reason, ok := moveRejectionReasonFor(err)
50	if !ok {
51		return moveRejectionDescriptor{}, false
52	}
53	return moveRejectionDescriptorByReason(reason), true
54}
55
56func moveRejectionReasonFor(err error) (moveRejectionReason, bool) {
57	var rejection *moveRejection
58	if errors.As(err, &rejection) {
59		return rejection.Reason, true
60	}
61	if errors.Is(err, errMoveLifecycleUnavailable) {
62		return moveRejectionStaleTarget, true
63	}
64	return 0, false
65}
66
67func moveRejectionDescriptorByReason(reason moveRejectionReason) moveRejectionDescriptor {
68	descriptor := moveRejectionDescriptor{
69		Reason:          reason,
70		PaletteCode:     domain.NoticeInternal,
71		PaletteSeverity: domain.NoticeError,
72		PaletteText:     "Move failed.",
73		CommandCode:     protocol.ErrNoSuchTarget,
74		CommandText:     "move request is invalid",
75	}
76	switch reason {
77	case moveRejectionNoDestination:
78		descriptor.PaletteCode = domain.NoticeSessionUnavailable
79		descriptor.PaletteSeverity = domain.NoticeWarn
80		descriptor.PaletteText = "No destination available."
81		descriptor.CommandText = "no move destination available"
82	case moveRejectionFloatingWarming:
83		descriptor.PaletteCode = domain.NoticeSessionUnavailable
84		descriptor.PaletteSeverity = domain.NoticeWarn
85		descriptor.PaletteText = "Wait for the floating pane to finish opening."
86		descriptor.CommandText = "wait for the floating pane to finish opening"
87	case moveRejectionFinalSourceFloating:
88		descriptor.PaletteCode = domain.NoticeLayoutTooSmall
89		descriptor.PaletteSeverity = domain.NoticeWarn
90		descriptor.PaletteText = "Close the floating pane or move the whole tab."
91		descriptor.CommandText = "close the floating pane or move the whole tab"
92	case moveRejectionStaleTarget:
93		descriptor.PaletteCode = domain.NoticeSessionUnavailable
94		descriptor.PaletteSeverity = domain.NoticeWarn
95		descriptor.PaletteText = "Destination is no longer available."
96		descriptor.CommandText = "move target is no longer available"
97	case moveRejectionTooSmall:
98		descriptor.PaletteCode = domain.NoticeLayoutTooSmall
99		descriptor.PaletteSeverity = domain.NoticeWarn
100		descriptor.PaletteText = "Not enough space in destination tab."
101		descriptor.CommandText = "not enough space in destination tab"
102	case moveRejectionPaneIDExhausted:
103		descriptor.PaletteText = "Destination pane IDs are exhausted."
104		descriptor.CommandText = "destination pane IDs are exhausted"
105	}
106	return descriptor
107}
108
109func movePickerUserError(err error) error {
110	if err == nil {
111		return nil
112	}
113	var userErr *domain.UserError
114	if errors.As(err, &userErr) {
115		return err
116	}
117	if errors.Is(err, errPurgeAdmissionClosed) {
118		// A transient KillAll admission rejection is retry state: surface a
119		// retryable warning instead of a permanent "Move failed" verdict.
120		return domain.UserWarn(domain.NoticeSessionUnavailable, sessionKillRetryMessage, err)
121	}
122	descriptor, ok := moveRejectionDescriptorFor(err)
123	if !ok {
124		descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)
125	}
126	if descriptor.PaletteSeverity == domain.NoticeWarn {
127		return domain.UserWarn(descriptor.PaletteCode, descriptor.PaletteText, err)
128	}
129	return domain.UserErr(descriptor.PaletteCode, descriptor.PaletteText, err)
130}
131
132func isMoveCommandError(err error) bool {
133	_, ok := moveRejectionReasonFor(err)
134	return ok
135}
136
137func moveCommandFailure(err error) protocol.CommandResult {
138	descriptor, ok := moveRejectionDescriptorFor(err)
139	if !ok {
140		descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)
141	}
142	return commandFailure(descriptor.CommandCode, descriptor.CommandText)
143}
144
145func normalizeMoveRejection(err error) error {
146	if err == nil {
147		return nil
148	}
149	var rejection *moveRejection
150	if errors.As(err, &rejection) {
151		return err
152	}
153	if errors.Is(err, errPurgeAdmissionClosed) {
154		// A transient KillAll admission rejection is retry state, not a move
155		// rejection: preserve the typed sentinel so the command and palette
156		// adapters map it consistently with the other command paths.
157		return err
158	}
159	if errors.Is(err, layout.ErrTooSmall) {
160		return errMoveTooSmall
161	}
162	if errors.Is(err, errMoveLifecycleUnavailable) {
163		return &moveRejection{Reason: moveRejectionStaleTarget, msg: errMoveStaleTarget.msg, cause: errors.Join(errMovePaneInvalid, err)}
164	}
165	return &moveRejection{Reason: moveRejectionInvalid, msg: errMovePaneInvalid.msg, cause: errors.Join(errMovePaneInvalid, err)}
166}
167
168var (
169	errMovePaneInvalid   = &moveRejection{Reason: moveRejectionInvalid, msg: "move request is invalid"}
170	errNoMoveDestination = &moveRejection{
171		Reason: moveRejectionNoDestination,
172		msg:    "no eligible move destination",
173	}
174	errMoveFloatingWarming = &moveRejection{
175		Reason: moveRejectionFloatingWarming,
176		msg:    "floating pane is still opening",
177	}
178	errMovePaneFloatingSibling = errors.New("cannot remove a source tab with a floating pane")
179	errMoveFinalSourceFloating = &moveRejection{
180		Reason: moveRejectionFinalSourceFloating,
181		msg:    "final source pane cannot move while its tab has a floating pane",
182		cause:  errMovePaneFloatingSibling,
183	}
184	errMoveStaleTarget = &moveRejection{
185		Reason: moveRejectionStaleTarget,
186		msg:    "move target is no longer available",
187		cause:  errMovePaneInvalid,
188	}
189	errMoveTooSmall = &moveRejection{
190		Reason: moveRejectionTooSmall,
191		msg:    "move destination is too small",
192		cause:  layout.ErrTooSmall,
193	}
194	errMovePaneIDExhausted = &moveRejection{
195		Reason: moveRejectionPaneIDExhausted,
196		msg:    "destination pane IDs exhausted",
197	}
198)

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