package ui import ( "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 { 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) 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 | Create a new profile /profile info | Show profile information /profile unlock | 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