162 lines
7.3 KiB
Go
162 lines
7.3 KiB
Go
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)
|
||
}
|