· 6 years ago · Aug 28, 2019, 03:12 PM
1local tArgs = { ... }
2
3
4--[[
5 Release version of Lattix, Version:
6 ]]
7local version = "a0.3"
8--[[
9 A lot of features from Lattice weren't adopted yet :/ but at least you can enjoy the extra stability
10
11 Made by Konlab
12
13 If you wonder why the screen is so stable (no flickering) then it's because of the awesomeness of CrazedProgrammmer's Surface API, go check it out here: http://www.computercraft.info/forums2/index.php?/topic/22397-surface-api-162/
14
15 Big thanks to:
16 EveryOS (thanks for pointing out the command thing)
17]]
18
19
20--CONFIG
21--colors of lattix
22local lat_theme = {
23 menu_back = colors.red;
24 menu_text = colors.white;
25 main_back = colors.white;
26 main_text = colors.black;
27 selected_text = colors.cyan;
28 dir_text = colors.lime;
29 multiselect_back = colors.lightGray;
30 blank_back = colors.white;
31 blank_text = colors.black;
32}
33--colors of lattix on basic comps
34local lat_basic_theme = {
35 menu_back = colors.gray;
36 menu_text = colors.white;
37 main_back = colors.white;
38 main_text = colors.black;
39 selected_text = colors.lightGray;
40 dir_text = colors.black;
41 multiselect_back = colors.white;
42 blank_back = colors.white;
43 blank_text = colors.black;
44}
45local dirprefix = "" --this text gets added before dirs
46local basic_dirprefix = "[] " --this text gets added before dirs on non-advanced computers
47local selectprefix = "" --this text gets added before selected things (even in multiselect)
48local basic_selectprefix = "> " --this text gets added before selected things (even in multiselect) on non-advanced computers
49--which programs to use to open .<xtension> files. Special codes: "run" and "api" and special extension "def" - def will be used for every extension not listed (def must be something), run runs it without args, api loads it with os.loadAPI (yes ikr very useful rn since os.loadAPI doesn't like weird extensions). The empty string indicates no extension
50--the file is opened by running the program chosen below with arg #1 being the path to the file
51local defaultPrograms = {
52 [""] = "/rom/programs/edit.lua",
53 ["txt"] = "/rom/programs/edit.lua",
54 ["cfg"] = "/rom/programs/edit.lua",
55 ["config"] = "/rom/programs/edit.lua",
56 ["nfp"] = "/rom/programs/fun/advanced/paint.lua",
57 ["lua"] = "run",
58-- ["api"] = "api", --if a custom OS sets up os.loadAPI to work with .api and stores its apis in .api then this could work
59 ["def"] = "/rom/programs/edit.lua"
60}
61--which programs can be used to create files (think clicking the New... button) Special code: choose means the program is chosen manually through a dialog
62--these programs are run with the path to the file that is to be created as first argument
63local editors = {
64 ["Other"] = "choose",
65 ["NFP painting"] = "/rom/programs/fun/advanced/paint.lua",
66 ["Lua script"] = "/rom/programs/edit.lua",
67 ["Text file"] = "/rom/programs/edit.lua"
68
69
70
71}
72--Extensions for the above editors
73local editors_extensions = {
74 ["NFP painting"] = "nfp",
75 ["Lua script"] = "lua",
76 ["Text file"] = "txt"
77
78
79}
80--paths and links to apis/third party software
81local surfacepath = "surface"
82
83--[[
84 TODO:
85 finish command line equivalents for the gui (e.g. delete <arg>)
86 open in (folders), open with (files) - design decisions!!!
87 run with args ... fix
88 pack/unpack folders
89 remove remnants of custom root/home folder
90 ctrl+c, ctrl+v support for files, make the bar clickable so more editing options can get available for the command thing
91 Have all messages have a large table - like localization
92 Replace shell.run and os.loadAPI everywhere with better alternatives/add config for OSes
93 make run run things inside the session
94 make a GUI for config and a config file, also different themes built in
95 Parent folder... popup option in search results only
96
97 multiselection
98 file choose box and dir choose box dialogs with mini-browser inside (for open with... etc.)
99
100 for a larger update, will probably never happen:
101
102 plugin support?
103 splitting this huge file into multiple files for simplicity and clearer code
104 apply consistent should-be-a-separate-function-or-not rules
105 consistent refresh and multi surface
106]]
107
108--code adopted and modified from CC's source code
109local function pastebinGet(paste, path)
110
111 if not http then
112 error("HTTP disabled", 2)
113 end
114 --- Attempts to guess the pastebin ID from the given code or URL
115 local extractId = function (paste)
116 local patterns = {
117 "^([%a%d]+)$",
118 "^https?://pastebin.com/([%a%d]+)$",
119 "^pastebin.com/([%a%d]+)$",
120 "^https?://pastebin.com/raw/([%a%d]+)$",
121 "^pastebin.com/raw/([%a%d]+)$",
122 }
123
124 for i = 1, #patterns do
125 local code = paste:match( patterns[i] )
126 if code then return code end
127 end
128
129 return nil
130 end
131
132 local get = function (url)
133 local paste = extractId( url )
134 if not paste then
135 error( "Invalid pastebin code.", 0 )
136 return
137 end
138
139 -- Add a cache buster so that spam protection is re-checked
140 local cacheBuster = ("%x"):format(math.random(0, 2^30))
141 local response, err = http.get(
142 "https://pastebin.com/raw/"..textutils.urlEncode( paste ).."?cb="..cacheBuster
143 )
144
145 if response then
146 -- If spam protection is activated, we get redirected to /paste with Content-Type: text/html
147 local headers = response.getResponseHeaders()
148 if not headers["Content-Type"] or not headers["Content-Type"]:find( "^text/plain" ) then
149 error( "Pastebin blocked the download due to spam protection. Please complete the captcha in a web browser: https://pastebin.com/" .. textutils.urlEncode( paste ) , 0)
150 return
151 end
152
153 local sResponse = response.readAll()
154 response.close()
155 return sResponse
156 else
157 error (err, 0)
158 end
159 end
160
161 -- Determine file to download
162 local sCode = paste
163 local sPath = path
164 if fs.exists( sPath ) then
165 error( "File already exists", 0 )
166 return
167 end
168
169 -- GET the contents from pastebin
170 local res = get(sCode)
171 if res then
172 local file = fs.open( sPath, "w" )
173 file.write( res )
174 file.close()
175 end
176end
177local function wGet(url, sPath)
178
179 if not http then
180 error( "HTTP disabled", 2 )
181 end
182
183 local function getFilename( sUrl )
184 sUrl = sUrl:gsub( "[#?].*" , "" ):gsub( "/+$" , "" )
185 return sUrl:match( "/([^/]+)$" )
186 end
187
188 local function get( sUrl )
189 -- Check if the URL is valid
190 local ok, err = http.checkURL( url )
191 if not ok then
192 error( err or "Invalid URL.", 0 )
193 return
194 end
195
196 local response = http.get( sUrl , nil , true )
197 if not response then
198 error( "Failed." )
199 end
200
201 local sResponse = response.readAll()
202 response.close()
203 return sResponse
204 end
205
206 if fs.exists( sPath ) then
207 error( "File already exists", 0 )
208 return
209 end
210
211 local res = get(url)
212 if not res then return end
213
214 local file = fs.open( sPath, "wb" )
215 file.write( res )
216 file.close()
217end
218
219
220local function program()
221
222--api loading
223if not surface then
224 if fs.exists(surfacepath) then
225 os.loadAPI(surfacepath)
226 else
227 print("installing missing component: " .. surfacepath)
228 pastebinGet("J2Y288mW", surfacepath)
229 os.loadAPI(surfacepath)
230 end
231end
232--term args
233local home --the directory Lattix is opened in
234local root --the root directory that you're not allowed to go above
235
236if #tArgs > 0 then
237 home = tArgs[1]
238else
239 home = ""
240end
241if #tArgs > 1 then
242 root = tArgs[2]
243else
244 root = ""
245end
246
247root = fs.combine("", root)
248home = fs.combine("", home)
249
250--variables
251local clipboard --a file path/table of files
252local clip_cut = false -- original file(s) will be deleted if this is true
253local history = {}
254local w,h = term.getSize()
255
256
257
258--setting up path
259if not term.isColor() then
260 lat_theme = lat_basic_theme
261 dirprefix = basic_dirprefix
262 selectprefix = basic_selectprefix
263end
264local path = root
265
266if fs.isDir(fs.combine(root,home)) then
267 path = fs.combine(root,home)
268elseif fs.isDir(root) then
269 path = root
270else
271 error("Not valid root folder",0)
272end
273local scroll = 0
274local selected = 0
275local endprogram = false
276local isCtrlDown = false
277local isShiftDown = false
278local selection = {}
279local isMultiSelect = false
280
281local index = 0
282local empty = function() end
283local shell_input = ""
284
285local itemNames = {} --displayed texts
286local itemPaths = {} --paths to items
287local customTitle --if not nil then the title of the program is replaced with this
288
289local prewrite = {} --for fancy command stuff
290
291--setting up the second session
292local alt_histories = {
293{path}, {path}, {path}, {path}
294}
295local alt_paths = {
296 [1] = path,
297 [2] = path,
298 [3] = path,
299 [4] = path
300}
301local alt_scrolls = {
302 0,0,0,0
303}
304local alt_selected = {
305 0,0,0,0
306}
307local cSession = 1
308
309local surf = surface.create(w,h," ",lat_theme.blank_back, lat_theme.blank_text)
310
311local pathValid = true
312
313--functions yet undefined:
314local redraw --so that any gui drawing can call a redraw
315local remap --so that redraw and remap can be overwritten if neccessary
316local restore --so that special popups that use the main loop can restore the file browser functionality
317--helpers
318local function mathdown(num)
319 if num % 1 ~= 0 then
320 return math.floor(num)
321 end
322 return num - 1
323end
324local function sort(path) --returns a sorted table of folder names and file names at path with folders being first
325 local tbl = {}
326 items = fs.list(path)
327 table.sort(items)
328 --insert dirs
329 for i=1,#items do
330 if fs.isDir(fs.combine(path,items[i])) then
331 tbl[#tbl+1] = fs.combine(path, items[i])
332 end
333 end
334 for i=1,#items do
335 if not fs.isDir(fs.combine(path,items[i])) then
336 tbl[#tbl+1] = fs.combine(path, items[i])
337 end
338 end
339 return tbl
340end
341local function formatNumber(num)
342 local extra = {"B","KB","MB"}
343 local eindex = 1
344 while num > 1024 do
345 num = num / 1024
346 num = math.floor(num)
347 eindex = eindex + 1
348 end
349 return tostring(num..extra[eindex])
350end
351local function posToIndex(maxn,x,y,scroll)
352 return y+scroll-2 < maxn and y+scroll-1 or 0
353end
354local function IndexToPos(index,scroll)
355 return 2,index-scroll
356end
357local function clear()
358 surf:clear()
359 surf:render()
360 term.setCursorPos(1,1)
361end
362
363local function readFolderRaw(folder)
364 customTitle = nil --to quit all special view modes
365 pathValid = true
366 path = folder
367 itemNames = {}
368 itemPaths = {}
369
370 local t = sort(folder)
371 for i=1,#t do
372 itemNames[i] = fs.getName(t[i])
373 itemPaths[i] = t[i]
374 end
375
376end
377
378local function switchFolderRaw(folder) --folder is in absolute path, does not add to history
379 readFolderRaw(folder)
380 selected = 0
381 scroll = 0
382 shell_input = ""
383end
384
385local function switchFolder(folder) --folder is in absolute path
386 history[#history + 1] = path
387 switchFolderRaw(folder)
388end
389
390local function waitForKeyOrClick()
391 while true do
392 local e = os.pullEvent()
393 if e == "key" or e == "mouse_click" then
394 break
395 end
396 end
397end
398local function split(inputstr, sep)
399 if sep == nil then
400 sep = "%s"
401 end
402 local t={} ; i=1
403 for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
404 t[i] = str
405 i = i + 1
406 end
407 return t
408end
409local function switchSessions(newI)
410 --if newI == cSession then return end
411 alt_histories[cSession] = history
412 alt_paths[cSession] = path
413 alt_scrolls[cSession] = scroll
414 alt_selected[cSession] = selected
415 history = alt_histories[newI]
416 --after all we need to update the item list
417 switchFolderRaw(alt_paths[newI])
418 scroll = alt_scrolls[newI]
419 selected = alt_selected[newI]
420 cSession = newI
421end
422local function extensionRead(extension, width, bcolor, tcolor, dtext)
423 if #prewrite > 0 then
424 local tmp = prewrite[1]
425 table.remove(prewrite, 1)
426 return tmp
427 end
428 local x,y = term.getCursorPos()
429 term.setCursorBlink(true)
430 local t = dtext or ""
431 local i = #t
432 local scroll = 0
433 local tbasis
434
435 while true do
436 surf:drawLine(x,y,x+width,y," ", bcolor,tcolor)
437 tbasis = t .. extension
438 if i < scroll then
439 scroll = i
440 end
441 if i > scroll + width - #extension then
442 scroll = i - width + #extension
443 end
444 if #tbasis > width then
445 if scroll + width > #tbasis then
446 scroll = #tbasis - width
447 end
448 tbasis = tbasis:sub(1+scroll, width+scroll)
449 else
450 scroll = 0
451 end
452 surf:drawText(x,y,tbasis,bcolor,tcolor)
453 surf:render()
454 term.setCursorPos(x+i-scroll, y)
455 local ev = {os.pullEvent()}
456 local e,k = ev[1], ev[2]
457 repeat
458 if e == "paste" then
459 e = "char"
460 table.remove(ev, 1)
461 k = table.concat(ev, " ")
462 end
463 if e == "char" then
464 if i == #t then
465 t = t .. k
466 i = i + #k
467 elseif i == 0 then
468 t = k .. t
469 i = i + #k
470 else
471 t = t:sub(1,i) .. k .. t:sub(i+1, #t)
472 i = i + #k
473 end
474 end
475
476 if e == "key" then
477 local mini = 0
478 local maxi = #t
479 if k == keys.left and i > mini then
480 i = i - 1
481 break
482 end
483 if k == keys.right and i < maxi then
484 i = i + 1
485 break
486 end
487
488 if k == keys.up then
489 i = mini
490 break
491 end
492 if k == keys.down then
493 i = maxi
494 break
495 end
496 if k == keys.enter then
497 term.setCursorBlink(false)
498 return t .. extension
499 end
500 if k == keys.delete and i < #t then
501 t = t:sub(1,i) .. t:sub(i+2,#t)
502 break
503 end
504 if k == keys.backspace and i > 0 then
505 t = t:sub(1,i-1) .. t:sub(i+1, #t)
506 i = i - 1
507 break
508 end
509 end
510 until true
511 end
512end
513--custom dialoges (just the GUI, it also draws it)
514local function drawInfobox(txt) --just info text, no buttons
515 surf:fillRect(math.floor(w/2-(#txt+2)/2), math.floor(h/2)-2, math.floor(w/2+(#txt+2)/2),math.floor(h/2)+2, " ", lat_theme.menu_back, lat_theme.menu_text)
516 surf:drawText(math.floor(w/2-(#txt+2)/2)+1,math.floor(h/2)-1, txt, lat_theme.menu_back, lat_theme.menu_text)
517 surf:render()
518end
519local function drawTextbox(txt) --text feedback
520 surf:fillRect(math.floor(w/2-(#txt+2)/2),math.floor(h/2)-2,math.floor(w/2+(#txt+2)/2),math.floor(h/2)+2, " ", lat_theme.menu_back, lat_theme.menu_text)
521 surf:drawText(math.floor(w/2-(#txt+2)/2)+1,math.floor(h/2)-1, txt, lat_theme.menu_back, lat_theme.menu_text)
522 surf:render()
523 term.setCursorPos(math.floor(w/2-(#txt+2)/2)+1,math.floor(h/2)+1)
524end
525local function drawButtonBox(txt,buttons) --multiple buttons, configurable
526 if type(buttons) ~= "table" then
527 buttons = {{" Yes ",colors.red,colors.white},{" No ",colors.gray,colors.white}}
528 end
529 if txt == "" or txt == nil then
530 txt = " Are you sure? "
531 end
532
533 drawTextbox(txt) --reuse the rectangle
534 local x,y = term.getCursorPos()
535
536 for i=1,#buttons do
537 x = math.floor((w/2-(#txt+2)/2)+((#txt+2)/i)*(i-1)) + 1
538 if term.isColor() then
539 surf:drawText(x,y, buttons[i][1], buttons[i][2], buttons[i][3])
540 else
541 surf:drawText(x,y,buttons[i][1], lat_theme.main_back, lat_theme.main_text)
542 end
543 end
544 surf:render()
545 return y
546end
547local function drawPopupBox(x,y,buttons) --x and y are the corners
548 local ydir = y < h/2 and 1 or -1
549 surf:fillRect(x,y,x+15,y+(#buttons+1)*ydir, " ", lat_theme.menu_back, lat_theme.menu_text)
550 for k,v in pairs(buttons) do
551 surf:drawText(x+1,y+k*ydir, v, lat_theme.menu_back, lat_theme.menu_text)
552 end
553 surf:render()
554end
555--custom dialogue functionality (difference from popups is that they stop the main program flow, program cannot close, etc. they are allowed to handle events)
556local function infoBox(txt, noredraw)
557 drawInfobox(txt)
558 waitForKeyOrClick()
559 if not noredraw then
560 redraw()
561 end
562end
563local function textBox(txt, dtext)
564 drawTextbox(txt)
565 local resp = extensionRead("", #txt, lat_theme.menu_back, lat_theme.menu_text, dtext)
566 redraw()
567 return resp
568end
569local function xtensionTextBox(txt, xts)
570 drawTextbox(txt)
571 local resp = extensionRead(xts, #txt, lat_theme.menu_back, lat_theme.menu_text)
572 redraw()
573 return resp
574end
575local function fileChooseBox(txt)
576 return textBox(txt)
577end
578local function dirChooseBox(txt)
579 return textBox(txt)
580end
581local function buttonBox(txt, buttons)
582 local y = drawButtonBox(txt, buttons)
583 while true do
584 local _,b,x2,y2 = os.pullEvent("mouse_click")
585 if b == 1 and y == y2 then
586 for i=1,#buttons do
587 local x = math.floor((w/2-(#txt+2)/2)+((#txt+2)/i)*(i-1)) + 1
588 if x2 > x - 1 and x2 < x + #buttons[i][1] then
589 redraw()
590 return i
591 end
592 end
593 end
594 end
595end
596--these were moved above popupbox:
597local function safeRun(func, ...)
598 local succ, msg = pcall(func, ...)
599 if not succ then
600 infoBox(msg)
601 end
602end
603local function refresh()
604 if pathValid then
605 readFolderRaw(path)
606 else
607 local t = {}
608 for k,v in pairs(itemPaths) do
609 if fs.exists(v) then
610 t[#t+1] = v
611 end
612 end
613 itemPaths = t
614 end
615end
616local function popupBox(x,y,buttons,functions) --popups defined to have width 15
617 drawPopupBox(x,y,buttons)
618 local ydir = y < h/2 and 1 or -1
619 while true do
620 local _,b,cx,cy = os.pullEvent("mouse_click")
621 if b == 1 then
622 if cx < x or cx > x + 15 then
623 os.queueEvent("mouse_click", b, cx, cy)
624 break
625 end
626 if not (cy*ydir > y*ydir and cy*ydir - y*ydir < #buttons+1) then
627 os.queueEvent("mouse_click", b, cx, cy)
628 break
629 end
630 --for menus inside popup boxes
631 redraw()
632 safeRun(functions[ydir*cy-ydir*y],x,y)
633 refresh()
634 break
635 else
636 os.queueEvent("mouse_click", b, cx, cy)
637 break
638 end
639 end
640
641end
642
643local findFileBox --for later
644local findDirBox
645
646--file editing functionality
647local function run(path, ...)
648 local tArgs = {...}
649 local function box()
650 clear()
651
652 shell.run(fs.combine("/", path), unpack(tArgs)) --better alternative to shell.run?
653 print("Press any key or click to return to Lattix")
654 waitForKeyOrClick()
655 end
656 local cor = coroutine.create(box)
657 coroutine.resume(cor)
658 --later better sandboxing required that doesn't allow modification of _G or some file defense
659 while (coroutine.status(cor) ~= "dead") do
660 local event = {os.pullEventRaw()} --terminate works only on the sub-program
661 coroutine.resume(cor, unpack(event))
662 end
663end
664local function mkdir(path, name)
665 fs.makeDir(fs.combine(path, name))
666end
667local function paste(path, clipboard, cutEnabled) --i wonder what happens if you try to move something inside of itself
668 if type(clipboard) == "table" then
669 for i=1,#clipboard do
670 paste(path, clipboard[i], cutEnabled)
671 end
672 return
673 end
674 if not clipboard then error("Clipboard is empty",0) end
675 if not fs.exists(clipboard) then error("File copied doesn't exist",0) end
676 local func = cutEnabled and fs.move or fs.copy
677 local goal = fs.combine(path, fs.getName(clipboard))
678 local i = 1
679 while fs.exists(goal) do
680 goal = fs.combine(path, "Copy" .. (i>1 and " " .. tostring(i) or "") .. " of " .. fs.getName(clipboard))
681 i = i + 1
682 end
683 func(clipboard, goal)
684end
685local function advFind(path, wildcard, results)
686 if not results then results = {} end
687 local t = fs.find(fs.combine(path, wildcard))
688 for i=1,#t do
689 results[#results+1] = t[i]
690 end
691 local dirs = fs.list(path)
692 for i=1,#dirs do
693 if fs.isDir(fs.combine(path, dirs[i])) then
694 results = advFind(fs.combine(path, dirs[i]), wildcard, results)
695 end
696 end
697
698 return results
699end
700--GUI core functionality
701
702local function doubleClick(path) --contains a reference to refresh
703 selected = 0
704 if fs.isDir(path) then
705 switchFolder(path)
706 else
707 local elements = split(fs.getName(path), ".")
708 if #elements == 1 then
709 elements[2] = "" --no extension
710 end
711 if not defaultPrograms[elements[#elements]] then
712 elements[#elements] = "def" --unknown extension
713 end
714 if (defaultPrograms[elements[#elements]]) == "api" then
715 os.loadAPI(path)
716 refresh()
717 elseif defaultPrograms[elements[#elements]] == "run" then
718 run(path)
719 refresh()
720 else
721 shell.run(defaultPrograms[elements[#elements]], path)
722 refresh()
723 end
724 end
725end
726local function gotoFolder()
727 local target
728 target = textBox("Please specify target folder")
729 if target == "" then return end --cancel if empty
730 if not target or not fs.exists(target) or not fs.isDir(target) then
731 infoBox(" Not a valid directory ")
732 return
733 end
734 switchFolder(fs.combine("", target))
735end
736local function makeDirPrompt()
737 local name = textBox("Enter the name of the new directory")
738 if name == "" then return end
739 if fs.exists(fs.combine(path, name)) then
740 infoBox("Failure - File name already used")
741 end
742 mkdir(path, name)
743end
744local function pastebin()
745 local link = textBox("Enter the pastebin link")
746 if link == "" then return end
747 local name = textBox("Enter the name of the file")
748 if name == "" then return end
749 if fs.exists(fs.combine(path,name)) then
750 infoBox("Failure - File name already used")
751 return
752 end
753 local succ, err = pcall(pastebinGet,link, fs.combine(path, name))
754 if not succ then
755 infoBox(err)
756 end
757end
758local function wgetPrompt()
759 local link = textBox("Enter the url")
760 if link == "" then return end
761 local name = textBox("Enter the name of the file")
762 if name == "" then return end
763 if fs.exists(fs.combine(path,name)) then
764 infoBox("Failure - File name already used")
765 return
766 end
767 local succ, err = pcall(wGet,link, fs.combine(path, name))
768 if not succ then
769 infoBox(err)
770 end
771end
772local function runPrompt(appath)
773 if not appath then
774 appath = fs.combine(path, textBox("Enter the name of the script"))
775 end
776 if not fs.exists(appath) then
777 infoBox("Script doesn't exist: " .. appath)
778 return
779 end
780 if fs.isDir(appath) then
781 infoBox("Cannot run a directory: " .. appath)
782 return
783 end
784 args = textBox("Please enter the arguments")
785 run(appath, split(args, " "))
786end
787local function copy(name)--name actually means full path
788 clip_cut = false
789 clipboard = name
790end
791local function cut(name) --name actually means full path here
792 copy(name)
793 clip_cut = true
794end
795local function renamePrompt(path)
796 if not fs.exists(path) then infoBox("Nothing to rename") return end
797 local name = textBox("Enter the new name", fs.getName(path))
798 if name == "" then return end
799 if fs.exists(fs.combine(fs.getDir(path), name)) then
800 infoBox("Failure - File already exists")
801 return
802 end
803 fs.move(path, fs.combine(fs.getDir(path), name))
804 selected = 0
805end
806local function deletePrompt(path)
807 if not fs.exists(path) then infoBox("Nothing to delete") return end
808 local response = buttonBox("Do you really want to delete " .. fs.getName(path) .. "?", {{" Delete ", colors.white, colors.red}, {" Cancel ", colors.yellow, colors.black }})
809 if response == 1 then
810 fs.delete(path)
811 end
812 selected = 0
813end
814local function findFilesPrompt()
815 local wildCard = textBox("Enter filename or part of it")
816 if wildCard == "" then return end --cancel option
817 local finds = advFind(path, "*" .. wildCard .. "*")
818 if #finds == 0 then
819 infoBox("No files found")
820 return
821 end
822 itemNames = {}
823 itemPaths = {}
824 selected = 0
825 scroll = 0
826 for i=1,#finds do
827 itemNames[i] = finds[i]
828 itemPaths[i] = finds[i]
829 customTitle = "Search results"
830 pathValid = false
831 end
832
833end
834--GUI functionality - event mapping
835local buttons = {}
836local keymap = {}
837local eventmap = {}
838
839for i=1,w do
840 buttons[i] = {}
841 for j=1,h do
842
843 buttons[i][j] = {function() end, function() end}
844 end
845end
846local function newButton(x, y, w, h, func_left, func_right)
847 for i=x, x+w-1 do
848 for j=y, y+h-1 do
849 buttons[i][j] = {func_left, func_right}
850 end
851 end
852end
853local function clearButtons(x, y, w, h)
854 newButton(x,y,w,h, empty, empty)
855end
856
857local function clearAllEvents()
858 keymap = {}
859 eventmap = {}
860 clearButtons(1,1,w,h)
861end
862--mappings:
863local popup_newmenu_names = {
864 "New ...",
865 "New dir",
866 "Paste",
867 "Pastebin",
868 "wget"
869}
870local popup_newmenu_functions = {
871 function(x,y)
872 local options = {}
873 local functions = {}
874 for k,v in pairs(editors) do
875 local i = #options+1
876 options[i] = k
877 functions[i] = function()
878 local ext = ""
879 if editors_extensions[k] then
880 ext = editors_extensions[k]
881 end
882 local app
883 if v == "choose" then
884
885 else
886 app = v
887 end
888 local item = xtensionTextBox("What should the new file be called?", "." .. ext)
889 local target = fs.combine(path, item)
890 if not fs.exists(target) then
891 shell.run(app, target)
892 else
893 infoBox("Failure - file already exists")
894 end
895 end
896 end
897 popupBox(x,y,options,functions)
898 end,
899 function() makeDirPrompt() end,
900 function() paste(path, clipboard, clip_cut) end,
901 function() pastebin() end,
902 function() wgetPrompt() end,
903
904}
905
906
907
908
909
910local popup_lockednewmenu_names = {
911 "Refresh"
912}
913local popup_lockednewmenu_functions = {
914 function() refresh() end
915}
916
917local popup_menu_names = {
918 "Go to dir",
919 "Find file",
920 "Version: "..version
921}
922local popup_menu_functions = {
923 function() gotoFolder() end,
924 function() findFilesPrompt() end,
925 function() infoBox("Lattix version " .. version) end,
926}
927local menu_functions = {
928 back = function()
929 if not pathValid then
930 pathValid = true
931 refresh()
932
933 return
934 end
935 if #history > 1 then
936 switchFolderRaw(history[#history])
937 table.remove(history, #history)
938 end
939 end,
940 up = function()
941 if not pathValid then
942 pathValid = true
943 refresh()
944 return
945 end
946 if path == "" or path == "/" then return end
947 switchFolder(fs.combine(path, ".."))
948 end,
949 menu = function()
950 --open advanced menu
951 popupBox(9,2,popup_menu_names, popup_menu_functions)
952 end,
953 root = function()
954 if not pathValid then
955 pathValid = true
956 end
957 switchFolder(root)
958 end,
959 plus = function()
960 --open new menu
961 if pathValid then
962 popupBox(5,2,popup_newmenu_names, popup_newmenu_functions)
963 else
964 popupBox(5,2,popup_lockednewmenu_names, popup_lockednewmenu_functions)
965 end
966 end,
967 quit = function()
968 clear()
969 endprogram = true
970 end
971}
972local filePopupNames = {
973-- "Edit", --opens default editor, as double click would, expect that for .lua it opens edit too
974 "Run", --runs
975 "Run w/ args", --runs with args
976-- "Open With", --select a program from a list and use it to open this, config file will be huge i see
977 "Rename",
978 "Copy",
979 "Cut",
980 "Delete"
981}
982local function getSelectedPath()
983 return itemPaths[selected]
984end
985local filePopupFunctions = {
986 function(x,y) run(getSelectedPath()) end,--run
987 function(x,y) runPrompt(getSelectedPath()) end,--run w args
988 function(x,y) renamePrompt(getSelectedPath()) end, --rename,
989 function(x,y) copy(getSelectedPath()) end, --copy
990 function(x,y) cut (getSelectedPath()) end, --cut
991 function(x,y) deletePrompt(getSelectedPath()) end --delete
992}
993local folderPopupNames = {
994 "Open",
995-- "Open in ...", --1,2,3,4 and program, TODO later bc open in and open with need some design decisions
996-- "Unpack",
997 "Rename",
998 "Copy",
999 "Cut",
1000 "Delete"
1001}
1002local folderPopupFunctions = {
1003 function(x,y) switchFolder(getSelectedPath()) end, --open
1004 function(x,y) renamePrompt(getSelectedPath()) end, --rename
1005 function(x,y) copy(getSelectedPath()) end, --copy
1006 function(x,y) cut(getSelectedPath()) end, --cut
1007 function(x,y) deletePrompt(getSelectedPath()) end --delete
1008}
1009local multiPopupNames = {}
1010local multiPopupFunctions = {}--for multiselect, copy, cut, pack into folder, delete
1011
1012local commands = { --table of functions, arg: list of words typed, including the command itself
1013 ["run"] = function(words)
1014 if #words == 1 then
1015 runPrompt()
1016 return
1017 end
1018
1019 local torun
1020 for i=1,#itemNames do
1021 if words[2] == itemNames[i] then
1022 torun = i
1023 break
1024 end
1025 end
1026 if not torun then return end
1027 local args = {}
1028 if #words > 2 then
1029 for i=3, #words do
1030 args[i-2] = words[i]
1031 end
1032 end
1033 if fs.exists(itemPaths[torun]) and not fs.isDir(itemPaths[torun]) then
1034 run(itemPaths[torun], unpack(args))
1035 end
1036 end,
1037 ["goto"] = function(words)
1038 local target
1039 if #words < 2 then
1040 gotoFolder()
1041 return
1042 else
1043 target = words[2]
1044 end
1045 --from local path
1046 if target:sub(1,1) ~= "/" and fs.exists(fs.combine(path, target)) and fs.isDir(fs.combine(path, target)) then
1047 switchFolder(fs.combine(path, target))
1048 return
1049 end
1050 --from absolute path
1051 if not target or not fs.exists(target) or not fs.isDir(target) then
1052 infoBox("Not a valid directory")
1053 return
1054 end
1055 switchFolder(fs.combine("", target))
1056 end,
1057 ["deselect"] = function(words)
1058 selected = 0
1059 end,
1060 ["select"] = function(words)
1061 --deselect cuz no arg
1062 if #words < 2 then
1063 selected = 0
1064 return
1065 end
1066 --select by name
1067 for i=1, #itemNames do
1068 if itemNames[i] == words[2] then
1069 selected = i
1070 return
1071 end
1072 end
1073
1074 --select by index
1075 local index = tonumber(words[2])
1076 if index and index == math.floor(index) and index <= #itemPaths and index >= 0 then
1077 selected = index
1078 return
1079 end
1080
1081 end,
1082 ["count"] = function(words)
1083 infoBox("Number of items: " .. #itemPaths)
1084 end,
1085 ["up"] = function(words)
1086 menu_functions.up()
1087 end,
1088 ["root"] = function(words)
1089 menu_functions.root()
1090 end,
1091 ["quit"] = function(words)
1092 menu_functions.quit()
1093 end,
1094 ["back"] = function(words)
1095 menu_functions.back()
1096 end,
1097 ["switch"] = function(words)
1098 if #words < 2 then
1099 local nses = cSession + 1
1100 switchSessions(nses < 5 and nses or 1)
1101 return
1102 end
1103 local index = tonumber(words[2])
1104 if index and index == math.floor(index) and index <= 4 and index >= 1 then
1105 switchSessions(index)
1106 end
1107 end,
1108 ["help"] = function(words)
1109 infoBox(" Read the Lattix CC forum post, commands section ")
1110 end,
1111}
1112local function raw(str)
1113 str = str:lower()
1114 str = str:gsub(' ','')
1115 str = str:gsub('_', '')
1116 return str
1117end
1118local function evaulate(nametbl, functbl, name)
1119 for i=1, #nametbl do
1120 if raw(nametbl[i]) == raw(name) then
1121 return functbl[i]
1122 end
1123 end
1124end
1125local alias = {
1126 ["cd"] = "goto",
1127 ["mkdir"] = "newdir",
1128 ["find"] = "findfile",
1129 ["search"] = "find",
1130 ["about"] = "version:" .. version,
1131 ["version"] = "version:"..version,
1132 ["exit"] = "quit",
1133 [".."] = "back",
1134 ["/"] = "root",
1135 ["x"] = "quit",
1136 ["#"] = "count"
1137}
1138
1139local mt = {
1140 __index = function(tbl, i)
1141 local function fullEval()
1142 if raw(i) == raw("new...") then
1143 return nil
1144 end
1145 local func
1146 if pathValid then
1147 func = evaulate(popup_newmenu_names, popup_newmenu_functions, i)
1148 if func then return func end
1149 else
1150 func = evaulate(popup_lockednewmenu_names, popup_lockednewmenu_functions, i)
1151 if func then return func end
1152 end
1153 func = evaulate(popup_menu_names, popup_menu_functions, i)
1154 if func then return func end
1155 --if multiselection...
1156 if selected ~= 0 and itemPaths[selected] and fs.exists(itemPaths[selected]) then
1157 if fs.isDir(itemPaths[selected]) then
1158 func = evaulate(folderPopupNames, folderPopupFunctions, i)
1159 if func then return func end
1160 else
1161 func = evaulate(filePopupNames, filePopupFunctions, i)
1162 if func then return func end
1163 end
1164 end
1165 end
1166 local final
1167 while true do
1168 final = fullEval()
1169 if final then break end
1170 if not alias[raw(i)] then
1171 break
1172 end
1173 i = alias[raw(i)]
1174 if commands[i] then final = commands[i] break end
1175 end
1176 return final
1177 end
1178}
1179setmetatable(commands, mt)
1180
1181
1182
1183local function mapMenu()
1184
1185 local switch = function(e)
1186 switchSessions(e[3]-w+6)
1187 end
1188 newButton(1,1,1,1, menu_functions.back, empty)
1189 newButton(3,1,1,1, menu_functions.up, empty)
1190 newButton(7,1,1,1, menu_functions.root, empty)
1191 newButton(5,1,1,1, menu_functions.plus, empty)
1192 newButton(9,1,1,1, menu_functions.menu, empty)
1193 newButton(w,1,1,1, menu_functions.quit, empty)
1194 newButton(w-5,1,4,1, switch, empty)
1195 keymap[keys.left] = up
1196end
1197local drawFiles
1198
1199local function enterPress()
1200 words = split(shell_input, " ")
1201 if words and #words > 0 then
1202 if commands[words[1]:lower()] then
1203 prewrite = {}
1204 if #words > 1 then
1205 for i=2, #words do
1206 prewrite[i-1] = words[i]
1207 end
1208 end
1209 commands[words[1]:lower()](words)
1210 refresh()
1211 prewrite = {}
1212 else
1213 infoBox("Unknown command: " .. words[1]:lower())
1214 end
1215 elseif selected > 0 and selected <= #itemPaths then
1216 doubleClick(itemPaths[selected])
1217 end
1218 shell_input = ""
1219end
1220local function filePopup(x,y,path)
1221 if fs.isDir(path) then
1222 --directory
1223 popupBox(x,y,folderPopupNames, folderPopupFunctions)
1224 else
1225 --file
1226 popupBox(x,y,filePopupNames, filePopupFunctions)
1227 end
1228end
1229local function mapFiles()
1230 --popup menu implementation (right click)
1231 local file_rightclick = function(e)
1232 local i = posToIndex(#itemNames,e[3],e[4],scroll) --get index
1233 if itemPaths[i] then
1234 if selected ~= i then --select if not selected
1235 selected = i --just for aesthetics
1236 redraw()
1237 end
1238 --show file/folder/multiselect relevant popup
1239 if selection and #selection > 0 then
1240 --multiselect
1241 else
1242 filePopup(e[3],e[4],getSelectedPath())
1243 end
1244 else
1245 selected = 0
1246 --show the same popup as the + button in the menu
1247 if pathValid then
1248 popupBox(e[3],e[4],popup_newmenu_names, popup_newmenu_functions)
1249 else
1250 popupBox(e[3], e[4], popup_lockednewmenu_names, popup_lockednewmenu_functions)
1251 end
1252 end
1253 end
1254 --select implementation (left click)
1255 local file_leftclick = function(e)
1256 local i = posToIndex(#itemNames,e[3],e[4],scroll)
1257 if itemPaths[i] then
1258 if selected == i then
1259 doubleClick(itemPaths[i])
1260 else
1261 selected = i
1262 end
1263 else
1264 selected = 0
1265 end
1266 end
1267
1268 newButton(1,2,w,h-2,file_leftclick, file_rightclick)
1269 --multiselect stuff
1270
1271 --scrolling
1272 eventmap["mouse_scroll"] = function(e)
1273 local b = e[2]
1274 if b == 1 and #itemPaths - scroll > h-2 then
1275 scroll = scroll + 1
1276 end
1277 if b == -1 and scroll > 0 then
1278 scroll = scroll - 1
1279 end
1280 end
1281
1282 keymap[keys.enter] = enterPress
1283 keymap[keys.up] = function()
1284 if selected > 1 then
1285 selected = selected - 1
1286 if scroll >= selected then
1287 scroll = selected - 1
1288 end
1289 end
1290 if selected == 0 then
1291 selected = #itemPaths
1292 if scroll + h - 2 < selected then
1293 scroll = selected - h + 2
1294 end
1295 end
1296 end
1297 keymap[keys.down] = function()
1298 if selected < #itemPaths then
1299 selected = selected + 1
1300 if scroll + h - 2 < selected then
1301 scroll = selected - h + 2
1302 end
1303 end
1304 end
1305 keymap[keys.right] = function()
1306 if selected > 0 and selected <= #itemPaths then
1307 local x,y = IndexToPos(selected, scroll)
1308 if x < 2 then
1309 x = 2
1310 end
1311 if x > h-1 then
1312 x = h-1
1313 end
1314 filePopup(x,y, getSelectedPath())
1315 end
1316 end
1317end
1318
1319local function mapBar()
1320 eventmap.char = function(e)
1321 shell_input = shell_input .. e[2]
1322 end
1323 keymap[keys.backspace] = function()
1324 shell_input = shell_input:sub(1, #shell_input-1)
1325 end
1326 --enter is mapped in mapFiles for the bar too
1327end
1328--draw components
1329local function drawMenu()
1330 surf:drawLine(1,1,w,1," ", lat_theme.menu_back, lat_theme.menu_text)
1331 if term.isColor() then
1332 surf:drawText(1,1,"< ^ + / m", lat_theme.menu_back, lat_theme.menu_text)
1333 end
1334 local str
1335 if customTitle then
1336 str = customTitle
1337 else
1338 if path ~= "" then
1339 str = cSession .. " - " .. path
1340 else
1341 str = tostring(cSession)
1342 end
1343 end
1344 str = #str < w/2 and str or str:sub(1,math.floor(w/2)) .. "..."
1345 surf:drawText(8+math.floor((w-12)/2-#str/2),1,str, lat_theme.menu_back, lat_theme.menu_text)
1346
1347 if term.isColor() then
1348 surf:drawText(w-5,1,"1234", lat_theme.menu_back, lat_theme.menu_text)
1349 surf:drawPixel(w,1,"x", colors.red, colors.white)
1350 end
1351
1352end
1353
1354drawFiles = function()
1355 if selection == nil then selection = {} end --just in case selection was nilled
1356 local cy = 2 --current y pos of drawing
1357 local i = scroll + 1 --index in "items"
1358 local tcol
1359 local bcol
1360 while IndexToPos(i,scroll) < h do
1361 if not itemNames[i] then break end --because the while condition checks for screen size, this checks for file count
1362 local twrite = ""
1363 bcol = (lat_theme.main_back)
1364 if selection[i] then
1365 bcol = (lat_theme.multiselect_back)
1366 twrite = twrite .. selectprefix
1367 end
1368 if i ~= selected then
1369 if not fs.isDir(itemPaths[i]) then
1370 tcol = (lat_theme.main_text)
1371 else
1372 tcol = (lat_theme.dir_text)
1373
1374 end
1375 else
1376 tcol = (lat_theme.selected_text)
1377 end
1378 if fs.isDir(itemPaths[i]) then
1379 twrite = twrite .. dirprefix
1380 end
1381 twrite = twrite .. itemNames[i]
1382 surf:drawLine(1, cy, w, cy, " ", bcol, tcol)
1383 surf:drawText(2, cy, twrite, bcol, tcol)
1384 local mwrite = "-"
1385 if not fs.isDir(itemPaths[i]) then
1386 mwrite = formatNumber(fs.getSize(itemPaths[i]))
1387 end
1388 local startX = w-6
1389 surf:drawLine(startX-1, cy, w, cy, " ", bcol, tcol)
1390 surf:drawText(startX,cy, mwrite, bcol, tcol)
1391 i = i + 1
1392 cy = cy + 1 --up both indexes
1393 end
1394 return itemNames
1395end
1396
1397local function drawBar()
1398 surf:drawLine(1,h,w,h, " ", lat_theme.menu_back, lat_theme.menu_text)
1399 surf:drawText(1,h, path .. "> " .. shell_input, lat_theme.menu_back, lat_theme.menu_text)
1400end
1401
1402
1403--GUI drawing functions
1404redraw = function()
1405 if not fs.exists(path) or not fs.isDir(path) then
1406 infoBox("Error: " .. path .. " is not a valid directory", true)
1407 path = root
1408 end
1409
1410 surf:clear(" ", lat_theme.blank_back, lat_theme.blank_text)
1411 drawMenu()
1412
1413 drawFiles()
1414
1415 drawBar()
1416
1417 surf:render()
1418end
1419
1420remap = function()
1421 clearAllEvents()
1422 mapMenu()
1423 mapFiles()
1424 mapBar()
1425end
1426local oRedraw = redraw --orginal backup
1427local oRemap = remap
1428restore = function()
1429 redraw = oRedraw
1430 remap = oRemap
1431end
1432
1433do --choose file/folder dialog
1434
1435end
1436
1437switchFolder(path)
1438
1439--main loop
1440while not endprogram do
1441 redraw()
1442 remap()
1443 local e = { os.pullEvent() }
1444 if e[1] == "mouse_click" then
1445 buttons[e[3]][e[4]][e[2]](e)
1446 elseif e[1] == "key" then
1447 if keymap[e[2]] then
1448 keymap[e[2]](e)
1449 end
1450 elseif eventmap[e[1]] then
1451 eventmap[e[1]](e)
1452 end
1453end
1454
1455end
1456
1457local succ, msg = pcall(program)
1458
1459term.setBackgroundColor(colors.black)
1460term.setTextColor(colors.white)
1461term.clear()
1462term.setCursorPos(1,1)
1463
1464if not succ and msg ~= "Terminated" then
1465 print("Congratulations, Lattix has crashed")
1466 print()
1467 print("Please report with steps to reproduce to the forum post to get your name added to the credits")
1468 print()
1469 print(msg)
1470end
1471if succ or msg == "Terminated" then
1472 print("[Lattix]: bye")
1473 print()
1474 print(msg or "version " .. version)
1475end