From 4543e7dacc3af4168cd5a86c6e8f5dc9d311fcd8 Mon Sep 17 00:00:00 2001 From: Dmytro Pavlov Date: Mon, 22 Sep 2025 18:40:10 +0300 Subject: [PATCH] improve(ui):improvements to formatting, colors, spacings, shortcut hints, readability --- internal/adapters/ui/header.go | 4 +-- internal/adapters/ui/hint_bar.go | 2 +- internal/adapters/ui/search_bar.go | 2 +- internal/adapters/ui/server_details.go | 8 ++--- internal/adapters/ui/server_form.go | 44 +++++++++++++------------- internal/adapters/ui/status_bar.go | 4 +-- internal/adapters/ui/utils.go | 20 ++++++------ 7 files changed, 41 insertions(+), 43 deletions(-) diff --git a/internal/adapters/ui/header.go b/internal/adapters/ui/header.go index 45f7bd2..3f3bbfd 100644 --- a/internal/adapters/ui/header.go +++ b/internal/adapters/ui/header.go @@ -16,7 +16,6 @@ package ui import ( "strings" - "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -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 } diff --git a/internal/adapters/ui/hint_bar.go b/internal/adapters/ui/hint_bar.go index de94973..2164abd 100644 --- a/internal/adapters/ui/hint_bar.go +++ b/internal/adapters/ui/hint_bar.go @@ -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 } diff --git a/internal/adapters/ui/search_bar.go b/internal/adapters/ui/search_bar.go index 7b03c90..7a37114 100644 --- a/internal/adapters/ui/search_bar.go +++ b/internal/adapters/ui/search_bar.go @@ -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). diff --git a/internal/adapters/ui/server_details.go b/internal/adapters/ui/server_details.go index 8e0a634..871d504 100644 --- a/internal/adapters/ui/server_details.go +++ b/internal/adapters/ui/server_details.go @@ -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, ", ") @@ -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) @@ -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 { @@ -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) } diff --git a/internal/adapters/ui/server_form.go b/internal/adapters/ui/server_form.go index 286b47f..d60f3d8 100644 --- a/internal/adapters/ui/server_form.go +++ b/internal/adapters/ui/server_form.go @@ -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). @@ -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)) } @@ -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 @@ -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")) @@ -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")) @@ -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 @@ -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"}) @@ -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")) @@ -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) @@ -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")) @@ -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"}) @@ -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"}) @@ -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"}) @@ -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"}) @@ -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")) @@ -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"}) @@ -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")) @@ -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 @@ -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"}) @@ -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 diff --git a/internal/adapters/ui/status_bar.go b/internal/adapters/ui/status_bar.go index d8ca0aa..3cd31e3 100644 --- a/internal/adapters/ui/status_bar.go +++ b/internal/adapters/ui/status_bar.go @@ -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 } diff --git a/internal/adapters/ui/utils.go b/internal/adapters/ui/utils.go index 95a3996..55ab95d 100644 --- a/internal/adapters/ui/utils.go +++ b/internal/adapters/ui/utils.go @@ -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.