testing version of LuaBIOS and OpenOS

people were having issues getting them to work so now we promote consistency
This commit is contained in:
2025-06-28 20:41:49 +02:00
parent 8210e20939
commit 687cfebd00
182 changed files with 14016 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
-- called from /init.lua
local raw_loadfile = ...
_G._OSVERSION = "OpenOS 1.8.8"
-- luacheck: globals component computer unicode _OSVERSION
local component = component
local computer = computer
local unicode = unicode
-- Runlevel information.
_G.runlevel = "S"
local shutdown = computer.shutdown
computer.runlevel = function() return _G.runlevel end
computer.shutdown = function(reboot)
_G.runlevel = reboot and 6 or 0
if os.sleep then
computer.pushSignal("shutdown")
os.sleep(0.1) -- Allow shutdown processing.
end
shutdown(reboot)
end
local w, h
local screen = component.list("screen", true)()
local gpu = screen and component.list("gpu", true)()
if gpu then
gpu = component.proxy(gpu)
if not gpu.getScreen() then
gpu.bind(screen)
end
_G.boot_screen = gpu.getScreen()
w, h = gpu.maxResolution()
gpu.setResolution(w, h)
gpu.setBackground(0x000000)
gpu.setForeground(0xFFFFFF)
gpu.fill(1, 1, w, h, " ")
end
-- Report boot progress if possible.
local y = 1
local uptime = computer.uptime
-- we actually want to ref the original pullSignal here because /lib/event intercepts it later
-- because of that, we must re-pushSignal when we use this, else things break badly
local pull = computer.pullSignal
local last_sleep = uptime()
local function status(msg)
if gpu then
gpu.set(1, y, msg)
if y == h then
gpu.copy(1, 2, w, h - 1, 0, -1)
gpu.fill(1, h, w, 1, " ")
else
y = y + 1
end
end
-- boot can be slow in some environments, protect from timeouts
if uptime() - last_sleep > 1 then
local signal = table.pack(pull(0))
-- there might not be any signal
if signal.n > 0 then
-- push the signal back in queue for the system to use it
computer.pushSignal(table.unpack(signal, 1, signal.n))
end
last_sleep = uptime()
end
end
status("Booting " .. _OSVERSION .. "...")
-- Custom low-level dofile implementation reading from our ROM.
local function dofile(file)
status("> " .. file)
local program, reason = raw_loadfile(file)
if program then
return program()
else
error(reason)
end
end
status("Initializing package management...")
-- Load file system related libraries we need to load other stuff moree
-- comfortably. This is basically wrapper stuff for the file streams
-- provided by the filesystem components.
local package = dofile("/lib/package.lua")
do
-- Unclutter global namespace now that we have the package module and a filesystem
_G.component = nil
_G.computer = nil
_G.process = nil
_G.unicode = nil
-- Inject the package modules into the global namespace, as in Lua.
_G.package = package
-- Initialize the package module with some of our own APIs.
package.loaded.component = component
package.loaded.computer = computer
package.loaded.unicode = unicode
package.loaded.buffer = dofile("/lib/buffer.lua")
package.loaded.filesystem = dofile("/lib/filesystem.lua")
-- Inject the io modules
_G.io = dofile("/lib/io.lua")
end
status("Initializing file system...")
-- Mount the ROM and temporary file systems to allow working on the file
-- system module from this point on.
require("filesystem").mount(computer.getBootAddress(), "/")
status("Running boot scripts...")
-- Run library startup scripts. These mostly initialize event handlers.
local function rom_invoke(method, ...)
return component.invoke(computer.getBootAddress(), method, ...)
end
local scripts = {}
for _, file in ipairs(rom_invoke("list", "boot")) do
local path = "boot/" .. file
if not rom_invoke("isDirectory", path) then
table.insert(scripts, path)
end
end
table.sort(scripts)
for i = 1, #scripts do
dofile(scripts[i])
end
status("Initializing components...")
for c, t in component.list() do
computer.pushSignal("component_added", c, t)
end
status("Initializing system...")
require("event").listen("component_added", debugprint)
require("event").listen("component_available", debugprint)
require("event").listen("term_available", debugprint)
computer.pushSignal("init") -- so libs know components are initialized.
require("event").pull(1, "init") -- Allow init processing.
require("tty").bind(component.gpu)
_G.runlevel = 1

View File

@@ -0,0 +1,270 @@
debugprint("loading cursor")
local unicode = require("unicode")
debugprint("loaded unicode")
local kb = require("keyboard")
debugprint("loaded keyboard")
local tty = require("tty")
debugprint("loaded tty")
local text = require("text")
debugprint("loaded text")
local computer = require("computer")
debugprint("loaded computer")
local keys = kb.keys
local core_cursor = {}
core_cursor.vertical = {}
function core_cursor.vertical:move(n)
local s = math.max(math.min(self.index + n, self.len), 0)
if s == self.index then return end
local echo_cmd = keys.left
local from = s + 1
local to = self.index
if s > self.index then
echo_cmd, from, to = keys.right, to + 1, s
end
self.index = s
local step = unicode.wlen(unicode.sub(self.data, from, to))
self:echo(echo_cmd, step)
end
-- back is used when arg comes after the cursor
function core_cursor.vertical:update(arg, back)
if not arg then
self.tails = {}
self.data = ""
self.index = 0
self.sy = 0
self.hindex = 0
end
local s1 = unicode.sub(self.data, 1, self.index)
local s2 = unicode.sub(self.data, self.index + 1)
if type(arg) == "string" then
if back == false then
arg, s2 = arg .. s2, ""
else
self.index = self.index + unicode.len(arg)
self:echo(arg)
end
self.data = s1 .. arg
elseif arg then -- number
local has_tail = arg < 0 or #s2 > 0
if arg < 0 then
-- backspace? ignore if at start
if self.index <= 0 then return end
self:move(arg)
s1 = unicode.sub(s1, 1, -1 + arg)
else
-- forward? ignore if at end
if self.index >= self.len then return end
s2 = unicode.sub(s2, 1 + arg)
end
self.data = s1
if has_tail then
self:echo(self.clear)
end
end
self.len = unicode.len(self.data) -- recompute len
self:move(back or 0)
if #s2 > 0 then
self:update(s2, -unicode.len(s2))
end
end
function core_cursor.vertical:echo(arg, num)
local win = tty.window
local gpu = win.gpu
-- we should not use io.write
-- the cursor should echo to the stream it is reading from
-- this makes sense because a process may redirect its io
-- but a cursor reading from a given stdin tty should also
-- echo to that same stream
-- but, if stdin has been piped - we do not echo the cursor
if not io.stdin.tty then
return
end
local out = io.stdin.stream
if not gpu then return end
win.nowrap = self.nowrap
if arg == "" then -- special scroll request
local width, x, y = win.width, win.x, win.y
if x > width then
win.x = ((x - 1) % width) + 1
win.y = y + math.floor(x / width)
out:write("") -- tty.stream:write knows how to scroll vertically
x, y = win.x, win.y
end
if x <= 0 or y <= 0 or y > win.height or not gpu then return end
return table.pack(select(2, pcall(gpu.get, x + win.dx, y + win.dy)))
elseif arg == keys.left then
local x = win.x - num
local y = win.y
while x < 1 do
x = x + win.width - #(self.tails[win.dy + y - self.sy - 1] or "")
y = y - 1
end
win.x, win.y = x, y
arg = ""
elseif arg == keys.right then
local x = win.x + num
local y = win.y
while true do
local width = win.width - #(self.tails[win.dy + y - self.sy] or "")
if x <= width then break end
x = x - width
y = y + 1
end
win.x, win.y = x, y
arg = ""
elseif not arg or arg == true then -- blink
local char = self.char_at_cursor
if (arg == nil and not char) or (arg and not self.blinked) then
char = char or self:echo("") --scroll and get char
if not char[1] then return false end
self.blinked = true
if not arg then
out:write("\0277")
char.saved = win.saved
gpu.setForeground(char[4] or char[2], not not char[4])
gpu.setBackground(char[5] or char[3], not not char[5])
end
out:write("\0277\27[7m"..char[1].."\0278")
elseif (arg and self.blinked) or (arg == false and char) then
self.blinked = false
gpu.set(win.x + win.dx, win.y + win.dy, char[1])
if not arg then
win.saved = char.saved
out:write("\0278")
char = nil
end
end
self.char_at_cursor = char
return true
end
return out:write(arg)
end
function core_cursor.vertical:handle(name, char, code)
if name == "clipboard" then
self.cache = nil -- this stops tab completion
local newline = char:find("\10") or #char
local printable_prefix, remainder = char:sub(1, newline), char:sub(newline + 1)
self:update(printable_prefix)
self:update(remainder, false)
elseif name == "touch" or name == "drag" then
core_cursor.touch(self, char, code)
elseif name == "interrupted" then
self:echo("^C\n")
return false, name
elseif name == "key_down" then
local data = self.data
local backup_cache = self.cache
self.cache = nil
local ctrl = kb.isControlDown()
if ctrl and code == keys.d then
return --nil:close
elseif code == keys.tab then
self.cache = backup_cache
core_cursor.tab(self)
elseif code == keys.enter or code == keys.numpadenter then
self:move(self.len)
self:update("\n")
elseif code == keys.up or code == keys.down then
local ni = self.hindex + (code == keys.up and 1 or -1)
if ni >= 0 and ni <= #self then
self[self.hindex] = data
self.hindex = ni
self:move(self.len)
self:update(-self.len)
self:update(self[ni])
end
elseif code == keys.left or code == keys.back or code == keys.w and ctrl then
local value = ctrl and ((unicode.sub(data, 1, self.index):find("%s[^%s]+%s*$") or 0) - self.index) or -1
if code == keys.left then
self:move(value)
else
self:update(value)
end
elseif code == keys.right then
self:move(ctrl and ((data:find("%s[^%s]", self.index + 1) or self.len) - self.index) or 1)
elseif code == keys.home then self:move(-self.len)
elseif code == keys["end"] then self:move( self.len)
elseif code == keys.delete then self:update(1)
elseif char >= 32 then self:update(unicode.char(char))
else self.cache = backup_cache -- ignored chars shouldn't clear hint cache
end
end
return true
end
-- echo'd to clear the input text in the tty
core_cursor.vertical.clear = "\27[J"
function core_cursor.new(base, index)
-- if base has defined any methods, those are called first
-- any new methods here are "super" methods to base
base = base or {}
base.super = base.super or index or core_cursor.vertical
setmetatable(base, getmetatable(base) or { __index = base.super })
if not base.data then
base:update()
end
return base
end
function core_cursor.read(cursor)
local last = cursor.next or ""
cursor.next = nil
if #last > 0 then
cursor:handle("clipboard", last)
end
-- address checks
local address_check =
{
key_down = tty.keyboard,
clipboard = tty.keyboard,
touch = tty.screen,
drag = tty.screen,
drop = tty.screen
}
while true do
local next_line = cursor.data:find("\10")
if next_line then
local result = cursor.data:sub(1, next_line)
local overflow = cursor.data:sub(next_line + 1)
local history = text.trim(result)
if history ~= "" and history ~= cursor[1] then
table.insert(cursor, 1, history)
cursor[(tonumber(os.getenv("HISTSIZE")) or 10) + 1] = nil
end
cursor[0] = nil
cursor:update()
cursor.next = overflow
return result
end
cursor:echo()
local pack = table.pack(computer.pullSignal(tty.window.blink and .5 or math.huge))
local name = pack[1]
cursor:echo(not name)
if name then
local filter_address = address_check[name]
if not filter_address or filter_address() == pack[2] then
local ret, why = cursor:handle(name, table.unpack(pack, 3, pack.n))
if not ret then
return ret, why
end
end
end
end
end
require("package").delay(core_cursor, "/lib/core/full_cursor.lua")
return core_cursor

View File

@@ -0,0 +1,131 @@
local comp = require("component")
local text = require("text")
local dcache = {}
local pcache = {}
local adapter_pwd = "/lib/core/devfs/adapters/"
local adapter_api = {}
function adapter_api.toArgsPack(input, pack)
local split = text.split(input, {"%s"}, true)
local req = pack[1]
local num = #split
if num < req then return nil, "insufficient args" end
local result = {n=num}
for index=1,num do
local typename = pack[index+1]
local token = split[index]
if typename == "boolean" then
if token ~= "true" and token ~= "false" then return nil, "bad boolean value" end
token = token == "true"
elseif typename == "number" then
token = tonumber(token)
if not token then return nil, "bad number value" end
end
result[index] = token
end
return result
end
function adapter_api.createWriter(callback, ...)
local types = table.pack(...)
return function(input)
local args, why = adapter_api.toArgsPack(input, types)
if not args then return why end
return callback(table.unpack(args, 1, args.n))
end
end
function adapter_api.create_toggle(read, write, switch)
return
{
read = read and function() return tostring(read()) end,
write = write and function(value)
value = text.trim(tostring(value))
local on = value == "1" or value == "true"
local off = value == "0" or value == "false"
if not on and not off then
return nil, "bad value"
end
if switch then
(off and switch or write)()
else
write(on)
end
end
}
end
function adapter_api.make_link(list, addr, prefix, bOmitZero)
prefix = prefix or ""
local zero = bOmitZero and "" or "0"
local id = 0
local name
repeat
name = string.format("%s%s", prefix, id == 0 and zero or tostring(id))
id = id + 1
until not list[name]
list[name] = {link=addr}
end
return
{
components =
{
list = function()
local dirs = {}
local types = {}
local labels = {}
local ads = {}
dirs["by-type"] = {list=function()return types end}
dirs["by-label"] = {list=function()return labels end}
dirs["by-address"] = {list=function()return ads end}
-- first sort the addr, primaries first, then sorted by address lexigraphically
local hw_addresses = {}
for addr,type in comp.list() do
local isPrim = comp.isPrimary(addr)
table.insert(hw_addresses, select(isPrim and 1 or 2, 1, {type,addr}))
end
for _,pair in ipairs(hw_addresses) do
local type, addr = table.unpack(pair)
if not dcache[type] then
local adapter_file = adapter_pwd .. type .. ".lua"
local loader = loadfile(adapter_file, "bt", _G)
dcache[type] = loader and loader(adapter_api)
end
local adapter = dcache[type]
if adapter then
local proxy = pcache[addr] or comp.proxy(addr)
pcache[addr] = proxy
ads[addr] =
{
list = function()
local devfs_proxy = adapter(proxy)
devfs_proxy.address = {proxy.address}
devfs_proxy.slot = {proxy.slot}
devfs_proxy.type = {proxy.type}
devfs_proxy.device = {device=proxy}
return devfs_proxy
end
}
-- by type building
local type_dir = types[type] or {list={}}
adapter_api.make_link(type_dir.list, "../../by-address/"..addr)
types[type] = type_dir
-- by label building (labels are only supported in filesystems
local label = require("devfs").getDeviceLabel(proxy)
if label then
adapter_api.make_link(labels, "../by-address/"..addr, label, true)
end
end
end
return dirs
end
},
}

View File

@@ -0,0 +1,57 @@
return
{
eeprom =
{
link = "components/by-type/eeprom/0/contents",
isAvailable = function()
local comp = require("component")
return comp.list("eeprom")()
end
},
["eeprom-data"] =
{
link = "components/by-type/eeprom/0/data",
isAvailable = function()
local comp = require("component")
return comp.list("eeprom")()
end
},
null =
{
open = function()
return
{
read = function() end,
write = function() end
}
end
},
random =
{
open = function(mode)
if mode and not mode:match("r") then
return nil, "read only"
end
return
{
read = function(_, n)
local chars = {}
for _=1,n do
table.insert(chars,string.char(math.random(0,255)))
end
return table.concat(chars)
end
}
end
},
zero =
{
open = function()
return
{
read = function(_, n) return ("\0"):rep(n) end,
write = function() end
}
end
},
}

View File

@@ -0,0 +1,9 @@
local adapter_api = ...
return function(proxy)
return
{
beep = {write=adapter_api.createWriter(proxy.beep, 0, "number", "number")},
running = adapter_api.create_toggle(proxy.isRunning, proxy.start, proxy.stop),
}
end

View File

@@ -0,0 +1,22 @@
local cache = {}
local function cload(callback)
local c = cache[callback]
if not c then
c = callback()
cache[callback] = c
end
return c
end
return function(proxy)
return
{
contents = {read=proxy.get, write=proxy.set},
data = {read=proxy.getData, write=proxy.setData},
checksum = {read=proxy.getChecksum,size=function() return 8 end},
size = {cload(proxy.getSize)},
dataSize = {cload(proxy.getDataSize)},
label = {write=proxy.setLabel,proxy.getLabel()},
makeReadonly = {write=proxy.makeReadonly}
}
end

View File

@@ -0,0 +1,25 @@
local fs = require("filesystem")
local text = require("text")
return function(proxy)
return
{
["label"] =
{
read = function() return proxy.getLabel() or "" end,
write= function(v) proxy.setLabel(text.trim(v)) end
},
["isReadOnly"] = {proxy.isReadOnly()},
["spaceUsed"] = {proxy.spaceUsed()},
["spaceTotal"] = {proxy.spaceTotal()},
["mounts"] = {read = function()
local mounts = {}
for mproxy,mpath in fs.mounts() do
if mproxy.address == proxy.address then
table.insert(mounts, mpath)
end
end
return table.concat(mounts, "\n")
end}
}
end

View File

@@ -0,0 +1,17 @@
local adapter_api = ...
return function(proxy)
local screen = proxy.getScreen()
screen = screen and ("../" .. screen)
return
{
viewport = {write = adapter_api.createWriter(proxy.setViewport, 2, "number", "number"), proxy.getViewport()},
resolution = {write = adapter_api.createWriter(proxy.setResolution, 2, "number", "number"), proxy.getResolution()},
maxResolution = {proxy.maxResolution()},
screen = {link=screen,isAvailable=proxy.getScreen},
depth = {write = adapter_api.createWriter(proxy.setDepth, 1, "number"), proxy.getDepth()},
maxDepth = {proxy.maxDepth()},
background = {write = adapter_api.createWriter(proxy.setBackground, 1, "number", "boolean"), proxy.getBackground()},
foreground = {write = adapter_api.createWriter(proxy.setForeground, 1, "number", "boolean"), proxy.getForeground()},
}
end

View File

@@ -0,0 +1,7 @@
return function(proxy)
return
{
httpEnabled = {proxy.isHttpEnabled()},
tcpEnabled = {proxy.isTcpEnabled()},
}
end

View File

@@ -0,0 +1,11 @@
return function(proxy)
return
{
wakeMessage =
{
read = function() return proxy.getWakeMessage() or "" end,
write= function(msg) return proxy.setWakeMessage(msg) end,
},
wireless = {proxy.isWireless()},
}
end

View File

@@ -0,0 +1,18 @@
local adapter_api = ...
return function(proxy)
return
{
["aspectRatio"] = {proxy.getAspectRatio()},
["keyboards"] = {read=function()
local ks = {}
for _,ka in ipairs(proxy.getKeyboards()) do
table.insert(ks, ka)
end
return table.concat(ks, "\n")
end},
["on"] = adapter_api.create_toggle(proxy.isOn, proxy.turnOn, proxy.turnOff), -- turnOn and turnOff
["precise"] = adapter_api.create_toggle(proxy.isPrecise, proxy.setPrecise),
["touchModeInverted"] = adapter_api.create_toggle(proxy.isTouchModeInverted, proxy.setTouchModeInverted),
}
end

View File

@@ -0,0 +1,101 @@
local fs = require("filesystem")
local lib = {}
local rules_path = "/etc/udev/rules.d/"
local auto_rules = "autogenerated.lua"
local function fs_key(dir, filename)
local long_name = dir .. '/' .. filename
local segments = fs.segments(long_name)
local result = '/' .. table.concat(segments, '/')
return result
end
function lib.loadRules(root_dir)
checkArg(1, root_dir, "string", "nil")
root_dir = (root_dir or rules_path)
lib.rules = {}
lib.rules[fs_key(root_dir, auto_rules)] = {}
for file in fs.list(root_dir) do
if file:match("%.lua$") then
local path = fs_key(root_dir, file)
local file_handle = io.open(path)
if file_handle then
local load_rule = load("return {" .. file_handle:read("*a") .. "}")
file_handle:close()
if load_rule then
local ok, rule = pcall(load_rule)
if ok and type(rule) == "table" then
local irule = {}
lib.rules[path] = irule
for _,v in ipairs(rule) do
if type(v) == "table" then
table.insert(irule, v)
end
-- else invalid rule
end
end
end
end
end
end
end
function lib.saveRule(rule_set, path)
checkArg(1, rule_set, "table")
checkArg(2, path, "string")
local file = io.open(path, "w")
if not file then return end -- fs may be read only, totally fine, this just won't persist
for index, irule in ipairs(rule_set) do
file:write(require("serialization").serialize(irule), ",\n")
end
file:close()
end
function lib.saveRules(rules)
for path, rule_set in pairs(rules) do
lib.saveRule(rule_set, path)
end
end
local function getIRule(proxy)
checkArg(1, proxy, "table")
for path,rule_set in pairs(lib.rules) do
for index, irule in ipairs(rule_set) do
if irule.address == proxy.address then
return irule, index, rule_set, path
end
end
end
end
function lib.getDeviceLabel(proxy)
local irule = getIRule(proxy)
if irule and irule.label then
return irule.label
elseif proxy.getLabel then
return proxy.getLabel()
end
end
function lib.setDeviceLabel(proxy, label)
local irule, index, rule_set, path = getIRule(proxy)
if not irule then
-- if the device supports labels, use it instead
if proxy.setLabel then
return proxy.setLabel(label)
end
path = fs_key(rules_path, auto_rules)
rule_set = lib.rules[path]
index = #rule_set + 1
irule = {address=proxy.address}
table.insert(rule_set, irule)
end
irule.label = label
lib.saveRule(rule_set, path)
end
return lib

View File

@@ -0,0 +1,238 @@
local buffer = require("buffer")
local unicode = require("unicode")
function buffer:getTimeout()
return self.readTimeout
end
function buffer:setTimeout(value)
self.readTimeout = tonumber(value)
end
function buffer:seek(whence, offset)
whence = tostring(whence or "cur")
assert(whence == "set" or whence == "cur" or whence == "end",
"bad argument #1 (set, cur or end expected, got " .. whence .. ")")
offset = offset or 0
checkArg(2, offset, "number")
assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")
if self.mode.w or self.mode.a then
self:flush()
elseif whence == "cur" then
offset = offset - #self.bufferRead
end
local result, reason = self.stream:seek(whence, offset)
if result then
self.bufferRead = ""
return result
else
return nil, reason
end
end
function buffer:buffered_write(arg)
local result, reason
if self.bufferMode == "full" then
if self.bufferSize - #self.bufferWrite < #arg then
result, reason = self:flush()
if not result then
return nil, reason
end
end
if #arg > self.bufferSize then
result, reason = self.stream:write(arg)
else
self.bufferWrite = self.bufferWrite .. arg
result = self
end
else--if self.bufferMode == "line" then
local l
repeat
local idx = arg:find("\n", (l or 0) + 1, true)
if idx then
l = idx
end
until not idx
if l or #arg > self.bufferSize then
result, reason = self:flush()
if not result then
return nil, reason
end
end
if l then
result, reason = self.stream:write(arg:sub(1, l))
if not result then
return nil, reason
end
arg = arg:sub(l + 1)
end
if #arg > self.bufferSize then
result, reason = self.stream:write(arg)
else
self.bufferWrite = self.bufferWrite .. arg
result = self
end
end
return result, reason
end
----------------------------------------------------------------------------------------------
function buffer:readNumber(readChunk)
local len, sub
if self.mode.b then
len = rawlen
sub = string.sub
else
len = unicode.len
sub = unicode.sub
end
local number_text = ""
local white_done
local function peek()
if len(self.bufferRead) == 0 then
local result, reason = readChunk(self)
if not result then
return result, reason
end
end
return sub(self.bufferRead, 1, 1)
end
local function pop()
local n = sub(self.bufferRead, 1, 1)
self.bufferRead = sub(self.bufferRead, 2)
return n
end
while true do
local peeked = peek()
if not peeked then
break
end
if peeked:match("[%s]") then
if white_done then
break
end
pop()
else
white_done = true
if not tonumber(number_text .. peeked .. "0") then
break
end
number_text = number_text .. pop() -- add pop to number_text
end
end
return tonumber(number_text)
end
function buffer:readBytesOrChars(readChunk, n)
n = math.max(n, 0)
local len, sub
if self.mode.b then
len = rawlen
sub = string.sub
else
len = unicode.len
sub = unicode.sub
end
local data = ""
while true do
local current_data_len = len(data)
local needed = n - current_data_len
if needed < 1 then
break
end
-- if the buffer is empty OR there is only 1 char left, read next chunk
-- this is to protect that last byte from bad unicode
if #self.bufferRead == 0 then
local result, reason = readChunk(self)
if not result then
if reason then
return result, reason
else -- eof
return current_data_len > 0 and data or nil
end
end
end
local splice = self.bufferRead
if len(self.bufferRead) > needed then
splice = sub(self.bufferRead, 1, needed)
if len(splice) ~= needed then
-- this can happen if the stream does not represent valid utf8 sequences
-- we could search the string for the bad sequence but regardless, we're going to just return the raw data
splice = self.bufferRead -- yes this is more than the user is asking for, but this is better than corrupting the stream
end
-- else -- we will read more chunks
end
data = data .. splice
self.bufferRead = string.sub(self.bufferRead, #splice + 1)
end
return data
end
function buffer:readAll(readChunk)
repeat
local result, reason = readChunk(self)
if not result and reason then
return result, reason
end
until not result -- eof
local result = self.bufferRead
self.bufferRead = ""
return result
end
function buffer:formatted_read(readChunk, ...)
self.timeout = require("computer").uptime() + self.readTimeout
local function read(n, format)
if type(format) == "number" then
return self:readBytesOrChars(readChunk, format)
else
local first_char_index = 1
if type(format) ~= "string" then
error("bad argument #" .. n .. " (invalid option)")
elseif unicode.sub(format, 1, 1) == "*" then
first_char_index = 2
end
format = unicode.sub(format, first_char_index, first_char_index)
if format == "n" then
return self:readNumber(readChunk)
elseif format == "l" then
return self:readLine(true, self.timeout)
elseif format == "L" then
return self:readLine(false, self.timeout)
elseif format == "a" then
return self:readAll(readChunk)
else
error("bad argument #" .. n .. " (invalid format)")
end
end
end
local results = {}
local formats = table.pack(...)
for i = 1, formats.n do
local result, reason = read(i, formats[i])
if result then
results[i] = result
elseif reason then
return nil, reason
end
end
return table.unpack(results, 1, formats.n)
end
function buffer:size()
local len = self.mode.b and rawlen or unicode.len
local size = len(self.bufferRead)
if self.stream.size then
size = size + self.stream:size()
end
return size
end

View File

@@ -0,0 +1,123 @@
local core_cursor = require("core/cursor")
local unicode = require("unicode")
local kb = require("keyboard")
local tty = require("tty")
core_cursor.horizontal = {}
function core_cursor.touch(cursor, gx, gy)
if cursor.len > 0 then
local win = tty.window
gx, gy = gx - win.dx, gy - win.dy
while true do
local x, y, d = win.x, win.y, win.width
local dx = ((gy*d+gx)-(y*d+x))
if dx == 1 then
dx = unicode.wlen(unicode.sub(cursor.data, cursor.index + 1, cursor.index + 1)) == 2 and 0 or dx
end
if dx == 0 then
break
end
cursor:move(dx > 0 and 1 or -1)
if x == win.x and y == win.y then
break
end
end
end
end
function core_cursor.tab(cursor)
local hints = cursor.hint
if not hints then return end
if not cursor.cache then
cursor.cache =
type(hints) == "table" and hints or
hints(cursor.data, cursor.index + 1) or
{}
cursor.cache.i = -1
end
local cache = cursor.cache
if #cache == 1 and cache.i == 0 then
-- there was only one solution, and the user is asking for the next
cursor.cache = hints(cache[1], cursor.index + 1)
if not cursor.cache then return end
cursor.cache.i = -1
cache = cursor.cache
end
local change = kb.isShiftDown() and -1 or 1
cache.i = (cache.i + change) % math.max(#cache, 1)
local next = cache[cache.i + 1]
if next then
local tail = unicode.len(cursor.data) - cursor.index
cursor:move(cursor.len)
cursor:update(-cursor.len)
cursor:update(next, -tail)
end
end
function core_cursor.horizontal:scroll(num, final_index)
self:move(self.vindex - self.index) -- go to left edge
-- shift (v)index by num
self.vindex = self.vindex + num
self.index = self.index + num
self:echo("\0277".. -- remember the location
unicode.sub(self.data, self.index + 1).. -- write part after
"\27[K\0278") -- clear tail and restore left edge
self:move(final_index - self.index) -- move to final_index location
end
function core_cursor.horizontal:echo(arg, num)
local w = tty.window
w.nowrap = self.nowrap
if arg == "" then -- special scroll request
local width = w.width
if w.x >= width then
-- the width that matters depends on the following char width
width = width - math.max(unicode.wlen(unicode.sub(self.data, self.index + 1, self.index + 1)) - 1, 0)
if w.x > width then
local s1 = unicode.sub(self.data, self.vindex + 1, self.index)
self:scroll(unicode.len(unicode.wtrunc(s1, w.x - width + 1)), self.index)
end
end
-- scroll is safe now, return as normal below
elseif arg == kb.keys.left then
if self.index < self.vindex then
local s2 = unicode.sub(self.data, self.index + 1)
w.x = w.x - num + unicode.wlen(unicode.sub(s2, 1, self.vindex - self.index))
local current_x = w.x
self:echo(s2)
w.x = current_x
self.vindex = self.index
return true
end
elseif arg == kb.keys.right then
w.x = w.x + num
return self:echo("") -- scroll
end
return core_cursor.vertical.echo(self, arg, num)
end
function core_cursor.horizontal:update(arg, back)
if back then
-- if we're just going to render arg and move back, and we're not wrapping, just render arg
-- back may be more or less from current x
self:update(arg, false)
local x = tty.window.x
self:echo(arg) -- nowrap echo
tty.window.x = x
self:move(self.len - self.index + back) -- back is negative from end
return true
elseif not arg then -- reset
self.nowrap = true
self.clear = "\27[K"
self.vindex = 0 -- visual/virtual index
end
return core_cursor.vertical.update(self, arg, back)
end
setmetatable(core_cursor.horizontal, { __index = core_cursor.vertical })

View File

@@ -0,0 +1,75 @@
local event = require("event")
local function createMultipleFilter(...)
local filter = table.pack(...)
if filter.n == 0 then
return nil
end
return function(...)
local signal = table.pack(...)
if type(signal[1]) ~= "string" then
return false
end
for i = 1, filter.n do
if filter[i] ~= nil and signal[1]:match(filter[i]) then
return true
end
end
return false
end
end
function event.pullMultiple(...)
local seconds
local args
if type(...) == "number" then
seconds = ...
args = table.pack(select(2,...))
for i=1,args.n do
checkArg(i+1, args[i], "string", "nil")
end
else
args = table.pack(...)
for i=1,args.n do
checkArg(i, args[i], "string", "nil")
end
end
return event.pullFiltered(seconds, createMultipleFilter(table.unpack(args, 1, args.n)))
end
function event.cancel(timerId)
checkArg(1, timerId, "number")
if event.handlers[timerId] then
event.handlers[timerId] = nil
return true
end
return false
end
function event.ignore(name, callback)
checkArg(1, name, "string")
checkArg(2, callback, "function")
for id, handler in pairs(event.handlers) do
if handler.key == name and handler.callback == callback then
event.handlers[id] = nil
return true
end
end
return false
end
function event.onError(message)
local log = io.open("/tmp/event.log", "a")
if log then
pcall(log.write, log, tostring(message), "\n")
log:close()
end
end
function event.timer(interval, callback, times)
checkArg(1, interval, "number")
checkArg(2, callback, "function")
checkArg(3, times, "number", "nil")
return event.register(false, callback, interval, times)
end

View File

@@ -0,0 +1,363 @@
local filesystem = require("filesystem")
local component = require("component")
local shell = require("shell")
function filesystem.makeDirectory(path)
if filesystem.exists(path) then
return nil, "file or directory with that name already exists"
end
local node, rest = filesystem.findNode(path)
if node.fs and rest then
local success, reason = node.fs.makeDirectory(rest)
if not success and not reason and node.fs.isReadOnly() then
reason = "filesystem is readonly"
end
return success, reason
end
if node.fs then
return nil, "virtual directory with that name already exists"
end
return nil, "cannot create a directory in a virtual directory"
end
function filesystem.lastModified(path)
local node, rest, vnode, vrest = filesystem.findNode(path, false, true)
if not node or not vnode.fs and not vrest then
return 0 -- virtual directory
end
if node.fs and rest then
return node.fs.lastModified(rest)
end
return 0 -- no such file or directory
end
function filesystem.mounts()
local tmp = {}
for path,node in pairs(filesystem.fstab) do
table.insert(tmp, {node.fs,path})
end
return function()
local next = table.remove(tmp)
if next then return table.unpack(next) end
end
end
function filesystem.link(target, linkpath)
checkArg(1, target, "string")
checkArg(2, linkpath, "string")
if filesystem.exists(linkpath) then
return nil, "file already exists"
end
local linkpath_parent = filesystem.path(linkpath)
if not filesystem.exists(linkpath_parent) then
return nil, "no such directory"
end
local linkpath_real, reason = filesystem.realPath(linkpath_parent)
if not linkpath_real then
return nil, reason
end
if not filesystem.isDirectory(linkpath_real) then
return nil, "not a directory"
end
local _, _, vnode, _ = filesystem.findNode(linkpath_real, true)
vnode.links[filesystem.name(linkpath)] = target
return true
end
function filesystem.umount(fsOrPath)
checkArg(1, fsOrPath, "string", "table")
local real
local fs
local addr
if type(fsOrPath) == "string" then
real = filesystem.realPath(fsOrPath)
addr = fsOrPath
else -- table
fs = fsOrPath
end
local paths = {}
for path,node in pairs(filesystem.fstab) do
if real == path or addr == node.fs.address or fs == node.fs then
table.insert(paths, path)
end
end
for _,path in ipairs(paths) do
local node = filesystem.fstab[path]
filesystem.fstab[path] = nil
node.fs = nil
node.parent.children[node.name] = nil
end
return #paths > 0
end
function filesystem.size(path)
local node, rest, vnode, vrest = filesystem.findNode(path, false, true)
if not node or not vnode.fs and (not vrest or vnode.links[vrest]) then
return 0 -- virtual directory or symlink
end
if node.fs and rest then
return node.fs.size(rest)
end
return 0 -- no such file or directory
end
function filesystem.isLink(path)
local name = filesystem.name(path)
local node, rest, vnode, vrest = filesystem.findNode(filesystem.path(path), false, true)
if not node then return nil, rest end
local target = vnode.links[name]
-- having vrest here indicates we are not at the
-- owning vnode due to a mount point above this point
-- but we can have a target when there is a link at
-- the mount point root, with the same name
if not vrest and target ~= nil then
return true, target
end
return false
end
function filesystem.copy(fromPath, toPath)
local data = false
local input, reason = filesystem.open(fromPath, "rb")
if input then
local output = filesystem.open(toPath, "wb")
if output then
repeat
data, reason = input:read(1024)
if not data then break end
data, reason = output:write(data)
if not data then data, reason = false, "failed to write" end
until not data
output:close()
end
input:close()
end
return data == nil, reason
end
local function readonly_wrap(proxy)
checkArg(1, proxy, "table")
if proxy.isReadOnly() then
return proxy
end
local function roerr() return nil, "filesystem is readonly" end
return setmetatable({
rename = roerr,
open = function(path, mode)
checkArg(1, path, "string")
checkArg(2, mode, "string")
if mode:match("[wa]") then
return roerr()
end
return proxy.open(path, mode)
end,
isReadOnly = function()
return true
end,
write = roerr,
setLabel = roerr,
makeDirectory = roerr,
remove = roerr,
}, {__index=proxy})
end
local function bind_proxy(path)
local real, reason = filesystem.realPath(path)
if not real then
return nil, reason
end
if not filesystem.isDirectory(real) then
return nil, "must bind to a directory"
end
local real_fs, real_fs_path = filesystem.get(real)
if real == real_fs_path then
return real_fs
end
-- turn /tmp/foo into foo
local rest = real:sub(#real_fs_path + 1)
local function wrap_relative(fp)
return function(mpath, ...)
return fp(filesystem.concat(rest, mpath), ...)
end
end
local bind = {
type = "filesystem_bind",
address = real,
isReadOnly = real_fs.isReadOnly,
list = wrap_relative(real_fs.list),
isDirectory = wrap_relative(real_fs.isDirectory),
size = wrap_relative(real_fs.size),
lastModified = wrap_relative(real_fs.lastModified),
exists = wrap_relative(real_fs.exists),
open = wrap_relative(real_fs.open),
remove = wrap_relative(real_fs.remove),
read = real_fs.read,
write = real_fs.write,
close = real_fs.close,
getLabel = function() return "" end,
setLabel = function() return nil, "cannot set the label of a bind point" end,
}
return bind
end
filesystem.internal = {}
function filesystem.internal.proxy(filter, options)
checkArg(1, filter, "string")
checkArg(2, options, "table", "nil")
options = options or {}
local address, proxy, reason
if options.bind then
proxy, reason = bind_proxy(filter)
else
-- no options: filter should be a label or partial address
for c in component.list("filesystem", true) do
if component.invoke(c, "getLabel") == filter then
address = c
break
end
if c:sub(1, filter:len()) == filter then
address = c
break
end
end
if not address then
return nil, "no such file system"
end
proxy, reason = component.proxy(address)
end
if not proxy then
return proxy, reason
end
if options.readonly then
proxy = readonly_wrap(proxy)
end
return proxy
end
function filesystem.remove(path)
local function removeVirtual()
local _, _, vnode, vrest = filesystem.findNode(filesystem.path(path), false, true)
-- vrest represents the remaining path beyond vnode
-- vrest is nil if vnode reaches the full path
-- thus, if vrest is NOT NIL, then we SHOULD NOT remove children nor links
if not vrest then
local name = filesystem.name(path)
if vnode.children[name] or vnode.links[name] then
vnode.children[name] = nil
vnode.links[name] = nil
while vnode and vnode.parent and not vnode.fs and not next(vnode.children) and not next(vnode.links) do
vnode.parent.children[vnode.name] = nil
vnode = vnode.parent
end
return true
end
end
-- return false even if vrest is nil because this means it was a expected
-- to be a real file
return false
end
local function removePhysical()
local node, rest = filesystem.findNode(path)
if node.fs and rest then
return node.fs.remove(rest)
end
return false
end
local success = removeVirtual()
success = removePhysical() or success -- Always run.
if success then return true
else return nil, "no such file or directory"
end
end
function filesystem.rename(oldPath, newPath)
if filesystem.isLink(oldPath) then
local _, _, vnode, _ = filesystem.findNode(filesystem.path(oldPath))
local target = vnode.links[filesystem.name(oldPath)]
local result, reason = filesystem.link(target, newPath)
if result then
filesystem.remove(oldPath)
end
return result, reason
else
local oldNode, oldRest = filesystem.findNode(oldPath)
local newNode, newRest = filesystem.findNode(newPath)
if oldNode.fs and oldRest and newNode.fs and newRest then
if oldNode.fs.address == newNode.fs.address then
return oldNode.fs.rename(oldRest, newRest)
else
local result, reason = filesystem.copy(oldPath, newPath)
if result then
return filesystem.remove(oldPath)
else
return nil, reason
end
end
end
return nil, "trying to read from or write to virtual directory"
end
end
local isAutorunEnabled = nil
local function saveConfig()
local root = filesystem.get("/")
if root and not root.isReadOnly() then
local f = filesystem.open("/etc/filesystem.cfg", "w")
if f then
f:write("autorun="..tostring(isAutorunEnabled))
f:close()
end
end
end
function filesystem.isAutorunEnabled()
if isAutorunEnabled == nil then
local env = {}
local config = loadfile("/etc/filesystem.cfg", nil, env)
if config then
pcall(config)
isAutorunEnabled = not not env.autorun
else
isAutorunEnabled = true
end
saveConfig()
end
return isAutorunEnabled
end
function filesystem.setAutorunEnabled(value)
checkArg(1, value, "boolean")
isAutorunEnabled = value
saveConfig()
end
-- luacheck: globals os
os.remove = filesystem.remove
os.rename = filesystem.rename
os.execute = function(command)
if not command then
return type(shell) == "table"
end
return shell.execute(command)
end
function os.exit(code)
error({reason="terminated", code=code}, 0)
end
function os.tmpname()
local path = os.getenv("TMPDIR") or "/tmp"
if filesystem.exists(path) then
for _ = 1, 10 do
local name = filesystem.concat(path, tostring(math.random(1, 0x7FFFFFFF)))
if not filesystem.exists(name) then
return name
end
end
end
end

View File

@@ -0,0 +1,143 @@
local keyboard = require("keyboard")
keyboard.keys["1"] = 0x02
keyboard.keys["2"] = 0x03
keyboard.keys["3"] = 0x04
keyboard.keys["4"] = 0x05
keyboard.keys["5"] = 0x06
keyboard.keys["6"] = 0x07
keyboard.keys["7"] = 0x08
keyboard.keys["8"] = 0x09
keyboard.keys["9"] = 0x0A
keyboard.keys["0"] = 0x0B
keyboard.keys.a = 0x1E
keyboard.keys.b = 0x30
keyboard.keys.c = 0x2E
keyboard.keys.d = 0x20
keyboard.keys.e = 0x12
keyboard.keys.f = 0x21
keyboard.keys.g = 0x22
keyboard.keys.h = 0x23
keyboard.keys.i = 0x17
keyboard.keys.j = 0x24
keyboard.keys.k = 0x25
keyboard.keys.l = 0x26
keyboard.keys.m = 0x32
keyboard.keys.n = 0x31
keyboard.keys.o = 0x18
keyboard.keys.p = 0x19
keyboard.keys.q = 0x10
keyboard.keys.r = 0x13
keyboard.keys.s = 0x1F
keyboard.keys.t = 0x14
keyboard.keys.u = 0x16
keyboard.keys.v = 0x2F
keyboard.keys.w = 0x11
keyboard.keys.x = 0x2D
keyboard.keys.y = 0x15
keyboard.keys.z = 0x2C
keyboard.keys.apostrophe = 0x28
keyboard.keys.at = 0x91
keyboard.keys.back = 0x0E -- backspace
keyboard.keys.backslash = 0x2B
keyboard.keys.capital = 0x3A -- capslock
keyboard.keys.colon = 0x92
keyboard.keys.comma = 0x33
keyboard.keys.enter = 0x1C
keyboard.keys.equals = 0x0D
keyboard.keys.grave = 0x29 -- accent grave
keyboard.keys.lbracket = 0x1A
keyboard.keys.lcontrol = 0x1D
keyboard.keys.lmenu = 0x38 -- left Alt
keyboard.keys.lshift = 0x2A
keyboard.keys.minus = 0x0C
keyboard.keys.numlock = 0x45
keyboard.keys.pause = 0xC5
keyboard.keys.period = 0x34
keyboard.keys.rbracket = 0x1B
keyboard.keys.rcontrol = 0x9D
keyboard.keys.rmenu = 0xB8 -- right Alt
keyboard.keys.rshift = 0x36
keyboard.keys.scroll = 0x46 -- Scroll Lock
keyboard.keys.semicolon = 0x27
keyboard.keys.slash = 0x35 -- / on main keyboard
keyboard.keys.space = 0x39
keyboard.keys.stop = 0x95
keyboard.keys.tab = 0x0F
keyboard.keys.underline = 0x93
-- Keypad (and numpad with numlock off)
keyboard.keys.up = 0xC8
keyboard.keys.down = 0xD0
keyboard.keys.left = 0xCB
keyboard.keys.right = 0xCD
keyboard.keys.home = 0xC7
keyboard.keys["end"] = 0xCF
keyboard.keys.pageUp = 0xC9
keyboard.keys.pageDown = 0xD1
keyboard.keys.insert = 0xD2
keyboard.keys.delete = 0xD3
-- Function keys
keyboard.keys.f1 = 0x3B
keyboard.keys.f2 = 0x3C
keyboard.keys.f3 = 0x3D
keyboard.keys.f4 = 0x3E
keyboard.keys.f5 = 0x3F
keyboard.keys.f6 = 0x40
keyboard.keys.f7 = 0x41
keyboard.keys.f8 = 0x42
keyboard.keys.f9 = 0x43
keyboard.keys.f10 = 0x44
keyboard.keys.f11 = 0x57
keyboard.keys.f12 = 0x58
keyboard.keys.f13 = 0x64
keyboard.keys.f14 = 0x65
keyboard.keys.f15 = 0x66
keyboard.keys.f16 = 0x67
keyboard.keys.f17 = 0x68
keyboard.keys.f18 = 0x69
keyboard.keys.f19 = 0x71
-- Japanese keyboards
keyboard.keys.kana = 0x70
keyboard.keys.kanji = 0x94
keyboard.keys.convert = 0x79
keyboard.keys.noconvert = 0x7B
keyboard.keys.yen = 0x7D
keyboard.keys.circumflex = 0x90
keyboard.keys.ax = 0x96
-- Numpad
keyboard.keys.numpad0 = 0x52
keyboard.keys.numpad1 = 0x4F
keyboard.keys.numpad2 = 0x50
keyboard.keys.numpad3 = 0x51
keyboard.keys.numpad4 = 0x4B
keyboard.keys.numpad5 = 0x4C
keyboard.keys.numpad6 = 0x4D
keyboard.keys.numpad7 = 0x47
keyboard.keys.numpad8 = 0x48
keyboard.keys.numpad9 = 0x49
keyboard.keys.numpadmul = 0x37
keyboard.keys.numpaddiv = 0xB5
keyboard.keys.numpadsub = 0x4A
keyboard.keys.numpadadd = 0x4E
keyboard.keys.numpaddecimal = 0x53
keyboard.keys.numpadcomma = 0xB3
keyboard.keys.numpadenter = 0x9C
keyboard.keys.numpadequals = 0x8D
-- Create inverse mapping for name lookup.
setmetatable(keyboard.keys,
{
__index = function(tbl, k)
if type(k) ~= "number" then return end
for name,value in pairs(tbl) do
if value == k then
return name
end
end
end
})

View File

@@ -0,0 +1,366 @@
local fs = require("filesystem")
local shell = require("shell")
local tty = require("tty")
local unicode = require("unicode")
local tx = require("transforms")
local text = require("text")
local dirsArg, ops = shell.parse(...)
if ops.help then
print([[Usage: ls [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 and/or -s, print human readable sizes
--si likewise, but use powers of 1000 not 1024
-l use a long listing format
-r, --reverse reverse order while sorting
-R, --recursive list subdirectories recursively
-S sort by file size
-t sort by modification time, newest first
-X sort alphabetically by entry extension
-1 list one file per line
-p append / indicator to directories
-M display Microsoft-style file and directory
count after listing
--no-color Do not colorize the output (default colorized)
--help display this help and exit
For more info run: man ls]])
return 0
end
if #dirsArg == 0 then
table.insert(dirsArg, ".")
end
local ec = 0
local fOut = tty.isAvailable() and io.output().tty
local function perr(msg) io.stderr:write(msg,"\n") ec = 2 end
local set_color = function() end
local function colorize() return end
if fOut and not ops["no-color"] then
local LSC = tx.foreach(text.split(os.getenv("LS_COLORS") or "", {":"}, true), function(e)
local parts = text.split(e, {"="}, true)
return parts[2], parts[1]
end)
colorize = function(info)
return
info.isLink and LSC.ln or
info.isDir and LSC.di or
LSC['*'..info.ext] or
LSC.fi
end
set_color=function(c)
io.write(string.char(0x1b), "[", c or "", "m")
end
end
local msft={reports=0,proxies={}}
function msft.report(files, dirs, used, proxy)
local free = proxy.spaceTotal() - proxy.spaceUsed()
set_color()
local pattern = "%5i File(s) %s bytes\n%5i Dir(s) %11s bytes free\n"
io.write(string.format(pattern, files, tostring(used), dirs, tostring(free)))
end
function msft.tail(names)
local fsproxy = fs.get(names.path)
if not fsproxy then
return
end
local totalSize, totalFiles, totalDirs = 0, 0, 0
for i=1,#names do
local info = names[i]
if info.isDir then
totalDirs = totalDirs + 1
else
totalFiles = totalFiles + 1
end
totalSize = totalSize + info.size
end
msft.report(totalFiles, totalDirs, totalSize, fsproxy)
local ps = msft.proxies
ps[fsproxy] = ps[fsproxy] or {files=0,dirs=0,used=0}
local p = ps[fsproxy]
p.files = p.files + totalFiles
p.dirs = p.dirs + totalDirs
p.used = p.used + totalSize
msft.reports = msft.reports + 1
end
function msft.final()
if msft.reports < 2 then return end
local groups = {}
for proxy,report in pairs(msft.proxies) do
table.insert(groups, {proxy=proxy,report=report})
end
set_color()
print("Total Files Listed:")
for _,pair in ipairs(groups) do
local proxy, report = pair.proxy, pair.report
if #groups>1 then
print("As pertaining to: "..proxy.address)
end
msft.report(report.files, report.dirs, report.used, proxy)
end
end
if not ops.M then
msft.tail=function()end
msft.final=function()end
end
local function nod(n)
return n and (tostring(n):gsub("(%.[0-9]+)0+$","%1")) or "0"
end
local function formatSize(size)
if not ops.h and not ops['human-readable'] and not ops.si then
return tostring(size)
end
local sizes = {"", "K", "M", "G"}
local unit = 1
local power = ops.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)
txt = tostring(txt)
return #txt >= 2 and txt or "0"..txt
end
local function formatDate(epochms)
--local day_names={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
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 ops["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 filter(names)
if ops.a then
return names
end
local set = { path = names.path }
for _, info in ipairs(names) do
if fs.name(info.name):sub(1, 1) ~= "." then
set[#set + 1] = info
end
end
return set
end
local function sort(names)
local once = false
local function ni(v)
for i=1,#names do
local info = names[i]
if info.key == v.key then
return i
end
end
end
local function sorter(key)
once = true
table.sort(names, function(a, b)
local ast = names[ni(a)]
local bst = names[ni(b)]
return ast[key] > bst[key]
end)
end
if ops.t then sorter("time") end
if ops.X then sorter("ext") end
if ops.S then sorter("size") end
local rev = ops.r or ops.reverse
if not once then sorter("sort_name") rev=not rev end
if rev then
for i=1,#names/2 do
names[i], names[#names - i + 1] = names[#names - i + 1], names[i]
end
end
return names
end
local function dig(names, dirs, dir)
if ops.R then
local di = 1
for i=1,#names do
local info = names[i]
if info.isDir then
local path = dir..(dir:sub(-1) == "/" and "" or "/")
table.insert(dirs, di, path..info.name)
di = di + 1
end
end
end
return names
end
local first_display = true
local function display(names)
local mt={}
local lines = setmetatable({}, mt)
if ops.l then
lines.n = #names
local max_size_width = 1
local max_date_width = 0
for i=1,lines.n do
local info = names[i]
max_size_width = math.max(max_size_width, formatSize(info.size):len())
max_date_width = math.max(max_date_width, formatDate(info.time):len())
end
mt.__index = function(_, index)
local info = names[index]
local file_type = info.isLink and 'l' or info.isDir and 'd' or 'f'
local link_target = info.isLink and string.format(" -> %s", info.link:gsub("/+$", "") .. (info.isDir and "/" or "")) or ""
local write_mode = info.fs.isReadOnly() and '-' or 'w'
local size = formatSize(info.size)
local modDate = formatDate(info.time)
local format = "%s-r%s %"..tostring(max_size_width).."s %"..tostring(max_date_width).."s"
local meta = string.format(format, file_type, write_mode, size, modDate)
local item = info.name..link_target
return {{name = meta}, {color = colorize(info), name = item}}
end
elseif ops["1"] or not fOut then
lines.n = #names
mt.__index = function(_, index)
local info = names[index]
return {{color = colorize(info), name = info.name}}
end
else -- columns
local num_columns, items_per_column, width = 0, 0, tty.getViewport() - 1
local function real(x, y)
local index = y + ((x-1) * items_per_column)
return index <= #names and index or nil
end
local function max_name(column_index)
local max = 0 -- return the width of the max element in column_index
for r=1,items_per_column do
local ri = real(column_index, r)
if not ri then break end
local info = names[ri]
max = math.max(max, unicode.wlen(info.name))
end
return max
end
local function measure()
local total = 0
for column_index=1,num_columns do
total = total + max_name(column_index) + (column_index > 1 and 2 or 0)
end
return total
end
while items_per_column<#names do
items_per_column = items_per_column + 1
num_columns = math.ceil(#names/items_per_column)
if measure() < width then
break
end
end
lines.n = items_per_column
mt.__index=function(_, line_index)
return setmetatable({},{
__len=function()return num_columns end,
__index=function(_, column_index)
local ri = real(column_index, line_index)
if not ri then return end
local info = names[ri]
local name = info.name
return {color = colorize(info), name = name .. string.rep(' ', max_name(column_index) - unicode.wlen(name) + (column_index < num_columns and 2 or 0))}
end,
})
end
end
for line_index=1,lines.n do
local line = lines[line_index]
for element_index=1,#line do
local e = line[element_index]
if not e then break end
first_display = false
set_color(e.color)
io.write(e.name)
end
set_color()
print()
end
msft.tail(names)
end
local header = function() end
if #dirsArg > 1 or ops.R then
header = function(path)
if not first_display then print() end
set_color()
io.write(path,":\n")
end
end
local function stat(path, name)
local info = {}
info.key = name
info.path = name:sub(1, 1) == "/" and "" or path
info.full_path = fs.concat(info.path, name)
info.isDir = fs.isDirectory(info.full_path)
info.name = name:gsub("/+$", "") .. (ops.p and info.isDir and "/" or "")
info.sort_name = info.name:gsub("^%.","")
info.isLink, info.link = fs.isLink(info.full_path)
info.size = info.isLink and 0 or fs.size(info.full_path)
info.time = fs.lastModified(info.full_path)/1000
info.fs = fs.get(info.full_path)
info.ext = info.name:match("(%.[^.]+)$") or ""
return info
end
local function displayDirList(dirs)
while #dirs > 0 do
local dir = table.remove(dirs, 1)
header(dir)
local path = shell.resolve(dir)
local list, reason = fs.list(path)
if not list then
perr(reason)
else
local names = { path = path }
for name in list do
names[#names + 1] = stat(path, name)
end
display(dig(sort(filter(names)), dirs, dir))
end
end
end
local dir_set, file_set = {}, {path=shell.getWorkingDirectory()}
for _,dir in ipairs(dirsArg) do
local path = shell.resolve(dir)
local real, why = fs.realPath(path)
local access_msg = "cannot access " .. tostring(path) .. ": "
if not real then
perr(access_msg .. why)
elseif not fs.exists(path) then
perr(access_msg .. "No such file or directory")
elseif fs.isDirectory(path) then
table.insert(dir_set, dir)
else -- file or link
table.insert(file_set, stat(dir, dir))
end
end
io.output():setvbuf("line")
local ok, msg = pcall(function()
if #file_set > 0 then display(sort(file_set)) end
displayDirList(dir_set)
msft.final()
end)
io.output():flush()
io.output():setvbuf("no")
assert(ok, msg)
return ec

View File

@@ -0,0 +1,604 @@
local fs = require("filesystem")
local process = require("process")
local shell = require("shell")
local text = require("text")
local tx = require("transforms")
local unicode = require("unicode")
local sh = require("sh")
local isWordOf = sh.internal.isWordOf
-------------------------------------------------------------------------------
function sh.internal.command_passed(ec)
return sh.internal.command_result_as_code(ec) == 0
end
-- takes ewords and searches for redirections (may not have any)
-- removes the redirects and their arguments from the ewords
-- returns a redirection table that is used during process load
-- returns false if no redirections are defined
-- to open the redirect handles, see openCommandRedirects
function sh.internal.buildCommandRedirects(words)
local redirects = {}
local index = 1 -- we move index manually to allow removals from ewords
local from_io, to_io, mode
local syn_err_msg = "syntax error near unexpected token "
-- hasValidPiping has been modified, it does not verify redirects now
-- we could have bad redirects such as "echo hi > > foo"
-- we must validate the input here
while true do
local word = words[index]
if not word then break end
-- redirections are
-- 1. single part
-- 2. not quoted
local part = word[1]
local token = not word[2] and not part.qr and part.txt or ""
local _, _, from_io_txt, mode_txt, to_io_txt = token:find("(%d*)([<>]>?)%&?(.*)")
if mode_txt then
if mode then
return nil, syn_err_msg .. token
end
mode = assert(({["<"]="r",[">"]="w",[">>"]="a",})[mode_txt], "redirect failed to detect mode")
from_io = from_io_txt ~= "" and tonumber(from_io_txt) or mode == "r" and 0 or 1
to_io = to_io_txt ~= "" and tonumber(to_io_txt)
elseif mode then
token = sh.internal.evaluate({word})
if #token > 1 then
return nil, string.format("%s: ambiguous redirect", part.txt)
end
to_io = token[1]
else
index = index + 1
end
if mode then
table.remove(words, index)
end
if to_io then
table.insert(redirects, {from_io, to_io, mode})
mode = nil
to_io = nil
end
end
if mode then
return nil, syn_err_msg .. "newline"
end
return redirects
end
-- redirects as built by buildCommentRedirects
function sh.internal.openCommandRedirects(redirects)
local data = process.info().data
local ios = data.io
for _,rjob in ipairs(redirects) do
local from_io, to_io, mode = table.unpack(rjob)
if type(to_io) == "number" then -- io to io
-- from_io and to_io should be numbers
ios[from_io] = io.dup(ios[to_io])
else
-- to_io should be a string
local file, reason = io.open(shell.resolve(to_io), mode)
if not file then
io.stderr:write("could not open '" .. to_io .. "': " .. reason .. "\n")
os.exit(1)
end
ios[from_io] = file
end
end
end
-- takes an eword, returns a list of glob hits or {word} if no globs exist
function sh.internal.glob(eword)
-- words are parts, parts are txt and qr
-- eword.txt is a convenience field of the parts
-- turn word into regex based on globits
local globbers = {{"*",".*"},{"?","."}}
local glob_pattern = ""
local has_globits
for _,part in ipairs(eword) do
local next = part.txt
-- globs only exist outside quotes
if not part.qr then
local escaped = text.escapeMagic(next)
next = escaped
for _,glob_rule in ipairs(globbers) do
--remove duplicates
while true do
local prev = next
next = next:gsub(text.escapeMagic(glob_rule[1]):rep(2), glob_rule[1])
if prev == next then
break
end
end
--revert globit
next = next:gsub("%%%"..glob_rule[1], glob_rule[2])
end
-- if next is still equal to escaped that means no globits were detected in this word part
-- this word may not contain a globit, the prior search did a cheap search for globits
has_globits = has_globits or next ~= escaped
end
glob_pattern = glob_pattern .. next
end
if not has_globits then
return {eword.txt}
end
local segments = text.split(glob_pattern, {"/"}, true)
local hiddens = tx.foreach(segments,function(e)return e:match("^%%%.")==nil end)
local function is_visible(s,i)
return not hiddens[i] or s:match("^%.") == nil
end
local function magical(s)
for _,glob_rule in ipairs(globbers) do
if (" "..s):match("[^%%]"..text.escapeMagic(glob_rule[2])) then
return true
end
end
end
local is_abs = glob_pattern:sub(1, 1) == "/"
local root = is_abs and '' or shell.getWorkingDirectory():gsub("([^/])$","%1/")
local paths = {is_abs and "/" or ''}
local relative_separator = ''
for i,segment in ipairs(segments) do
local enclosed_pattern = string.format("^(%s)/?$", segment)
local next_paths = {}
for _,path in ipairs(paths) do
if fs.isDirectory(root..path) then
if magical(segment) then
for file in fs.list(root..path) do
if file:match(enclosed_pattern) and is_visible(file, i) then
table.insert(next_paths, path..relative_separator..file:gsub("/+$",''))
end
end
else -- not a globbing segment, just use it raw
local plain = text.removeEscapes(segment)
local fpath = root..path..relative_separator..plain
local hit = path..relative_separator..plain:gsub("/+$",'')
if fs.exists(fpath) then
table.insert(next_paths, hit)
end
end
end
end
paths = next_paths
if not next(paths) then
-- if no next_paths were hit here, the ENTIRE glob value is not a path
return {eword.txt}
end
relative_separator = "/"
end
return paths
end
function sh.getMatchingPrograms(baseName)
if not baseName or baseName == "" then return {} end
local result = {}
local result_keys = {} -- cache for fast value lookup
local function check(key)
if key:find(baseName, 1, true) == 1 and not result_keys[key] then
table.insert(result, key)
result_keys[key] = true
end
end
for alias in shell.aliases() do
check(alias)
end
for basePath in string.gmatch(os.getenv("PATH"), "[^:]+") do
for file in fs.list(shell.resolve(basePath)) do
check(file:gsub("%.lua$", ""))
end
end
return result
end
function sh.getMatchingFiles(partial_path)
-- name: text of the partial file name being expanded
local name = partial_path:gsub("^.*/", "")
-- here we remove the name text from the partialPrefix
local basePath = unicode.sub(partial_path, 1, -unicode.len(name) - 1)
local resolvedPath = shell.resolve(basePath)
local result, baseName = {}
-- note: we strip the trailing / to make it easier to navigate through
-- directories using tab completion (since entering the / will then serve
-- as the intention to go into the currently hinted one).
-- if we have a directory but no trailing slash there may be alternatives
-- on the same level, so don't look inside that directory... (cont.)
if fs.isDirectory(resolvedPath) and name == "" then
baseName = "^(.-)/?$"
else
baseName = "^(" .. text.escapeMagic(name) .. ".-)/?$"
end
for file in fs.list(resolvedPath) do
local match = file:match(baseName)
if match then
table.insert(result, basePath .. match:gsub("(%s)", "\\%1"))
end
end
-- (cont.) but if there's only one match and it's a directory, *then* we
-- do want to add the trailing slash here.
if #result == 1 and fs.isDirectory(shell.resolve(result[1])) then
result[1] = result[1] .. "/"
end
return result
end
function sh.internal.hintHandlerSplit(line)
-- I do not plan on having text tokenizer parse error on
-- trailiing \ in case of future support for multiple line
-- input. But, there are also no hints for it
if line:match("\\$") then return nil end
local splits = text.internal.tokenize(line,{show_escapes=true})
if not splits then -- parse error, e.g. unclosed quotes
return nil -- no split, no hints
end
local num_splits = #splits
-- search for last statement delimiters
local last_close = 0
for index = num_splits, 1, -1 do
local word = splits[index]
if isWordOf(word, {";","&&","||","|"}) then
last_close = index
break
end
end
-- if the very last word of the line is a delimiter
-- consider this a fresh new, empty line
-- this captures edge cases with empty input as well (i.e. no splits)
if last_close == num_splits then
return nil -- no hints on empty command
end
local last_word = splits[num_splits]
local normal = text.internal.normalize({last_word})[1]
-- if there is white space following the words
-- and we have at least one word following the last delimiter
-- then in all cases we are looking for ANY arg
if unicode.sub(line, -unicode.len(normal)) ~= normal then
return line, nil, ""
end
local prefix = unicode.sub(line, 1, -unicode.len(normal) - 1)
-- renormlizing the string will create 'printed' quality text
normal = text.internal.normalize(text.internal.tokenize(normal), true)[1]
-- one word: cmd
-- many: arg
if last_close == num_splits - 1 then
return prefix, normal, nil
else
return prefix, nil, normal
end
end
function sh.internal.hintHandlerImpl(full_line, cursor)
-- line: text preceding the cursor: we want to hint this part (expand it)
local line = unicode.sub(full_line, 1, cursor - 1)
-- suffix: text following the cursor (if any, else empty string) to append to the hints
local suffix = unicode.sub(full_line, cursor)
-- hintHandlerSplit helps make the hints work even after delimiters such as ;
-- it also catches parse errors such as unclosed quotes
-- prev: not needed for this hint
-- cmd: the command needing hint
-- arg: the argument needing hint
local prev, cmd, arg = sh.internal.hintHandlerSplit(line)
-- also, if there is no text to hint, there are no hints
if not prev then -- no hints e.g. unclosed quote, e.g. no text
return {}
end
local result
local searchInPath = cmd and not cmd:find("/")
if searchInPath then
result = sh.getMatchingPrograms(cmd)
else
-- special arg issue, after equal sign
if arg then
local equal_index = arg:find("=[^=]*$")
if equal_index then
prev = prev .. unicode.sub(arg, 1, equal_index)
arg = unicode.sub(arg, equal_index + 1)
end
end
result = sh.getMatchingFiles(cmd or arg)
end
-- in very special cases, the suffix should include a blank space to indicate to the user that the hint is discrete
local resultSuffix = suffix
if #result > 0 and unicode.sub(result[1], -1) ~= "/" and
not suffix:sub(1,1):find('%s') and
#result == 1 or searchInPath then
resultSuffix = " " .. resultSuffix
end
table.sort(result)
for i = 1, #result do
-- the hints define the whole line of text
result[i] = prev .. result[i] .. resultSuffix
end
return result
end
-- verifies that no pipes are doubled up nor at the start nor end of words
function sh.internal.hasValidPiping(words, pipes)
checkArg(1, words, "table")
checkArg(2, pipes, "table", "nil")
if #words == 0 then
return true
end
local semi_split = tx.first(text.syntax, {{";"}}) -- symbols before ; are redirects and follow slightly different rules, see buildCommandRedirects
pipes = pipes or tx.sub(text.syntax, semi_split + 1)
local state = "" -- cannot start on a pipe
for w=1,#words do
local word = words[w]
for p=1,#word do
local part = word[p]
if part.qr then
state = nil
elseif part.txt == "" then
state = nil -- not sure how this is possible (empty part without quotes?)
elseif #text.split(part.txt, pipes, true) == 0 then
local prev = state
state = part.txt
if prev then -- cannot have two pipes in a row
word = nil
break
end
else
state = nil
end
end
if not word then -- bad pipe
break
end
end
if state then
return false, "syntax error near unexpected token " .. state
else
return true
end
end
function sh.internal.boolean_executor(chains, predicator)
local function not_gate(result, reason)
return sh.internal.command_passed(result) and 1 or 0, reason
end
local last = true
local last_reason
local boolean_stage = 1
local negation_stage = 2
local command_stage = 0
local stage = negation_stage
local skip = false
for ci=1,#chains do
local next = chains[ci]
local single = #next == 1 and #next[1] == 1 and not next[1][1].qr and next[1][1].txt
if single == "||" then
if stage ~= command_stage or #chains == 0 then
return nil, "syntax error near unexpected token '"..single.."'"
end
if sh.internal.command_passed(last) then
skip = true
end
stage = boolean_stage
elseif single == "&&" then
if stage ~= command_stage or #chains == 0 then
return nil, "syntax error near unexpected token '"..single.."'"
end
if not sh.internal.command_passed(last) then
skip = true
end
stage = boolean_stage
elseif not skip then
local chomped = #next
local negate = sh.internal.remove_negation(next)
chomped = chomped ~= #next
if negate then
local prev = predicator
predicator = function(n,i)
local result, reason = not_gate(prev(n,i))
predicator = prev
return result, reason
end
end
if chomped then
stage = negation_stage
end
if #next > 0 then
last, last_reason = predicator(next,ci)
stage = command_stage
end
else
skip = false
stage = command_stage
end
end
if stage == negation_stage then
last = not_gate(last)
end
return last, last_reason
end
function sh.internal.splitStatements(words, semicolon)
checkArg(1, words, "table")
checkArg(2, semicolon, "string", "nil")
semicolon = semicolon or ";"
return tx.partition(words, function(g, i)
if isWordOf(g, {semicolon}) then
return i, i
end
end, true)
end
function sh.internal.splitChains(s,pc)
checkArg(1, s, "table")
checkArg(2, pc, "string", "nil")
pc = pc or "|"
return tx.partition(s, function(w)
-- each word has multiple parts due to quotes
if isWordOf(w, {pc}) then
return true
end
end, true) -- drop |s
end
function sh.internal.groupChains(s)
checkArg(1,s,"table")
return tx.partition(s,function(w)return isWordOf(w,{"&&","||"})end)
end
function sh.internal.remove_negation(chain)
if isWordOf(chain[1], {"!"}) then
table.remove(chain, 1)
return not sh.internal.remove_negation(chain)
end
return false
end
function sh.internal.execute_complex(words, eargs, env)
-- we shall validate pipes before any statement execution
local statements = sh.internal.splitStatements(words)
for i=1,#statements do
local ok, why = sh.internal.hasValidPiping(statements[i])
if not ok then return nil,why end
end
for si=1,#statements do local s = statements[si]
local chains = sh.internal.groupChains(s)
local last_code, reason = sh.internal.boolean_executor(chains, function(chain, chain_index)
local pipe_parts = sh.internal.splitChains(chain)
local next_args = chain_index == #chains and si == #statements and eargs or {}
return sh.internal.executePipes(pipe_parts, next_args, env)
end)
sh.internal.ec.last = sh.internal.command_result_as_code(last_code, reason)
end
return sh.internal.ec.last == 0
end
-- params: words[tokenized word list]
-- return: command args, redirects
function sh.internal.evaluate(words)
local redirects, why = sh.internal.buildCommandRedirects(words)
if not redirects then
return nil, why
end
do
local normalized = text.internal.normalize(words)
local command_text = table.concat(normalized, " ")
local subbed = sh.internal.parse_sub(command_text)
if subbed ~= command_text then
words = text.internal.tokenize(subbed)
end
end
local repack = false
for _, word in ipairs(words) do
for _, part in pairs(word) do
if not (part.qr or {})[3] then
local expanded = sh.expand(part.txt)
if expanded ~= part.txt then
part.txt = expanded
repack = true
end
end
end
end
if repack then
local normalized = text.internal.normalize(words)
local command_text = table.concat(normalized, " ")
words = text.internal.tokenize(command_text)
end
local args = {}
for _, word in ipairs(words) do
local eword = { txt = "" }
for _, part in ipairs(word) do
eword.txt = eword.txt .. part.txt
eword[#eword + 1] = { qr = part.qr, txt = part.txt }
end
for _, arg in ipairs(sh.internal.glob(eword)) do
args[#args + 1] = arg
end
end
return args, redirects
end
function sh.internal.parse_sub(input, quotes)
-- unquoted command substituted text is parsed as individual parameters
-- there is not a concept of "keeping whitespace" as previously thought
-- we see removal of whitespace only because they are separate arguments
-- e.g. /echo `echo a b`/ becomes /echo a b/ quite literally, and the a and b are separate inputs
-- e.g. /echo a"`echo b c`"d/ becomes /echo a"b c"d/ which is a single input
if quotes and quotes[1] == '`' then
input = string.format("`%s`", input)
quotes[1], quotes[2] = "", "" -- substitution removes the quotes
end
-- cannot use gsub here becuase it is a [C] call, and io.popen needs to yield at times
local packed = {}
-- not using for i... because i can skip ahead
local i, len = 1, #input
while i <= len do
local fi, si, capture = input:find("`([^`]*)`", i)
if not fi then
table.insert(packed, input:sub(i))
break
end
table.insert(packed, input:sub(i, fi - 1))
local sub = io.popen(capture)
local result = sub:read("*a")
sub:close()
-- command substitution cuts trailing newlines
table.insert(packed, (result:gsub("\n+$","")))
i = si+1
end
return table.concat(packed)
end

View File

@@ -0,0 +1,25 @@
local shell = require("shell")
local process = require("process")
function shell.aliases()
return pairs(process.info().data.aliases)
end
function shell.execute(command, env, ...)
local sh, reason = shell.getShell()
if not sh then
return false, reason
end
local proc = process.load(sh, nil, nil, command)
local result = table.pack(process.internal.continue(proc, env, command, ...))
if result.n == 0 then return true end
return table.unpack(result, 1, result.n)
end
function shell.getPath()
return os.getenv("PATH")
end
function shell.setPath(value)
os.setenv("PATH", value)
end

View File

@@ -0,0 +1,286 @@
local text = require("text")
local tx = require("transforms")
local unicode = require("unicode")
local process = require("process")
local buffer = require("buffer")
-- separate string value into an array of words delimited by whitespace
-- groups by quotes
-- options is a table used for internal undocumented purposes
function text.tokenize(value, options)
checkArg(1, value, "string")
checkArg(2, options, "table", "nil")
options = options or {}
local tokens, reason = text.internal.tokenize(value, options)
if type(tokens) ~= "table" then
return nil, reason
end
if options.doNotNormalize then
return tokens
end
return text.internal.normalize(tokens)
end
-------------------------------------------------------------------------------
-- like tokenize, but does not drop any text such as whitespace
-- splits input into an array for sub strings delimited by delimiters
-- delimiters are included in the result if not dropDelims
function text.split(input, delimiters, dropDelims, di)
checkArg(1, input, "string")
checkArg(2, delimiters, "table")
checkArg(3, dropDelims, "boolean", "nil")
checkArg(4, di, "number", "nil")
if #input == 0 then return {} end
di = di or 1
local result = {input}
if di > #delimiters then return result end
local function add(part, index, r, s, e)
local sub = part:sub(s,e)
if #sub == 0 then return index end
local subs = r and text.split(sub,delimiters,dropDelims,r) or {sub}
for i=1,#subs do
table.insert(result, index+i-1, subs[i])
end
return index+#subs
end
local i,d=1,delimiters[di]
while true do
local next = table.remove(result,i)
if not next then break end
local si,ei = next:find(d)
if si and ei and ei~=0 then -- delim found
i=add(next, i, di+1, 1, si-1)
i=dropDelims and i or add(next, i, false, si, ei)
i=add(next, i, di, ei+1)
else
i=add(next, i, di+1, 1, #next)
end
end
return result
end
-----------------------------------------------------------------------------
-- splits each word into words at delimiters
-- delimiters are kept as their own words
-- quoted word parts are not split
function text.internal.splitWords(words, delimiters)
checkArg(1,words,"table")
checkArg(2,delimiters,"table")
local split_words = {}
local next_word
local function add_part(part)
if next_word then
split_words[#split_words+1] = {}
end
table.insert(split_words[#split_words], part)
next_word = false
end
for wi=1,#words do local word = words[wi]
next_word = true
for pi=1,#word do local part = word[pi]
local qr = part.qr
if qr then
add_part(part)
else
local part_text_splits = text.split(part.txt, delimiters)
tx.foreach(part_text_splits, function(sub_txt)
local delim = #text.split(sub_txt, delimiters, true) == 0
next_word = next_word or delim
add_part({txt=sub_txt,qr=qr})
next_word = delim
end)
end
end
end
return split_words
end
function text.internal.normalize(words, omitQuotes)
checkArg(1, words, "table")
checkArg(2, omitQuotes, "boolean", "nil")
local norms = {}
for _,word in ipairs(words) do
local norm = {}
for _,part in ipairs(word) do
norm = tx.concat(norm, not omitQuotes and part.qr and {part.qr[1], part.txt, part.qr[2]} or {part.txt})
end
norms[#norms+1]=table.concat(norm)
end
return norms
end
function text.internal.stream_base(binary)
return
{
binary = binary,
plen = binary and string.len or unicode.len,
psub = binary and string.sub or unicode.sub,
seek = function (handle, whence, to)
if not handle.txt then
return nil, "bad file descriptor"
end
to = to or 0
local offset = handle:indexbytes()
if whence == "cur" then
offset = offset + to
elseif whence == "set" then
offset = to
elseif whence == "end" then
offset = handle.len + to
end
offset = math.max(0, math.min(offset, handle.len))
handle:byteindex(offset)
return offset
end,
indexbytes = function (handle)
return handle.psub(handle.txt, 1, handle.index):len()
end,
byteindex = function (handle, offset)
local sub = string.sub(handle.txt, 1, offset)
handle.index = handle.plen(sub)
end,
}
end
function text.internal.reader(txt, mode)
checkArg(1, txt, "string")
local reader = setmetatable(
{
txt = txt,
len = string.len(txt),
index = 0,
read = function(_, n)
checkArg(1, n, "number")
if not _.txt then
return nil, "bad file descriptor"
end
if _.index >= _.plen(_.txt) then
return nil
end
local next = _.psub(_.txt, _.index + 1, _.index + n)
_.index = _.index + _.plen(next)
return next
end,
close = function(_)
if not _.txt then
return nil, "bad file descriptor"
end
_.txt = nil
return true
end,
}, {__index=text.internal.stream_base((mode or ""):match("b"))})
return process.addHandle(buffer.new((mode or "r"):match("[rb]+"), reader))
end
function text.internal.writer(ostream, mode, append_txt)
if type(ostream) == "table" then
local mt = getmetatable(ostream) or {}
checkArg(1, mt.__call, "function")
end
checkArg(1, ostream, "function", "table")
checkArg(2, append_txt, "string", "nil")
local writer = setmetatable(
{
txt = "",
index = 0, -- last location of write
len = 0,
write = function(_, ...)
if not _.txt then
return nil, "bad file descriptor"
end
local pre = _.psub(_.txt, 1, _.index)
local vs = {}
local pos = _.psub(_.txt, _.index + 1)
for _,v in ipairs({...}) do
table.insert(vs, v)
end
vs = table.concat(vs)
_.index = _.index + _.plen(vs)
_.txt = pre .. vs .. pos
_.len = string.len(_.txt)
return true
end,
close = function(_)
if not _.txt then
return nil, "bad file descriptor"
end
ostream((append_txt or "") .. _.txt)
_.txt = nil
return true
end,
}, {__index=text.internal.stream_base((mode or ""):match("b"))})
return process.addHandle(buffer.new((mode or "w"):match("[awb]+"), writer))
end
function text.detab(value, tabWidth)
checkArg(1, value, "string")
checkArg(2, tabWidth, "number", "nil")
tabWidth = tabWidth or 8
local function rep(match)
local spaces = tabWidth - match:len() % tabWidth
return match .. string.rep(" ", spaces)
end
local result = value:gsub("([^\n]-)\t", rep) -- truncate results
return result
end
function text.padLeft(value, length)
checkArg(1, value, "string", "nil")
checkArg(2, length, "number")
if not value or unicode.wlen(value) == 0 then
return string.rep(" ", length)
else
return string.rep(" ", length - unicode.wlen(value)) .. value
end
end
function text.padRight(value, length)
checkArg(1, value, "string", "nil")
checkArg(2, length, "number")
if not value or unicode.wlen(value) == 0 then
return string.rep(" ", length)
else
return value .. string.rep(" ", length - unicode.wlen(value))
end
end
function text.wrap(value, width, maxWidth)
checkArg(1, value, "string")
checkArg(2, width, "number")
checkArg(3, maxWidth, "number")
local line, nl = value:match("([^\r\n]*)(\r?\n?)") -- read until newline
if unicode.wlen(line) > width then -- do we even need to wrap?
local partial = unicode.wtrunc(line, width)
local wrapped = partial:match("(.*[^a-zA-Z0-9._()'`=])")
if wrapped or unicode.wlen(line) > maxWidth then
partial = wrapped or partial
return partial, unicode.sub(value, unicode.len(partial) + 1), true
else
return "", value, true -- write in new line.
end
end
local start = unicode.len(line) + unicode.len(nl) + 1
return line, start <= unicode.len(value) and unicode.sub(value, start) or nil, unicode.len(nl) > 0
end
function text.wrappedLines(value, width, maxWidth)
local line
return function()
if value then
line, value = text.wrap(value, width, maxWidth)
return line
end
end
end

View File

@@ -0,0 +1,109 @@
local lib = require("transforms")
local adjust=lib.internal.range_adjust
local view=lib.internal.table_view
-- works like string.sub but on elements of an indexed table
function lib.sub(tbl,f,l)
checkArg(1,tbl,'table')
local r,s={},#tbl
f,l=adjust(f,l,s)
l=math.min(l,s)
for i=math.max(f,1),l do
r[#r+1]=tbl[i]
end
return r
end
-- Returns a list of subsets of tbl where partitioner acts as a delimiter.
function lib.partition(tbl,partitioner,dropEnds,f,l)
checkArg(1,tbl,'table')
checkArg(2,partitioner,'function','table')
checkArg(3,dropEnds,'boolean','nil')
if type(partitioner)=='table'then
return lib.partition(tbl,function(e,i,tbl)
return lib.first(tbl,partitioner,i)
end,dropEnds,f,l)
end
local s=#tbl
f,l=adjust(f,l,s)
local cut=view(tbl,f,l)
local result={}
local need=true
local exp=function()if need then result[#result+1]={}need=false end end
local i=f
while i<=l do
local e=cut[i]
local ds,de=partitioner(e,i,cut)
-- true==partition here
if ds==true then ds,de=i,i
elseif ds==false then ds,de=nil,nil end
if ds~=nil then
ds,de=adjust(ds,de,l)
ds=ds>=i and ds--no more
end
if not ds then -- false or nil
exp()
table.insert(result[#result],e)
else
local sub=lib.sub(cut,i,not dropEnds and de or (ds-1))
if #sub>0 then
exp()
result[#result+math.min(#result[#result],1)]=sub
end
-- ensure i moves forward
local ensured=math.max(math.max(de or ds,ds),i)
if de and ds and de<ds and ensured==i then
if #result==0 then result[1]={} end
table.insert(result[#result],e)
end
i=ensured
need=true
end
i=i+1
end
return result
end
-- calls callback(e,i,tbl) for each ith element e in table tbl from first
function lib.foreach(tbl,c,f,l)
checkArg(1,tbl,'table')
checkArg(2,c,'function','string')
local ck=c
c=type(c)=="string" and function(e) return e[ck] end or c
local s=#tbl
f,l=adjust(f,l,s)
tbl=view(tbl,f,l)
local r={}
for i=f,l do
local n,k=c(tbl[i],i,tbl)
if n~=nil then
if k then r[k]=n
else r[#r+1]=n end
end
end
return r
end
function lib.where(tbl,p,f,l)
return lib.foreach(tbl,
function(e,i,tbl)
return p(e,i,tbl)and e or nil
end,f,l)
end
-- works with pairs on tables
-- returns the kv pair, or nil and the number of pairs iterated
function lib.at(tbl, index)
checkArg(1, tbl, "table")
checkArg(2, index, "number", "nil")
local current_index = 1
for k,v in pairs(tbl) do
if current_index == index then
return k,v
end
current_index = current_index + 1
end
return nil, current_index - 1 -- went one too far
end

View File

@@ -0,0 +1,93 @@
local vt100 = require("vt100")
local rules = vt100.rules
-- [?7[hl] wrap mode
rules[{"%[", "%?", "7", "[hl]"}] = function(window, _, _, _, nowrap)
window.nowrap = nowrap == "l"
end
-- helper scroll function
local function set_cursor(window, x, y)
window.x = math.min(math.max(x, 1), window.width)
window.y = math.min(math.max(y, 1), window.height)
end
-- -- These DO NOT SCROLL
-- [(%d*)A move cursor up n lines
-- [(%d*)B move cursor down n lines
-- [(%d*)C move cursor right n lines
-- [(%d*)D move cursor left n lines
rules[{"%[", "%d*", "[ABCD]"}] = function(window, _, n, dir)
local dx, dy = 0, 0
n = tonumber(n) or 1
if dir == "A" then
dy = -n
elseif dir == "B" then
dy = n
elseif dir == "C" then
dx = n
else -- D
dx = -n
end
set_cursor(window, window.x + dx, window.y + dy)
end
-- [Line;ColumnH Move cursor to screen location v,h
-- [Line;Columnf ^ same
-- [;H Move cursor to upper left corner
-- [;f ^ same
rules[{"%[", "%d*", ";", "%d*", "[Hf]"}] = function(window, _, y, _, x)
set_cursor(window, tonumber(x) or 1, tonumber(y) or 1)
end
-- [H move cursor to upper left corner
-- [f ^ same
rules[{"%[[Hf]"}] = function(window)
set_cursor(window, 1, 1)
end
-- [K clear line from cursor right
-- [0K ^ same
-- [1K clear line from cursor left
-- [2K clear entire line
local function clear_line(window, _, n)
n = tonumber(n) or 0
local x = (n == 0 and window.x or 1)
local rep = n == 1 and window.x or (window.width - x + 1)
window.gpu.fill(x + window.dx, window.y + window.dy, rep, 1, " ")
end
rules[{"%[", "[012]?", "K"}] = clear_line
-- [J clear screen from cursor down
-- [0J ^ same
-- [1J clear screen from cursor up
-- [2J clear entire screen
rules[{"%[", "[012]?", "J"}] = function(window, _, n)
clear_line(window, _, n)
n = tonumber(n) or 0
local y = n == 0 and (window.y + 1) or 1
local rep = n == 1 and (window.y - 1) or (window.height)
window.gpu.fill(1 + window.dx, y + window.dy, window.width, rep, " ")
end
-- [6n get the cursor position [ EscLine;ColumnR Response: cursor is at v,h ]
rules[{"%[", "6", "n"}] = function(window)
-- this solution puts the response on stdin, but it isn't echo'd
-- I'm personally fine with the lack of echo
io.stdin.bufferRead = string.format("%s%s[%d;%dR", io.stdin.bufferRead, string.char(0x1b), window.y, window.x)
end
-- D scroll up one line -- moves cursor down
-- E move to next line (acts the same ^, but x=1)
-- M scroll down one line -- moves cursor up
rules[{"[DEM]"}] = function(window, _, dir)
if dir == "D" then
window.y = window.y + 1
elseif dir == "E" then
window.y = window.y + 1
window.x = 1
else -- M
window.y = window.y - 1
end
end

View File

@@ -0,0 +1,231 @@
local computer = require("computer")
local shell = require("shell")
local fs = require("filesystem")
local args, options = shell.parse(...)
if options.help then
io.write([[Usage: install [OPTION]...
--from=ADDR install filesystem at ADDR
default: builds list of
candidates and prompts user
--to=ADDR same as --from but for target
--fromDir=PATH install PATH from source
--root=PATH same as --fromDir but target
--toDir=PATH same as --root
-u, --update update files interactively
--label override label from .prop
--nosetlabel do not label target
--nosetboot do not use target for boot
--noreboot do not reboot after install
]])
return nil -- exit success
end
local utils_path = "/lib/core/install_utils.lua"
local utils
local rootfs = fs.get("/")
if not rootfs then
io.stderr:write("no root filesystem, aborting\n")
os.exit(1)
end
local label = args[1]
options.label = label
local source_filter = options.from
local source_filter_dev
if source_filter then
local from_path = shell.resolve(source_filter)
if fs.isDirectory(from_path) then
source_filter_dev = fs.get(from_path)
source_filter = source_filter_dev.address
options.from = from_path
end
end
local target_filter = options.to
local target_filter_dev
if target_filter then
local to_path = shell.resolve(target_filter)
if fs.isDirectory(target_filter) then
target_filter_dev = fs.get(to_path)
target_filter = target_filter_dev.address
options.to = to_path
end
end
local sources = {}
local targets = {}
-- tmpfs is not a candidate unless it is specified
local comps = require("component").list("filesystem")
local devices = {}
-- not all mounts are components, only use components
for dev, path in fs.mounts() do
if comps[dev.address] then
local known = devices[dev]
devices[dev] = known and #known < #path and known or path
end
end
local dev_dev = fs.get("/dev")
devices[dev_dev == rootfs or dev_dev] = nil
local tmpAddress = computer.tmpAddress()
for dev, path in pairs(devices) do
local address = dev.address
local install_path = dev == target_filter_dev and options.to or path
local specified = target_filter and address:find(target_filter, 1, true) == 1
if dev.isReadOnly() then
if specified then
io.stderr:write("Cannot install to " .. options.to .. ", it is read only\n")
os.exit(1)
end
elseif
specified or
not (source_filter and address:find(source_filter, 1, true) == 1) and -- specified for source
not target_filter and
address ~= tmpAddress
then
table.insert(targets, {dev = dev, path = install_path, specified = specified})
end
end
local target = targets[1]
-- if there is only 1 target, the source selection cannot include it
if #targets == 1 then
devices[targets[1].dev] = nil
end
for dev, path in pairs(devices) do
local address = dev.address
local install_path = dev == source_filter_dev and options.from or path
local specified = source_filter and address:find(source_filter, 1, true) == 1
if
fs.list(install_path)() and
(specified or
not source_filter and address ~= tmpAddress and not (address == rootfs.address and not rootfs.isReadOnly()))
then
local prop = {}
local prop_path = path .. "/.prop"
local prop_file = fs.open(prop_path)
if prop_file then
local prop_data = prop_file:read(math.maxinteger or math.huge)
prop_file:close()
local prop_load = load("return " .. prop_data)
prop = prop_load and prop_load()
if not prop then
io.stderr:write("Ignoring " .. path .. " due to malformed prop file\n")
prop = {ignore = true}
end
end
if not prop.ignore then
if not label or label:lower() == (prop.label or dev.getLabel() or ""):lower() then
table.insert(sources, {dev = dev, path = install_path, prop = prop, specified = specified})
end
end
end
end
-- Ask the user to select a source
local source = sources[1]
if #sources ~= 1 then
utils = loadfile(utils_path, "bt", _G)
source = utils("select", "sources", options, sources)
end
if not source then
return
end
options = {
from = source.path .. "/",
fromDir = fs.canonical(options.fromDir or source.prop.fromDir or ""),
root = fs.canonical(options.root or options.toDir or source.prop.root or ""),
update = options.update or options.u,
label = source.prop.label or label,
setlabel = not (options.nosetlabel or options.nolabelset) and source.prop.setlabel,
setboot = not (options.nosetboot or options.noboot) and source.prop.setboot,
reboot = not options.noreboot and source.prop.reboot
}
local source_display = options.label or source.dev.getLabel() or source.path
-- Remove the source from the target options
for index, entry in ipairs(targets) do
if entry.dev == source.dev then
table.remove(targets, index)
target = targets[1]
end
end
-- Ask the user to select a target
if #targets ~= 1 then
if #sources == 1 then
io.write(source_display, " selected for install\n")
end
utils = utils or loadfile(utils_path, "bt", _G)
target = utils("select", "targets", options, targets)
end
if not target then
return
end
options.to = target.path .. "/"
local function resolveFrom(path)
return fs.concat(options.from, options.fromDir) .. "/" .. path
end
local fullTargetPath = fs.concat(options.to, options.root)
local transfer_args = {
{
{resolveFrom("."), fullTargetPath},
{
cmd = "cp",
r = true, v = true, x = true, u = options.update, i = options.update,
skip = {resolveFrom(".prop")},
}
}
}
if source.prop.noclobber and #source.prop.noclobber > 0 then
local noclobber_opts = {cmd = "cp", v = true, n = true}
for _, noclobber in ipairs(source.prop.noclobber or {}) do
local noclobberFrom = resolveFrom(noclobber)
local noclobberTo = fs.concat(fullTargetPath, noclobber)
table.insert(transfer_args[1][2].skip, noclobberFrom)
table.insert(transfer_args, {{noclobberFrom, noclobberTo}, noclobber_opts})
end
end
local special_target = ""
if #targets > 1 or target_filter or source_filter then
special_target = " to " .. transfer_args[1][1][2]
end
io.write("Install " .. source_display .. special_target .. "? [Y/n] ")
if not ((io.read() or "n") .. "y"):match("^%s*[Yy]") then
io.write("Installation cancelled\n")
os.exit()
end
local installer_path = options.from .. "/.install"
if fs.exists(installer_path) then
local installer, reason = loadfile(installer_path, "bt", setmetatable({install = options}, {__index = _G}))
if not installer then
io.stderr:write("installer failed to load: " .. tostring(reason) .. "\n")
os.exit(1)
end
os.exit(installer())
end
options.cp_args = transfer_args
options.target = target
return options

View File

@@ -0,0 +1,68 @@
local cmd, arg, options, devices = ...
local function select_prompt(devs, prompt)
table.sort(devs, function(a, b) return a.path<b.path end)
local num_devs = #devs
if num_devs < 2 then
return devs[1]
end
io.write(prompt,'\n')
for i = 1, num_devs do
local src = devs[i]
local dev = src.dev
local selection_label = (src.prop or {}).label or dev.getLabel()
if selection_label then
selection_label = string.format("%s (%s...)", selection_label, dev.address:sub(1, 8))
else
selection_label = dev.address
end
io.write(string.format("%d) %s at %s [r%s]\n", i, selection_label, src.path, dev.isReadOnly() and 'o' or 'w'))
end
io.write("Please enter a number between 1 and " .. num_devs .. '\n')
io.write("Enter 'q' to cancel the installation: ")
for _=1,5 do
local result = io.read() or "q"
if result == "q" then
os.exit()
end
local number = tonumber(result)
if number and number > 0 and number <= num_devs then
return devs[number]
else
io.write("Invalid input, please try again: ")
os.sleep(0)
end
end
print("\ntoo many bad inputs, aborting")
os.exit(1)
end
if cmd == "select" then
if arg == "sources" then
if #devices == 0 then
if options.label then
io.stderr:write("Nothing to install labeled: " .. options.label .. '\n')
elseif options.from then
io.stderr:write("Nothing to install from: " .. options.from .. '\n')
else
io.stderr:write("Nothing to install\n")
end
os.exit(1)
end
return select_prompt(devices, "What do you want to install?")
elseif arg == "targets" then
if #devices == 0 then
if options.to then
io.stderr:write("No such target to install to: " .. options.to .. '\n')
else
io.stderr:write("No writable disks found, aborting\n")
end
os.exit(1)
end
return select_prompt(devices, "Where do you want to install to?")
end
end

View File

@@ -0,0 +1,129 @@
local package = require("package")
local term = require("term")
local function optrequire(...)
local success, module = pcall(require, ...)
if success then
return module
end
end
local env -- forward declare for binding in metamethod
env = setmetatable({}, {
__index = function(_, k)
_ENV[k] = _ENV[k] or optrequire(k)
return _ENV[k]
end,
__pairs = function(t)
return function(_, key)
local k, v = next(t, key)
if not k and t == env then
t = _ENV
k, v = next(t)
end
if not k and t == _ENV then
t = package.loaded
k, v = next(t)
end
return k, v
end
end,
})
env._PROMPT = tostring(env._PROMPT or "\27[32mlua> \27[37m")
local function findTable(t, path)
if type(t) ~= "table" then return nil end
if not path or #path == 0 then return t end
local name = string.match(path, "[^.]+")
for k, v in pairs(t) do
if k == name then
return findTable(v, string.sub(path, #name + 2))
end
end
local mt = getmetatable(t)
if t == env then mt = {__index=_ENV} end
if mt then
return findTable(mt.__index, path)
end
return nil
end
local function findKeys(t, r, prefix, name)
if type(t) ~= "table" then return end
for k, v in pairs(t) do
if type(k) == "string" and string.match(k, "^"..name) then
local postfix = ""
if type(v) == "function" then postfix = "()"
elseif type(v) == "table" and getmetatable(v) and getmetatable(v).__call then postfix = "()"
elseif type(v) == "table" then postfix = "."
end
r[prefix..k..postfix] = true
end
end
local mt = getmetatable(t)
if t == env then mt = {__index=_ENV} end
if mt then
return findKeys(mt.__index, r, prefix, name)
end
end
local read_handler = {hint = function(line, index)
line = (line or "")
local tail = line:sub(index)
line = line:sub(1, index - 1)
local path = string.match(line, "[a-zA-Z_][a-zA-Z0-9_.]*$")
if not path then return nil end
local suffix = string.match(path, "[^.]+$") or ""
local prefix = string.sub(path, 1, #path - #suffix)
local tbl = findTable(env, prefix)
if not tbl then return nil end
local keys = {}
local hints = {}
findKeys(tbl, keys, string.sub(line, 1, #line - #suffix), suffix)
for key in pairs(keys) do
table.insert(hints, key .. tail)
end
return hints
end}
io.write("\27[37m".._VERSION .. " Copyright (C) 1994-2022 Lua.org, PUC-Rio\n")
io.write("\27[33mEnter a statement and hit enter to evaluate it.\n")
io.write("Prefix an expression with '=' to show its value.\n")
io.write("Press Ctrl+D to exit the interpreter.\n\27[37m")
while term.isAvailable() do
io.write(env._PROMPT)
local command = term.read(read_handler)
if not command then -- eof
return
end
local code, reason
if string.sub(command, 1, 1) == "=" then
code, reason = load("return " .. string.sub(command, 2), "=stdin", "t", env)
else
code, reason = load("return " .. command, "=stdin", "t", env)
if not code then
code, reason = load(command, "=stdin", "t", env)
end
end
if code then
local result = table.pack(xpcall(code, debug.traceback))
if not result[1] then
if type(result[2]) == "table" and result[2].reason == "terminated" then
os.exit(result[2].code)
end
io.stderr:write(tostring(result[2]) .. "\n")
else
local ok, why = pcall(function()
for i = 2, result.n do
io.write(require("serialization").serialize(result[i], true), i < result.n and "\t" or "\n")
end
end)
if not ok then
io.stderr:write("crashed serializing result: ", tostring(why))
end
end
else
io.stderr:write(tostring(reason) .. "\n")
end
end