WIP: buffers and command history [ci skip]
This commit is contained in:
parent
6db960bb78
commit
60d3c0bf97
|
@ -91,7 +91,7 @@ func main() {
|
||||||
cairdeHomeDir := path.Join(user.HomeDir, ".cairde")
|
cairdeHomeDir := path.Join(user.HomeDir, ".cairde")
|
||||||
cairdeLogsDir := path.Join(cairdeHomeDir, "logs")
|
cairdeLogsDir := path.Join(cairdeHomeDir, "logs")
|
||||||
for _, baseDir := range []string{cairdeHomeDir, cairdeLogsDir} {
|
for _, baseDir := range []string{cairdeHomeDir, cairdeLogsDir} {
|
||||||
if err := os.Mkdir(baseDir, 0764); err != nil {
|
if err := os.Mkdir(baseDir, 0760); err != nil {
|
||||||
if !os.IsExist(err) {
|
if !os.IsExist(err) {
|
||||||
log.Fatalf("main: unable to create directory %s: %s", baseDir, err)
|
log.Fatalf("main: unable to create directory %s: %s", baseDir, err)
|
||||||
}
|
}
|
||||||
|
|
6
makefile
6
makefile
|
@ -16,11 +16,7 @@ check:
|
||||||
ci:
|
ci:
|
||||||
@golangci-lint run ./...
|
@golangci-lint run ./...
|
||||||
|
|
||||||
clean:
|
build:
|
||||||
@go clean && \
|
|
||||||
find ~/.cairde -type f -name "*.log" -exec rm '{}' \;
|
|
||||||
|
|
||||||
build: clean
|
|
||||||
@go build -ldflags=$(DIST_LDFLAGS) ./cmd/cairde
|
@go build -ldflags=$(DIST_LDFLAGS) ./cmd/cairde
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
|
|
|
@ -103,7 +103,7 @@ func turnAcnOn(m *model) tea.Msg {
|
||||||
|
|
||||||
func turnAcnOff(m *model, quitProgram bool) tea.Msg {
|
func turnAcnOff(m *model, quitProgram bool) tea.Msg {
|
||||||
if m.acnState != offline {
|
if m.acnState != offline {
|
||||||
logToFile(*m, "Shutting down application and ACN now")
|
logDebug(*m, "Shutting down application and ACN now")
|
||||||
m.app.Shutdown()
|
m.app.Shutdown()
|
||||||
m.acn.Close()
|
m.acn.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
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
|
||||||
|
inputHistoryCursor int // where we are in the input history
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.inputHistoryCursor = 0
|
||||||
|
if len(buffer.inputHistory) > 0 {
|
||||||
|
buffer.inputHistoryCursor = len(buffer.inputHistory) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
m.buffers[m.menuState] = buffer
|
||||||
|
m.statusBuffer = buffer
|
||||||
|
|
||||||
|
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)
|
||||||
|
buffer.inputHistoryCursor++
|
||||||
|
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, 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
|
83
ui/input.go
83
ui/input.go
|
@ -2,8 +2,6 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const unknownCmdHelp = "Unknown command? Run /help to see available commands"
|
const unknownCmdHelp = "Unknown command? Run /help to see available commands"
|
||||||
|
@ -18,85 +16,8 @@ const cmdHelp = `/acn on | Turn on the Tor
|
||||||
/start | Show getting started guide
|
/start | Show getting started guide
|
||||||
/quit | Quit the program`
|
/quit | Quit the program`
|
||||||
|
|
||||||
func handleCmd(cmd, hiddenInput string) tea.Msg {
|
|
||||||
cmds := strings.Split(cmd, " ")
|
|
||||||
|
|
||||||
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 hidePasswordInput(m *model) {
|
func hidePasswordInput(m *model) {
|
||||||
val := strings.TrimSpace(m.input.Value())
|
val := m.input.Value()
|
||||||
|
|
||||||
if len(val) == 0 {
|
if len(val) == 0 {
|
||||||
return
|
return
|
||||||
|
@ -114,8 +35,8 @@ func hidePasswordInput(m *model) {
|
||||||
if cmds[1] == "create" && len(cmds) > 3 ||
|
if cmds[1] == "create" && len(cmds) > 3 ||
|
||||||
cmds[1] == "unlock" && len(cmds) > 2 {
|
cmds[1] == "unlock" && len(cmds) > 2 {
|
||||||
lastChar := string(val[len(val)-1])
|
lastChar := string(val[len(val)-1])
|
||||||
|
m.hiddenInput += lastChar
|
||||||
if lastChar != " " {
|
if lastChar != " " {
|
||||||
m.hiddenInput += lastChar
|
|
||||||
newCmd := []rune(val)
|
newCmd := []rune(val)
|
||||||
newCmd[len(val)-1] = '*'
|
newCmd[len(val)-1] = '*'
|
||||||
m.input.SetValue(string(newCmd))
|
m.input.SetValue(string(newCmd))
|
||||||
|
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogToFile logs a message to a log file.
|
// logDebug logs debug messages.
|
||||||
func logToFile(m model, msg string) {
|
func logDebug(m model, msg string) {
|
||||||
if m.debug {
|
if m.debug {
|
||||||
log.Printf("debug: %s", msg)
|
log.Printf("debug: %s", msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,3 +68,5 @@ type renderStatusMsg struct{ line string }
|
||||||
type cmdMsg struct{ output []string }
|
type cmdMsg struct{ output []string }
|
||||||
|
|
||||||
type clearScreenMsg struct{}
|
type clearScreenMsg struct{}
|
||||||
|
|
||||||
|
type gracefulShutdownMsg struct{}
|
||||||
|
|
180
ui/model.go
180
ui/model.go
|
@ -56,38 +56,26 @@ network, initialise Cwtch and create your first profile. It's simple!
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// model is the core data structure for the entire programme
|
||||||
type model struct {
|
type model struct {
|
||||||
username string
|
username string // system username
|
||||||
userDir string
|
userDir string // system user home directory
|
||||||
|
acnState int // ACN state - online, offline (or in between...)
|
||||||
acnState int
|
menuState int // which buffer is currently selected
|
||||||
|
menuBar []string // list of buffers in use (e.g. status, profile1)
|
||||||
menuState int
|
app app.Application // cwtch application manager
|
||||||
menuBar []string
|
acn connectivity.ACN // cwtch ACN connectivity manager
|
||||||
|
width int // width of the TUI
|
||||||
app app.Application
|
height int // height of the TUI
|
||||||
acn connectivity.ACN
|
showWelcomeMessage bool // whether or not we show the welcome message when turning on
|
||||||
|
buffers []Buffer // all buffers
|
||||||
width int
|
statusBuffer Buffer // the central status buffer
|
||||||
height int
|
profiles profiles // all unlocked cwtch profiles
|
||||||
|
profileState int // which profile is currently selected
|
||||||
showWelcomeMessage bool
|
input textinput.Model // the central user input buffer
|
||||||
|
hiddenInput string // the hidden input storage for e.g. passwords
|
||||||
statusViewport viewport.Model
|
version string // the version of cairde
|
||||||
statusViewportLines []string
|
debug bool // whether or not to run under debug mode
|
||||||
statusViewportReady bool
|
|
||||||
|
|
||||||
profiles profiles
|
|
||||||
profileState int
|
|
||||||
|
|
||||||
input textinput.Model
|
|
||||||
hiddenInput string
|
|
||||||
|
|
||||||
version string
|
|
||||||
|
|
||||||
statusBuffer chan string
|
|
||||||
|
|
||||||
debug bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(username, homeDir, version string, debug bool) model { // nolint:revive
|
func NewModel(username, homeDir, version string, debug bool) model { // nolint:revive
|
||||||
|
@ -99,24 +87,28 @@ func NewModel(username, homeDir, version string, debug bool) model { // nolint:r
|
||||||
input.TextStyle = inputTextStyle
|
input.TextStyle = inputTextStyle
|
||||||
input.Focus()
|
input.Focus()
|
||||||
|
|
||||||
|
statusBuffer := Buffer{
|
||||||
|
BufferHandler: StatusBufferHandler{},
|
||||||
|
name: "status",
|
||||||
|
inputChannel: make(chan string),
|
||||||
|
viewportIsReady: false,
|
||||||
|
persistence: true, // TODO: configurable
|
||||||
|
}
|
||||||
|
|
||||||
return model{
|
return model{
|
||||||
username: username,
|
username: username,
|
||||||
userDir: path.Join(homeDir, "/.cairde/"),
|
userDir: path.Join(homeDir, "/.cairde/"),
|
||||||
|
|
||||||
acnState: offline,
|
acnState: offline,
|
||||||
|
menuBar: []string{
|
||||||
menuBar: []string{"STATUS"},
|
strings.ToUpper(statusBuffer.name),
|
||||||
menuState: 0,
|
},
|
||||||
|
menuState: 0,
|
||||||
input: input,
|
statusBuffer: statusBuffer,
|
||||||
|
buffers: []Buffer{statusBuffer},
|
||||||
version: version,
|
input: input,
|
||||||
|
version: version,
|
||||||
statusBuffer: make(chan string),
|
|
||||||
|
|
||||||
showWelcomeMessage: true,
|
showWelcomeMessage: true,
|
||||||
|
debug: debug,
|
||||||
debug: debug,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +116,7 @@ func NewModel(username, homeDir, version string, debug bool) model { // nolint:r
|
||||||
// the statusViewport. This command is repeatedly called by the logic which
|
// the statusViewport. This command is repeatedly called by the logic which
|
||||||
// responds to a renderStatusMsg in Update.
|
// responds to a renderStatusMsg in Update.
|
||||||
func (m model) receiveStatusCmd() tea.Msg {
|
func (m model) receiveStatusCmd() tea.Msg {
|
||||||
return renderStatusMsg{line: <-m.statusBuffer}
|
return renderStatusMsg{line: <-m.statusBuffer.inputChannel}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendStatusCmd delivers a tea.Msg to bubbletea for rendering lines from the
|
// sendStatusCmd delivers a tea.Msg to bubbletea for rendering lines from the
|
||||||
|
@ -141,8 +133,8 @@ func (m model) sendStatusCmd(lines ...string) tea.Cmd {
|
||||||
// we want to render messages to the status buffer but still have work to do.
|
// we want to render messages to the status buffer but still have work to do.
|
||||||
func (m model) sendStatus(lines ...string) {
|
func (m model) sendStatus(lines ...string) {
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
logToFile(m, line)
|
logDebug(m, line)
|
||||||
m.statusBuffer <- line
|
m.statusBuffer.inputChannel <- line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +171,12 @@ func (m model) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
textinput.Blink,
|
textinput.Blink,
|
||||||
m.receiveStatusCmd,
|
m.receiveStatusCmd,
|
||||||
|
func() tea.Msg {
|
||||||
|
if err := m.statusBuffer.open(&m); err != nil {
|
||||||
|
return cmdMsg{output: []string{err.Error()}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +189,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.input, cmd = m.input.Update(msg)
|
m.input, cmd = m.input.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
m.statusViewport, cmd = m.statusViewport.Update(msg)
|
m.statusBuffer.viewport, cmd = m.statusBuffer.viewport.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
for _, p := range m.profiles {
|
for _, p := range m.profiles {
|
||||||
|
@ -218,12 +216,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
m.input.Width = m.width - 5
|
m.input.Width = m.width - 5
|
||||||
|
|
||||||
if !m.statusViewportReady {
|
if !m.statusBuffer.viewportIsReady {
|
||||||
m.statusViewport = newViewport(m.width, m.height-3)
|
m.statusBuffer.viewport = newViewport(m.width, m.height-3)
|
||||||
m.statusViewportReady = true
|
m.statusBuffer.viewportIsReady = true
|
||||||
} else {
|
} else {
|
||||||
m.statusViewport.Width = m.width
|
m.statusBuffer.viewport.Width = m.width
|
||||||
m.statusViewport.Height = m.height - 3
|
m.statusBuffer.viewport.Height = m.height - 3
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range m.profiles {
|
for _, p := range m.profiles {
|
||||||
|
@ -233,6 +231,41 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
buffer := m.buffers[m.menuState]
|
||||||
|
if len(buffer.inputHistory) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
m.input.Reset()
|
||||||
|
|
||||||
|
if buffer.inputHistoryCursor < len(buffer.inputHistory)-1 {
|
||||||
|
buffer.inputHistoryCursor++
|
||||||
|
}
|
||||||
|
|
||||||
|
m.buffers[m.menuState] = buffer
|
||||||
|
|
||||||
|
lastCmd := buffer.inputHistory[buffer.inputHistoryCursor]
|
||||||
|
m.input.SetValue(lastCmd)
|
||||||
|
|
||||||
|
case "up":
|
||||||
|
buffer := m.buffers[m.menuState]
|
||||||
|
if len(buffer.inputHistory) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
m.input.Reset()
|
||||||
|
|
||||||
|
if buffer.inputHistoryCursor > 0 {
|
||||||
|
buffer.inputHistoryCursor--
|
||||||
|
}
|
||||||
|
|
||||||
|
m.buffers[m.menuState] = buffer
|
||||||
|
|
||||||
|
lastCmd := buffer.inputHistory[buffer.inputHistoryCursor]
|
||||||
|
m.input.SetValue(lastCmd)
|
||||||
|
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
return turnAcnOff(&m, true)
|
return turnAcnOff(&m, true)
|
||||||
|
@ -244,16 +277,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(cmdInput[0]) != "/" && m.menuState == 0 {
|
buffer := m.buffers[Status]
|
||||||
|
|
||||||
|
if err := buffer.validate(m, cmdInput); err != nil {
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
errMsg := "Woops, this is not a chat buffer. Only commands are allowed"
|
return cmdMsg{output: []string{err.Error()}}
|
||||||
return cmdMsg{output: []string{errMsg}}
|
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer.persist(&m, cmdInput)
|
||||||
|
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
return handleCmd(cmdInput[1:], m.hiddenInput)
|
return buffer.handle(cmdInput[1:], m.hiddenInput)
|
||||||
})
|
})
|
||||||
|
|
||||||
case "ctrl+n":
|
case "ctrl+n":
|
||||||
|
@ -298,9 +334,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return turnAcnOff(&m, msg.quitProgram)
|
return turnAcnOff(&m, msg.quitProgram)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
case gracefulShutdownMsg:
|
||||||
|
if err := m.statusBuffer.close(m); err != nil {
|
||||||
|
panic(err) // TODO: ???
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
case acnOffMsg:
|
case acnOffMsg:
|
||||||
if msg.quitProgram {
|
if msg.quitProgram {
|
||||||
return m, tea.Quit
|
return m, func() tea.Msg {
|
||||||
|
return gracefulShutdownMsg{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cmds = append(cmds, m.sendStatusCmd("ACN successfully turned off"))
|
cmds = append(cmds, m.sendStatusCmd("ACN successfully turned off"))
|
||||||
m.acnState = offline
|
m.acnState = offline
|
||||||
|
@ -431,23 +475,23 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case sendStatusMsg:
|
case sendStatusMsg:
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
for _, line := range msg.lines {
|
for _, line := range msg.lines {
|
||||||
logToFile(m, line)
|
logDebug(m, line)
|
||||||
m.statusBuffer <- line
|
m.statusBuffer.inputChannel <- line
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
case renderStatusMsg:
|
case renderStatusMsg:
|
||||||
m.statusViewportLines = append(m.statusViewportLines, renderLines(m.width, msg.line)...)
|
m.statusBuffer.viewportLines = append(m.statusBuffer.viewportLines, renderLines(m.width, msg.line)...)
|
||||||
m.statusViewport.SetContent(strings.Join(m.statusViewportLines, "\n"))
|
m.statusBuffer.viewport.SetContent(strings.Join(m.statusBuffer.viewportLines, "\n"))
|
||||||
m.statusViewport.GotoBottom()
|
m.statusBuffer.viewport.GotoBottom()
|
||||||
cmds = append(cmds, m.receiveStatusCmd)
|
cmds = append(cmds, m.receiveStatusCmd)
|
||||||
|
|
||||||
case clearScreenMsg:
|
case clearScreenMsg:
|
||||||
m.input.Reset()
|
m.input.Reset()
|
||||||
m.statusViewportLines = []string{}
|
m.statusBuffer.viewportLines = []string{}
|
||||||
m.statusViewport.SetContent("")
|
m.statusBuffer.viewport.SetContent("")
|
||||||
m.statusViewport.GotoTop()
|
m.statusBuffer.viewport.GotoTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
|
@ -459,10 +503,10 @@ func (m model) View() string {
|
||||||
var chosenViewport viewport.Model
|
var chosenViewport viewport.Model
|
||||||
switch m.menuState {
|
switch m.menuState {
|
||||||
case 0:
|
case 0:
|
||||||
chosenViewport = m.statusViewport
|
chosenViewport = m.statusBuffer.viewport
|
||||||
default:
|
default:
|
||||||
if m.menuState > len(m.profiles) {
|
if m.menuState > len(m.profiles) {
|
||||||
chosenViewport = m.statusViewport
|
chosenViewport = m.statusBuffer.viewport
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
chosenViewport = m.profiles[m.menuState-1].statusViewport
|
chosenViewport = m.profiles[m.menuState-1].statusViewport
|
||||||
|
|
Loading…
Reference in New Issue