From 3a7764be1ab615cf3ce3114327514961650ed9a4 Mon Sep 17 00:00:00 2001 From: David Budnick Date: Mon, 27 Jul 2026 15:27:17 -0500 Subject: [PATCH 1/2] Add filterable database switcher popup --- README.md | 2 + internal/types/screens.go | 38 ++-- internal/ui/database_picker_test.go | 311 ++++++++++++++++++++++++++++ internal/ui/keys.go | 2 +- internal/ui/model.go | 2 +- internal/ui/update.go | 109 +++++++++- internal/ui/view.go | 54 ++++- internal/ui/view_sidebar.go | 7 + 8 files changed, 502 insertions(+), 23 deletions(-) create mode 100644 internal/ui/database_picker_test.go diff --git a/README.md b/README.md index bedda73..6dd6380 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,9 @@ rm -f $(go env GOPATH)/bin/postgres-tui | `ctrl+enter` / `ctrl+e` | Run SQL in the editor | | `A` | Activity | | `i` | Server info | +| `b` | Switch database (filterable popup) | | `/` | Filter objects | +| `ctrl+p` | Command palette | | `?` | Help | | `esc` | Back | | `q` | Quit | diff --git a/internal/types/screens.go b/internal/types/screens.go index 4437edf..39b0750 100644 --- a/internal/types/screens.go +++ b/internal/types/screens.go @@ -22,29 +22,31 @@ const ( ScreenFavorites ScreenExport ScreenCommandPalette + ScreenDatabasePicker ) // String returns a human-readable name for the screen. func (s Screen) String() string { names := map[Screen]string{ - ScreenConnections: "Connections", - ScreenAddConnection: "Add Connection", - ScreenEditConnection: "Edit Connection", - ScreenDatabases: "Databases", - ScreenBrowser: "Browser", - ScreenTableData: "Table Data", - ScreenTableDetail: "Table Detail", - ScreenQuery: "Query", - ScreenActivity: "Activity", - ScreenERD: "ERD", - ScreenServerInfo: "Server Info", - ScreenHelp: "Help", - ScreenConfirmDelete: "Confirm Delete", - ScreenTestConnection: "Test Connection", - ScreenLogs: "Logs", - ScreenFavorites: "Favorites", - ScreenExport: "Export", - ScreenCommandPalette: "Command Palette", + ScreenConnections: "Connections", + ScreenAddConnection: "Add Connection", + ScreenEditConnection: "Edit Connection", + ScreenDatabases: "Databases", + ScreenBrowser: "Browser", + ScreenTableData: "Table Data", + ScreenTableDetail: "Table Detail", + ScreenQuery: "Query", + ScreenActivity: "Activity", + ScreenERD: "ERD", + ScreenServerInfo: "Server Info", + ScreenHelp: "Help", + ScreenConfirmDelete: "Confirm Delete", + ScreenTestConnection: "Test Connection", + ScreenLogs: "Logs", + ScreenFavorites: "Favorites", + ScreenExport: "Export", + ScreenCommandPalette: "Command Palette", + ScreenDatabasePicker: "Switch Database", } if name, ok := names[s]; ok { return name diff --git a/internal/ui/database_picker_test.go b/internal/ui/database_picker_test.go new file mode 100644 index 0000000..74f3fbc --- /dev/null +++ b/internal/ui/database_picker_test.go @@ -0,0 +1,311 @@ +package ui + +import ( + "errors" + "testing" + + tea "charm.land/bubbletea/v2" + "github.com/davidbudnick/postgres-tui/internal/types" +) + +func TestOpenDatabasePicker(t *testing.T) { + m := khModel(t) + nm, cmd := m.openDatabasePicker() + m = nm.(Model) + if m.StatusMsg != "Connect first" || cmd != nil { + t.Fatalf("disconnected: status=%q cmd=%v", m.StatusMsg, cmd != nil) + } + + m = khWorkspace(t) + m.Databases = nil + nm, cmd = m.openDatabasePicker() + m = nm.(Model) + if m.Screen != types.ScreenDatabasePicker { + t.Fatalf("screen=%v", m.Screen) + } + if cmd == nil || !m.Loading { + t.Fatal("expected LoadDatabases when empty") + } + + m.Databases = []types.DatabaseInfo{ + {Name: "analytics", Owner: "pg", SizePretty: "1 MB"}, + {Name: "demo", Owner: "app", SizePretty: "12 MB"}, + {Name: "postgres", Owner: "pg", SizePretty: "8 MB"}, + } + m.CurrentDatabase = "demo" + m.Loading = false + nm, cmd = m.openDatabasePicker() + m = nm.(Model) + if cmd != nil { + t.Fatal("no load when list present") + } + if m.PaletteIdx != 1 { + t.Fatalf("preselect idx=%d", m.PaletteIdx) + } + if m.Inputs == nil || m.Inputs.PaletteInput.Placeholder != "Filter databases…" { + t.Fatal("placeholder") + } + + // Re-open while already on picker keeps PrevScreen. + prev := m.PrevScreen + nm, _ = m.openDatabasePicker() + m = nm.(Model) + if m.PrevScreen != prev { + t.Fatalf("prev changed: %v -> %v", prev, m.PrevScreen) + } +} + +func TestFilteredDatabases(t *testing.T) { + m := khWorkspace(t) + m.Databases = []types.DatabaseInfo{ + {Name: "analytics", Owner: "reader"}, + {Name: "demo", Owner: "app"}, + {Name: "postgres", Owner: "pg"}, + } + m.Inputs.PaletteInput.SetValue("") + if n := len(m.filteredDatabases()); n != 3 { + t.Fatalf("all=%d", n) + } + m.Inputs.PaletteInput.SetValue("DEMO") + got := m.filteredDatabases() + if len(got) != 1 || got[0].Name != "demo" { + t.Fatalf("name filter: %+v", got) + } + m.Inputs.PaletteInput.SetValue("reader") + got = m.filteredDatabases() + if len(got) != 1 || got[0].Name != "analytics" { + t.Fatalf("owner filter: %+v", got) + } + m.Inputs.PaletteInput.SetValue("zzzz") + if len(m.filteredDatabases()) != 0 { + t.Fatal("no match") + } +} + +func TestKeysDatabasePicker(t *testing.T) { + m := khWorkspace(t) + m.Databases = []types.DatabaseInfo{ + {Name: "analytics", SizePretty: "1 MB"}, + {Name: "demo", SizePretty: "2 MB"}, + {Name: "postgres", SizePretty: "3 MB"}, + } + m.CurrentDatabase = "demo" + nm, _ := m.openDatabasePicker() + m = nm.(Model) + + nm, _ = m.keysDatabasePicker("down", tea.KeyPressMsg{}) + m = nm.(Model) + if m.PaletteIdx != 2 { + t.Fatalf("down idx=%d", m.PaletteIdx) + } + nm, _ = m.keysDatabasePicker("j", tea.KeyPressMsg{}) + m = nm.(Model) + if m.PaletteIdx != 2 { + t.Fatalf("clamp bottom idx=%d", m.PaletteIdx) + } + nm, _ = m.keysDatabasePicker("up", tea.KeyPressMsg{}) + m = nm.(Model) + if m.PaletteIdx != 1 { + t.Fatalf("up idx=%d", m.PaletteIdx) + } + nm, _ = m.keysDatabasePicker("k", tea.KeyPressMsg{}) + m = nm.(Model) + if m.PaletteIdx != 0 { + t.Fatalf("k idx=%d", m.PaletteIdx) + } + nm, _ = m.keysDatabasePicker("ctrl+n", tea.KeyPressMsg{}) + m = nm.(Model) + nm, _ = m.keysDatabasePicker("ctrl+p", tea.KeyPressMsg{}) + m = nm.(Model) + + // enter current DB closes without select cmd + m.PaletteIdx = 1 + nm, cmd := m.keysDatabasePicker("enter", tea.KeyPressMsg{Code: tea.KeyEnter}) + m = nm.(Model) + if cmd != nil || m.Screen != types.ScreenBrowser { + t.Fatalf("current db: screen=%v cmd=%v", m.Screen, cmd != nil) + } + + // enter other DB issues SelectDatabase + nm, _ = m.openDatabasePicker() + m = nm.(Model) + m.PaletteIdx = 0 + nm, cmd = m.keysDatabasePicker("enter", tea.KeyPressMsg{Code: tea.KeyEnter}) + m = nm.(Model) + if cmd == nil || !m.Loading { + t.Fatal("expected SelectDatabase") + } + if m.Screen != types.ScreenBrowser { + t.Fatalf("screen after select=%v", m.Screen) + } + + // esc + nm, _ = m.openDatabasePicker() + m = nm.(Model) + nm, _ = m.keysDatabasePicker("esc", tea.KeyPressMsg{Code: tea.KeyEsc}) + m = nm.(Model) + if m.Screen != types.ScreenBrowser { + t.Fatalf("esc screen=%v", m.Screen) + } + + // empty list enter / filter typing + m.Databases = nil + m.Screen = types.ScreenDatabasePicker + m.PrevScreen = types.ScreenBrowser + nm, cmd = m.keysDatabasePicker("enter", tea.KeyPressMsg{Code: tea.KeyEnter}) + if cmd != nil { + t.Fatal("empty enter") + } + m.Databases = []types.DatabaseInfo{{Name: "demo"}, {Name: "other"}} + m.Inputs.PaletteInput.SetValue("") + nm, _ = m.keysDatabasePicker("o", tea.KeyPressMsg{Text: "o", Code: 'o'}) + m = nm.(Model) + if m.PaletteIdx != 0 { + t.Fatalf("type resets idx=%d", m.PaletteIdx) + } + + // nil cmds + empty items branches + m.Cmds = nil + m.Databases = []types.DatabaseInfo{{Name: "x"}} + m.PaletteIdx = 0 + nm, cmd = m.keysDatabasePicker("enter", tea.KeyPressMsg{Code: tea.KeyEnter}) + if cmd != nil { + t.Fatal("nil cmds") + } + _ = nm + + // PrevScreen self-heal on esc + m = khWorkspace(t) + m.Screen = types.ScreenDatabasePicker + m.PrevScreen = types.ScreenDatabasePicker + nm, _ = m.keysDatabasePicker("esc", tea.KeyPressMsg{Code: tea.KeyEsc}) + m = nm.(Model) + if m.Screen != types.ScreenBrowser { + t.Fatalf("esc self-heal=%v", m.Screen) + } + + // nil inputs path + m.Inputs = nil + m.Databases = []types.DatabaseInfo{{Name: "a"}, {Name: "b"}} + m.Screen = types.ScreenDatabasePicker + nm, _ = m.keysDatabasePicker("down", tea.KeyPressMsg{}) + m = nm.(Model) + nm, _ = m.keysDatabasePicker("x", tea.KeyPressMsg{Text: "x"}) + _ = nm +} + +func TestDatabasePickerHotkeyAndView(t *testing.T) { + m := khWorkspace(t) + m.Databases = []types.DatabaseInfo{ + {Name: "analytics", SizePretty: "1 MB"}, + {Name: "demo", SizePretty: "2 MB"}, + {Name: "postgres", SizePretty: "3 MB"}, + } + m.CurrentDatabase = "demo" + m.Width = 100 + m.Height = 30 + + nm, cmd := m.Update(tea.KeyPressMsg{Text: "b", Code: 'b'}) + m = nm.(Model) + if m.Screen != types.ScreenDatabasePicker || cmd != nil { + t.Fatalf("hotkey b: screen=%v cmd=%v", m.Screen, cmd != nil) + } + view := m.viewDatabasePicker() + if view == "" { + t.Fatal("empty view") + } + _ = m.render() + _ = m.getScreenView() + + // loading empty list + m.Databases = nil + m.Loading = true + _ = m.viewDatabasePicker() + + // no matches + m.Databases = []types.DatabaseInfo{{Name: "demo"}} + m.Loading = false + m.Inputs.PaletteInput.SetValue("zzz") + _ = m.viewDatabasePicker() + + // many rows for windowing + m.Inputs.PaletteInput.SetValue("") + m.Databases = nil + for i := 0; i < 20; i++ { + m.Databases = append(m.Databases, types.DatabaseInfo{ + Name: "db" + string(rune('a'+i%26)) + string(rune('0'+i%10)), SizePretty: "1k", + }) + } + m.PaletteIdx = 15 + _ = m.viewDatabasePicker() + + // palette path + m = khWorkspace(t) + m.Databases = []types.DatabaseInfo{{Name: "demo"}} + nm, cmd = m.runPalette("databases") + m = nm.(Model) + if m.Screen != types.ScreenDatabasePicker { + t.Fatalf("palette databases screen=%v", m.Screen) + } + _ = cmd + + // loaded msg while picker open preselects + m.CurrentDatabase = "app" + nm, _ = m.Update(types.DatabasesLoadedMsg{Databases: []types.DatabaseInfo{ + {Name: "analytics"}, + {Name: "app"}, + }}) + m = nm.(Model) + if m.PaletteIdx != 1 || m.Loading { + t.Fatalf("loaded preselect idx=%d loading=%v", m.PaletteIdx, m.Loading) + } + + // error while picker stays quiet + nm, _ = m.Update(types.DatabasesLoadedMsg{Err: errors.New("load failed")}) + m = nm.(Model) + if m.Err != nil { + t.Fatalf("picker load err should not clobber: %v", m.Err) + } + + // typingContext includes picker + m.Screen = types.ScreenDatabasePicker + if !m.typingContext() { + t.Fatal("picker is typing context") + } + + // handleKeyPress routes picker keys + m.Databases = []types.DatabaseInfo{ + {Name: "analytics", SizePretty: "1 MB"}, + {Name: "demo", SizePretty: "2 MB"}, + } + m.CurrentDatabase = "demo" + m.Screen = types.ScreenDatabasePicker + m.PrevScreen = types.ScreenBrowser + m.PaletteIdx = 1 + // green branch: current DB visible but not selected + m.PaletteIdx = 0 + _ = m.viewDatabasePicker() + + nm, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyDown}) + m = nm.(Model) + if m.PaletteIdx != 1 { + t.Fatalf("routed down idx=%d", m.PaletteIdx) + } + + // enter with PrevScreen stuck on picker + m.Screen = types.ScreenDatabasePicker + m.PrevScreen = types.ScreenDatabasePicker + m.PaletteIdx = 0 + m.CurrentDatabase = "demo" + m.Databases = []types.DatabaseInfo{{Name: "analytics"}, {Name: "demo"}} + nm, cmd = m.keysDatabasePicker("enter", tea.KeyPressMsg{Code: tea.KeyEnter}) + m = nm.(Model) + if cmd == nil || m.Screen != types.ScreenBrowser { + t.Fatalf("enter self-heal prev: screen=%v cmd=%v", m.Screen, cmd != nil) + } + + // screen string + if types.ScreenDatabasePicker.String() != "Switch Database" { + t.Fatal(types.ScreenDatabasePicker.String()) + } +} diff --git a/internal/ui/keys.go b/internal/ui/keys.go index 5b7aa26..e09b7a4 100644 --- a/internal/ui/keys.go +++ b/internal/ui/keys.go @@ -87,7 +87,7 @@ func (m Model) typingContext() bool { } switch m.Screen { case types.ScreenAddConnection, types.ScreenEditConnection, - types.ScreenExport, types.ScreenCommandPalette: + types.ScreenExport, types.ScreenCommandPalette, types.ScreenDatabasePicker: return true case types.ScreenQuery: return m.Focus == focusContent && m.QueryFocus == "editor" && m.QueryArea != nil diff --git a/internal/ui/model.go b/internal/ui/model.go index 6ff4c44..bd09421 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -333,7 +333,7 @@ func defaultPaletteItems() []PaletteItem { {ID: "server", Label: "Server info", Keys: "i", Group: "Server"}, {ID: "tables", Label: "Browse tables", Keys: "", Group: "Browse"}, {ID: "views", Label: "Browse views", Keys: "", Group: "Browse"}, - {ID: "databases", Label: "Switch database", Keys: "", Group: "Connection"}, + {ID: "databases", Label: "Switch database", Keys: "b", Group: "Connection"}, {ID: "disconnect", Label: "Disconnect", Keys: "", Group: "Connection"}, {ID: "help", Label: "Help", Keys: "?", Group: "App"}, {ID: "logs", Label: "App logs", Keys: "L", Group: "App"}, diff --git a/internal/ui/update.go b/internal/ui/update.go index a6d220e..88c05b7 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -163,7 +163,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Loading = false if msg.Err != nil { // Don't clobber a healthy workspace with a stale list-databases race. - if !m.isWorkspaceScreen() { + if !m.isWorkspaceScreen() && m.Screen != types.ScreenDatabasePicker { m.Err = msg.Err } return m, nil @@ -173,6 +173,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for i, db := range m.Databases { if db.Name == m.CurrentDatabase { m.SelectedDBIdx = i + if m.Screen == types.ScreenDatabasePicker { + m.PaletteIdx = i + } break } } @@ -405,12 +408,18 @@ func (m Model) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.Screen = types.ScreenCommandPalette m.PaletteIdx = 0 if m.Inputs != nil { + m.Inputs.PaletteInput.Placeholder = "Filter commands…" m.Inputs.PaletteInput.SetValue("") m.Inputs.PaletteInput.Focus() } return m, nil } + // Quick database switcher popup (connected only) + if key == "b" && !m.typingContext() && m.CurrentConn != nil { + return m.openDatabasePicker() + } + // Object tree search is workspace-global (not just ScreenBrowser). if m.isWorkspaceScreen() { if m.FilterActive { @@ -466,6 +475,8 @@ func (m Model) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { return m.keysExport(key, msg) case types.ScreenCommandPalette: return m.keysPalette(key, msg) + case types.ScreenDatabasePicker: + return m.keysDatabasePicker(key, msg) } if key == "q" && !m.typingContext() { @@ -1100,6 +1111,100 @@ func (m Model) openDatabasesContent() (tea.Model, tea.Cmd) { return m, nil } +// openDatabasePicker opens a filterable modal to switch databases. +func (m Model) openDatabasePicker() (tea.Model, tea.Cmd) { + if m.CurrentConn == nil { + m.StatusMsg = "Connect first" + return m, nil + } + if m.Screen != types.ScreenDatabasePicker { + m.PrevScreen = m.Screen + } + m.Screen = types.ScreenDatabasePicker + m.PaletteIdx = 0 + if m.Inputs != nil { + m.Inputs.PaletteInput.Placeholder = "Filter databases…" + m.Inputs.PaletteInput.SetValue("") + m.Inputs.PaletteInput.Focus() + } + for i, db := range m.Databases { + if db.Name == m.CurrentDatabase { + m.PaletteIdx = i + break + } + } + if len(m.Databases) == 0 && m.Cmds != nil { + m.Loading = true + return m, m.Cmds.LoadDatabases() + } + return m, nil +} + +// filteredDatabases returns databases matching the picker filter. +func (m Model) filteredDatabases() []types.DatabaseInfo { + filter := "" + if m.Inputs != nil { + filter = strings.ToLower(strings.TrimSpace(m.Inputs.PaletteInput.Value())) + } + if filter == "" { + out := make([]types.DatabaseInfo, len(m.Databases)) + copy(out, m.Databases) + return out + } + var out []types.DatabaseInfo + for _, db := range m.Databases { + if strings.Contains(strings.ToLower(db.Name), filter) || + strings.Contains(strings.ToLower(db.Owner), filter) { + out = append(out, db) + } + } + return out +} + +func (m Model) keysDatabasePicker(key string, msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + items := m.filteredDatabases() + switch key { + case "esc": + m.Screen = m.PrevScreen + if m.Screen == types.ScreenDatabasePicker { + m.Screen = types.ScreenBrowser + } + return m, nil + case "down", "ctrl+n", "j": + if len(items) > 0 { + m.PaletteIdx = min(m.PaletteIdx+1, len(items)-1) + } + return m, nil + case "up", "ctrl+p", "k": + m.PaletteIdx = max(m.PaletteIdx-1, 0) + return m, nil + case "enter": + if len(items) == 0 || m.Cmds == nil { + return m, nil + } + db := items[clamp(m.PaletteIdx, 0, len(items)-1)] + prev := m.PrevScreen + if prev == types.ScreenDatabasePicker { + prev = types.ScreenBrowser + } + m.Screen = prev + if db.Name == m.CurrentDatabase { + m.StatusMsg = m.CurrentDatabase + return m, nil + } + m.Loading = true + m.Err = nil + return m, m.Cmds.SelectDatabase(db.Name) + } + if m.Inputs != nil { + var cmd tea.Cmd + m.Inputs.PaletteInput, cmd = m.Inputs.PaletteInput.Update(msg) + m.PaletteIdx = 0 + return m, cmd + } + return m, nil +} + func (m Model) openERD() (tea.Model, tea.Cmd) { m = m.clearObjectContent() m.ContentMode = contentPreview @@ -1931,7 +2036,7 @@ func (m Model) runPalette(id string) (tea.Model, tea.Cmd) { m.ContentMode = contentPreview return m.loadObjectsForNav() case "databases": - return m.openDatabasesContent() + return m.openDatabasePicker() case "disconnect": if m.Cmds != nil { return m, m.Cmds.Disconnect() diff --git a/internal/ui/view.go b/internal/ui/view.go index e64a5c9..c80a21f 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -43,6 +43,8 @@ func (m Model) getScreenView() string { return m.viewExport() case types.ScreenCommandPalette: return m.viewCommandPalette() + case types.ScreenDatabasePicker: + return m.viewDatabasePicker() default: return "" } @@ -82,7 +84,7 @@ func (m Model) render() string { switch m.Screen { case types.ScreenConnections, types.ScreenAddConnection, types.ScreenEditConnection, types.ScreenConfirmDelete, types.ScreenTestConnection, types.ScreenHelp, - types.ScreenCommandPalette, types.ScreenExport: + types.ScreenCommandPalette, types.ScreenDatabasePicker, types.ScreenExport: vPos = lipgloss.Center hPos = lipgloss.Center case types.ScreenBrowser, types.ScreenTableData, types.ScreenTableDetail, @@ -977,6 +979,7 @@ func (m Model) viewHelp() string { {"space", "Toggle filter"}, {"1-6", "Kind filters"}, {"/", "Search"}, + {"b", "Switch database"}, {";", "SQL editor"}, {":", "SQL editor"}, {"D", "Structure"}, @@ -1188,3 +1191,52 @@ func (m Model) viewCommandPalette() string { } return m.formModal(b.String()) } + +func (m Model) viewDatabasePicker() string { + var b strings.Builder + b.WriteString(titleStyle.Render("Switch Database")) + if m.CurrentDatabase != "" { + b.WriteString(dimStyle.Render(" · " + m.CurrentDatabase)) + } + b.WriteString("\n\n") + if m.Inputs != nil { + b.WriteString(m.Inputs.PaletteInput.View()) + b.WriteString("\n\n") + } + if m.Loading && len(m.Databases) == 0 { + b.WriteString(dimStyle.Render("Loading databases…")) + return m.formModal(b.String()) + } + items := m.filteredDatabases() + if len(items) == 0 { + b.WriteString(dimStyle.Render("No matches")) + } else { + idx := clamp(m.PaletteIdx, 0, len(items)-1) + maxVisible := 12 + start, end := listWindow(idx, len(items), maxVisible) + for i := start; i < end; i++ { + db := items[i] + mark := " " + if db.Name == m.CurrentDatabase { + mark = "● " + } + line := fmt.Sprintf("%s%s %s", mark, padRight(db.Name, 28), padRight(db.SizePretty, 10)) + switch { + case i == idx: + b.WriteString(selectedStyle.Render(padRight(line, 48))) + case db.Name == m.CurrentDatabase: + b.WriteString(greenStyle.Render(padRight(line, 48))) + default: + b.WriteString(dimStyle.Render(line)) + } + b.WriteString("\n") + } + if len(items) > maxVisible { + b.WriteString(dimStyle.Render(fmt.Sprintf(" ↕ %d–%d of %d", start+1, end, len(items)))) + b.WriteString("\n") + } + } + b.WriteString("\n") + b.WriteString(dimStyle.Render("↑/↓ navigate enter open esc cancel type to filter")) + return m.formModal(b.String()) +} diff --git a/internal/ui/view_sidebar.go b/internal/ui/view_sidebar.go index 2b209b9..569db27 100644 --- a/internal/ui/view_sidebar.go +++ b/internal/ui/view_sidebar.go @@ -826,6 +826,7 @@ func (m Model) viewWorkspaceFooter() string { struct{ key, desc string }{"D", "struct"}, struct{ key, desc string }{"/", "search"}, struct{ key, desc string }{";", "query"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "disconnect"}, ) } @@ -837,12 +838,14 @@ func (m Model) viewWorkspaceFooter() string { struct{ key, desc string }{"Y", "row"}, struct{ key, desc string }{"x", "export"}, struct{ key, desc string }{"D", "struct"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "tree"}, ) case types.ScreenTableDetail: keys = append(keys, struct{ key, desc string }{"h/l", "tabs"}, struct{ key, desc string }{"enter", "data"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "tree"}, ) case types.ScreenQuery: @@ -852,22 +855,26 @@ func (m Model) viewWorkspaceFooter() string { struct{ key, desc string }{"tab", "complete"}, struct{ key, desc string }{"↑↓", "suggest"}, struct{ key, desc string }{"x", "export"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "tree"}, ) case types.ScreenActivity: keys = append(keys, struct{ key, desc string }{"r", "refresh"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "tree"}, ) case types.ScreenERD: keys = append(keys, struct{ key, desc string }{"j/k", "scroll"}, struct{ key, desc string }{"r", "refresh"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "tree"}, ) case types.ScreenServerInfo: keys = append(keys, struct{ key, desc string }{"r", "refresh"}, + struct{ key, desc string }{"b", "db"}, struct{ key, desc string }{"esc", "tree"}, ) } From 7e5312722531a9655f5917b237244e62f328003b Mon Sep 17 00:00:00 2001 From: David Budnick Date: Mon, 27 Jul 2026 15:27:54 -0500 Subject: [PATCH 2/2] gofmt screens.go --- internal/types/screens.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/types/screens.go b/internal/types/screens.go index 39b0750..804826f 100644 --- a/internal/types/screens.go +++ b/internal/types/screens.go @@ -28,25 +28,25 @@ const ( // String returns a human-readable name for the screen. func (s Screen) String() string { names := map[Screen]string{ - ScreenConnections: "Connections", - ScreenAddConnection: "Add Connection", - ScreenEditConnection: "Edit Connection", - ScreenDatabases: "Databases", - ScreenBrowser: "Browser", - ScreenTableData: "Table Data", - ScreenTableDetail: "Table Detail", - ScreenQuery: "Query", - ScreenActivity: "Activity", - ScreenERD: "ERD", - ScreenServerInfo: "Server Info", - ScreenHelp: "Help", - ScreenConfirmDelete: "Confirm Delete", - ScreenTestConnection: "Test Connection", - ScreenLogs: "Logs", - ScreenFavorites: "Favorites", - ScreenExport: "Export", - ScreenCommandPalette: "Command Palette", - ScreenDatabasePicker: "Switch Database", + ScreenConnections: "Connections", + ScreenAddConnection: "Add Connection", + ScreenEditConnection: "Edit Connection", + ScreenDatabases: "Databases", + ScreenBrowser: "Browser", + ScreenTableData: "Table Data", + ScreenTableDetail: "Table Detail", + ScreenQuery: "Query", + ScreenActivity: "Activity", + ScreenERD: "ERD", + ScreenServerInfo: "Server Info", + ScreenHelp: "Help", + ScreenConfirmDelete: "Confirm Delete", + ScreenTestConnection: "Test Connection", + ScreenLogs: "Logs", + ScreenFavorites: "Favorites", + ScreenExport: "Export", + ScreenCommandPalette: "Command Palette", + ScreenDatabasePicker: "Switch Database", } if name, ok := names[s]; ok { return name