apps/blobs/tui_update.go 7.7 K raw
1
package main
2
3
import (
4
	"context"
5
	"time"
6
7
	"charm.land/bubbles/v2/key"
8
	"charm.land/bubbles/v2/list"
9
	tea "charm.land/bubbletea/v2"
10
	sharedtui "github.com/stevedylandev/andromeda/pkg/tui"
11
)
12
13
func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
14
	switch msg := msg.(type) {
15
16
	case tea.WindowSizeMsg:
17
		m.width, m.height = msg.Width, msg.Height
18
		m.ready = true
19
		m.applyLayout()
20
		return m, nil
21
22
	case bucketsLoadedMsg:
23
		m.loading = false
24
		if msg.Err != nil {
25
			return m, m.setStatus("buckets: "+msg.Err.Error(), false)
26
		}
27
		items := make([]list.Item, 0, len(msg.Buckets))
28
		for _, b := range msg.Buckets {
29
			items = append(items, bucketItem{b: b})
30
		}
31
		cmd := m.bucketsList.SetItems(items)
32
		return m, cmd
33
34
	case listingLoadedMsg:
35
		m.loading = false
36
		if msg.Err != nil {
37
			return m, m.setStatus("list: "+msg.Err.Error(), false)
38
		}
39
		m.currentBucket = msg.Bucket
40
		m.currentPrefix = msg.Prefix
41
		m.browseList.Title = msg.Bucket + ":/" + msg.Prefix
42
		cmd := setItemsFromListing(&m.browseList, msg.Prefix, msg.Folders, msg.Files)
43
		return m, tea.Batch(cmd, m.maybePreviewCmd())
44
45
	case previewDebounceMsg:
46
		if msg.Seq != m.previewSeq || msg.Bucket != m.currentBucket {
47
			return m, nil
48
		}
49
		return m, loadPreviewCmd(m.s3, m.previewProto, msg.Seq, msg.Bucket, msg.Key, msg.W, msg.H)
50
51
	case previewLoadedMsg:
52
		if msg.Seq != m.previewSeq || msg.Bucket != m.currentBucket {
53
			return m, nil
54
		}
55
		if msg.Err != nil {
56
			m.preview.SetContent("preview error: " + msg.Err.Error())
57
			return m, nil
58
		}
59
		m.preview.SetContent(msg.Content)
60
		m.preview.GotoTop()
61
		return m, nil
62
63
	case deletedMsg:
64
		if msg.Err != nil {
65
			return m, m.setStatus("delete: "+msg.Err.Error(), false)
66
		}
67
		return m, tea.Batch(
68
			loadListingCmd(m.s3, m.currentBucket, m.currentPrefix),
69
			m.setStatus("deleted "+msg.Key, true),
70
		)
71
72
	case uploadedMsg:
73
		if msg.Err != nil {
74
			return m, m.setStatus("upload: "+msg.Err.Error(), false)
75
		}
76
		cmds := []tea.Cmd{
77
			loadListingCmd(m.s3, m.currentBucket, m.currentPrefix),
78
			m.setStatus("uploaded "+msg.Key, true),
79
		}
80
		if msg.URL != "" {
81
			cmds = append(cmds, sharedtui.CopyToClipboardCmd(msg.URL, "copied url"))
82
		}
83
		return m, tea.Batch(cmds...)
84
85
	case statusMsg:
86
		return m, m.setStatus(msg.Text, msg.OK)
87
88
	case clearStatusMsg:
89
		if time.Now().Before(m.statusUntil) {
90
			return m, nil
91
		}
92
		m.status = ""
93
		return m, nil
94
95
	case tea.KeyPressMsg:
96
		return m.handleKey(msg)
97
	}
98
99
	var cmd tea.Cmd
100
	switch m.state {
101
	case stateBuckets:
102
		m.bucketsList, cmd = m.bucketsList.Update(msg)
103
	case stateBrowse:
104
		m.browseList, cmd = m.browseList.Update(msg)
105
	}
106
	return m, cmd
107
}
108
109
func (m tuiModel) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
110
	if msg.String() == "ctrl+c" {
111
		return m, tea.Quit
112
	}
113
114
	if m.uploadPrompt == uploadPromptActive {
115
		switch msg.String() {
116
		case "esc":
117
			m.uploadPrompt = uploadPromptOff
118
			m.uploadInput.Blur()
119
			return m, nil
120
		case "enter":
121
			path := m.uploadInput.Value()
122
			m.uploadPrompt = uploadPromptOff
123
			m.uploadInput.Blur()
124
			m.uploadInput.SetValue("")
125
			if path == "" {
126
				return m, nil
127
			}
128
			return m, uploadCmd(m.s3, m.cfg, m.currentBucket, m.currentPrefix, path)
129
		}
130
		var cmd tea.Cmd
131
		m.uploadInput, cmd = m.uploadInput.Update(msg)
132
		return m, cmd
133
	}
134
135
	if m.confirmDelete {
136
		switch msg.String() {
137
		case "y", "Y":
138
			m.confirmDelete = false
139
			f, ok := m.selectedFile()
140
			if !ok {
141
				return m, nil
142
			}
143
			return m, deleteCmd(m.s3, m.currentBucket, f.Key)
144
		case "n", "N", "esc", "q":
145
			m.confirmDelete = false
146
		}
147
		return m, nil
148
	}
149
150
	if m.showHelp {
151
		if key.Matches(msg, m.keys.Help) || msg.String() == "esc" || msg.String() == "q" {
152
			m.showHelp = false
153
		}
154
		return m, nil
155
	}
156
157
	switch m.state {
158
	case stateBuckets:
159
		return m.handleBucketsKey(msg)
160
	case stateBrowse:
161
		return m.handleBrowseKey(msg)
162
	}
163
	return m, nil
164
}
165
166
func (m tuiModel) handleBucketsKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
167
	if m.bucketsList.SettingFilter() {
168
		var cmd tea.Cmd
169
		m.bucketsList, cmd = m.bucketsList.Update(msg)
170
		return m, cmd
171
	}
172
	switch {
173
	case key.Matches(msg, m.keys.Quit):
174
		return m, tea.Quit
175
	case key.Matches(msg, m.keys.Open):
176
		b, ok := m.selectedBucket()
177
		if !ok {
178
			return m, nil
179
		}
180
		m.state = stateBrowse
181
		m.currentBucket = b
182
		m.currentPrefix = ""
183
		m.loading = true
184
		m.applyLayout()
185
		return m, loadListingCmd(m.s3, b, "")
186
	case key.Matches(msg, m.keys.Refresh):
187
		m.loading = true
188
		return m, loadBucketsCmd(m.s3)
189
	case key.Matches(msg, m.keys.Help):
190
		m.showHelp = true
191
		return m, nil
192
	}
193
	var cmd tea.Cmd
194
	m.bucketsList, cmd = m.bucketsList.Update(msg)
195
	return m, cmd
196
}
197
198
func (m tuiModel) handleBrowseKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
199
	if m.browseList.SettingFilter() {
200
		var cmd tea.Cmd
201
		m.browseList, cmd = m.browseList.Update(msg)
202
		return m, cmd
203
	}
204
	switch {
205
	case key.Matches(msg, m.keys.Quit):
206
		return m, tea.Quit
207
	case key.Matches(msg, m.keys.Buckets):
208
		m.state = stateBuckets
209
		m.loading = true
210
		m.applyLayout()
211
		return m, loadBucketsCmd(m.s3)
212
	case key.Matches(msg, m.keys.Back):
213
		if m.currentPrefix == "" {
214
			if m.cfg.DefaultBucket == "" {
215
				m.state = stateBuckets
216
				m.loading = true
217
				m.applyLayout()
218
				return m, loadBucketsCmd(m.s3)
219
			}
220
			return m, nil
221
		}
222
		parent := parentPrefix(m.currentPrefix)
223
		m.loading = true
224
		return m, loadListingCmd(m.s3, m.currentBucket, parent)
225
	case key.Matches(msg, m.keys.Open):
226
		if pfx, ok := m.selectedFolderPrefix(); ok {
227
			m.loading = true
228
			return m, loadListingCmd(m.s3, m.currentBucket, pfx)
229
		}
230
		if f, ok := m.selectedFile(); ok {
231
			url, err := ResolveURL(context.Background(), m.s3, m.currentBucket, f.Key)
232
			if err != nil {
233
				return m, m.setStatus("url: "+err.Error(), false)
234
			}
235
			return m, sharedtui.OpenURLCmd(url)
236
		}
237
		return m, nil
238
	case key.Matches(msg, m.keys.Copy):
239
		f, ok := m.selectedFile()
240
		if !ok {
241
			return m, nil
242
		}
243
		url, err := ResolveURL(context.Background(), m.s3, m.currentBucket, f.Key)
244
		if err != nil {
245
			return m, m.setStatus("url: "+err.Error(), false)
246
		}
247
		return m, sharedtui.CopyToClipboardCmd(url, "copied url")
248
	case key.Matches(msg, m.keys.CopyLink):
249
		f, ok := m.selectedFile()
250
		if !ok {
251
			return m, nil
252
		}
253
		u, ok := m.s3.PublicURL(m.currentBucket, f.Key)
254
		if !ok {
255
			return m, m.setStatus("no public URL for bucket "+m.currentBucket, false)
256
		}
257
		return m, sharedtui.CopyToClipboardCmd(u, "copied public url")
258
	case key.Matches(msg, m.keys.CopyKey):
259
		f, ok := m.selectedFile()
260
		if !ok {
261
			return m, nil
262
		}
263
		return m, sharedtui.CopyToClipboardCmd(f.Key, "copied key")
264
	case key.Matches(msg, m.keys.OpenBrowser):
265
		f, ok := m.selectedFile()
266
		if !ok {
267
			return m, nil
268
		}
269
		url, err := ResolveURL(context.Background(), m.s3, m.currentBucket, f.Key)
270
		if err != nil {
271
			return m, m.setStatus("url: "+err.Error(), false)
272
		}
273
		return m, sharedtui.OpenURLCmd(url)
274
	case key.Matches(msg, m.keys.Delete):
275
		if _, ok := m.selectedFile(); ok {
276
			m.confirmDelete = true
277
		}
278
		return m, nil
279
	case key.Matches(msg, m.keys.Upload):
280
		m.uploadPrompt = uploadPromptActive
281
		m.uploadInput.Focus()
282
		return m, nil
283
	case key.Matches(msg, m.keys.Preview):
284
		m.showPreview = !m.showPreview
285
		m.applyLayout()
286
		if m.showPreview {
287
			return m, m.maybePreviewCmd()
288
		}
289
		return m, nil
290
	case key.Matches(msg, m.keys.Refresh):
291
		m.loading = true
292
		return m, loadListingCmd(m.s3, m.currentBucket, m.currentPrefix)
293
	case key.Matches(msg, m.keys.Help):
294
		m.showHelp = true
295
		return m, nil
296
	}
297
	var cmd tea.Cmd
298
	m.browseList, cmd = m.browseList.Update(msg)
299
	if m.showPreview {
300
		return m, tea.Batch(cmd, m.maybePreviewCmd())
301
	}
302
	return m, cmd
303
}
304
305
func (m *tuiModel) maybePreviewCmd() tea.Cmd {
306
	if !m.showPreview {
307
		return nil
308
	}
309
	f, ok := m.selectedFile()
310
	if !ok {
311
		m.preview.SetContent("")
312
		return nil
313
	}
314
	m.previewSeq++
315
	return debouncePreviewCmd(m.previewSeq, m.currentBucket, f.Key, m.preview.Width(), m.preview.Height())
316
}