Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions internal/tui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ func DefaultKeyMap() KeyMap {
),
Left: key.NewBinding(
key.WithKeys("left"),
key.WithHelp("←", "prev tab"),
key.WithHelp("←", "back/prev tab"),
),
Right: key.NewBinding(
key.WithKeys("right"),
key.WithHelp("→", "next tab"),
key.WithHelp("→", "open/next tab"),
),
Enter: key.NewBinding(
key.WithKeys("enter"),
Expand Down Expand Up @@ -128,7 +128,7 @@ func DefaultKeyMap() KeyMap {
),
Cancel: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
key.WithHelp("esc", "cancel/quit"),
),
Help: key.NewBinding(
key.WithKeys("?"),
Expand All @@ -143,15 +143,15 @@ func DefaultKeyMap() KeyMap {

// ShortHelp returns keybindings for the short help view
func (k KeyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Help, k.Quit}
return []key.Binding{k.Help, k.Cancel, k.Quit}
}

// FullHelp returns keybindings for the expanded help view
func (k KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Up, k.Down, k.Enter, k.Back},
{k.Up, k.Down, k.Right, k.Left},
{k.Tab, k.Buckets, k.Browser, k.Bookmarks},
{k.Download, k.Sync, k.AddBookmark, k.Refresh},
{k.Help, k.Quit},
{k.Help, k.Cancel, k.Quit},
}
}
49 changes: 46 additions & 3 deletions internal/tui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.showHelp = !m.showHelp
return m, nil

case key.Matches(msg, m.keys.Tab), key.Matches(msg, m.keys.Right):
case key.Matches(msg, m.keys.Tab):
m.nextView()
return m, nil

case key.Matches(msg, m.keys.ShiftTab), key.Matches(msg, m.keys.Left):
case key.Matches(msg, m.keys.ShiftTab):
m.prevView()
return m, nil

case key.Matches(msg, m.keys.Right):
// In the browser, → opens a folder (handled by the view). Elsewhere
// it switches to the next tab.
if m.activeView != ViewBrowser {
m.nextView()
return m, nil
}

case key.Matches(msg, m.keys.Left):
// In the browser, ← goes to the parent folder (handled by the view).
// Elsewhere it switches to the previous tab.
if m.activeView != ViewBrowser {
m.prevView()
return m, nil
}

case key.Matches(msg, m.keys.Buckets):
m.activeView = ViewBuckets
return m, nil
Expand All @@ -63,17 +79,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil

case key.Matches(msg, m.keys.Cancel):
// Cancel an in-progress download first.
if m.activeView == ViewDownload && m.downloadView.IsActive() {
if m.downloadMgr != nil {
m.downloadMgr.Cancel()
}
return m, nil
}
// Close help if open
// Close help if open.
if m.showHelp {
m.showHelp = false
return m, nil
}
// If the active view is filtering, let it handle Esc (cancel filter)
// instead of quitting.
if m.isActiveViewFiltering() {
break
}
// Otherwise Esc quits the app.
m.cancel()
return m, tea.Quit

case key.Matches(msg, m.keys.Refresh):
return m.handleRefresh()
Expand Down Expand Up @@ -266,6 +291,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}

// isActiveViewFiltering reports whether the currently active list view is in
// filter-input mode, in which case Esc should cancel the filter rather than
// quit the app.
func (m Model) isActiveViewFiltering() bool {
switch m.activeView {
case ViewProfiles:
return m.profilesView.IsFiltering()
case ViewBuckets:
return m.bucketsView.IsFiltering()
case ViewBrowser:
return m.browserView.IsFiltering()
case ViewBookmarks:
return m.bookmarksView.IsFiltering()
}
return false
}

func (m *Model) nextView() {
switch m.activeView {
case ViewBuckets:
Expand Down Expand Up @@ -298,6 +340,7 @@ func (m Model) handleRefresh() (tea.Model, tea.Cmd) {
m.bucketsView.SetLoading(true)
return m, m.loadBuckets()
case ViewBrowser:
m.browserView.RememberCursor()
m.browserView.SetLoading(true)
return m, m.loadObjects()
case ViewBookmarks:
Expand Down
6 changes: 6 additions & 0 deletions internal/views/bookmarksview/bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ func (m *Model) SetSize(width, height int) {
m.list.SetSize(width, height)
}

// IsFiltering reports whether a filter is active (being typed or applied), so
// callers can let the list handle Esc (cancel/clear filter) instead of quitting.
func (m Model) IsFiltering() bool {
return m.list.FilterState() != list.Unfiltered
}

// SetStore sets the bookmark store
func (m *Model) SetStore(store *bookmarks.Store) {
m.store = store
Expand Down
47 changes: 41 additions & 6 deletions internal/views/browser/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ type Model struct {
width int
height int

// cursorByPrefix remembers the cursor position for each visited prefix so
// navigating back (or refreshing) restores where the user was.
cursorByPrefix map[string]int

// Multi-select
selected map[string]bool // map of Key -> selected

Expand Down Expand Up @@ -102,9 +106,10 @@ func New() Model {
Padding(0, 1)

return Model{
list: l,
history: []string{},
selected: make(map[string]bool),
list: l,
history: []string{},
selected: make(map[string]bool),
cursorByPrefix: make(map[string]int),
}
}

Expand All @@ -115,12 +120,25 @@ func (m *Model) SetSize(width, height int) {
m.list.SetSize(width, height-2) // Reserve space for path
}

// IsFiltering reports whether a filter is active (being typed or applied), so
// callers can let the list handle Esc (cancel/clear filter) instead of quitting.
func (m Model) IsFiltering() bool {
return m.list.FilterState() != list.Unfiltered
}

// RememberCursor saves the current cursor position for the current prefix so a
// subsequent reload (e.g. refresh) can restore it.
func (m *Model) RememberCursor() {
m.cursorByPrefix[m.prefix] = m.list.Index()
}

// SetBucket sets the current bucket
func (m *Model) SetBucket(bucket string) {
m.bucket = bucket
m.prefix = ""
m.history = []string{}
m.selected = make(map[string]bool) // Clear selection
m.cursorByPrefix = make(map[string]int)
m.updateTitle()
}

Expand All @@ -141,6 +159,17 @@ func (m *Model) SetObjects(objects []aws.S3Object) {
items[i] = Item{object: obj, selected: false}
}
m.list.SetItems(items)

// Restore the remembered cursor position for this prefix.
if idx, ok := m.cursorByPrefix[m.prefix]; ok && len(objects) > 0 {
if idx >= len(objects) {
idx = len(objects) - 1
}
if idx < 0 {
idx = 0
}
m.list.Select(idx)
}
}

// SetError sets an error state
Expand Down Expand Up @@ -201,10 +230,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
return m, nil

case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
case key.Matches(msg, key.NewBinding(key.WithKeys("enter", "right"))):
if item, ok := m.list.SelectedItem().(Item); ok {
if item.object.IsPrefix {
// Navigate into prefix
// Remember where we are, then navigate into the prefix
m.cursorByPrefix[m.prefix] = m.list.Index()
m.history = append(m.history, m.prefix)
m.prefix = item.object.Key
m.selectedObject = item.object
Expand All @@ -214,20 +244,25 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
}

case key.Matches(msg, key.NewBinding(key.WithKeys("backspace"))):
case key.Matches(msg, key.NewBinding(key.WithKeys("backspace", "left"))):
if len(m.history) > 0 {
m.cursorByPrefix[m.prefix] = m.list.Index()
m.prefix = m.history[len(m.history)-1]
m.history = m.history[:len(m.history)-1]
m.action = ActionBack
m.updateTitle()
return m, nil
} else if m.prefix != "" {
// Go back to bucket root
m.cursorByPrefix[m.prefix] = m.list.Index()
m.prefix = ""
m.action = ActionBack
m.updateTitle()
return m, nil
}
// At bucket root with no history: swallow so it doesn't fall
// through (the global handler already declined to switch tabs).
return m, nil

case key.Matches(msg, key.NewBinding(key.WithKeys("d"))):
// Download selected items, or current item if none selected
Expand Down
6 changes: 6 additions & 0 deletions internal/views/buckets/buckets.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ func (m *Model) SetSize(width, height int) {
m.list.SetSize(width, height)
}

// IsFiltering reports whether a filter is active (being typed or applied), so
// callers can let the list handle Esc (cancel/clear filter) instead of quitting.
func (m Model) IsFiltering() bool {
return m.list.FilterState() != list.Unfiltered
}

// SetBuckets updates the bucket list
func (m *Model) SetBuckets(buckets []aws.Bucket) {
m.buckets = buckets
Expand Down
6 changes: 6 additions & 0 deletions internal/views/profiles/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ func (m *Model) SetSize(width, height int) {
m.list.SetSize(width, height)
}

// IsFiltering reports whether a filter is active (being typed or applied), so
// callers can let the list handle Esc (cancel/clear filter) instead of quitting.
func (m Model) IsFiltering() bool {
return m.list.FilterState() != list.Unfiltered
}

// LoadProfiles loads available AWS profiles
func (m *Model) LoadProfiles() error {
profiles, err := aws.ListProfiles()
Expand Down