This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/banking/style.go
Torjus Håkestad d226c32b9b fix: banking shell screen rendering artifacts and transfer panic
Fix rendering issues where content from previous screens bled through
when switching between views of different heights/widths:

- Pad every line to full terminal width (ANSI-aware) so shorter lines
  overwrite leftover content from previous renders
- Track terminal height via WindowSizeMsg and pad between content and
  footer to fill the screen
- Send tea.ClearScreen on all screen transitions for height changes
- Fix panic in transfer completion when routing number is < 4 chars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 00:50:34 +01:00

168 lines
4.2 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.
func padLine(line string) string {
w := lipgloss.Width(line)
if w >= termWidth {
return line
}
return line + 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 := 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())
}