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

157 lines
5.5 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 (
"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()
}