| 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 | } |