From 905a77f8b09e330cb69ab50b7350434732675418 Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 24 Sep 2024 20:42:44 +0200 Subject: [PATCH] paginator and styles --- examples/main.go | 19 +++++++ osui.go | 9 ++-- styles/styles.go | 36 ++++++++++++++ ui/{components.go => inputBox.go} | 57 ++++++++++----------- ui/paginator.go | 83 +++++++++++++++++++++++++++++++ ui/text.go | 22 ++++++++ ui/ui.go | 9 ---- utils.go | 73 +++++++++++++++++++++++++-- 8 files changed, 263 insertions(+), 45 deletions(-) create mode 100644 examples/main.go create mode 100644 styles/styles.go rename ui/{components.go => inputBox.go} (53%) create mode 100644 ui/paginator.go create mode 100644 ui/text.go delete mode 100644 ui/ui.go diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..6d642b4 --- /dev/null +++ b/examples/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/orus-dev/osui" + "github.com/orus-dev/osui/styles" + "github.com/orus-dev/osui/ui" +) + +func main() { + colors := ui.Text( + styles.Red + "Red!\n" + styles.Green + "Green!\n" + styles.Blue + "Blue!\n", + ) + input := ui.InputBox(30) + paginator := ui.Paginator(colors, input) + paginator.UpdatePages[1] = input.Update + screen := osui.NewScreen(paginator) + screen.Render() + paginator.Run() +} diff --git a/osui.go b/osui.go index e28be07..77295ba 100644 --- a/osui.go +++ b/osui.go @@ -20,6 +20,8 @@ type Component interface { type Screen struct { components []Component + Width int + Height int } func NewScreen(c ...Component) *Screen { @@ -38,13 +40,12 @@ func NewScreen(c ...Component) *Screen { func (s *Screen) Render() error { Clear() width, height, err := term.GetSize(0) + s.Width = width + s.Height = height if err != nil { return fmt.Errorf("error getting the terminal size: %s", err) } - frame := make([]string, height) - for i := 0; i < height; i++ { - frame[i] = strings.Repeat(" ", width) - } + frame := NewFrame(width, height) for _, component := range s.components { RenderOnFrame(component, &frame) } diff --git a/styles/styles.go b/styles/styles.go new file mode 100644 index 0000000..98ab190 --- /dev/null +++ b/styles/styles.go @@ -0,0 +1,36 @@ +package styles + +import "fmt" + +const ( + Reset = "\033[0m" + Bold = "\033[1m" + Underline = "\033[4m" + Italic = "\033[3m" + Reverse = "\033[7m" + Strike = "\033[9m" + + // Font Colors + Black = "\033[30m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + White = "\033[37m" + + // Background Colors + BgBlack = "\033[40m" + BgRed = "\033[41m" + BgGreen = "\033[42m" + BgYellow = "\033[43m" + BgBlue = "\033[44m" + BgMagenta = "\033[45m" + BgCyan = "\033[46m" + BgWhite = "\033[47m" +) + +func Rgb(r, g, b uint8) string { + return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r, g, b) +} diff --git a/ui/components.go b/ui/inputBox.go similarity index 53% rename from ui/components.go rename to ui/inputBox.go index 3008b6f..b17fffb 100644 --- a/ui/components.go +++ b/ui/inputBox.go @@ -7,21 +7,9 @@ import ( "github.com/orus-dev/osui" ) -type TextComponent struct { - Data osui.ComponentData - Text string -} - -func (t *TextComponent) GetComponentData() *osui.ComponentData { - return &t.Data -} - -func (t TextComponent) Render() string { - return t.Text -} - type InputBoxComponent struct { Data osui.ComponentData + cursor string max_size uint InputData string } @@ -32,39 +20,52 @@ func (s *InputBoxComponent) GetComponentData() *osui.ComponentData { func (s InputBoxComponent) Render() string { return fmt.Sprintf( - " %s\n|%s%s|\n %s", + " %s\n|%s%s|\n %s%s", strings.Repeat("_", int(s.max_size)), s.InputData, strings.Repeat(" ", int(s.max_size)-len(s.InputData)), strings.Repeat("‾", int(s.max_size)), + s.cursor, ) } func (s *InputBoxComponent) Read() error { for { - fmt.Printf("\x1B[%d;%dH", s.Data.Y+2, int(s.Data.X+2)+len(s.InputData)) key, err := osui.ReadKey() if err != nil { fmt.Print("\n\n") fmt.Println(err) break } - switch key { - case osui.Key.Enter: + if s.Update(key) { fmt.Print("\n\n") return nil - - case osui.Key.Backspace: - if len(s.InputData) > 0 { - s.InputData = s.InputData[:len(s.InputData)-1] - } - - default: - if int(s.max_size) > len(s.InputData) { - s.InputData += key - } } - s.Data.Screen.Render() } return nil } + +func (s *InputBoxComponent) Update(key string) bool { + // s.cursor = fmt.Sprintf("\x1B[%d;%dH", s.Data.Y+2, int(s.Data.X+2)+len(s.InputData)) + // s.Render() + switch key { + case osui.Key.Enter: + return true + + case osui.Key.Backspace: + if len(s.InputData) > 0 { + s.InputData = s.InputData[:len(s.InputData)-1] + } + + default: + if int(s.max_size) > len(s.InputData) { + s.InputData += key + } + } + s.Data.Screen.Render() + return false +} + +func InputBox(max_size uint) *InputBoxComponent { + return &InputBoxComponent{max_size: max_size} +} diff --git a/ui/paginator.go b/ui/paginator.go new file mode 100644 index 0000000..411ff75 --- /dev/null +++ b/ui/paginator.go @@ -0,0 +1,83 @@ +package ui + +import ( + "fmt" + "strings" + + "github.com/orus-dev/osui" + "github.com/orus-dev/osui/styles" +) + +type PaginatorStyle struct { + Active string `default:"\033[32m"` +} + +type PaginatorComponent struct { + Data osui.ComponentData + Style *PaginatorStyle + Pages []osui.Component + UpdatePages map[int]func(string) bool + ActivePage int +} + +func (p *PaginatorComponent) GetComponentData() *osui.ComponentData { + return &p.Data +} + +func (p PaginatorComponent) Render() string { + pgs := strings.Repeat(" ", p.Data.Screen.Width/2) + for page := range p.Pages { + if page == p.ActivePage { + pgs += p.Style.Active + "•" + styles.Reset + } else { + pgs += styles.Reset + "•" + } + } + frame := osui.NewFrame(p.Data.Screen.Width, p.Data.Screen.Height-3) + osui.RenderOnFrame(p.Pages[p.ActivePage], &frame) + return fmt.Sprintf("%s\n%s", pgs, styles.Reset+strings.Join(frame, "\n")) +} + +func (p *PaginatorComponent) Run() { + for _, c := range p.Pages { + d := c.GetComponentData() + if d.Screen == nil { + d.Screen = p.Data.Screen + } + } + for { + key, _ := osui.ReadKey() + switch key { + case "\x1b[Z": + if p.ActivePage > 0 { + p.ActivePage-- + } else { + p.ActivePage = len(p.Pages) - 1 + } + case osui.Key.Tab: + if p.ActivePage < len(p.Pages)-1 { + p.ActivePage++ + } else { + p.ActivePage = 0 + } + case osui.Key.Escape: + fmt.Print("\n\n") + return + default: + for i, c := range p.UpdatePages { + if i == p.ActivePage { + c(key) + } + } + } + p.Data.Screen.Render() + } +} + +func Paginator(pages ...osui.Component) *PaginatorComponent { + return &PaginatorComponent{ + Pages: pages, + Style: osui.SetDefaults(&PaginatorStyle{}).(*PaginatorStyle), + UpdatePages: make(map[int]func(string) bool), + } +} diff --git a/ui/text.go b/ui/text.go new file mode 100644 index 0000000..6ad666b --- /dev/null +++ b/ui/text.go @@ -0,0 +1,22 @@ +package ui + +import ( + "github.com/orus-dev/osui" +) + +type TextComponent struct { + Data osui.ComponentData + Text string +} + +func (t *TextComponent) GetComponentData() *osui.ComponentData { + return &t.Data +} + +func (t TextComponent) Render() string { + return t.Text +} + +func Text(text string) *TextComponent { + return &TextComponent{Text: text} +} diff --git a/ui/ui.go b/ui/ui.go deleted file mode 100644 index b94e34b..0000000 --- a/ui/ui.go +++ /dev/null @@ -1,9 +0,0 @@ -package ui - -func Text(text string) *TextComponent { - return &TextComponent{Text: text} -} - -func InputBox(max_size uint) *InputBoxComponent { - return &InputBoxComponent{max_size: max_size} -} diff --git a/utils.go b/utils.go index 7c814be..1528c4e 100644 --- a/utils.go +++ b/utils.go @@ -1,8 +1,11 @@ package osui import ( - "fmt" "os" + "os/exec" + "reflect" + "regexp" + "runtime" "strings" "golang.org/x/term" @@ -27,13 +30,36 @@ func renderLine(frameLine, line string, x int) string { func RenderOnFrame(c Component, frame *[]string) { componentData := c.GetComponentData() - for i, line := range strings.Split(c.Render(), "\n") { + for i, lo := range strings.Split(c.Render(), "\n") { if int(componentData.Y)+i < len(*frame) { - (*frame)[int(componentData.Y)+i] = renderLine((*frame)[int(componentData.Y)+i], line, int(componentData.X)) + fo := (*frame)[int(componentData.Y)+i] + f, fa := CompressString(fo, "\b") + line, la := CompressString(lo, "\t") + r := renderLine(f, line, int(componentData.X)) + (*frame)[int(componentData.Y)+i] = DecompressString(DecompressString(r, la, "\t"), fa, "\b") } } } +func CompressString(input, repl string) (string, []string) { + pattern := `\x1b\[([0-9;]*)m` + re := regexp.MustCompile(pattern) + matches := re.FindAllString(input, -1) + return re.ReplaceAllString(input, repl), matches +} + +func DecompressString(modified string, outputArray []string, c string) string { + parts := strings.Split(modified, c) + reconstructed := "" + for i, part := range parts { + reconstructed += part + if i < len(outputArray) { + reconstructed += outputArray[i] + } + } + return reconstructed +} + func ReadKey() (string, error) { oldState, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { @@ -48,8 +74,47 @@ func ReadKey() (string, error) { return string(b[:n]), nil } +func NewFrame(width, height int) []string { + frame := make([]string, height) + for i := 0; i < height; i++ { + frame[i] = strings.Repeat(" ", width) + } + return frame +} + func Clear() { - fmt.Print("\x1b[H\x1b[2J\x1b[3J") + switch runtime.GOOS { + case "windows": + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + cmd.Run() + default: + cmd := exec.Command("clear") + cmd.Stdout = os.Stdout + cmd.Run() + } +} + +func SetDefaults(p interface{}) interface{} { + v := reflect.ValueOf(p) + if v.Kind() != reflect.Ptr { + panic("setDefaults: expected a pointer to a struct") + } + val := v.Elem() + if val.Kind() != reflect.Struct { + panic("setDefaults: expected a pointer to a struct") + } + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + structField := typ.Field(i) + if defaultValue, ok := structField.Tag.Lookup("default"); ok { + if field.Kind() == reflect.String && field.String() == "" { + field.SetString(defaultValue) + } + } + } + return p } type Key_ = string