225 lines
6.6 KiB
Nim
225 lines
6.6 KiB
Nim
import unicode
|
|
import escapeSequences
|
|
import sequtils
|
|
import strformat
|
|
|
|
type
|
|
Buffer* = ref object
|
|
# AS IS ON SCREEN VALUES
|
|
stdout: File # usually you want to put stdout here
|
|
displayed: seq[Cell]
|
|
displayX, displayY: int # where the cursor is on display
|
|
positionX, positionY: int # where the buffer's top left corner is
|
|
|
|
# SIMULATED VALUES
|
|
buffered: seq[Cell]
|
|
# cursor position
|
|
bufferX, bufferY: int # where the cursor is, in buffer
|
|
width, height: int
|
|
# current style we're writing with
|
|
cursorAttributes: seq[uint8]
|
|
# scrolling debt
|
|
toScroll: int
|
|
|
|
ForegroundColors* = enum
|
|
fcBlack = 30, fcRed, fcGreen, fcYellow, fcBlue,
|
|
fcMagenta, fcCyan, fcLightGray,
|
|
fcDefault = 39,
|
|
fcDarkGray = 90, fcLightRed, fcLightGreen,
|
|
fcLightYellow, fcLightBlue, fcLightMagenta,
|
|
fcLightCyan, fcWhite
|
|
BackgroundColors* = enum
|
|
bcBlack, bcRed, bcGreen, bcYellow, bcBlue,
|
|
bcMagenta, bcCyan, bcLightGray,
|
|
bcDefault = 49,
|
|
bcDarkGray = 100, bcLightRed, bcLightGreen,
|
|
bcLightYellow, bcLightBlue, bcLightMagenta,
|
|
bcLightCyan, bcWhite
|
|
FontStyle* = enum
|
|
# these two are the most widely supported
|
|
fsReset = 0,
|
|
fsBold = 1,
|
|
fsUnderlined = 4,
|
|
Cell = object
|
|
attributes: seq[uint8]
|
|
text: Rune
|
|
|
|
|
|
func getCursorPos*(buf: Buffer): (int, int) =
|
|
(buf.bufferX, buf.bufferY)
|
|
|
|
proc setCursorPos*(buf: Buffer, x, y: int) =
|
|
if x < 0 or y < 0 or x >= buf.width or y >= buf.height:
|
|
raise newException(ValueError, &"Provided x ({x}) or y ({y}) out of bounds (x: {0}..{buf.width-1}; y: {0}..{buf.height-1}) for SetCursorPos.")
|
|
buf.bufferX = x
|
|
buf.bufferY = y
|
|
|
|
func getCursorXPos*(buf: Buffer): int =
|
|
buf.bufferX
|
|
|
|
proc setCursorXPos*(buf: Buffer, x: int) =
|
|
if x < 0 or x >= buf.width:
|
|
raise newException(ValueError, "Provided x is out of bounds.")
|
|
buf.bufferX = x
|
|
|
|
proc setAttributes*(buf: Buffer, attributes: seq[uint8]) =
|
|
buf.cursorAttributes = attributes
|
|
|
|
proc addAttribute*(buf: Buffer, attribute: uint8) =
|
|
buf.cursorAttributes.add(attribute)
|
|
|
|
proc clearAttributes*(buf: Buffer) =
|
|
buf.cursorAttributes = @[]
|
|
|
|
proc clear*(buf: Buffer) =
|
|
for i in 0..buf.buffered.high():
|
|
buf.buffered[i] = Cell(text: Rune(0))
|
|
buf.bufferX = 0
|
|
buf.bufferY = 0
|
|
|
|
template lineStart(buf: Buffer, line: int): int =
|
|
# returns where line <line> starts
|
|
buf.width * line
|
|
|
|
template lineEnd(buf: Buffer, line: int): int =
|
|
buf.lineStart(line + 1) - 1
|
|
|
|
proc clearLine*(buf: Buffer) =
|
|
for i in buf.lineStart(buf.bufferY)..buf.lineEnd(buf.bufferY):
|
|
buf.buffered[i] = Cell(text: Rune(0))
|
|
buf.bufferX = 0
|
|
|
|
proc write*(buf: Buffer, runes: seq[Rune]) =
|
|
# if it would go out of the screen, crash
|
|
if buf.bufferX + runes.len() >= buf.width:
|
|
raise newException(ValueError, "Text too long, would overflow the buffer.")
|
|
if buf.bufferY >= buf.height:
|
|
raise newException(ValueError, "BufferY is outside of height.")
|
|
|
|
let fromPos = buf.lineStart(buf.bufferY) + buf.bufferX
|
|
let toPos = fromPos + runes.high()
|
|
|
|
for i in fromPos..toPos:
|
|
buf.buffered[i] = Cell(text: runes[i-fromPos], attributes: buf.cursorAttributes)
|
|
|
|
buf.bufferX += runes.len()
|
|
|
|
proc write*(buf: Buffer, text: string) =
|
|
# convert text to characters
|
|
let runes = text.toRunes()
|
|
buf.write(runes)
|
|
|
|
proc newLine*(buf: Buffer) =
|
|
if buf.bufferY >= buf.height - 1:
|
|
raise newException(ValueError, "Attempt to go to new line at the last line.")
|
|
buf.bufferY += 1
|
|
buf.bufferX = 0
|
|
|
|
proc redraw*(buf: Buffer, force: bool = false) =
|
|
var force = force
|
|
if buf.toScroll > 0:
|
|
force = true
|
|
var toPrint = escScroll(buf.toScroll)
|
|
buf.toScroll = 0
|
|
|
|
# go over display and buffer to find any differences
|
|
for i in 0..buf.buffered.high():
|
|
let cellBuffer = buf.buffered[i]
|
|
let cellDisplay = buf.displayed[i]
|
|
|
|
# TODO: this is a spot for obvious optimization
|
|
if cellBuffer != cellDisplay or force:
|
|
let y = i div buf.width
|
|
let x = i mod buf.width
|
|
# go to given place
|
|
toPrint &= escSetCursorPos(x+buf.positionX, y+buf.positionY)
|
|
|
|
# get the right attributes
|
|
# first reset
|
|
toPrint &= escAttributes(@[fsReset.uint8])
|
|
# set cell attributes
|
|
toPrint &= escAttributes(cellBuffer.attributes)
|
|
# write the character if it's a non null
|
|
if cellBuffer.text != Rune(0):
|
|
toPrint &= $cellBuffer.text
|
|
else:
|
|
# erase a character since it's a null
|
|
toPrint &= escEraseCharacters(1)
|
|
|
|
# update displayed buffer
|
|
buf.displayed[i] = cellBuffer
|
|
|
|
# finish
|
|
toPrint &= escAttributes(@[fsReset.uint8])
|
|
toPrint &= escSetCursorPos(buf.bufferX + buf.positionX, buf.bufferY + buf.positionY)
|
|
buf.stdout.write(toPrint)
|
|
|
|
|
|
func getSize*(buf: Buffer): (int, int) =
|
|
(buf.width, buf.height)
|
|
|
|
proc resize*(buf: Buffer, newWidth, newHeight: int, terminalWidth, terminalHeight: int) =
|
|
|
|
# assert sizes
|
|
if newWidth + buf.positionX > terminalWidth or newHeight + buf.positionY > terminalHeight:
|
|
raise newException(ValueError, "Given height/width would go beyond terminal limits.")
|
|
|
|
# shrinking X
|
|
if newWidth < buf.width:
|
|
# dropping cells outside the screen
|
|
var i = 0
|
|
buf.buffered.keepItIf(i mod buf.width < newWidth)
|
|
i = 0
|
|
buf.displayed.keepItIf(i mod buf.width < newWidth)
|
|
# increasing X
|
|
elif newWidth > buf.width:
|
|
let extra = newWidth - buf.width
|
|
let extraCells = Cell(text: Rune(0)).repeat(extra)
|
|
for i in 0..buf.height-1:
|
|
let pos = i * newWidth + buf.width
|
|
buf.buffered.insert(extraCells, pos)
|
|
buf.displayed.insert(extraCells, pos)
|
|
|
|
# shrinking Y
|
|
if newHeight < buf.height:
|
|
buf.buffered.setLen(newHeight * newWidth)
|
|
buf.displayed.setLen(newHeight * newWidth)
|
|
# increasing Y
|
|
elif newHeight > buf.height:
|
|
let extra = newHeight - buf.height
|
|
let extraCells = Cell(text: Rune(0)).repeat(extra * newWidth)
|
|
buf.buffered &= extraCells
|
|
buf.displayed &= extraCells
|
|
|
|
buf.width = newWidth
|
|
buf.height = newHeight
|
|
|
|
func getPosition*(buf: Buffer): (int, int) =
|
|
(buf.positionX, buf.positionY)
|
|
|
|
proc scroll*(buf: Buffer, delta: int) =
|
|
# scrolls terminal and moves the Y position
|
|
if delta < 1:
|
|
raise newException(ValueError, "Only positive values are allowed for delta.")
|
|
|
|
buf.toScroll += delta
|
|
buf.positionY -= delta
|
|
|
|
proc newBuffer*(x, y, w, h: int, stdout: File): Buffer =
|
|
new(result)
|
|
result.positionX = x
|
|
result.positionY = y
|
|
result.width = w
|
|
result.height = h
|
|
result.stdout = stdout
|
|
let length = w * h
|
|
result.buffered = repeat(Cell(text: Rune(0)), length)
|
|
result.displayed = repeat(Cell(text: Rune(0)), length)
|
|
result.bufferX = 0
|
|
result.bufferY = 0
|
|
result.displayX = 0
|
|
result.displayY = 0
|
|
result.cursorAttributes = @[]
|
|
result.redraw(force = true)
|
|
|