scattergun

MineExplorerPlus

Sep 17th, 2024 (edited)
173
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 29.76 KB | None | 0 0
  1. -- Original Program:
  2. -- MineExplorer by Reimar
  3. -- License can be viewed here:
  4. -- https://github.com/ReimarPB/MineExplorer/blob/master/LICENSE
  5. -- GNU GENERAL PUBLIC LICENSE
  6.  
  7. -- MineExplorerPlus by Missooni
  8. -- All original Minex files have been condensed into a single file.
  9. -- Added file extensions for images, web files, system files, archives, and more.
  10. -- Program now shows file sizes.
  11. -- Stores settings api files in 'usr/minexp/' and deleted files in 'temp/minexp/' Configurable.
  12. -- (Original settings API code had a typo in it.)
  13. -- Colors used in the file explorer are customizable now.
  14. -- (All minexp.colors.* settings use decimal values. 1 = white, 32768 = black)
  15. -- A lot of new hotkeys have been added, some have been edited.
  16. -- Spacebar is now the dedicated key for deselecting files and exiting the context window.
  17. -- (Pressing any button that isn't a hotkey will do this as well.)
  18. -- There is now an X on the top right you can use to exit the program in conjunction to the original hotkey,'q'.
  19. -- File list will now re-sort automatically after renaming files.
  20. -- Pasting a folder that already exists in a directory will combine the contents of both into a new 'foldername-merged'
  21. -- The folder you are copying from will overwrite any matching files when merging.
  22. -- If you try to paste a single file that already exists in a directory it will paste a duplicate with a number added to the end.
  23.  
  24. -- Mouse-only context menu will appear when right-clicking. Program contains the following operations and hotkeys.
  25. -- Copy file/folder (1 or c)
  26. -- Paste file/folder (2 or v)
  27. -- Rename file/folder (f1 or f2)
  28. -- Delete file/folder (x or Delete)
  29. -- Edit file (leftCtrl)
  30. -- Undo Delete (z or Backspace)
  31. -- Run program in shell (Enter)
  32. -- Run program in a new tab (Tab Key)
  33. -- Create new file (Insert)
  34. -- Deselect file or exit context menu (Spacebar)
  35.  
  36. -- Creates a directory for temporarily storing deleted files and user configurations.
  37. -- You can change these variables to use different directories / use an existing directory if necessary.
  38.  
  39. -- MineExplorerPlus can delete and remake the directory that stores deleted files. It is off by default.
  40. -- If you have limited space on your computer, enable this setting or routinely delete your recycling folder.
  41. -- Do not keep anything important in the recycling folder!
  42. -- Use 'Undo' if you accidentally delete an important file.
  43.  
  44. recycleDir = "temp/minexp/"
  45. cfgDir = "usr/minexp/"
  46.  
  47. --
  48. settings.clear()
  49. if not fs.exists(cfgDir.."explorer.cfg") then
  50. settings.set("minexp.disable_hotkeys", false)
  51. settings.set("minexp.recycle_directory", recycleDir)
  52. settings.set("minexp.recycle_on_leave", false)
  53. settings.save("usr/minexp/explorer.cfg")
  54. settings.clear()
  55. end
  56. if not fs.exists(cfgDir.."colors.cfg") then
  57. settings.set("minexp.colors.bg", colors.black)
  58. settings.set("minexp.colors.fg", colors.white)
  59. settings.set("minexp.colors.select", colors.blue)
  60. settings.set("minexp.colors.active", colors.gray)
  61. settings.save("usr/minexp/colors.cfg")
  62. settings.clear()
  63. end
  64. if not fs.exists(cfgDir.."programs.cfg") then
  65. settings.set("minexp.default_program", "edit")
  66. settings.set("minexp.programs.nft", "paint")
  67. settings.set("minexp.programs.nfp", "paint")
  68. settings.save("usr/minexp/programs.cfg")
  69. settings.clear()
  70. end
  71. settings.load(cfgDir.."explorer.cfg")
  72. settings.load(cfgDir.."colors.cfg")
  73. settings.load(cfgDir.."programs.cfg")
  74. if not fs.exists(recycleDir) then fs.makeDir(recycleDir) end
  75. if not fs.exists(cfgDir) then fs.makeDir(cfgDir) end
  76.  
  77. -- Create Context Window
  78. local ogTerm = term.current()
  79. local contextWindow = window.create(term.current(), 1, 1, 16, 8)
  80. contextWindow.setVisible(false)
  81. function drawContextWindow()
  82. contextWindow.setTextColor(colors.white)
  83. contextWindow.setBackgroundColor(colors.gray)
  84. contextWindow.clear()
  85. term.redirect(contextWindow)
  86. term.setCursorPos(1,1)
  87. print(" Context Menu>")
  88. term.blit(" Copy ", "77777777", "88888888") term.blit(" Paste ", "777777777", "000000000") write("\n")
  89. term.blit(" Rename ", "77777777", "00000000") term.blit(" Delete ", "000000000", "eeeeeeeee") write("\n")
  90. term.blit(" Edit ", "77777777", "88888888") term.blit(" Undo ", "eeeeeeeee", "000000000") write("\n")
  91. write(" Run Program> \n")
  92. term.blit(" Shell ", "77777777", "88888888") term.blit(" NewTab ", "777777777", "000000000") write("\n")
  93. write(" Create New> \n")
  94. term.blit(" File ", "77777777", "00000000") term.blit(" Folder ", "777777777", "888888888")
  95. term.redirect(ogTerm)
  96. end
  97.  
  98. local listeners = {}
  99. local currentFocus = nil;
  100.  
  101. Focus = {
  102. FILES = 0,
  103. INPUT = 1,
  104. }
  105.  
  106. function addListener(event, focus, callback)
  107. if not listeners[event] then listeners[event] = {} end
  108. table.insert(listeners[event], {
  109. focus = focus,
  110. callback = callback,
  111. })
  112. end
  113.  
  114. function listen()
  115. while true do
  116. local event, p1, p2, p3 = os.pullEvent()
  117.  
  118. if listeners[event] then
  119. for _, listener in ipairs(listeners[event]) do
  120. if listener.focus == currentFocus then
  121. if listener.callback(p1, p2, p3) then return end -- Exit when callback returns true
  122. if currentFocus ~= listener.focus then break end -- Break out if focus changed
  123. end
  124. end
  125. end
  126. end
  127. end
  128.  
  129. function setFocus(focus)
  130. currentFocus = focus
  131. end
  132.  
  133. local scrollY = 0
  134. local CONTENT_OFFSET_Y = 1
  135.  
  136. local bgColor = settings.get("minexp.colors.bg")
  137. local txtColor = settings.get("minexp.colors.fg")
  138. local selectColor = settings.get("minexp.colors.select")
  139. local activeColor = settings.get("minexp.colors.active")
  140.  
  141. function showPath()
  142. local path = getCurrentPath()
  143.  
  144. term.setCursorPos(1, 1)
  145. term.setBackgroundColor(colors.gray)
  146. term.setTextColor(colors.white)
  147. term.write(path)
  148.  
  149. -- Fill remaining space
  150. local width, _ = term.getSize()
  151. term.write(string.rep(" ", width - #path + 1 - 3))
  152. term.blit(" X ", "000", "eee")
  153. end
  154.  
  155. function showFiles()
  156. for i, file in ipairs(files) do
  157. showFile(i)
  158. end
  159.  
  160. -- Fill remaining space
  161. local width, height = term.getSize()
  162. if #files < height then
  163. term.setBackgroundColor(bgColor)
  164. for i = #files + 1 + CONTENT_OFFSET_Y, height do
  165. term.setCursorPos(1, i)
  166. term.write(string.rep(" ", width))
  167. end
  168. end
  169. end
  170.  
  171. function showFile(index)
  172. local width, height = term.getSize()
  173. local y = index - scrollY + CONTENT_OFFSET_Y
  174.  
  175. if y < 1 + CONTENT_OFFSET_Y or y > height then return end
  176.  
  177. local file = files[index]
  178.  
  179. term.setBackgroundColor(bgColor)
  180. term.setTextColor(txtColor)
  181. term.setCursorPos(1, y)
  182.  
  183. term.write(string.rep(" ", file.depth - 1))
  184.  
  185. -- Arrow + Icon
  186. local color1, color2
  187. if file.type ~= FileType.FILE then
  188. if file.expanded then
  189. term.write("-")
  190. else
  191. term.write("+")
  192. end
  193.  
  194. if file.readonly then
  195. color1 = colors.orange
  196. color2 = colors.orange
  197. elseif file.type == FileType.DIRECTORY then
  198. color1 = colors.yellow
  199. color2 = colors.yellow
  200. else
  201. color1 = colors.gray
  202. color2 = colors.gray
  203. end
  204. else
  205. term.write(" ")
  206. color1 = colors.lightGray
  207. color2 = colors.lightGray
  208. end
  209.  
  210. if file.name:match("%.lua$") or file.name:match("%.sh$") then color2 = colors.blue
  211. elseif file.name:match("%.tmp$") or file.name:match("%.temp$") or file.name:match("%.txt$") then color2 = colors.white
  212. elseif file.name:match("%.ls$") or file.name:match("%.db$") or file.name:match("%.cfg") then color2 = colors.lime
  213. elseif file.name:match("%.man$") or file.name:match("%.d$") then color2 = colors.yellow
  214. elseif file.name:match("%.v$") or file.name:match("%.al$") then color2 = colors.gray
  215. elseif file.name:match("%.dep$") then color2 = colors.purple
  216. elseif file.name:match("%.pal$") then color2 = colors.red
  217. elseif file.name:match("%.nft$") or file.name:match("%.nfp$") then
  218. color1 = colors.red
  219. color2 = colors.red
  220. elseif file.name:match("%.sys$") then
  221. color1 = colors.lightBlue
  222. color2 = colors.lightBlue
  223. elseif file.name:match("%.html$") then
  224. color1 = colors.white
  225. color2 = colors.white
  226. elseif file.name:match("%.css$") then
  227. color1 = colors.white
  228. color2 = colors.lightBlue
  229. elseif file.name:match("%.js$") then
  230. color1 = colors.white
  231. color2 = colors.yellow
  232. elseif file.name:match("%.tar$") or file.name:match("%.a$") or file.name:match("%.pdr$") then
  233. color1 = colors.yellow
  234. color2 = colors.orange
  235. end
  236.  
  237. term.setTextColor(color1)
  238. term.write("\138")
  239. term.setTextColor(color2)
  240. term.write("\133")
  241.  
  242. -- Name
  243. if file.selected then
  244. term.setBackgroundColor(selectColor)
  245. else
  246. term.setBackgroundColor(bgColor)
  247. end
  248.  
  249. if not file.selected and file.name:find("^%.") then
  250. term.setTextColor(colors.lightGray)
  251. else
  252. term.setTextColor(txtColor)
  253. end
  254. term.write(file.name)
  255.  
  256. -- File Size
  257. term.setBackgroundColor(bgColor)
  258. term.setTextColor(colors.lightGray)
  259. local suffix = "B"
  260. local fileSize = fs.getSize(file.path)
  261. if fileSize > 1024 then
  262. fileSize = fileSize / 1024
  263. fileSize = math.floor(fileSize + .5)
  264. suffix = "kB"
  265. end
  266. if file.type == FileType.FILE then
  267. term.write(" "..fileSize)
  268. term.setTextColor(colors.gray)
  269. term.write(suffix)
  270. end
  271.  
  272. -- Fill remaining space
  273. local x, _ = term.getCursorPos()
  274. term.setBackgroundColor(bgColor)
  275. term.write(string.rep(" ", width - x + 1))
  276. end
  277.  
  278. function showEverything()
  279. showPath()
  280. showFiles()
  281. end
  282.  
  283. function showContextWindow(x,y)
  284. local width, height = term.getSize()
  285. if y > height - 8 then y = height - 7 end
  286. if x > width - 16 then x = width - 15 end
  287. contextWindow.reposition(x, y)
  288. contextWindow.setVisible(true)
  289. waitForContext(x,y)
  290. end
  291.  
  292. undoBuffer = {}
  293. clipboard = {}
  294. deleteIndex = nil
  295.  
  296. function waitForContext(ogx,ogy)
  297. local selection = getSelectedIndex()
  298. local file = files[selection]
  299. repeat
  300. event, button, x, y = os.pullEvent()
  301. until event == "mouse_click" or button == keys.space or button == keys.insert
  302. if button == keys.space then
  303. hideContextWindow()
  304. return
  305. elseif button == keys.insert then
  306. fileFromContext("file", file, ogx, ogy)
  307. elseif button == 2 then
  308. showEverything()
  309. showContextWindow(x,y)
  310. elseif (x < ogx or x > ogx+15) or (y < ogy or y > ogy+7) then
  311. hideContextWindow()
  312. return
  313. elseif (y == ogy+1 and x >= ogx and x <= ogx+7) then
  314. if file then
  315. copyFile(file)
  316. hideContextWindow()
  317. else
  318. contextWhenClicked(1,2,"8","e"," NoFile ")
  319. waitForContext(ogx,ogy)
  320. end
  321. elseif (y == ogy+1 and x >= ogx+8 and x <= ogx+15) then
  322. local pasteFile = pasteFile(file)
  323. if pasteFile then
  324. hideContextWindow()
  325. redrawFromContext()
  326. else
  327. contextWhenClicked(9,2,"0","e"," NoClip ")
  328. waitForContext(ogx,ogy)
  329. end
  330. elseif (y == ogy+2 and x >= ogx and x <= ogx+7) then
  331. if file then
  332. hideContextWindow()
  333. renameFile(selection)
  334. else
  335. contextWhenClicked(1,3,"0","e"," NoFile ")
  336. waitForContext(ogx,ogy)
  337. end
  338. elseif (y == ogy+2 and x >= ogx+8 and x <= ogx+15) then
  339. if file and not file.readonly then
  340. deleteFile(file)
  341. redrawFromContext("delete")
  342. hideContextWindow()
  343. else
  344. contextWhenClicked(9,3,"e","0"," NoFile ")
  345. waitForContext(ogx,ogy)
  346. end
  347. elseif (y == ogy+3 and x >= ogx and x <= ogx+7) then
  348. if file then doPrimaryAction(file) else
  349. contextWhenClicked(1,4,"8","e"," NoFile ")
  350. waitForContext(ogx,ogy)
  351. end
  352. elseif (y == ogy+3 and x >= ogx+8 and x <= ogx+15) then
  353. if #undoBuffer ~= 0 then
  354. undoDelete()
  355. hideContextWindow()
  356. redrawFromContext()
  357. else
  358. contextWhenClicked(9,4,"0","e"," N/A ")
  359. waitForContext(ogx,ogy)
  360. end
  361. elseif (y == ogy+5 and x >= ogx and x <= ogx+7) then
  362. if file and file.type == FileType.FILE then doSecondaryAction(file) else
  363. contextWhenClicked(1,6,"8","e"," NoFile ")
  364. waitForContext(ogx,ogy)
  365. end
  366. elseif (y == ogy+5 and x >= ogx+8 and x <= ogx+15) then
  367. if file and file.type == FileType.FILE then doSecondaryAction(file, true) else
  368. contextWhenClicked(9,6,"0","e"," NoFile ")
  369. waitForContext(ogx,ogy)
  370. end
  371. elseif (y == ogy+7 and x >= ogx and x <= ogx+7) then
  372. fileFromContext("file", file, ogx, ogy)
  373. elseif (y == ogy+7 and x >= ogx+8 and x <= ogx+15) then
  374. fileFromContext("folder", file, ogx, ogy)
  375. else
  376. waitForContext(ogx,ogy)
  377. end
  378. end
  379.  
  380. function hideContextWindow()
  381. contextWindow.setVisible(false)
  382. drawContextWindow()
  383. showEverything()
  384. end
  385.  
  386. function fileFromContext(param, file, ogx, ogy)
  387. local madeFile = makeNewFile(param, file)
  388. local x = 1
  389. local col = "0"
  390. if param == "folder" then
  391. x = 9
  392. col = "8"
  393. end
  394. if madeFile == true then
  395. hideContextWindow()
  396. redrawFromContext()
  397. else
  398. drawContextWindow()
  399. contextWhenClicked(x,8,col,"e"," Voided ")
  400. waitForContext(ogx,ogy)
  401. end
  402. end
  403.  
  404. function redrawFromContext(operation)
  405. if getSelectedIndex() and files[getSelectedIndex()].depth > 1 then
  406. local targetDepth = files[getSelectedIndex()].depth - 1
  407. if files[getSelectedIndex()].type == FileType.FOLDER then targetDepth = targetDepth - 2 end
  408. repeat
  409. index = getSelectedIndex()
  410. if deleteIndex ~= nil then
  411. index = deleteIndex
  412. deleteIndex = nil
  413. end
  414. if not (files[index].depth == targetDepth and files[index].type ~= FileType.FILE) then
  415. setSelection(index-1) end
  416. until files[index].depth == targetDepth and files[index].type ~= FileType.FILE
  417. updateSelection(index, index+1)
  418. collapse()
  419. expand()
  420. showFiles()
  421. elseif operation ~= "delete" and getSelectedIndex() and (files[getSelectedIndex()].type == FileType.DIRECTORY or files[getSelectedIndex()].type == FileType.DISK) then
  422. collapse()
  423. expand()
  424. showFiles()
  425. else
  426. local index = getSelectedIndex()
  427. if not index then index = 1 end
  428. files = {}
  429. loadAllFiles()
  430. setSelection(index)
  431. if getSelectedIndex() then files[index].selected = false end
  432. fixScreen()
  433. end
  434. end
  435.  
  436. function contextWhenClicked(x,y,bg,fg,string)
  437. term.redirect(contextWindow)
  438. term.setCursorPos(x,y)
  439. term.blit(string, string.rep(fg,#string), string.rep(bg,#string))
  440. os.sleep(0.4)
  441. drawContextWindow()
  442. end
  443.  
  444. function fixScreen()
  445. local width, height = term.getSize()
  446. repeat
  447. local newScrollY = scrollY - 1
  448. scrollY = newScrollY
  449. local index = getSelectedIndex()
  450. if not index then index = #files end
  451. until newScrollY < 1 or scrollTo(index)
  452. if scrollY < 0 then scrollY = 0 end
  453. showFiles()
  454. end
  455.  
  456. -- Returns whether it actually scrolled
  457. function scrollTo(index)
  458. local _, height = term.getSize()
  459. height = height - CONTENT_OFFSET_Y
  460.  
  461. if index <= scrollY + 1 then
  462. scrollY = index - 1
  463. return true
  464. end
  465.  
  466. if index > scrollY + height - 1 then
  467. scrollY = index - height
  468. return true
  469. end
  470.  
  471. return false
  472. end
  473.  
  474. -- Scrolls to new selection if necessary and draws changes
  475. function updateSelection(oldIndex, newIndex)
  476. if scrollTo(newIndex) then
  477. showFiles()
  478. else
  479. if oldIndex and files[oldIndex] then showFile(oldIndex) end
  480. showFile(newIndex)
  481. end
  482. showPath()
  483. end
  484.  
  485. function getFileIndexFromY(y)
  486. return y + scrollY - CONTENT_OFFSET_Y
  487. end
  488.  
  489. function getYFromFileIndex(index)
  490. return index - scrollY + CONTENT_OFFSET_Y
  491. end
  492.  
  493. function drawInput(input, cursorPos)
  494. local width, _ = term.getSize()
  495.  
  496. term.setCursorPos(input.x, input.y)
  497. term.setTextColor(input.color)
  498.  
  499. term.setBackgroundColor(input.highlightColor)
  500. term.write(input.text)
  501.  
  502. term.setBackgroundColor(input.backgroundColor)
  503. term.write(string.rep(" ", width - input.x - #input.text))
  504.  
  505. term.setCursorBlink(true)
  506. term.setCursorPos(input.x + cursorPos - 1, input.y)
  507. end
  508.  
  509. addListener("term_resize", Focus.FILES, function()
  510. showEverything()
  511. end)
  512.  
  513. addListener("mouse_scroll", Focus.FILES, function(direction)
  514. local width, height = term.getSize()
  515. local newScrollY = scrollY + direction
  516.  
  517. if newScrollY < 0 or newScrollY > #files - height + 1 then return end
  518.  
  519. scrollY = newScrollY
  520.  
  521. showFiles()
  522. end)
  523.  
  524. function deleteFile(file)
  525. deleteIndex = getSelectedIndex() - 1
  526. if file and not file.readonly and file.type ~= FileType.DISK then
  527. table.insert(undoBuffer, {
  528. ogPath = file.path,
  529. recyclePath = recycleDir..file.name,
  530. })
  531. if fs.exists(recycleDir..file.name) then fs.delete(recycleDir..file.name) end
  532. fs.move(file.path, recycleDir..file.name)
  533.  
  534. end
  535. end
  536.  
  537. function undoDelete()
  538. local index = #undoBuffer
  539. if index ~= 0 then
  540. local recycledFile = undoBuffer[index]
  541. fs.move(recycledFile.recyclePath, recycledFile.ogPath)
  542. table.remove(undoBuffer, index)
  543. end
  544. end
  545.  
  546. function pasteFile(file)
  547. local clippedFile = clipboard[1]
  548. local pastePath = "/"
  549. if file and file.type ~= FileType.FILE then pastePath = file.path.."/" end
  550. if clippedFile ~= nil and fs.exists(pastePath..clippedFile.name) and fs.exists(clippedFile.path) and fs.isDir(clippedFile.path) and fs.getDrive(clippedFile.path) ~= "hdd" then
  551. mergeFolders(pastePath, clippedFile)
  552. elseif clippedFile ~= nil and fs.exists(pastePath..clippedFile.name) and fs.exists(clippedFile.path) then
  553. local clippedName = clippedFile.name
  554. local clippedNum = tonumber(string.match(clippedName, "-(%d*)"))
  555. local clippedExt = string.match(clippedName, "%.(.*)")
  556. if clippedExt == nil then clippedExt = "" else clippedExt = "."..clippedExt end
  557. local justName = string.match(clippedName,"(.*)"..clippedExt)
  558. if string.find(justName, "%-") then
  559. justName = string.match(justName,"(.+)-")
  560. elseif string.find(justName, "%.") then
  561. justName = string.sub(justName, "%.", "")
  562. end
  563. if clippedNum == nil then clippedNum = 1 end
  564. repeat
  565. clippedNum = clippedNum + 1
  566. until not fs.exists(pastePath..justName.."-"..clippedNum..clippedExt)
  567. clippedNum = "-"..(clippedNum)
  568. clippedName = justName..clippedNum..clippedExt
  569.  
  570. fs.copy(clippedFile.path, pastePath..clippedName)
  571. return true
  572. elseif clippedFile ~= nil and fs.exists(clippedFile.path) then
  573. fs.copy(clippedFile.path, recycleDir.."copy/"..clippedFile.name)
  574. fs.copy(recycleDir.."copy/"..clippedFile.name, pastePath..clippedFile.name)
  575. fs.delete(recycleDir.."copy/")
  576. return true
  577. else
  578. return false
  579. end
  580. end
  581.  
  582. function copyFile(file)
  583. if file then
  584. clipboard = {}
  585. table.insert(clipboard, {
  586. name = file.name,
  587. path = file.path,
  588. })
  589. end
  590. end
  591.  
  592. function makeNewFile(param, file)
  593. term.redirect(contextWindow)
  594. term.setCursorPos(1,8)
  595. term.setBackgroundColor(colors.lightGray)
  596. local nameString = "Name "..param
  597. local fg = "0"
  598. local bg = "8"
  599. term.blit(nameString,string.rep(fg,#nameString),string.rep(bg,#nameString))
  600. term.write(" ")
  601. term.setCursorPos(#nameString+1,8)
  602. term.setCursorBlink(true)
  603. event, key = os.pullEvent("key")
  604. if key ~= keys.enter then
  605. term.setTextColor(colors.gray)
  606. term.setCursorPos(1,8)
  607. term.blit(string.rep(" ",#nameString),string.rep(fg,#nameString),string.rep(bg,#nameString))
  608. term.setCursorPos(1,8)
  609. local input = read()
  610. local targetDir = "/"
  611. if file and not file.readonly and file.type ~= FileType.FILE then targetDir = (file.path.."/") end
  612. if input ~= nil and input ~= "" and param == "folder" and not fs.exists(targetDir..input) then
  613. fs.makeDir(targetDir..input)
  614. return true
  615. elseif input ~= nil and input ~= "" and param == "file" and not fs.exists(targetDir..input) then
  616. local writeFile = fs.open(targetDir..input, "w")
  617. writeFile.close()
  618. return true
  619. else return false
  620. end
  621. end
  622. term.setCursorBlink(false)
  623. term.redirect(ogTerm)
  624. end
  625.  
  626. function mergeFolders(pastePath, clippedFile)
  627. local mergedFilePath = pastePath..clippedFile.name.."-merged"
  628. if fs.exists(mergedFilePath) then fs.delete(mergedFilePath) end
  629. fs.copy(clippedFile.path.."/*", mergedFilePath)
  630. if pastePath..clippedFile.name ~= "/"..clippedFile.path then
  631. -- Recursive file sorting inspired by shorun's ls.sh
  632. local pathname = pastePath..clippedFile.name
  633. local tempfile = fs.list(pathname)
  634. local count = 1
  635. local filename = tempfile[count]
  636. local filelist = {}
  637. local todo = {}
  638. repeat
  639. if filename and fs.isDir(pathname.."/"..filename) then
  640. table.insert(todo, pathname.."/"..filename)
  641. count = count + 1
  642. filename = tempfile[count]
  643. elseif filename and fs.exists(pathname.."/"..filename) then
  644. table.insert(filelist, filename)
  645. count = count + 1
  646. filename = tempfile[count]
  647. elseif todo[1] then
  648. pathname = todo[1]
  649. tempfile = fs.list(pathname)
  650. count = 1
  651. filename = tempfile[count]
  652. table.remove(todo, 1)
  653. else filename = nil end
  654. until filename == nil and not todo[1]
  655. for key, value in pairs(filelist) do
  656. if not fs.exists(mergedFilePath.."/"..value) then
  657. fs.copy(pastePath..clippedFile.name.."/"..value, mergedFilePath.."/"..value)
  658. end
  659. end
  660. end
  661. end
  662.  
  663. local function clearScreen()
  664. term.setBackgroundColor(colors.black)
  665. term.setTextColor(colors.white)
  666. term.setCursorPos(1, 1)
  667. term.clear()
  668. end
  669.  
  670. local function getProgramForExtension(extension)
  671. if not settings then return "edit" end
  672. return settings.get("minexp.programs." .. extension, settings.get("minexp.default_program", "edit"))
  673. end
  674.  
  675. -- Edit files, expand/collapse folders
  676. function doPrimaryAction(file)
  677. if file.type == FileType.FILE then
  678. local ext = getFileExtension(file.name)
  679. shell.run(getProgramForExtension(ext), "/" .. file.path)
  680. showEverything()
  681. else
  682. if file.expanded then collapse()
  683. else expand() end
  684. showFiles()
  685. end
  686. end
  687.  
  688. -- Execute files, switch to folders and quit
  689. -- Returns whether to exit
  690. function doSecondaryAction(file, isTab)
  691. if file and file.type == FileType.FILE then
  692. if not isTab then
  693. clearScreen()
  694. shell.run(file.path)
  695. term.write("Press any key to continue")
  696. os.pullEvent("key")
  697. showEverything()
  698. return false
  699. else
  700. shell.switchTab(shell.openTab(file.path))
  701. return false
  702. end
  703. end
  704. end
  705.  
  706. addListener("key", Focus.FILES, function(key)
  707. local selection = getSelectedIndex()
  708. local file = files[selection]
  709. if settings.get("minexp.disable_hotkeys") == true then return
  710. elseif key == keys.down or key == keys.j then
  711. if not selection then selection = 0 end
  712.  
  713. if (selection <= #files - 1) then
  714. setSelection(selection + 1)
  715. updateSelection(selection, selection + 1)
  716. end
  717.  
  718. elseif key == keys.up or key == keys.k then
  719. if not selection then selection = #files + 1 end
  720.  
  721. if (selection > 1) then
  722. setSelection(selection - 1)
  723. updateSelection(selection, selection - 1)
  724. end
  725.  
  726. elseif key == keys.home then
  727. setSelection(1)
  728. updateSelection(selection, 1)
  729.  
  730. elseif key == keys["end"] then
  731. setSelection(#files)
  732. updateSelection(selection, #files)
  733.  
  734. elseif key == keys.right or key == keys.l then
  735. expand()
  736. showFiles()
  737.  
  738. elseif key == keys.left or key == keys.h then
  739. collapse()
  740. showFiles()
  741.  
  742. -- Edit on leftCtrl
  743. elseif key == keys.leftCtrl then
  744. if not selection then return end
  745.  
  746. doPrimaryAction(file)
  747.  
  748. -- Run in shell on 'Enter'
  749. elseif key == keys.enter then
  750. if not selection then return end
  751.  
  752. local file = files[selection]
  753. return doSecondaryAction(file)
  754.  
  755. -- Rename on F1 or F2
  756. elseif key == keys.f1 or key == keys.f2 then
  757. if not selection then return end
  758. renameFile(selection)
  759.  
  760. -- Deselect with 'Space'
  761. elseif key == keys.space then
  762. deselect()
  763. showEverything()
  764.  
  765. -- Copy file/folder with 1 or 'c'
  766. elseif key == keys.one or key == keys.c then
  767. copyFile(file)
  768.  
  769. -- Paste file/folder with 2 or 'v'
  770. elseif key == keys.two or key == keys.v then
  771. pasteFile(file)
  772. redrawFromContext()
  773.  
  774. -- Delete file/folder with 'x' or 'Delete'
  775. elseif key == keys.x or key == keys.delete then
  776. if not selection then return end
  777. deleteFile(file)
  778. redrawFromContext("delete")
  779.  
  780. -- Undo delete with 'z' or 'Backspace'
  781. elseif key == keys.z or key == keys.backspace then
  782. undoDelete()
  783. redrawFromContext()
  784.  
  785. -- Run program in new tab with 'Tab'
  786. elseif key == keys.tab then
  787. if not selection then return end
  788.  
  789. local file = files[selection]
  790. return doSecondaryAction(file, true)
  791.  
  792. -- Create new file with 'Insert'
  793. elseif key == keys.insert then
  794. contextWindow.reposition(1,1)
  795. contextWindow.setVisible(true)
  796. fileFromContext("file", file, 1, 1)
  797.  
  798. end
  799. end)
  800.  
  801. function renameFile(selection)
  802. if not selection then return end
  803. local file = files[selection]
  804. create({
  805. text = file.name,
  806. x = file.depth + 3,
  807. y = getYFromFileIndex(selection),
  808. color = txtColor,
  809. backgroundColor = bgColor,
  810. highlightColor = activeColor,
  811. cancelKey = keys.f1,
  812. callback = function(newName)
  813. if #newName == 0 then return true end
  814.  
  815. local newPath = fs.combine(fs.getDir(file.path), newName)
  816.  
  817. if fs.exists(newPath) or fs.isReadOnly(file.path) then
  818. return false
  819. end
  820.  
  821. fs.move(file.path, newPath)
  822. file.name = newName
  823. file.path = newPath
  824.  
  825. showPath()
  826. if file.depth > 0 then redrawFromContext() else showFiles() end
  827.  
  828. return true
  829. end
  830. })
  831. end
  832.  
  833. function handleRecycling()
  834. if settings.get("minexp.recycle_on_leave") == true then fs.delete(recycleDir) end
  835. end
  836.  
  837. function shutdownExplorer()
  838. clearScreen()
  839. handleRecycling()
  840. settings.clear()
  841. error("Exited MineExplorer+", 0)
  842. end
  843.  
  844. -- Quit when pressing Q
  845. -- This is in key up to prevent typing 'q' in the terminal
  846. addListener("key_up", Focus.FILES, function(key)
  847. if key == keys.q then
  848. shutdownExplorer()
  849. return true
  850. end
  851. end)
  852.  
  853. addListener("mouse_click", Focus.FILES, function(btn, x, y)
  854. local width, _ = term.getSize()
  855. if btn == 2 then showContextWindow(x,y) return end
  856. if btn ~= 1 then return end
  857. if (x <= width and x >= width-1 and y == 1) then
  858. shutdownExplorer()
  859. return
  860. end
  861.  
  862. local oldSelection = getSelectedIndex()
  863. local fileIndex = getFileIndexFromY(y)
  864. local file = files[fileIndex]
  865.  
  866. -- Deselect when pressing outside
  867. if not file then
  868. deselect()
  869. showFiles()
  870. showPath()
  871. return
  872. end
  873.  
  874. -- Select if not selected already
  875. if not file.selected then
  876. setSelection(fileIndex)
  877. updateSelection(oldSelection, fileIndex)
  878. return
  879. end
  880.  
  881. doPrimaryAction(file)
  882. end)
  883.  
  884.  
  885.  
  886. -- Input
  887. -- * text
  888. -- * x
  889. -- * y
  890. -- * color
  891. -- * backgroundColor
  892. -- * highlightColor
  893. -- * cancelKey
  894. -- * callback
  895. local currentInput = nil
  896. local cursorPos = nil
  897.  
  898. function create(input)
  899. currentInput = input
  900. cursorPos = #currentInput.text + 1
  901. setFocus(Focus.INPUT)
  902. drawInput(currentInput, cursorPos)
  903. end
  904.  
  905. local function endInput()
  906. currentInput = nil
  907. setFocus(Focus.FILES)
  908. showFiles()
  909. end
  910.  
  911. addListener("char", Focus.INPUT, function(char)
  912. currentInput.text = string.sub(currentInput.text, 1, cursorPos - 1) .. char .. string.sub(currentInput.text, cursorPos)
  913. cursorPos = cursorPos + 1
  914. drawInput(currentInput, cursorPos)
  915. end)
  916.  
  917. addListener("key", Focus.INPUT, function(key)
  918.  
  919. if key == keys.right then
  920. cursorPos = math.min(cursorPos + 1, #currentInput.text + 1)
  921. drawInput(currentInput, cursorPos)
  922.  
  923. elseif key == keys.left then
  924. cursorPos = math.max(cursorPos - 1, 1)
  925. drawInput(currentInput, cursorPos)
  926.  
  927. elseif key == keys.home then
  928. cursorPos = 1
  929. drawInput(currentInput, cursorPos)
  930.  
  931. elseif key == keys["end"] then
  932. cursorPos = #currentInput.text + 1
  933. drawInput(currentInput, cursorPos)
  934.  
  935. elseif key == keys.backspace then
  936. if cursorPos == 1 then return end
  937. currentInput.text = string.sub(currentInput.text, 1, cursorPos - 2) .. string.sub(currentInput.text, cursorPos)
  938. cursorPos = cursorPos - 1
  939. drawInput(currentInput, cursorPos)
  940.  
  941. elseif key == keys.delete then
  942. if cursorPos == #currentInput.text + 1 then return end
  943. currentInput.text = string.sub(currentInput.text, 1, cursorPos - 1) .. string.sub(currentInput.text, cursorPos + 1)
  944. drawInput(currentInput, cursorPos)
  945.  
  946. elseif key == keys.enter then
  947. currentInput.callback(currentInput.text)
  948. endInput()
  949.  
  950. elseif key == currentInput.cancelKey then
  951. endInput()
  952. end
  953. end)
  954.  
  955.  
  956.  
  957. FileType = {
  958. FILE = 0,
  959. DIRECTORY = 1,
  960. DISK = 2
  961. }
  962.  
  963. -- File:
  964. -- * name: string
  965. -- * path: string
  966. -- * type: FileType
  967. -- * readonly: bool
  968. -- * depth: int
  969. -- * selected: bool
  970. -- * expanded: bool
  971. files = {}
  972.  
  973. function loadFiles(path, depth, index)
  974. -- Get new files
  975. local newFiles = {}
  976. for _, file in ipairs(fs.list(path)) do
  977. local filePath = fs.combine(path, file)
  978. local type
  979.  
  980. if filePath == file and fs.getDrive(filePath) ~= "hdd" then type = FileType.DISK
  981. elseif fs.isDir(filePath) then type = FileType.DIRECTORY
  982. else type = FileType.FILE end
  983.  
  984. table.insert(newFiles, {
  985. name = file,
  986. path = filePath,
  987. type = type,
  988. readonly = fs.isReadOnly(filePath),
  989. depth = depth + 1,
  990. selected = false,
  991. expanded = false,
  992. })
  993. end
  994.  
  995. -- Sort by file type
  996. table.sort(newFiles, function(a, b)
  997. if a.type ~= b.type then
  998. return a.type > b.type
  999. end
  1000. return a.name < b.name
  1001. end)
  1002.  
  1003. -- Add to files array
  1004. for i, file in ipairs(newFiles) do
  1005. table.insert(files, index + i, file)
  1006. end
  1007. end
  1008.  
  1009. function loadAllFiles()
  1010. loadFiles("/", 0, 0)
  1011. end
  1012.  
  1013. function getSelectedIndex()
  1014. for i, file in ipairs(files) do
  1015. if file.selected then return i end
  1016. end
  1017. return nil
  1018. end
  1019.  
  1020. function setSelection(index)
  1021. for i, file in ipairs(files) do
  1022. file.selected = i == index
  1023. end
  1024. end
  1025.  
  1026. function deselect()
  1027. for _, file in ipairs(files) do
  1028. file.selected = false
  1029. end
  1030. end
  1031.  
  1032. function expand()
  1033. local index = getSelectedIndex()
  1034. local file = files[index]
  1035.  
  1036. if not file or file.type == FileType.FILE or file.expanded then return end
  1037.  
  1038. loadFiles(file.path, file.depth, index)
  1039.  
  1040. file.expanded = true
  1041. end
  1042.  
  1043. function collapse()
  1044. local index = getSelectedIndex()
  1045. local file = files[index]
  1046.  
  1047. if not file or file.type == FileType.FILE or not file.expanded then return end
  1048.  
  1049. local i = index + 1
  1050. while i <= #files and files[i].depth > file.depth do
  1051. table.remove(files, i)
  1052. end
  1053.  
  1054. file.expanded = false
  1055. fixScreen()
  1056. end
  1057.  
  1058. function getFileExtension(name)
  1059. if not string.find(name, "%.") then return "" end
  1060. return string.gsub(name, "%w*%.", "")
  1061. end
  1062.  
  1063. function getCurrentPath()
  1064. local index = getSelectedIndex()
  1065. if not index then return "/" end
  1066. return "/" .. files[index].path
  1067. end
  1068.  
  1069. _G["dir"] = fs.getDir(shell.getRunningProgram())
  1070. _G["shell"] = shell
  1071.  
  1072. term.clear()
  1073.  
  1074. loadAllFiles()
  1075. files[1].selected = true
  1076.  
  1077. showEverything()
  1078. drawContextWindow()
  1079.  
  1080. setFocus(Focus.FILES)
  1081. listen()
Add Comment
Please, Sign In to add comment