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/ports"
9 "github.com/bnema/vev/internal/usecase/keys"
10)
11
12const (
13 resizeStepCols = 2
14 resizeStepRows = 1
15)
16
17func (d *Daemon) resizePane(target daemonActionTarget, axis layout.Axis, delta int) error {
18 return d.mutateTargetLayout(target, true, func(candidate *layout.Tree, area domain.Rect) error {
19 originalFocus := candidate.Focus
20 candidate.Focus = target.pane.id
21 if err := candidate.ResizeFocus(axis, delta, area); err != nil {
22 return err
23 }
24 candidate.Focus = originalFocus
25 return nil
26 })
27}
28
29func (d *Daemon) equalizePanes(target daemonActionTarget) error {
30 return d.mutateTargetLayout(target, false, func(candidate *layout.Tree, area domain.Rect) error {
31 return candidate.Equalize(area)
32 })
33}
34
35func resizeUserError(err error) error {
36 switch {
37 case errors.Is(err, layout.ErrNotInSplit):
38 return domain.UserWarn(domain.NoticeLayoutTooSmall, "pane is not in a split", err)
39 case errors.Is(err, layout.ErrTooSmall):
40 return domain.UserWarn(domain.NoticeLayoutTooSmall, "pane cannot be resized further", err)
41 default:
42 return err
43 }
44}
45
46func (d *Daemon) enterResizeMode(sess *session, ac *attachedClient) error {
47 if sess == nil || ac == nil {
48 return layout.ErrNotFound
49 }
50 target := resolveDaemonActionTargetForAttachment(sess, ac)
51 if target.tab == nil || target.pane == nil {
52 return layout.ErrNotFound
53 }
54 target.tab.mu.Lock()
55 canResize := target.tab.tree != nil && target.tab.tree.CanResize()
56 target.tab.mu.Unlock()
57 if !canResize {
58 return resizeUserError(layout.ErrNotInSplit)
59 }
60 d.exitCopyMode(ac)
61 ac.initOverlays()
62 ac.overlays.resizeMu.Lock()
63 ac.overlays.resizeESC.stop()
64 ac.overlays.resizeActive = true
65 ac.overlays.resizePending = nil
66 ac.overlays.resizeMu.Unlock()
67 d.invalidateRender(sess, ac, true, "resize_mode.go")
68 return nil
69}
70
71func (d *Daemon) handleResizeInput(ac *attachedClient, data []byte) {
72 if ac == nil || ac.overlays == nil {
73 return
74 }
75 rt := ac.overlays
76 var requests []daemonActionRequest
77 exit := false
78 rt.resizeMu.Lock()
79 if !rt.resizeActive {
80 rt.resizePending = nil
81 rt.resizeESC.stop()
82 rt.resizeMu.Unlock()
83 return
84 }
85 hadPending := len(rt.resizePending) != 0
86 if rt.resizeESC.timer != nil {
87 rt.resizeESC.stop()
88 }
89 retainEscape := !hadPending && len(data) != 0 && data[len(data)-1] == keys.ESC
90 if retainEscape {
91 data = data[:len(data)-1]
92 }
93 appendResize := func(axis layout.Axis, delta int) {
94 requests = append(requests, daemonActionRequest{kind: daemonActionResizePane, axis: axis, delta: delta})
95 }
96 routeOverlayBytes(data, &rt.resizePending, overlayEvents{
97 rune: func(r rune) {
98 switch r {
99 case 'h':
100 appendResize(layout.Width, -resizeStepCols)
101 case 'l':
102 appendResize(layout.Width, resizeStepCols)
103 case 'k':
104 appendResize(layout.Height, -resizeStepRows)
105 case 'j':
106 appendResize(layout.Height, resizeStepRows)
107 case '=':
108 requests = append(requests, daemonActionRequest{kind: daemonActionEqualizePanes})
109 case 'q':
110 exit = true
111 }
112 },
113 left: func() { appendResize(layout.Width, -resizeStepCols) },
114 right: func() { appendResize(layout.Width, resizeStepCols) },
115 up: func() { appendResize(layout.Height, -resizeStepRows) },
116 down: func() { appendResize(layout.Height, resizeStepRows) },
117 cancel: func() { exit = true },
118 enter: func() { exit = true },
119 })
120 if exit {
121 rt.resizeActive = false
122 rt.resizePending = nil
123 rt.resizeESC.stop()
124 } else if retainEscape {
125 d.retainResizeESCLocked(ac)
126 }
127 rt.resizeMu.Unlock()
128
129 sess := ac.currentSession()
130 if sess == nil {
131 return
132 }
133 for _, request := range requests {
134 if _, err := d.dispatchAction(daemonActionDispatch{
135 request: request,
136 serialize: sess,
137 resolveFrom: sess,
138 attachment: ac,
139 producer: "resize_mode.go",
140 }); err != nil {
141 d.reportError(sess, resizeUserError(err))
142 }
143 }
144 if exit {
145 d.invalidateRender(sess, ac, true, "resize_mode.go")
146 }
147}
148
149func (d *Daemon) retainResizeESCLocked(ac *attachedClient) {
150 rt := ac.overlays
151 rt.resizePending = append(rt.resizePending[:0], keys.ESC)
152 rt.resizeESC.retain(d.clock, keys.ESCDelay, func(timer ports.Timer) {
153 rt.resizeMu.Lock()
154 if rt.resizeESC.timer != timer {
155 rt.resizeMu.Unlock()
156 return
157 }
158 rt.resizePending = nil
159 rt.resizeESC.timer = nil
160 rt.resizeESC.done = nil
161 active := rt.resizeActive
162 rt.resizeActive = false
163 rt.resizeMu.Unlock()
164 if active {
165 if sess := ac.currentSession(); sess != nil {
166 d.invalidateRender(sess, ac, true, "resize_mode.go")
167 }
168 }
169 })
170}