Cybersecurity-Projects/PROJECTS/beginner/steganography-multi-tool/internal/tui/update.go

400 lines
8.2 KiB
Go

/*
©AngelaMos | 2026
update.go
Message dispatch, per-stage key handling, and async engine result wiring
*/
package tui
import (
"errors"
"path/filepath"
"strings"
"github.com/CarterPerez-dev/crypha/internal/payload"
tea "github.com/charmbracelet/bubbletea"
)
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
return m.onResize(msg)
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
m.quitting = true
return m, tea.Quit
}
return m.onKey(msg)
case fileLoadedMsg:
return m.onFileLoaded(msg)
case hideDoneMsg:
return m.onHideDone(msg)
case revealDoneMsg:
return m.onRevealDone(msg)
case savedMsg:
return m.onSaved(msg)
case tickMsg:
return m.onTick()
default:
return m.forward(msg)
}
}
func (m Model) onResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
m.width = msg.Width
m.height = msg.Height
m.ready = true
var cmd tea.Cmd
m.files, cmd = m.files.Update(msg)
return m, cmd
}
func (m Model) onKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.err = nil
switch m.stage {
case stageOperation:
return m.keyOperation(msg)
case stageFormat:
return m.keyFormat(msg)
case stageTechnique:
return m.keyTechnique(msg)
case stageCover:
return m.keyCover(msg)
case stagePayload:
return m.keyPayload(msg)
case stageSecure:
return m.keySecure(msg)
case stageSave:
return m.keySave(msg)
case stageReview:
return m.keyReview(msg)
case stagePassphrase:
return m.keyPassphrase(msg)
case stageResult:
return m.keyResult(msg)
}
return m, nil
}
func (m Model) keyOperation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q":
m.quitting = true
return m, tea.Quit
case "up", "k":
m.opPick.up()
case "down", "j":
m.opPick.down()
case "enter":
m.op = operation(m.opPick.cursor)
return m.advance()
}
return m, nil
}
func (m Model) keyFormat(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q":
m.quitting = true
return m, tea.Quit
case "esc":
return m.back()
case "up", "k":
m.fmtPick.up()
case "down", "j":
m.fmtPick.down()
case "enter":
m.format = m.fmtPick.selected().value
m.technique = ""
return m.advance()
}
return m, nil
}
func (m Model) keyTechnique(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q":
m.quitting = true
return m, tea.Quit
case "esc":
return m.back()
case "up", "k":
m.techPick.up()
case "down", "j":
m.techPick.down()
case "enter":
m.technique = m.techPick.selected().value
return m.advance()
}
return m, nil
}
func (m Model) keyCover(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if msg.String() == "esc" {
return m.back()
}
var cmd tea.Cmd
m.files, cmd = m.files.Update(msg)
if ok, path := m.files.DidSelectFile(msg); ok {
return m, loadFileCmd(stageCover, path)
}
return m, cmd
}
func (m Model) keyPayload(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.payloadMode == payloadFile {
switch msg.String() {
case "esc", "tab":
m.payloadMode = payloadMessage
return m, m.message.Focus()
}
var cmd tea.Cmd
m.files, cmd = m.files.Update(msg)
if ok, path := m.files.DidSelectFile(msg); ok {
return m, loadFileCmd(stagePayload, path)
}
return m, cmd
}
switch msg.String() {
case "esc":
return m.back()
case "tab":
m.payloadMode = payloadFile
m.message.Blur()
return m, m.files.Init()
case "enter":
if strings.TrimSpace(m.message.Value()) == "" {
m.err = errNeedPayload
return m, nil
}
m.payloadBytes = []byte(m.message.Value())
m.payloadLabel = inlineLabel
return m.advance()
}
var cmd tea.Cmd
m.message, cmd = m.message.Update(msg)
return m, cmd
}
func (m Model) keySecure(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc":
return m.back()
case "enter":
if m.secure.encrypt && len(m.secure.passphrase()) == 0 {
m.err = errNeedPassphrase
return m, nil
}
m.pass = m.secure.passphrase()
return m.advance()
}
var cmd tea.Cmd
m.secure, cmd = m.secure.update(msg)
return m, cmd
}
func (m Model) keySave(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc":
return m.back()
case "enter":
if strings.TrimSpace(m.outPath.Value()) == "" {
m.err = errNeedOutput
return m, nil
}
return m.advance()
}
var cmd tea.Cmd
m.outPath, cmd = m.outPath.Update(msg)
return m, cmd
}
func (m Model) keyReview(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q":
m.quitting = true
return m, tea.Quit
case "esc":
return m.back()
case "n":
return m.reset()
}
if m.op == opCapacity {
return m, nil
}
if msg.String() == "enter" {
if !m.hideFits() {
m.err = errPayloadTooBig
return m, nil
}
return m.advance()
}
return m, nil
}
func (m Model) keyPassphrase(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc":
m.revPass.Blur()
cmd := m.enter(stageCover)
return m, cmd
case "enter":
return m, m.enter(stageRunning)
}
var cmd tea.Cmd
m.revPass, cmd = m.revPass.Update(msg)
return m, cmd
}
func (m Model) keyResult(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.saving {
return m.keyResultSaving(msg)
}
switch msg.String() {
case "q", "esc", "enter":
m.quitting = true
return m, tea.Quit
case "n":
return m.reset()
case "s":
if m.canSaveReveal() {
m.saving = true
m.outPath.SetValue(m.suggestReveal())
return m, m.outPath.Focus()
}
}
return m, nil
}
func (m Model) keyResultSaving(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc":
m.saving = false
m.outPath.Blur()
return m, nil
case "enter":
if strings.TrimSpace(m.outPath.Value()) == "" {
m.err = errNeedOutput
return m, nil
}
return m, saveCmd(strings.TrimSpace(m.outPath.Value()), m.revealRes.Data)
}
var cmd tea.Cmd
m.outPath, cmd = m.outPath.Update(msg)
return m, cmd
}
func (m Model) onFileLoaded(msg fileLoadedMsg) (tea.Model, tea.Cmd) {
if msg.err != nil {
m.err = msg.err
return m, nil
}
switch msg.origin {
case stageCover:
m.coverPath = msg.path
m.coverBytes = msg.data
m.passPrompts = 0
return m.advance()
case stagePayload:
if len(msg.data) == 0 {
m.err = errEmptyFile
return m, nil
}
m.payloadBytes = msg.data
m.payloadLabel = filepath.Base(msg.path)
m.payloadMode = payloadMessage
return m.advance()
}
return m, nil
}
func (m Model) onHideDone(msg hideDoneMsg) (tea.Model, tea.Cmd) {
m.engineDone = true
if msg.err != nil {
m.engineErr = msg.err
} else {
m.hideRes = msg.res
m.stego = msg.data
m.outputAt = m.outPath.Value()
}
return m.maybeFinish()
}
func (m Model) onRevealDone(msg revealDoneMsg) (tea.Model, tea.Cmd) {
m.engineDone = true
m.revealRes = msg.res
m.engineErr = msg.err
return m.maybeFinish()
}
func (m Model) onSaved(msg savedMsg) (tea.Model, tea.Cmd) {
m.saving = false
m.outPath.Blur()
if msg.err != nil {
m.err = msg.err
return m, nil
}
m.savedAt = msg.path
return m, nil
}
func (m Model) onTick() (tea.Model, tea.Cmd) {
if m.stage != stageRunning {
return m, nil
}
m.animFrac += animStep
if m.animFrac >= 1 {
m.animFrac = 1
return m.maybeFinish()
}
return m, tick()
}
func (m Model) maybeFinish() (tea.Model, tea.Cmd) {
if m.stage != stageRunning || !m.engineDone || m.animFrac < 1 {
return m, nil
}
if m.op == opReveal && needsPassphrase(m.engineErr) && m.passPrompts < maxRevealPrompts {
m.passPrompts++
m.revPass.Reset()
m.stage = stagePassphrase
return m, m.revPass.Focus()
}
m.stage = stageResult
return m, nil
}
func (m Model) forward(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch m.stage {
case stageCover:
m.files, cmd = m.files.Update(msg)
case stagePayload:
if m.payloadMode == payloadFile {
m.files, cmd = m.files.Update(msg)
} else {
m.message, cmd = m.message.Update(msg)
}
case stageSecure:
m.secure, cmd = m.secure.update(msg)
case stageSave:
m.outPath, cmd = m.outPath.Update(msg)
case stagePassphrase:
m.revPass, cmd = m.revPass.Update(msg)
case stageResult:
if m.saving {
m.outPath, cmd = m.outPath.Update(msg)
}
}
return m, cmd
}
func (m Model) canSaveReveal() bool {
return m.op == opReveal && m.engineErr == nil && len(m.revealRes.Data) > 0
}
func needsPassphrase(err error) bool {
return errors.Is(err, payload.ErrPassphraseRequired) || errors.Is(err, payload.ErrDecrypt)
}