TUI IDE multiplexer?!
golang
tui
ide
1package main
2
3import (
4 "fmt"
5
6 "github.com/gdamore/tcell/v2"
7 "github.com/rivo/tview"
8)
9
10type Gripper struct {
11 *tview.Box
12 isVertical bool
13 isHeld bool
14}
15
16func NewGripper(isVertical bool) *Gripper {
17 return &Gripper{
18 Box: tview.NewBox(),
19 isVertical: isVertical,
20 }
21}
22
23func (g *Gripper) Draw(screen tcell.Screen) {
24 g.DrawForSubclass(screen, g)
25 x, y, w, h := g.GetInnerRect()
26
27 ch := '\u2502'
28 if g.isVertical {
29 ch = '\u2500'
30 }
31 if g.isHeld {
32 ch = '\u2503'
33 if g.isVertical {
34 ch = '\u2501'
35 }
36 }
37
38 color := tcell.StyleDefault.Dim(true)
39
40 for x1 := range w {
41 for y1 := range h {
42 screen.SetContent(x+x1, y+y1, ch, nil, color)
43 }
44 }
45}
46
47// Resizable is a rather hacky wrapper around Flex to allow resizable items controlled by grippers within.
48type Resizable struct {
49 *tview.Flex
50 items []ResizableItem
51 grippers []*Gripper
52 heldGripper *Gripper
53 heldGripperIndex int
54 lastX, lastY int
55 isVertical bool
56 subscription int
57 id string
58}
59
60type ResizableItem struct {
61 item tview.Primitive
62 size int // static size
63 proportion int
64 preferResize bool
65}
66
67func NewResizable(id string, isVertical bool) *Resizable {
68 r := &Resizable{
69 id: id,
70 Flex: tview.NewFlex(),
71 isVertical: isVertical,
72 }
73 if r.isVertical {
74 r.Flex.SetDirection(tview.FlexRow)
75 } else {
76 r.Flex.SetDirection(tview.FlexColumn)
77 }
78
79 // Hook into global bus for convenient program-wide handling.
80 // eh... register as well.
81 bus.Register(UI_CANCEL)
82 r.subscription = bus.Subscribe(UI_CANCEL, func(v ...any) {
83 if r.heldGripper != nil {
84 r.heldGripper.isHeld = false
85 r.heldGripper = nil
86 }
87 })
88
89 return r
90}
91
92func (r *Resizable) Cleanup() {
93 bus.Unsubscribe(UI_CANCEL, r.subscription)
94}
95
96func (r *Resizable) Restore() {
97 if ps, ok := econfig.PanelSizes[r.id]; ok {
98 for index, size := range ps {
99 if index < len(r.items) {
100 r.Flex.ResizeItem(r.items[index].item, size, r.items[index].proportion)
101 }
102 }
103 }
104}
105
106func (r *Resizable) AddItem(item tview.Primitive, size int, preferResize bool) {
107 ritem := ResizableItem{item: item, proportion: 1000, size: size, preferResize: preferResize}
108 r.items = append(r.items, ritem)
109
110 if len(r.items) > 1 {
111 gripper := NewGripper(r.isVertical)
112 r.Flex.AddItem(gripper, 1, 0, false)
113 r.grippers = append(r.grippers, gripper)
114 }
115 r.Flex.AddItem(ritem.item, ritem.size, ritem.proportion, true)
116
117 // FIXME: Inefficient...
118 r.Restore()
119}
120
121func (r *Resizable) Draw(screen tcell.Screen) {
122 r.Flex.Draw(screen)
123 if r.heldGripper != nil {
124 startIndex := r.heldGripperIndex
125 x, y, w, h := r.items[startIndex].item.GetRect()
126
127 _, _, sw, sh := r.GetRect()
128 var size int
129 if r.isVertical {
130 size = sh
131 } else {
132 size = sw
133 }
134 step := int(1000.0 / float64(size))
135
136 screen.PutStr(x, y, fmt.Sprintf("%dx%d %d %d %d %d", w, h, r.items[startIndex].proportion, step, size, size*step))
137 }
138}
139
140func (r *Resizable) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
141 return r.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
142 x, y := event.Position()
143 if !r.InRect(x, y) {
144 return false, nil
145 }
146
147 if r.heldGripper != nil {
148 if action == tview.MouseMove {
149 startIndex := r.heldGripperIndex
150 var delta int
151 var rsize int
152
153 which := startIndex
154 if r.items[startIndex].preferResize {
155 which = startIndex + 1
156 }
157
158 _, _, rw, rh := r.items[which].item.GetRect()
159 if r.isVertical {
160 delta = y - r.lastY
161 rsize = rh
162 } else {
163 delta = x - r.lastX
164 rsize = rw
165 }
166 if which == startIndex {
167 delta *= -1
168 }
169 r.items[which].size = rsize - delta
170 r.Flex.ResizeItem(r.items[which].item, r.items[which].size, r.items[which].proportion)
171
172 if _, ok := econfig.PanelSizes[r.id]; !ok {
173 econfig.PanelSizes[r.id] = make(map[int]int)
174 }
175 econfig.PanelSizes[r.id][which] = r.items[which].size
176 econfig.Save()
177
178 r.lastX, r.lastY = x, y
179 } else if action == tview.MouseLeftUp || action == tview.MouseLeftClick || action == tview.MouseLeftDoubleClick {
180 r.heldGripper.isHeld = false
181 r.heldGripper = nil
182 } else {
183 return r.MouseHandleItems(action, event, setFocus)
184 }
185 return true, r
186 }
187
188 // This is hacky, but whatever for now.
189 if action == tview.MouseLeftDown {
190 var gripper *Gripper
191 var gripperIndex int
192 for i, g := range r.grippers {
193 if g.InRect(x, y) {
194 gripper = g
195 gripperIndex = i
196 break
197 }
198 }
199
200 if gripper != nil {
201 gripper.isHeld = true
202 r.heldGripper = gripper
203 r.heldGripperIndex = gripperIndex
204 r.lastX, r.lastY = x, y
205 return true, gripper
206 }
207 }
208
209 // If we hit here, pass down to actual items.
210 return r.MouseHandleItems(action, event, setFocus)
211 })
212}
213
214func (r *Resizable) MouseHandleItems(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
215 for _, item := range r.items {
216 consumed, capture = item.item.MouseHandler()(action, event, setFocus)
217 if consumed {
218 return
219 }
220 }
221 return false, nil
222}
223
224func (r *Resizable) SetRect(x, y, width, height int) {
225 r.Flex.SetRect(x, y, width, height)
226 // TODO: Redistribute items.
227}