jal3/terminalUtils/buffer.nim

228 lines
6.7 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)
func getHeight*(buf: Buffer): int =
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)