Files
lazyssh/internal/adapters/ui/utils.go
T

140 lines
3.8 KiB
Go

// Copyright 2025.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ui
import (
"fmt"
"strings"
"time"
"github.com/Adembc/lazyssh/internal/core/domain"
"github.com/mattn/go-runewidth"
)
// renderTagBadgesForList renders up to two colored tag chips for the server list.
// If there are more tags, it appends a subtle gray "+N" badge. Returns an empty
// string when there are no tags to avoid cluttering the list.
func renderTagBadgesForList(tags []string) string {
if len(tags) == 0 {
return ""
}
maxTags := 2
shown := tags
if len(tags) > maxTags {
shown = tags[:maxTags]
}
parts := make([]string, 0, len(shown)+1)
for _, t := range shown {
// Light blue background chip, similar to details view.
parts = append(parts, fmt.Sprintf("[black:#5FAFFF] %s [-:-:-]", t))
}
if extra := len(tags) - len(shown); extra > 0 {
parts = append(parts, fmt.Sprintf("[#8A8A8A]+%d[-]", extra))
}
return strings.Join(parts, " ")
}
// cellPad pads a string with spaces so its display width is at least `width` cells.
// This keeps emoji-based icons from breaking alignment in tview.
func cellPad(s string, width int) string {
w := runewidth.StringWidth(s)
if w >= width {
return s
}
return s + strings.Repeat(" ", width-w)
}
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 "📌" // 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))
secondary = ""
return
}
func humanizeDuration(t time.Time) string {
if t.IsZero() {
return "never"
}
d := time.Since(t)
if d < time.Minute {
return "just now"
}
if d < time.Hour {
m := int(d.Minutes())
return fmt.Sprintf("%dm ago", m)
}
if d < 48*time.Hour {
h := int(d.Hours())
return fmt.Sprintf("%dh ago", h)
}
if d < 60*24*time.Hour {
days := int(d.Hours()) / 24
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)
}
years := int(d.Hours()) / (24 * 365)
if years < 1 {
years = 1
}
return fmt.Sprintf("%dy ago", years)
}
// BuildSSHCommand constructs a ready-to-run ssh command for the given server.
// Format: ssh [user@]host [-p PORT if not 22] [-i KEY if provided]
func BuildSSHCommand(s domain.Server) string {
parts := []string{"ssh"}
userHost := ""
switch {
case s.User != "" && s.Host != "":
userHost = fmt.Sprintf("%s@%s", s.User, s.Host)
case s.Host != "":
userHost = s.Host
default:
userHost = s.Alias
}
parts = append(parts, userHost)
if s.Port != 0 && s.Port != 22 {
parts = append(parts, "-p", fmt.Sprintf("%d", s.Port))
}
if s.Key != "" {
parts = append(parts, "-i", quoteIfNeeded(s.Key))
}
return strings.Join(parts, " ")
}
// quoteIfNeeded returns the value quoted if it contains spaces.
func quoteIfNeeded(val string) string {
if strings.ContainsAny(val, " \t") {
return fmt.Sprintf("%q", val)
}
return val
}