mirror of
https://github.com/NeoFlock/neonucleus.git
synced 2025-09-25 01:23:31 +02:00
332 lines
8.3 KiB
Lua
332 lines
8.3 KiB
Lua
local computer = require("computer")
|
|
local shell = require("shell")
|
|
local fs = require("filesystem")
|
|
local tx = require("transforms")
|
|
local text = require("text")
|
|
|
|
local args, opts = shell.parse(...)
|
|
|
|
local function die(...)
|
|
io.stderr:write(...)
|
|
os.exit(1)
|
|
end
|
|
|
|
do -- handle cli
|
|
if opts.help then
|
|
print([[Usage: tree [OPTION]... [FILE]...
|
|
-a, --all do not ignore entries starting with .
|
|
--full-time with -l, print time in full iso format
|
|
-h, --human-readable with -l, print human readable sizes
|
|
--si likewise, but use powers of 1000 not 1024
|
|
--level=LEVEL descend only LEVEL directories deep
|
|
--color=WHEN WHEN can be
|
|
auto - colorize output only if writing to a tty,
|
|
always - always colorize output,
|
|
never - never colorize output; (default: auto)
|
|
-l use a long listing format
|
|
-f print the full path prefix for each file
|
|
-i do not print indentation lines
|
|
-p append "/" indicator to directories
|
|
-Q, --quote quote filenames with double quotes
|
|
-r, --reverse reverse order while sorting
|
|
-S sort by file size
|
|
-t sort by modification type, newest first
|
|
-X sort alphabetically by entry extension
|
|
-C do not count files and directories
|
|
-R count root directories like other files
|
|
--help print this help and exit]])
|
|
return 0
|
|
end
|
|
|
|
if #args == 0 then
|
|
table.insert(args, ".")
|
|
end
|
|
|
|
opts.level = tonumber(opts.level) or math.huge
|
|
if opts.level < 1 then
|
|
die("Invalid level, must be greater than 0")
|
|
end
|
|
|
|
opts.color = opts.color or "auto"
|
|
if opts.color == "auto" then
|
|
opts.color = io.stdout.tty and "always" or "never"
|
|
end
|
|
|
|
if opts.color ~= "always" and opts.color ~= "never" then
|
|
die("Invalid value for --color=WHEN option; WHEN should be auto, always or never")
|
|
end
|
|
end
|
|
|
|
local lastYield = computer.uptime()
|
|
local function yieldopt()
|
|
if computer.uptime() - lastYield > 2 then
|
|
lastYield = computer.uptime()
|
|
os.sleep(0)
|
|
end
|
|
end
|
|
|
|
local function peekable(iterator, state, var1)
|
|
local nextItem = {iterator(state, var1)}
|
|
|
|
return setmetatable({
|
|
peek = function()
|
|
return table.unpack(nextItem)
|
|
end
|
|
}, {
|
|
__call = coroutine.wrap(function()
|
|
while true do
|
|
local item = nextItem
|
|
nextItem = {iterator(state, nextItem[1])}
|
|
coroutine.yield(table.unpack(item))
|
|
if nextItem[1] == nil then break end
|
|
end
|
|
end)
|
|
})
|
|
end
|
|
|
|
local function filter(entry)
|
|
return opts.a or entry:sub(1, 1) ~= "."
|
|
end
|
|
|
|
local function stat(path)
|
|
local st = {}
|
|
st.path = path
|
|
st.name = fs.name(path) or "/"
|
|
st.sortName = st.name:gsub("^%.","")
|
|
st.time = fs.lastModified(path)
|
|
st.isLink = fs.isLink(path)
|
|
st.isDirectory = fs.isDirectory(path)
|
|
st.size = st.isLink and 0 or fs.size(path)
|
|
st.extension = st.name:match("(%.[^.]+)$") or ""
|
|
st.fs = fs.get(path)
|
|
return st
|
|
end
|
|
|
|
local colorize
|
|
if opts.color == "always" then
|
|
-- from /lib/core/full_ls.lua
|
|
local colors = tx.foreach(text.split(os.getenv("LS_COLORS") or "", {":"}, true), function(e)
|
|
local parts = text.split(e, {"="}, true)
|
|
return parts[2], parts[1]
|
|
end)
|
|
|
|
function colorize(stat)
|
|
return stat.isLink and colors.ln or
|
|
stat.isDirectory and colors.di or
|
|
colors["*" .. stat.extension] or
|
|
colors.fi
|
|
end
|
|
end
|
|
|
|
local function list(path)
|
|
return coroutine.wrap(function()
|
|
local l = {}
|
|
for entry in fs.list(path) do
|
|
if filter(entry) then
|
|
table.insert(l, stat(fs.concat(path, entry)))
|
|
end
|
|
end
|
|
|
|
if opts.S then
|
|
table.sort(l, function(a, b)
|
|
return a.size < b.size
|
|
end)
|
|
elseif opts.t then
|
|
table.sort(l, function(a, b)
|
|
return a.time < b.time
|
|
end)
|
|
elseif opts.X then
|
|
table.sort(l, function(a, b)
|
|
return a.extension < b.extension
|
|
end)
|
|
else
|
|
table.sort(l, function(a, b)
|
|
return a.sortName < b.sortName
|
|
end)
|
|
end
|
|
|
|
for i = opts.r and #l or 1, opts.r and 1 or #l, opts.r and -1 or 1 do
|
|
coroutine.yield(l[i])
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function digRoot(rootPath)
|
|
coroutine.yield(stat(rootPath), {})
|
|
|
|
if not fs.isDirectory(rootPath) then
|
|
return
|
|
end
|
|
local iterStack = {peekable(list(rootPath))}
|
|
local pathStack = {rootPath}
|
|
local levelStack = {not not iterStack[#iterStack]:peek()}
|
|
|
|
|
|
repeat
|
|
local entry = iterStack[#iterStack]()
|
|
|
|
if entry then
|
|
levelStack[#levelStack] = not not iterStack[#iterStack]:peek()
|
|
|
|
local path = fs.concat(fs.concat(table.unpack(pathStack)), entry.name)
|
|
|
|
coroutine.yield(entry, levelStack)
|
|
|
|
if entry.isDirectory and opts.level > #levelStack then
|
|
table.insert(iterStack, peekable(list(path)))
|
|
table.insert(pathStack, entry.name)
|
|
table.insert(levelStack, not not iterStack[#iterStack]:peek())
|
|
end
|
|
else
|
|
table.remove(iterStack)
|
|
table.remove(pathStack)
|
|
table.remove(levelStack)
|
|
end
|
|
until #iterStack == 0
|
|
end
|
|
|
|
local function dig(roots)
|
|
return coroutine.wrap(function()
|
|
for _, root in ipairs(roots) do
|
|
digRoot(root)
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function nod(n) -- from /lib/core/full_ls.lua
|
|
return n and (tostring(n):gsub("(%.[0-9]+)0+$","%1")) or "0"
|
|
end
|
|
|
|
local function formatFSize(size) -- from /lib/core/full_ls.lua
|
|
if not opts.h and not opts["human-readable"] and not opts.si then
|
|
return tostring(size)
|
|
end
|
|
|
|
local sizes = {"", "K", "M", "G"}
|
|
local unit = 1
|
|
local power = opts.si and 1000 or 1024
|
|
|
|
while size > power and unit < #sizes do
|
|
unit = unit + 1
|
|
size = size / power
|
|
end
|
|
|
|
return nod(math.floor(size*10)/10)..sizes[unit]
|
|
end
|
|
|
|
local function pad(txt) -- from /lib/core/full_ls.lua
|
|
txt = tostring(txt)
|
|
return #txt >= 2 and txt or "0" .. txt
|
|
end
|
|
|
|
local function formatTime(epochms) -- from /lib/core/full_ls.lua
|
|
local month_names = {"January","February","March","April","May","June",
|
|
"July","August","September","October","November","December"}
|
|
|
|
if epochms == 0 then return "" end
|
|
|
|
local d = os.date("*t", epochms)
|
|
local day, hour, min, sec = nod(d.day), pad(nod(d.hour)), pad(nod(d.min)), pad(nod(d.sec))
|
|
|
|
if opts["full-time"] then
|
|
return string.format("%s-%s-%s %s:%s:%s ", d.year, pad(nod(d.month)), pad(day), hour, min, sec)
|
|
else
|
|
return string.format("%s %2s %2s:%2s ", month_names[d.month]:sub(1,3), day, hour, pad(min))
|
|
end
|
|
end
|
|
|
|
local function writeEntry(entry, levelStack)
|
|
for i, hasNext in ipairs(levelStack) do
|
|
if opts.i then break end
|
|
|
|
if i == #levelStack then
|
|
if hasNext then
|
|
io.write("├── ")
|
|
else
|
|
io.write("└── ")
|
|
end
|
|
else
|
|
if hasNext then
|
|
io.write("│ ")
|
|
else
|
|
io.write(" ")
|
|
end
|
|
end
|
|
end
|
|
|
|
if opts.l then
|
|
io.write("[")
|
|
|
|
io.write(entry.isDirectory and "d" or entry.isLink and "l" or "f", "-")
|
|
io.write("r", entry.fs.isReadOnly() and "-" or "w", " ")
|
|
|
|
io.write(formatFSize(entry.size), " ")
|
|
|
|
io.write(formatTime(entry.time))
|
|
io.write("] ")
|
|
end
|
|
|
|
if opts.Q then io.write('"') end
|
|
|
|
if opts.color == "always" then
|
|
io.write("\27[" .. colorize(entry) .. "m")
|
|
end
|
|
|
|
if opts.f then
|
|
io.write(entry.path)
|
|
else
|
|
io.write(entry.name)
|
|
end
|
|
|
|
if opts.color == "always" then
|
|
io.write("\27[0m")
|
|
end
|
|
|
|
if opts.p and entry.isDirectory then
|
|
io.write("/")
|
|
end
|
|
|
|
if opts.Q then io.write('"') end
|
|
io.write("\n")
|
|
end
|
|
|
|
local function writeCount(dirs, files)
|
|
io.write("\n")
|
|
io.write(dirs, " director", dirs == 1 and "y" or "ies")
|
|
io.write(", ")
|
|
io.write(files, " file", files == 1 and "" or "s")
|
|
io.write("\n")
|
|
end
|
|
|
|
local dirs, files = 0, 0
|
|
|
|
local roots = {}
|
|
for _, arg in ipairs(args) do
|
|
local path = shell.resolve(arg)
|
|
local real, reason = fs.realPath(path)
|
|
if not real then
|
|
die("cannot access ", path, ": ", reason or "unknown error")
|
|
elseif not fs.exists(path) then
|
|
die("cannot access ", path, ":", "No such file or directory")
|
|
else
|
|
table.insert(roots, real)
|
|
end
|
|
end
|
|
|
|
for entry, levelStack in dig(roots) do
|
|
if opts.R or #levelStack > 0 then
|
|
if entry.isDirectory then
|
|
dirs = dirs + 1
|
|
else
|
|
files = files + 1
|
|
end
|
|
end
|
|
writeEntry(entry, levelStack)
|
|
yieldopt()
|
|
end
|
|
|
|
if not opts.C then
|
|
writeCount(dirs, files)
|
|
end
|
|
|