Compare commits
2 Commits
4323e5872c
...
e49d4799f3
Author | SHA1 | Date |
---|---|---|
Art | e49d4799f3 | |
Art | 4b73239b55 |
38
editor.nim
38
editor.nim
|
@ -51,6 +51,13 @@ proc read*(ed: EditorState): (EditorResult, string) =
|
|||
|
||||
var scroll: Scroll = new(Scroll)
|
||||
|
||||
template moveInHistory(delta: int) =
|
||||
let newHistoryIndex = min(max(ed.historyIndex + delta, 0), ed.history.high())
|
||||
if newHistoryIndex != ed.historyIndex:
|
||||
ed.textBuffer = ed.history[newHistoryIndex]
|
||||
scroll.reset()
|
||||
ed.historyIndex = newHistoryIndex
|
||||
|
||||
while true:
|
||||
render(ed.textBuffer, ed.termBuffer, ed.prompt, scroll)
|
||||
let (getKeyResult, key) = getKey()
|
||||
|
@ -61,7 +68,8 @@ proc read*(ed: EditorState): (EditorResult, string) =
|
|||
let control = key.JaleKeycode()
|
||||
case control:
|
||||
of jkEnter:
|
||||
if ed.textBuffer.isCursorAtEnd():
|
||||
if ed.textBuffer.isLineEmpty() and ed.textBuffer.isLastLine():
|
||||
ed.textBuffer.stripFinalNewline()
|
||||
editorResult = erEnter
|
||||
break
|
||||
else:
|
||||
|
@ -82,22 +90,38 @@ proc read*(ed: EditorState): (EditorResult, string) =
|
|||
of jkRight:
|
||||
ed.textBuffer.moveCursor(1, 0)
|
||||
of jkDown:
|
||||
ed.textBuffer.moveCursor(0, 1)
|
||||
if ed.textBuffer.isLastLine():
|
||||
moveInHistory(1)
|
||||
else:
|
||||
ed.textBuffer.moveCursor(0, 1)
|
||||
of jkUp:
|
||||
ed.textBuffer.moveCursor(0, -1)
|
||||
if ed.textBuffer.isFistLine():
|
||||
moveInHistory(-1)
|
||||
else:
|
||||
ed.textBuffer.moveCursor(0, -1)
|
||||
of jkEnd:
|
||||
ed.textBuffer.moveToEnd(true)
|
||||
of jkHome:
|
||||
ed.textBuffer.moveToEnd(false)
|
||||
of jkPageDown:
|
||||
let termHeight = termGetHeight()
|
||||
ed.textBuffer.moveCursor(0, termHeight div 2)
|
||||
of jkPageUp:
|
||||
let termHeight = termGetHeight()
|
||||
ed.textBuffer.moveCursor(0, -(termHeight div 2))
|
||||
of jkCtrlDown:
|
||||
ed.textBuffer.newLine()
|
||||
else:
|
||||
discard # not implemented
|
||||
|
||||
# return val
|
||||
result = (editorResult, ed.textBuffer.getContent())
|
||||
|
||||
# return val, strip final newline (due to how double enter is how you enter)
|
||||
let cont = ed.textBuffer.getContent()
|
||||
result = (editorResult, cont)
|
||||
|
||||
# cleanup
|
||||
stdout.write("\n")
|
||||
ed.termBuffer = nil
|
||||
|
||||
# don't add empty lines to history
|
||||
if ed.history[ed.history.high()].getContent() == "":
|
||||
ed.history.del(ed.history.high())
|
||||
|
78
renderer.nim
78
renderer.nim
|
@ -11,33 +11,24 @@ import terminalUtils/terminalGetInfo
|
|||
type Scroll* = ref object
|
||||
x, y: int
|
||||
|
||||
proc reset*(scroll: Scroll) =
|
||||
scroll.x = 0
|
||||
scroll.y = 0
|
||||
|
||||
proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll: Scroll) =
|
||||
# TODO: vertical scroll
|
||||
# we are free to "redraw" everything everytime
|
||||
# since termBuffer double buffers
|
||||
|
||||
# resizing the buffer if needed
|
||||
let lineCount = textBuffer.getLineCount()
|
||||
# GET INFO
|
||||
let (x, y) = textBuffer.getCursorPos()
|
||||
let lines = textBuffer.lines()
|
||||
let lineCount = lines.len()
|
||||
let termWidth = termGetWidth()
|
||||
let termHeight = termGetHeight()
|
||||
|
||||
let (bufWidth, bufHeight) = termBuffer.getSize()
|
||||
|
||||
if bufWidth != termWidth or bufHeight != min(lineCount, termHeight):
|
||||
termBuffer.resize(termWidth, min(lineCount, termHeight))
|
||||
|
||||
|
||||
termBuffer.clear()
|
||||
|
||||
let (x, y) = textBuffer.getCursorPos()
|
||||
|
||||
let lines = textBuffer.lines()
|
||||
let promptLen = prompt.len()
|
||||
let maxTextLen = termWidth - promptLen
|
||||
if maxTextLen < 1:
|
||||
termBuffer.redraw()
|
||||
return # nothing will fit anyways
|
||||
|
||||
# SCROLL
|
||||
const scrollOffset = 2
|
||||
# right scroll
|
||||
if x - scroll.x >= maxTextLen - scrollOffset:
|
||||
|
@ -47,10 +38,55 @@ proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll:
|
|||
scroll.x = x
|
||||
# left scroll on backspace - TODO IMPLEMENT
|
||||
|
||||
for i in 0..lines.high():
|
||||
# vertical scroll
|
||||
# scroll down - when it would be out of reach
|
||||
# mode 1: by moving the location up
|
||||
# this one is based on content, not y position
|
||||
# if this one scrolls too much
|
||||
block:
|
||||
# in a block so positionY can't be used below, since it can change!
|
||||
let (_, positionY) = termBuffer.getPosition()
|
||||
if positionY > 0 and positionY + lineCount > termHeight:
|
||||
termBuffer.scroll(min(positionY + lineCount - termHeight, positionY))
|
||||
|
||||
# scroll down
|
||||
# mode 2 - only if positionY is 0, so mode 1 is already done
|
||||
# this one is based on the cursor
|
||||
block:
|
||||
let (_, positionY) = termBuffer.getPosition()
|
||||
if positionY == 0 and y - scroll.y > termHeight - 1:
|
||||
scroll.y = y - termHeight + 1
|
||||
|
||||
# scroll up
|
||||
if y < scroll.y:
|
||||
scroll.y = y
|
||||
|
||||
# resizing the buffer if needed
|
||||
block:
|
||||
# in a block, so bufWidth and height can't be used below, since they get changed!
|
||||
let (bufWidth, bufHeight) = termBuffer.getSize()
|
||||
# prevent shrinking vertically to avoid uncleaned parts
|
||||
let desiredBufHeight = min(max(lineCount, bufHeight), termHeight)
|
||||
if bufWidth != termWidth or bufHeight != desiredBufHeight:
|
||||
termBuffer.resize(termWidth, desiredBufHeight, termWidth, termHeight)
|
||||
|
||||
termBuffer.clear()
|
||||
|
||||
if maxTextLen < 1:
|
||||
termBuffer.redraw()
|
||||
return # nothing will fit anyways
|
||||
|
||||
let fromY = scroll.y
|
||||
# assumes that the MODE 1 scroll up has happened
|
||||
# and if lineCount doesn't fit in termheight
|
||||
# then it has already been scrolled to pos 0
|
||||
let toY = min(fromY+termHeight-1, lineCount-1)
|
||||
for i in fromY..toY:
|
||||
let line = lines[i]
|
||||
if i == 0:
|
||||
termBuffer.write(prompt)
|
||||
elif i == y:
|
||||
termBuffer.write(">" & " ".repeat(promptLen - 1))
|
||||
else:
|
||||
termBuffer.write(" ".repeat(promptLen))
|
||||
|
||||
|
@ -59,7 +95,9 @@ proc render*(textBuffer: TextBuffer, termBuffer: Buffer, prompt: string, scroll:
|
|||
let toPos = min(scroll.x+maxTextLen-2, line.high())
|
||||
if fromPos <= line.high():
|
||||
termBuffer.write(line[fromPos..toPos])
|
||||
termBuffer.newLine()
|
||||
if i < toY:
|
||||
termBuffer.newLine()
|
||||
|
||||
termBuffer.setCursorPos(x + promptLen - scroll.x, y - scroll.y)
|
||||
termBuffer.redraw()
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ type
|
|||
width, height: int
|
||||
# current style we're writing with
|
||||
cursorAttributes: seq[uint8]
|
||||
# scrolling debt
|
||||
toScroll: int
|
||||
|
||||
ForegroundColors* = enum
|
||||
fcBlack = 30, fcRed, fcGreen, fcYellow, fcBlue,
|
||||
|
@ -91,6 +93,8 @@ 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()
|
||||
|
@ -106,11 +110,17 @@ proc write*(buf: Buffer, text: string) =
|
|||
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 toPrint = ""
|
||||
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():
|
||||
|
@ -148,7 +158,12 @@ proc redraw*(buf: Buffer, force: bool = false) =
|
|||
func getSize*(buf: Buffer): (int, int) =
|
||||
(buf.width, buf.height)
|
||||
|
||||
proc resize*(buf: Buffer, newWidth, newHeight: int) =
|
||||
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
|
||||
|
@ -182,9 +197,13 @@ proc resize*(buf: Buffer, newWidth, newHeight: int) =
|
|||
func getPosition*(buf: Buffer): (int, int) =
|
||||
(buf.positionX, buf.positionY)
|
||||
|
||||
proc setPosition*(buf: Buffer, x, y: int) =
|
||||
buf.positionX = x
|
||||
buf.positionY = y
|
||||
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)
|
||||
|
|
|
@ -3,43 +3,23 @@
|
|||
|
||||
import strformat
|
||||
import strutils
|
||||
import terminal
|
||||
|
||||
func escSetCursorPos*(x, y: int): string =
|
||||
&"\e[{y+1};{x+1}H"
|
||||
|
||||
const escGetCursorPos* = "\e[6n"
|
||||
|
||||
type InvalidResponseError* = object of CatchableError
|
||||
|
||||
proc termGetCursorPos*(ouput: File): (int, int) =
|
||||
|
||||
ouput.write(escGetCursorPos)
|
||||
|
||||
var response = ""
|
||||
if getch() != '\e':
|
||||
raise newException(InvalidResponseError, "Unsupported terminal - can't get cursor position.")
|
||||
if getch() != '[':
|
||||
raise newException(InvalidResponseError, "Can't parse response in getCursorPos")
|
||||
var newChar = getch()
|
||||
while newChar != ';':
|
||||
response &= newChar
|
||||
newChar = getch()
|
||||
let y = parseInt(response) - 1
|
||||
response = ""
|
||||
newChar = getch()
|
||||
while newChar != 'R':
|
||||
response &= newChar
|
||||
newChar = getch()
|
||||
let x = parseInt(response) - 1
|
||||
|
||||
return (x, y)
|
||||
|
||||
|
||||
type InvalidResponseError* = object of CatchableError
|
||||
|
||||
func escAttributes*(attributes: seq[uint8]): string =
|
||||
let joined = attributes.join(";")
|
||||
&"\e[{joined}m"
|
||||
|
||||
func escEraseCharacters*(n: int): string =
|
||||
&"\e[{n}X"
|
||||
&"\e[{n}X"
|
||||
|
||||
func escScroll*(n: int): string =
|
||||
if n == 0:
|
||||
""
|
||||
else:
|
||||
&"\e[{n}S"
|
|
@ -7,21 +7,22 @@ import unicode
|
|||
|
||||
type
|
||||
JaleKeycode* = enum
|
||||
jkNull = 0,
|
||||
jkCtrlA = 1, jkCtrlB, jkCtrlC, jkCtrlD, jkCtrlE,
|
||||
jkCtrlF, jkCtrlG, # ctrl+h invalid
|
||||
jkBackspace = 8,
|
||||
jkTab = 9,
|
||||
jkCtrlF, jkCtrlG,
|
||||
jkBackspace = 8, # ctrl+h = backspace
|
||||
jkTab = 9, # ctrl+i = tab
|
||||
jkCtrlJ = 10, jkCtrlK, jkCtrlL,
|
||||
jkEnter = 13,
|
||||
jkEnter = 13, # ctrl+m = enter
|
||||
jkCtrlN = 14, jkCtrlO, jkCtrlP, jkCtrlQ,
|
||||
jkCtrlR, jkCtrlS, jkCtrlT, jkCtrlU, jkCtrlV,
|
||||
jkCtrlW, jkCtrlX, jkCtrlY, jkCtrlZ,
|
||||
jkEscape = 27,
|
||||
jkCtrlBackslash = 28,
|
||||
jkCtrlRightBracket = 29,
|
||||
jkAltBackspace = 127,
|
||||
|
||||
jkLeft = 256, jkRight, jkUp, jkDown,
|
||||
# the rest are handled by escapeSeqs
|
||||
jkAltBackspace,
|
||||
jkLeft, jkRight, jkUp, jkDown,
|
||||
jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown,
|
||||
# other 4 move keys
|
||||
jkHome, jkEnd, jkPageUp, jkPageDown,
|
||||
|
|
|
@ -4,7 +4,7 @@ import strutils
|
|||
import escapeSequences
|
||||
|
||||
proc termGetCursorPos*(ouput: File): (int, int) =
|
||||
|
||||
# TODO, must only call this at the start of the program
|
||||
ouput.write(escGetCursorPos)
|
||||
|
||||
var response = ""
|
||||
|
|
|
@ -38,7 +38,11 @@ proc delete*(buf: TextBuffer) =
|
|||
if buf.cursorX == 0 and buf.cline().len() > 0:
|
||||
buf.cline() = buf.cline[1..buf.cline.high()]
|
||||
elif buf.cursorX > buf.cline.high():
|
||||
discard # merge two lines TODO
|
||||
if buf.cursorY < buf.content.high():
|
||||
# merging lines
|
||||
let left = buf.cline()
|
||||
buf.content.delete(buf.cursorY)
|
||||
buf.cline() = left & buf.cline()
|
||||
else:
|
||||
buf.cline() = buf.cline[0..buf.cursorX-1] & buf.cline[buf.cursorX+1..buf.cline().high()]
|
||||
|
||||
|
@ -46,7 +50,13 @@ proc delete*(buf: TextBuffer) =
|
|||
proc backspace*(buf: TextBuffer) =
|
||||
# emulates a backspace key
|
||||
if buf.cursorX == 0:
|
||||
discard # merge two lines TODO
|
||||
if buf.cursorY > 0:
|
||||
# merging lines
|
||||
let right = buf.cline()
|
||||
buf.content.delete(buf.cursorY)
|
||||
dec buf.cursorY
|
||||
buf.cursorX = buf.cline().len()
|
||||
buf.cline() &= right
|
||||
elif buf.cursorX > buf.cline().high():
|
||||
buf.cline() = buf.cline[0..buf.cline().high()-1]
|
||||
dec buf.cursorX
|
||||
|
@ -106,4 +116,32 @@ proc getLineCount*(buf: TextBuffer): int =
|
|||
buf.content.len()
|
||||
|
||||
proc lines*(buf: TextBuffer): seq[seq[Rune]] =
|
||||
buf.content
|
||||
buf.content
|
||||
|
||||
proc isLastLine*(buf: TextBuffer): bool =
|
||||
buf.cursorY == buf.content.high()
|
||||
|
||||
proc isFistLine*(buf: TextBuffer): bool =
|
||||
buf.cursorY == 0
|
||||
|
||||
proc isLineEmpty*(buf: TextBuffer): bool =
|
||||
buf.cline().len() == 0
|
||||
|
||||
proc stripFinalNewline*(buf: TextBuffer) =
|
||||
# nothing if only 1 line
|
||||
if buf.content.len() <= 1:
|
||||
return
|
||||
|
||||
# nothing if last line not empty
|
||||
if buf.content[buf.content.high()].len() > 0:
|
||||
return
|
||||
|
||||
# adjust cursor
|
||||
if buf.cursorY == buf.content.high():
|
||||
dec buf.cursorY
|
||||
buf.cursorX = buf.cline().len()
|
||||
|
||||
# pop final element
|
||||
buf.content.del(buf.content.high())
|
||||
|
||||
|
Loading…
Reference in New Issue