burterm/internal/tui/theme.go
2026-09-14 10:55:07 +03:00

162 lines
7.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// Палитра для структурного UI (рамки, заголовки панелей, таб-бар) —
// намеренно отдельная от statusStyle/severityStyle в style.go (те красят
// ДАННЫЕ — статус-коды, находки; эти красят КАРКАС интерфейса). Числа —
// код цвета из 256-цветной ANSI-палитры, не hex/truecolor: она надёжнее
// деградирует на терминалах со скромной поддержкой цвета, а truecolor
// имело бы смысл включать только при уверенности, что терминал его
// действительно поддерживает — здесь такой уверенности нет.
var (
colorBorder = lipgloss.Color("62") // приглушённый синий/фиолетовый — рамки панелей
colorPanelTitle = lipgloss.Color("213") // розовый — заголовки панелей
colorAccent = lipgloss.Color("86") // циан — активная вкладка, акценты
colorMuted = lipgloss.Color("245") // серый — неактивные вкладки, второстепенный текст
colorLogo = lipgloss.Color("86")
)
var (
panelBorderStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(colorBorder).
Padding(0, 1)
panelTitleStyle = lipgloss.NewStyle().
Foreground(colorPanelTitle).
Bold(true)
tabActiveStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("0")).
Background(colorAccent).
Padding(0, 1)
tabInactiveStyle = lipgloss.NewStyle().
Foreground(colorMuted).
Padding(0, 1)
logoStyle = lipgloss.NewStyle().
Foreground(colorLogo).
Bold(true)
)
// panel оборачивает body в рамку с заголовком первой строкой внутри неё.
// Это не буквально врезанный в саму границу заголовок (как "┌─ TITLE ─┐"
// на референсном скриншоте) — честно говоря, аккуратно вписать текст
// прямо в линию рамки с учётом произвольной ширины панели без реального
// терминала для проверки выравнивания я делать не рискнул: разъедется на
// один символ — и всё выглядит сломанным. Заголовок жирным текстом
// первой строкой внутри рамки — тот же смысловой эффект ("вот подпись
// этой панели"), но без риска съехавшей границы.
func panel(title, body string, width int) string {
if width < 10 {
width = 10
}
content := panelTitleStyle.Render(strings.ToUpper(title)) + "\n" + body
return panelBorderStyle.Width(width).Render(content)
}
// tabDef — одна вкладка таб-бара: горячая клавиша, подпись, идентификатор.
type tabDef struct {
key string
label string
view view
}
var allTabs = []tabDef{
{"f1", "Dash", viewDashboard},
{"f2", "Req", viewRequestBuilder},
{"f3", "Res", viewResults},
{"f4", "Proxy", viewProxy},
{"f5", "Rep", viewRepeater},
{"f6", "Spider", viewSpider},
{"f7", "Target", viewTarget},
{"f8", "Decoder", viewDecoder},
{"f9", "Comparer", viewComparer},
{"f10", "Sequencer", viewSequencer},
{"f11", "Scanner", viewScanner},
{"f12", "Project", viewProject},
{"PgUp", "Fuzzer", viewFuzzer},
}
// renderTabs строит таб-бар в два ряда по семь вкладок — один длинный
// ряд из 14 вкладок надёжно убегает за границу экрана почти на любом
// терминале (сам от этого пострадал на скриншоте: последняя видимая
// вкладка обрезалась серединой слова). Два ряда фиксированной длины —
// не идеальное решение (на очень широком терминале часть строки
// простаивает), но предсказуемое и не требует динамического измерения
// реальной ширины текста с учётом ANSI-кодов, с которым я бы не смог
// свериться без реального терминала под рукой.
//
// Активная вкладка — заливка акцентным цветом, остальные — приглушённый
// текст. reqModified/repModified добавляют "*" к подписи Req/Rep — та
// же индикация несохранённых правок, что была и в чисто текстовой
// версии.
func renderTabs(active view, reqModified, repModified bool) string {
render := func(t tabDef) string {
label := t.label
switch t.view {
case viewRequestBuilder:
if reqModified {
label += "*"
}
case viewRepeater:
if repModified {
label += "*"
}
}
text := fmt.Sprintf("%s:%s", t.key, label)
if t.view == active {
return tabActiveStyle.Render(text)
}
return tabInactiveStyle.Render(text)
}
mid := (len(allTabs) + 1) / 2
row1 := make([]string, 0, mid)
for _, t := range allTabs[:mid] {
row1 = append(row1, render(t))
}
row2 := make([]string, 0, len(allTabs)-mid)
for _, t := range allTabs[mid:] {
row2 = append(row2, render(t))
}
return lipgloss.JoinVertical(lipgloss.Left,
lipgloss.JoinHorizontal(lipgloss.Top, row1...),
lipgloss.JoinHorizontal(lipgloss.Top, row2...),
)
}
// tabTitle возвращает короткую подпись вкладки по её view — используется
// для заголовка внешней рамки-панели, в которую оборачивается тело
// каждой вкладки (см. Model.View в model.go).
func tabTitle(v view) string {
for _, t := range allTabs {
if t.view == v {
return t.label
}
}
return ""
}
// renderLogo — простой стилизованный баннер вместо блочного ASCII-арта.
// Причина не пытаться повторить "PENTOOL" с референса буква-в-букву:
// блочные ASCII-буквы требуют точной посимвольной геометрии (высота
// каждой буквы, зазоры), и без реального терминала под рукой я не могу
// проверить, не съехало ли что-то на один пробел — а кривой ASCII-арт
// выглядит хуже, чем его отсутствие. Разрядка через пробел + акцентный
// цвет + горизонтальная линия — тот же "заставочный" эффект с кратно
// меньшим риском.
func renderLogo() string {
spaced := strings.Join(strings.Split("BURTERM", ""), " ")
line := strings.Repeat("─", len(spaced))
return logoStyle.Render(spaced) + "\n" + panelTitleStyle.Render(line)
}