2024-10-16 22:07:31 +02:00
|
|
|
package simple
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
2024-10-22 20:10:37 +02:00
|
|
|
"log"
|
2024-10-16 22:07:31 +02:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Command struct {
|
|
|
|
id uint
|
|
|
|
Key string
|
|
|
|
Name string
|
|
|
|
Descr string
|
|
|
|
Action CommandAction
|
|
|
|
MoveTo bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type CommandAction func(m *Menu) error
|
|
|
|
|
|
|
|
func EmptyAction(m *Menu) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Command) Print() {
|
|
|
|
if c.Descr != "" {
|
|
|
|
fmt.Printf("%s: %s (%s)\n", c.Key, c.Name, c.Descr)
|
|
|
|
} else {
|
|
|
|
fmt.Printf("%s: %s\n", c.Key, c.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type MenuConfig struct {
|
2024-10-18 06:22:28 +02:00
|
|
|
Title string
|
|
|
|
BackKey string
|
|
|
|
UsingEscapeCodes bool
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func getDefConfig() *MenuConfig {
|
|
|
|
return &MenuConfig{
|
2024-10-18 06:22:28 +02:00
|
|
|
Title: "Default Menu Title",
|
|
|
|
BackKey: "<",
|
|
|
|
UsingEscapeCodes: false,
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type commandNode struct {
|
|
|
|
Val *Command
|
|
|
|
Children []*commandNode
|
|
|
|
Parent *commandNode
|
|
|
|
}
|
|
|
|
|
|
|
|
func createCommandNodeWoParent(cmd *Command) *commandNode {
|
|
|
|
return &commandNode{
|
|
|
|
Val: cmd,
|
|
|
|
Children: []*commandNode{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func createCommandNode(cmd *Command, parent *commandNode) *commandNode {
|
|
|
|
return &commandNode{
|
|
|
|
Val: cmd,
|
|
|
|
Children: []*commandNode{},
|
|
|
|
Parent: parent,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cn *commandNode) AddChild(child *commandNode) {
|
|
|
|
cn.Children = append(cn.Children, child)
|
|
|
|
}
|
|
|
|
|
|
|
|
type commandTree struct {
|
|
|
|
Root *commandNode
|
|
|
|
Pointer *commandNode
|
|
|
|
}
|
|
|
|
|
|
|
|
func createCommandTree() *commandTree {
|
|
|
|
root := createCommandNode(&Command{
|
|
|
|
Name: "Root",
|
|
|
|
}, nil)
|
|
|
|
return &commandTree{
|
|
|
|
Root: root,
|
|
|
|
Pointer: root,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type Menu struct {
|
|
|
|
Title string
|
|
|
|
BackKey string
|
2024-10-18 06:22:28 +02:00
|
|
|
// Escape Code part
|
|
|
|
//
|
|
|
|
usingEscapeCodes bool
|
|
|
|
lineCounter uint
|
2024-10-16 22:07:31 +02:00
|
|
|
//
|
2024-10-19 09:04:17 +02:00
|
|
|
Log *BufferedLogger
|
|
|
|
//
|
2024-10-16 22:07:31 +02:00
|
|
|
counterForIDs uint
|
|
|
|
cmdTree *commandTree
|
2024-10-22 20:10:37 +02:00
|
|
|
//
|
2024-10-22 22:37:20 +02:00
|
|
|
interrupt chan *MenuInterrupt
|
2024-10-22 20:10:37 +02:00
|
|
|
resumeSignal chan struct{}
|
|
|
|
inputChan chan string
|
|
|
|
errorChan chan error
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func CreateMenu(options ...SimpleMenuOption) *Menu {
|
|
|
|
conf := getDefConfig()
|
|
|
|
|
|
|
|
for _, opt := range options {
|
|
|
|
opt(conf)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Menu{
|
2024-10-18 06:22:28 +02:00
|
|
|
Title: conf.Title,
|
|
|
|
BackKey: conf.BackKey,
|
|
|
|
usingEscapeCodes: conf.UsingEscapeCodes,
|
2024-10-19 09:04:17 +02:00
|
|
|
Log: &BufferedLogger{},
|
2024-10-18 06:22:28 +02:00
|
|
|
lineCounter: 0,
|
|
|
|
counterForIDs: 1,
|
|
|
|
cmdTree: createCommandTree(),
|
2024-10-22 22:37:20 +02:00
|
|
|
interrupt: make(chan *MenuInterrupt),
|
2024-10-22 20:10:37 +02:00
|
|
|
resumeSignal: make(chan struct{}),
|
|
|
|
inputChan: make(chan string),
|
|
|
|
errorChan: make(chan error),
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (node *commandNode) AddCommand(key, name string, action CommandAction) *commandNode {
|
|
|
|
cmd := &Command{
|
|
|
|
Key: key,
|
|
|
|
Name: name,
|
|
|
|
Action: action,
|
|
|
|
MoveTo: true,
|
|
|
|
}
|
|
|
|
newNode := createCommandNode(cmd, node)
|
|
|
|
node.AddChild(newNode)
|
|
|
|
|
|
|
|
return newNode
|
|
|
|
}
|
|
|
|
|
|
|
|
func (node *commandNode) AddExecCommand(key, name string, action CommandAction) *commandNode {
|
|
|
|
cmd := &Command{
|
|
|
|
Key: key,
|
|
|
|
Name: name,
|
|
|
|
Action: action,
|
|
|
|
MoveTo: false,
|
|
|
|
}
|
|
|
|
newNode := createCommandNode(cmd, node)
|
|
|
|
node.AddChild(newNode)
|
|
|
|
|
|
|
|
return newNode
|
|
|
|
}
|
|
|
|
|
|
|
|
func (node *commandNode) AddGroupingCommand(key, name string) *commandNode {
|
|
|
|
cmd := &Command{
|
|
|
|
Key: key,
|
|
|
|
Name: name,
|
|
|
|
Action: EmptyAction,
|
|
|
|
MoveTo: true,
|
|
|
|
}
|
|
|
|
newNode := createCommandNode(cmd, node)
|
|
|
|
node.AddChild(newNode)
|
|
|
|
|
|
|
|
return newNode
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) AddCommand(key, name string, action CommandAction) (*commandNode, error) {
|
|
|
|
if m.cmdTree == nil {
|
|
|
|
return nil, CommandTree_notInit
|
|
|
|
}
|
|
|
|
return m.cmdTree.Root.AddCommand(key, name, action), nil
|
|
|
|
}
|
|
|
|
|
2024-10-22 20:46:54 +02:00
|
|
|
func (m *Menu) AddExecCommand(key, name string, action CommandAction) (*commandNode, error) {
|
|
|
|
if m.cmdTree == nil {
|
|
|
|
return nil, CommandTree_notInit
|
|
|
|
}
|
|
|
|
return m.cmdTree.Root.AddExecCommand(key, name, action), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) AddGroupingCommand(key, name string) (*commandNode, error) {
|
|
|
|
if m.cmdTree == nil {
|
|
|
|
return nil, CommandTree_notInit
|
|
|
|
}
|
|
|
|
return m.cmdTree.Root.AddGroupingCommand(key, name), nil
|
|
|
|
}
|
|
|
|
|
2024-10-18 06:25:56 +02:00
|
|
|
func (m *Menu) clearLines() {
|
|
|
|
for i := 0; i < int(m.lineCounter); i++ {
|
|
|
|
fmt.Printf("\033[F\033[K")
|
|
|
|
}
|
|
|
|
m.lineCounter = 0
|
|
|
|
}
|
|
|
|
|
2024-10-18 06:27:05 +02:00
|
|
|
func (m *Menu) handleInput(input string) {
|
|
|
|
var preHandler func()
|
|
|
|
var action CommandAction
|
|
|
|
var afterHandler func()
|
|
|
|
if m.usingEscapeCodes {
|
|
|
|
preHandler = func() {
|
|
|
|
m.clearLines()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, node := range m.cmdTree.Pointer.Children {
|
|
|
|
cmd := node.Val
|
|
|
|
if cmd == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if cmd.Key == input {
|
|
|
|
action = cmd.Action
|
|
|
|
if cmd.MoveTo {
|
|
|
|
afterHandler = func() {
|
|
|
|
m.cmdTree.Pointer = node
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2024-10-19 09:33:53 +02:00
|
|
|
if m.cmdTree.Pointer != m.cmdTree.Root && m.BackKey == input {
|
2024-10-18 06:27:05 +02:00
|
|
|
afterHandler = func() {
|
|
|
|
m.cmdTree.Pointer = m.cmdTree.Pointer.Parent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if preHandler != nil {
|
|
|
|
preHandler()
|
|
|
|
}
|
|
|
|
if action != nil {
|
|
|
|
action(m)
|
|
|
|
}
|
|
|
|
if afterHandler != nil {
|
|
|
|
afterHandler()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-19 09:35:34 +02:00
|
|
|
func (m *Menu) printMenu() {
|
2024-10-16 22:07:31 +02:00
|
|
|
fmt.Printf("%s\n", m.Title)
|
2024-10-18 06:26:29 +02:00
|
|
|
m.lineCounter++
|
2024-10-16 22:07:31 +02:00
|
|
|
path := ""
|
|
|
|
for node := m.cmdTree.Pointer; node != nil; node = node.Parent {
|
|
|
|
if node.Parent == nil {
|
|
|
|
path += node.Val.Name
|
|
|
|
} else {
|
|
|
|
path += fmt.Sprintf("%s -> ", node.Val.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fmt.Printf("At %s\n", path)
|
2024-10-18 06:26:29 +02:00
|
|
|
m.lineCounter++
|
2024-10-16 22:07:31 +02:00
|
|
|
for _, node := range m.cmdTree.Pointer.Children {
|
|
|
|
cmd := node.Val
|
|
|
|
if cmd == nil {
|
|
|
|
// TODO: Check it
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
cmd.Print()
|
2024-10-18 06:26:29 +02:00
|
|
|
m.lineCounter++
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
|
|
|
if m.cmdTree.Pointer != m.cmdTree.Root {
|
|
|
|
fmt.Printf("%s: Go back one layer\n", m.BackKey)
|
2024-10-18 06:26:29 +02:00
|
|
|
m.lineCounter++
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
2024-10-19 09:35:34 +02:00
|
|
|
}
|
|
|
|
|
2024-10-22 20:10:37 +02:00
|
|
|
func (m *Menu) readInput() {
|
|
|
|
stdinReader := bufio.NewReader(os.Stdin)
|
|
|
|
for {
|
|
|
|
msg, err := stdinReader.ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
// fmt.Printf("Error: ReadString: %v\n", err)
|
|
|
|
m.errorChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
m.lineCounter++
|
|
|
|
m.inputChan <- msg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) GetInput(prompt string) string {
|
|
|
|
nlCount := strings.Count(prompt, "\n")
|
2024-10-22 21:52:00 +02:00
|
|
|
fmt.Printf(prompt)
|
2024-10-22 20:10:37 +02:00
|
|
|
m.lineCounter += uint(nlCount)
|
2024-10-22 22:37:20 +02:00
|
|
|
rawMsg := <-m.inputChan
|
2024-10-18 06:26:29 +02:00
|
|
|
m.lineCounter++
|
2024-10-22 20:10:37 +02:00
|
|
|
msg := strings.TrimRight(rawMsg, "\n")
|
|
|
|
return msg
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) iteration() {
|
|
|
|
m.Log.Flush()
|
|
|
|
m.printMenu()
|
|
|
|
|
|
|
|
// for {
|
|
|
|
select {
|
|
|
|
case msg := <-m.inputChan:
|
|
|
|
msg = strings.TrimRight(msg, "\n")
|
|
|
|
m.handleInput(msg)
|
|
|
|
case err := <-m.errorChan:
|
|
|
|
m.Log.Logf("err: %v", err)
|
2024-10-22 22:37:20 +02:00
|
|
|
case intr := <-m.interrupt:
|
2024-10-22 20:10:37 +02:00
|
|
|
if m.usingEscapeCodes {
|
|
|
|
m.clearLines()
|
|
|
|
}
|
|
|
|
log.Printf("Interrupt")
|
2024-10-22 22:37:20 +02:00
|
|
|
intr.Func(m)
|
|
|
|
if intr.Status&IntrOptStatus_ClearAfterFinish > 0 {
|
|
|
|
m.clearLines()
|
|
|
|
}
|
2024-10-22 20:10:37 +02:00
|
|
|
}
|
|
|
|
// }
|
|
|
|
}
|
|
|
|
|
2024-10-22 22:37:20 +02:00
|
|
|
type MenuInterrupt struct {
|
|
|
|
Func func(m *Menu)
|
|
|
|
Status IntrOptStatus
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) SendInterrupt(f func(m *Menu), intrOptions ...InterruptOption) {
|
|
|
|
intr := &MenuInterrupt{
|
|
|
|
Func: f,
|
|
|
|
Status: 0,
|
|
|
|
}
|
|
|
|
for _, opt := range intrOptions {
|
|
|
|
opt(intr)
|
|
|
|
}
|
|
|
|
m.interrupt <- intr
|
2024-10-16 22:07:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) Start() {
|
2024-10-22 20:10:37 +02:00
|
|
|
go m.readInput()
|
2024-10-16 22:07:31 +02:00
|
|
|
for {
|
|
|
|
m.iteration()
|
|
|
|
}
|
|
|
|
}
|