From 929c302cb0b4a4a3819a9cd0b5c62810b72a37b5 Mon Sep 17 00:00:00 2001 From: Shane C Date: Thu, 4 Jul 2024 15:22:54 -0400 Subject: [PATCH] initial commit --- .idea/.gitignore | 8 + .idea/modules.xml | 8 + .idea/ui-module.iml | 9 + .idea/vcs.xml | 6 + README.md | 7 + confirmation/confirmation.go | 83 +++++++++ go.mod | 13 ++ go.sum | 12 ++ list/list.go | 324 +++++++++++++++++++++++++++++++++++ progress/progress.go | 183 ++++++++++++++++++++ validators/interface.go | 6 + validators/mc_version.go | 46 +++++ validators/range.go | 33 ++++ 13 files changed, 738 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/ui-module.iml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 confirmation/confirmation.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 list/list.go create mode 100644 progress/progress.go create mode 100644 validators/interface.go create mode 100644 validators/mc_version.go create mode 100644 validators/range.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a06ce12 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/ui-module.iml b/.idea/ui-module.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/ui-module.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9da1320 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# TUI Module + +This is the TUI module for Eggactyl, it can handle lists validators and progress bars. + + +## Usage +Please see [the wiki](https://git.shadowhosting.xyz/Eggactyl/tui/wiki). \ No newline at end of file diff --git a/confirmation/confirmation.go b/confirmation/confirmation.go new file mode 100644 index 0000000..f26680e --- /dev/null +++ b/confirmation/confirmation.go @@ -0,0 +1,83 @@ +package confirmation + +import ( + "fmt" + "strings" + + "golang.org/x/term" +) + +type InputData struct { + Notice string + Question string +} + +func New(data InputData) (*bool, error) { + + if data.Notice != "" { + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;214m!\033[38;5;247m]\033[22m \033[3m%s\033[0m\n", data.Notice) + } + + if data.Question != "" { + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;214m?\033[38;5;247m]\033[0m %s (\033[38;5;34my\033[0m/\033[38;5;167mn\033[0m) \033[38;5;247m>>\033[3m\033[38;5;214m\n", data.Question) + } + + var input string + var chosen bool + +inputLoop: + for { + if _, err := fmt.Scanln(&input); err != nil { + return nil, err + } + + width, _, err := term.GetSize(0) + if err != nil { + return nil, err + } + + switch strings.ToLower(input) { + case "y", "yes": + chosen = true + break inputLoop + case "n", "no": + chosen = false + break inputLoop + default: + var lineNum int + + if width == 0 { + if data.Notice != "" { + lineNum++ + } + lineNum += 2 + } else { + if data.Notice == "" { + lineNum = ((len(data.Question) + 5 + width - 1) / width) + 1 + } else { + lineNum = ((len(data.Notice) + 5 + width) / width) + ((len(data.Question) + 5 + width - 1) / width) + 1 + } + + } + + for i := 0; i < lineNum; i++ { + fmt.Printf("\033[A\033[K\033[0G") + } + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;167m!\033[38;5;247m]\033[0m Invalid input, please try again!\n") + + if data.Notice != "" { + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;214m!\033[38;5;247m]\033[22m \033[3m%s\033[0m\n", data.Notice) + } + + if data.Question != "" { + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;214m?\033[38;5;247m]\033[0m %s (\033[38;5;34my\033[0m/\033[38;5;167mn\033[0m) \033[38;5;247m>>\033[3m\033[38;5;214m\n", data.Question) + } + continue inputLoop + } + } + + fmt.Println("\033[0m") + + return &chosen, nil + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b87a44c --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.shadowhosting.xyz/Eggactyl/tui + +go 1.22.4 + +require ( + github.com/nicksnyder/go-i18n/v2 v2.4.0 + golang.org/x/term v0.22.0 +) + +require ( + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..72f1f8f --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= +github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/list/list.go b/list/list.go new file mode 100644 index 0000000..b016bc5 --- /dev/null +++ b/list/list.go @@ -0,0 +1,324 @@ +package list + +import ( + "bufio" + "fmt" + "os" + "regexp" + "sort" + "strconv" + + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/term" +) + +var regex = regexp.MustCompile("\033\\[[0-9;]+m") + +func removeANSIEscapeCodes(input string) string { + return regex.ReplaceAllString(input, "") +} + +type ListData struct { + pages []ListPage + history []int + currentPage int + strLengths []int + localizer *i18n.Localizer +} + +type ListPage struct { + Title string + Items []ListItem + Render func() []ListItem + Cache []ListItem +} + +type ListItem struct { + Label string + Notice string + Order int + Render func() ListItem + LinkTo int + Value interface{} +} + +func New(localizer *i18n.Localizer) ListData { + return ListData{ + pages: []ListPage{}, + history: []int{}, + localizer: localizer, + } +} + +func (l *ListData) Execute() interface{} { + return l.listRunner() +} + +func (l *ListData) AddPage(page ListPage) { + l.pages = append(l.pages, page) +} + +func (l *ListData) listRunner() interface{} { + + var option interface{} + + for { + + var tempItemStore []ListItem + + for _, item := range l.pages[l.currentPage].Items { + + if len(l.pages[l.currentPage].Cache) == 0 { + if item.Render != nil { + renderedItem := item.Render() + renderedItem.Value = item.Value + tempItemStore = append(tempItemStore, renderedItem) + } else { + tempItemStore = append(tempItemStore, item) + } + + sort.SliceStable(tempItemStore, func(i, j int) bool { + return tempItemStore[i].Order < tempItemStore[j].Order + }) + } + + } + + if len(tempItemStore) != 0 { + l.pages[l.currentPage].Cache = append(l.pages[l.currentPage].Cache, tempItemStore...) + + if l.currentPage != 0 { + l.pages[l.currentPage].Cache = append( + l.pages[l.currentPage].Cache, + []ListItem{ + { + Label: "Back", + Value: "action_back", + }, + }..., + ) + } + + } + + l.renderList() + + chosen := l.inputHandler(l.pages[l.currentPage].Cache) + + if chosen.LinkTo != 0 { + + width, _, _ := term.GetSize(0) + + var totalLineNum int + + if width == 0 { + totalLineNum = len(l.strLengths) + } else { + for _, strLength := range l.strLengths { + totalLineNum += ((strLength) / width) + } + } + totalLineNum++ //User input line + + for i := 0; i < totalLineNum; i++ { + fmt.Printf("\033[A\033[K\033[0G") + } + + l.history = append(l.history, l.currentPage) + l.currentPage = chosen.LinkTo + + continue + + } + + if chosen.Value != nil { + + if _, ok := chosen.Value.(string); ok { + if chosen.Value == "action_home" { + + l.currentPage = 0 + l.history = []int{} + + width, _, _ := term.GetSize(0) + + var totalLineNum int + + if width == 0 { + totalLineNum = len(l.strLengths) + } else { + for _, strLength := range l.strLengths { + totalLineNum += ((strLength) / width) + } + } + totalLineNum++ //User input line + + for i := 0; i < totalLineNum; i++ { + fmt.Printf("\033[A\033[K\033[0G") + } + + continue + + } + + if chosen.Value == "action_back" { + + l.currentPage = l.history[len(l.history)-1] + + if len(l.history) == 1 { + l.history = []int{} + } else { + l.history = l.history[0 : len(l.history)-2] + } + + width, _, _ := term.GetSize(0) + + var totalLineNum int + + if width == 0 { + totalLineNum = len(l.strLengths) + } else { + for _, strLength := range l.strLengths { + totalLineNum += ((strLength) / width) + } + } + totalLineNum++ //User input line + + for i := 0; i < totalLineNum; i++ { + fmt.Printf("\033[A\033[K\033[0G") + } + + continue + + } + } + + option = chosen.Value + break + + } + + } + + return option + +} + +func (l *ListData) renderList() { + + l.strLengths = []int{} + + currentPage := l.pages[l.currentPage] + + listNotice := fmt.Sprintf("\033[1m\033[38;5;247m[\033[38;5;214m!\033[38;5;247m]\033[22m \033[3mPlease choose an option from 1 - %d\033[0m\n", len(currentPage.Cache)) + l.strLengths = append(l.strLengths, len(removeANSIEscapeCodes(listNotice))) + fmt.Print(listNotice) + + listTitle := fmt.Sprintf("\033[1m\033[38;5;247m\033[4m%s:\033[0m\n", currentPage.Title) + l.strLengths = append(l.strLengths, len(removeANSIEscapeCodes(listTitle))) + fmt.Print(listTitle) + + longestStrLength := 0 + + for _, item := range currentPage.Cache { + formattedLabel := removeANSIEscapeCodes(item.Label) + if len([]rune(formattedLabel)) > longestStrLength { + longestStrLength = len([]rune(formattedLabel)) + } + } + + for index, item := range currentPage.Cache { + var listItem string + + var userInputColor string + if index == len(currentPage.Cache)-1 { + userInputColor = "\033[3m\033[38;5;214m" + } + + if _, ok := item.Value.(string); ok { + if item.Value == "action_back" { + listItem = fmt.Sprintf(" \033[38;5;247m[\033[1m\033[38;5;167m%d\033[22m\033[38;5;247m]\033[0m %-*s \033[3m\033[38;5;247m%s\033[0m%s\n", index+1, longestStrLength, item.Label, item.Notice, userInputColor) + } else { + listItem = fmt.Sprintf(" \033[38;5;247m[\033[1m\033[38;5;214m%d\033[22m\033[38;5;247m]\033[0m %-*s \033[3m\033[38;5;247m%s\033[0m%s\n", index+1, longestStrLength, item.Label, item.Notice, userInputColor) + } + } else { + listItem = fmt.Sprintf(" \033[38;5;247m[\033[1m\033[38;5;214m%d\033[22m\033[38;5;247m]\033[0m %-*s \033[3m\033[38;5;247m%s\033[0m%s\n", index+1, longestStrLength, item.Label, item.Notice, userInputColor) + } + + l.strLengths = append(l.strLengths, len(removeANSIEscapeCodes(listItem))) + fmt.Print(listItem) + } + +} + +func (l *ListData) inputHandler(items []ListItem) ListItem { + + scanner := bufio.NewScanner(os.Stdin) + scanner.Split(bufio.ScanLines) + + var input int + + for scanner.Scan() { + + text := scanner.Text() + + inputAnswer, err := strconv.Atoi(text) + if err != nil { + width, _, _ := term.GetSize(0) + + var totalLineNum int + + if width == 0 { + totalLineNum = len(l.strLengths) + } else { + for _, strLength := range l.strLengths { + totalLineNum += ((strLength) / width) + } + } + totalLineNum++ //User input line + + for i := 0; i < totalLineNum; i++ { + fmt.Printf("\033[0m\033[A\033[K\033[0G") + } + + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;167m!\033[38;5;247m]\033[0m Invalid input, please try again!\n") + l.strLengths = []int{} + + l.renderList() + continue + } + + if inputAnswer < 1 || inputAnswer > len(items) { + width, _, _ := term.GetSize(0) + + var totalLineNum int + + if width == 0 { + totalLineNum = len(l.strLengths) + } else { + for _, strLength := range l.strLengths { + totalLineNum += ((strLength) / width) + } + } + totalLineNum++ //User input line + + for i := 0; i < totalLineNum; i++ { + fmt.Printf("\033[A\033[K\033[0G") + } + + fmt.Printf("\033[1m\033[38;5;247m[\033[38;5;167m!\033[38;5;247m]\033[0m Invalid input, please try again!\n") + l.strLengths = []int{} + + l.renderList() + continue + } + + input = inputAnswer + + break + + } + + chosenItem := items[input-1] + + return chosenItem +} diff --git a/progress/progress.go b/progress/progress.go new file mode 100644 index 0000000..cc85a88 --- /dev/null +++ b/progress/progress.go @@ -0,0 +1,183 @@ +package progress + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + "sync" + + "golang.org/x/term" +) + +var regex = regexp.MustCompile("\033\\[[0-9;]+m") + +//║░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░║ 40 characters + +type ProgressInfo struct { + Size int64 + Desc string + ClearOnFinish bool +} + +type ProgressBar struct { + lock sync.Mutex + maxBytes int64 + currentBytes int64 + hasStarted bool + currentPercent int + desc string + clearFinish bool + textWidth int +} + +func New(info ProgressInfo) *ProgressBar { + return &ProgressBar{ + maxBytes: info.Size, + desc: info.Desc, + clearFinish: info.ClearOnFinish, + } + +} + +func (p *ProgressBar) render(final bool) { + + p.lock.Lock() + defer p.lock.Unlock() + + var sb strings.Builder + + numFilled := math.Round((float64(p.currentBytes) / float64(p.maxBytes)) * 40) + numBlank := 40 - numFilled + donePercent := math.Round(float64(p.currentBytes) / float64(p.maxBytes) * 100) + + if int(donePercent) == p.currentPercent { + return + } + + var blockColor string + + if final { + blockColor = "34" + } else { + blockColor = "214" + } + + sb.WriteString(p.desc) + sb.WriteString(" ") + + percentString := strconv.Itoa(int(donePercent)) + "%" + blankPercent := 4 - len(percentString) + + for i := 0; i < blankPercent; i++ { + sb.WriteString(" ") + } + + sb.WriteString(percentString) + + sb.WriteString("\033[1m\033[38;5;247m [\033[0m") + + for i := 0; i < int(numFilled); i++ { + + sb.WriteString(fmt.Sprintf("\033[38;5;%sm█\033[0m", blockColor)) + + } + + if numFilled < 40 { + + numBlank = numBlank - 1 + + sb.WriteString("\033[38;5;214m▒\033[0m") + + for i := 0; i < int(numBlank); i++ { + sb.WriteString("\033[38;5;247m░\033[0m") + } + + } else { + + for i := 0; i < int(numBlank); i++ { + sb.WriteString("\033[38;5;247m░\033[0m") + } + + } + + sb.WriteString("\033[1m\033[38;5;247m]\033[0m") + + width, _, err := term.GetSize(0) + if err != nil { + return + } + + var lineNum int + + if width == 0 { + lineNum = 1 + } else { + lineNum = ((len(removeANSIEscapeCodes(sb.String())) + width - 1) / width) + } + + if p.hasStarted { + for i := 0; i < lineNum; i++ { + if i == lineNum-1 { + fmt.Println("\033[A\033[0G\033[K" + sb.String()) + } else { + fmt.Println("\033[A\033[0G\033[K") + } + } + } else { + fmt.Println(sb.String()) + p.hasStarted = true + } + + p.textWidth = len(removeANSIEscapeCodes(sb.String())) + +} + +func (p *ProgressBar) Add(num int64) { + p.currentBytes = p.currentBytes + num + + p.render(false) +} + +func (p *ProgressBar) Close() (err error) { + + if p.clearFinish { + + width, _, err := term.GetSize(0) + if err != nil { + return err + } + + var lineNum int + + if width == 0 { + lineNum = 1 + } else { + lineNum = ((p.textWidth + width - 1) / width) + } + + for i := 0; i < lineNum; i++ { + fmt.Println("\033[A\033[0G\033[K") + } + + } else { + p.render(true) + } + + return +} +func (p *ProgressBar) Write(b []byte) (n int, err error) { + n = len(b) + p.Add(int64(n)) + return +} + +func (p *ProgressBar) Read(b []byte) (n int, err error) { + p.Add(int64(n)) + return +} + +func removeANSIEscapeCodes(input string) string { + return regex.ReplaceAllString(input, "") +} diff --git a/validators/interface.go b/validators/interface.go new file mode 100644 index 0000000..d0c6c67 --- /dev/null +++ b/validators/interface.go @@ -0,0 +1,6 @@ +package validators + +type TextInputValidator interface { + Notice() string + ValidationFunc(input string) bool +} diff --git a/validators/mc_version.go b/validators/mc_version.go new file mode 100644 index 0000000..6d562dc --- /dev/null +++ b/validators/mc_version.go @@ -0,0 +1,46 @@ +package validators + +import ( + "fmt" +) + +func McVersion(versions []string) TextInputValidator { + return TextInputMCVersion{ + versions: versions, + } +} + +type TextInputMCVersion struct { + versions []string +} + +func (d TextInputMCVersion) Notice() string { + var versionString string + + for index, version := range d.versions { + if index == (len(d.versions) - 1) { + versionString = versionString + version + } else { + versionString = versionString + version + ", " + } + } + + return fmt.Sprintf("Available Versions: %s", versionString) +} + +func (d TextInputMCVersion) ValidationFunc(input string) bool { + + isFound := false + + for _, ver := range d.versions { + + if ver == input { + isFound = true + break + } + + } + + return isFound + +} diff --git a/validators/range.go b/validators/range.go new file mode 100644 index 0000000..1cea93b --- /dev/null +++ b/validators/range.go @@ -0,0 +1,33 @@ +package validators + +import ( + "fmt" + "strconv" +) + +func Range(min int, max int) TextInputValidator { + return TextInputRange{ + min: min, + max: max, + } +} + +type TextInputRange struct { + min int + max int +} + +func (d TextInputRange) Notice() string { + return fmt.Sprintf("Valid values: (%d - %d)", d.min, d.max) +} + +func (d TextInputRange) ValidationFunc(input string) bool { + portNum, err := strconv.Atoi(input) + if err != nil { + return false + } + if portNum > 65535 || portNum < 1 { + return false + } + return true +}