4a8b4a2bc4
- Add test/crypto/ (19 tests): AES-256-GCM encrypt/decrypt, PBKDF2, IsEncrypted, HashPassword - Add test/knownhosts/ (14 tests): TOFU verify, MITM detection, CRUD, persistence - Add test/storage/ (20 tests): KeyPair/Snippet CRUD, encryption, MergeStrategy - Add test/config/ (9 tests): config lifecycle, path getters - Add test/tui/ (10 tests): WrapFooter, ClampWidth, TruncateStr - Fix knownhosts deadlock: Add/Remove use saveInternal() - Export responsive.go functions for testing - Add docs/TEST_PLAN.md with full scenario documentation Coverage: crypto 0%→100%, knownhosts 0%→100%, storage 40%→90%, config 22%→80%, tui 40%→70%
126 lines
2.7 KiB
Go
126 lines
2.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// Responsive breakpoints
|
|
const (
|
|
widthCompact = 60 // mobile / Termux
|
|
widthMedium = 100 // tablet
|
|
)
|
|
|
|
// boxOverhead is the horizontal chars consumed by BorderStyle (border 2 + padding 4)
|
|
const boxOverhead = 6
|
|
|
|
// wrapFooter wraps a footer string into multiple lines that fit availW.
|
|
// Words are split on double-space separators (" ") and grouped greedily.
|
|
// Returns the wrapped string with "\n" line breaks.
|
|
func wrapFooter(text string, availW int) string {
|
|
if availW < 1 {
|
|
availW = 1
|
|
}
|
|
if lipgloss.Width(text) <= availW {
|
|
return text
|
|
}
|
|
|
|
words := strings.Split(text, " ")
|
|
var lines []string
|
|
var current strings.Builder
|
|
|
|
for _, word := range words {
|
|
word = strings.TrimSpace(word)
|
|
if word == "" {
|
|
continue
|
|
}
|
|
if current.Len() == 0 {
|
|
current.WriteString(word)
|
|
} else if current.Len()+2+lipgloss.Width(word) <= availW {
|
|
current.WriteString(" ")
|
|
current.WriteString(word)
|
|
} else {
|
|
lines = append(lines, current.String())
|
|
current.Reset()
|
|
current.WriteString(word)
|
|
}
|
|
}
|
|
if current.Len() > 0 {
|
|
lines = append(lines, current.String())
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
// adaptiveSidePad returns horizontal padding based on terminal width.
|
|
// wide: 6, medium: 3, compact: 1
|
|
func adaptiveSidePad(termWidth int) int {
|
|
switch {
|
|
case termWidth < widthCompact:
|
|
return 1
|
|
case termWidth < widthMedium:
|
|
return 3
|
|
default:
|
|
return 6
|
|
}
|
|
}
|
|
|
|
// clampWidth clamps a target box width to fit within the terminal.
|
|
// Reserves boxOverhead for border+padding. Enforces a minimum of 20.
|
|
func clampWidth(target, termWidth int) int {
|
|
maxW := termWidth - boxOverhead
|
|
if maxW < 20 {
|
|
maxW = 20
|
|
}
|
|
if target > maxW {
|
|
return maxW
|
|
}
|
|
if target < 20 {
|
|
return 20
|
|
}
|
|
return target
|
|
}
|
|
|
|
// truncateStr truncates a string to maxLen with an ellipsis character.
|
|
func truncateStr(s string, maxLen int) string {
|
|
if maxLen < 1 {
|
|
return ""
|
|
}
|
|
if lipgloss.Width(s) <= maxLen {
|
|
return s
|
|
}
|
|
if maxLen <= 1 {
|
|
return "…"
|
|
}
|
|
runes := []rune(s)
|
|
var result []rune
|
|
resultW := 0
|
|
for _, r := range runes {
|
|
rw := lipgloss.Width(string(r))
|
|
if resultW+rw > maxLen-1 {
|
|
break
|
|
}
|
|
result = append(result, r)
|
|
resultW += rw
|
|
}
|
|
return string(result) + "…"
|
|
}
|
|
|
|
// Exported wrappers for testing
|
|
|
|
// WrapFooter wraps a footer string into multiple lines that fit availW
|
|
func WrapFooter(text string, availW int) string {
|
|
return wrapFooter(text, availW)
|
|
}
|
|
|
|
// ClampWidth clamps a target box width to fit within the terminal
|
|
func ClampWidth(target, termWidth int) int {
|
|
return clampWidth(target, termWidth)
|
|
}
|
|
|
|
// TruncateStr truncates a string to maxLen with an ellipsis character
|
|
func TruncateStr(s string, maxLen int) string {
|
|
return truncateStr(s, maxLen)
|
|
}
|