package tui import ( "strings" "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/r3g1tpr0cs/burterm/internal/comparer" ) // comparerModel — вкладка Comparer: два независимых поля ввода (A и B) // и вычисляемый по запросу токенный diff между ними. Diff не // пересчитывается на каждое нажатие клавиши (это было бы дорого и // мельтешило бы перед глазами при печати) — только по ctrl+d. type comparerModel struct { left textarea.Model right textarea.Model diff viewport.Model focus int // 0 — left, 1 — right, 2 — diff (скролл) } func newComparerView() comparerModel { left := textarea.New() left.Placeholder = "Текст A" left.Focus() right := textarea.New() right.Placeholder = "Текст B" return comparerModel{ left: left, right: right, diff: viewport.New(0, 0), } } func (m *comparerModel) SetSize(width, height int) { if width < 0 { width = 0 } if height < 0 { height = 0 } inputHeight := height / 4 halfWidth := width / 2 m.left.SetWidth(halfWidth) m.left.SetHeight(inputHeight) m.right.SetWidth(width - halfWidth) m.right.SetHeight(inputHeight) m.diff.Width = width // Второй провал точного построчного подсчёта подряд на этой вкладке // (сначала -5, потом -6, оба раза всё равно вылезало за экран) — // значит моё предположение о том, что textarea.View() рендерит ровно // столько строк, сколько задано через SetHeight, скорее всего неверно // (возможно, у неё есть свой скрытый оверхед — рамка/строка счётчика // символов/что-то ещё, чего я не вижу в исходниках bubbles без // реального терминала под рукой). Вместо третьей попытки угадать // точное число — солидный запас (-12 вместо расчётных 6), плюс // inputHeight теперь height/4, а не height/3, чтобы оставить больше // свободного места на непредвиденный оверхед. Цена: diff-viewport // будет заметно ниже, чем мог бы быть — гарантированная видимость // меню важнее. m.diff.Height = height - inputHeight - 12 if m.diff.Height < 3 { m.diff.Height = 3 } } func (m comparerModel) Update(msg tea.Msg) (comparerModel, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "tab": m.focus = (m.focus + 1) % 3 m.left.Blur() m.right.Blur() switch m.focus { case 0: m.left.Focus() case 1: m.right.Focus() } return m, nil case "ctrl+d": m.runDiff() return m, nil } } var cmd tea.Cmd switch m.focus { case 0: m.left, cmd = m.left.Update(msg) case 1: m.right, cmd = m.right.Update(msg) case 2: m.diff, cmd = m.diff.Update(msg) } return m, cmd } func (m *comparerModel) runDiff() { chunks := comparer.Diff(m.left.Value(), m.right.Value()) m.diff.SetContent(renderDiffChunks(chunks)) } // renderDiffChunks размечает удалённые куски как [-...-], добавленные // как [+...+], а совпадающие оставляет как есть — инлайновая разметка // вместо честных двух колонок, потому что ширина терминала на мобильном // экране редко позволяет разместить полноценный side-by-side diff. func renderDiffChunks(chunks []comparer.Chunk) string { var b strings.Builder for _, c := range chunks { switch c.Op { case comparer.OpEqual: b.WriteString(c.Text) case comparer.OpDelete: b.WriteString("[-" + c.Text + "-]") case comparer.OpInsert: b.WriteString("[+" + c.Text + "+]") } } return b.String() } // View рисует A и B рядом (side by side), а не друг под другом — именно // так и предполагает SetSize (делит ширину пополам через halfWidth). // Раньше здесь было "A\n[поле]\n\nB\n[поле]", то есть друг под другом — // это удваивало реальную высоту относительно того, что SetSize // закладывал в diff.Height, и Comparer стабильно вылезал за экран // независимо от общего бюджета высоты, который передаёт model.go. func (m comparerModel) View() string { focusMark := func(i int) string { if m.focus == i { return "▸ " } return " " } leftBlock := focusMark(0) + "A\n" + m.left.View() rightBlock := focusMark(1) + "B\n" + m.right.View() row := lipgloss.JoinHorizontal(lipgloss.Top, leftBlock, " ", rightBlock) var b strings.Builder b.WriteString(row) b.WriteString("\n\n") b.WriteString(focusMark(2) + "Diff ([-удалено-] / [+добавлено+])\n") b.WriteString(m.diff.View()) b.WriteString("\n\ntab — между полями · ctrl+d — сравнить\n") b.WriteString("f1-f12, pgup — вкладки\n") return b.String() }