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,147 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// sizeClass buckets the terminal width into the breakpoints every screen's
|
||||
// view function reads to decide how much chrome it can afford.
|
||||
type sizeClass int
|
||||
|
||||
const (
|
||||
sizeTiny sizeClass = iota // < tinyWidth columns: single column, minimal chrome
|
||||
sizeCompact // < wideWidth columns: single column, full chrome
|
||||
sizeWide // >= wideWidth columns: room for a side panel
|
||||
)
|
||||
|
||||
const (
|
||||
minWidth, minHeight = 44, 12 // below this, render the "too small" notice instead
|
||||
tinyWidth = 60
|
||||
wideWidth = 100
|
||||
|
||||
// Panel border(1)+padding(0,1) on each side: 2 cols border, 2 cols
|
||||
// padding, 2 rows border, 0 rows padding. Every screen that renders
|
||||
// inside styles.Panel/PanelActive/SidePanel must size its content to
|
||||
// panelInner of the outer box it's about to be wrapped in.
|
||||
panelBorderW, panelBorderH = 2, 2
|
||||
panelPadW, panelPadH = 2, 0
|
||||
)
|
||||
|
||||
// layout is the single source of truth for how the terminal's cells are
|
||||
// divided, recomputed once per tea.WindowSizeMsg and read by every view
|
||||
// function instead of each screen re-deriving its own magic numbers.
|
||||
type layout struct {
|
||||
w, h int
|
||||
class sizeClass
|
||||
tooSmall bool
|
||||
|
||||
headerH, footerH int
|
||||
bodyW, bodyH int // full-width body area below the header, above the footer
|
||||
|
||||
// Two-column split of bodyW, valid only when class == sizeWide and a
|
||||
// screen opts into it (mainW+sideW+1 == bodyW; the extra column is a
|
||||
// one-cell gutter between the two panels).
|
||||
mainW, sideW int
|
||||
}
|
||||
|
||||
func computeLayout(w, h int) layout {
|
||||
l := layout{w: w, h: h}
|
||||
if w < minWidth || h < minHeight {
|
||||
l.tooSmall = true
|
||||
return l
|
||||
}
|
||||
|
||||
switch {
|
||||
case w < tinyWidth:
|
||||
l.class = sizeTiny
|
||||
case w < wideWidth:
|
||||
l.class = sizeCompact
|
||||
default:
|
||||
l.class = sizeWide
|
||||
}
|
||||
|
||||
l.headerH = 1
|
||||
l.footerH = 2
|
||||
if l.class == sizeTiny {
|
||||
l.footerH = 1
|
||||
}
|
||||
l.bodyH = h - l.headerH - l.footerH
|
||||
if l.bodyH < 3 {
|
||||
l.bodyH = 3
|
||||
}
|
||||
l.bodyW = w
|
||||
|
||||
if l.class == sizeWide {
|
||||
l.sideW = l.bodyW * 2 / 5
|
||||
if l.sideW > 48 {
|
||||
l.sideW = 48
|
||||
}
|
||||
l.mainW = l.bodyW - l.sideW - 1
|
||||
} else {
|
||||
l.mainW = l.bodyW
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// panelInner returns the content area available inside a bordered,
|
||||
// (0,1)-padded panel of the given outer size — what a screen must wrap its
|
||||
// text to before handing it to styles.Panel/PanelActive/SidePanel.Render.
|
||||
func panelInner(outerW, outerH int) (w, h int) {
|
||||
w = outerW - panelBorderW - panelPadW
|
||||
h = outerH - panelBorderH - panelPadH
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// fit pads/truncates s to exactly h lines of at most w cells each, so a
|
||||
// panel's rendered size never depends on its content — no reflow, no
|
||||
// leftover scrollback in the alt-screen buffer.
|
||||
func fit(s string, w, h int) string {
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
lines := strings.Split(s, "\n")
|
||||
out := make([]string, h)
|
||||
for i := 0; i < h; i++ {
|
||||
if i >= len(lines) {
|
||||
out[i] = ""
|
||||
continue
|
||||
}
|
||||
line := lines[i]
|
||||
lw := lipgloss.Width(line)
|
||||
switch {
|
||||
case lw > w:
|
||||
out[i] = lipgloss.NewStyle().MaxWidth(w).Render(line)
|
||||
case lw < w:
|
||||
out[i] = line + strings.Repeat(" ", w-lw)
|
||||
default:
|
||||
out[i] = line
|
||||
}
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
// renderPanel wraps content (already fit to panelInner(outerW, outerH)) in
|
||||
// style, sized so the rendered result is exactly outerW x outerH.
|
||||
//
|
||||
// Deliberately does NOT call style.Width()/Height(): lipgloss.Style.Render
|
||||
// treats a set Width as a word-wrap target of width-minus-border-minus-
|
||||
// padding and re-wraps anything wider, which corrupts content we've
|
||||
// already sized to the exact cell grid ourselves via fit(). Handing it
|
||||
// pre-fit, uniform-width content and leaving Width/Height unset gets the
|
||||
// same result (border+padding add their fixed, known cost on top) without
|
||||
// that reprocessing.
|
||||
func renderPanel(style lipgloss.Style, outerW, outerH int, content string) string {
|
||||
iw, ih := panelInner(outerW, outerH)
|
||||
return style.Render(fit(content, iw, ih))
|
||||
}
|
||||
Reference in New Issue
Block a user