mirror of
https://codeberg.org/Kankys/doxa.git
synced 2026-11-11 14:44:36 +01:00
new-doxa-monorepo
This commit is contained in:
1 parent
b4401527a3
commit
5eacbb0182
162 files changed
+21820
-2
No files matched your search
@@ -0,0 +1,613 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
appStyle = lipgloss.NewStyle().
|
||||
Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("62"))
|
||||
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#FAFAFA")).
|
||||
Background(lipgloss.Color("#7D56F4")).
|
||||
Padding(0, 1).
|
||||
MarginBottom(1)
|
||||
|
||||
selectedStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7D56F4")).
|
||||
Bold(true)
|
||||
|
||||
statusStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("205"))
|
||||
|
||||
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||
cursorStyle = focusedStyle.Copy()
|
||||
|
||||
confirmBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("205")).
|
||||
Padding(1, 2).Align(lipgloss.Center)
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateStartup AppState = iota
|
||||
StateMainMenu
|
||||
StateWriteMessage
|
||||
StateDecryptMessage
|
||||
StateShowDecryptedMessage
|
||||
StateManageCiphers
|
||||
StateCreateCipherModal
|
||||
StateImportCipherModal
|
||||
StateExportCipherModal
|
||||
StateConfirmDeleteCipher
|
||||
)
|
||||
|
||||
type model struct {
|
||||
state AppState
|
||||
ciphers []string
|
||||
activeCipher string
|
||||
activeConfig *CipherConfig
|
||||
cursor int
|
||||
textInput textinput.Model // Reusable path/name textinput
|
||||
textarea textarea.Model // Plaintext message content
|
||||
filenameInput textinput.Model // Filename input for write screen
|
||||
viewport viewport.Model // Scrolling viewport for decrypted messages
|
||||
focusIndex int // For dual fields (e.g. Write Screen)
|
||||
statusMsg string
|
||||
lang string
|
||||
width int
|
||||
height int
|
||||
ready bool
|
||||
}
|
||||
|
||||
func NewModel() model {
|
||||
ti := textinput.New()
|
||||
ti.Cursor.Style = cursorStyle
|
||||
|
||||
ta := textarea.New()
|
||||
ta.SetWidth(60)
|
||||
ta.SetHeight(10)
|
||||
ta.FocusedStyle.CursorLine = lipgloss.NewStyle().Background(lipgloss.Color("62"))
|
||||
|
||||
fi := textinput.New()
|
||||
fi.Cursor.Style = cursorStyle
|
||||
|
||||
m := model{
|
||||
state: StateStartup,
|
||||
textInput: ti,
|
||||
textarea: ta,
|
||||
filenameInput: fi,
|
||||
lang: "cs",
|
||||
focusIndex: 0,
|
||||
}
|
||||
m.loadCiphersAndActivateDefault()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *model) loadCiphersAndActivateDefault() {
|
||||
cList, err := ListCiphers()
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(cList) == 0 {
|
||||
err = GenerateNewCipher("default")
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf("Error generating default: %v", err)
|
||||
return
|
||||
}
|
||||
cList, _ = ListCiphers()
|
||||
}
|
||||
|
||||
m.ciphers = cList
|
||||
|
||||
// Find if activeCipher is still in the list, otherwise select first
|
||||
found := false
|
||||
for _, c := range cList {
|
||||
if c == m.activeCipher {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && len(cList) > 0 {
|
||||
m.activeCipher = cList[0]
|
||||
}
|
||||
|
||||
if m.activeCipher != "" {
|
||||
cfg, err := LoadCipher(m.activeCipher)
|
||||
if err == nil {
|
||||
m.activeConfig = cfg
|
||||
} else {
|
||||
m.statusMsg = fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(textinput.Blink, textarea.Blink, tea.EnterAltScreen)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
tr := Translations[m.lang]
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.textarea.SetWidth(msg.Width - 10)
|
||||
m.textarea.SetHeight(msg.Height - 12)
|
||||
|
||||
vpHeight := msg.Height - 8
|
||||
if vpHeight < 1 {
|
||||
vpHeight = 1
|
||||
}
|
||||
|
||||
if !m.ready {
|
||||
m.viewport = viewport.New(msg.Width-6, vpHeight)
|
||||
m.ready = true
|
||||
} else {
|
||||
m.viewport.Width = msg.Width - 6
|
||||
m.viewport.Height = vpHeight
|
||||
}
|
||||
|
||||
case tea.KeyMsg:
|
||||
// Global keys
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
switch m.state {
|
||||
case StateStartup:
|
||||
if msg.String() == "enter" {
|
||||
m.state = StateMainMenu
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
}
|
||||
if msg.String() == "q" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case StateMainMenu:
|
||||
switch msg.String() {
|
||||
case "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "l":
|
||||
if m.lang == "cs" {
|
||||
m.lang = "en"
|
||||
} else {
|
||||
m.lang = "cs"
|
||||
}
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "1":
|
||||
m.state = StateWriteMessage
|
||||
m.textarea.SetValue("")
|
||||
m.textarea.Focus()
|
||||
m.filenameInput.SetValue(fmt.Sprintf("zprava_%s.txt", time.Now().Format("20060102_150405")))
|
||||
m.focusIndex = 0
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "2":
|
||||
m.state = StateDecryptMessage
|
||||
m.textInput.Focus()
|
||||
m.textInput.SetValue("")
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "3":
|
||||
m.state = StateManageCiphers
|
||||
m.loadCiphersAndActivateDefault()
|
||||
// Find cursor index for active cipher
|
||||
m.cursor = 0
|
||||
for idx, val := range m.ciphers {
|
||||
if val == m.activeCipher {
|
||||
m.cursor = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case StateWriteMessage:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "tab", "shift+tab":
|
||||
if m.focusIndex == 0 {
|
||||
m.focusIndex = 1
|
||||
m.textarea.Blur()
|
||||
m.filenameInput.Focus()
|
||||
} else {
|
||||
m.focusIndex = 0
|
||||
m.filenameInput.Blur()
|
||||
m.textarea.Focus()
|
||||
}
|
||||
return m, nil
|
||||
case "ctrl+s":
|
||||
// Save & encrypt
|
||||
content := m.textarea.Value()
|
||||
filename := strings.TrimSpace(m.filenameInput.Value())
|
||||
if filename == "" {
|
||||
filename = "zprava.txt"
|
||||
}
|
||||
if m.activeConfig == nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, "není vybraná žádná šifra")
|
||||
return m, nil
|
||||
}
|
||||
encrypted := EncryptMessage(content, m.activeConfig)
|
||||
|
||||
exportDir, err := getExportDir()
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
return m, nil
|
||||
}
|
||||
fullPath := filepath.Join(exportDir, filename)
|
||||
err = os.WriteFile(fullPath, []byte(encrypted), 0644)
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
} else {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusSaved, fullPath)
|
||||
m.state = StateMainMenu
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Forward keys to the focused input
|
||||
if m.focusIndex == 0 {
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
} else {
|
||||
m.filenameInput, cmd = m.filenameInput.Update(msg)
|
||||
}
|
||||
return m, cmd
|
||||
|
||||
case StateDecryptMessage:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
path := strings.TrimSpace(m.textInput.Value())
|
||||
if path == "" {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var fullPath string
|
||||
if filepath.IsAbs(path) {
|
||||
fullPath = path
|
||||
} else {
|
||||
importDir, err := getImportDir()
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
return m, nil
|
||||
}
|
||||
fullPath = filepath.Join(importDir, path)
|
||||
}
|
||||
|
||||
contentBytes, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.activeConfig == nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, "není vybraná žádná šifra")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
decrypted := DecryptMessage(string(contentBytes), m.activeConfig)
|
||||
m.viewport.SetContent(decrypted)
|
||||
m.viewport.YOffset = 0
|
||||
m.state = StateShowDecryptedMessage
|
||||
m.statusMsg = tr.StatusDecrypted
|
||||
return m, nil
|
||||
}
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case StateShowDecryptedMessage:
|
||||
switch msg.String() {
|
||||
case "esc", "q":
|
||||
m.state = StateMainMenu
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
}
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case StateManageCiphers:
|
||||
switch msg.String() {
|
||||
case "esc", "q":
|
||||
m.state = StateMainMenu
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.cursor < len(m.ciphers)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
case "enter":
|
||||
if len(m.ciphers) > 0 {
|
||||
m.activeCipher = m.ciphers[m.cursor]
|
||||
m.loadCiphersAndActivateDefault()
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusActive, m.activeCipher)
|
||||
m.state = StateMainMenu
|
||||
}
|
||||
case "c":
|
||||
m.state = StateCreateCipherModal
|
||||
m.textInput.SetValue("")
|
||||
m.textInput.Focus()
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "i":
|
||||
m.state = StateImportCipherModal
|
||||
m.textInput.SetValue("")
|
||||
m.textInput.Focus()
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "e":
|
||||
if m.activeCipher != "" {
|
||||
m.state = StateExportCipherModal
|
||||
m.textInput.SetValue(fmt.Sprintf("%s_export.json", m.activeCipher))
|
||||
m.textInput.Focus()
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
}
|
||||
case "d":
|
||||
if len(m.ciphers) > 0 {
|
||||
m.state = StateConfirmDeleteCipher
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
case StateCreateCipherModal:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = StateManageCiphers
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
name := strings.TrimSpace(m.textInput.Value())
|
||||
if name == "" {
|
||||
return m, nil
|
||||
}
|
||||
err := GenerateNewCipher(name)
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
} else {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusCreated, name)
|
||||
m.activeCipher = name
|
||||
m.loadCiphersAndActivateDefault()
|
||||
}
|
||||
m.state = StateManageCiphers
|
||||
return m, nil
|
||||
}
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case StateImportCipherModal:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = StateManageCiphers
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
path := strings.TrimSpace(m.textInput.Value())
|
||||
if path == "" {
|
||||
return m, nil
|
||||
}
|
||||
name, err := ImportCipher(path)
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
} else {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusImported, name)
|
||||
m.activeCipher = name
|
||||
m.loadCiphersAndActivateDefault()
|
||||
}
|
||||
m.state = StateManageCiphers
|
||||
return m, nil
|
||||
}
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case StateExportCipherModal:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = StateManageCiphers
|
||||
m.statusMsg = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
path := strings.TrimSpace(m.textInput.Value())
|
||||
if path == "" {
|
||||
return m, nil
|
||||
}
|
||||
err := ExportCipher(m.activeCipher, path)
|
||||
if err != nil {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusError, err.Error())
|
||||
} else {
|
||||
m.statusMsg = fmt.Sprintf(tr.StatusExported, path)
|
||||
}
|
||||
m.state = StateManageCiphers
|
||||
return m, nil
|
||||
}
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case StateConfirmDeleteCipher:
|
||||
switch msg.String() {
|
||||
case "y", "Y":
|
||||
if len(m.ciphers) > 0 {
|
||||
selected := m.ciphers[m.cursor]
|
||||
dir, err := getCipherDir()
|
||||
if err == nil {
|
||||
path := filepath.Join(dir, selected+".json")
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
m.loadCiphersAndActivateDefault()
|
||||
m.cursor = 0
|
||||
}
|
||||
m.state = StateManageCiphers
|
||||
return m, nil
|
||||
case "n", "N", "esc":
|
||||
m.state = StateManageCiphers
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
tr := Translations[m.lang]
|
||||
var b strings.Builder
|
||||
|
||||
// Top Title
|
||||
b.WriteString(titleStyle.Render(tr.Title))
|
||||
b.WriteString("\n")
|
||||
|
||||
// Status Line
|
||||
activeName := m.activeCipher
|
||||
if activeName == "" {
|
||||
activeName = "none"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(tr.ActiveCipher, activeName) + "\n")
|
||||
if m.statusMsg != "" {
|
||||
b.WriteString(statusStyle.Render(m.statusMsg) + "\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
switch m.state {
|
||||
case StateStartup:
|
||||
eagle := `
|
||||
_
|
||||
( ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("`") + `
|
||||
\ ` + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("\\") + `
|
||||
/ / _
|
||||
__ / / ( \
|
||||
( "---" / / /
|
||||
\ / / /
|
||||
\ /-----' /
|
||||
\ / /
|
||||
\ / /
|
||||
'-------'
|
||||
`
|
||||
b.WriteString(lipgloss.Place(m.width-12, 14, lipgloss.Center, lipgloss.Center,
|
||||
eagle+"\n"+
|
||||
lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4")).Render("DOXA-SECRET 🦅")+"\n\n"+
|
||||
tr.StartupSubtitle+"\n\n"+
|
||||
tr.StartupHelp+"\n\n"+
|
||||
"License: MIT | © 2026 Kankys"))
|
||||
|
||||
case StateMainMenu:
|
||||
b.WriteString(fmt.Sprintf(" %s\n %s\n %s\n\n", tr.MenuWrite, tr.MenuDecrypt, tr.MenuManage))
|
||||
b.WriteString(fmt.Sprintf(" %s | %s\n", tr.MenuLang, tr.MenuQuit))
|
||||
|
||||
case StateWriteMessage:
|
||||
b.WriteString(tr.WriteTitle + "\n\n")
|
||||
|
||||
// Render content textarea
|
||||
taView := m.textarea.View()
|
||||
if m.focusIndex == 0 {
|
||||
taView = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("205")).Render(taView)
|
||||
} else {
|
||||
taView = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("240")).Render(taView)
|
||||
}
|
||||
|
||||
b.WriteString(tr.WritePromptContent + "\n" + taView + "\n\n")
|
||||
|
||||
// Render filename input
|
||||
b.WriteString(tr.WritePromptFilename)
|
||||
if m.focusIndex == 1 {
|
||||
b.WriteString(focusedStyle.Render(m.filenameInput.View()) + "\n\n")
|
||||
} else {
|
||||
b.WriteString(m.filenameInput.View() + "\n\n")
|
||||
}
|
||||
|
||||
b.WriteString(tr.WriteHelp)
|
||||
|
||||
case StateDecryptMessage:
|
||||
b.WriteString(tr.DecryptTitle + "\n\n")
|
||||
b.WriteString(tr.DecryptPromptPath + m.textInput.View() + "\n\n")
|
||||
b.WriteString(tr.DecryptHelp)
|
||||
|
||||
case StateShowDecryptedMessage:
|
||||
b.WriteString(tr.DecryptResult + "\n")
|
||||
b.WriteString(confirmBoxStyle.Render(m.viewport.View()) + "\n\n")
|
||||
b.WriteString("q/esc: " + tr.MenuQuit)
|
||||
|
||||
case StateManageCiphers:
|
||||
b.WriteString(fmt.Sprintf("--- %s ---\n\n", tr.ManageTitle))
|
||||
if len(m.ciphers) == 0 {
|
||||
b.WriteString(" " + tr.NoCiphers + "\n\n")
|
||||
} else {
|
||||
for i, c := range m.ciphers {
|
||||
cursor := " "
|
||||
if m.cursor == i {
|
||||
cursor = "> "
|
||||
}
|
||||
nameStr := c
|
||||
if c == m.activeCipher {
|
||||
nameStr += " (aktivní)"
|
||||
}
|
||||
if m.cursor == i {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("%s%s", cursor, nameStr)) + "\n")
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("%s%s\n", cursor, nameStr) + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteString("\n" + fmt.Sprintf("%s | %s | %s | %s | %s\n",
|
||||
tr.ManageCreateKey, tr.ManageImportKey, tr.ManageExportKey, tr.ManageDeleteKey, tr.ManageBackKey))
|
||||
b.WriteString(tr.ManageHelp)
|
||||
|
||||
case StateCreateCipherModal:
|
||||
b.WriteString(tr.CreateTitle + "\n\n")
|
||||
b.WriteString(tr.CreatePrompt + m.textInput.View() + "\n\n")
|
||||
b.WriteString(tr.CreateHelp)
|
||||
|
||||
case StateImportCipherModal:
|
||||
b.WriteString(tr.ImportTitle + "\n\n")
|
||||
b.WriteString(tr.ImportPrompt + m.textInput.View() + "\n\n")
|
||||
b.WriteString(tr.ImportHelp)
|
||||
|
||||
case StateExportCipherModal:
|
||||
b.WriteString(tr.ExportTitle + "\n\n")
|
||||
b.WriteString(tr.ExportPrompt + m.textInput.View() + "\n\n")
|
||||
b.WriteString(tr.ExportHelp)
|
||||
|
||||
case StateConfirmDeleteCipher:
|
||||
selected := ""
|
||||
if len(m.ciphers) > m.cursor {
|
||||
selected = m.ciphers[m.cursor]
|
||||
}
|
||||
confirmText := fmt.Sprintf(tr.ConfirmDelete+"\n\n%s | %s", selected, tr.ConfirmYes, tr.ConfirmNo)
|
||||
b.WriteString(confirmBoxStyle.Render(confirmText))
|
||||
}
|
||||
|
||||
return appStyle.Render(b.String())
|
||||
}
|
||||
Reference in new issue
Block a user