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