32 Commits
Author SHA1 Message Date
selimaj-dev 553804ec51 a few more changes 2024-10-13 16:32:05 +02:00
selimaj-dev 905bb6711e updated structure 2024-10-12 22:36:20 +02:00
selimaj-dev 8ea250c1b1 updated structure 2024-10-12 22:36:09 +02:00
selimaj-dev 039bc27999 updated structure 2024-10-12 17:49:55 +02:00
selimaj-dev d4f6c4104d fixed flickering on all platforms 2024-10-04 22:55:30 +02:00
selimaj-dev 0f052d7652 perfomance boost 2024-10-03 20:02:56 +02:00
selimaj-dev f2f7e51750 small performance increase 2024-10-03 19:29:18 +02:00
selimaj-dev 7e714abddf fixed color bug 2024-10-03 13:19:43 +02:00
selimaj-dev 3ceaa13b3f fixed color bug 2024-10-03 13:10:43 +02:00
selimaj-dev 8165e80b05 fixed ui when horizontal but color bug 2024-10-01 19:53:19 +02:00
selimaj-dev f77c04c908 fixed ui when horizontal 2024-10-01 16:37:54 +02:00
selimaj-dev 27530fe8cc fixed ui when horizontal 2024-10-01 16:34:50 +02:00
selimaj-dev 705bd8d61b ids and classes 2024-09-30 14:10:00 +02:00
RudideC bf70ceacf1 this seems to fix an issue i was facing 2024-09-30 10:10:32 +02:00
selimaj-dev 6cdb685eb5 small change 2024-09-29 22:13:54 +02:00
selimaj-dev cdb8576cc4 readme update 2024-09-29 22:11:05 +02:00
selimaj-dev b2b5c9c00d windows bug fix 02 2024-09-29 21:52:17 +02:00
selimaj-dev dd8f1fbc1f bug fixes and clean code 2024-09-29 21:22:25 +02:00
Rudi e873c01e50 wow 2024-09-29 17:35:30 +02:00
selimaj-dev 3bab6d3cd6 readme update 2024-09-29 14:39:33 +02:00
selimaj-dev 06f9e4577c readme update 2024-09-29 14:39:25 +02:00
selimaj-dev 716d1d4a08 First version 2024-09-29 13:40:46 +02:00
selimaj-dev 8592b1cc50 menus 2024-09-28 19:29:01 +02:00
selimaj-dev 90f106f127 buttons and styles 2024-09-28 17:12:45 +02:00
selimaj-dev 5a94fc34ad bugs fixed 2024-09-27 21:48:28 +02:00
selimaj-dev fdbb54a557 renderLine fine length 2024-09-27 20:51:22 +02:00
selimaj-dev 5024087358 renderLine fine length 2024-09-27 20:50:15 +02:00
selimaj-dev cf93b3383f bug fix (There is a small bug on inputBox again) 2024-09-27 14:38:39 +02:00
selimaj-dev 3b2ff24c1f bug fix but still a new one :( 2024-09-26 23:03:24 +02:00
selimaj-dev 4214f96c61 bug fix but still a new one :( 2024-09-26 23:03:17 +02:00
RudideC 3b3e92fae5 fix bug but now new bug hehe
fixed the bug where the "some text here" example would have spaces inserted inside when typing in a textbox
2024-09-26 19:57:20 +02:00
selimaj-dev 179de20dba styling 2024-09-26 17:49:03 +02:00
21 changed files with 899 additions and 329 deletions
+3
View File
@@ -23,3 +23,6 @@ go.work.sum
# env file
.env
# macOS
.DS_Store
+19 -7
View File
@@ -1,18 +1,30 @@
# osui
A Component-based TUI library written in go!
<div style="text-align: center;">
<img src="assets/osui.png" width="170px" />
<h1>A Component-based TUI library written in Go</h1>
<img src="assets/osui.gif" />
</div>
# Hello World
```go
package main
import (
"github.com/orus-dev/osui/osui"
"github.com/orus-dev/osui/osui/ui"
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/ui"
)
func main() {
components := osui.NewRender()
components.Add(ui.Text("Hello, World!"))
components.Render()
app := ui.Div(
ui.Text("Hello, World!")
)
screen := osui.NewScreen(app)
screen.Run()
}
```
# [Documentation](https://github.com/orus-dev/osui/wiki)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+59
View File
@@ -0,0 +1,59 @@
package colors
import (
"fmt"
"regexp"
"strings"
)
const (
Reset = "\x1b[0m"
Bold = "\x1b[1m"
Underline = "\x1b[4m"
Italic = "\x1b[3m"
Reverse = "\x1b[7m"
Strike = "\x1b[9m"
Black = "\x1b[30m"
Red = "\x1b[31m"
Green = "\x1b[32m"
Yellow = "\x1b[33m"
Blue = "\x1b[34m"
Magenta = "\x1b[35m"
Cyan = "\x1b[36m"
White = "\x1b[37m"
)
func Rgb(r, g, b uint8) string {
return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r, g, b)
}
func AsBg(s string) string {
re, _ := regexp.Compile(`\x1b\[3([0-9]+)`)
return re.ReplaceAllStringFunc(s, func(match string) string {
groups := re.FindStringSubmatch(match)
if len(groups) >= 2 {
return fmt.Sprintf("\x1b[4%s", groups[1])
}
return match
})
}
func AsFg(s string) string {
re, _ := regexp.Compile(`\x1b\[4([0-9]+)`)
return re.ReplaceAllStringFunc(s, func(match string) string {
groups := re.FindStringSubmatch(match)
if len(groups) >= 2 {
return fmt.Sprintf("\x1b[3%s", groups[1])
}
return match
})
}
func Combine(s, s1 string) string {
if strings.Contains(s, Reset) {
return s + s1
} else {
return s1 + s
}
}
+126
View File
@@ -0,0 +1,126 @@
package osui
import (
"github.com/orus-dev/osui/colors"
)
type Style struct {
Background string `json:"background"`
Foreground string `json:"foreground"`
Outline string `json:"outline"`
ActiveBackground string `json:"activeBackground"`
ActiveForeground string `json:"activeForeground"`
ActiveOutline string `json:"activeOutline"`
ClickedBackground string `json:"clickedBackground"`
ClickedForeground string `json:"clickedForeground"`
ClickedOutline string `json:"clickedOutline"`
SelectedBackground string `json:"selectedBackground"`
SelectedForeground string `json:"selectedForeground"`
SelectedOutline string `json:"selectedOutline"`
Cursor string `json:"cursor"`
CursorColor string `json:"cursorColor"`
InactiveCursor string `json:"inactiveCursor"`
}
func (s *Style) SetDefaults() {
defaultValue(&s.Cursor, "> ")
defaultValue(&s.SelectedForeground, colors.Black)
defaultValue(&s.SelectedBackground, colors.Magenta)
defaultValue(&s.ActiveOutline, colors.Blue)
defaultValue(&s.ClickedForeground, colors.Blue)
defaultValue(&s.ClickedOutline, colors.Blue)
s.UseStyle()
}
func (s *Style) UseStyle() {
s.Foreground = colors.AsFg(s.Foreground)
s.Background = colors.AsBg(s.Background)
s.Outline = colors.AsFg(s.Outline)
s.ActiveForeground = colors.AsFg(s.ActiveForeground)
s.ActiveBackground = colors.AsBg(s.ActiveBackground)
s.ActiveOutline = colors.AsFg(s.ActiveOutline)
s.ClickedForeground = colors.AsFg(s.ClickedForeground)
s.ClickedBackground = colors.AsBg(s.ClickedBackground)
s.ClickedOutline = colors.AsFg(s.ClickedOutline)
s.SelectedForeground = colors.AsFg(s.SelectedForeground)
s.SelectedBackground = colors.AsBg(s.SelectedBackground)
s.SelectedOutline = colors.AsFg(s.SelectedOutline)
}
func defaultValue(sd *string, s string) {
if *sd == "" {
*sd = s
}
}
type Param struct {
Style Style `json:"style"`
X int `json:"y"`
Y int `json:"x"`
Width int `json:"width"`
Height int `json:"height"`
OnClick func()
Keys map[string]string
Toggle bool `json:"useToggle"`
}
func (p *Param) SetDefaultBindings(keys map[string]string) {
p.Style.SetDefaults()
if p.Keys == nil {
p.Keys = keys
return
}
for k, f := range keys {
if _, ok := p.Keys[k]; !ok {
p.Keys[k] = f
}
}
}
func (p *Param) UseParam(c Component) Component {
data := c.GetComponentData()
p.Style.SetDefaults()
data.Style = p.Style
data.X = p.X
data.Y = p.Y
if p.Height != 0 {
data.Height = p.Height
}
if p.Width != 0 {
data.Width = p.Width
}
if p.OnClick != nil {
data.OnClick = p.OnClick
} else {
data.OnClick = func() {}
}
data.Toggle = p.Toggle
data.Keys = p.Keys
return c
}
type ComponentData struct {
X int
Y int
Width int
Height int
DefaultColor string
IsActive bool
Style Style
OnClick func()
Keys map[string]string
Toggle bool
Screen *Screen
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/ui"
)
func main() {
// screen := osui.NewScreen()
// my_btn := ui.Button(osui.Param{},
// "Button",
// )
// screen.Run(my_btn)
// var menu ui.Id[*ui.MenuComponent]
// screen := osui.NewScreen()
// screen.Run(ui.Div(osui.Param{},
// menu.Id(ui.Menu(osui.Param{OnClick: func() {
// screen.SetComponent(ui.Text(osui.Param{}, menu.Component.Items[menu.Component.SelectedItem]))
// screen.Exit()
// }},
// "Item 1", "Item 24324322", "Item 366",
// )),
// ))
// screen := osui.NewScreen()
// my_btn := ui.InputBox(osui.Param{},
// 24,
// )
// screen.Run(my_btn)
screen := osui.NewScreen()
my_btn := ui.Paginator(osui.Param{OnClick: func() {screen.Exit()}},
ui.Div(osui.Param{},
ui.Text(osui.Param{}, "Page 1"),
),
ui.Div(osui.Param{},
ui.Text(osui.Param{}, "Page 2"),
),
)
screen.Run(my_btn)
}
-16
View File
@@ -1,16 +0,0 @@
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!" + styles.Reset,
)
paginator := ui.Paginator(ui.Div(colors), ui.Div(ui.InputBox(30)))
screen := osui.NewScreen(paginator)
screen.Run()
}
+3 -1
View File
@@ -2,7 +2,9 @@ module github.com/orus-dev/osui
go 1.23.1
require golang.org/x/term v0.24.0 // direct
require (
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // direct
)
+2
View File
@@ -1,3 +1,5 @@
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d h1:NqRhLdNVlozULwM1B3VaHhcXYSgrOAv8V5BE65om+1Q=
github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d/go.mod h1:cxIIfNMTwff8f/ZvRouvWYF6wOoO7nj99neWSx2q/Es=
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=
+55
View File
@@ -0,0 +1,55 @@
package keys
const (
Enter = "Enter"
Tab = "Tab"
ShiftTab = "ShiftTab"
Backspace = "Backspace"
Escape = "Escape"
Up = "Up"
Down = "Down"
Left = "Left"
Right = "Right"
CtrlW = "CtrlW"
CtrlS = "CtrlS"
CtrlA = "CtrlA"
CtrlD = "CtrlD"
Error = "Error"
)
func Char(key string) string {
return "Char(" + key + ")"
}
func GetKeyName(key string) string {
switch key {
case "\r", "\n":
return Enter
case "\x7f", "\b":
return Backspace
case "\t":
return Tab
case "\x1b[Z":
return ShiftTab
case "\x1b":
return Escape
case "\x1b[A":
return Up
case "\x1b[B":
return Down
case "\x1b[C":
return Right
case "\x1b[D":
return Left
case "\x17":
return CtrlW
case "\x13":
return CtrlS
case "\x01":
return CtrlA
case "\x04":
return CtrlD
default:
return Char(key)
}
}
+78 -34
View File
@@ -2,62 +2,106 @@ package osui
import (
"fmt"
"os"
"strings"
"time"
"github.com/orus-dev/osui/colors"
"golang.org/x/term"
)
type ComponentData struct {
X uint16
Y uint16
Width int
Height int
IsActive bool
Screen *Screen
type Key struct {
Chars [3]rune
Name string
}
type UpdateContext struct {
UpdateKind uint8
Tick uint8
Key Key
}
type Component interface {
Render() string
GetComponentData() *ComponentData
Update(string) bool
Update(UpdateContext) bool
}
type Screen struct {
component Component
Component Component
tickRate uint8
running bool
}
func NewScreen(c Component) *Screen {
HideCursor()
s := &Screen{component: c}
return s
func NewScreen() *Screen {
s := Screen{}
s.tickRate = 1
return &s
}
func (s *Screen) Render() error {
Clear()
width, height, err := term.GetSize(0)
if err != nil {
return fmt.Errorf("error getting the terminal size: %s", err)
func (s *Screen) Render() {
width, height := GetTerminalSize()
data := s.Component.GetComponentData()
if data.Height == 0 {
data.Height = height
}
if data.Width == 0 {
data.Width = width
}
frame := NewFrame(width, height)
data := s.component.GetComponentData()
data.Width = width
data.Height = height
data.Screen = s
data.IsActive = true
RenderOnFrame(s.component, &frame)
fmt.Print(strings.Join(frame, "\n"))
return nil
RenderOnFrame(s.Component, &frame)
Clear()
fmt.Print(strings.Join(frame, ""))
}
func (s *Screen) Run() {
s.component.GetComponentData().Screen = s
s.component.Update("OSUI:RENDER")
for {
func (s *Screen) Run(c Component) {
s.SetComponent(c)
HideCursor()
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
s.running = true
for s.running {
s.Render()
k, _ := ReadKey()
if s.component.Update(k) {
ShowCursor()
return
s.Component.Update(UpdateContext{UpdateKind: UpdateKindKey, Key: ReadKey()})
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
}
func (s *Screen) _() {
var tick uint8 = 0
for s.running {
s.Component.Update(UpdateContext{UpdateKind: UpdateKindTick, Tick: tick})
if tick == 255 {
tick = 0
}
time.Sleep(time.Duration(1000/int(s.tickRate)) * time.Millisecond)
}
}
func (s *Screen) SetComponent(c Component) {
s.Component = c
width, height := GetTerminalSize()
data := s.Component.GetComponentData()
if data.Height == 0 {
data.Height = height
}
if data.Width == 0 {
data.Width = width
}
data.Screen = s
data.IsActive = true
data.DefaultColor = colors.Reset
}
func (s *Screen) Exit() {
s.running = false
ShowCursor()
}
const (
UpdateKindKey = uint8(iota)
UpdateKindTick
)
-36
View File
@@ -1,36 +0,0 @@
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)
}
+86
View File
@@ -0,0 +1,86 @@
package ui
import (
"fmt"
"strings"
"time"
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/colors"
"github.com/orus-dev/osui/keys"
)
type ButtonComponent struct {
Data osui.ComponentData
Text string
clicked bool
}
func (b *ButtonComponent) Render() string {
b.Data.Style.UseStyle()
if b.clicked {
return fmt.Sprintf(" %s\n%s│%s│%s\n %s",
colors.Reset+b.Data.Style.ClickedOutline+strings.Repeat("_", b.Data.Width-2)+colors.Reset+b.Data.DefaultColor,
colors.Reset+b.Data.Style.ClickedOutline,
colors.Reset+colors.Combine(b.Data.Style.ClickedBackground, b.Data.Style.ClickedForeground)+centerText(b.Text, b.Data.Width-2)+colors.Reset+b.Data.Style.ClickedOutline,
colors.Reset+b.Data.DefaultColor,
colors.Reset+b.Data.Style.ClickedOutline+strings.Repeat("‾", b.Data.Width-2)+colors.Reset+b.Data.DefaultColor,
)
}
if b.Data.IsActive {
return fmt.Sprintf(" %s\n%s│%s│%s\n %s",
colors.Reset+b.Data.Style.ActiveOutline+strings.Repeat("_", b.Data.Width-2)+colors.Reset+b.Data.DefaultColor,
colors.Reset+b.Data.Style.ActiveOutline,
colors.Reset+colors.Combine(b.Data.Style.ActiveBackground, b.Data.Style.ActiveForeground)+centerText(b.Text, b.Data.Width-2)+colors.Reset+b.Data.Style.ActiveOutline,
colors.Reset+b.Data.DefaultColor,
colors.Reset+b.Data.Style.ActiveOutline+strings.Repeat("‾", b.Data.Width-2)+colors.Reset+b.Data.DefaultColor,
)
}
return fmt.Sprintf(" %s\n%s│%s│%s\n %s",
colors.Reset+b.Data.Style.Outline+strings.Repeat("_", b.Data.Width-2)+colors.Reset+b.Data.DefaultColor,
colors.Reset+b.Data.Style.Outline,
colors.Reset+colors.Combine(b.Data.Style.Background, b.Data.Style.Foreground)+centerText(b.Text, b.Data.Width-2)+colors.Reset+b.Data.Style.Outline,
colors.Reset+b.Data.DefaultColor,
colors.Reset+b.Data.Style.Outline+strings.Repeat("‾", b.Data.Width-2)+colors.Reset+b.Data.DefaultColor,
)
}
func (b *ButtonComponent) Update(ctx osui.UpdateContext) bool {
if ctx.UpdateKind == osui.UpdateKindKey {
switch b.Data.Keys[ctx.Key.Name] {
case "click":
b.Data.OnClick()
if b.Data.Toggle {
b.clicked = !b.clicked
return false
}
b.clicked = true
b.Data.Screen.Render()
time.Sleep(time.Millisecond * 120)
b.clicked = false
}
} else if ctx.UpdateKind == osui.UpdateKindTick {
}
return false
}
func (b *ButtonComponent) GetComponentData() *osui.ComponentData {
return &b.Data
}
func Button(param osui.Param, text string) *ButtonComponent {
param.SetDefaultBindings(map[string]string{
keys.Enter: "click",
})
return param.UseParam(&ButtonComponent{Text: text,
Data: osui.ComponentData{
Width: 20,
Height: 1,
},
clicked: false,
}).(*ButtonComponent)
}
+63 -47
View File
@@ -4,6 +4,8 @@ import (
"strings"
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/colors"
"github.com/orus-dev/osui/keys"
)
type DivComponent struct {
@@ -12,70 +14,84 @@ type DivComponent struct {
ActiveComponent int
}
func (p *DivComponent) GetComponentData() *osui.ComponentData {
return &p.Data
func (d *DivComponent) GetComponentData() *osui.ComponentData {
return &d.Data
}
func (p DivComponent) Render() string {
frame := osui.NewFrame(p.Data.Width, p.Data.Height)
for _, c := range p.Components {
func (d *DivComponent) Render() string {
d.Data.Style.UseStyle()
frame := osui.NewFrame(d.Data.Width-2, d.Data.Height-2)
for i, c := range d.Components {
data := c.GetComponentData()
data.Width = p.Data.Width
data.Height = p.Data.Height
data.Screen = p.Data.Screen
if i == d.ActiveComponent {
data.IsActive = d.Data.IsActive
} else {
data.IsActive = false
}
if data.Width == 0 {
data.Width = d.Data.Width - 2
}
if data.Height == 0 {
data.Height = d.Data.Height - 2
}
data.DefaultColor = colors.Combine(d.Data.Style.Background, d.Data.Style.Foreground)
data.Screen = d.Data.Screen
osui.RenderOnFrame(c, &frame)
}
return strings.Join(frame, "\n")
}
func (p *DivComponent) Run() {
for _, c := range p.Components {
d := c.GetComponentData()
d.Screen = p.Data.Screen
}
for {
key, _ := osui.ReadKey()
if p.Update(key) {
return
if d.Data.Style.Outline == "" {
for i, f := range frame {
frame[i] = colors.Combine(d.Data.Style.Foreground, d.Data.Style.Background) + f + colors.Reset
}
return strings.Join(frame, "\n")
} else {
for i, f := range frame {
frame[i] = d.Data.Style.Outline + "│" + colors.Reset + colors.Combine(d.Data.Style.Foreground, d.Data.Style.Background) + f + colors.Reset + d.Data.Style.Outline + "│" + colors.Reset
}
}
return " " + d.Data.Style.Outline + strings.Repeat("_", d.Data.Width-2) + colors.Reset + "\n" + strings.Join(frame, "\n") + "\n " + d.Data.Style.Outline + strings.Repeat("‾", d.Data.Width-2) + colors.Reset
}
func (p *DivComponent) Update(key string) bool {
switch key {
case "":
p.updateActive(p.ActiveComponent)
case osui.Key.Up:
p.updateActive(p.ActiveComponent - 1)
case osui.Key.Down:
p.updateActive(p.ActiveComponent + 1)
default:
if len(p.Components) > 0 {
p.Components[p.ActiveComponent].GetComponentData().IsActive = p.Data.IsActive
if p.Components[p.ActiveComponent].Update(key) {
if p.ActiveComponent < len(p.Components)-1 {
p.updateActive(p.ActiveComponent + 1)
} else {
return true
}
func (d *DivComponent) Update(ctx osui.UpdateContext) bool {
if ctx.UpdateKind == osui.UpdateKindKey {
switch d.Data.Keys[ctx.Key.Name] {
case "up":
d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "up"))
case "down":
d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "down"))
case "left":
d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "left"))
case "right":
d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "right"))
default:
d.Components[d.ActiveComponent].GetComponentData().IsActive = d.Data.IsActive
if d.Components[d.ActiveComponent].Update(ctx) {
d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "down"))
}
}
} else if ctx.UpdateKind == osui.UpdateKindTick {
d.Components[d.ActiveComponent].GetComponentData().IsActive = d.Data.IsActive
if d.Components[d.ActiveComponent].Update(ctx) {
d.updateActive(findClosestComponent(d.Components, d.ActiveComponent, "down"))
}
}
return false
}
func (p *DivComponent) updateActive(newIndex int) {
if newIndex >= 0 && newIndex < len(p.Components) && len(p.Components) > 0 {
p.Components[p.ActiveComponent].GetComponentData().IsActive = false
p.ActiveComponent = newIndex
p.Components[p.ActiveComponent].GetComponentData().IsActive = p.Data.IsActive
p.Components[p.ActiveComponent].Update("")
func (d *DivComponent) updateActive(newIndex int) {
if newIndex >= 0 && newIndex < len(d.Components) && len(d.Components) > 0 {
d.ActiveComponent = newIndex
}
}
func Div(components ...osui.Component) *DivComponent {
return &DivComponent{
func Div(param osui.Param, components ...osui.Component) *DivComponent {
param.SetDefaultBindings(map[string]string{
keys.CtrlW: "up",
keys.CtrlS: "down",
keys.CtrlA: "left",
keys.CtrlD: "right",
})
return param.UseParam(&DivComponent{
Components: components,
}
}).(*DivComponent)
}
+50 -41
View File
@@ -5,11 +5,14 @@ import (
"strings"
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/colors"
"github.com/orus-dev/osui/keys"
)
type InputBoxComponent struct {
Data osui.ComponentData
max_size uint
cursor uint
InputData string
}
@@ -18,59 +21,65 @@ func (s *InputBoxComponent) GetComponentData() *osui.ComponentData {
}
func (s InputBoxComponent) Render() string {
s.Data.Style.UseStyle()
if s.max_size > uint(len(s.InputData)) {
return fmt.Sprintf(
" %s\n|%s%s|\n %s",
strings.Repeat("_", int(s.max_size)),
s.InputData+osui.LogicValue(s.Data.IsActive, "█", ""),
strings.Repeat(" ", int(s.max_size)-len(s.InputData)-osui.LogicValueInt(s.Data.IsActive, 1, 0)),
strings.Repeat("‾", int(s.max_size)),
" %s\n%s│%s%s│%s\n %s",
colors.Reset+s.Data.Style.Outline+strings.Repeat("_", int(s.max_size))+colors.Reset,
colors.Reset+s.Data.Style.Outline,
colors.Combine(s.Data.Style.Foreground, s.Data.Style.Background)+s.InputData+osui.LogicValue(s.Data.IsActive, s.Data.Style.CursorColor+"█"+colors.Combine(s.Data.Style.Foreground, s.Data.Style.Background), ""),
strings.Repeat(" ", int(s.max_size)-len(s.InputData)-osui.LogicValueInt(s.Data.IsActive, 1, 0))+colors.Reset+s.Data.Style.Outline,
colors.Reset+s.Data.DefaultColor,
s.Data.Style.Outline+strings.Repeat("‾", int(s.max_size))+colors.Reset+s.Data.DefaultColor,
)
}
return fmt.Sprintf(
" %s\n|%s\n %s",
strings.Repeat("_", int(s.max_size)),
s.InputData+osui.LogicValue(s.Data.IsActive, "█", "|"),
strings.Repeat("‾", int(s.max_size)),
" %s\n%s│%s%s\n %s",
colors.Reset+s.Data.Style.Outline+strings.Repeat("_", int(s.max_size))+colors.Reset,
colors.Reset+s.Data.Style.Outline,
colors.Combine(s.Data.Style.Foreground, s.Data.Style.Background)+s.InputData+osui.LogicValue(s.Data.IsActive, s.Data.Style.CursorColor+"█"+colors.Reset, s.Data.Style.Outline+"|"+colors.Reset),
colors.Reset+s.Data.DefaultColor,
colors.Reset+s.Data.Style.Outline+strings.Repeat("‾", int(s.max_size))+colors.Reset+s.Data.DefaultColor,
)
}
func (s *InputBoxComponent) Read() error {
for {
key, err := osui.ReadKey()
if err != nil {
fmt.Print("\n\n")
fmt.Println(err)
break
}
if s.Update(key) {
fmt.Print("\n\n")
return nil
}
}
return nil
}
func (s *InputBoxComponent) Update(key string) bool {
switch key {
case "":
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 len(key) == 1 {
if int(s.max_size) > len(s.InputData) {
s.InputData += key
func (s *InputBoxComponent) Update(ctx osui.UpdateContext) bool {
if ctx.UpdateKind == osui.UpdateKindKey {
switch s.Data.Keys[ctx.Key.Name] {
case "done":
s.Data.OnClick()
return true
case "remove":
if len(s.InputData) > 0 {
s.InputData = s.InputData[:len(s.InputData)-1]
}
case "left":
if s.cursor > 0 {
s.cursor--
}
case "right":
if s.cursor < s.max_size {
s.cursor++
}
default:
if ctx.Key.Chars[1] == '\x00' {
if int(s.max_size) > len(s.InputData) {
s.InputData += string(ctx.Key.Chars[0])
}
}
}
}
return false
}
func InputBox(max_size uint) *InputBoxComponent {
return &InputBoxComponent{max_size: max_size}
func InputBox(param osui.Param, max_size uint) *InputBoxComponent {
param.SetDefaultBindings(map[string]string{
keys.Enter: "done",
keys.Backspace: "remove",
keys.Left: "left",
keys.Right: "right",
})
return param.UseParam(&InputBoxComponent{max_size: max_size, Data: osui.ComponentData{}}).(*InputBoxComponent)
}
+78
View File
@@ -0,0 +1,78 @@
package ui
import (
"strings"
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/colors"
"github.com/orus-dev/osui/keys"
)
type MenuComponent struct {
Data osui.ComponentData
Items []string
SelectedItem int
}
func (m *MenuComponent) GetComponentData() *osui.ComponentData {
return &m.Data
}
func (m *MenuComponent) Render() string {
m.Data.Style.UseStyle()
res := []string{}
cursor := m.Data.Style.Cursor
if !m.Data.IsActive {
cursor = m.Data.Style.InactiveCursor
}
d, _ := osui.CompressString(m.Data.Style.Cursor)
empty := strings.Repeat(" ", len(d))
for i, item := range m.Items {
if i == m.SelectedItem {
res = append(res, colors.Reset+cursor+m.Data.Style.SelectedBackground+m.Data.Style.SelectedForeground+item+"\x1b[0m ")
} else {
res = append(res, colors.Reset+empty+m.Data.Style.Foreground+m.Data.Style.Background+item)
}
}
return strings.Join(res, "\n")
}
func (m *MenuComponent) Update(ctx osui.UpdateContext) bool {
if ctx.UpdateKind == osui.UpdateKindKey {
switch m.Data.Keys[ctx.Key.Name] {
case "up":
if m.SelectedItem > 0 {
m.SelectedItem--
} else {
m.SelectedItem = len(m.Items) - 1
}
case "down":
if m.SelectedItem+1 < len(m.Items) {
m.SelectedItem++
} else {
m.SelectedItem = 0
}
case "select":
m.Data.OnClick()
return true
case "exit":
return true
}
}
return false
}
func Menu(param osui.Param, items ...string) *MenuComponent {
param.SetDefaultBindings(map[string]string{
keys.Up: "up",
keys.Down: "down",
keys.Enter: "select",
})
return param.UseParam(&MenuComponent{
Items: items,
}).(*MenuComponent)
}
+61 -53
View File
@@ -5,16 +5,12 @@ import (
"strings"
"github.com/orus-dev/osui"
"github.com/orus-dev/osui/styles"
"github.com/orus-dev/osui/colors"
"github.com/orus-dev/osui/keys"
)
type PaginatorStyle struct {
Active string `default:"\033[32m"`
}
type PaginatorComponent struct {
Data osui.ComponentData
Style *PaginatorStyle
Components []osui.Component
ActiveComponent int
}
@@ -23,53 +19,64 @@ func (p *PaginatorComponent) GetComponentData() *osui.ComponentData {
return &p.Data
}
func (p PaginatorComponent) Render() string {
pgs := strings.Repeat(" ", p.Data.Width/2)
for page, c := range p.Components {
if page == p.ActiveComponent {
pgs += p.Style.Active + "•" + styles.Reset
func (p *PaginatorComponent) Render() string {
width, _ := osui.GetTerminalSize()
pgs := strings.Repeat(" ", (width-len(p.Components))/2)
p.Data.Style.UseStyle()
frame := osui.NewFrame(p.Data.Width, p.Data.Height)
for i, c := range p.Components {
data := c.GetComponentData()
if i == p.ActiveComponent {
pgs += p.Data.Style.SelectedForeground + data.Style.SelectedBackground + "•" + colors.Reset
data.IsActive = true
} else {
pgs += styles.Reset + "•"
pgs += colors.Reset + p.Data.Style.Foreground + data.Style.Background + "•"
data.IsActive = false
}
d := c.GetComponentData()
if d.Screen == nil {
d.Screen = p.Data.Screen
if data.Width == 0 {
data.Width = p.Data.Width
}
d.Height = p.Data.Height - 1
d.Width = p.Data.Width
if data.Height == 0 {
data.Height = p.Data.Height
}
data.DefaultColor = p.Data.DefaultColor
data.Screen = p.Data.Screen
}
frame := osui.NewFrame(p.Data.Width, p.Data.Height-3)
osui.RenderOnFrame(p.Components[p.ActiveComponent], &frame)
return fmt.Sprintf("%s\n%s", pgs, styles.Reset+strings.Join(frame, "\n"))
for i, f := range frame {
frame[i] = colors.Reset + p.Data.DefaultColor + f + colors.Reset
}
return fmt.Sprintf("%s\n%s", pgs+" ", colors.Reset+strings.Join(frame, "\n"))
}
func (p *PaginatorComponent) Update(key string) bool {
switch key {
case "":
p.updateActive(p.ActiveComponent)
case "\x1b[Z":
if p.ActiveComponent > 0 {
p.updateActive(p.ActiveComponent - 1)
} else {
p.updateActive(len(p.Components) - 1)
}
case osui.Key.Tab:
if p.ActiveComponent < len(p.Components)-1 {
p.updateActive(p.ActiveComponent + 1)
} else {
p.updateActive(0)
}
case osui.Key.Escape:
fmt.Print("\n\n")
return true
default:
if len(p.Components) > 0 {
p.Components[p.ActiveComponent].GetComponentData().IsActive = p.Data.IsActive
if p.Components[p.ActiveComponent].Update(key) {
if p.ActiveComponent < len(p.Components)-1 {
p.updateActive(p.ActiveComponent + 1)
} else {
p.updateActive(0)
func (p *PaginatorComponent) Update(ctx osui.UpdateContext) bool {
if ctx.UpdateKind == osui.UpdateKindKey {
switch p.Data.Keys[ctx.Key.Name] {
case "previous":
if p.ActiveComponent > 0 {
p.updateActive(p.ActiveComponent - 1)
} else {
p.updateActive(len(p.Components) - 1)
}
case "next":
if p.ActiveComponent < len(p.Components)-1 {
p.updateActive(p.ActiveComponent + 1)
} else {
p.updateActive(0)
}
case "exit":
p.Data.OnClick()
fmt.Print("\n\n")
return true
default:
if len(p.Components) > 0 {
p.Components[p.ActiveComponent].GetComponentData().IsActive = p.Data.IsActive
if p.Components[p.ActiveComponent].Update(ctx) {
if p.ActiveComponent < len(p.Components)-1 {
p.updateActive(p.ActiveComponent + 1)
} else {
p.updateActive(0)
}
}
}
}
@@ -79,16 +86,17 @@ func (p *PaginatorComponent) Update(key string) bool {
func (p *PaginatorComponent) updateActive(newIndex int) {
if newIndex >= 0 && newIndex < len(p.Components) && len(p.Components) > 0 {
p.Components[p.ActiveComponent].GetComponentData().IsActive = false
p.ActiveComponent = newIndex
p.Components[p.ActiveComponent].GetComponentData().IsActive = p.Data.IsActive
p.Components[p.ActiveComponent].Update("")
}
}
func Paginator(pages ...osui.Component) *PaginatorComponent {
return &PaginatorComponent{
func Paginator(param osui.Param, pages ...osui.Component) *PaginatorComponent {
param.SetDefaultBindings(map[string]string{
keys.ShiftTab: "previous",
keys.Tab: "next",
keys.Escape: "exit",
})
return param.UseParam(&PaginatorComponent{
Components: pages,
Style: osui.SetDefaults(&PaginatorStyle{}).(*PaginatorStyle),
}
}).(*PaginatorComponent)
}
+4 -3
View File
@@ -9,7 +9,7 @@ type TextComponent struct {
Text string
}
func (t *TextComponent) Update(string) bool {
func (t *TextComponent) Update(ctx osui.UpdateContext) bool {
return false
}
@@ -21,6 +21,7 @@ func (t TextComponent) Render() string {
return t.Text
}
func Text(text string) *TextComponent {
return &TextComponent{Text: text}
func Text(param osui.Param, text string) *TextComponent {
param.SetDefaultBindings(map[string]string{})
return param.UseParam(&TextComponent{Text: text}).(*TextComponent)
}
+115
View File
@@ -0,0 +1,115 @@
package ui
import (
"fmt"
"math"
"strings"
"github.com/orus-dev/osui"
)
func WithPosition(x, y int, c osui.Component) osui.Component {
data := c.GetComponentData()
data.X = x
data.Y = y
return c
}
func WithSize(width, height int, c osui.Component) osui.Component {
data := c.GetComponentData()
data.Width = width
data.Height = height
return c
}
func centerText(text string, width int) string {
if len(text) > width {
return text[:width]
}
padding := (width - len(text)) / 2
leftPadding := strings.Repeat(" ", padding)
rightPadding := strings.Repeat(" ", padding)
return fmt.Sprintf("%s%s%s", leftPadding, text, rightPadding)
}
func findClosestComponent(a []osui.Component, i int, d string) int {
if len(a) == 0 || i < 0 || i >= len(a) {
return -1 // Invalid input
}
current := a[i].GetComponentData()
closestIndex := -1
minDistance := math.MaxFloat64
for j, c := range a {
component := c.GetComponentData()
if i == j {
continue // Skip the current component
}
// Check if the component is in the correct direction
isValidDirection := false
switch d {
case "up":
if component.Y < current.Y {
isValidDirection = true
}
case "down":
if component.Y > current.Y {
isValidDirection = true
}
case "left":
if component.X < current.X {
isValidDirection = true
}
case "right":
if component.X > current.X {
isValidDirection = true
}
}
// If the direction is valid, calculate distance
if isValidDirection {
distance := math.Hypot(float64(component.X-current.X), float64(component.Y-current.Y))
if distance < minDistance {
minDistance = distance
closestIndex = j
}
}
}
return closestIndex
}
type Id[T osui.Component] struct {
changer func(T) T
Component T
}
func (i *Id[T]) Id(c T) T {
i.Component = c
if i.changer != nil {
return i.changer(i.Component)
}
return i.Component
}
func (i *Id[T]) SetProperties(c func(T) T) {
i.changer = c
}
type Class[T osui.Component] struct {
changer func(T) T
}
func (c *Class[T]) Class(co T) T {
if c.changer != nil {
return c.changer(co)
}
return co
}
func (c *Class[T]) SetProperties(ch func(T) T) {
c.changer = ch
}
+50 -90
View File
@@ -4,75 +4,77 @@ import (
"fmt"
"os"
"os/exec"
"reflect"
"regexp"
"runtime"
"strings"
"golang.org/x/term"
"github.com/nathan-fiscaletti/consolesize-go"
"github.com/orus-dev/osui/keys"
)
func renderLine(frameLine, line string, x int) string {
result := ""
if x >= len(frameLine) {
return result
}
var re = regexp.MustCompile(`(\x1b\[([0-9;]*)[a-zA-Z])+`)
lChars := []rune(line)
for i, c := range frameLine {
if i >= x && len(lChars) > i-x {
result += string(lChars[i-x])
func RenderLine(frame_, line_ string, x int, fm, lm map[int]string) string {
var res strings.Builder
frame := []rune(frame_)
line := []rune(line_)
flen, llen := len(frame), len(line)
for i := 0; i < flen; i++ {
if i >= x && i-x < llen {
if v, ok := lm[i-x]; ok {
res.WriteString(v)
}
res.WriteRune(line[i-x])
} else {
result += string(c)
if v, ok := fm[i]; ok {
res.WriteString(v)
}
res.WriteRune(frame[i])
}
}
return result
return res.String()
}
func CompressString(input string) (string, map[int]string) {
matchesMap := make(map[int]string)
res := []rune{}
i := 0
for i < len([]rune(input)) {
loc := re.FindStringIndex(string([]rune(input)[i:]))
if loc != nil && loc[0] == 0 {
ansiSeq := re.FindString(string([]rune(input)[i:]))
matchesMap[len(res)] = ansiSeq
i += len([]rune(ansiSeq))
} else {
res = append(res, []rune(input)[i])
i++
}
}
return string(res), matchesMap
}
func RenderOnFrame(c Component, frame *[]string) {
componentData := c.GetComponentData()
for i, lo := range strings.Split(c.Render(), "\n") {
for i, line := range strings.Split(c.Render(), "\n") {
if int(componentData.Y)+i < len(*frame) {
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")
fo, fm := CompressString((*frame)[int(componentData.Y)+i])
lo, lm := CompressString(line)
(*frame)[int(componentData.Y)+i] = RenderLine(fo, lo, componentData.X, fm, lm)
}
}
}
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 {
return "", err
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
func ReadKey() Key {
var b [3]byte
n, err := os.Stdin.Read(b[:])
if err != nil {
return "", err
return Key{Chars: [3]rune{'E', 'r', 'r'}, Name: err.Error()}
}
return string(b[:n]), nil
return Key{Chars: [3]rune{rune(b[0]), rune(b[1]), rune(b[2])}, Name: keys.GetKeyName(string(b[:n]))}
}
func NewFrame(width, height int) []string {
@@ -96,28 +98,6 @@ func Clear() {
}
}
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
}
func ShowCursor() {
fmt.Print("\033[?25h")
}
@@ -140,26 +120,6 @@ func LogicValueInt(b bool, _if, _else int) int {
return _else
}
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",
func GetTerminalSize() (int, int) {
return consolesize.GetConsoleSize()
}