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)