package ansii import ( "fmt" "log/slog" "math" "os" "strings" "golang.org/x/term" ) type ANSI string const ( reset ANSI = "\033[0m" plain ANSI = "" bold ANSI = "\033[1m" underline ANSI = "\033[4m" black ANSI = "\033[30m" red ANSI = "\033[31m" green ANSI = "\033[32m" yellow ANSI = "\033[33m" blue ANSI = "\033[34m" purple ANSI = "\033[35m" cyan ANSI = "\033[36m" white ANSI = "\033[37m" blackBg ANSI = "\033[40m" redBg ANSI = "\033[41m" greenBg ANSI = "\033[42m" yellowBg ANSI = "\033[43m" blueBg ANSI = "\033[44m" purpleBg ANSI = "\033[45m" cyanBg ANSI = "\033[46m" whiteBg ANSI = "\033[47m" clearScreen ANSI = "\033[2J" hideCursor ANSI = "\033[?25l" showCursor ANSI = "\033[?25h" ) type Offset struct { X float32 Y float32 } type style struct { Reset ANSI Plain ANSI Bold ANSI Underline ANSI } type color struct { Black ANSI Red ANSI Green ANSI Yellow ANSI Blue ANSI Purple ANSI Cyan ANSI White ANSI } type screen struct { ClearScreen ANSI HideCursor ANSI ShowCursor ANSI } type ascii struct { Block string } func GetTermSize() (width int, height int) { var fd int = int(os.Stdout.Fd()) width, height, err := term.GetSize(fd) if err != nil { fmt.Println(string(Screen.ClearScreen) + "Fatal: error getting terminal size.") os.Exit(1) } return width, height } func MakeTermRaw() (*term.State, error) { var fd int = int(os.Stdout.Fd()) return term.MakeRaw(fd) } func RestoreTerm(prev *term.State) error { var fd int = int(os.Stdout.Fd()) return term.Restore(fd, prev) } func (s screen) PlaceCursor(X, Y int) ANSI { return ANSI(fmt.Sprintf("\033[%d;%dH", Y, X)) } var ( Styles = style{Bold: bold, Underline: underline, Reset: reset, Plain: plain} Colors = color{Red: red, Green: green, Yellow: yellow, Blue: blue, Purple: purple, Cyan: cyan, White: white} Screen = screen{ClearScreen: clearScreen, HideCursor: hideCursor, ShowCursor: showCursor} Blocks = ascii{Block: "█"} ) // Draws a box of dimensions `height` and `width` at `offset`. // The `offset` is the top left cell of the square. // Blocks that would be placed off screen are clipped. func DrawBox(builder *strings.Builder, offset Offset, height int, width int, style ANSI) { builder.WriteString(string(style)) for hIdx := range height { if hIdx == 0 || hIdx == height-1 { for wIdx := range width { drawPixel(builder, offset.X+float32(wIdx), offset.Y+float32(hIdx)) } } else { drawPixel(builder, offset.X, offset.Y+float32(hIdx)) drawPixel(builder, offset.X+float32(width-1), offset.Y+float32(hIdx)) } } builder.WriteString(string(Styles.Reset)) return } func drawPixel(builder *strings.Builder, offsetX, offsetY float32) { termWidth, termHeight := GetTermSize() scaledX := math.Floor(float64(offsetX * float32(termWidth))) scaledY := math.Floor(float64(offsetY * float32(termHeight))) slog.Debug("positions", slog.Any("x", scaledX), slog.Any("y", scaledY)) // TODO: Does float to int convertion cause many problems? builder.WriteString(string(Screen.PlaceCursor(int(scaledX), int(scaledY)) + ANSI(Blocks.Block))) } func DrawPixelStyle(builder *strings.Builder, offset Offset, style ANSI) { builder.WriteString(string(style)) drawPixel(builder, offset.X, offset.Y) builder.WriteString(string(Styles.Reset)) }