package tui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/r3g1tpr0cs/burterm/internal/sequencer" ) // sequencerModel — вкладка Sequencer: textarea с токенами (по одному на // строку), запуск FIPS-анализа по ctrl+r и читаемый отчёт по каждому // проанализированному 20 000-битному блоку. type sequencerModel struct { input textarea.Model output viewport.Model focus int // 0 — вход, 1 — выход (скролл) errMsg string } func newSequencerView() sequencerModel { ta := textarea.New() ta.Placeholder = "Токены по одному на строку — либо hex-строки, либо сырой текст" ta.ShowLineNumbers = true ta.Focus() return sequencerModel{ input: ta, output: viewport.New(0, 0), } } func (m *sequencerModel) SetSize(width, height int) { if width < 0 { width = 0 } if height < 0 { height = 0 } inHeight := height / 2 m.input.SetWidth(width) m.input.SetHeight(inHeight) m.output.Width = width // 6 фиксированных строк вокруг output-viewport (построчный подсчёт по // View(): "Токены"-метка 1, разделитель 1, "Отчёт FIPS"-метка 1, // разделитель+футер(две строки) 3 — итого 6). Раньше стояло -4 — не // совпадало с View() ещё до того, как футер стал двухстрочным. m.output.Height = height - inHeight - 6 } func (m sequencerModel) Update(msg tea.Msg) (sequencerModel, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "tab": m.focus = (m.focus + 1) % 2 if m.focus == 0 { m.input.Focus() } else { m.input.Blur() } return m, nil case "ctrl+r": m.analyze() return m, nil } } var cmd tea.Cmd if m.focus == 0 { m.input, cmd = m.input.Update(msg) } else { m.output, cmd = m.output.Update(msg) } return m, cmd } func (m *sequencerModel) analyze() { var samples [][]byte for _, line := range strings.Split(m.input.Value(), "\n") { line = strings.TrimSpace(line) if line == "" { continue } samples = append(samples, sequencer.ParseSample(line)) } if len(samples) == 0 { m.errMsg = "нет сэмплов — вставь токены, по одному на строку" return } results, blocks, bitsAvailable, err := sequencer.AnalyzeConcatenated(samples) if err != nil { m.errMsg = err.Error() m.output.SetContent(fmt.Sprintf("Сэмплов: %d, доступно бит: %d — недостаточно для анализа.", len(samples), bitsAvailable)) return } m.errMsg = "" m.output.SetContent(renderSequencerReport(samples, results, blocks, bitsAvailable)) } func renderSequencerReport(samples [][]byte, results []sequencer.BlockResult, blocks, bitsAvailable int) string { var b strings.Builder b.WriteString(fmt.Sprintf("Сэмплов: %d · доступно бит: %d · проанализировано блоков по 20000 бит: %d\n\n", len(samples), bitsAvailable, blocks)) passCount := 0 for _, r := range results { if r.AllPass() { passCount++ } } b.WriteString(fmt.Sprintf("Итог: %d/%d блоков прошли все четыре теста\n\n", passCount, blocks)) for i, r := range results { b.WriteString(fmt.Sprintf("Блок %d:\n", i+1)) b.WriteString(fmt.Sprintf(" Monobit: единиц=%d %s\n", r.MonobitOnes, passLabel(r.MonobitPass))) b.WriteString(fmt.Sprintf(" Poker: X=%.2f %s\n", r.PokerX, passLabel(r.PokerPass))) b.WriteString(fmt.Sprintf(" Runs: %s\n", passLabel(r.RunsPass))) b.WriteString(fmt.Sprintf(" Long run: макс. серия=%d бит %s\n", r.LongRunMax, passLabel(r.LongRunPass))) b.WriteString("\n") } return b.String() } func passLabel(pass bool) string { if pass { return "PASS" } return "FAIL" } func (m sequencerModel) View() string { focusMark := func(i int) string { if m.focus == i { return "▸ " } return " " } var b strings.Builder b.WriteString(focusMark(0) + "Токены (по одному на строку)\n") b.WriteString(m.input.View()) b.WriteString("\n\n") if m.errMsg != "" { b.WriteString("Ошибка: " + m.errMsg + "\n\n") } b.WriteString(focusMark(1) + "Отчёт FIPS 140-2\n") b.WriteString(m.output.View()) b.WriteString("\n\ntab — между полями · ctrl+r — запустить анализ\n") b.WriteString("f1-f12, pgup — вкладки\n") return b.String() }