diff options
| -rw-r--r-- | football/main.go | 104 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 6 | ||||
| -rw-r--r-- | tui/draw.go | 203 | ||||
| -rw-r--r-- | tui/input.go | 57 | ||||
| -rw-r--r-- | tui/style.go | 32 |
6 files changed, 409 insertions, 0 deletions
diff --git a/football/main.go b/football/main.go new file mode 100644 index 0000000..a6cd954 --- /dev/null +++ b/football/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "citrons.xyz/talk/tui" + "math/rand" + "log" + "strings" + "time" +) + +type ball struct { + x int + y int + vx int + vy int + inGoal bool +} + +func main() { + tui.Start() + + s := tui.Size() + var balls [20]ball + for i := 0; i < len(balls); i++ { + balls[i].x = rand.Int() % s.Width - 1 + balls[i].y = rand.Int() % s.Height + if rand.Int() % 2 == 1 { + balls[i].vx = 1 + } else { + balls[i].vx = -1 + } + if rand.Int() % 2 == 1 { + balls[i].vy = 1 + } else { + balls[i].vy = -1 + } + } + + var sb strings.Builder + + for { + tui.Clear() + s := tui.Size() + for y := 0; y < s.Height; y++ { + for x := 0; x < s.Width; x++ { + if (x + 66) % 10 == 1 && (y + 2) % 5 == 1 { + tui.Write(x, y, "*", tui.Style {Fg: tui.Green}) + } + } + } + tui.Write(2, 1, "normal", tui.Style {Fg: tui.Red}) + tui.Write(2, 2, "bold", tui.Style {Fg: tui.Yellow, Bold: true}) + tui.Write(2, 3, "italic", tui.Style {Fg: tui.Green, Italic: true}) + tui.Write(2, 4, "underline", tui.Style {Fg: tui.Blue, Underline: true}) + tui.Write(2, 5, "strikethrough", tui.Style {Fg: tui.Magenta, Strikethrough: true}) + tui.Write(2, 6, "hooray!", tui.Style {Fg: tui.White, Bold: true, Italic: true, Underline: true, Strikethrough: true}) + gx := s.Width / 2 - 1 + for i := 0; i < len(balls); i++ { + if balls[i].inGoal { + continue + } + balls[i].x += balls[i].vx + balls[i].y += balls[i].vy + if balls[i].x < 0 { + balls[i].x = 0 + balls[i].vx = -balls[i].vx + } else if balls[i].x >= s.Width - 1 { + balls[i].x = s.Width - 2 + balls[i].vx = -balls[i].vx + } + if balls[i].y < 0 { + balls[i].y = 0 + balls[i].vy = -balls[i].vy + } else if balls[i].y >= s.Height { + balls[i].y = s.Height - 1 + balls[i].vy = -balls[i].vy + } + if balls[i].x >= gx - 1 && balls[i].x <= gx + 1 && balls[i].y == 0 { + balls[i].inGoal = true + continue + } + tui.Write(balls[i].x, balls[i].y, "⚽", tui.DefaultStyle) + } + tui.Write(gx, 0, "🥅", tui.DefaultStyle) + + w := tui.Write(2, 8, sb.String(), tui.DefaultStyle) + tui.MoveCursor(2 + w, 8) + err := tui.Present() + if err != nil { + log.Fatal(err) + } + + select { + case ev := <-tui.Events(): + switch ev := ev.(type) { + case tui.TextInput: + sb.WriteRune(rune(ev)) + case error: + log.Fatal(ev) + } + case <-time.After(time.Second / 16): + } + } +} @@ -1,3 +1,10 @@ module citrons.xyz/talk go 1.23.4 + +require ( + github.com/rivo/uniseg v0.4.7 + golang.org/x/term v0.28.0 +) + +require golang.org/x/sys v0.29.0 // indirect @@ -0,0 +1,6 @@ +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 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} |
