191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
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/decoder"
|
||
)
|
||
|
||
// decoderModel — вкладка Decoder: вход + выход, набор кодировок за
|
||
// ctrl-хоткеями (работают независимо от того, в каком поле курсор —
|
||
// как и остальные ctrl-комбинации в приложении, они не печатаются как
|
||
// текст, поэтому их можно ловить прямо поверх textarea.Update) и тумблер
|
||
// направления encode/decode.
|
||
type decoderModel struct {
|
||
input textarea.Model
|
||
output viewport.Model
|
||
|
||
encode bool // true — кодировать, false — декодировать (для MD5 неприменимо)
|
||
lastOp string
|
||
errMsg string
|
||
|
||
focus int // 0 — вход, 1 — выход (скролл)
|
||
}
|
||
|
||
func newDecoderView() decoderModel {
|
||
ta := textarea.New()
|
||
ta.Placeholder = "Вставь текст для кодирования/декодирования"
|
||
ta.Focus()
|
||
|
||
return decoderModel{
|
||
input: ta,
|
||
output: viewport.New(0, 0),
|
||
encode: true,
|
||
}
|
||
}
|
||
|
||
func (m *decoderModel) 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
|
||
// 8 фиксированных строк вокруг output-viewport (построчный подсчёт по
|
||
// View(): "Вход"-метка 1, разделитель 1, строка статуса 1,
|
||
// разделитель+"Выход"-метка 2, разделитель+футер(две строки) 3 — итого
|
||
// 8). Раньше здесь стояло -4 — не совпадало с тем, что реально рисует
|
||
// View(), причём задолго до того, как футер стал двухстрочным: это
|
||
// самостоятельная, отдельная от разбиения футера ошибка в подсчёте.
|
||
m.output.Height = height - inHeight - 8
|
||
}
|
||
|
||
func (m decoderModel) Update(msg tea.Msg) (decoderModel, 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+z":
|
||
m.encode = !m.encode
|
||
return m, nil
|
||
case "ctrl+u":
|
||
m.apply("URL", func(s string) (string, error) {
|
||
if m.encode {
|
||
return decoder.URLEncode(s), nil
|
||
}
|
||
return decoder.URLDecode(s)
|
||
})
|
||
return m, nil
|
||
case "ctrl+b":
|
||
m.apply("Base64", func(s string) (string, error) {
|
||
if m.encode {
|
||
return decoder.Base64Encode(s), nil
|
||
}
|
||
return decoder.Base64Decode(s)
|
||
})
|
||
return m, nil
|
||
case "ctrl+e":
|
||
m.apply("HTML", func(s string) (string, error) {
|
||
if m.encode {
|
||
return decoder.HTMLEncode(s), nil
|
||
}
|
||
return decoder.HTMLDecode(s), nil
|
||
})
|
||
return m, nil
|
||
case "ctrl+x":
|
||
m.apply("Hex", func(s string) (string, error) {
|
||
if m.encode {
|
||
return decoder.HexEncode(s), nil
|
||
}
|
||
return decoder.HexDecode(s)
|
||
})
|
||
return m, nil
|
||
case "ctrl+k":
|
||
m.errMsg = ""
|
||
m.lastOp = "MD5"
|
||
m.output.SetContent(decoder.MD5Hash(m.input.Value()))
|
||
return m, nil
|
||
case "ctrl+a":
|
||
m.autoDecode()
|
||
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 *decoderModel) apply(name string, fn func(string) (string, error)) {
|
||
out, err := fn(m.input.Value())
|
||
if err != nil {
|
||
m.errMsg = fmt.Sprintf("%s: %v", name, err)
|
||
return
|
||
}
|
||
m.errMsg = ""
|
||
dir := "encode"
|
||
if !m.encode {
|
||
dir = "decode"
|
||
}
|
||
m.lastOp = name + " " + dir
|
||
m.output.SetContent(out)
|
||
}
|
||
|
||
func (m *decoderModel) autoDecode() {
|
||
steps := decoder.SmartDecode(m.input.Value())
|
||
if len(steps) == 0 {
|
||
m.errMsg = "не удалось распознать ни одной известной кодировки"
|
||
return
|
||
}
|
||
m.errMsg = ""
|
||
var b strings.Builder
|
||
for i, s := range steps {
|
||
b.WriteString(fmt.Sprintf("[%d] %s:\n%s\n\n", i+1, s.Operation, s.Result))
|
||
}
|
||
m.lastOp = fmt.Sprintf("авто-цепочка (%d шаг(ов))", len(steps))
|
||
m.output.SetContent(b.String())
|
||
}
|
||
|
||
func (m decoderModel) View() string {
|
||
focusMark := func(i int) string {
|
||
if m.focus == i {
|
||
return "▸ "
|
||
}
|
||
return " "
|
||
}
|
||
|
||
dir := "encode"
|
||
if !m.encode {
|
||
dir = "decode"
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString(focusMark(0) + "Вход\n")
|
||
b.WriteString(m.input.View())
|
||
b.WriteString("\n\n")
|
||
|
||
status := fmt.Sprintf("Направление: %s (ctrl+z)", dir)
|
||
if m.lastOp != "" {
|
||
status += " · последняя операция: " + m.lastOp
|
||
}
|
||
b.WriteString(status + "\n")
|
||
if m.errMsg != "" {
|
||
b.WriteString("Ошибка: " + m.errMsg + "\n")
|
||
}
|
||
|
||
b.WriteString("\n" + focusMark(1) + "Выход\n")
|
||
b.WriteString(m.output.View())
|
||
b.WriteString("\n\nctrl+u URL · ctrl+b Base64 · ctrl+e HTML · ctrl+x Hex · ctrl+k MD5 · ctrl+a авто-цепочка\n")
|
||
b.WriteString("f1-f12, pgup — вкладки\n")
|
||
return b.String()
|
||
}
|