Skip to content
Open
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
4 changes: 1 addition & 3 deletions internal/adapters/ui/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package ui

import (
"strings"
"time"

"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
Expand Down Expand Up @@ -98,8 +97,7 @@ func (h *AppHeader) buildRightSection(bg tcell.Color) *tview.TextView {
SetDynamicColors(true).
SetTextAlign(tview.AlignRight)
right.SetBackgroundColor(bg)
currentTime := time.Now().Format("Mon, 02 Jan 2006 15:04")
right.SetText("[#55AAFF::u]🔗 " + h.repoURL + "[-] [#AAAAAA]• " + currentTime + "[-]")
right.SetText("[#55AAFF::u]" + h.repoURL + "[-]")
return right
}

Expand Down
2 changes: 1 addition & 1 deletion internal/adapters/ui/hint_bar.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ import (
func NewHintBar() *tview.TextView {
hint := tview.NewTextView().SetDynamicColors(true)
hint.SetBackgroundColor(tcell.Color233)
hint.SetText("[#BBBBBB]Press [::b]/[-:-:b] to search… • ↑↓ Navigate Enter SSH • c Copy SSH • g Ping • r Refresh • a Add • e Edit • t Tags • d Delete • p Pin/Unpin • s Sort[-]")
hint.SetText(" [red]/[-] Search [red]↑↓[-] Navigate [red]Enter[-] SSH [red]c[-] Copy [red]g[-] Ping [red]r[-] Refresh [red]a[-] Add [red]e[-] Edit [red]t[-] Tags [red]d[-] Delete [red]p[-] Pin/Unpin [red]s[-] Sort")
return hint
}
2 changes: 1 addition & 1 deletion internal/adapters/ui/search_bar.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewSearchBar() *SearchBar {
}

func (s *SearchBar) build() {
s.InputField.SetLabel(" 🔍 Search: ").
s.InputField.SetLabel(" Search: ").
SetFieldBackgroundColor(tcell.Color233).
SetFieldTextColor(tcell.Color252).
SetFieldWidth(30).
Expand Down
8 changes: 4 additions & 4 deletions internal/adapters/ui/server_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func renderTagChips(tags []string) string {
func (sd *ServerDetails) UpdateServer(server domain.Server) {
lastSeen := server.LastSeen.Format("2006-01-02 15:04:05")
if server.LastSeen.IsZero() {
lastSeen = "Never"
lastSeen = "never"
}
serverKey := strings.Join(server.IdentityFiles, ", ")

Expand All @@ -83,7 +83,7 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) {
}

text := fmt.Sprintf(
"[::b]%s[-]\n\n[::b]Basic Settings:[-]\n Host: [white]%s[-]\n User: [white]%s[-]\n Port: [white]%s[-]\n Key: [white]%s[-]\n Tags: %s\n Pinned: [white]%s[-]\n Last SSH: %s\n SSH Count: [white]%d[-]\n",
"[yellow::b]%s[-::-]\n\n[::b]Basic Settings[-::-]\n\n Host: [white]%s[-]\n User: [white]%s[-]\n Port: [white]%s[-]\n Key: [white]%s[-]\n Tags: %s\n Pinned: [white]%s[-]\n Last SSH: %s\n SSH Count: [white]%d[-]\n",
aliasText, hostText, userText, portText,
serverKey, tagsText, pinnedStr,
lastSeen, server.SSHCount)
Expand Down Expand Up @@ -197,7 +197,7 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) {

// Build advanced settings text without group labels for cleaner display
hasAdvanced := false
advancedText := "\n[::b]Advanced Settings:[-]\n"
advancedText := "\n[::b]Advanced Settings[-::-]\n\n"

for _, group := range groups {
for _, field := range group.fields {
Expand All @@ -213,7 +213,7 @@ func (sd *ServerDetails) UpdateServer(server domain.Server) {
}

// Commands list
text += "\n[::b]Commands:[-]\n Enter: SSH connect\n c: Copy SSH command\n g: Ping server\n r: Refresh list\n a: Add new server\n e: Edit entry\n t: Edit tags\n d: Delete entry\n p: Pin/Unpin"
text += "\n[::b]Commands[-::-]\n\n Enter: SSH connect\n c: Copy SSH command\n g: Ping server\n r: Refresh list\n a: Add new server\n e: Edit entry\n t: Edit tags\n d: Delete entry\n p: Pin/Unpin"

sd.TextView.SetText(text)
}
Expand Down
44 changes: 22 additions & 22 deletions internal/adapters/ui/server_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ func (sf *ServerForm) build() {
// Create hint bar with same background as main screen's status bar
hintBar := tview.NewTextView().SetDynamicColors(true)
hintBar.SetBackgroundColor(tcell.Color235)
hintBar.SetTextAlign(tview.AlignCenter)
hintBar.SetText("[white]^H/^L[-] Navigate • [white]^S[-] Save • [white]Esc[-] Cancel")
hintBar.SetTextAlign(tview.AlignLeft)
hintBar.SetText(" [red]^H/^L[-] Navigate [red]^S[-] Save [red]Esc[-] Cancel")

// Setup main container - header at top, hint bar at bottom
sf.Flex.AddItem(sf.header, 2, 0, false).
Expand Down Expand Up @@ -451,7 +451,7 @@ func (sf *ServerForm) updateHelp(fieldName string) {
if len(help.Examples) > 0 {
example = help.Examples[0]
}
content = fmt.Sprintf("[yellow]%s:[-] %s", help.Field, escapeForTview(help.Description))
content = fmt.Sprintf("[yellow]%s:[-]%s", help.Field, escapeForTview(help.Description))
if example != "" {
content += fmt.Sprintf(" [dim](e.g., %s)[-]", escapeForTview(example))
}
Expand Down Expand Up @@ -484,7 +484,7 @@ func (sf *ServerForm) formatDetailedHelp(help *FieldHelp) string {
}

// Title with field name and separator below
b.WriteString(fmt.Sprintf("[yellow::b]📖 %s[-::-]\n", help.Field))
b.WriteString(fmt.Sprintf("[yellow::b]%s[-::-]\n", help.Field))
b.WriteString("[#444444]" + strings.Repeat("─", separatorWidth) + "[-]\n\n")

// Description - needs escaping as it might contain brackets
Expand Down Expand Up @@ -1270,7 +1270,7 @@ func (sf *ServerForm) createConnectionForm() {
form := tview.NewForm()
defaultValues := sf.getDefaultValues()

form.AddTextView("\n[yellow]Proxy & Command[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Proxy & Command[-]", "", 0, 1, true, false)
sf.addInputFieldWithHelp(form, "ProxyJump:", "ProxyJump", defaultValues.ProxyJump, 40, GetFieldPlaceholder("ProxyJump"))
sf.addInputFieldWithHelp(form, "ProxyCommand:", "ProxyCommand", defaultValues.ProxyCommand, 40, GetFieldPlaceholder("ProxyCommand"))
sf.addInputFieldWithHelp(form, "RemoteCommand:", "RemoteCommand", defaultValues.RemoteCommand, 40, GetFieldPlaceholder("RemoteCommand"))
Expand All @@ -1285,7 +1285,7 @@ func (sf *ServerForm) createConnectionForm() {
sessionTypeIndex := sf.findOptionIndex(sessionTypeOptions, defaultValues.SessionType)
sf.addDropDownWithHelp(form, "SessionType:", "SessionType", sessionTypeOptions, sessionTypeIndex)

form.AddTextView("\n[yellow]Connection Settings[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Connection Settings[-]", "", 0, 1, true, false)
sf.addValidatedInputField(form, "ConnectTimeout:", "ConnectTimeout", defaultValues.ConnectTimeout, 10, GetFieldPlaceholder("ConnectTimeout"))
sf.addValidatedInputField(form, "ConnectionAttempts:", "ConnectionAttempts", defaultValues.ConnectionAttempts, 10, GetFieldPlaceholder("ConnectionAttempts"))
sf.addValidatedInputField(form, "IPQoS:", "IPQoS", defaultValues.IPQoS, 20, GetFieldPlaceholder("IPQoS"))
Expand All @@ -1295,7 +1295,7 @@ func (sf *ServerForm) createConnectionForm() {
batchModeIndex := sf.findOptionIndex(batchModeOptions, defaultValues.BatchMode)
sf.addDropDownWithHelp(form, "BatchMode:", "BatchMode", batchModeOptions, batchModeIndex)

form.AddTextView("\n[yellow]Bind Options[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Bind Options[-]", "", 0, 1, true, false)
sf.addValidatedInputField(form, "BindAddress:", "BindAddress", defaultValues.BindAddress, 40, GetFieldPlaceholder("BindAddress"))

// BindInterface dropdown with available network interfaces
Expand All @@ -1308,7 +1308,7 @@ func (sf *ServerForm) createConnectionForm() {
addressFamilyIndex := sf.findOptionIndex(addressFamilyOptions, defaultValues.AddressFamily)
sf.addDropDownWithHelp(form, "AddressFamily:", "AddressFamily", addressFamilyOptions, addressFamilyIndex)

form.AddTextView("\n[yellow]Hostname Canonicalization[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Hostname Canonicalization[-]", "", 0, 1, true, false)

// CanonicalizeHostname dropdown
canonicalizeOptions := createOptionsWithDefault("CanonicalizeHostname", []string{"", "yes", "no", "always"})
Expand All @@ -1326,7 +1326,7 @@ func (sf *ServerForm) createConnectionForm() {

sf.addInputFieldWithHelp(form, "CanonicalizePermittedCNAMEs:", "CanonicalizePermittedCNAMEs", defaultValues.CanonicalizePermittedCNAMEs, 40, GetFieldPlaceholder("CanonicalizePermittedCNAMEs"))

form.AddTextView("\n[yellow]Keep-Alive[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Keep-Alive[-]", "", 0, 1, true, false)
sf.addValidatedInputField(form, "ServerAliveInterval:", "ServerAliveInterval", defaultValues.ServerAliveInterval, 10, GetFieldPlaceholder("ServerAliveInterval"))
sf.addValidatedInputField(form, "ServerAliveCountMax:", "ServerAliveCountMax", defaultValues.ServerAliveCountMax, 10, GetFieldPlaceholder("ServerAliveCountMax"))

Expand All @@ -1340,7 +1340,7 @@ func (sf *ServerForm) createConnectionForm() {
tcpKeepAliveIndex := sf.findOptionIndex(tcpKeepAliveOptions, defaultValues.TCPKeepAlive)
sf.addDropDownWithHelp(form, "TCPKeepAlive:", "TCPKeepAlive", tcpKeepAliveOptions, tcpKeepAliveIndex)

form.AddTextView("\n[yellow]Multiplexing[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Multiplexing[-]", "", 0, 1, true, false)
// ControlMaster dropdown
controlMasterOptions := createOptionsWithDefault("ControlMaster", []string{"", "yes", "no", "auto", "ask", "autoask"})
controlMasterIndex := sf.findOptionIndex(controlMasterOptions, defaultValues.ControlMaster)
Expand All @@ -1364,7 +1364,7 @@ func (sf *ServerForm) createForwardingForm() {
form := tview.NewForm()
defaultValues := sf.getDefaultValues()

form.AddTextView("\n[yellow]Port Forwarding[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Port Forwarding[-]", "", 0, 1, true, false)
sf.addValidatedInputField(form, "LocalForward:", "LocalForward", defaultValues.LocalForward, 40, GetFieldPlaceholder("LocalForward"))
sf.addValidatedInputField(form, "RemoteForward:", "RemoteForward", defaultValues.RemoteForward, 40, GetFieldPlaceholder("RemoteForward"))
sf.addValidatedInputField(form, "DynamicForward:", "DynamicForward", defaultValues.DynamicForward, 40, GetFieldPlaceholder("DynamicForward"))
Expand All @@ -1384,7 +1384,7 @@ func (sf *ServerForm) createForwardingForm() {
gatewayPortsIndex := sf.findOptionIndex(gatewayPortsOptions, defaultValues.GatewayPorts)
sf.addDropDownWithHelp(form, "GatewayPorts:", "GatewayPorts", gatewayPortsOptions, gatewayPortsIndex)

form.AddTextView("\n[yellow]Agent & X11 Forwarding[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Agent & X11 Forwarding[-]", "", 0, 1, true, false)

// ForwardAgent dropdown
forwardAgentOptions := createOptionsWithDefault("ForwardAgent", []string{"", "yes", "no"})
Expand Down Expand Up @@ -1479,7 +1479,7 @@ func (sf *ServerForm) createAuthenticationForm() {
defaultValues := sf.getDefaultValues()

// Most common: Public key authentication
form.AddTextView("\n[yellow]Public Key Authentication[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Public Key Authentication[-]", "", 0, 1, true, false)

// PubkeyAuthentication dropdown
pubkeyOptions := createOptionsWithDefault("PubkeyAuthentication", []string{"", "yes", "no"})
Expand All @@ -1492,7 +1492,7 @@ func (sf *ServerForm) createAuthenticationForm() {
sf.addDropDownWithHelp(form, "IdentitiesOnly:", "IdentitiesOnly", identitiesOnlyOptions, identitiesOnlyIndex)

// SSH Agent settings
form.AddTextView("\n[yellow]SSH Agent[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]SSH Agent[-]", "", 0, 1, true, false)

// AddKeysToAgent dropdown
addKeysOptions := createOptionsWithDefault("AddKeysToAgent", []string{"", "yes", "no", "ask", "confirm"})
Expand All @@ -1502,7 +1502,7 @@ func (sf *ServerForm) createAuthenticationForm() {
sf.addInputFieldWithHelp(form, "IdentityAgent:", "IdentityAgent", defaultValues.IdentityAgent, 40, GetFieldPlaceholder("IdentityAgent"))

// Password/Interactive authentication
form.AddTextView("\n[yellow]Password & Interactive[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Password & Interactive[-]", "", 0, 1, true, false)

// PasswordAuthentication dropdown
passwordOptions := createOptionsWithDefault("PasswordAuthentication", []string{"", "yes", "no"})
Expand All @@ -1518,7 +1518,7 @@ func (sf *ServerForm) createAuthenticationForm() {
sf.addValidatedInputField(form, "NumberOfPasswordPrompts:", "NumberOfPasswordPrompts", defaultValues.NumberOfPasswordPrompts, 10, GetFieldPlaceholder("NumberOfPasswordPrompts"))

// Advanced: Authentication order preference
form.AddTextView("\n[yellow]Advanced[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Advanced[-]", "", 0, 1, true, false)

sf.addInputFieldWithHelp(form, "PreferredAuthentications:", "PreferredAuthentications", defaultValues.PreferredAuthentications, 40, GetFieldPlaceholder("PreferredAuthentications"))

Expand Down Expand Up @@ -1546,7 +1546,7 @@ func (sf *ServerForm) createAdvancedForm() {
form := tview.NewForm()
defaultValues := sf.getDefaultValues()

form.AddTextView("\n[yellow]Security[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Security[-]", "", 0, 1, true, false)

// StrictHostKeyChecking dropdown
strictHostKeyOptions := createOptionsWithDefault("StrictHostKeyChecking", []string{"", "yes", "no", "ask", "accept-new"})
Expand Down Expand Up @@ -1587,7 +1587,7 @@ func (sf *ServerForm) createAdvancedForm() {
knownHostsField := sf.addValidatedInputField(form, "UserKnownHostsFile:", "UserKnownHostsFile", defaultValues.UserKnownHostsFile, 40, GetFieldPlaceholder("UserKnownHostsFile"))
knownHostsField.SetAutocompleteFunc(sf.createKnownHostsAutocomplete())

form.AddTextView("\n[yellow]Cryptography[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Cryptography[-]", "", 0, 1, true, false)

// Ciphers with autocomplete support
ciphersField := sf.addInputFieldWithHelp(form, "Ciphers:", "Ciphers", defaultValues.Ciphers, 40, GetFieldPlaceholder("Ciphers"))
Expand All @@ -1605,7 +1605,7 @@ func (sf *ServerForm) createAdvancedForm() {
hostKeyField := sf.addInputFieldWithHelp(form, "HostKeyAlgorithms:", "HostKeyAlgorithms", defaultValues.HostKeyAlgorithms, 40, GetFieldPlaceholder("HostKeyAlgorithms"))
hostKeyField.SetAutocompleteFunc(sf.createAlgorithmAutocomplete(hostKeyAlgorithms))

form.AddTextView("\n[yellow]Command Execution[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Command Execution[-]", "", 0, 1, true, false)
sf.addInputFieldWithHelp(form, "LocalCommand:", "LocalCommand", defaultValues.LocalCommand, 40, GetFieldPlaceholder("LocalCommand"))

// PermitLocalCommand dropdown
Expand All @@ -1616,11 +1616,11 @@ func (sf *ServerForm) createAdvancedForm() {
// EscapeChar input field
sf.addValidatedInputField(form, "EscapeChar:", "EscapeChar", defaultValues.EscapeChar, 10, GetFieldPlaceholder("EscapeChar"))

form.AddTextView("\n[yellow]Environment[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Environment[-]", "", 0, 1, true, false)
sf.addInputFieldWithHelp(form, "SendEnv:", "SendEnv", defaultValues.SendEnv, 40, GetFieldPlaceholder("SendEnv"))
sf.addInputFieldWithHelp(form, "SetEnv:", "SetEnv", defaultValues.SetEnv, 40, GetFieldPlaceholder("SetEnv"))

form.AddTextView("\n[yellow]Debugging[-]", "", 0, 1, true, false)
form.AddTextView("\n[yellow]Debugging[-]", "", 0, 1, true, false)

// LogLevel dropdown
logLevelOptions := createOptionsWithDefault("LogLevel", []string{"", "QUIET", "FATAL", "ERROR", "INFO", "VERBOSE", "DEBUG", "DEBUG1", "DEBUG2", "DEBUG3"})
Expand Down Expand Up @@ -1948,7 +1948,7 @@ func (sf *ServerForm) handleCancel() {
if sf.app != nil {
modal := tview.NewModal().
SetText("You have unsaved changes. Are you sure you want to exit?").
AddButtons([]string{"[yellow]S[-]ave", "[yellow]D[-]iscard", "[yellow]C[-]ancel"}).
AddButtons([]string{"[red]S[-]ave", "[red]D[-]iscard", "[red]C[-]ancel"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
switch buttonIndex {
case 0: // Save
Expand Down
4 changes: 2 additions & 2 deletions internal/adapters/ui/status_bar.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import (
)

func DefaultStatusText() string {
return "[white]↑↓[-] Navigate • [white]Enter[-] SSH • [white]c[-] Copy SSH • [white]a[-] Add • [white]e[-] Edit • [white]g[-] Ping • [white]d[-] Delete • [white]p[-] Pin/Unpin • [white]/[-] Search • [white]q[-] Quit"
return " [red]↑↓[-] Navigate [red]Enter[-] SSH [red]c[-] Copy [red]a[-] Add [red]e[-] Edit [red]g[-] Ping [red]d[-] Delete [red]p[-] Pin/Unpin [red]/[-] Search [red]q[-] Quit"
}

func NewStatusBar() *tview.TextView {
status := tview.NewTextView().SetDynamicColors(true)
status.SetBackgroundColor(tcell.Color235)
status.SetTextAlign(tview.AlignCenter)
status.SetTextAlign(tview.AlignLeft)
status.SetText(DefaultStatusText())
return status
}
20 changes: 10 additions & 10 deletions internal/adapters/ui/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,51 +73,51 @@ func cellPad(s string, width int) string {
func pinnedIcon(pinnedAt time.Time) string {
// Use emojis for a nicer UI; combined with cellPad to keep widths consistent in tview.
if pinnedAt.IsZero() {
return "📡" // not pinned
return "" // not pinned
}
return "📌" // pinned
return "" // pinned
}

func formatServerLine(s domain.Server) (primary, secondary string) {
icon := cellPad(pinnedIcon(s.PinnedAt), 2)
// Use a consistent color for alias; the icon reflects pinning
primary = fmt.Sprintf("%s [white::b]%-12s[-] [#AAAAAA]%-18s[-] [#888888]Last SSH: %s[-] %s", icon, s.Alias, s.Host, humanizeDuration(s.LastSeen), renderTagBadgesForList(s.Tags))
primary = fmt.Sprintf("[red] %s[-][white]%s[-] [green]%s[-] [yellow]%s[-] %s", icon, s.Alias, s.Host, humanizeDuration(s.LastSeen), renderTagBadgesForList(s.Tags))
secondary = ""
return
}

func humanizeDuration(t time.Time) string {
if t.IsZero() {
return "never"
return ""
}
d := time.Since(t)
if d < time.Minute {
return "just now"
return "(just now)"
}
if d < time.Hour {
m := int(d.Minutes())
return fmt.Sprintf("%dm ago", m)
return fmt.Sprintf("(%dm ago)", m)
}
if d < 48*time.Hour {
h := int(d.Hours())
return fmt.Sprintf("%dh ago", h)
return fmt.Sprintf("(%dh ago)", h)
}
if d < 60*24*time.Hour {
days := int(d.Hours()) / 24
return fmt.Sprintf("%dd ago", days)
return fmt.Sprintf("(%dd ago)", days)
}
if d < 365*24*time.Hour {
months := int(d.Hours()) / (24 * 30)
if months < 1 {
months = 1
}
return fmt.Sprintf("%dmo ago", months)
return fmt.Sprintf("(%dmo ago)", months)
}
years := int(d.Hours()) / (24 * 365)
if years < 1 {
years = 1
}
return fmt.Sprintf("%dy ago", years)
return fmt.Sprintf("(%dy ago)", years)
}

// BuildSSHCommand constructs a ready-to-run ssh command for the given server.
Expand Down