initial commit

This commit is contained in:
Shane C 2024-07-04 15:22:54 -04:00
commit 929c302cb0
Signed by: shanec
GPG key ID: E46B5FEA35B22FF9
13 changed files with 738 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View file

@ -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

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ui-module.iml" filepath="$PROJECT_DIR$/.idea/ui-module.iml" />
</modules>
</component>
</project>

9
.idea/ui-module.iml Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

7
README.md Normal file
View file

@ -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).

View file

@ -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
}

13
go.mod Normal file
View file

@ -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
)

12
go.sum Normal file
View file

@ -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=

324
list/list.go Normal file
View file

@ -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
}

183
progress/progress.go Normal file
View file

@ -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, "")
}

6
validators/interface.go Normal file
View file

@ -0,0 +1,6 @@
package validators
type TextInputValidator interface {
Notice() string
ValidationFunc(input string) bool
}

46
validators/mc_version.go Normal file
View file

@ -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
}

33
validators/range.go Normal file
View file

@ -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
}