feat(vault-tui): implement KV client and service for managing secrets
- Added internal/vault/client.go for creating a Vault client with configuration settings. - Introduced internal/vault/errors.go to classify Vault API errors for better UI handling. - Created internal/vault/kv.go to manage KV secrets, including listing, reading, writing, and deleting operations. - Implemented internal/vault/mounts.go to list and describe secret engine mounts. - Developed internal/vault/service.go to provide a unified entry point for Vault operations. - Added internal/vault/kv_test.go for comprehensive testing of KV operations. - Introduced internal/ui/toast.go for transient notifications in the UI. - Added renovate.json for dependency management and updates.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"git.morlana.online/f.weber/vault-tui/internal/vault"
|
||||
)
|
||||
|
||||
// Symbols used throughout the TUI. Deliberately plain, single-width
|
||||
// Unicode box-drawing/geometric shapes — no Nerd Font glyphs, no emoji —
|
||||
// so rendering never depends on a font the user's terminal might not have.
|
||||
const (
|
||||
symMount = "▣"
|
||||
symDir = "▸"
|
||||
symSecret = "▪"
|
||||
symUnsupported = "▨"
|
||||
symCursor = "┃"
|
||||
symCurrent = "●"
|
||||
symDeleted = "◐"
|
||||
symDestroyed = "✕"
|
||||
symOK = "✓"
|
||||
symWarn = "⚠"
|
||||
symErr = "✕"
|
||||
symLocked = "●"
|
||||
symUnlocked = "○"
|
||||
symChevronR = "▸"
|
||||
symEllipsis = "…"
|
||||
)
|
||||
|
||||
// truncate clips s to at most w cells wide, replacing the tail with an
|
||||
// ellipsis when it doesn't fit — unlike fit/MaxWidth, which hard-crop
|
||||
// without any indication that content was cut off.
|
||||
func truncate(s string, w int) string {
|
||||
if w <= 0 {
|
||||
return ""
|
||||
}
|
||||
if lipgloss.Width(s) <= w {
|
||||
return s
|
||||
}
|
||||
if w == 1 {
|
||||
return symEllipsis
|
||||
}
|
||||
runes := []rune(s)
|
||||
// Binary-search-free linear trim: widths are small (labels/paths), and
|
||||
// wide runes are rare enough that a byte-at-a-time trim is plenty fast.
|
||||
for i := len(runes); i > 0; i-- {
|
||||
cand := string(runes[:i]) + symEllipsis
|
||||
if lipgloss.Width(cand) <= w {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
return symEllipsis
|
||||
}
|
||||
|
||||
// padRight pads s with spaces to exactly w cells, or truncates it if it's
|
||||
// already wider — for building fixed-width table cells inline (as opposed
|
||||
// to fit, which pads/crops a whole multi-line block).
|
||||
func padRight(s string, w int) string {
|
||||
if w <= 0 {
|
||||
return ""
|
||||
}
|
||||
lw := lipgloss.Width(s)
|
||||
if lw > w {
|
||||
return truncate(s, w)
|
||||
}
|
||||
return s + strings.Repeat(" ", w-lw)
|
||||
}
|
||||
|
||||
// padLeft right-aligns s within w cells.
|
||||
func padLeft(s string, w int) string {
|
||||
if w <= 0 {
|
||||
return ""
|
||||
}
|
||||
lw := lipgloss.Width(s)
|
||||
if lw >= w {
|
||||
return truncate(s, w)
|
||||
}
|
||||
return strings.Repeat(" ", w-lw) + s
|
||||
}
|
||||
|
||||
// humanTime renders an RFC3339 timestamp (as returned by Vault's KV
|
||||
// metadata) as a short relative time, falling back to the raw string if it
|
||||
// doesn't parse.
|
||||
func humanTime(rfc3339 string) string {
|
||||
if rfc3339 == "" {
|
||||
return ""
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, rfc3339)
|
||||
if err != nil {
|
||||
return rfc3339
|
||||
}
|
||||
d := time.Since(t)
|
||||
switch {
|
||||
case d < 0:
|
||||
return t.Format("2006-01-02 15:04")
|
||||
case d < time.Minute:
|
||||
return "just now"
|
||||
case d < time.Hour:
|
||||
return fmt.Sprintf("%dm ago", int(d/time.Minute))
|
||||
case d < 24*time.Hour:
|
||||
return fmt.Sprintf("%dh ago", int(d/time.Hour))
|
||||
case d < 30*24*time.Hour:
|
||||
return fmt.Sprintf("%dd ago", int(d/(24*time.Hour)))
|
||||
default:
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
|
||||
// ttlString renders a duration the way a status pill wants it: compact,
|
||||
// never more than two units ("42m", "1h05m", "3d02h"), never negative.
|
||||
func ttlString(d time.Duration) string {
|
||||
if d <= 0 {
|
||||
return "expired"
|
||||
}
|
||||
d = d.Round(time.Second)
|
||||
days := d / (24 * time.Hour)
|
||||
d -= days * 24 * time.Hour
|
||||
hours := d / time.Hour
|
||||
d -= hours * time.Hour
|
||||
mins := d / time.Minute
|
||||
d -= mins * time.Minute
|
||||
secs := d / time.Second
|
||||
|
||||
switch {
|
||||
case days > 0:
|
||||
return fmt.Sprintf("%dd%02dh", days, hours)
|
||||
case hours > 0:
|
||||
return fmt.Sprintf("%dh%02dm", hours, mins)
|
||||
case mins > 0:
|
||||
return fmt.Sprintf("%dm%02ds", mins, secs)
|
||||
default:
|
||||
return fmt.Sprintf("%ds", secs)
|
||||
}
|
||||
}
|
||||
|
||||
// engineLabel is the short badge text for a mount's engine kind.
|
||||
func engineLabel(m vault.Mount) string {
|
||||
switch m.Kind {
|
||||
case vault.EngineKVv2:
|
||||
return "kv v2"
|
||||
case vault.EngineKVv1:
|
||||
return "kv v1"
|
||||
default:
|
||||
return m.Type
|
||||
}
|
||||
}
|
||||
|
||||
// maskValue replaces v with repeated mask characters, capped at a fixed
|
||||
// display width so long secrets don't stretch the layout.
|
||||
func maskValue(v, char string) string {
|
||||
if char == "" {
|
||||
char = "•"
|
||||
}
|
||||
n := lipgloss.Width(v)
|
||||
if n > 12 {
|
||||
n = 12
|
||||
}
|
||||
if n == 0 {
|
||||
n = 1
|
||||
}
|
||||
return strings.Repeat(char, n)
|
||||
}
|
||||
Reference in New Issue
Block a user