Padding spaces (end-of-line and blank filler lines) were unstyled, causing the terminal's default background to bleed through. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
4.3 KiB
Go
169 lines
4.3 KiB
Go
package banking
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
const termWidth = 80
|
|
|
|
// Color palette — green-on-black retro terminal.
|
|
var (
|
|
colorGreen = lipgloss.Color("#00FF00")
|
|
colorDim = lipgloss.Color("#007700")
|
|
colorBlack = lipgloss.Color("#000000")
|
|
colorBright = lipgloss.Color("#AAFFAA")
|
|
colorRed = lipgloss.Color("#FF3333")
|
|
)
|
|
|
|
// Reusable styles.
|
|
var (
|
|
baseStyle = lipgloss.NewStyle().
|
|
Foreground(colorGreen).
|
|
Background(colorBlack)
|
|
|
|
headerStyle = lipgloss.NewStyle().
|
|
Foreground(colorBright).
|
|
Background(colorBlack).
|
|
Bold(true).
|
|
Width(termWidth).
|
|
Align(lipgloss.Center)
|
|
|
|
titleStyle = lipgloss.NewStyle().
|
|
Foreground(colorGreen).
|
|
Background(colorBlack).
|
|
Bold(true)
|
|
|
|
dimStyle = lipgloss.NewStyle().
|
|
Foreground(colorDim).
|
|
Background(colorBlack)
|
|
|
|
errorStyle = lipgloss.NewStyle().
|
|
Foreground(colorRed).
|
|
Background(colorBlack).
|
|
Bold(true)
|
|
|
|
inputStyle = lipgloss.NewStyle().
|
|
Foreground(colorBright).
|
|
Background(colorBlack)
|
|
)
|
|
|
|
// divider returns an 80-column === line.
|
|
func divider() string {
|
|
return dimStyle.Render(strings.Repeat("=", termWidth))
|
|
}
|
|
|
|
// thinDivider returns an 80-column --- line.
|
|
func thinDivider() string {
|
|
return dimStyle.Render(strings.Repeat("-", termWidth))
|
|
}
|
|
|
|
// centerText centers text within 80 columns.
|
|
func centerText(s string) string {
|
|
return headerStyle.Render(s)
|
|
}
|
|
|
|
// padRight pads a string to the given width.
|
|
func padRight(s string, width int) string {
|
|
if len(s) >= width {
|
|
return s[:width]
|
|
}
|
|
return s + strings.Repeat(" ", width-len(s))
|
|
}
|
|
|
|
// formatCurrency formats cents as $X,XXX.XX
|
|
func formatCurrency(cents int64) string {
|
|
negative := cents < 0
|
|
if negative {
|
|
cents = -cents
|
|
}
|
|
dollars := cents / 100
|
|
remainder := cents % 100
|
|
|
|
// Add thousands separators.
|
|
ds := fmt.Sprintf("%d", dollars)
|
|
if len(ds) > 3 {
|
|
var parts []string
|
|
for len(ds) > 3 {
|
|
parts = append([]string{ds[len(ds)-3:]}, parts...)
|
|
ds = ds[:len(ds)-3]
|
|
}
|
|
parts = append([]string{ds}, parts...)
|
|
ds = strings.Join(parts, ",")
|
|
}
|
|
|
|
if negative {
|
|
return fmt.Sprintf("-$%s.%02d", ds, remainder)
|
|
}
|
|
return fmt.Sprintf("$%s.%02d", ds, remainder)
|
|
}
|
|
|
|
// padLine pads a single line (which may contain ANSI codes) to termWidth
|
|
// using its visual width. Padding uses a black background so the terminal's
|
|
// default background doesn't bleed through.
|
|
func padLine(line string) string {
|
|
w := lipgloss.Width(line)
|
|
if w >= termWidth {
|
|
return line
|
|
}
|
|
return line + baseStyle.Render(strings.Repeat(" ", termWidth-w))
|
|
}
|
|
|
|
// padLines pads every line in a multi-line string to termWidth so that
|
|
// shorter lines fully overwrite previous content in the terminal.
|
|
func padLines(s string) string {
|
|
lines := strings.Split(s, "\n")
|
|
for i, line := range lines {
|
|
lines[i] = padLine(line)
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
// screenFrame wraps content in the persistent header and footer.
|
|
// The height parameter is used to pad the output to fill the terminal,
|
|
// preventing leftover lines from previous renders bleeding through.
|
|
func screenFrame(bankName, terminalID, region, content string, height int) string {
|
|
var b strings.Builder
|
|
|
|
// Header (4 lines).
|
|
b.WriteString(divider())
|
|
b.WriteString("\n")
|
|
b.WriteString(centerText(bankName + " FEDERAL RESERVE SYSTEM"))
|
|
b.WriteString("\n")
|
|
b.WriteString(centerText("SECURE BANKING TERMINAL"))
|
|
b.WriteString("\n")
|
|
b.WriteString(divider())
|
|
b.WriteString("\n")
|
|
|
|
// Content.
|
|
b.WriteString(content)
|
|
|
|
// Pad with blank lines between content and footer so the footer
|
|
// stays at the bottom and the total output fills the terminal height.
|
|
if height > 0 {
|
|
const headerLines = 4
|
|
const footerLines = 2
|
|
// strings.Count gives newlines; add 1 for the line after the last \n.
|
|
contentLines := strings.Count(content, "\n") + 1
|
|
used := headerLines + contentLines + footerLines
|
|
blankLine := baseStyle.Render(strings.Repeat(" ", termWidth))
|
|
for i := used; i < height; i++ {
|
|
b.WriteString(blankLine)
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// Footer (2 lines).
|
|
b.WriteString("\n")
|
|
b.WriteString(divider())
|
|
b.WriteString("\n")
|
|
footer := fmt.Sprintf(" TERMINAL: %s | REGION: %s | ENCRYPTED SESSION ACTIVE", terminalID, region)
|
|
b.WriteString(dimStyle.Render(padRight(footer, termWidth)))
|
|
|
|
// Pad every line to full terminal width so shorter lines overwrite
|
|
// leftover content from previous renders.
|
|
return padLines(b.String())
|
|
}
|