-
Notifications
You must be signed in to change notification settings - Fork 441
Expand file tree
/
Copy pathlist.go
More file actions
374 lines (318 loc) · 9.88 KB
/
list.go
File metadata and controls
374 lines (318 loc) · 9.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package ui
import (
"claude-squad/log"
"claude-squad/session"
"errors"
"fmt"
"strings"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
)
const readyIcon = "● "
const pausedIcon = "⏸ "
var readyStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#51bd73", Dark: "#51bd73"})
var addedLinesStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#51bd73", Dark: "#51bd73"})
var removedLinesStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#de613e"))
var pausedStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#888888", Dark: "#888888"})
var titleStyle = lipgloss.NewStyle().
Padding(1, 1, 0, 1).
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
var listDescStyle = lipgloss.NewStyle().
Padding(0, 1, 1, 1).
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"})
var selectedTitleStyle = lipgloss.NewStyle().
Padding(1, 1, 0, 1).
Background(lipgloss.Color("#dde4f0")).
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#1a1a1a"})
var selectedDescStyle = lipgloss.NewStyle().
Padding(0, 1, 1, 1).
Background(lipgloss.Color("#dde4f0")).
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#1a1a1a"})
var mainTitle = lipgloss.NewStyle().
Background(lipgloss.Color("62")).
Foreground(lipgloss.Color("230"))
var autoYesStyle = lipgloss.NewStyle().
Background(lipgloss.Color("#dde4f0")).
Foreground(lipgloss.Color("#1a1a1a"))
type List struct {
items []*session.Instance
selectedIdx int
height, width int
renderer *InstanceRenderer
autoyes bool
// map of repo name to number of instances using it. Used to display the repo name only if there are
// multiple repos in play.
repos map[string]int
}
func NewList(spinner *spinner.Model, autoYes bool) *List {
return &List{
items: []*session.Instance{},
renderer: &InstanceRenderer{spinner: spinner},
repos: make(map[string]int),
autoyes: autoYes,
}
}
// SetSize sets the height and width of the list.
func (l *List) SetSize(width, height int) {
l.width = width
l.height = height
l.renderer.setWidth(width)
}
// SetSessionPreviewSize sets the height and width for the tmux sessions. This makes the stdout line have the correct
// width and height.
func (l *List) SetSessionPreviewSize(width, height int) (err error) {
for i, item := range l.items {
if !item.Started() || item.Paused() {
continue
}
if innerErr := item.SetPreviewSize(width, height); innerErr != nil {
err = errors.Join(
err, fmt.Errorf("could not set preview size for instance %d: %v", i, innerErr))
}
}
return
}
func (l *List) NumInstances() int {
return len(l.items)
}
// InstanceRenderer handles rendering of session.Instance objects
type InstanceRenderer struct {
spinner *spinner.Model
width int
}
func (r *InstanceRenderer) setWidth(width int) {
r.width = AdjustPreviewWidth(width)
}
// ɹ and ɻ are other options.
const branchIcon = "Ꮧ"
func (r *InstanceRenderer) Render(i *session.Instance, idx int, selected bool, hasMultipleRepos bool) string {
prefix := fmt.Sprintf(" %d. ", idx)
if idx >= 10 {
prefix = prefix[:len(prefix)-1]
}
titleS := selectedTitleStyle
descS := selectedDescStyle
if !selected {
titleS = titleStyle
descS = listDescStyle
}
// add spinner next to title if it's running
var join string
switch i.Status {
case session.Running:
join = fmt.Sprintf("%s ", r.spinner.View())
case session.Loading:
join = fmt.Sprintf("%s ", r.spinner.View())
case session.Ready:
join = readyStyle.Render(readyIcon)
case session.Paused:
join = pausedStyle.Render(pausedIcon)
default:
}
// Cut the title if it's too long
titleText := i.Title
widthAvail := r.width - 3 - runewidth.StringWidth(prefix) - 1
if widthAvail > 0 && runewidth.StringWidth(titleText) > widthAvail {
titleText = runewidth.Truncate(titleText, widthAvail-3, "...")
}
title := titleS.Render(lipgloss.JoinHorizontal(
lipgloss.Left,
lipgloss.Place(r.width-3, 1, lipgloss.Left, lipgloss.Center, fmt.Sprintf("%s %s", prefix, titleText)),
" ",
join,
))
stat := i.GetDiffStats()
var diff string
var addedDiff, removedDiff string
if stat == nil || stat.Error != nil || stat.IsEmpty() {
// Don't show diff stats if there's an error or if they don't exist
addedDiff = ""
removedDiff = ""
diff = ""
} else {
addedDiff = fmt.Sprintf("+%d", stat.Added)
removedDiff = fmt.Sprintf("-%d ", stat.Removed)
diff = lipgloss.JoinHorizontal(
lipgloss.Center,
addedLinesStyle.Background(descS.GetBackground()).Render(addedDiff),
lipgloss.Style{}.Background(descS.GetBackground()).Foreground(descS.GetForeground()).Render(","),
removedLinesStyle.Background(descS.GetBackground()).Render(removedDiff),
)
}
remainingWidth := r.width
remainingWidth -= runewidth.StringWidth(prefix)
remainingWidth -= runewidth.StringWidth(branchIcon)
diffWidth := runewidth.StringWidth(addedDiff) + runewidth.StringWidth(removedDiff)
if diffWidth > 0 {
diffWidth += 1
}
// Use fixed width for diff stats to avoid layout issues
remainingWidth -= diffWidth
branch := i.Branch
if i.Started() && hasMultipleRepos {
repoName, err := i.RepoName()
if err != nil {
log.ErrorLog.Printf("could not get repo name in instance renderer: %v", err)
} else {
branch += fmt.Sprintf(" (%s)", repoName)
}
}
// Don't show branch if there's no space for it. Or show ellipsis if it's too long.
branchWidth := runewidth.StringWidth(branch)
if remainingWidth < 0 {
branch = ""
} else if remainingWidth < branchWidth {
if remainingWidth < 3 {
branch = ""
} else {
// We know the remainingWidth is at least 4 and branch is longer than that, so this is safe.
branch = runewidth.Truncate(branch, remainingWidth-3, "...")
}
}
remainingWidth -= runewidth.StringWidth(branch)
// Add spaces to fill the remaining width.
spaces := ""
if remainingWidth > 0 {
spaces = strings.Repeat(" ", remainingWidth)
}
branchLine := fmt.Sprintf("%s %s-%s%s%s", strings.Repeat(" ", len(prefix)), branchIcon, branch, spaces, diff)
// join title and subtitle
text := lipgloss.JoinVertical(
lipgloss.Left,
title,
descS.Render(branchLine),
)
return text
}
func (l *List) String() string {
const titleText = " Instances "
const autoYesText = " auto-yes "
// Write the title.
var b strings.Builder
b.WriteString("\n")
b.WriteString("\n")
// Write title line
// add padding of 2 because the border on list items adds some extra characters
titleWidth := AdjustPreviewWidth(l.width) + 2
if !l.autoyes {
b.WriteString(lipgloss.Place(
titleWidth, 1, lipgloss.Left, lipgloss.Bottom, mainTitle.Render(titleText)))
} else {
title := lipgloss.Place(
titleWidth/2, 1, lipgloss.Left, lipgloss.Bottom, mainTitle.Render(titleText))
autoYes := lipgloss.Place(
titleWidth-(titleWidth/2), 1, lipgloss.Right, lipgloss.Bottom, autoYesStyle.Render(autoYesText))
b.WriteString(lipgloss.JoinHorizontal(
lipgloss.Top, title, autoYes))
}
b.WriteString("\n")
b.WriteString("\n")
// Render the list.
for i, item := range l.items {
b.WriteString(l.renderer.Render(item, i+1, i == l.selectedIdx, len(l.repos) > 1))
if i != len(l.items)-1 {
b.WriteString("\n\n")
}
}
return lipgloss.Place(l.width, l.height, lipgloss.Left, lipgloss.Top, b.String())
}
// Down selects the next item in the list.
func (l *List) Down() {
if len(l.items) == 0 {
return
}
if l.selectedIdx < len(l.items)-1 {
l.selectedIdx++
}
}
// Kill selects the next item in the list.
func (l *List) Kill() {
if len(l.items) == 0 {
return
}
targetInstance := l.items[l.selectedIdx]
// Kill the tmux session
if err := targetInstance.Kill(); err != nil {
log.ErrorLog.Printf("could not kill instance: %v", err)
}
// If you delete the last one in the list, select the previous one.
if l.selectedIdx == len(l.items)-1 {
defer l.Up()
}
// Unregister the reponame.
repoName, err := targetInstance.RepoName()
if err != nil {
log.ErrorLog.Printf("could not get repo name: %v", err)
} else {
l.rmRepo(repoName)
}
// Since there's items after this, the selectedIdx can stay the same.
l.items = append(l.items[:l.selectedIdx], l.items[l.selectedIdx+1:]...)
}
func (l *List) Attach() (chan struct{}, error) {
targetInstance := l.items[l.selectedIdx]
return targetInstance.Attach()
}
// Up selects the prev item in the list.
func (l *List) Up() {
if len(l.items) == 0 {
return
}
if l.selectedIdx > 0 {
l.selectedIdx--
}
}
func (l *List) addRepo(repo string) {
if _, ok := l.repos[repo]; !ok {
l.repos[repo] = 0
}
l.repos[repo]++
}
func (l *List) rmRepo(repo string) {
if _, ok := l.repos[repo]; !ok {
log.ErrorLog.Printf("repo %s not found", repo)
return
}
l.repos[repo]--
if l.repos[repo] == 0 {
delete(l.repos, repo)
}
}
// AddInstance adds a new instance to the list. It returns a finalizer function that should be called when the instance
// is started. If the instance was restored from storage or is paused, you can call the finalizer immediately.
// When creating a new one and entering the name, you want to call the finalizer once the name is done.
func (l *List) AddInstance(instance *session.Instance) (finalize func()) {
l.items = append(l.items, instance)
// The finalizer registers the repo name once the instance is started.
return func() {
repoName, err := instance.RepoName()
if err != nil {
log.ErrorLog.Printf("could not get repo name: %v", err)
return
}
l.addRepo(repoName)
}
}
// GetSelectedInstance returns the currently selected instance
func (l *List) GetSelectedInstance() *session.Instance {
if len(l.items) == 0 {
return nil
}
return l.items[l.selectedIdx]
}
// SetSelectedInstance sets the selected index. Noop if the index is out of bounds.
func (l *List) SetSelectedInstance(idx int) {
if idx >= len(l.items) {
return
}
l.selectedIdx = idx
}
// GetInstances returns all instances in the list
func (l *List) GetInstances() []*session.Instance {
return l.items
}