diff --git a/README.md b/README.md index ffcc0c0..2445e96 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ # osui A Component-based TUI library written in go! + + +```go +package main + +import ( + "github.com/orus-dev/osui/osui" + "github.com/orus-dev/osui/osui/ui" +) + +func main() { + components := osui.NewRender() + components.Add(ui.Text("Hello, World!")) + components.Render() +} +``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6e61cc4 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/orus-dev/osui + +go 1.23.1 + +require ( + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // direct +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b256fe0 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= diff --git a/osui.go b/osui.go new file mode 100644 index 0000000..1f486d9 --- /dev/null +++ b/osui.go @@ -0,0 +1,156 @@ +package osui + +import ( + "fmt" + "os" + "strings" + + "golang.org/x/term" +) + +type Component interface { + Render(*ComponentWrapper) string + Run(*ComponentWrapper, ...any) any +} + +type ComponentWrapper struct { + Component Component + X uint16 + Y uint16 + Render *Render + Update func(*ComponentWrapper, any) UpdateOutput_ + Data string +} + +func (c *ComponentWrapper) Run(a ...any) any { + c.Render.Render() + return c.Component.Run(c, a) +} + +func NewComponent(c Component) *ComponentWrapper { + return &ComponentWrapper{ + Component: c, + } +} + +type Render struct { + components []*ComponentWrapper +} + +func NewRender() *Render { + return &Render{components: []*ComponentWrapper{}} +} + +func (r *Render) Add(c *ComponentWrapper) *ComponentWrapper { + c.Render = r + r.components = append(r.components, c) + return c +} + +func (r *Render) Render() error { + Clear() + width, height, err := term.GetSize(0) + 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) + } + for _, c := range r.components { + for i, line := range strings.Split(c.Component.Render(c), "\n") { + if int(c.Y)+i < len(frame) { + frame[int(c.Y)+i] = renderLine(frame[int(c.Y)+i], line, int(c.X)) + } + } + } + fmt.Print(strings.Join(frame, "\n")) + return nil +} + +func Clear() { + fmt.Print("\x1b[H\x1b[2J\x1b[3J") +} + +func renderLine(frameLine, line string, x int) string { + result := "" + if x >= len(frameLine) { + return result + } + + lChars := []rune(line) + for i, c := range frameLine { + if i >= x && len(lChars) > i-x { + result += string(lChars[i-x]) + } else { + result += string(c) + } + } + return result +} + +func ReadKey() (string, error) { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + defer term.Restore(int(os.Stdin.Fd()), oldState) + var b [3]byte + n, err := os.Stdin.Read(b[:]) + if err != nil { + return "", err + } + return string(b[:n]), nil +} + +func KeyUpdate(keys map[string]UpdateOutput_) func(c *ComponentWrapper, key any) UpdateOutput_ { + return func(c *ComponentWrapper, k any) UpdateOutput_ { + action, ok := keys[fmt.Sprint(k)] + if ok { + return action + } else { + return UpdateOutput.Continue + } + } +} + +type UpdateOutput_ = int + +type updateOutput struct { + /*Continue the default check*/ + Continue UpdateOutput_ + /*Jump to the next check*/ + Jmp UpdateOutput_ + /*Leave the loop and stop the function*/ + Exit UpdateOutput_ +} + +var UpdateOutput = updateOutput{Continue: 0, Jmp: 1, Exit: 2} + +/* +Keys +*/ + +type Key_ = string + +type key struct { + Enter Key_ + Tab Key_ + Backspace Key_ + Escape Key_ + Up Key_ + Down Key_ + Right Key_ + Left Key_ +} + +var Key = key{ + Enter: "\r", + Tab: "\t", + Backspace: "\x7f", + Escape: "\x1b", + Up: "\x1b[A", + Down: "\x1b[B", + Right: "\x1b[C", + Left: "\x1b[D", +} diff --git a/ui/components.go b/ui/components.go new file mode 100644 index 0000000..7931883 --- /dev/null +++ b/ui/components.go @@ -0,0 +1,70 @@ +package ui + +import ( + "fmt" + "strings" + + "github.com/orus-dev/osui" +) + +type ComponentText struct{ Text string } + +func (t ComponentText) Render(cw *osui.ComponentWrapper) string { + return t.Text +} + +func (t ComponentText) Run(cw *osui.ComponentWrapper, a ...any) any { + return nil +} + +type ComponentInputBox struct { + max_size uint +} + +func (s ComponentInputBox) Render(cw *osui.ComponentWrapper) string { + return fmt.Sprintf( + " %s\n|%s%s|\n %s", + strings.Repeat("_", int(s.max_size)), + cw.Data, + strings.Repeat(" ", int(s.max_size)-len(cw.Data)), + strings.Repeat("‾", int(s.max_size)), + ) +} + +func (s ComponentInputBox) Run(cw *osui.ComponentWrapper, a ...any) any { + for { + fmt.Printf("\x1B[%d;%dH", cw.Y+2, int(cw.X+2)+len(cw.Data)) + key, err := osui.ReadKey() + if err != nil { + fmt.Println(err) + break + } + switch cw.Update(cw, key) { + + case osui.UpdateOutput.Jmp: + continue + case osui.UpdateOutput.Exit: + fmt.Print("\n\n") + return cw.Data + + } + switch key { + + case osui.Key.Enter: + fmt.Print("\n\n") + return cw.Data + + case osui.Key.Backspace: + if len(cw.Data) > 0 { + cw.Data = cw.Data[:len(cw.Data)-1] + } + + default: + if int(s.max_size) > len(cw.Data) { + cw.Data += key + } + } + cw.Render.Render() + } + return cw.Data +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..9e99e1c --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,13 @@ +package ui + +import ( + "github.com/orus-dev/osui" +) + +func InputBox(max_size uint) *osui.ComponentWrapper { + return osui.NewComponent(ComponentInputBox{max_size: max_size}) +} + +func Text(text string) *osui.ComponentWrapper { + return osui.NewComponent(ComponentText{Text: text}) +}