196 lines
5.6 KiB
Go
196 lines
5.6 KiB
Go
package ui
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
const (
|
|
// Status is the status buffer.
|
|
Status = iota
|
|
// Profile is the profile buffer.
|
|
Profile
|
|
)
|
|
|
|
// BufferHandler is the concrete functionality of a Buffer.
|
|
type BufferHandler interface {
|
|
open(m *model) error // gracefully open a new buffer
|
|
validate(m model, input string) error // validate incoming input
|
|
handle(input, hiddenInput string) tea.Msg // handle and dispatch messages from input
|
|
persist(m *model, input string) // store buffer input for persistence
|
|
close(m model) error // gracefully destroy a buffer
|
|
}
|
|
|
|
// Buffer is a user-facing interactive buffer in the TUI.
|
|
type Buffer struct {
|
|
name string // user-friendly buffer name for the menu
|
|
inputChannel chan string // channel for passing input to handlers
|
|
viewport viewport.Model // viewport for rendering lines
|
|
viewportLines []string // lines for viewport renderer
|
|
viewportIsReady bool // whether or not the viewport is ready to render
|
|
inputHistory []string // buffer input history for optional persistence
|
|
persistence bool // whether or not to persist buffer input to file
|
|
|
|
BufferHandler
|
|
}
|
|
|
|
// StatusBuffer is the privileged status buffer where e.g. you input
|
|
// program-wide commands. It can also report general status information, e.g.
|
|
// the status of ACN connectivity.
|
|
type StatusBufferHandler Buffer
|
|
|
|
func (s StatusBufferHandler) open(m *model) error {
|
|
buffer := m.buffers[m.menuState]
|
|
cairdeLogsDir := path.Join(m.userDir, "logs")
|
|
bufferLogsPath := path.Join(cairdeLogsDir, buffer.name)
|
|
|
|
f, err := os.OpenFile(bufferLogsPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create/open file %s: %s", bufferLogsPath, err)
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
buffer.inputHistory = append(buffer.inputHistory, scanner.Text())
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return fmt.Errorf("unable to read lines from %s : %s", bufferLogsPath, err)
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
return fmt.Errorf("unable to close %s file handler: %s", bufferLogsPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s StatusBufferHandler) validate(m model, input string) error {
|
|
if string(input[0]) != "/" {
|
|
return fmt.Errorf("Woops, this is not a chat buffer. Only commands are allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s StatusBufferHandler) handle(input, hiddenInput string) tea.Msg {
|
|
cmds := strings.Split(input, " ")
|
|
|
|
switch {
|
|
case cmds[0] == "help":
|
|
return cmdMsg{output: strings.Split(cmdHelp, "\n")}
|
|
|
|
case cmds[0] == "quit":
|
|
return turnAcnOffMsg{quitProgram: true}
|
|
|
|
case cmds[0] == "start":
|
|
return showGettingStartedGuideMsg{}
|
|
|
|
case cmds[0] == "profile":
|
|
if cmds[1] != "unlock" && cmds[1] != "info" && cmds[1] != "create" {
|
|
profileHelp := `Unknown "profile" command?
|
|
/profile create <name> <password> <password> | Create a new profile
|
|
/profile info | Show profile information
|
|
/profile unlock <password> | Unlock profile(s)`
|
|
return cmdMsg{output: strings.Split(profileHelp, "\n")}
|
|
}
|
|
|
|
switch {
|
|
case cmds[1] == "unlock":
|
|
if len(cmds) != 3 {
|
|
return cmdMsg{output: strings.Split(cmdHelp, "\n")}
|
|
}
|
|
|
|
return profileUnlockMsg{
|
|
password: strings.TrimSpace(hiddenInput),
|
|
}
|
|
|
|
case cmds[1] == "info":
|
|
return profileInfoMsg{}
|
|
|
|
case cmds[1] == "create":
|
|
if len(cmds) != 5 {
|
|
return cmdMsg{output: strings.Split(cmdHelp, "\n")}
|
|
}
|
|
|
|
passwords := strings.Split(strings.TrimSpace(hiddenInput), " ")
|
|
if len(passwords) != 2 {
|
|
return cmdMsg{output: []string{"Profile create: unable to parse hidden input"}}
|
|
}
|
|
|
|
if passwords[0] != passwords[1] {
|
|
return cmdMsg{output: []string{"Profile create: passwords do not match?"}}
|
|
}
|
|
|
|
return profileCreateMsg{
|
|
name: cmds[2],
|
|
password: passwords[1],
|
|
}
|
|
}
|
|
|
|
case cmds[0] == "acn":
|
|
if cmds[1] != "on" && cmds[1] != "off" {
|
|
acnHelp := `Unknown "acn" command?
|
|
/acn on | Turn on the Tor ACN
|
|
/acn off | Turn off the Tor ACN`
|
|
return cmdMsg{output: strings.Split(acnHelp, "\n")}
|
|
}
|
|
|
|
switch {
|
|
case cmds[1] == "on":
|
|
return turnAcnOnMsg{}
|
|
case cmds[1] == "off":
|
|
return turnAcnOffMsg{}
|
|
}
|
|
|
|
case cmds[0] == "clear":
|
|
return clearScreenMsg{}
|
|
}
|
|
|
|
return cmdMsg{output: []string{unknownCmdHelp}}
|
|
}
|
|
|
|
func (s StatusBufferHandler) persist(m *model, input string) {
|
|
buffer := m.buffers[m.menuState]
|
|
if buffer.persistence {
|
|
buffer.inputHistory = append(buffer.inputHistory, input)
|
|
m.buffers[m.menuState] = buffer
|
|
}
|
|
}
|
|
|
|
func (s StatusBufferHandler) close(m model) error {
|
|
buffer := m.buffers[m.menuState]
|
|
cairdeLogsDir := path.Join(m.userDir, "logs")
|
|
bufferLogsPath := path.Join(cairdeLogsDir, buffer.name)
|
|
|
|
if !buffer.persistence {
|
|
return nil
|
|
}
|
|
|
|
f, err := os.OpenFile(bufferLogsPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create/open file %s: %s", bufferLogsPath, err)
|
|
}
|
|
|
|
for _, cmd := range buffer.inputHistory {
|
|
if _, err := f.WriteString(cmd + "\n"); err != nil {
|
|
return fmt.Errorf("unable to append input to %s: %s", bufferLogsPath, err)
|
|
}
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
return fmt.Errorf("unable to close %s file handler: %s", bufferLogsPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProfileBuffer is the profile buffer. There can be several profile buffers
|
|
// which correspond to several profiles. Profile buffers take input and report
|
|
// output for profile-specific information, e.g. invites from a new contact.
|
|
type ProfileBuffer Buffer
|