// Package tui реализует единственный экран stegterm — стеганография // изображений и аудио/SSTV, без таб-бара: тут нечего переключать между // собой, есть один режим работы, ветвящийся по типу загруженного // файла (см. stegoKind в stegoview.go). package tui import ( "fmt" "image" tea "github.com/charmbracelet/bubbletea" "github.com/r3g1tpr0cs/stegterm/internal/audio" "github.com/r3g1tpr0cs/stegterm/internal/stego" ) // --- Типы сообщений — в burterm они жили в общей корневой model.go // (там таких вкладок было четырнадцать, у каждой свой набор), здесь // вкладка одна, так что и сообщения естественно живут рядом с ней. --- type stegoLoadedMsg struct { img *image.RGBA path string format string size int64 } type audioLoadedMsg struct { wav *audio.WAV path string size int64 } type stegoResultMsg struct{ result *image.RGBA } type stegoExtractedMsg struct{ data []byte } type stegoSavedMsg struct{ path string } type stegoErrMsg struct{ err error } // Model — корневая модель приложения: тонкая обёртка над stegoModel, // адаптирующая её под интерфейс tea.Model верхнего уровня (stegoModel // сама по себе не реализует tea.Model — её Update возвращает stegoModel, // не tea.Model, тот же паттерн "суб-модели", что был в burterm для // каждой вкладки). type Model struct { stego stegoModel } // New создаёт начальное состояние. func New() Model { return Model{stego: newStegoView()} } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.stego.SetSize(msg.Width-2, msg.Height-2) return m, nil case tea.KeyMsg: if msg.String() == "ctrl+c" { return m, tea.Quit } case stegoLoadedMsg: m.stego.kind = stegoKindImage m.stego.original = msg.img m.stego.wav = nil m.stego.loadedPath = msg.path m.stego.format = msg.format m.stego.fileSize = msg.size m.stego.result = nil m.stego.kittyEscape = "" m.stego.extractedText = "" m.stego.extractedHex = "" m.stego.errMsg = "" m.stego.statusMsg = fmt.Sprintf("загружено: %s (%dx%d)", msg.path, msg.img.Bounds().Dx(), msg.img.Bounds().Dy()) return m, nil case audioLoadedMsg: m.stego.kind = stegoKindAudio m.stego.wav = msg.wav m.stego.original = nil m.stego.loadedPath = msg.path m.stego.fileSize = msg.size m.stego.result = nil m.stego.kittyEscape = "" m.stego.extractedText = "" m.stego.extractedHex = "" m.stego.errMsg = "" var dur float64 if msg.wav.SampleRate > 0 { dur = float64(len(msg.wav.Samples)) / float64(msg.wav.SampleRate) } m.stego.statusMsg = fmt.Sprintf("загружено: %s (%d Гц, %.2fс) — ctrl+r считает результат", msg.path, msg.wav.SampleRate, dur) return m, nil case stegoResultMsg: m.stego.result = msg.result m.stego.extractedText = "" m.stego.extractedHex = "" pngData, err := stego.EncodePNG(msg.result) if err != nil { m.stego.errMsg = err.Error() m.stego.kittyEscape = "" return m, nil } m.stego.errMsg = "" m.stego.kittyEscape = stego.KittyEscape(pngData) bnd := msg.result.Bounds() m.stego.statusMsg = fmt.Sprintf("готово: %dx%d (ctrl+v — показать/повторить)", bnd.Dx(), bnd.Dy()) return m, kittyPrintCmd(m.stego.kittyEscape) case stegoExtractedMsg: text, hexStr := formatExtracted(msg.data) m.stego.extractedText = text m.stego.extractedHex = hexStr m.stego.result = nil m.stego.kittyEscape = "" m.stego.errMsg = "" m.stego.statusMsg = fmt.Sprintf("извлечено %d байт", len(msg.data)) return m, nil case stegoSavedMsg: m.stego.errMsg = "" m.stego.statusMsg = "сохранено в " + msg.path return m, nil case stegoErrMsg: m.stego.errMsg = msg.err.Error() return m, nil } var cmd tea.Cmd m.stego, cmd = m.stego.Update(msg) return m, cmd } func (m Model) View() string { return m.stego.View() }