double enter, backspace and delete can merge lines

This commit is contained in:
Art 2022-12-30 00:17:51 +01:00
parent 4323e5872c
commit 4b73239b55
Signed by: prod2
GPG Key ID: F3BB5A97A70A8DDE
7 changed files with 146 additions and 69 deletions

View File

@ -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, 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,7 @@ 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():
editorResult = erEnter
break
else:
@ -82,20 +89,33 @@ 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, if cont.len() == 0: "" else: cont[0..^2])
# cleanup
stdout.write("\n")

View File

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

View File

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

View File

@ -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"

View File

@ -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,

View File

@ -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 = ""

View File

@ -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,13 @@ 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