You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

524 lines
14 KiB

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,
delCharBack = function()
if actualX == 1 then return end
local line = buffer[actualY]
buffer[actualY] = line:sub(1, actualX-2) .. line:sub(actualX, #line)
changed = true
redrawLine(actualY)
moveActualCursor(-1, 0)
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 .. "<alt>"
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
local beforeCursor = string.sub(buffer[actualY], 0, actualX-1)
local afterCursor = string.sub(buffer[actualY], actualX, #buffer[actualY])
buffer[actualY] = beforeCursor
table.insert(buffer, actualY+1, afterCursor)
gpu.copy(1, cursorY+1, w, h-cursorY-1, 0, 1) -- move text down to make space for the new line
gpu.fill(1, cursorY+1, w, 1, ' ')
redrawLine(actualY)
redrawLine(actualY+1)
setActualCursor(0, actualY+1, false)
elseif keyboard.keys[code] == 'back' then
functions.delCharBack()
--local line = buffer[actualY]
--buffer[actualY] = line:sub(1, actualX-2) .. line:sub(actualX, #line)
--redrawLine(actualY)
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)