opus/apps/Files.lua

376 lines
9.1 KiB
Lua

require = requireInjector(getfenv(1))
local Util = require('util')
local Event = require('event')
local UI = require('ui')
local cleanEnv = Util.shallowCopy(getfenv(1))
cleanEnv.require = nil
multishell.setTitle(multishell.getCurrent(), 'Files')
UI:configure('Files', ...)
local copied = { }
local marked = { }
local directories = { }
local hidden = true
local cutMode = false
function formatSize(size)
if size >= 1000000 then
return string.format('%dM', math.floor(size/1000000, 2))
elseif size >= 1000 then
return string.format('%dK', math.floor(size/1000, 2))
end
return size
end
local Browser = UI.Page {
menuBar = UI.MenuBar {
buttons = {
{ text = '^-', event = 'updir' },
{ text = 'File', event = 'dropdown', dropdown = 'fileMenu' },
{ text = 'Edit', event = 'dropdown', dropdown = 'editMenu' },
{ text = 'View', event = 'dropdown', dropdown = 'viewMenu' },
},
},
fileMenu = UI.DropMenu {
buttons = {
{ text = 'Run', event = 'run' },
{ text = 'Edit e', event = 'edit' },
{ text = 'Shell s', event = 'shell' },
{ text = 'Quit q', event = 'quit' },
}
},
editMenu = UI.DropMenu {
buttons = {
{ text = 'Mark m', event = 'mark' },
{ text = 'Cut ^x', event = 'cut' },
{ text = 'Copy ^c', event = 'copy' },
{ text = 'Paste ^v', event = 'paste' },
{ text = 'Delete del', event = 'delete' },
{ text = 'Unmark all u', event = 'unmark' },
}
},
viewMenu = UI.DropMenu {
buttons = {
{ text = 'Refresh r', event = 'refresh' },
{ text = 'Hidden ^h', event = 'toggle_hidden' },
}
},
grid = UI.ScrollingGrid {
columns = {
{ heading = 'Name', key = 'name', width = UI.term.width-11 },
{ key = 'flags', width = 2 },
{ heading = 'Size', key = 'fsize', width = 6 },
},
sortColumn = 'name',
y = 2,
height = UI.term.height-2,
},
statusBar = UI.StatusBar {
columns = {
{ '', 'status', UI.term.width - 19 },
{ '', 'info', 10 },
{ 'Size: ', 'totalSize', 8 },
},
},
accelerators = {
q = 'quit',
e = 'edit',
s = 'shell',
r = 'refresh',
space = 'mark',
backspace = 'updir',
m = 'move',
u = 'unmark',
d = 'delete',
delete = 'delete',
[ 'control-h' ] = 'toggle_hidden',
[ 'control-x' ] = 'cut',
[ 'control-c' ] = 'copy',
paste = 'paste',
},
}
function Browser:enable()
UI.Page.enable(self)
self:setFocus(self.grid)
end
function Browser.grid:sortCompare(a, b)
if self.sortColumn == 'fsize' then
return a.size < b.size
elseif self.sortColumn == 'flags' then
return a.flags < b.flags
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function Browser.grid:getRowTextColor(file, selected)
if file.marked then
return colors.green
end
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end
function Browser.grid:getRowBackgroundColorX(file, selected)
if selected then
return colors.gray
end
return self.backgroundColor
end
function Browser.statusBar:draw()
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end
function Browser:setStatus(status, ...)
self.statusBar:timedStatus(string.format(status, ...))
end
function Browser:unmarkAll()
for k,m in pairs(marked) do
m.marked = false
end
Util.clear(marked)
end
function Browser:getDirectory(directory)
local s, dir = pcall(function()
local dir = directories[directory]
if not dir then
dir = {
name = directory,
size = 0,
files = { },
totalSize = 0,
index = 1
}
directories[directory] = dir
end
self:updateDirectory(dir)
return dir
end)
return s, dir
end
function Browser:updateDirectory(dir)
dir.size = 0
dir.totalSize = 0
Util.clear(dir.files)
local files = fs.list(dir.name, true)
if files then
dir.size = #files
for _, file in pairs(files) do
file.fullName = fs.combine(dir.name, file.name)
file.directory = directory
file.flags = ''
if not file.isDir then
dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size)
else
file.flags = 'D'
end
if file.isReadOnly then
file.flags = file.flags .. 'R'
end
if not hidden or file.name:sub(1, 1) ~= '.' then
dir.files[file.fullName] = file
end
end
end
-- self.grid:update()
-- self.grid:setIndex(dir.index)
self.grid:setValues(dir.files)
end
function Browser:setDir(dirName, noStatus)
self:unmarkAll()
if self.dir then
self.dir.index = self.grid:getIndex()
end
DIR = fs.combine('', dirName)
shell.setDir(DIR)
local s, dir = self:getDirectory(DIR)
if s then
self.dir = dir
elseif noStatus then
error(dir)
else
self:setStatus(dir)
self:setDir('', true)
return
end
if not noStatus then
self.statusBar:setValue('status', '/' .. self.dir.name)
self.statusBar:draw()
end
self.grid:setIndex(self.dir.index)
end
function Browser:run(path, ...)
local tabId = multishell.launch(cleanEnv, path, ...)
multishell.setFocus(tabId)
end
function Browser:hasMarked()
if Util.size(marked) == 0 then
local file = self.grid:getSelected()
if file then
file.marked = true
marked[file.fullName] = file
self.grid:draw()
end
end
return Util.size(marked) > 0
end
function Browser:eventHandler(event)
local file = self.grid:getSelected()
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'edit' and file then
self:run('/apps/shell', 'edit', file.name)
elseif event.type == 'shell' then
self:run('/apps/shell')
elseif event.type == 'refresh' then
self:updateDirectory(self.dir)
self.grid:draw()
self:setStatus('Refreshed')
elseif event.type == 'toggle_hidden' then
hidden = not hidden
self:updateDirectory(self.dir)
self.grid:draw()
if hidden then
self:setStatus('Hiding hidden')
else
self:setStatus('Displaying hidden')
end
elseif event.type == 'mark' and file then
file.marked = not file.marked
if file.marked then
marked[file.fullName] = file
else
marked[file.fullName] = nil
end
self.grid:draw()
self.statusBar:draw()
elseif event.type == 'unmark' then
self:unmarkAll()
self.grid:draw()
self:setStatus('Marked files cleared')
elseif event.type == 'grid_select' or event.type == 'run' then
if file then
if file.isDir then
self:setDir(file.fullName)
else
self:run('/apps/shell', file.name)
end
end
elseif event.type == 'updir' then
local dir = (self.dir.name:match("(.*/)"))
self:setDir(dir or '/')
elseif event.type == 'delete' then
if self:hasMarked() then
local width = self.statusBar:getColumnWidth('status')
self.statusBar:setColumnWidth('status', UI.term.width)
self.statusBar:setValue('status', 'Delete marked? (y/n)')
self.statusBar:draw()
self.statusBar:sync()
local _, ch = os.pullEvent('char')
if ch == 'y' or ch == 'Y' then
for k,m in pairs(marked) do
pcall(function()
fs.delete(m.fullName)
end)
end
end
marked = { }
self.statusBar:setColumnWidth('status', width)
self.statusBar:setValue('status', '/' .. self.dir.name)
self:updateDirectory(self.dir)
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
end
elseif event.type == 'copy' or event.type == 'cut' then
if self:hasMarked() then
cutMode = event.type == 'cut'
Util.clear(copied)
Util.merge(copied, marked)
--self:unmarkAll()
self.grid:draw()
self:setStatus('Copied %d file(s)', Util.size(copied))
end
elseif event.type == 'paste' then
for k,m in pairs(copied) do
local s, m = pcall(function()
if cutMode then
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
else
fs.copy(m.fullName, fs.combine(self.dir.name, m.name))
end
end)
end
self:updateDirectory(self.dir)
self.grid:draw()
self:setStatus('Pasted ' .. Util.size(copied) .. ' file(s)')
else
return UI.Page.eventHandler(self, event)
end
self:setFocus(self.grid)
return true
end
--[[-- Startup logic --]]--
local args = { ... }
Browser:setDir(args[1] or shell.dir())
UI:setPage(Browser)
Event.pullEvents()
UI.term:reset()