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) }