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