- 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.
148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
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))
|
|
}
|