Guest User

edit.lua

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