Daniel_I_Am

myEditor

Feb 26th, 2018
128
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 18.36 KB | None | 0 0
  1. local event = require("event")
  2. local fs = require("filesystem")
  3. local keyboard = require("keyboard")
  4. local shell = require("shell")
  5. local term = require("term")
  6. local text = require("text")
  7. local unicode = require("unicode")
  8. local hl = require("hl")
  9.  
  10. if not term.isAvailable() then
  11.   return
  12. end
  13. local gpu = term.gpu()
  14. local args, options = shell.parse(...)
  15. if #args == 0 then
  16.   io.write("Usage: edit <filename>")
  17.   return
  18. end
  19.  
  20. local filename = shell.resolve(args[1])
  21.  
  22. local readonly = options.r or fs.get(filename) == nil or fs.get(filename).isReadOnly()
  23.  
  24. if fs.isDirectory(filename) then
  25.   io.stderr:write("file is a directory\n")
  26.   return 1
  27. elseif not fs.exists(filename) and readonly then
  28.   io.stderr:write("file system is read only\n")
  29.   return 1
  30. end
  31.  
  32. local function loadConfig()
  33.   -- Try to load user settings.
  34.   local env = {}
  35.   local config = loadfile("/etc/edit.cfg", nil, env)
  36.   if config then
  37.     pcall(config)
  38.   end
  39.   -- Fill in defaults.
  40.   env.keybinds = env.keybinds or {
  41.     left = {{"left"}},
  42.     right = {{"right"}},
  43.     up = {{"up"}},
  44.     down = {{"down"}},
  45.     home = {{"home"}},
  46.     eol = {{"end"}},
  47.     pageUp = {{"pageUp"}},
  48.     pageDown = {{"pageDown"}},
  49.  
  50.     backspace = {{"back"}},
  51.     delete = {{"delete"}},
  52.     deleteLine = {{"control", "delete"}, {"shift", "delete"}},
  53.     newline = {{"enter"}},
  54.  
  55.     save = {{"control", "s"}},
  56.     close = {{"control", "w"}},
  57.     find = {{"control", "f"}},
  58.     findnext = {{"control", "g"}, {"control", "n"}, {"f3"}}
  59.   }
  60.   -- Generate config file if it didn't exist.
  61.   if not config then
  62.     local root = fs.get("/")
  63.     if root and not root.isReadOnly() then
  64.       fs.makeDirectory("/etc")
  65.       local f = io.open("/etc/edit.cfg", "w")
  66.       if f then
  67.         local serialization = require("serialization")
  68.         for k, v in pairs(env) do
  69.           f:write(k.."="..tostring(serialization.serialize(v, math.huge)).."\n")
  70.         end
  71.         f:close()
  72.       end
  73.     end
  74.   end
  75.   return env
  76. end
  77.  
  78. term.clear()
  79. term.setCursorBlink(true)
  80.  
  81. local running = true
  82. local buffer = {}
  83. local scrollX, scrollY = 0, 0
  84. local config = loadConfig()
  85.  
  86. local getKeyBindHandler -- forward declaration for refind()
  87.  
  88. local function helpStatusText()
  89.   local function prettifyKeybind(label, command)
  90.     local keybind = type(config.keybinds) == "table" and config.keybinds[command]
  91.     if type(keybind) ~= "table" or type(keybind[1]) ~= "table" then return "" end
  92.     local alt, control, shift, key
  93.     for _, value in ipairs(keybind[1]) do
  94.       if value == "alt" then alt = true
  95.       elseif value == "control" then control = true
  96.       elseif value == "shift" then shift = true
  97.       else key = value end
  98.     end
  99.     if not key then return "" end
  100.     return label .. ": [" ..
  101.            (control and "Ctrl+" or "") ..
  102.            (alt and "Alt+" or "") ..
  103.            (shift and "Shift+" or "") ..
  104.            unicode.upper(key) ..
  105.            "] "
  106.   end
  107.   return prettifyKeybind("Save", "save") ..
  108.          prettifyKeybind("Close", "close") ..
  109.          prettifyKeybind("Find", "find")
  110. end
  111.  
  112. -------------------------------------------------------------------------------
  113.  
  114. local function setStatus(value)
  115.   local x, y, w, h = term.getGlobalArea()
  116.   value = unicode.wlen(value) > w - 10 and unicode.wtrunc(value, w - 9) or value
  117.   value = text.padRight(value, w - 10)
  118.   gpu.set(x, y + h - 1, value)
  119. end
  120.  
  121. local function getArea()
  122.   local x, y, w, h = term.getGlobalArea()
  123.   return x, y, w, h - 1
  124. end
  125.  
  126. local function removePrefix(line, length)
  127.   if length >= unicode.wlen(line) then
  128.     return ""
  129.   else
  130.     local prefix = unicode.wtrunc(line, length + 1)
  131.     local suffix = unicode.sub(line, unicode.len(prefix) + 1)
  132.     length = length - unicode.wlen(prefix)
  133.     if length > 0 then
  134.       suffix = (" "):rep(unicode.charWidth(suffix) - length) .. unicode.sub(suffix, 2)
  135.     end
  136.     return suffix
  137.   end
  138. end
  139.  
  140. local function lengthToChars(line, length)
  141.   if length > unicode.wlen(line) then
  142.     return unicode.len(line) + 1
  143.   else
  144.     local prefix = unicode.wtrunc(line, length)
  145.     return unicode.len(prefix) + 1
  146.   end
  147. end
  148.  
  149.  
  150. local function isWideAtPosition(line, x)
  151.   local index = lengthToChars(line, x)
  152.   if index > unicode.len(line) then
  153.     return false, false
  154.   end
  155.   local prefix = unicode.sub(line, 1, index)
  156.   local char = unicode.sub(line, index, index)
  157.   --isWide, isRight
  158.   return unicode.isWide(char), unicode.wlen(prefix) == x
  159. end
  160.  
  161. local function drawLine(x, y, w, h, lineNr)
  162.   local yLocal = lineNr - scrollY
  163.   if yLocal > 0 and yLocal <= h then
  164.     local str = removePrefix(buffer[lineNr] or "", scrollX)
  165.     str = unicode.wlen(str) > w and unicode.wtrunc(str, w + 1) or str
  166.     str = text.padRight(str, w)
  167.     hl.parseline(x, y - 1 + lineNr - scrollY, str)
  168.     --gpu.set(x, y - 1 + lineNr - scrollY, str)
  169.   end
  170. end
  171.  
  172. local function getCursor()
  173.   local cx, cy = term.getCursor()
  174.   return cx + scrollX, cy + scrollY
  175. end
  176.  
  177. local function line()
  178.   local cbx, cby = getCursor()
  179.   return buffer[cby]
  180. end
  181.  
  182. local function getNormalizedCursor()
  183.   local cbx, cby = getCursor()
  184.   local wide, right = isWideAtPosition(buffer[cby], cbx)
  185.   if wide and right then
  186.     cbx = cbx - 1
  187.   end
  188.   return cbx, cby
  189. end
  190.  
  191. local function setCursor(nbx, nby)
  192.   local x, y, w, h = getArea()
  193.   nby = math.max(1, math.min(#buffer, nby))
  194.  
  195.   local ncy = nby - scrollY
  196.   if ncy > h then
  197.     term.setCursorBlink(false)
  198.     local sy = nby - h
  199.     local dy = math.abs(scrollY - sy)
  200.     scrollY = sy
  201.     if h > dy then
  202.       gpu.copy(x, y + dy, w, h - dy, 0, -dy)
  203.     end
  204.     for lineNr = nby - (math.min(dy, h) - 1), nby do
  205.       drawLine(x, y, w, h, lineNr)
  206.     end
  207.   elseif ncy < 1 then
  208.     term.setCursorBlink(false)
  209.     local sy = nby - 1
  210.     local dy = math.abs(scrollY - sy)
  211.     scrollY = sy
  212.     if h > dy then
  213.       gpu.copy(x, y, w, h - dy, 0, dy)
  214.     end
  215.     for lineNr = nby, nby + (math.min(dy, h) - 1) do
  216.       drawLine(x, y, w, h, lineNr)
  217.     end
  218.   end
  219.   term.setCursor(term.getCursor(), nby - scrollY)
  220.  
  221.   nbx = math.max(1, math.min(unicode.wlen(line()) + 1, nbx))
  222.   local wide, right = isWideAtPosition(line(), nbx)
  223.   local ncx = nbx - scrollX
  224.   if ncx > w or (ncx + 1 > w and wide and not right) then
  225.     term.setCursorBlink(false)
  226.     scrollX = nbx - w + ((wide and not right) and 1 or 0)
  227.     for lineNr = 1 + scrollY, math.min(h + scrollY, #buffer) do
  228.       drawLine(x, y, w, h, lineNr)
  229.     end
  230.   elseif ncx < 1 or (ncx - 1 < 1 and wide and right) then
  231.     term.setCursorBlink(false)
  232.     scrollX = nbx - 1 - ((wide and right) and 1 or 0)
  233.     for lineNr = 1 + scrollY, math.min(h + scrollY, #buffer) do
  234.       drawLine(x, y, w, h, lineNr)
  235.     end
  236.   end
  237.   term.setCursor(nbx - scrollX, nby - scrollY)
  238.   --update with term lib
  239.   nbx, nby = getCursor()
  240.   gpu.set(x + w - 10, y + h, text.padLeft(string.format("%d,%d", nby, nbx), 10))
  241. end
  242.  
  243. local function highlight(bx, by, length, enabled)
  244.   local x, y, w, h = getArea()
  245.   local cx, cy = bx - scrollX, by - scrollY
  246.   cx = math.max(1, math.min(w, cx))
  247.   cy = math.max(1, math.min(h, cy))
  248.   length = math.max(1, math.min(w - cx, length))
  249.  
  250.   local fg, fgp = gpu.getForeground()
  251.   local bg, bgp = gpu.getBackground()
  252.   if enabled then
  253.     gpu.setForeground(bg, bgp)
  254.     gpu.setBackground(fg, fgp)
  255.   end
  256.   local indexFrom = lengthToChars(buffer[by], bx)
  257.   local value = unicode.sub(buffer[by], indexFrom)
  258.   if unicode.wlen(value) > length then
  259.     value = unicode.wtrunc(value, length + 1)
  260.   end
  261.   gpu.set(x - 1 + cx, y - 1 + cy, value)
  262.   if enabled then
  263.     gpu.setForeground(fg, fgp)
  264.     gpu.setBackground(bg, bgp)
  265.   end
  266. end
  267.  
  268. local function home()
  269.   local cbx, cby = getCursor()
  270.   setCursor(1, cby)
  271. end
  272.  
  273. local function ende()
  274.   local cbx, cby = getCursor()
  275.   setCursor(unicode.wlen(line()) + 1, cby)
  276. end
  277.  
  278. local function left()
  279.   local cbx, cby = getNormalizedCursor()
  280.   if cbx > 1 then
  281.     local wideTarget, rightTarget = isWideAtPosition(line(), cbx - 1)
  282.     if wideTarget and rightTarget then
  283.       setCursor(cbx - 2, cby)
  284.     else
  285.       setCursor(cbx - 1, cby)
  286.     end
  287.     return true -- for backspace
  288.   elseif cby > 1 then
  289.     setCursor(cbx, cby - 1)
  290.     ende()
  291.     return true -- again, for backspace
  292.   end
  293. end
  294.  
  295. local function right(n)
  296.   n = n or 1
  297.   local cbx, cby = getNormalizedCursor()
  298.   local be = unicode.wlen(line()) + 1
  299.   local wide, right = isWideAtPosition(line(), cbx + n)
  300.   if wide and right then
  301.     n = n + 1
  302.   end
  303.   if cbx + n <= be then
  304.     setCursor(cbx + n, cby)
  305.   elseif cby < #buffer then
  306.     setCursor(1, cby + 1)
  307.   end
  308. end
  309.  
  310. local function up(n)
  311.   n = n or 1
  312.   local cbx, cby = getCursor()
  313.   if cby > 1 then
  314.     setCursor(cbx, cby - n)
  315.   end
  316. end
  317.  
  318. local function down(n)
  319.   n = n or 1
  320.   local cbx, cby = getCursor()
  321.   if cby < #buffer then
  322.     setCursor(cbx, cby + n)
  323.   end
  324. end
  325.  
  326. local function delete(fullRow)
  327.   local cx, cy = term.getCursor()
  328.   local cbx, cby = getCursor()
  329.   local x, y, w, h = getArea()
  330.   local function deleteRow(row)
  331.     local content = table.remove(buffer, row)
  332.     local rcy = cy + (row - cby)
  333.     if rcy <= h then
  334.       gpu.copy(x, y + rcy, w, h - rcy, 0, -1)
  335.       drawLine(x, y, w, h, row + (h - rcy))
  336.     end
  337.     return content
  338.   end
  339.   if fullRow then
  340.     term.setCursorBlink(false)
  341.     if #buffer > 1 then
  342.       deleteRow(cby)
  343.     else
  344.       buffer[cby] = ""
  345.       gpu.fill(x, y - 1 + cy, w, 1, " ")
  346.     end
  347.     setCursor(1, cby)
  348.   elseif cbx <= unicode.wlen(line()) then
  349.     term.setCursorBlink(false)
  350.     local index = lengthToChars(line(), cbx)
  351.     buffer[cby] = unicode.sub(line(), 1, index - 1) ..
  352.                   unicode.sub(line(), index + 1)
  353.     drawLine(x, y, w, h, cby)
  354.   elseif cby < #buffer then
  355.     term.setCursorBlink(false)
  356.     local append = deleteRow(cby + 1)
  357.     buffer[cby] = buffer[cby] .. append
  358.     drawLine(x, y, w, h, cby)
  359.   else
  360.     return
  361.   end
  362.   setStatus(helpStatusText())
  363. end
  364.  
  365. local function insert(value)
  366.   if not value or unicode.len(value) < 1 then
  367.     return
  368.   end
  369.   term.setCursorBlink(false)
  370.   local cx, cy = term.getCursor()
  371.   local cbx, cby = getCursor()
  372.   local x, y, w, h = getArea()
  373.   local index = lengthToChars(line(), cbx)
  374.   buffer[cby] = unicode.sub(line(), 1, index - 1) ..
  375.                 value ..
  376.                 unicode.sub(line(), index)
  377.   drawLine(x, y, w, h, cby)
  378.   right(unicode.wlen(value))
  379.   setStatus(helpStatusText())
  380. end
  381.  
  382. local function enter()
  383.   term.setCursorBlink(false)
  384.   local cx, cy = term.getCursor()
  385.   local cbx, cby = getCursor()
  386.   local x, y, w, h = getArea()
  387.   local index = lengthToChars(line(), cbx)
  388.   table.insert(buffer, cby + 1, unicode.sub(buffer[cby], index))
  389.   buffer[cby] = unicode.sub(buffer[cby], 1, index - 1)
  390.   drawLine(x, y, w, h, cby)
  391.   if cy < h then
  392.     if cy < h - 1 then
  393.       gpu.copy(x, y + cy, w, h - (cy + 1), 0, 1)
  394.     end
  395.     drawLine(x, y, w, h, cby + 1)
  396.   end
  397.   setCursor(1, cby + 1)
  398.   setStatus(helpStatusText())
  399. end
  400.  
  401. local findText = ""
  402.  
  403. local function find()
  404.   local x, y, w, h = getArea()
  405.   local cx, cy = term.getCursor()
  406.   local cbx, cby = getCursor()
  407.   local ibx, iby = cbx, cby
  408.   while running do
  409.     if unicode.len(findText) > 0 then
  410.       local sx, sy
  411.       for syo = 1, #buffer do -- iterate lines with wraparound
  412.         sy = (iby + syo - 1 + #buffer - 1) % #buffer + 1
  413.         sx = string.find(buffer[sy], findText, syo == 1 and ibx or 1, true)
  414.         if sx and (sx >= ibx or syo > 1) then
  415.           break
  416.         end
  417.       end
  418.       if not sx then -- special case for single matches
  419.         sy = iby
  420.         sx = string.find(buffer[sy], findText, nil, true)
  421.       end
  422.       if sx then
  423.         sx = unicode.wlen(string.sub(buffer[sy], 1, sx - 1)) + 1
  424.         cbx, cby = sx, sy
  425.         setCursor(cbx, cby)
  426.         highlight(cbx, cby, unicode.wlen(findText), true)
  427.       end
  428.     end
  429.     term.setCursor(7 + unicode.wlen(findText), h + 1)
  430.     setStatus("Find: " .. findText)
  431.  
  432.     local _, address, char, code = term.pull("key_down")
  433.     if address == term.keyboard() then
  434.       local handler, name = getKeyBindHandler(code)
  435.       highlight(cbx, cby, unicode.wlen(findText), false)
  436.       if name == "newline" then
  437.         break
  438.       elseif name == "close" then
  439.         handler()
  440.       elseif name == "backspace" then
  441.         findText = unicode.sub(findText, 1, -2)
  442.       elseif name == "find" or name == "findnext" then
  443.         ibx = cbx + 1
  444.         iby = cby
  445.       elseif not keyboard.isControl(char) then
  446.         findText = findText .. unicode.char(char)
  447.       end
  448.     end
  449.   end
  450.   setCursor(cbx, cby)
  451.   setStatus(helpStatusText())
  452. end
  453.  
  454. -------------------------------------------------------------------------------
  455.  
  456. local keyBindHandlers = {
  457.   left = left,
  458.   right = right,
  459.   up = up,
  460.   down = down,
  461.   home = home,
  462.   eol = ende,
  463.   pageUp = function()
  464.     local x, y, w, h = getArea()
  465.     up(h - 1)
  466.   end,
  467.   pageDown = function()
  468.     local x, y, w, h = getArea()
  469.     down(h - 1)
  470.   end,
  471.  
  472.   backspace = function()
  473.     if not readonly and left() then
  474.       delete()
  475.     end
  476.   end,
  477.   delete = function()
  478.     if not readonly then
  479.       delete()
  480.     end
  481.   end,
  482.   deleteLine = function()
  483.     if not readonly then
  484.       delete(true)
  485.     end
  486.   end,
  487.   newline = function()
  488.     if not readonly then
  489.       enter()
  490.     end
  491.   end,
  492.  
  493.   save = function()
  494.     if readonly then return end
  495.     local new = not fs.exists(filename)
  496.     local backup
  497.     if not new then
  498.       backup = filename .. "~"
  499.       for i = 1, math.huge do
  500.         if not fs.exists(backup) then
  501.           break
  502.         end
  503.         backup = filename .. "~" .. i
  504.       end
  505.       fs.copy(filename, backup)
  506.     end
  507.     local f, reason = io.open(filename, "w")
  508.     if f then
  509.       local chars, firstLine = 0, true
  510.       for _, line in ipairs(buffer) do
  511.         if not firstLine then
  512.           line = "\n" .. line
  513.         end
  514.         firstLine = false
  515.         f:write(line)
  516.         chars = chars + unicode.len(line)
  517.       end
  518.       f:close()
  519.       local format
  520.       if new then
  521.         format = [["%s" [New] %dL,%dC written]]
  522.       else
  523.         format = [["%s" %dL,%dC written]]
  524.       end
  525.       setStatus(string.format(format, fs.name(filename), #buffer, chars))
  526.     else
  527.       setStatus(reason)
  528.     end
  529.     if not new then
  530.       fs.remove(backup)
  531.     end
  532.   end,
  533.   close = function()
  534.     -- TODO ask to save if changed
  535.     running = false
  536.   end,
  537.   find = function()
  538.     findText = ""
  539.     find()
  540.   end,
  541.   findnext = find
  542. }
  543.  
  544. getKeyBindHandler = function(code)
  545.   if type(config.keybinds) ~= "table" then return end
  546.   -- Look for matches, prefer more 'precise' keybinds, e.g. prefer
  547.   -- ctrl+del over del.
  548.   local result, resultName, resultWeight = nil, nil, 0
  549.   for command, keybinds in pairs(config.keybinds) do
  550.     if type(keybinds) == "table" and keyBindHandlers[command] then
  551.       for _, keybind in ipairs(keybinds) do
  552.         if type(keybind) == "table" then
  553.           local alt, control, shift, key
  554.           for _, value in ipairs(keybind) do
  555.             if value == "alt" then alt = true
  556.             elseif value == "control" then control = true
  557.             elseif value == "shift" then shift = true
  558.             else key = value end
  559.           end
  560.           local keyboardAddress = term.keyboard()
  561.           if (not alt or keyboard.isAltDown(keyboardAddress)) and
  562.              (not control or keyboard.isControlDown(keyboardAddress)) and
  563.              (not shift or keyboard.isShiftDown(keyboardAddress)) and
  564.              code == keyboard.keys[key] and
  565.              #keybind > resultWeight
  566.           then
  567.             resultWeight = #keybind
  568.             resultName = command
  569.             result = keyBindHandlers[command]
  570.           end
  571.         end
  572.       end
  573.     end
  574.   end
  575.   return result, resultName
  576. end
  577.  
  578. -------------------------------------------------------------------------------
  579.  
  580. local function onKeyDown(char, code)
  581.   local handler = getKeyBindHandler(code)
  582.   if handler then
  583.     handler()
  584.   elseif readonly and code == keyboard.keys.q then
  585.     running = false
  586.   elseif not readonly then
  587.     if not keyboard.isControl(char) then
  588.       insert(unicode.char(char))
  589.     elseif unicode.char(char) == "\t" then
  590.       insert("  ")
  591.     end
  592.   end
  593. end
  594.  
  595. local function onClipboard(value)
  596.   value = value:gsub("\r\n", "\n")
  597.   local cbx, cby = getCursor()
  598.   local start = 1
  599.   local l = value:find("\n", 1, true)
  600.   if l then
  601.     repeat
  602.       local line = string.sub(value, start, l - 1)
  603.       line = text.detab(line, 2)
  604.       insert(line)
  605.       enter()
  606.       start = l + 1
  607.       l = value:find("\n", start, true)
  608.     until not l
  609.   end
  610.   insert(string.sub(value, start))
  611. end
  612.  
  613. local function onClick(x, y)
  614.   setCursor(x + scrollX, y + scrollY)
  615. end
  616.  
  617. local function onScroll(direction)
  618.   local cbx, cby = getCursor()
  619.   setCursor(cbx, cby - direction * 12)
  620. end
  621.  
  622. -------------------------------------------------------------------------------
  623.  
  624. do
  625.   local f = io.open(filename)
  626.   if f then
  627.     local x, y, w, h = getArea()
  628.     local chars = 0
  629.     for line in f:lines() do
  630.       if line:sub(-1) == "\r" then
  631.         line = line:sub(1, -2)
  632.       end
  633.       table.insert(buffer, line)
  634.       chars = chars + unicode.len(line)
  635.       if #buffer <= h then
  636.         drawLine(x, y, w, h, #buffer)
  637.       end
  638.     end
  639.     f:close()
  640.     if #buffer == 0 then
  641.       table.insert(buffer, "")
  642.     end
  643.     local format
  644.     if readonly then
  645.       format = [["%s" [readonly] %dL,%dC]]
  646.     else
  647.       format = [["%s" %dL,%dC]]
  648.     end
  649.     setStatus(string.format(format, fs.name(filename), #buffer, chars))
  650.   else
  651.     table.insert(buffer, "")
  652.     setStatus(string.format([["%s" [New File] ]], fs.name(filename)))
  653.   end
  654.   setCursor(1, 1)
  655. end
  656.  
  657. while running do
  658.   local event, address, arg1, arg2, arg3 = term.pull()
  659.   if address == term.keyboard() or address == term.screen() then
  660.     local blink = true
  661.     if event == "key_down" then
  662.       onKeyDown(arg1, arg2)
  663.     elseif event == "clipboard" and not readonly then
  664.       onClipboard(arg1)
  665.     elseif event == "touch" or event == "drag" then
  666.       local x, y, w, h = getArea()
  667.       arg1 = arg1 - x + 1
  668.       arg2 = arg2 - y + 1
  669.       if arg1 >= 1 and arg2 >= 1 and arg1 <= w and arg2 <= h then
  670.         onClick(arg1, arg2)
  671.       end
  672.     elseif event == "scroll" then
  673.       onScroll(arg3)
  674.     else
  675.       blink = false
  676.     end
  677.     if blink then
  678.       term.setCursorBlink(true)
  679.     end
  680.   end
  681. end
  682.  
  683. term.clear()
  684. term.setCursorBlink(false)
Advertisement
Add Comment
Please, Sign In to add comment