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() }