package tui import ( "github.com/rivo/uniseg" "golang.org/x/term" "zgo.at/termfo" "zgo.at/termfo/caps" "unicode" "unicode/utf8" "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 ScreenSize writer *bufio.Writer cursor pos showCursor bool } var ( terminfo *termfo.Terminfo scr screen ) func init() { scr.writer = bufio.NewWriterSize(os.Stdout, 50000) Clear() } type ScreenSize struct { Width int Height int } func Size() ScreenSize { w, h, _ := term.GetSize(0) return ScreenSize {w, h} } func WriteAt(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) ClearCursor() } func ShowCursor(x int, y int) { scr.cursor = pos {x, y} scr.showCursor = true } func ClearCursor() { scr.showCursor = false } 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 } terminfo, err = termfo.New("") if err != nil { if terminfo, err = termfo.New("xterm"); err != nil { return err } } scr.back = make(surface) _, err = os.Stdout.WriteString( terminfo.Get(caps.EnterCaMode) + terminfo.Get(caps.XM, 1) + terminfo.Get(caps.ClearScreen), ) return err } func End() { if saved == nil { return } os.Stdout.WriteString( terminfo.Get(caps.ClearScreen) + terminfo.Get(caps.XM, 0) + terminfo.Get(caps.ExitCaMode), ) if term.Restore(0, saved) != nil { return } saved = nil } func writeClearCursor() { scr.writer.WriteString(terminfo.Get(caps.CursorInvisible)) } func writeCursor(x int, y int) { scr.writer.WriteString(terminfo.Get(caps.CursorNormal)) scr.writer.WriteString(terminfo.Get(caps.CursorAddress, y, x)) } func writeStyle(style Style) { scr.writer.WriteString(terminfo.Get(caps.ExitAttributeMode)) scr.writer.WriteString(terminfo.Get(caps.SetAForeground, int(style.Fg))) scr.writer.WriteString(terminfo.Get(caps.SetABackground, int(style.Bg))) if style.Bold { scr.writer.WriteString(terminfo.Get(caps.EnterBoldMode)) } if style.Italic { scr.writer.WriteString(terminfo.Get(caps.EnterItalicsMode)) } if style.Underline { scr.writer.WriteString(terminfo.Get(caps.EnterUnderlineMode)) } if style.Strikethrough { scr.writer.WriteString(terminfo.Get(caps.Smxx)) } } func Present() error { writeClearCursor() 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; { 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 reset { writeCursor(p.x, p.y) } if style != c.style { style = c.style writeStyle(style) } scr.writer.WriteString(c.c) // 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 } } if scr.showCursor { writeCursor(scr.cursor.x, scr.cursor.y) } scr.prevSize = s f := scr.front scr.front = scr.back scr.back = f return scr.writer.Flush() }