HangMan23

Untitled

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