Skip to content

Commit 5ccfef0

Browse files
committed
chore: better tab completion on ssh key path
1 parent 76303f0 commit 5ccfef0

File tree

2 files changed

+78
-18
lines changed

2 files changed

+78
-18
lines changed

internal/tui/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ type model struct {
106106
formDefaultUser string
107107
formUserConfigs []formUserConfig
108108
formUserCursor int
109+
formScroll int
109110
formPathSuggestions []string
110111
formPathSuggestIndex int
111112
pendingHost host.Host

internal/tui/hostform.go

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func (m model) startHostForm(editID string, duplicate bool) (model, tea.Cmd) {
111111
m.formDefaultUser = ""
112112
m.formUserConfigs = nil
113113
m.formUserCursor = 0
114+
m.formScroll = 0
114115
m.formPathSuggestions = nil
115116
m.formPathSuggestIndex = 0
116117

@@ -180,6 +181,12 @@ func (m model) updateHostForm(msg tea.Msg) (tea.Model, tea.Cmd) {
180181
if m.cyclePathSuggestion(-1) {
181182
return m, nil
182183
}
184+
case "pgdown":
185+
m.formScroll += 4
186+
return m, nil
187+
case "pgup":
188+
m.formScroll = max(0, m.formScroll-4)
189+
return m, nil
183190
case "left", "h":
184191
switch m.formFocus {
185192
case fSelectedUser:
@@ -250,6 +257,12 @@ func (m model) cycleFormFocus(dir int) (tea.Model, tea.Cmd) {
250257
}
251258
cur = (cur + dir + len(focuses)) % len(focuses)
252259
m.formFocus = focuses[cur]
260+
if m.formFocus >= fSelectedUser {
261+
// Auto-jump to lower section when entering per-user auth controls.
262+
m.formScroll = 9999
263+
} else {
264+
m.formScroll = 0
265+
}
253266

254267
if idx := formInputIdx(m.formFocus); idx >= 0 {
255268
m.refreshPathSuggestions()
@@ -387,7 +400,17 @@ func (m model) viewHostForm() string {
387400

388401
formW := 90
389402
formH := 38
390-
colW := (formW - 6) / 2
403+
contentW := formW - 6
404+
colGap := 2
405+
colW := (contentW - colGap) / 2
406+
407+
// Keep input widths in sync with the rendered layout so fields use available space.
408+
m.formInputs[0].Width = colW
409+
m.formInputs[1].Width = colW
410+
m.formInputs[2].Width = colW
411+
m.formInputs[3].Width = colW
412+
m.formInputs[4].Width = min(10, colW)
413+
m.formInputs[5].Width = contentW
391414

392415
var b strings.Builder
393416
b.WriteString(titleStyle.Render("📝 "+title) + "\n\n")
@@ -424,7 +447,7 @@ func (m model) viewHostForm() string {
424447
b.WriteString(hintStyle.Render(" Comma-separated usernames. Example: main, ubuntu, deploy") + "\n\n")
425448

426449
if len(m.formUserConfigs) > 0 {
427-
b.WriteString(m.renderSelectedUserSection())
450+
b.WriteString(m.renderSelectedUserSection(contentW))
428451
} else {
429452
b.WriteString(hintStyle.Render(" Type usernames above to configure per-user auth settings.") + "\n")
430453
}
@@ -433,18 +456,35 @@ func (m model) viewHostForm() string {
433456
b.WriteString("\n" + errorStyle.Render("✗ "+m.formErr) + "\n")
434457
}
435458

436-
b.WriteString("\n" + statusBarStyle.Render("tab/↑↓ navigate • space default user • ←→ adjust • enter save • esc cancel"))
459+
b.WriteString("\n" + statusBarStyle.Render("tab/↑↓ navigate • space default user • ←→ adjust • pgup/pgdn scroll • enter save • esc cancel"))
437460

438461
content := b.String()
462+
content = m.renderFormViewport(content, formH)
463+
return boxStyle.Width(formW).Render(content)
464+
}
465+
466+
func (m model) renderFormViewport(content string, height int) string {
439467
lines := strings.Split(content, "\n")
440-
for len(lines) < formH {
441-
lines = append(lines, "")
468+
if len(lines) <= height {
469+
for len(lines) < height {
470+
lines = append(lines, "")
471+
}
472+
return strings.Join(lines, "\n")
442473
}
443-
content = strings.Join(lines[:formH], "\n")
444-
return boxStyle.Width(formW).Render(content)
474+
475+
maxOffset := len(lines) - height
476+
offset := m.formScroll
477+
if offset < 0 {
478+
offset = 0
479+
}
480+
if offset > maxOffset {
481+
offset = maxOffset
482+
}
483+
484+
return strings.Join(lines[offset:offset+height], "\n")
445485
}
446486

447-
func (m model) renderSelectedUserSection() string {
487+
func (m model) renderSelectedUserSection(contentW int) string {
448488
user := m.currentFormUser()
449489
if user == nil {
450490
return ""
@@ -463,7 +503,9 @@ func (m model) renderSelectedUserSection() string {
463503
}
464504
b.WriteString("\n")
465505

466-
cardBorder := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(highlight).Padding(1, 2).Width(80)
506+
cardBorder := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(highlight).Padding(1, 2).Width(contentW)
507+
cardContentW := max(20, contentW-6)
508+
m.formInputs[6].Width = cardContentW
467509

468510
var card strings.Builder
469511
headerLabel := "👤 " + user.Username
@@ -502,7 +544,7 @@ func (m model) renderSelectedUserSection() string {
502544
card.WriteString(credLabel + "\n")
503545
card.WriteString(m.formInputs[6].View() + "\n")
504546
card.WriteString(hintStyle.Render(" Key path or paste private key") + "\n")
505-
card.WriteString(m.renderPathSuggestions())
547+
card.WriteString(m.renderPathSuggestions(cardContentW))
506548
if m.formEditing != "" && (user.ExistingKeyPath != "" || len(user.ExistingEncKey) > 0) {
507549
card.WriteString(hintStyle.Render(" Leave empty to keep current SSH key") + "\n")
508550
}
@@ -727,6 +769,7 @@ func (m *model) acceptPathSuggestion() bool {
727769
}
728770
suggestion := m.formPathSuggestions[m.formPathSuggestIndex]
729771
input.SetValue(suggestion)
772+
input.CursorEnd()
730773
if m.formFocus == fSelectedUserCredential {
731774
m.storeSelectedUserCredentialInput()
732775
}
@@ -850,20 +893,36 @@ func identityConflictMessage(aliasConflict, hostnameConflict bool) string {
850893
}
851894
}
852895

853-
func (m model) renderPathSuggestions() string {
896+
func (m model) renderPathSuggestions(contentW int) string {
854897
if len(m.formPathSuggestions) == 0 {
855898
return ""
856899
}
857-
var parts []string
900+
itemW := max(20, contentW-2)
901+
var b strings.Builder
902+
b.WriteString(hintStyle.Render(" Suggestions (tab accept • ctrl+n/p select):") + "\n")
903+
858904
for i, s := range m.formPathSuggestions {
905+
label := truncateForWidth(s, itemW-4)
906+
prefix := " "
859907
style := lipgloss.NewStyle().Foreground(subtle)
860908
if i == m.formPathSuggestIndex {
861-
style = lipgloss.NewStyle().Foreground(highlight).Bold(true)
862-
s = "> " + s
863-
} else {
864-
s = " " + s
909+
prefix = "▸ "
910+
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#111827")).Background(highlight).Bold(true).Padding(0, 1)
865911
}
866-
parts = append(parts, style.Render(s))
912+
line := prefix + label
913+
b.WriteString(" " + style.Render(line) + "\n")
914+
}
915+
916+
return b.String()
917+
}
918+
919+
func truncateForWidth(s string, width int) string {
920+
if width <= 3 {
921+
return s
922+
}
923+
r := []rune(s)
924+
if len(r) <= width {
925+
return s
867926
}
868-
return " " + strings.Join(parts, " ") + "\n"
927+
return string(r[:width-3]) + "..."
869928
}

0 commit comments

Comments
 (0)