Cybersecurity-Projects/PROJECTS/intermediate/security-news-scraper/internal/tui/view.go

160 lines
3.8 KiB
Go

// ©AngelaMos | 2026
// view.go
package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/lipgloss"
)
const (
brandName = "NADEZHDA"
tagline = "THREAT.WIRE"
markOpen = "◤ "
markClose = " ◢"
markMid = "═◆═"
errWrapMax = 80
errWrapMin = 20
errWrapMargin = 6
minRenderWidth = 44
minRenderHeight = 10
)
func (m Model) View() string {
if m.width < minRenderWidth || m.height < minRenderHeight {
return m.tooSmallView()
}
switch m.state {
case stateLoading:
return m.loadingView()
case stateError:
return m.errorView()
case stateDetail:
return m.detailView()
default:
return m.listView()
}
}
func (m Model) tooSmallView() string {
w, h := m.width, m.height
if w < 1 {
w = 1
}
if h < 1 {
h = 1
}
msg := m.theme.Muted.Render(fmt.Sprintf("terminal too small — need at least %dx%d", minRenderWidth, minRenderHeight))
return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, msg)
}
func (m Model) wordmark() string {
t := m.theme
return t.BrandMark.Render(markOpen) + t.Brand.Render(brandName) + t.BrandMark.Render(markClose) +
" " + t.fg(colorCyan).Render(markMid) + " " + t.fg(colorViolet).Render(tagline) + " " + t.fg(colorCyan).Render(markMid)
}
func (m Model) spread(left, right string) string {
gap := m.width - lipgloss.Width(left) - lipgloss.Width(right)
if gap < 1 {
gap = 1
}
return left + strings.Repeat(" ", gap) + right
}
func (m Model) keyHints(bindings ...key.Binding) string {
t := m.theme
parts := make([]string, 0, len(bindings))
for _, b := range bindings {
h := b.Help()
parts = append(parts, t.KeyGlyph.Render(h.Key)+" "+t.KeyDesc.Render(h.Desc))
}
return strings.Join(parts, t.Dim.Render(" · "))
}
func (m Model) severityLegend() string {
t := m.theme
item := func(color, label string) string {
return t.fg(color).Render("■") + " " + t.KeyDesc.Render(label)
}
return strings.Join([]string{
item(colorMagenta, "KEV/CRIT"),
item(colorAmber, "HIGH"),
item(colorYellow, "MED"),
item(colorCyan, "LOW"),
}, " ")
}
func (m Model) loadingView() string {
t := m.theme
brand := t.BrandMark.Render(markOpen) + t.Brand.Render(brandName) + t.BrandMark.Render(markClose)
tag := t.fg(colorViolet).Render(tagline)
line := m.spinner.View() + " " + t.Meta.Render("assembling threat wire") + " " + m.spinner.View()
block := lipgloss.JoinVertical(lipgloss.Center, brand, tag, "", line)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, block)
}
func (m Model) errorView() string {
t := m.theme
title := t.fg(colorMagenta).Bold(true).Render("▚ WIRE FAULT ▞")
block := lipgloss.JoinVertical(lipgloss.Center,
title,
"",
m.errString(),
"",
t.KeyDesc.Render("press ")+t.KeyGlyph.Render("q")+t.KeyDesc.Render(" to quit"),
)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, block)
}
func (m Model) errString() string {
if m.err == nil {
return m.theme.Text.Render("unknown fault")
}
w := m.width - errWrapMargin
if w > errWrapMax {
w = errWrapMax
}
if w < errWrapMin {
w = errWrapMin
}
return m.theme.Text.Width(w).Align(lipgloss.Center).Render(m.err.Error())
}
func (m Model) detailView() string {
t := m.theme
scroll := fmt.Sprintf("%3.0f%%", m.viewport.ScrollPercent()*100)
left := t.PanelTitle.Render(markOpen+"DOSSIER"+markClose) + " " +
t.Muted.Render(fmt.Sprintf("story %d of %d", m.cursor+1, len(m.scored)))
head := m.spread(left, t.Meta.Render(scroll))
foot := m.spread(
m.keyHints(m.keys.Back, m.keys.Browser, m.keys.Down, m.keys.Up, m.keys.Quit),
m.statusText(),
)
return lipgloss.JoinVertical(lipgloss.Left,
head,
t.rule(m.width),
m.viewport.View(),
t.rule(m.width),
foot,
)
}
func (m Model) statusText() string {
if m.status == "" {
return ""
}
color := colorCyan
if m.statusErr {
color = colorMagenta
}
return m.theme.fg(color).Render(m.status)
}