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 }