From ebf24532e4d3bfda31d141f039818abefd4830af Mon Sep 17 00:00:00 2001 From: D4VID Date: Sun, 4 May 2025 18:19:41 +0200 Subject: [PATCH] Write foundation of the editor --- keybinds.lua | 35 ++++ vim.lua | 502 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 537 insertions(+) create mode 100644 keybinds.lua create mode 100644 vim.lua diff --git a/keybinds.lua b/keybinds.lua new file mode 100644 index 0000000..525a48d --- /dev/null +++ b/keybinds.lua @@ -0,0 +1,35 @@ +local keybinds = {} + +keybinds.normal = {} + +keybinds.normal["i"] = "insertMode" +keybinds.normal[":"] = "commandMode" +keybinds.normal["a"] = "append" + +keybinds.normal["h"] = "left" +keybinds.normal["j"] = "down" +keybinds.normal["k"] = "up" +keybinds.normal["l"] = "right" + +keybinds.normal["left"] = "left" +keybinds.normal["down"] = "down" +keybinds.normal["up"] = "up" +keybinds.normal["right"] = "right" + +keybinds.normal["x"] = "delChar" +keybinds.normal["X"] = "delChar" + + +keybinds.normal["H"] = "moveHigh" +keybinds.normal["M"] = "moveMiddle" +keybinds.normal["L"] = "moveLow" + + +keybinds.normal["gg"] = "moveToStart" +keybinds.normal["G"] = "moveToEnd" + + +keybinds.normal["^e"] = "scrollDown" +keybinds.normal["^y"] = "scrollUp" + +return keybinds diff --git a/vim.lua b/vim.lua new file mode 100644 index 0000000..3d4299a --- /dev/null +++ b/vim.lua @@ -0,0 +1,502 @@ +local fs = require('filesystem') +local term = require('term') +local shell = require("shell") +local keyboard = require('keyboard') +local keybinds = require('keybinds') + +local gpu = term.gpu() +local args, options = shell.parse(...) + +-------------------------------------------------------------------------------- + +local modes = {NORMAL=0, INSERT=1, COMMAND=2} + +local mode = modes.NORMAL + +local buffer = {} + +local cursorX, cursorY = 1, 1 -- where on screen is the cursor +local actualX, actualY = 1, 1 -- where in buffer is the cursor +local scrollX, scrollY = 0, 0 + +local motion = 1 -- how many times to repeat an action + +local keybind = "" +local command = "" + +local currentFilename = "" + +local _, _, w, h = term.getGlobalArea() -- get width and height of the terminal +h = h - 1 -- leave space for bottom bar + +local readonly = false -- is the file being edited read only (ro filesystem or no permission) +local changed = false -- is the file was changed since load + +local running = true + +local ctrl, shift, alt = false, false, false -- is the control keys are pressed + +-------------------------------------------------------------------------------- + +function string.insert(str1, str2, pos) + return str1:sub(1,pos)..str2..str1:sub(pos+1) +end + +-------------------------------------------------------------------------------- + +local function redrawLine(line) + local y = line-scrollY + local len = #buffer[line] + gpu.set(1, y, buffer[line]) + gpu.fill(len+1, y, w-len, 1, ' ') +end + +local function redrawAll() + local lines = #buffer + for y=1+scrollY,math.min(lines,h+scrollY) do + redrawLine(y) + end + local last = lines-scrollY + if last < h then + gpu.fill(1, last+1, 1, h-last, '~') + gpu.fill(2, last+1, w-1, h-last, ' ') + end +end + +-------------------------------------------------------------------------------- + +local function setStatus(status) + gpu.set(1,h+1, status) + local x = #status + gpu.fill(x+1, h+1, w-x, 1, ' ') +end + +local function setStatusError(err) + local bg, fg = gpu.getBackground(), gpu.getForeground() + gpu.setBackground(0xff0000) + gpu.setForeground(0xffffff) + + setStatus(err) + + gpu.setBackground(bg) -- put back what was there before + gpu.setForeground(fg) +end + +local function updateStatusPos() + gpu.fill(w-7, h+1, 7, 1, ' ') + gpu.set(w-7, h+1, actualY .. ',' .. actualX) +end + +local function updateStatusKeybind() + gpu.fill(w-14, h+1, 7, 1, ' ') + gpu.set(w-14, h+1, keybind) +end + +-------------------------------------------------------------------------------- + +local function setActualCursor(newActualX, newActualY, ignoreChecks, append) + if ignoreChecks then + actualX, actualY = newActualX, newActualY + else + if newActualY > #buffer then + newActualY = #buffer + end + if newActualY < 1 then + newActualY = 1 + end + actualY = newActualY + + if newActualX > #buffer[actualY] then + if append then + newActualX = #buffer[actualY]+1 + else + newActualX = #buffer[actualY] + end + end + if newActualX < 1 then + newActualX = 1 + end + + actualX = newActualX + end + + cursorX = actualX-scrollX + cursorY = actualY-scrollY + + if cursorY < 1 then + cursorY = 1 + scrollY = scrollY-1 + redrawAll() + end + if cursorY > h then + cursorY = h + scrollY = scrollY+1 + redrawAll() + end + + term.setCursor(cursorX, cursorY) + updateStatusPos() +end + +local function moveActualCursor(x, y, append) + setActualCursor(actualX+x, actualY+y, false, append) +end + +local function setScreenCursor(newCursorX, newCursorY) + local newActualY = newCursorY + scrollY + local newActualX = newCursorX + scrollX + + if newActualY > #buffer then + newActualY = #buffer + end + actualY = newActualY + + if newActualX > #buffer[actualY] then + newActualX = #buffer[actualY] + end + actualX = newActualX + + cursorX = actualX-scrollX + cursorY = actualY-scrollY + + term.setCursor(cursorX, cursorY) + updateStatusPos() +end + +-------------------------------------------------------------------------------- + +local function onClipboard(value) -- TODO: implement function + +end + +local function readFromFile(filename) + local f = io.open(filename) + if f then + for line in f:lines() do + table.insert(buffer, line) + end + f:close() + + end + setActualCursor(1, 1, true) +end + +local function writeToFile(filename) + local f = io.open(filename, "w") + for _,line in ipairs(buffer) do + f:write(line .. "\n") + end + f:close() +end + +-------------------------------------------------------------------------------- + +local functions = { + normalMode = function() + mode = modes.NORMAL + setStatus("--NORMAL--") + term.setCursor(cursorX, cursorY) + end, + insertMode = function() + mode = modes.INSERT + setStatus("--INSERT--") + end, + commandMode = function() + mode = modes.COMMAND + command = "" + setStatus(":") + term.setCursor(2, h+1) + end, + + append = function() + mode = modes.INSERT + setStatus("--INSERT--") + setActualCursor(actualX + 1, actualY, true) + end, + + left = function() + moveActualCursor(-1,0) + end, + down = function() + moveActualCursor(0,1) + end, + up = function() + moveActualCursor(0,-1) + end, + right = function() + moveActualCursor(1,0) + end, + + delChar = function() + local line = buffer[actualY] + buffer[actualY] = line:sub(1, actualX-1) .. line:sub(actualX+1, #line) + changed = true + redrawLine(actualY) + if actualX > #buffer[actualY] then + moveActualCursor(-1, 0) + end + end, + + scrollDown = function() -- (content moves up) + if scrollY < #buffer-1 then + --gpu.copy(1, 2, w, h-1, 0, -1) + --gpu.set(1, h, '~') + --gpu.fill(2, h, w, 1, ' ') + scrollY = scrollY + 1 + setActualCursor(actualX, actualY + 1, false) + redrawAll() + end + end, + scrollUp = function() -- (content moves down) + if scrollY > 0 then + scrollY = scrollY - 1 + setActualCursor(actualX, actualY - 1, false) + --gpu.copy(1, 1, w, h-1, 0, 1) + --redrawLine(actualY-scrollY) -- first line on screen + redrawAll() + end + end, + + moveToStart = function() + setActualCursor(actualX, 1, false) + --scrollX = 0 + scrollY = 0 + redrawAll() + end, + moveToEnd = function() + scrollY = #buffer - h + if scrollY < 0 then + scrollY = 0 + end + setActualCursor(actualX, #buffer, false) + redrawAll() + end, + moveHigh = function() + setScreenCursor(cursorX, 1) + end, + moveMiddle = function() + setScreenCursor(cursorX, math.floor(h/2)) + end, + moveLow = function() + setScreenCursor(cursorX, h) + end, +} + +-------------------------------------------------------------------------------- + +local function onKeyNormal(char, code) + local key = keyboard.keys[code] + + if keyboard.keys[code] == 'tab' then + keybind = "" + updateStatusKeybind() + return + end + + if ctrl then + keybind = keybind .. "^" + end + if alt then + keybind = keybind .. "" + end + + local ch = string.char(char) + + -- TODO: not really working correctly (shift + numbers) + if not keyboard.isControl(char) then + keybind = keybind .. ch + else + keybind = keybind .. key + end + + + local fn + fn = functions[keybinds.normal[keybind]] + if fn then + fn() + keybind = "" + end + updateStatusKeybind() +end + +local function onKeyInsert(char, code) + + if not keyboard.isControl(char) then + buffer[actualY] = string.insert(buffer[actualY], string.char(char), actualX-1) + changed = true + moveActualCursor(1, 0, true) + redrawLine(actualY) + else + if keyboard.keys[code] == 'tab' then + functions.normalMode() + if actualX > #buffer[actualY] then + moveActualCursor(-1, 0) + end + elseif keyboard.keys[code] == 'enter' then + -- TODO: move text after cursor on the new line + table.insert(buffer, actualY+1, "") + gpu.copy(1, cursorY+1, w, h-cursorY-1, 0, 1) + gpu.fill(1, cursorY+1, w, 1, ' ') + moveActualCursor(0, 1) + end + end + +end + +local function onKeyCommand(char, code) + + -- exit command mode + if keyboard.keys[code] == 'tab' then + functions.normalMode() + return + end + + -- execute command + if keyboard.keys[code] == 'enter' then + + -- quit + if command:sub(1,1) == 'q' then + if changed then + if command:sub(2,2) == '!' then + running = false + return + else + command = "" + functions.normalMode() + setStatusError("No write since last change (add ! to override)") + return + end + else + running = false + end + + -- write + elseif command:sub(1,1) == 'w' then + -- TODO: different filename as param + if readonly then + setStatusError("File is read only") + else + writeToFile(currentFilename) + changed = false + + if command:sub(2,2) == 'q' then + running = false + return + end + end + + -- edit a different file + elseif command:sub(1,1) == 'e' then + end + + command = "" + functions.normalMode() + return + end + + -- append typed char to command + if not keyboard.isControl(char) then + command = command .. string.char(char) + else + if keyboard.keys[code] == 'back' then + command = command:sub(1, -2) + end + end + setStatus(":" .. command) + term.setCursor(#command+2, h+1) +end + +local function onKeyDown(char, code) + --print('Key down: ' .. string.char(char) .. "," .. keyboard.keys[code]) + if keyboard.keys[code] == 'lcontrol' then + ctrl = true + elseif keyboard.keys[code] == 'lshift' then + shift = true + elseif keyboard.keys[code] == 'lmenu' then + alt = true + + elseif mode == modes.NORMAL then + onKeyNormal(char, code) + elseif mode == modes.INSERT then + onKeyInsert(char, code) + elseif mode == modes.COMMAND then + onKeyCommand(char, code) + end +end + +local function onKeyUp(char, code) + if keyboard.keys[code] == 'lcontrol' then + ctrl = false + elseif keyboard.keys[code] == 'lshift' then + shift = false + elseif keyboard.keys[code] == 'lmenu' then + alt = false + end +end + +-------------------------------------------------------------------------------- + +term.clear() +term.setCursorBlink(true) + + +-- if a filename was passed in as argument +if #args == 1 then + currentFilename = shell.resolve(args[1]) + local file_parentpath = fs.path(currentFilename) + + if fs.exists(file_parentpath) and not fs.isDirectory(file_parentpath) then + io.stderr:write(string.format("Not a directory: %s\n", file_parentpath)) + return 1 + end + + readonly = options.r or fs.get(currentFilename) == nil or fs.get(currentFilename).isReadOnly() + + if fs.isDirectory(currentFilename) then + io.stderr:write("file is a directory\n") + return 1 + elseif not fs.exists(currentFilename) and readonly then + io.stderr:write("file system is read only\n") + return 1 + end + + if not fs.exists(currentFilename) then + table.insert(buffer, "") + setStatus(string.format([["%s" [New File] ]], currentFilename)) + else + readFromFile(currentFilename) + + if readonly then + setStatus(string.format([["%s" [readonly] %dL]], currentFilename, #buffer)) + else + setStatus(string.format([["%s" %dL]], currentFilename, #buffer)) + end + end +else + -- if no filename was passed as argument, add an empty line to buffer + table.insert(buffer, "") + setStatus(" [New File] ") +end + +redrawAll() +setActualCursor(1,1,true) + +-------------------------------------------------------------------------------- + +while running do + local event, address, arg1, arg2, _ = term.pull() + if address == term.keyboard() or address == term.screen() then + if event == "key_down" then + onKeyDown(arg1, arg2) + elseif event == "key_up" then + onKeyUp(arg1, arg2) + elseif event == "clipboard" and not readonly then + onClipboard(arg1) + end + end +end + +-------------------------------------------------------------------------------- + +term.clear() +term.setCursorBlink(true)