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