sshpong/internal/ansii/ansii.go
2024-10-03 22:41:33 -06:00

141 lines
3.3 KiB
Go

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