apps/blobs/tui_model.go 4.1 K raw
1
package main
2
3
import (
4
	"time"
5
6
	"charm.land/bubbles/v2/help"
7
	"charm.land/bubbles/v2/list"
8
	"charm.land/bubbles/v2/textinput"
9
	"charm.land/bubbles/v2/viewport"
10
	tea "charm.land/bubbletea/v2"
11
12
	"github.com/stevedylandev/andromeda/apps/blobs/preview"
13
)
14
15
type tuiState uint8
16
17
const (
18
	stateBuckets tuiState = iota
19
	stateBrowse
20
)
21
22
type uploadPromptState uint8
23
24
const (
25
	uploadPromptOff uploadPromptState = iota
26
	uploadPromptActive
27
)
28
29
type tuiModel struct {
30
	s3   *S3Client
31
	cfg  ClientConfig
32
	opts tuiOptions
33
34
	state tuiState
35
36
	bucketsList list.Model
37
	browseList  list.Model
38
39
	preview      viewport.Model
40
	previewProto preview.Protocol
41
	showPreview  bool
42
	previewSeq   int
43
44
	currentBucket string
45
	currentPrefix string
46
47
	width, height int
48
	ready         bool
49
	loading       bool
50
51
	// modals
52
	confirmDelete bool
53
	uploadPrompt  uploadPromptState
54
	uploadInput   textinput.Model
55
56
	// help overlay
57
	showHelp bool
58
	help     help.Model
59
	keys     tuiKeyMap
60
61
	// status
62
	status      string
63
	statusOK    bool
64
	statusUntil time.Time
65
}
66
67
func newTUIModel(s3 *S3Client, cfg ClientConfig, opts tuiOptions) tuiModel {
68
	bl := newList("buckets", nil)
69
	brl := newList("/", nil)
70
	ti := textinput.New()
71
	ti.Placeholder = "/path/to/file"
72
	ti.Prompt = "upload: "
73
74
	m := tuiModel{
75
		s3:           s3,
76
		cfg:          cfg,
77
		opts:         opts,
78
		state:        stateBuckets,
79
		bucketsList:  bl,
80
		browseList:   brl,
81
		preview:      viewport.New(),
82
		previewProto: preview.Detect(),
83
		showPreview:  true,
84
		help:         help.New(),
85
		keys:         defaultTUIKeys(),
86
		uploadInput:  ti,
87
		loading:      true,
88
	}
89
	if cfg.DefaultBucket != "" {
90
		m.state = stateBrowse
91
		m.currentBucket = cfg.DefaultBucket
92
		m.currentPrefix = opts.Prefix
93
	}
94
	return m
95
}
96
97
func (m tuiModel) Init() tea.Cmd {
98
	cmds := []tea.Cmd{tea.RequestWindowSize}
99
	if m.state == stateBrowse {
100
		cmds = append(cmds, loadListingCmd(m.s3, m.currentBucket, m.currentPrefix))
101
	} else {
102
		cmds = append(cmds, loadBucketsCmd(m.s3))
103
	}
104
	return tea.Batch(cmds...)
105
}
106
107
func (m *tuiModel) setStatus(text string, ok bool) tea.Cmd {
108
	m.status = text
109
	m.statusOK = ok
110
	m.statusUntil = time.Now().Add(2 * time.Second)
111
	return tea.Tick(2*time.Second, func(time.Time) tea.Msg { return clearStatusMsg{} })
112
}
113
114
func (m *tuiModel) selectedFile() (ObjectInfo, bool) {
115
	if m.state != stateBrowse {
116
		return ObjectInfo{}, false
117
	}
118
	it := m.browseList.SelectedItem()
119
	if it == nil {
120
		return ObjectInfo{}, false
121
	}
122
	f, ok := it.(fileItemTUI)
123
	if !ok {
124
		return ObjectInfo{}, false
125
	}
126
	return f.obj, true
127
}
128
129
func (m *tuiModel) selectedFolderPrefix() (string, bool) {
130
	if m.state != stateBrowse {
131
		return "", false
132
	}
133
	it := m.browseList.SelectedItem()
134
	if it == nil {
135
		return "", false
136
	}
137
	f, ok := it.(folderItemTUI)
138
	if !ok {
139
		return "", false
140
	}
141
	return f.prefix, true
142
}
143
144
func (m *tuiModel) selectedBucket() (string, bool) {
145
	if m.state != stateBuckets {
146
		return "", false
147
	}
148
	it := m.bucketsList.SelectedItem()
149
	if it == nil {
150
		return "", false
151
	}
152
	b, ok := it.(bucketItem)
153
	if !ok {
154
		return "", false
155
	}
156
	return b.b.Name, true
157
}
158
159
func (m *tuiModel) applyLayout() {
160
	if !m.ready {
161
		return
162
	}
163
	bodyH := m.height - 2
164
	if bodyH < 5 {
165
		bodyH = 5
166
	}
167
168
	switch m.state {
169
	case stateBuckets:
170
		fw, fh := paneFrameW(), paneFrameH()
171
		m.bucketsList.SetSize(max(m.width-fw, 1), max(bodyH-fh, 1))
172
	case stateBrowse:
173
		listW := m.width
174
		if m.showPreview {
175
			listW = m.width / 2
176
		}
177
		fw, fh := paneFrameW(), paneFrameH()
178
		m.browseList.SetSize(max(listW-fw, 1), max(bodyH-fh, 1))
179
		if m.showPreview {
180
			pw := m.width - listW
181
			m.preview.SetWidth(max(pw-fw, 1))
182
			m.preview.SetHeight(max(bodyH-fh-1, 1))
183
		}
184
	}
185
}
186
187
func setItemsFromListing(l *list.Model, prefix string, folders []string, files []ObjectInfo) tea.Cmd {
188
	items := make([]list.Item, 0, len(folders)+len(files))
189
	for _, fp := range folders {
190
		name := fp
191
		if len(prefix) <= len(fp) {
192
			name = fp[len(prefix):]
193
		}
194
		name = trimTrailingSlash(name)
195
		items = append(items, folderItemTUI{prefix: fp, name: name})
196
	}
197
	for _, f := range files {
198
		items = append(items, fileItemTUI{obj: f})
199
	}
200
	return l.SetItems(items)
201
}
202
203
func trimTrailingSlash(s string) string {
204
	if len(s) > 0 && s[len(s)-1] == '/' {
205
		return s[:len(s)-1]
206
	}
207
	return s
208
}