v0.2.0 80c61e3

move_rejection.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/bnema/vev/internal/ports"
8	"github.com/bnema/vev/internal/usecase/layout"
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:     ports.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	descriptor, ok := moveRejectionDescriptorFor(err)
118	if !ok {
119		descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)
120	}
121	if descriptor.PaletteSeverity == domain.NoticeWarn {
122		return domain.UserWarn(descriptor.PaletteCode, descriptor.PaletteText, err)
123	}
124	return domain.UserErr(descriptor.PaletteCode, descriptor.PaletteText, err)
125}
126
127func isMoveCommandError(err error) bool {
128	_, ok := moveRejectionReasonFor(err)
129	return ok
130}
131
132func moveCommandFailure(err error) ports.CommandResult {
133	descriptor, ok := moveRejectionDescriptorFor(err)
134	if !ok {
135		descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)
136	}
137	return commandFailure(descriptor.CommandCode, descriptor.CommandText)
138}
139
140func normalizeMoveRejection(err error) error {
141	if err == nil {
142		return nil
143	}
144	var rejection *moveRejection
145	if errors.As(err, &rejection) {
146		return err
147	}
148	if errors.Is(err, layout.ErrTooSmall) {
149		return errMoveTooSmall
150	}
151	if errors.Is(err, errMoveLifecycleUnavailable) {
152		return &moveRejection{Reason: moveRejectionStaleTarget, msg: errMoveStaleTarget.msg, cause: errors.Join(errMovePaneInvalid, err)}
153	}
154	return &moveRejection{Reason: moveRejectionInvalid, msg: errMovePaneInvalid.msg, cause: errors.Join(errMovePaneInvalid, err)}
155}
156
157var (
158	errMovePaneInvalid   = &moveRejection{Reason: moveRejectionInvalid, msg: "move request is invalid"}
159	errNoMoveDestination = &moveRejection{
160		Reason: moveRejectionNoDestination,
161		msg:    "no eligible move destination",
162	}
163	errMoveFloatingWarming = &moveRejection{
164		Reason: moveRejectionFloatingWarming,
165		msg:    "floating pane is still opening",
166	}
167	errMovePaneFloatingSibling = errors.New("cannot remove a source tab with a floating pane")
168	errMoveFinalSourceFloating = &moveRejection{
169		Reason: moveRejectionFinalSourceFloating,
170		msg:    "final source pane cannot move while its tab has a floating pane",
171		cause:  errMovePaneFloatingSibling,
172	}
173	errMoveStaleTarget = &moveRejection{
174		Reason: moveRejectionStaleTarget,
175		msg:    "move target is no longer available",
176		cause:  errMovePaneInvalid,
177	}
178	errMoveTooSmall = &moveRejection{
179		Reason: moveRejectionTooSmall,
180		msg:    "move destination is too small",
181		cause:  layout.ErrTooSmall,
182	}
183	errMovePaneIDExhausted = &moveRejection{
184		Reason: moveRejectionPaneIDExhausted,
185		msg:    "destination pane IDs exhausted",
186	}
187)

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