summaryrefslogtreecommitdiff
path: root/tui
diff options
context:
space:
mode:
Diffstat (limited to 'tui')
-rw-r--r--tui/draw.go203
-rw-r--r--tui/input.go57
-rw-r--r--tui/style.go32
3 files changed, 292 insertions, 0 deletions
diff --git a/tui/draw.go b/tui/draw.go
new file mode 100644
index 0000000..671d7e1
--- /dev/null
+++ b/tui/draw.go
@@ -0,0 +1,203 @@
+package tui
+
+import (
+ "github.com/rivo/uniseg"
+ "golang.org/x/term"
+ "unicode"
+ "unicode/utf8"
+ "fmt"
+ "bufio"
+ "os"
+)
+
+type pos struct {
+ x int
+ y int
+}
+
+type char struct {
+ c string
+ style Style
+}
+
+type surface map[pos]char
+
+type screen struct {
+ front surface
+ back surface
+ prevSize Dims
+ writer *bufio.Writer
+ cursor pos
+}
+
+var scr screen
+func init() {
+ scr.writer = bufio.NewWriterSize(os.Stdout, 50000)
+ Clear()
+}
+
+type Dims struct {
+ Width int
+ Height int
+}
+
+func Size() Dims {
+ w, h, _ := term.GetSize(0)
+ return Dims {w, h}
+}
+
+func Write(x int, y int, text string, style Style) int {
+ width := 0
+ g := uniseg.NewGraphemes(text)
+ for g.Next() {
+ c := g.Str()
+ r, _ := utf8.DecodeRuneInString(c)
+ if !unicode.IsGraphic(r) {
+ continue
+ }
+ scr.front[pos {x, y}] = char {c, style}
+ w := g.Width()
+ for i := 1; i < w; i++ {
+ scr.front[pos {x + i, y}] = char {"", style}
+ }
+ x += w
+ width += w
+ }
+ return width
+}
+
+func Clear() {
+ scr.front = make(surface)
+}
+
+func MoveCursor(x int, y int) {
+ scr.cursor = pos {x, y}
+}
+
+var saved *term.State
+func Start() error {
+ if saved != nil {
+ return nil
+ }
+ var err error
+ saved, err = term.MakeRaw(0)
+ if err != nil {
+ return err
+ }
+ scr.back = make(surface)
+ _, err = os.Stdout.WriteString("\033[?47h\033[2J\033[?2004h")
+ return err
+}
+
+func End() {
+ if saved == nil {
+ return
+ }
+ if term.Restore(0, saved) != nil {
+ return
+ }
+ os.Stdout.WriteString("\033[?2004l\033[2J\033[0;0H\033[?47l")
+}
+
+func writeCursor(x int, y int) error {
+ _, err := scr.writer.WriteString(fmt.Sprintf("\033[%d;%dH", y + 1, x + 1))
+ return err
+}
+
+func writeStyle(style Style) error {
+ _, err := scr.writer.WriteString("\033[0m")
+ if err != nil {
+ return err
+ }
+ _, err = scr.writer.WriteString(fmt.Sprintf("\033[38;5;%dm", style.Fg))
+ if err != nil {
+ return err
+ }
+ _, err = scr.writer.WriteString(fmt.Sprintf("\033[48;5;%dm", style.Bg))
+ if err != nil {
+ return err
+ }
+ if style.Bold {
+ _, err = scr.writer.WriteString("\033[1m")
+ if err != nil {
+ return err
+ }
+ }
+ if style.Italic {
+ _, err = scr.writer.WriteString("\033[3m")
+ if err != nil {
+ return err
+ }
+ }
+ if style.Underline {
+ _, err = scr.writer.WriteString("\033[4m")
+ if err != nil {
+ return err
+ }
+ }
+ if style.Strikethrough {
+ _, err = scr.writer.WriteString("\033[9m")
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func Present() error {
+ s := Size()
+ reset := true
+ style := DefaultStyle
+ var p pos
+ for p.y = 0; p.y < s.Height; p.y++ {
+ for p.x = 0; p.x < s.Width; {
+ if reset {
+ err := writeCursor(p.x, p.y)
+ if err != nil {
+ return err
+ }
+ }
+ c := scr.front[p]
+ if c.c == "" {
+ c.c = " "
+ }
+ cw := uniseg.StringWidth(c.c)
+ if p.x + cw > s.Width {
+ c.c = " "
+ }
+ if c != scr.back[p] || scr.prevSize != s {
+ if style != c.style {
+ style = c.style
+ err := writeStyle(style)
+ if err != nil {
+ return err
+ }
+ }
+ _, err := scr.writer.WriteString(c.c)
+ if err != nil {
+ return err
+ }
+ // make sure that the cursor position remains synced even if
+ // the terminal mangles a wide char or grapheme
+ if cw == 1 && len([]rune(c.c)) == 1 {
+ reset = false
+ } else {
+ reset = true
+ }
+ } else {
+ reset = true
+ }
+ p.x += cw
+ }
+ }
+ err := writeCursor(scr.cursor.x, scr.cursor.y)
+ if err != nil {
+ return err
+ }
+ scr.prevSize = s
+
+ f := scr.front
+ scr.front = scr.back
+ scr.back = f
+ return scr.writer.Flush()
+}
diff --git a/tui/input.go b/tui/input.go
new file mode 100644
index 0000000..a200e53
--- /dev/null
+++ b/tui/input.go
@@ -0,0 +1,57 @@
+package tui
+
+import (
+ "os"
+ "os/signal"
+ "golang.org/x/term"
+ "unicode"
+ "syscall"
+ "bufio"
+)
+
+type Event interface {}
+
+type TextInput rune
+type Paste string
+type Resize Dims
+
+var evChan chan Event
+func Events() <-chan Event {
+ if evChan != nil {
+ return evChan
+ }
+ evChan = make(chan Event, 1)
+
+ go func() {
+ rd := bufio.NewReader(os.Stdin)
+ for {
+ r, _, err := rd.ReadRune()
+ if err != nil {
+ evChan <- err
+ return
+ }
+ switch {
+ case r == '\033':
+ // todo
+ case unicode.IsControl(r):
+ default:
+ evChan <- TextInput(r)
+ }
+ }
+ }()
+
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGWINCH)
+ go func() {
+ for _ = range sigs {
+ w, h, err := term.GetSize(0)
+ if err == nil {
+ evChan <- Resize {w, h}
+ } else {
+ evChan <- err
+ }
+ }
+ }()
+
+ return evChan
+} \ No newline at end of file
diff --git a/tui/style.go b/tui/style.go
new file mode 100644
index 0000000..dedd287
--- /dev/null
+++ b/tui/style.go
@@ -0,0 +1,32 @@
+package tui
+
+type Style struct {
+ Fg int32
+ Bg int32
+ Truecolor bool
+ Bold bool
+ Italic bool
+ Underline bool
+ Strikethrough bool
+}
+
+const (
+ Black = iota
+ Red
+ Green
+ Yellow
+ Blue
+ Magenta
+ Cyan
+ White
+ BrightBlack
+ BrightRed
+ BrightGreen
+ BrightYellow
+ BrightBlue
+ BrightMagenta
+ BrightCyan
+ BrightWhite
+)
+
+var DefaultStyle = Style {Fg: BrightWhite, Bg: Black}