feat: add Banking TUI shell using bubbletea
Add an 80s-style green-on-black bank terminal shell ("banking") using
charmbracelet/bubbletea for full-screen TUI rendering over SSH.
Screens: login, main menu, account summary, account detail with
transactions, wire transfer wizard (6-step form capturing routing
number, destination, beneficiary, amount, memo, auth code), transaction
history with pagination, secure messages with breadcrumb content (fake
internal IPs, vault codes), change PIN, and hidden admin access (99)
that locks after 3 failed attempts with COBOL-style error output.
All key actions (login, navigation, wire transfers, admin attempts) are
logged to the session store. Wire transfer data is the honeypot gold.
Configurable via [shell.banking] in TOML: bank_name, terminal_id, region.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
122
internal/shell/banking/screen_messages.go
Normal file
122
internal/shell/banking/screen_messages.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package banking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type messagesModel struct {
|
||||
messages []SecureMessage
|
||||
viewing int // -1 = list, >= 0 = detail
|
||||
choice string
|
||||
}
|
||||
|
||||
func newMessagesModel(messages []SecureMessage) messagesModel {
|
||||
return messagesModel{messages: messages, viewing: -1}
|
||||
}
|
||||
|
||||
func (m messagesModel) Update(msg tea.Msg) (messagesModel, tea.Cmd) {
|
||||
keyMsg, ok := msg.(tea.KeyMsg)
|
||||
if !ok {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.viewing >= 0 {
|
||||
// In detail view, any key goes back to list.
|
||||
m.viewing = -1
|
||||
m.choice = ""
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch keyMsg.Type {
|
||||
case tea.KeyEnter:
|
||||
if m.choice == "0" {
|
||||
m.choice = "back"
|
||||
return m, nil
|
||||
}
|
||||
for i := range m.messages {
|
||||
if m.choice == fmt.Sprintf("%d", i+1) {
|
||||
m.viewing = i
|
||||
m.messages[i].Unread = false
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
m.choice = ""
|
||||
case tea.KeyBackspace:
|
||||
if len(m.choice) > 0 {
|
||||
m.choice = m.choice[:len(m.choice)-1]
|
||||
}
|
||||
default:
|
||||
ch := keyMsg.String()
|
||||
if len(ch) == 1 && ch[0] >= '0' && ch[0] <= '9' && len(m.choice) < 2 {
|
||||
m.choice += ch
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m messagesModel) View() string {
|
||||
if m.viewing >= 0 {
|
||||
return m.viewDetail()
|
||||
}
|
||||
return m.viewList()
|
||||
}
|
||||
|
||||
func (m messagesModel) viewList() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(centerText("SECURE MESSAGES"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(thinDivider())
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(titleStyle.Render(fmt.Sprintf(" %-4s %-3s %-12s %-22s %s", "#", "", "DATE", "FROM", "SUBJECT")))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(dimStyle.Render(" " + strings.Repeat("-", 68)))
|
||||
b.WriteString("\n")
|
||||
|
||||
for i, msg := range m.messages {
|
||||
marker := " "
|
||||
if msg.Unread {
|
||||
marker = " * "
|
||||
}
|
||||
b.WriteString(baseStyle.Render(fmt.Sprintf(" [%d]%s%-12s %-22s %s",
|
||||
i+1, marker, msg.Date, msg.From, msg.Subj)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(baseStyle.Render(" [0] RETURN TO MAIN MENU"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(thinDivider())
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(titleStyle.Render(" SELECT MESSAGE: "))
|
||||
b.WriteString(inputStyle.Render(m.choice))
|
||||
b.WriteString(inputStyle.Render("_"))
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m messagesModel) viewDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
msg := m.messages[m.viewing]
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(centerText(fmt.Sprintf("MESSAGE #%d", m.viewing+1)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(thinDivider())
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(baseStyle.Render(msg.Body))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(thinDivider())
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" PRESS ANY KEY TO RETURN TO MESSAGE LIST"))
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user