1
0
Fork 0
mirror of https://github.com/postmannen/ctrl.git synced 2024-12-14 12:37:31 +00:00
ctrl/tui.go
2022-01-10 13:21:30 +01:00

482 lines
13 KiB
Go

package steward
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type tui struct {
}
func newTui() (*tui, error) {
s := tui{}
return &s, nil
}
type slide struct {
name string
key tcell.Key
primitive tview.Primitive
}
func (s *tui) Start() error {
pages := tview.NewPages()
app := tview.NewApplication()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyF1 {
pages.SwitchToPage("info")
return nil
} else if event.Key() == tcell.KeyF2 {
pages.SwitchToPage("message")
return nil
}
return event
})
info := tview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetWrap(false)
// The slides to draw, and their name.
// NB: This slice is being looped over further below, to create the menu
// elements. If adding a new slide, make sure that slides are ordered in
// chronological order, so we can auto generate the info menu with it's
// corresponding F key based on the slice index+1.
slides := []slide{
{name: "message", key: tcell.KeyF2, primitive: messageSlide(app)},
{name: "info", key: tcell.KeyF1, primitive: infoSlide(app)},
}
// Add on page for each slide.
for i, v := range slides {
if i == 0 {
pages.AddPage(v.name, v.primitive, true, true)
fmt.Fprintf(info, " F%v:%v ", i+1, v.name)
continue
}
pages.AddPage(v.name, v.primitive, true, false)
fmt.Fprintf(info, " F%v:%v ", i+1, v.name)
}
// Create the main layout.
layout := tview.NewFlex()
//layout.SetBorder(true)
layout.SetDirection(tview.FlexRow).
AddItem(pages, 0, 10, true).
AddItem(info, 1, 1, false)
root := app.SetRoot(layout, true)
root.EnableMouse(true)
if err := root.Run(); err != nil {
log.Printf("error: root.Run(): %v\n", err)
os.Exit(1)
}
return nil
}
func infoSlide(app *tview.Application) tview.Primitive {
flex := tview.NewFlex()
flex.SetTitle("info")
flex.SetBorder(true)
textView := tview.NewTextView()
flex.AddItem(textView, 0, 1, false)
fmt.Fprintf(textView, "Information page for Stew.\n")
return flex
}
func messageSlide(app *tview.Application) tview.Primitive {
type pageMessage struct {
flex *tview.Flex
msgInputForm *tview.Form
msgOutputForm *tview.TextView
logForm *tview.TextView
}
p := pageMessage{}
app = tview.NewApplication()
p.msgInputForm = tview.NewForm()
p.msgInputForm.SetBorder(true).SetTitle("Request values").SetTitleAlign(tview.AlignLeft)
p.msgOutputForm = tview.NewTextView()
p.msgOutputForm.SetBorder(true).SetTitle("Message output").SetTitleAlign(tview.AlignLeft)
p.msgOutputForm.SetChangedFunc(func() {
// Will cause the log window to be redrawn as soon as
// new output are detected.
app.Draw()
})
p.logForm = tview.NewTextView()
p.logForm.SetBorder(true).SetTitle("Log/Status").SetTitleAlign(tview.AlignLeft)
p.logForm.SetChangedFunc(func() {
// Will cause the log window to be redrawn as soon as
// new output are detected.
app.Draw()
})
// Create a flex layout.
//
// First create the outer flex layout.
p.flex = tview.NewFlex().SetDirection(tview.FlexRow).
// Add the top windows with columns.
AddItem(tview.NewFlex().SetDirection(tview.FlexColumn).
AddItem(p.msgInputForm, 0, 10, false).
AddItem(p.msgOutputForm, 0, 10, false),
0, 10, false).
// Add the bottom log window.
AddItem(tview.NewFlex().
AddItem(p.logForm, 0, 2, false),
0, 2, false)
m := tuiMessage{}
// Draw all the message input field with values on the screen.
//
// Loop trough all the fields of the Message struct, and create
// a an input field or dropdown selector for each field.
// If a field of the struct is not defined below, it will be
// created a "no defenition" element, so it we can easily spot
// Message fields who miss an item in the form.
//
// INFO: The reason that reflect are being used here is to have
// a simple way of detecting that we are creating form fields
// for all the fields in the struct. If we have forgot'en one
// it will create a "no case" field in the console, to easily
// detect that a struct field are missing a defenition below.
mRefVal := reflect.ValueOf(m)
for i := 0; i < mRefVal.NumField(); i++ {
fieldName := mRefVal.Type().Field(i).Name
switch fieldName {
case "_":
case "ToNode":
// Get nodes from file.
values, err := getNodeNames("nodeslist.cfg")
if err != nil {
log.Printf("error: unable to open file: %v\n", err)
}
item := tview.NewDropDown()
item.SetLabelColor(tcell.ColorIndianRed)
item.SetLabel(fieldName).SetOptions(values, nil)
p.msgInputForm.AddFormItem(item)
//c.msgForm.AddDropDown(mRefVal.Type().Field(i).Name, values, 0, nil).SetItemPadding(1)
case "ToNodes":
value := `"ship1","ship2","ship3"`
p.msgInputForm.AddInputField(fieldName, value, 30, nil, nil)
case "Method":
var m Method
ma := m.GetMethodsAvailable()
values := []string{}
for k := range ma.Methodhandlers {
values = append(values, string(k))
}
p.msgInputForm.AddDropDown(fieldName, values, 0, nil).SetItemPadding(1)
case "MethodArgs":
value := ``
p.msgInputForm.AddInputField(fieldName, value, 30, nil, nil)
case "ReplyMethod":
var m Method
rm := m.GetReplyMethods()
values := []string{}
for _, k := range rm {
values = append(values, string(k))
}
p.msgInputForm.AddDropDown(fieldName, values, 0, nil).SetItemPadding(1)
case "ReplyMethodArgs":
value := ``
p.msgInputForm.AddInputField(fieldName, value, 30, nil, nil)
case "ACKTimeout":
value := 30
p.msgInputForm.AddInputField(fieldName, fmt.Sprintf("%d", value), 30, validateInteger, nil)
case "Retries":
value := 1
p.msgInputForm.AddInputField(fieldName, fmt.Sprintf("%d", value), 30, validateInteger, nil)
case "ReplyACKTimeout":
value := 30
p.msgInputForm.AddInputField(fieldName, fmt.Sprintf("%d", value), 30, validateInteger, nil)
case "ReplyRetries":
value := 1
p.msgInputForm.AddInputField(fieldName, fmt.Sprintf("%d", value), 30, validateInteger, nil)
case "MethodTimeout":
value := 120
p.msgInputForm.AddInputField(fieldName, fmt.Sprintf("%d", value), 30, validateInteger, nil)
case "ReplyMethodTimeout":
value := 120
p.msgInputForm.AddInputField(fieldName, fmt.Sprintf("%d", value), 30, validateInteger, nil)
case "Directory":
value := "/some-dir/"
p.msgInputForm.AddInputField(fieldName, value, 30, nil, nil)
case "FileName":
value := ".log"
p.msgInputForm.AddInputField(fieldName, value, 30, nil, nil)
case "RelayViaNode":
// Get nodes from file.
values, err := getNodeNames("nodeslist.cfg")
if err != nil {
log.Printf("error: unable to open file: %v\n", err)
os.Exit(1)
}
item := tview.NewDropDown()
item.SetLabelColor(tcell.ColorIndianRed)
item.SetLabel(fieldName).SetOptions(values, nil)
p.msgInputForm.AddFormItem(item)
//c.msgForm.AddDropDown(mRefVal.Type().Field(i).Name, values, 0, nil).SetItemPadding(1)
case "RelayReplyMethod":
var m Method
rm := m.GetReplyMethods()
values := []string{}
for _, k := range rm {
values = append(values, string(k))
}
p.msgInputForm.AddDropDown(fieldName, values, 0, nil).SetItemPadding(1)
default:
// Add a no definition fields to the form if a a field within the
// struct were missing an action above, so we can easily detect
// if there is missing a case action for one of the struct fields.
p.msgInputForm.AddDropDown("error: no case for: "+fieldName, []string{"1", "2"}, 0, nil).SetItemPadding(1)
}
}
// Add Buttons below the message fields. Like Generate and Exit.
p.msgInputForm.
// Add a generate button, which when pressed will loop through all the
// message form items, and if found fill the value into a msg struct,
// and at last write it to a file.
AddButton("generate to console", func() {
p.msgOutputForm.Clear()
fh := p.msgOutputForm
m := tuiMessage{}
// Loop trough all the form fields, check the value of each
// form field, and add the value to m.
for i := 0; i < p.msgInputForm.GetFormItemCount(); i++ {
fi := p.msgInputForm.GetFormItem(i)
label, value := getLabelAndValue(fi)
switch label {
case "ToNode":
v := Node(value)
m.ToNode = &v
case "ToNodes":
slice, err := stringToNode(value)
if err != nil {
fmt.Fprintf(p.logForm, "%v : error: ReplyMethodArgs missing or malformed format, should be \"arg0\",\"arg1\",\"arg2\", %v\n", time.Now().Format("Mon Jan _2 15:04:05 2006"), err)
return
}
m.ToNodes = slice
case "Method":
v := Method(value)
m.Method = &v
case "MethodArgs":
slice, err := stringToSlice(value)
if err != nil {
fmt.Fprintf(p.logForm, "%v : error: ReplyMethodArgs missing or malformed format, should be \"arg0\",\"arg1\",\"arg2\", %v\n", time.Now().Format("Mon Jan _2 15:04:05 2006"), err)
return
}
m.MethodArgs = slice
case "ReplyMethod":
v := Method(value)
m.ReplyMethod = &v
case "ReplyMethodArgs":
slice, err := stringToSlice(value)
if err != nil {
fmt.Fprintf(p.logForm, "%v : error: ReplyMethodArgs missing or malformed format, should be \"arg0\",\"arg1\",\"arg2\", %v\n", time.Now().Format("Mon Jan _2 15:04:05 2006"), err)
return
}
m.ReplyMethodArgs = slice
case "ACKTimeout":
v, _ := strconv.Atoi(value)
m.ACKTimeout = &v
case "Retries":
v, _ := strconv.Atoi(value)
m.Retries = &v
case "ReplyACKTimeout":
v, _ := strconv.Atoi(value)
m.ReplyACKTimeout = &v
case "ReplyRetries":
v, _ := strconv.Atoi(value)
m.ReplyRetries = &v
case "MethodTimeout":
v, _ := strconv.Atoi(value)
m.MethodTimeout = &v
case "ReplyMethodTimeout":
v, _ := strconv.Atoi(value)
m.ReplyMethodTimeout = &v
case "Directory":
m.Directory = &value
case "FileName":
m.FileName = &value
case "RelayViaNode":
v := Node(value)
m.RelayViaNode = &v
case "RelayReplyMethod":
v := Method(value)
m.RelayReplyMethod = &v
default:
fmt.Fprintf(p.logForm, "%v : error: did not find case definition for how to handle the \"%v\" within the switch statement\n", time.Now().Format("Mon Jan _2 15:04:05 2006"), label)
return
}
}
msgs := []tuiMessage{}
msgs = append(msgs, m)
msgsIndented, err := json.MarshalIndent(msgs, "", " ")
if err != nil {
fmt.Fprintf(p.logForm, "%v : error: jsonIndent failed: %v\n", time.Now().Format("Mon Jan _2 15:04:05 2006"), err)
}
_, err = fh.Write(msgsIndented)
if err != nil {
fmt.Fprintf(p.logForm, "%v : error: write to fh failed: %v\n", time.Now().Format("Mon Jan _2 15:04:05 2006"), err)
}
}).
// Add exit button.
AddButton("exit", func() {
app.Stop()
})
app.SetFocus(p.msgInputForm)
return p.flex
}
// stringToSlice will Split the comma separated string
// into a and remove the start and end ampersand.
func stringToSlice(s string) (*[]string, error) {
if s == "" {
return nil, nil
}
var stringSlice []string
sp := strings.Split(s, ",")
for _, v := range sp {
// Check if format is correct, return if not.
pre := strings.HasPrefix(v, "\"")
suf := strings.HasSuffix(v, "\"")
if !pre || !suf {
return nil, fmt.Errorf("stringToSlice: missing leading or ending ampersand")
}
// Remove leading and ending ampersand.
v = v[1:]
v = strings.TrimSuffix(v, "\"")
stringSlice = append(stringSlice, v)
}
return &stringSlice, nil
}
// stringToNodes will Split the comma separated slice
// of nodes, and remove the start and end ampersand.
func stringToNode(s string) (*[]Node, error) {
if s == "" {
return nil, nil
}
var nodeSlice []Node
sp := strings.Split(s, ",")
for _, v := range sp {
// Check if format is correct, return if not.
pre := strings.HasPrefix(v, "\"")
suf := strings.HasSuffix(v, "\"")
if !pre || !suf {
return nil, fmt.Errorf("stringToSlice: missing leading or ending ampersand")
}
// Remove leading and ending ampersand.
v = v[1:]
v = strings.TrimSuffix(v, "\"")
nodeSlice = append(nodeSlice, Node(v))
}
return &nodeSlice, nil
}
// Will return the Label And the text Value of an input or dropdown form field.
func getLabelAndValue(fi tview.FormItem) (string, string) {
var label string
var value string
switch v := fi.(type) {
case *tview.InputField:
value = v.GetText()
label = v.GetLabel()
case *tview.DropDown:
label = v.GetLabel()
_, value = v.GetCurrentOption()
}
return label, value
}
// Check if number is int.
func validateInteger(text string, ch rune) bool {
if text == "-" {
return true
}
_, err := strconv.Atoi(text)
return err == nil
}
// getNodes will load all the node names from a file, and return a slice of
// string values, each representing a unique node.
func getNodeNames(fileName string) ([]string, error) {
dirPath, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("error: tui: unable to get working directory: %v", err)
}
filePath := filepath.Join(dirPath, fileName)
log.Printf(" * filepath : %v\n", filePath)
fh, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("error: tui: you should create a file named nodeslist.cfg with all your nodes : %v", err)
}
defer fh.Close()
nodes := []string{}
scanner := bufio.NewScanner(fh)
for scanner.Scan() {
node := scanner.Text()
nodes = append(nodes, node)
}
return nodes, nil
}