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