From 7d41391e135f3c1ce52f14a8ba993bec25dd701d Mon Sep 17 00:00:00 2001 From: swanadiva Date: Tue, 23 Jun 2026 18:48:50 +0700 Subject: [PATCH] fix: Space/Left/Right passthrough in host form, key/snippet refresh on save/delete, WindowSize to all tabs, footer improvements --- pkg/tui/host_form_tab.go | 7 +++- pkg/tui/key_form_tab.go | 2 +- pkg/tui/key_list_tab.go | 18 +++++++++ pkg/tui/sftp_browser_tab.go | 2 +- pkg/tui/snippet_form_tab.go | 2 +- pkg/tui/snippet_list_tab.go | 18 +++++++++ pkg/tui/tabs.go | 7 +++- pkg/tui/tui.go | 73 ++++++++++++++++++++++++++++++++++--- 8 files changed, 118 insertions(+), 11 deletions(-) diff --git a/pkg/tui/host_form_tab.go b/pkg/tui/host_form_tab.go index 97a8e6c..2f5f7bf 100644 --- a/pkg/tui/host_form_tab.go +++ b/pkg/tui/host_form_tab.go @@ -159,7 +159,10 @@ func (t *HostFormTab) Update(msg tea.Msg) (Tab, tea.Cmd) { t.toggleAuthType() return t, nil } - fallthrough + // pass through to text input (allow typing spaces, cursor nav) + var cmd tea.Cmd + t.inputs[t.focus], cmd = t.inputs[t.focus].Update(msg) + return t, cmd case "tab", "down": t.nextField() @@ -356,7 +359,7 @@ func (t *HostFormTab) View() string { inner.WriteString("\n") inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, - SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Enter:next Ctrl+S:save Esc:cancel"))) + SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Shift+Tab:prev ↑↓:nav Enter:next Ctrl+S:save Esc:cancel"))) box := BorderStyle.Render(inner.String()) var b strings.Builder diff --git a/pkg/tui/key_form_tab.go b/pkg/tui/key_form_tab.go index cb37c47..81c8b10 100644 --- a/pkg/tui/key_form_tab.go +++ b/pkg/tui/key_form_tab.go @@ -236,7 +236,7 @@ func (t *KeyFormTab) View() string { inner.WriteString("\n") inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, - SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Enter:next Ctrl+S:save Esc:cancel"))) + SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Shift+Tab:prev ↑↓:nav Enter:next Ctrl+S:save Esc:cancel"))) box := BorderStyle.Render(inner.String()) var b strings.Builder diff --git a/pkg/tui/key_list_tab.go b/pkg/tui/key_list_tab.go index 56ac9ac..603eca4 100644 --- a/pkg/tui/key_list_tab.go +++ b/pkg/tui/key_list_tab.go @@ -229,6 +229,24 @@ func (t *KeyListTab) View() string { func (t *KeyListTab) Close() {} +// SetKeys updates the key list data directly (used for refresh) +func (t *KeyListTab) SetKeys(keys []*models.KeyPair) { + t.mu.Lock() + defer t.mu.Unlock() + t.state = keyListReady + t.keys = keys +} + +// FindKeyListTab finds the first KeyListTab in a list of tabs +func FindKeyListTab(tabs []Tab) *KeyListTab { + for _, tab := range tabs { + if kt, ok := tab.(*KeyListTab); ok { + return kt + } + } + return nil +} + // keyListLoadedMsg carries the loaded key list type keyListLoadedMsg struct { keys []*models.KeyPair diff --git a/pkg/tui/sftp_browser_tab.go b/pkg/tui/sftp_browser_tab.go index 3893860..67a3de7 100644 --- a/pkg/tui/sftp_browser_tab.go +++ b/pkg/tui/sftp_browser_tab.go @@ -567,7 +567,7 @@ func (t *SFTPBrowserTab) View() string { } b.WriteString("\n" + lipgloss.PlaceHorizontal(t.width, lipgloss.Center, - SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:pane ↑↓:nav /:filter C:copy D:delete N:mkdir R:refresh Esc:close"))) + SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:pane ↑↓:nav Enter/→:open ←/Backspace:up /:filter C:copy D:delete N:mkdir R:refresh Esc:close"))) return b.String() } diff --git a/pkg/tui/snippet_form_tab.go b/pkg/tui/snippet_form_tab.go index a32b280..e093660 100644 --- a/pkg/tui/snippet_form_tab.go +++ b/pkg/tui/snippet_form_tab.go @@ -244,7 +244,7 @@ func (t *SnippetFormTab) View() string { inner.WriteString("\n") inner.WriteString(lipgloss.PlaceHorizontal(contentW, lipgloss.Center, - SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Enter:next Ctrl+S:save Esc:cancel"))) + SubtitleStyle.Render("Ctrl+Tab:switch Ctrl+Q:close Tab:next Shift+Tab:prev ↑↓:nav Enter:next Ctrl+S:save Esc:cancel"))) box := BorderStyle.Render(inner.String()) var b strings.Builder diff --git a/pkg/tui/snippet_list_tab.go b/pkg/tui/snippet_list_tab.go index 7a28456..c55e2a9 100644 --- a/pkg/tui/snippet_list_tab.go +++ b/pkg/tui/snippet_list_tab.go @@ -229,6 +229,24 @@ func (t *SnippetListTab) View() string { func (t *SnippetListTab) Close() {} +// SetSnippets updates the snippet list data directly (used for refresh) +func (t *SnippetListTab) SetSnippets(snippets []*models.Snippet) { + t.mu.Lock() + defer t.mu.Unlock() + t.state = snippetListReady + t.snippets = snippets +} + +// FindSnippetListTab finds the first SnippetListTab in a list of tabs +func FindSnippetListTab(tabs []Tab) *SnippetListTab { + for _, tab := range tabs { + if st, ok := tab.(*SnippetListTab); ok { + return st + } + } + return nil +} + // snippetListLoadedMsg carries the loaded snippet list type snippetListLoadedMsg struct { snippets []*models.Snippet diff --git a/pkg/tui/tabs.go b/pkg/tui/tabs.go index 207c4b4..c64e469 100644 --- a/pkg/tui/tabs.go +++ b/pkg/tui/tabs.go @@ -146,9 +146,14 @@ func (tm *TabManager) Update(msg tea.Msg) (tea.Cmd, error) { } } - // Handle window resize + // Handle window resize — forward to ALL tabs if wsMsg, ok := msg.(tea.WindowSizeMsg); ok { tm.SetSize(wsMsg.Width, wsMsg.Height) + for i, t := range tm.tabs { + updated, _ := t.Update(msg) + tm.tabs[i] = updated + } + return nil, nil } updated, cmd := tm.tabs[tm.active].Update(msg) diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index a956a1b..9e17b45 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -151,33 +151,96 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Error = msg.err return m, nil } - // Close form tab and return to list (which auto-refreshes via Init) + // Close form tab and switch back to list if m.tabs.Len() > 1 { m.tabs.CloseActive() } - return m, nil + // Reload keys + return m, func() tea.Msg { + store, err := storage.NewJSONStorage(m.dataDir) + if err != nil { + return nil + } + keys, err := store.ListKeyPairs(context.Background()) + if err != nil { + return nil + } + return keyListLoadedMsg{keys: keys} + } case deleteKeyResultMsg: if msg.err != nil { m.Error = msg.err } - return m, nil + // Reload keys + return m, func() tea.Msg { + store, err := storage.NewJSONStorage(m.dataDir) + if err != nil { + return nil + } + keys, err := store.ListKeyPairs(context.Background()) + if err != nil { + return nil + } + return keyListLoadedMsg{keys: keys} + } + + case keyListLoadedMsg: + if msg.err != nil { + m.Error = msg.err + return m, nil + } + if kt := FindKeyListTab(m.tabs.tabs); kt != nil { + kt.SetKeys(msg.keys) + } case saveSnippetResultMsg: if msg.err != nil { m.Error = msg.err return m, nil } + // Close form tab and switch back to list if m.tabs.Len() > 1 { m.tabs.CloseActive() } - return m, nil + // Reload snippets + return m, func() tea.Msg { + store, err := storage.NewJSONStorage(m.dataDir) + if err != nil { + return nil + } + snippets, err := store.ListSnippets(context.Background()) + if err != nil { + return nil + } + return snippetListLoadedMsg{snippets: snippets} + } case deleteSnippetResultMsg: if msg.err != nil { m.Error = msg.err } - return m, nil + // Reload snippets + return m, func() tea.Msg { + store, err := storage.NewJSONStorage(m.dataDir) + if err != nil { + return nil + } + snippets, err := store.ListSnippets(context.Background()) + if err != nil { + return nil + } + return snippetListLoadedMsg{snippets: snippets} + } + + case snippetListLoadedMsg: + if msg.err != nil { + m.Error = msg.err + return m, nil + } + if st := FindSnippetListTab(m.tabs.tabs); st != nil { + st.SetSnippets(msg.snippets) + } }