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

102
data/OpenOS/lib/bit32.lua Normal file
View File

@@ -0,0 +1,102 @@
--[[ Backwards compat for Lua 5.3; only loaded in 5.3 because package.loaded is
prepopulated with the existing global bit32 in 5.2. ]]
local bit32 = {}
-------------------------------------------------------------------------------
local function fold(init, op, ...)
local result = init
local args = table.pack(...)
for i = 1, args.n do
result = op(result, args[i])
end
return result
end
local function trim(n)
return n & 0xFFFFFFFF
end
local function mask(w)
return ~(0xFFFFFFFF << w)
end
function bit32.arshift(x, disp)
return x // (2 ^ disp)
end
function bit32.band(...)
return fold(0xFFFFFFFF, function(a, b) return a & b end, ...)
end
function bit32.bnot(x)
return ~x
end
function bit32.bor(...)
return fold(0, function(a, b) return a | b end, ...)
end
function bit32.btest(...)
return bit32.band(...) ~= 0
end
function bit32.bxor(...)
return fold(0, function(a, b) return a ~ b end, ...)
end
local function fieldargs(f, w)
w = w or 1
assert(f >= 0, "field cannot be negative")
assert(w > 0, "width must be positive")
assert(f + w <= 32, "trying to access non-existent bits")
return f, w
end
function bit32.extract(n, field, width)
local f, w = fieldargs(field, width)
return (n >> f) & mask(w)
end
function bit32.replace(n, v, field, width)
local f, w = fieldargs(field, width)
local m = mask(w)
return (n & ~(m << f)) | ((v & m) << f)
end
function bit32.lrotate(x, disp)
if disp == 0 then
return x
elseif disp < 0 then
return bit32.rrotate(x, -disp)
else
disp = disp & 31
x = trim(x)
return trim((x << disp) | (x >> (32 - disp)))
end
end
function bit32.lshift(x, disp)
return trim(x << disp)
end
function bit32.rrotate(x, disp)
if disp == 0 then
return x
elseif disp < 0 then
return bit32.lrotate(x, -disp)
else
disp = disp & 31
x = trim(x)
return trim((x >> disp) | (x << (32 - disp)))
end
end
function bit32.rshift(x, disp)
return trim(x >> disp)
end
-------------------------------------------------------------------------------
return bit32

182
data/OpenOS/lib/buffer.lua Normal file
View File

@@ -0,0 +1,182 @@
local computer = require("computer")
local unicode = require("unicode")
local buffer = {}
local metatable = {
__index = buffer,
__metatable = "file"
}
function buffer.new(mode, stream)
local result = {
closed = false,
tty = false,
mode = {},
stream = stream,
bufferRead = "",
bufferWrite = "",
bufferSize = math.max(512, math.min(8 * 1024, computer.freeMemory() / 8)),
bufferMode = "full",
readTimeout = math.huge,
}
mode = mode or "r"
for i = 1, unicode.len(mode) do
result.mode[unicode.sub(mode, i, i)] = true
end
-- when stream closes, result should close first
-- when result closes, stream should close after
-- when stream closes, it is removed from the proc
stream.close = setmetatable({close = stream.close,parent = result},{__call = buffer.close})
return setmetatable(result, metatable)
end
function buffer:close()
-- self is either the buffer, or the stream.close callable
local meta = getmetatable(self)
if meta == metatable.__metatable then
return self.stream:close()
end
local parent = self.parent
if parent.mode.w or parent.mode.a then
parent:flush()
end
parent.closed = true
return self.close(parent.stream)
end
function buffer:flush()
if #self.bufferWrite > 0 then
local tmp = self.bufferWrite
self.bufferWrite = ""
local result, reason = self.stream:write(tmp)
if not result then
return nil, reason or "bad file descriptor"
end
end
return self
end
function buffer:lines(...)
local args = table.pack(...)
return function()
local result = table.pack(self:read(table.unpack(args, 1, args.n)))
if not result[1] and result[2] then
error(result[2])
end
return table.unpack(result, 1, result.n)
end
end
local function readChunk(self)
if computer.uptime() > self.timeout then
error("timeout")
end
local result, reason = self.stream:read(math.max(1,self.bufferSize))
if result then
self.bufferRead = self.bufferRead .. result
return self
else -- error or eof
return result, reason
end
end
function buffer:readLine(chop, timeout)
self.timeout = timeout or (computer.uptime() + self.readTimeout)
local start = 1
while true do
local buf = self.bufferRead
local i = buf:find("[\r\n]", start)
local c = i and buf:sub(i,i)
local is_cr = c == "\r"
if i and (not is_cr or i < #buf) then
local n = buf:sub(i+1,i+1)
if is_cr and n == "\n" then
c = c .. n
end
local result = buf:sub(1, i - 1) .. (chop and "" or c)
self.bufferRead = buf:sub(i + #c)
return result
else
start = #self.bufferRead - (is_cr and 1 or 0)
local result, reason = readChunk(self)
if not result then
if reason then
return result, reason
else -- eof
result = #self.bufferRead > 0 and self.bufferRead or nil
self.bufferRead = ""
return result
end
end
end
end
end
function buffer:read(...)
if not self.mode.r then
return nil, "read mode was not enabled for this stream"
end
if self.mode.w or self.mode.a then
self:flush()
end
if select("#", ...) == 0 then
return self:readLine(true)
end
return self:formatted_read(readChunk, ...)
end
function buffer:setvbuf(mode, size)
mode = mode or self.bufferMode
size = size or self.bufferSize
assert(mode == "no" or mode == "full" or mode == "line",
"bad argument #1 (no, full or line expected, got " .. tostring(mode) .. ")")
assert(mode == "no" or type(size) == "number",
"bad argument #2 (number expected, got " .. type(size) .. ")")
self.bufferMode = mode
self.bufferSize = size
return self.bufferMode, self.bufferSize
end
function buffer:write(...)
if self.closed then
return nil, "bad file descriptor"
end
if not self.mode.w and not self.mode.a then
return nil, "write mode was not enabled for this stream"
end
local args = table.pack(...)
for i = 1, args.n do
if type(args[i]) == "number" then
args[i] = tostring(args[i])
end
checkArg(i, args[i], "string")
end
for i = 1, args.n do
local arg = args[i]
local result, reason
if self.bufferMode == "no" then
result, reason = self.stream:write(arg)
else
result, reason = buffer.buffered_write(self, arg)
end
if not result then
return nil, reason
end
end
return self
end
require("package").delay(buffer, "/lib/core/full_buffer.lua")
return buffer

View File

@@ -0,0 +1,30 @@
local colors = {
[0] = "white",
[1] = "orange",
[2] = "magenta",
[3] = "lightblue",
[4] = "yellow",
[5] = "lime",
[6] = "pink",
[7] = "gray",
[8] = "silver",
[9] = "cyan",
[10] = "purple",
[11] = "blue",
[12] = "brown",
[13] = "green",
[14] = "red",
[15] = "black"
}
do
local keys = {}
for k in pairs(colors) do
table.insert(keys, k)
end
for _, k in pairs(keys) do
colors[colors[k]] = k
end
end
return colors

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

345
data/OpenOS/lib/devfs.lua Normal file
View File

@@ -0,0 +1,345 @@
local fs = require("filesystem")
local text = require("text")
local api = {}
local function new_node(proxy)
local node = {proxy=proxy}
if not proxy or not proxy.list then
node.children = {}
end
return node
end
local function array_read(array, separator)
separator = separator or " "
local builder = {}
for _,value in ipairs(array) do
table.insert(builder, tostring(value))
end
return table.concat(builder, separator)
end
local function child_iterator(node)
-- a node can either list or have children, but not both (see add_child)
-- a node can be a file, which has a proxy, but no children
local listed = {}
if node then
if node.proxy and node.proxy.list then
-- list should return a table, not another iterator
-- the elements in the list are not nodes, but proxies
-- we have to wrap each entry with a virtual node (a node that is not in a child-parent tree)
-- list can be a function that returns a table, or the table already
local list = node.proxy.list
listed = type(list) == "table" and list or list()
elseif node.children then
listed = node.children
end
end
local availables = {}
for name, item in pairs(listed) do
if name:len() > 0 then
if not item.proxy then item = new_node(item) end
if not item.proxy.isAvailable or item.proxy.isAvailable() then
availables[name] = item
end
end
end
return pairs(availables)
end
local function get_child(node, name)
for child_name, child in child_iterator(node) do
if child_name == name then
return child
end
end
end
local function add_child(node, name, proxy)
if not node or node.proxy and node.proxy.list then
return nil, "cannot add child to listing proxy"
end
local child = new_node(proxy)
node.children[name] = child
return child
end
local function findNode(path, bCreate)
local segments = fs.segments(path)
local node = api.root
while #segments > 0 do
local name = table.remove(segments, 1)
local next = get_child(node, name)
if not next then
if bCreate then
if not add_child(node, name) then
return nil, "cannot create child node"
end
else
return nil, "no such file or directory"
end
end
node = next or get_child(node, name)
end
return node
end
-- devfs api
api.root = new_node()
function api.create(path, proxy)
checkArg(1, path, "string")
checkArg(2, proxy, "table", "nil")
local pwd = fs.path(path)
local name = fs.name(path)
if not name then return nil, "invalid devfs path" end
local pnode, why = findNode(pwd, true)
if not pnode then
return nil, why
end
if get_child(pnode, name) then
return nil, "file or directory exists"
end
return add_child(pnode, name, proxy)
end
-- the filesystem object as seen from the system mount interface
api.proxy = {}
-- forward declare injector
local inject_dynamic_pairs
local function dynamic_list(path, fsnode)
local nodes, links, dirs = {}, {}, {}
local node = findNode(path)
if node then
for name,cnode in child_iterator(node) do
if cnode.proxy and cnode.proxy.link then
links[name] = cnode.proxy.link
elseif cnode.proxy and cnode.proxy.list then
local child = {name=name,parent=fsnode}
local child_path = path .. "/" .. name
inject_dynamic_pairs(child, child_path, true)
dirs[name] = child
else
nodes[name] = cnode
end
end
end
return nodes, links, dirs
end
inject_dynamic_pairs = function(fsnode, path, bStoreUse)
if getmetatable(fsnode) then return end
fsnode.children = nil
fsnode.links = nil
setmetatable(fsnode,
{
__index = function(tbl, key)
local bLinks = key == "links"
local bChildren = key == "children"
if not bLinks and not bChildren then return end
local _, links, dirs = dynamic_list(path, tbl)
if bStoreUse then
tbl.children = dirs
tbl.links = links
end
return bLinks and links or dirs
end
})
end
local label_lib = dofile("/lib/core/device_labeling.lua")
label_lib.loadRules()
api.getDeviceLabel = label_lib.getDeviceLabel
api.setDeviceLabel = label_lib.setDeviceLabel
local registered = false
function api.register(public_proxy)
if registered then return end
registered = true
local start_path = "/lib/core/devfs/"
for starter in fs.list(start_path) do
local full_path = start_path .. starter
local _,matched = starter:gsub("%.lua$","")
if matched > 0 then
local data = dofile(full_path)
for name, entry in pairs(data) do
api.create(name, entry)
end
end
end
if rawget(public_proxy, "fsnode") then
inject_dynamic_pairs(public_proxy.fsnode, "")
end
end
function api.proxy.list(path)
local result = {}
for name in pairs(dynamic_list(path, false)) do
table.insert(result, name)
end
return result
end
function api.proxy.isDirectory(path)
local node = findNode(path)
return node and node.proxy and node.proxy.list
end
function api.proxy.size(path)
checkArg(1, path, "string")
local node = findNode(path)
if not node or not node.proxy then
return 0
end
local proxy = node.proxy
if proxy.list then return 0 end
if proxy.size then return proxy.size() end
if proxy.open then return 0 end
if proxy.read then return proxy.read():len() end
if proxy[1] ~= nil then return array_read(proxy):len() end
return 0
end
function api.proxy.lastModified()
return 0
end
function api.proxy.exists(path)
checkArg(1, path, "string")
return not not findNode(path)
end
function api.getDevice(path)
checkArg(1, path, "string")
local device
local reason = "no such device"
local real, why = fs.realPath(require("shell").resolve(path))
if not real then return nil, why end
if fs.exists(real) then
-- we don't have a good way of knowing where dev is mounted still
-- similar hack in api.proxy.open
real = fs.path(real) .. (fs.name(real) or "")
local part, subbed = real:gsub("^/dev/", "")
if subbed > 0 and part:len() > 0 then
local node = findNode(part)
if node and node.proxy then
-- must be a special device node
device = node.proxy.device
end
if not device then
reason = "not a device"
end
else
device, reason = fs.get(real)
end
end
return device, reason
end
function api.proxy.open(path, mode)
checkArg(1, path, "string")
checkArg(2, mode, "string", "nil")
mode = mode or "r"
local bRead = mode:match("[ra]")
local bWrite = mode:match("[wa]")
if not bRead and not bWrite then
return nil, "invalid mode"
end
local node, why = findNode(path)
if not node then
return nil, why
elseif not node.proxy or node.proxy.list then
return nil, "is a directory"
end
local proxy = node.proxy
-- in case someone tries to open a link directly, refer them back to fs
-- this is an unfortunate pathing hack due to optimizations for memory
if proxy.link then
return fs.open("/dev/"..path, mode)
end
-- special (but common) simple readonly cases
if proxy[1] ~= nil then -- contains special readonly value
local array = proxy
proxy.read = function()return array_read(array) end
end
if proxy.open then
return proxy.open(mode)
end
if bRead and not proxy.read then
return nil, "cannot open for read"
elseif bWrite and not proxy.write then
return nil, "cannot open for write"
end
local txtRead = bRead and proxy.read()
if bWrite then
return text.internal.writer(proxy.write, mode, txtRead)
end
return text.internal.reader(txtRead, mode)
end
-- as long as the fsnode hack is used, fs.isLink is not needed here
-- function api.proxy.isLink(path) end
local function checked_invoke(handle, method, ...)
checkArg(1, handle, "table")
checkArg(2, method, "string")
checkArg(3, handle[method], "function", "table", "nil")
local m = handle[method]
if not m then
return nil, "bad file handle"
elseif type(m) == "table" then
local mm = getmetatable(m)
assert(mm and mm.__call, string.format("FILE handle [%s] method defined, but is not callable", tostring(method)))
end
return m(handle, ...)
end
function api.proxy.read(h, ...)
return checked_invoke(h, "read", ...)
end
function api.proxy.close(h, ...)
return checked_invoke(h, "close", ...)
end
function api.proxy.write(h, ...)
return checked_invoke(h, "write", ...)
end
function api.proxy.seek(h, ...)
return checked_invoke(h, "seek", ...)
end
function api.proxy.remove()
return nil, "cannot remove file or directory"
end
function api.proxy.makeDirectory()
return nil, "use create in the devfs api"
end
function api.proxy.setLabel()
return nil, "cannot set label on devfs"
end
return api

165
data/OpenOS/lib/event.lua Normal file
View File

@@ -0,0 +1,165 @@
local computer = require("computer")
local keyboard = require("keyboard")
local event = {}
local handlers = {}
local lastInterrupt = -math.huge
event.handlers = handlers
function event.register(key, callback, interval, times, opt_handlers)
local handler =
{
key = key,
times = times or 1,
callback = callback,
interval = interval or math.huge,
}
handler.timeout = computer.uptime() + handler.interval
opt_handlers = opt_handlers or handlers
local id = 0
repeat
id = id + 1
until not opt_handlers[id]
opt_handlers[id] = handler
return id
end
local _pullSignal = computer.pullSignal
setmetatable(handlers, {__call=function(_,...)return _pullSignal(...)end})
computer.pullSignal = function(seconds) -- dispatch
checkArg(1, seconds, "number", "nil")
seconds = seconds or math.huge
local uptime = computer.uptime
local deadline = uptime() + seconds
repeat
local interrupting = uptime() - lastInterrupt > 1 and keyboard.isControlDown() and keyboard.isKeyDown(keyboard.keys.c)
if interrupting then
lastInterrupt = uptime()
if keyboard.isAltDown() then
require("process").info().data.signal("interrupted", 0)
return
end
event.push("interrupted", lastInterrupt)
end
local closest = deadline
for _,handler in pairs(handlers) do
closest = math.min(handler.timeout, closest)
end
local event_data = table.pack(handlers(closest - uptime()))
local signal = event_data[1]
local copy = {}
for id,handler in pairs(handlers) do
copy[id] = handler
end
for id,handler in pairs(copy) do
-- timers have false keys
-- nil keys match anything
if (handler.key == nil or handler.key == signal) or uptime() >= handler.timeout then
handler.times = handler.times - 1
handler.timeout = handler.timeout + handler.interval
-- we have to remove handlers before making the callback in case of timers that pull
-- and we have to check handlers[id] == handler because callbacks may have unregistered things
if handler.times <= 0 and handlers[id] == handler then
handlers[id] = nil
end
-- call
local result, message = pcall(handler.callback, table.unpack(event_data, 1, event_data.n))
if not result then
pcall(event.onError, message)
elseif message == false and handlers[id] == handler then
handlers[id] = nil
end
end
end
if signal then
return table.unpack(event_data, 1, event_data.n)
end
until uptime() >= deadline
end
local function createPlainFilter(name, ...)
local filter = table.pack(...)
if name == nil and filter.n == 0 then
return nil
end
return function(...)
local signal = table.pack(...)
if name and not (type(signal[1]) == "string" and signal[1]:match(name)) then
return false
end
for i = 1, filter.n do
if filter[i] ~= nil and filter[i] ~= signal[i + 1] then
return false
end
end
return true
end
end
-------------------------------------------------------------------------------
function event.listen(name, callback)
checkArg(1, name, "string")
checkArg(2, callback, "function")
for _, handler in pairs(handlers) do
if handler.key == name and handler.callback == callback then
return false
end
end
return event.register(name, callback, math.huge, math.huge)
end
function event.pull(...)
local args = table.pack(...)
if type(args[1]) == "string" then
return event.pullFiltered(createPlainFilter(...))
else
checkArg(1, args[1], "number", "nil")
checkArg(2, args[2], "string", "nil")
return event.pullFiltered(args[1], createPlainFilter(select(2, ...)))
end
end
function event.pullFiltered(...)
local args = table.pack(...)
local seconds, filter = math.huge
if type(args[1]) == "function" then
filter = args[1]
else
checkArg(1, args[1], "number", "nil")
checkArg(2, args[2], "function", "nil")
seconds = args[1]
filter = args[2]
end
local deadline = computer.uptime() + (seconds or math.huge)
repeat
local waitTime = deadline - computer.uptime()
if waitTime <= 0 then
break
end
local signal = table.pack(computer.pullSignal(waitTime))
if signal.n > 0 then
if not (seconds or filter) or filter == nil or filter(table.unpack(signal, 1, signal.n)) then
return table.unpack(signal, 1, signal.n)
end
end
until signal.n == 0
end
-- users may expect to find event.push to exist
event.push = computer.pushSignal
require("package").delay(event, "/lib/core/full_event.lua")
-------------------------------------------------------------------------------
return event

View File

@@ -0,0 +1,313 @@
local component = require("component")
local unicode = require("unicode")
local filesystem = {}
local mtab = {name="", children={}, links={}}
local fstab = {}
local function segments(path)
local parts = {}
for part in path:gmatch("[^\\/]+") do
local current, up = part:find("^%.?%.$")
if current then
if up == 2 then
table.remove(parts)
end
else
table.insert(parts, part)
end
end
return parts
end
local function findNode(path, create, resolve_links)
checkArg(1, path, "string")
local visited = {}
local parts = segments(path)
local ancestry = {}
local node = mtab
local index = 1
while index <= #parts do
local part = parts[index]
ancestry[index] = node
if not node.children[part] then
local link_path = node.links[part]
if link_path then
if not resolve_links and #parts == index then break end
if visited[path] then
return nil, string.format("link cycle detected '%s'", path)
end
-- the previous parts need to be conserved in case of future ../.. link cuts
visited[path] = index
local pst_path = "/" .. table.concat(parts, "/", index + 1)
local pre_path
if link_path:match("^[^/]") then
pre_path = table.concat(parts, "/", 1, index - 1) .. "/"
local link_parts = segments(link_path)
local join_parts = segments(pre_path .. link_path)
local back = (index - 1 + #link_parts) - #join_parts
index = index - back
node = ancestry[index]
else
pre_path = ""
index = 1
node = mtab
end
path = pre_path .. link_path .. pst_path
parts = segments(path)
part = nil -- skip node movement
elseif create then
node.children[part] = {name=part, parent=node, children={}, links={}}
else
break
end
end
if part then
node = node.children[part]
index = index + 1
end
end
local vnode, vrest = node, #parts >= index and table.concat(parts, "/", index)
local rest = vrest
while node and not node.fs do
rest = rest and filesystem.concat(node.name, rest) or node.name
node = node.parent
end
return node, rest, vnode, vrest
end
-------------------------------------------------------------------------------
function filesystem.canonical(path)
local result = table.concat(segments(path), "/")
if unicode.sub(path, 1, 1) == "/" then
return "/" .. result
else
return result
end
end
function filesystem.concat(...)
local set = table.pack(...)
for index, value in ipairs(set) do
checkArg(index, value, "string")
end
return filesystem.canonical(table.concat(set, "/"))
end
function filesystem.get(path)
local node = findNode(path)
if node.fs then
local proxy = node.fs
path = ""
while node and node.parent do
path = filesystem.concat(node.name, path)
node = node.parent
end
path = filesystem.canonical(path)
if path ~= "/" then
path = "/" .. path
end
return proxy, path
end
return nil, "no such file system"
end
function filesystem.realPath(path)
checkArg(1, path, "string")
local node, rest = findNode(path, false, true)
if not node then return nil, rest end
local parts = {rest or nil}
repeat
table.insert(parts, 1, node.name)
node = node.parent
until not node
return table.concat(parts, "/")
end
function filesystem.mount(fs, path)
checkArg(1, fs, "string", "table")
if type(fs) == "string" then
fs = filesystem.proxy(fs)
end
assert(type(fs) == "table", "bad argument #1 (file system proxy or address expected)")
checkArg(2, path, "string")
local real
if not mtab.fs then
if path == "/" then
real = path
else
return nil, "rootfs must be mounted first"
end
else
local why
real, why = filesystem.realPath(path)
if not real then
return nil, why
end
if filesystem.exists(real) and not filesystem.isDirectory(real) then
return nil, "mount point is not a directory"
end
end
local fsnode
if fstab[real] then
return nil, "another filesystem is already mounted here"
end
for _,node in pairs(fstab) do
if node.fs.address == fs.address then
fsnode = node
break
end
end
if not fsnode then
fsnode = select(3, findNode(real, true))
-- allow filesystems to intercept their own nodes
fs.fsnode = fsnode
else
local pwd = filesystem.path(real)
local parent = select(3, findNode(pwd, true))
local name = filesystem.name(real)
fsnode = setmetatable({name=name,parent=parent},{__index=fsnode})
parent.children[name] = fsnode
end
fsnode.fs = fs
fstab[real] = fsnode
return true
end
function filesystem.path(path)
local parts = segments(path)
local result = table.concat(parts, "/", 1, #parts - 1) .. "/"
if unicode.sub(path, 1, 1) == "/" and unicode.sub(result, 1, 1) ~= "/" then
return "/" .. result
else
return result
end
end
function filesystem.name(path)
checkArg(1, path, "string")
local parts = segments(path)
return parts[#parts]
end
function filesystem.proxy(filter, options)
checkArg(1, filter, "string")
if not component.list("filesystem")[filter] or next(options or {}) then
-- if not, load fs full library, it has a smarter proxy that also supports options
return filesystem.internal.proxy(filter, options)
end
return component.proxy(filter) -- it might be a perfect match
end
function filesystem.exists(path)
if not filesystem.realPath(filesystem.path(path)) then
return false
end
local node, rest, vnode, vrest = findNode(path)
if not vrest or vnode.links[vrest] then -- virtual directory or symbolic link
return true
elseif node and node.fs then
return node.fs.exists(rest)
end
return false
end
function filesystem.isDirectory(path)
local real, reason = filesystem.realPath(path)
if not real then return nil, reason end
local node, rest, vnode, vrest = findNode(real)
if not vnode.fs and not vrest then
return true -- virtual directory (mount point)
end
if node.fs then
return not rest or node.fs.isDirectory(rest)
end
return false
end
function filesystem.list(path)
local node, rest, vnode, vrest = findNode(path, false, true)
local result = {}
if node then
result = node.fs and node.fs.list(rest or "") or {}
-- `if not vrest` indicates that vnode reached the end of path
-- in other words, vnode[children, links] represent path
if not vrest then
for k,n in pairs(vnode.children) do
if not n.fs or fstab[filesystem.concat(path, k)] then
table.insert(result, k .. "/")
end
end
for k in pairs(vnode.links) do
table.insert(result, k)
end
end
end
local set = {}
for _,name in ipairs(result) do
set[filesystem.canonical(name)] = name
end
return function()
local key, value = next(set)
set[key or false] = nil
return value
end
end
function filesystem.open(path, mode)
checkArg(1, path, "string")
mode = tostring(mode or "r")
checkArg(2, mode, "string")
assert(({r=true, rb=true, w=true, wb=true, a=true, ab=true})[mode],
"bad argument #2 (r[b], w[b] or a[b] expected, got " .. mode .. ")")
local node, rest = findNode(path, false, true)
if not node then
return nil, rest
end
if not node.fs or not rest or (({r=true,rb=true})[mode] and not node.fs.exists(rest)) then
return nil, "file not found"
end
local handle, reason = node.fs.open(rest, mode)
if not handle then
return nil, reason
end
return setmetatable({
fs = node.fs,
handle = handle,
}, {__index = function(tbl, key)
if not tbl.fs[key] then return end
if not tbl.handle then
return nil, "file is closed"
end
return function(self, ...)
local h = self.handle
if key == "close" then
self.handle = nil
end
return self.fs[key](h, ...)
end
end})
end
filesystem.findNode = findNode
filesystem.segments = segments
filesystem.fstab = fstab
-------------------------------------------------------------------------------
return filesystem

View File

@@ -0,0 +1,130 @@
local buffer = require("buffer")
local component = require("component")
local internet = {}
-------------------------------------------------------------------------------
function internet.request(url, data, headers, method)
checkArg(1, url, "string")
checkArg(2, data, "string", "table", "nil")
checkArg(3, headers, "table", "nil")
checkArg(4, method, "string", "nil")
if not component.isAvailable("internet") then
error("no primary internet card found", 2)
end
local inet = component.internet
local post
if type(data) == "string" then
post = data
elseif type(data) == "table" then
for k, v in pairs(data) do
post = post and (post .. "&") or ""
post = post .. tostring(k) .. "=" .. tostring(v)
end
end
local request, reason = inet.request(url, post, headers, method)
if not request then
error(reason, 2)
end
return setmetatable(
{
["()"] = "function():string -- Tries to read data from the socket stream and return the read byte array.",
close = setmetatable({},
{
__call = request.close,
__tostring = function() return "function() -- closes the connection" end
})
},
{
__call = function()
while true do
local data, reason = request.read()
if not data then
request.close()
if reason then
error(reason, 2)
else
return nil -- eof
end
elseif #data > 0 then
return data
end
-- else: no data, block
os.sleep(0)
end
end,
__index = request,
})
end
-------------------------------------------------------------------------------
local socketStream = {}
function socketStream:close()
if self.socket then
self.socket.close()
self.socket = nil
end
end
function socketStream:seek()
return nil, "bad file descriptor"
end
function socketStream:read(n)
if not self.socket then
return nil, "connection is closed"
end
return self.socket.read(n)
end
function socketStream:write(value)
if not self.socket then
return nil, "connection is closed"
end
while #value > 0 do
local written, reason = self.socket.write(value)
if not written then
return nil, reason
end
value = string.sub(value, written + 1)
end
return true
end
function internet.socket(address, port)
checkArg(1, address, "string")
checkArg(2, port, "number", "nil")
if port then
address = address .. ":" .. port
end
local inet = component.internet
local socket, reason = inet.connect(address)
if not socket then
return nil, reason
end
local stream = {inet = inet, socket = socket}
local metatable = {__index = socketStream,
__metatable = "socketstream"}
return setmetatable(stream, metatable)
end
function internet.open(address, port)
local stream, reason = internet.socket(address, port)
if not stream then
return nil, reason
end
return buffer.new("rwb", stream)
end
-------------------------------------------------------------------------------
return internet

123
data/OpenOS/lib/io.lua Normal file
View File

@@ -0,0 +1,123 @@
local io = {}
-------------------------------------------------------------------------------
function io.close(file)
return (file or io.output()):close()
end
function io.flush()
return io.output():flush()
end
function io.lines(filename, ...)
if filename then
local file, reason = io.open(filename)
if not file then
error(reason, 2)
end
local args = table.pack(...)
return function()
local result = table.pack(file:read(table.unpack(args, 1, args.n)))
if not result[1] then
if result[2] then
error(result[2], 2)
else -- eof
file:close()
return nil
end
end
return table.unpack(result, 1, result.n)
end
else
return io.input():lines()
end
end
function io.open(path, mode)
-- These requires are not on top because this is a bootstrapped file.
local resolved_path = require("shell").resolve(path)
local stream, result = require("filesystem").open(resolved_path, mode)
if stream then
return require("buffer").new(mode, stream)
else
return nil, result
end
end
function io.stream(fd,file,mode)
checkArg(1,fd,'number')
checkArg(2, file, "table", "string", "nil")
assert(fd>=0,'fd must be >= 0. 0 is input, 1 is stdout, 2 is stderr')
local dio = require("process").info().data.io
if file then
if type(file) == "string" then
file = assert(io.open(file, mode))
end
dio[fd] = file
end
return dio[fd]
end
function io.input(file)
return io.stream(0, file, 'r')
end
function io.output(file)
return io.stream(1, file,'w')
end
function io.error(file)
return io.stream(2, file,'w')
end
function io.popen(prog, mode, env)
return require("pipe").popen(prog, mode, env)
end
function io.read(...)
return io.input():read(...)
end
function io.tmpfile()
local name = os.tmpname()
if name then
return io.open(name, "a")
end
end
function io.type(object)
if type(object) == "table" then
if getmetatable(object) == "file" then
if object.stream.handle then
return "file"
else
return "closed file"
end
end
end
return nil
end
function io.write(...)
return io.output():write(...)
end
local dup_mt = {__index = function(dfd, key)
local fd_value = dfd.fd[key]
if key ~= "close" and type(fd_value) ~= "function" then return fd_value end
return function(self, ...)
if key == "close" or self._closed then self._closed = true return end
return fd_value(self.fd, ...)
end
end, __newindex = function(dfd, key, value)
dfd.fd[key] = value
end}
function io.dup(fd)
return setmetatable({fd=fd,_closed=false}, dup_mt)
end
-------------------------------------------------------------------------------
return io

View File

@@ -0,0 +1,63 @@
local keyboard = {pressedChars = {}, pressedCodes = {}}
-- these key definitions are only a subset of all the defined keys
-- __index loads all key data from /lib/tools/keyboard_full.lua (only once)
-- new key metadata should be added here if required for boot
keyboard.keys = {
c = 0x2E,
d = 0x20,
q = 0x10,
w = 0x11,
back = 0x0E, -- backspace
delete = 0xD3,
down = 0xD0,
enter = 0x1C,
home = 0xC7,
lcontrol = 0x1D,
left = 0xCB,
lmenu = 0x38, -- left Alt
lshift = 0x2A,
pageDown = 0xD1,
rcontrol = 0x9D,
right = 0xCD,
rmenu = 0xB8, -- right Alt
rshift = 0x36,
space = 0x39,
tab = 0x0F,
up = 0xC8,
["end"] = 0xCF,
numpadenter = 0x9C,
}
-------------------------------------------------------------------------------
function keyboard.isAltDown()
return keyboard.pressedCodes[keyboard.keys.lmenu] or keyboard.pressedCodes[keyboard.keys.rmenu]
end
function keyboard.isControl(char)
return type(char) == "number" and (char < 0x20 or (char >= 0x7F and char <= 0x9F))
end
function keyboard.isControlDown()
return keyboard.pressedCodes[keyboard.keys.lcontrol] or keyboard.pressedCodes[keyboard.keys.rcontrol]
end
function keyboard.isKeyDown(charOrCode)
checkArg(1, charOrCode, "string", "number")
if type(charOrCode) == "string" then
return keyboard.pressedChars[utf8 and utf8.codepoint(charOrCode) or charOrCode:byte()]
elseif type(charOrCode) == "number" then
return keyboard.pressedCodes[charOrCode]
end
end
function keyboard.isShiftDown()
return keyboard.pressedCodes[keyboard.keys.lshift] or keyboard.pressedCodes[keyboard.keys.rshift]
end
-------------------------------------------------------------------------------
require("package").delay(keyboard.keys, "/lib/core/full_keyboard.lua")
return keyboard

126
data/OpenOS/lib/note.lua Normal file
View File

@@ -0,0 +1,126 @@
--Provides all music notes in range of computer.beep in MIDI and frequency form
--Author: Vexatos
local computer = require("computer")
local note = {}
--The table that maps note names to their respective MIDI codes
local notes = {}
--The reversed table "notes"
local reverseNotes = {}
do
--All the base notes
local tempNotes = {
"c",
"c#",
"d",
"d#",
"e",
"f",
"f#",
"g",
"g#",
"a",
"a#",
"b"
}
--The table containing all the standard notes and # semitones in correct order, temporarily
local sNotes = {}
--The table containing all the b semitones
local bNotes = {}
--Registers all possible notes in order
do
table.insert(sNotes,"a0")
table.insert(sNotes,"a#0")
table.insert(bNotes,"bb0")
table.insert(sNotes,"b0")
for i = 1,6 do
for _,v in ipairs(tempNotes) do
table.insert(sNotes,v..tostring(i))
if #v == 1 and v ~= "c" and v ~= "f" then
table.insert(bNotes,v.."b"..tostring(i))
end
end
end
end
for i=21,95 do
notes[sNotes[i-20]]=tostring(i)
end
--Reversing the whole table in reverseNotes, used for note.get
do
for k,v in pairs(notes) do
reverseNotes[tonumber(v)]=k
end
end
--This is registered after reverseNotes to avoid conflicts
for k,v in ipairs(bNotes) do
notes[v]=tostring(notes[string.gsub(v,"(.)b(.)","%1%2")]-1)
end
end
--Converts string or frequency into MIDI code
function note.midi(n)
if type(n) == "string" then
n = string.lower(n)
if tonumber(notes[n])~=nil then
return tonumber(notes[n])
else
error("Wrong input "..tostring(n).." given to note.midi, needs to be <note>[semitone sign]<octave>, e.g. A#0 or Gb4")
end
elseif type(n) == "number" then
return math.floor((12*math.log(n/440,2))+69)
else
error("Wrong input "..tostring(n).." given to note.midi, needs to be a number or a string")
end
end
--Converts String or MIDI code into frequency
function note.freq(n)
if type(n) == "string" then
n = string.lower(n)
if tonumber(notes[n])~=nil then
return math.pow(2,(tonumber(notes[n])-69)/12)*440
else
error("Wrong input "..tostring(n).." given to note.freq, needs to be <note>[semitone sign]<octave>, e.g. A#0 or Gb4",2)
end
elseif type(n) == "number" then
return math.pow(2,(n-69)/12)*440
else
error("Wrong input "..tostring(n).." given to note.freq, needs to be a number or a string",2)
end
end
--Converts a MIDI value back into a string
function note.name(n)
n = tonumber(n)
if reverseNotes[n] then
return string.upper(string.match(reverseNotes[n],"^(.)"))..string.gsub(reverseNotes[n],"^.(.*)","%1")
else
error("Attempt to get a note for a non-exsisting MIDI code",2)
end
end
--Converts Note block ticks (0-24) to MIDI code (34-58) and vice-versa
function note.ticks(n)
if type(n) == "number" then
if n>=0 and n<=24 then
return n+34
elseif n>=34 and n<=58 then
return n-34
else
error("Wrong input "..tostring(n).." given to note.ticks, needs to be a number [0-24 or 34-58]",2)
end
else
error("Wrong input "..tostring(n).." given to note.ticks, needs to be a number",2)
end
end
--Plays a tone, input is either the note as a string or the MIDI code as well as the duration of the tone
function note.play(tone,duration)
computer.beep(note.freq(tone),duration)
end
return note

119
data/OpenOS/lib/package.lua Normal file
View File

@@ -0,0 +1,119 @@
local package = {}
package.config = "/\n;\n?\n!\n-\n"
package.path = "/lib/?.lua;/usr/lib/?.lua;/home/lib/?.lua;./?.lua;/lib/?/init.lua;/usr/lib/?/init.lua;/home/lib/?/init.lua;./?/init.lua"
local loading = {}
local preload = {}
local searchers = {}
local loaded = {
["_G"] = _G,
["bit32"] = bit32,
["coroutine"] = coroutine,
["math"] = math,
["os"] = os,
["package"] = package,
["string"] = string,
["table"] = table
}
package.loaded = loaded
package.preload = preload
package.searchers = searchers
function package.searchpath(name, path, sep, rep)
checkArg(1, name, "string")
checkArg(2, path, "string")
sep = sep or '.'
rep = rep or '/'
sep, rep = '%' .. sep, rep
name = string.gsub(name, sep, rep)
local fs = require("filesystem")
local errorFiles = {}
for subPath in string.gmatch(path, "([^;]+)") do
subPath = string.gsub(subPath, "?", name)
if subPath:sub(1, 1) ~= "/" and os.getenv then
subPath = fs.concat(os.getenv("PWD") or "/", subPath)
end
if fs.exists(subPath) then
local file = fs.open(subPath, "r")
if file then
file:close()
return subPath
end
end
table.insert(errorFiles, "no file '" .. subPath .. "'")
end
return nil, table.concat(errorFiles, "\n\t")
end
table.insert(searchers, function(module)
if package.preload[module] then
return package.preload[module]
end
return "no field package.preload['" .. module .. "']"
end)
table.insert(searchers, function(module)
local library, path, status
path, status = package.searchpath(module, package.path)
if not path then
return status
end
library, status = loadfile(path)
if not library then
error(string.format("error loading module '%s' from file '%s':\n\t%s", module, path, status))
end
return library, module
end)
function require(module)
checkArg(1, module, "string")
if loaded[module] ~= nil then
return loaded[module]
elseif loading[module] then
error("already loading: " .. module .. "\n" .. debug.traceback(), 2)
else
local library, status, arg
local errors = ""
if type(searchers) ~= "table" then error("'package.searchers' must be a table") end
for _, searcher in pairs(searchers) do
library, arg = searcher(module)
if type(library) == "function" then break end
if type(library) ~= nil then
errors = errors .. "\n\t" .. tostring(library)
library = nil
end
end
if not library then error(string.format("module '%s' not found:%s", module, errors)) end
loading[module] = true
library, status = pcall(library, arg or module)
loading[module] = false
assert(library, string.format("module '%s' load failed:\n%s", module, status))
loaded[module] = status
return status
end
end
function package.delay(lib, file)
local mt = {}
function mt.__index(tbl, key)
mt.__index = nil
dofile(file)
return tbl[key]
end
if lib.internal then
setmetatable(lib.internal, mt)
end
setmetatable(lib, mt)
end
-------------------------------------------------------------------------------
return package

230
data/OpenOS/lib/pipe.lua Normal file
View File

@@ -0,0 +1,230 @@
local process = require("process")
local shell = require("shell")
local buffer = require("buffer")
local command_result_as_code = require("sh").internal.command_result_as_code
local pipe = {}
local _root_co = assert(process.info(), "process metadata failed to load").data.coroutine_handler
-- root can be a coroutine or a function
function pipe.createCoroutineStack(root, env, name)
checkArg(1, root, "thread", "function")
if type(root) == "function" then
root = assert(process.load(root, env, nil, name or "pipe"), "failed to load proc data for given function")
end
local proc = assert(process.list[root], "coroutine must be a process thread else the parent process is corrupted")
local pco = setmetatable({root=root}, {__index=_root_co})
proc.data.coroutine_handler = pco
function pco.yield(...)
return _root_co.yield(nil, ...)
end
function pco.yield_past(co, ...)
return _root_co.yield(co, ...)
end
function pco.resume(co, ...)
checkArg(1, co, "thread")
local args = table.pack(...)
while true do -- for consecutive sysyields
local result = table.pack(_root_co.resume(co, table.unpack(args, 1, args.n)))
local target = result[2] == true and pco.root or result[2]
if not result[1] or _root_co.status(co) == "dead" then
return table.unpack(result, 1, result.n)
elseif target and target ~= co then
args = table.pack(_root_co.yield(table.unpack(result, 2, result.n)))
else
return true, table.unpack(result, 3, result.n)
end
end
end
return pco
end
local pipe_stream =
{
continue = function(self, exit)
local result = table.pack(coroutine.resume(self.next))
while true do -- repeat resumes if B (A|B) makes a natural yield
-- if B crashed or closed in the last resume
-- then we can close the stream
if coroutine.status(self.next) == "dead" then
self:close()
-- always cause os.exit when the pipe closes
-- this is very important
-- e.g. cat very_large_file | head
-- when head is done, cat should stop
result[1] = nil
end
-- the pipe closed or crashed
if not result[1] then
if exit then
os.exit(command_result_as_code(result[2]))
end
return self
end
-- next is suspended, read_mode indicates why
if self.read_mode then
-- B wants A to write again, resume A
return self
end
-- not reading, it is requesting a yield
-- yield_past(true) will exit this coroutine stack
result = table.pack(coroutine.yield_past(true, table.unpack(result, 2, result.n)))
result = table.pack(coroutine.resume(self.next, table.unpack(result, 1, result.n))) -- the request was for an event
end
end,
close = function(self)
self.closed = true
if coroutine.status(self.next) == "suspended" then
self:continue()
end
end,
seek = function()
return nil, "bad file descriptor"
end,
write = function(self, value)
if self.closed then
-- if next is dead, ignore all writes
if coroutine.status(self.next) ~= "dead" then
io.stderr:write("attempt to use a closed stream\n")
os.exit(1)
end
else
self.buffer = self.buffer .. value
return self:continue(true)
end
os.exit(0) -- abort the current process: SIGPIPE
end,
read = function(self, n)
if self.closed then
return nil -- eof
end
if self.buffer == "" then
-- the pipe_stream write resume is waiting on this process B (A|B) to yield
-- yield here requests A to output again. However, B may elsewhere want a
-- natural yield (i.e. for events). To differentiate this yield from natural
-- yields we set read_mode here, which the pipe_stream write detects
self.read_mode = true
coroutine.yield_past(self.next) -- next is the first croutine in this stack
self.read_mode = false
end
local result = string.sub(self.buffer, 1, n)
self.buffer = string.sub(self.buffer, n + 1)
return result
end
}
-- prog1 | prog2 | ... | progn
function pipe.buildPipeChain(progs)
local chain = {}
local prev_piped_stream
for i=1,#progs do
local thread = progs[i]
-- A needs to be a stack in case any thread in A call write and then B natural yields
-- B needs to be a stack in case any thread in B calls read
pipe.createCoroutineStack(thread)
chain[i] = thread
local proc = process.info(thread)
local pio = proc.data.io
local piped_stream
if i < #progs then
local handle = setmetatable({buffer = ""}, {__index = pipe_stream})
process.addHandle(handle, proc)
piped_stream = buffer.new("rw", handle)
piped_stream:setvbuf("no", 1024)
pio[1] = piped_stream
end
if prev_piped_stream then
prev_piped_stream.stream.next = thread
pio[0] = prev_piped_stream
end
prev_piped_stream = piped_stream
end
return chain
end
local chain_stream =
{
read = function(self, value, ...)
if self.io_stream.closed then return nil end
-- wake up prog
self.ready = false -- the pipe proc sets this true when ios completes
local ret = table.pack(coroutine.resume(self.pco.root, value, ...))
if coroutine.status(self.pco.root) == "dead" then
return nil
elseif not ret[1] then
return table.unpack(ret, 1, ret.n)
end
if not self.ready then
-- prog yielded back without writing/reading
return self:read(coroutine.yield())
end
return ret[2]
end,
write = function(self, ...)
return self:read(...)
end,
close = function(self)
self.io_stream:close()
end,
}
function pipe.popen(prog, mode, env)
mode = mode or "r"
if mode ~= "r" and mode ~= "w" then
return nil, "bad argument #2: invalid mode " .. tostring(mode) .. " must be r or w"
end
local r = mode == "r"
local chain = {}
-- to simplify the code - shell.execute is run within a function to pass (prog, env)
-- if cmd_proc were to come second (mode=="w") then the pipe_proc would have to pass
-- the starting args. which is possible, just more complicated
local cmd_proc = process.load(function() return shell.execute(prog, env) end, nil, nil, prog)
-- the chain stream is the popen controller
local stream = setmetatable({}, { __index = chain_stream })
-- the stream needs its own process for io
local pipe_proc = process.load(function()
local n = r and 0 or ""
local key = r and "read" or "write"
local ios = stream.io_stream
while not ios.closed do
-- read from pipe
local ret = table.pack(ios[key](ios, n))
stream.ready = true
-- yield outside the chain now
n = coroutine.yield_past(chain[1], table.unpack(ret, 1, ret.n))
end
end, nil, nil, "pipe_handler")
chain[r and 1 or 2] = cmd_proc
chain[r and 2 or 1] = pipe_proc
-- link the cmd and pipe proc io
pipe.buildPipeChain(chain)
local cmd_data = process.info(chain[1]).data
local cmd_stack = cmd_data.coroutine_handler
-- store handle to io_stream from easy access later
stream.io_stream = cmd_data.io[1].stream
stream.pco = cmd_stack
-- popen commands start out running, like threads
cmd_stack.resume(cmd_stack.root)
local buffered_stream = buffer.new(mode, stream)
buffered_stream:setvbuf("no", 1024)
return buffered_stream
end
return pipe

195
data/OpenOS/lib/process.lua Normal file
View File

@@ -0,0 +1,195 @@
local process = {}
-------------------------------------------------------------------------------
--Initialize coroutine library--
process.list = setmetatable({}, {__mode="k"})
function process.findProcess(co)
co = co or coroutine.running()
for main, p in pairs(process.list) do
if main == co then
return p
end
for _, instance in pairs(p.instances) do
if instance == co then
return p
end
end
end
end
-------------------------------------------------------------------------------
function process.load(path, env, init, name)
checkArg(1, path, "string", "function")
checkArg(2, env, "table", "nil")
checkArg(3, init, "function", "nil")
checkArg(4, name, "string", "nil")
assert(type(path) == "string" or env == nil, "process cannot load function environments")
local p = process.findProcess()
env = env or p.env
local code
if type(path) == "string" then
code = function(...)
local fs, shell = require("filesystem"), require("shell")
local program, reason = shell.resolve(path, "lua")
if not program then
return require("tools/programLocations").reportNotFound(path, reason)
end
os.setenv("_", program)
local f = fs.open(program)
if f then
local shebang = (f:read(1024) or ""):match("^#!([^\n]+)")
f:close()
if shebang then
path = shebang:gsub("%s","")
return code(program, ...)
end
end
-- local command
return assert(loadfile(program, "bt", env))(...)
end
else -- path is code
code = path
end
local thread = nil
thread = coroutine.create(function(...)
-- pcall code so that we can remove it from the process list on exit
local result =
{
xpcall(function(...)
init = init or function(...) return ... end
return code(init(...))
end,
function(msg)
if type(msg) == "table" and msg.reason == "terminated" then
return msg.code or 0
end
return {msg, debug.traceback()}
end, ...)
}
if not result[1] and type(result[2]) == "table" then
-- run exception handler
xpcall(function()
local stack = result[2][2]:gsub("^([^\n]*\n)[^\n]*\n[^\n]*\n","%1")
io.stderr:write(string.format("%s:\n%s", result[2][1] or "", stack))
end,
function(msg)
io.stderr:write("process library exception handler crashed: ", tostring(msg))
end)
result[2] = 128
end
-- onError opens a file, you can't open a file without a process, we close the process last
process.internal.close(thread, result)
return select(2, table.unpack(result))
end, true)
local new_proc =
{
path = path,
command = name or tostring(path),
env = env,
data =
{
handles = {},
io = {},
},
parent = p,
instances = setmetatable({}, {__mode="v"}),
}
for i,fd in pairs(p.data.io) do
new_proc.data.io[i] = io.dup(fd)
end
setmetatable(new_proc.data, {__index=p.data})
process.list[thread] = new_proc
return thread
end
function process.info(levelOrThread)
checkArg(1, levelOrThread, "thread", "number", "nil")
local p
if type(levelOrThread) == "thread" then
p = process.findProcess(levelOrThread)
else
local level = levelOrThread or 1
p = process.findProcess()
while level > 1 and p do
p = p.parent
level = level - 1
end
end
if p then
return {path=p.path, env=p.env, command=p.command, data=p.data}
end
end
--table of undocumented api subject to change and intended for internal use
process.internal = {}
--this is a future stub for a more complete method to kill a process
function process.internal.close(thread, result)
checkArg(1,thread,"thread")
local pdata = process.info(thread).data
pdata.result = result
while pdata.handles[1] do
local h = table.remove(pdata.handles)
if h.close then
pcall(h.close, h)
end
end
process.list[thread] = nil
end
function process.internal.continue(co, ...)
local result = {}
-- Emulate CC behavior by making yields a filtered event.pull()
local args = table.pack(...)
while coroutine.status(co) ~= "dead" do
result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
if coroutine.status(co) ~= "dead" then
args = table.pack(coroutine.yield(table.unpack(result, 2, result.n)))
elseif not result[1] then
io.stderr:write(result[2])
end
end
return table.unpack(result, 2, result.n)
end
function process.removeHandle(handle, proc)
local handles = (proc or process.info()).data.handles
for pos, h in ipairs(handles) do
if h == handle then
return table.remove(handles, pos)
end
end
end
function process.addHandle(handle, proc)
local _close = handle.close
local handles = (proc or process.info()).data.handles
table.insert(handles, handle)
function handle:close(...)
if _close then
self.close = _close
_close = nil
process.removeHandle(self, proc)
return self:close(...)
end
end
return handle
end
function process.running(level) -- kept for backwards compat, prefer process.info
local info = process.info(level)
if info then
return info.path, info.env, info.command
end
end
return process

7
data/OpenOS/lib/rc.lua Normal file
View File

@@ -0,0 +1,7 @@
-- Keeps track of loaded scripts to retain local values between invocation
-- of their command callbacks.
local rc = {}
rc.loaded = {}
return rc

View File

@@ -0,0 +1,145 @@
local serialization = {}
-- delay loaded tables fail to deserialize cross [C] boundaries (such as when having to read files that cause yields)
local local_pairs = function(tbl)
local mt = getmetatable(tbl)
return (mt and mt.__pairs or pairs)(tbl)
end
-- Important: pretty formatting will allow presenting non-serializable values
-- but may generate output that cannot be unserialized back.
function serialization.serialize(value, pretty)
local kw = {["and"]=true, ["break"]=true, ["do"]=true, ["else"]=true,
["elseif"]=true, ["end"]=true, ["false"]=true, ["for"]=true,
["function"]=true, ["goto"]=true, ["if"]=true, ["in"]=true,
["local"]=true, ["nil"]=true, ["not"]=true, ["or"]=true,
["repeat"]=true, ["return"]=true, ["then"]=true, ["true"]=true,
["until"]=true, ["while"]=true}
local id = "^[%a_][%w_]*$"
local ts = {}
local result_pack = {}
local function recurse(current_value, depth)
local t = type(current_value)
if t == "number" then
if current_value ~= current_value then
table.insert(result_pack, "0/0")
elseif current_value == math.huge then
table.insert(result_pack, "math.huge")
elseif current_value == -math.huge then
table.insert(result_pack, "-math.huge")
else
table.insert(result_pack, tostring(current_value))
end
elseif t == "string" then
table.insert(result_pack, (string.format("%q", current_value):gsub("\\\n","\\n")))
elseif
t == "nil" or
t == "boolean" or
pretty and (t ~= "table" or (getmetatable(current_value) or {}).__tostring) then
table.insert(result_pack, tostring(current_value))
elseif t == "table" then
if ts[current_value] then
if pretty then
table.insert(result_pack, "recursion")
return
else
error("tables with cycles are not supported")
end
end
ts[current_value] = true
local f
if pretty then
local ks, sks, oks = {}, {}, {}
for k in local_pairs(current_value) do
if type(k) == "number" then
table.insert(ks, k)
elseif type(k) == "string" then
table.insert(sks, k)
else
table.insert(oks, k)
end
end
table.sort(ks)
table.sort(sks)
for _, k in ipairs(sks) do
table.insert(ks, k)
end
for _, k in ipairs(oks) do
table.insert(ks, k)
end
local n = 0
f = table.pack(function()
n = n + 1
local k = ks[n]
if k ~= nil then
return k, current_value[k]
else
return nil
end
end)
else
f = table.pack(local_pairs(current_value))
end
local i = 1
local first = true
table.insert(result_pack, "{")
for k, v in table.unpack(f) do
if not first then
table.insert(result_pack, ",")
if pretty then
table.insert(result_pack, "\n" .. string.rep(" ", depth))
end
end
first = nil
local tk = type(k)
if tk == "number" and k == i then
i = i + 1
recurse(v, depth + 1)
else
if tk == "string" and not kw[k] and string.match(k, id) then
table.insert(result_pack, k)
else
table.insert(result_pack, "[")
recurse(k, depth + 1)
table.insert(result_pack, "]")
end
table.insert(result_pack, "=")
recurse(v, depth + 1)
end
end
ts[current_value] = nil -- allow writing same table more than once
table.insert(result_pack, "}")
else
error("unsupported type: " .. t)
end
end
recurse(value, 1)
local result = table.concat(result_pack)
if pretty then
local limit = type(pretty) == "number" and pretty or 10
local truncate = 0
while limit > 0 and truncate do
truncate = string.find(result, "\n", truncate + 1, true)
limit = limit - 1
end
if truncate then
return result:sub(1, truncate) .. "..."
end
end
return result
end
function serialization.unserialize(data)
checkArg(1, data, "string")
local result, reason = load("return " .. data, "=data", nil, {math={huge=math.huge}})
if not result then
return nil, reason
end
local ok, output = pcall(result)
if not ok then
return nil, output
end
return output
end
return serialization

222
data/OpenOS/lib/sh.lua Normal file
View File

@@ -0,0 +1,222 @@
local process = require("process")
local shell = require("shell")
local text = require("text")
local tx = require("transforms")
local sh = {}
sh.internal = {}
function sh.internal.isWordOf(w, vs)
return w and #w == 1 and not w[1].qr and tx.first(vs,{{w[1].txt}}) ~= nil
end
local isWordOf = sh.internal.isWordOf
-------------------------------------------------------------------------------
--SH API
sh.internal.ec = {}
sh.internal.ec.parseCommand = 127
sh.internal.ec.last = 0
function sh.getLastExitCode()
return sh.internal.ec.last
end
function sh.internal.command_result_as_code(ec, reason)
-- convert lua result to bash ec
local code
if ec == false then
code = 1
elseif ec == nil or ec == true then
code = 0
elseif type(ec) ~= "number" then
code = 2 -- illegal number
else
code = ec
end
if reason and code ~= 0 then io.stderr:write(reason, "\n") end
return code
end
function sh.internal.resolveActions(input, resolved)
resolved = resolved or {}
local processed = {}
local prev_was_delim = true
local words, reason = text.internal.tokenize(input)
if not words then
return nil, reason
end
while #words > 0 do
local next = table.remove(words,1)
if isWordOf(next, {";","&&","||","|"}) then
prev_was_delim = true
resolved = {}
elseif prev_was_delim then
prev_was_delim = false
-- if current is actionable, resolve, else pop until delim
if next and #next == 1 and not next[1].qr then
local key = next[1].txt
if key == "!" then
prev_was_delim = true -- special redo
elseif not resolved[key] then
resolved[key] = shell.getAlias(key)
local value = resolved[key]
if value and key ~= value then
local replacement_tokens, resolve_reason = sh.internal.resolveActions(value, resolved)
if not replacement_tokens then
return replacement_tokens, resolve_reason
end
words = tx.concat(replacement_tokens, words)
next = table.remove(words,1)
end
end
end
end
table.insert(processed, next)
end
return processed
end
-- returns true if key is a string that represents a valid command line identifier
function sh.internal.isIdentifier(key)
if type(key) ~= "string" then
return false
end
return key:match("^[%a_][%w_]*$") == key
end
-- expand (interpret) a single quoted area
-- examples: $foo or "$foo"
function sh.expand(value)
local expanded = value
:gsub("%$([_%w%?]+)", function(key)
if key == "?" then
return tostring(sh.getLastExitCode())
end
return os.getenv(key) or ''
end)
:gsub("%${(.*)}", function(key)
if sh.internal.isIdentifier(key) then
return os.getenv(key) or ''
end
io.stderr:write("${" .. key .. "}: bad substitution\n")
os.exit(1)
end)
return expanded
end
function sh.internal.createThreads(commands, env, start_args)
-- Piping data between programs works like so:
-- program1 gets its output replaced with our custom stream.
-- program2 gets its input replaced with our custom stream.
-- repeat for all programs
-- custom stream triggers execution of "next" program after write.
-- custom stream triggers yield before read if buffer is empty.
-- custom stream may have "redirect" entries for fallback/duplication.
local threads = {}
for i = 1, #commands do
local command = commands[i]
local program, args, redirects = table.unpack(command)
local name = tostring(program)
local thread_env = type(program) == "string" and env or nil
local thread, reason = process.load(program or "/dev/null", thread_env, function(...)
if redirects then
sh.internal.openCommandRedirects(redirects)
end
args = tx.concat(args, start_args[i] or {}, table.pack(...))
-- popen expects each process to first write an empty string
-- this is required for proper thread order
io.write("")
return table.unpack(args, 1, args.n or #args)
end, name)
if not thread then
for _,t in ipairs(threads) do
process.internal.close(t)
end
return nil, reason
end
threads[i] = thread
end
if #threads > 1 then
require("pipe").buildPipeChain(threads)
end
return threads
end
function sh.internal.executePipes(pipe_parts, eargs, env)
local commands = {}
for _,words in ipairs(pipe_parts) do
local args = {}
local reparse
for _,word in ipairs(words) do
local value = ""
for _,part in ipairs(word) do
reparse = reparse or part.qr or part.txt:find("[%$%*%?<>]")
value = value .. part.txt
end
args[#args + 1] = value
end
local redirects
if reparse then
args, redirects = sh.internal.evaluate(words)
if not args then
return false, redirects -- in this failure case, redirects has the error message
end
end
commands[#commands + 1] = table.pack(table.remove(args, 1), args, redirects)
end
local threads, reason = sh.internal.createThreads(commands, env, {[#commands]=eargs})
if not threads then return false, reason end
return process.internal.continue(threads[1])
end
function sh.execute(env, command, ...)
checkArg(2, command, "string")
if command:find("^%s*#") then return true, 0 end
local words, reason = sh.internal.resolveActions(command)
if type(words) ~= "table" then
return words, reason
elseif #words == 0 then
return true
end
-- MUST be table.pack for non contiguous ...
local eargs = table.pack(...)
-- simple
if not command:find("[;%$&|!<>]") then
sh.internal.ec.last = sh.internal.command_result_as_code(sh.internal.executePipes({words}, eargs, env))
return sh.internal.ec.last == 0
end
return sh.internal.execute_complex(words, eargs, env)
end
function sh.hintHandler(full_line, cursor)
return sh.internal.hintHandlerImpl(full_line, cursor)
end
require("package").delay(sh, "/lib/core/full_sh.lua")
return sh

144
data/OpenOS/lib/shell.lua Normal file
View File

@@ -0,0 +1,144 @@
local fs = require("filesystem")
local unicode = require("unicode")
local process = require("process")
local shell = {}
-- Cache loaded shells for command execution. This puts the requirement on
-- shells that they do not keep a global state, since they may be called
-- multiple times, but reduces memory usage a lot.
local shells = setmetatable({}, {__mode="v"})
function shell.getShell()
local shellPath = os.getenv("SHELL") or "/bin/sh"
local shellName, reason = shell.resolve(shellPath, "lua")
if not shellName then
return nil, "cannot resolve shell `" .. shellPath .. "': " .. reason
end
if shells[shellName] then
return shells[shellName]
end
local sh, load_reason = loadfile(shellName, nil, setmetatable({}, {__index=_G}))
if sh then
shells[shellName] = sh
end
return sh, load_reason
end
-------------------------------------------------------------------------------
function shell.prime()
local data = process.info().data
for _,key in ipairs({'aliases','vars'}) do
-- first time get need to populate
local raw = rawget(data, key)
if not raw then
-- current process does not have the key
local current = data[key]
data[key] = {}
if current then
for k,v in pairs(current) do
data[key][k] = v
end
end
end
end
end
function shell.getAlias(alias)
return process.info().data.aliases[alias]
end
function shell.setAlias(alias, value)
checkArg(1, alias, "string")
checkArg(2, value, "string", "nil")
process.info().data.aliases[alias] = value
end
function shell.getWorkingDirectory()
-- if no env PWD default to /
return os.getenv("PWD") or "/"
end
function shell.setWorkingDirectory(dir)
checkArg(1, dir, "string")
-- ensure at least /
-- and remove trailing /
dir = fs.canonical(dir):gsub("^$", "/"):gsub("(.)/$", "%1")
if fs.isDirectory(dir) then
os.setenv("PWD", dir)
return true
else
return nil, "not a directory"
end
end
function shell.resolve(path, ext)
checkArg(1, path, "string")
local dir = path
if dir:find("/") ~= 1 then
dir = fs.concat(shell.getWorkingDirectory(), dir)
end
local name = fs.name(path)
dir = fs[name and "path" or "canonical"](dir)
local fullname = fs.concat(dir, name or "")
if not ext then
return fullname
elseif name then
checkArg(2, ext, "string")
-- search for name in PATH if no dir was given
-- no dir was given if path has no /
local search_in = path:find("/") and dir or os.getenv("PATH")
for search_path in string.gmatch(search_in, "[^:]+") do
-- resolve search_path because they may be relative
local search_name = fs.concat(shell.resolve(search_path), name)
if not fs.exists(search_name) then
search_name = search_name .. "." .. ext
end
-- extensions are provided when the caller is looking for a file
if fs.exists(search_name) and not fs.isDirectory(search_name) then
return search_name
end
end
end
return nil, "file not found"
end
function shell.parse(...)
local params = table.pack(...)
local args = {}
local options = {}
local doneWithOptions = false
for i = 1, params.n do
local param = params[i]
if not doneWithOptions and type(param) == "string" then
if param == "--" then
doneWithOptions = true -- stop processing options at `--`
elseif param:sub(1, 2) == "--" then
local key, value = param:match("%-%-(.-)=(.*)")
if not key then
key, value = param:sub(3), true
end
options[key] = value
elseif param:sub(1, 1) == "-" and param ~= "-" then
for j = 2, unicode.len(param) do
options[unicode.sub(param, j, j)] = true
end
else
table.insert(args, param)
end
else
table.insert(args, param)
end
end
return args, options
end
-------------------------------------------------------------------------------
require("package").delay(shell, "/lib/core/full_shell.lua")
return shell

62
data/OpenOS/lib/sides.lua Normal file
View File

@@ -0,0 +1,62 @@
local sides = {
[0] = "bottom",
[1] = "top",
[2] = "back",
[3] = "front",
[4] = "right",
[5] = "left",
[6] = "unknown",
bottom = 0,
top = 1,
back = 2,
front = 3,
right = 4,
left = 5,
unknown = 6,
down = 0,
up = 1,
north = 2,
south = 3,
west = 4,
east = 5,
negy = 0,
posy = 1,
negz = 2,
posz = 3,
negx = 4,
posx = 5,
forward = 3
}
local metatable = getmetatable(sides) or {}
-- sides[0..5] are mapped to itertable[1..6].
local itertable = {
sides[0],
sides[1],
sides[2],
sides[3],
sides[4],
sides[5]
}
-- Future-proofing against the possible introduction of additional
-- logical sides (e.g. [7] = "all", [8] = "none", etc.).
function metatable.__len(sides)
return #itertable
end
-- Allow `sides` to be iterated over like a normal (1-based) array.
function metatable.__ipairs(sides)
return ipairs(itertable)
end
setmetatable(sides, metatable)
-------------------------------------------------------------------------------
return sides

172
data/OpenOS/lib/term.lua Normal file
View File

@@ -0,0 +1,172 @@
local tty = require("tty")
local computer = require("computer")
local process = require("process")
local event = require("event")
local core_cursor = require("core/cursor")
local kb = require("keyboard")
local keys = kb.keys
local term = setmetatable({internal={}}, {__index=tty})
local function as_window(window, func, ...)
local data = process.info().data
if not data.window or not window then
return func(...)
end
local prev = rawget(data, "window")
data.window = window
local ret = table.pack(func(...))
data.window = prev
return table.unpack(ret, 1, ret.n)
end
function term.internal.open(...)
local dx, dy, w, h = ...
local window = {fullscreen=select("#",...) == 0, blink = true, output_buffer = ""}
-- support legacy code using direct manipulation of w and h
-- (e.g. wocchat) instead of using setViewport
setmetatable(window,
{
__index = function(tbl, key)
key = key == "w" and "width" or key == "h" and "height" or key
return rawget(tbl, key)
end,
__newindex = function(tbl, key, value)
key = key == "w" and "width" or key == "h" and "height" or key
return rawset(tbl, key, value)
end
})
-- first time we open a pty the current tty.window must become the process window
if rawget(tty, "window") then
for _,p in pairs(process.list) do
if not p.parent then
p.data.window = tty.window
break
end
end
tty.window = nil
setmetatable(tty,
{
__index = function(_, key)
if key == "window" then
return process.info().data.window
end
end
})
end
as_window(window, tty.setViewport, w, h, dx, dy, 1, 1)
as_window(window, tty.bind, tty.gpu())
return window
end
local function create_cursor(history, ops)
local cursor = history or {}
cursor.hint = ops.hint or cursor.hint
local filter = ops.filter or cursor.filter
if filter then
if type(filter) == "string" then
local filter_text = filter
filter = function(text)
return text:match(filter_text)
end
end
function cursor:handle(name, char, code)
if name == "key_down" and (code == keys.enter or code == keys.numpadenter) then
if not filter(self.data) then
computer.beep(2000, 0.1)
return true -- handled
end
end
return self.super.handle(self, name, char, code)
end
end
local pwchar = ops.pwchar or cursor.pwchar
local nobreak = ops.dobreak == false or cursor.dobreak == false
if pwchar or nobreak then
if type(pwchar) == "string" then
local pwchar_text = pwchar
pwchar = function(text)
return text:gsub(".", pwchar_text)
end
end
function cursor:echo(arg, ...)
if pwchar and type(arg) == "string" and #arg > 0 and not arg:match("^\27") then -- "" is used for scrolling
arg = pwchar(arg)
elseif nobreak and arg == "\n" then
arg = ""
end
return self.super.echo(self, arg, ...)
end
end
return core_cursor.new(cursor, cursor.nowrap and core_cursor.horizontal)
end
-- cannot use term.write = io.write because io.write invokes metatable
function term.write(value, wrap)
io.stdout:flush()
local previous_nowrap = tty.window.nowrap
tty.window.nowrap = wrap == false
io.write(value)
io.stdout:flush()
tty.window.nowrap = previous_nowrap
end
function term.read(history, dobreak, hint, pwchar, filter)
tty.window.cursor = create_cursor(history, {
dobreak = dobreak,
pwchar = pwchar,
filter = filter,
hint = hint
})
return io.stdin:readLine(false)
end
function term.getGlobalArea()
local w,h,dx,dy = tty.getViewport()
return dx+1,dy+1,w,h
end
function term.clearLine()
term.write("\27[2K\27[999D")
end
function term.setCursorBlink(enabled)
tty.window.blink = enabled
end
function term.getCursorBlink()
return tty.window.blink
end
function term.pull(...)
local args = table.pack(...)
local timeout = math.huge
if type(args[1]) == "number" then
timeout = computer.uptime() + table.remove(args, 1)
args.n = args.n - 1
end
local cursor = core_cursor.new()
while timeout >= computer.uptime() do
cursor:echo()
local s = table.pack(event.pull(.5, table.unpack(args, 1, args.n)))
cursor:echo(not s[1])
if s.n > 1 then return table.unpack(s, 1, s.n) end
end
end
function term.bind(gpu, window)
return as_window(window, tty.bind, gpu)
end
term.scroll = tty.stream.scroll
term.internal.run_in_window = as_window
return term

109
data/OpenOS/lib/text.lua Normal file
View File

@@ -0,0 +1,109 @@
local unicode = require("unicode")
local tx = require("transforms")
local text = {}
text.internal = {}
text.syntax = {"^%d?>>?&%d+","^%d?>>?",">>?","<%&%d+","<",";","&&","||?"}
function text.trim(value) -- from http://lua-users.org/wiki/StringTrim
local from = string.match(value, "^%s*()")
return from > #value and "" or string.match(value, ".*%S", from)
end
-- used by lib/sh
function text.escapeMagic(txt)
return txt:gsub('[%(%)%.%%%+%-%*%?%[%^%$]', '%%%1')
end
function text.removeEscapes(txt)
return txt:gsub("%%([%(%)%.%%%+%-%*%?%[%^%$])","%1")
end
function text.internal.tokenize(value, options)
checkArg(1, value, "string")
checkArg(2, options, "table", "nil")
options = options or {}
local delimiters = options.delimiters
local custom = not not options.delimiters
delimiters = delimiters or text.syntax
local words, reason = text.internal.words(value, options)
local splitter = text.escapeMagic(custom and table.concat(delimiters) or "<>|;&")
if type(words) ~= "table" or
#splitter == 0 or
not value:find("["..splitter.."]") then
return words, reason
end
return text.internal.splitWords(words, delimiters)
end
-- tokenize input by quotes and whitespace
function text.internal.words(input, options)
checkArg(1, input, "string")
checkArg(2, options, "table", "nil")
options = options or {}
local quotes = options.quotes
local show_escapes = options.show_escapes
local qr = nil
quotes = quotes or {{"'","'",true},{'"','"'},{'`','`'}}
local function append(dst, txt, _qr)
local size = #dst
if size == 0 or dst[size].qr ~= _qr then
dst[size+1] = {txt=txt, qr=_qr}
else
dst[size].txt = dst[size].txt..txt
end
end
-- token meta is {string,quote rule}
local tokens, token = {}, {}
local escaped, start = false, -1
for i = 1, unicode.len(input) do
local char = unicode.sub(input, i, i)
if escaped then -- escaped character
escaped = false
-- include escape char if show_escapes
-- or the followwing are all true
-- 1. qr active
-- 2. the char escaped is NOT the qr closure
-- 3. qr is not literal
if show_escapes or (qr and not qr[3] and qr[2] ~= char) then
append(token, '\\', qr)
end
append(token, char, qr)
elseif char == "\\" and (not qr or not qr[3]) then
escaped = true
elseif qr and qr[2] == char then -- end of quoted string
-- if string is empty, we can still capture a quoted empty arg
if #token == 0 or #token[#token] == 0 then
append(token, '', qr)
end
qr = nil
elseif not qr and tx.first(quotes,function(Q)
qr=Q[1]==char and Q or nil return qr end) then
start = i
elseif not qr and string.find(char, "%s") then
if #token > 0 then
table.insert(tokens, token)
end
token = {}
else -- normal char
append(token, char, qr)
end
end
if qr then
return nil, "unclosed quote at index " .. start
end
if #token > 0 then
table.insert(tokens, token)
end
return tokens
end
require("package").delay(text, "/lib/core/full_text.lua")
return text

320
data/OpenOS/lib/thread.lua Normal file
View File

@@ -0,0 +1,320 @@
local pipe = require("pipe")
local event = require("event")
local process = require("process")
local computer = require("computer")
local thread = {}
local init_thread
local function waitForDeath(threads, timeout, all)
checkArg(1, threads, "table")
checkArg(2, timeout, "number", "nil")
checkArg(3, all, "boolean")
timeout = timeout or math.huge
local mortician = {}
local timed_out = true
local deadline = computer.uptime() + timeout
while deadline > computer.uptime() do
local dieing = {}
local living = false
for _,t in ipairs(threads) do
local mt = getmetatable(t)
local result = mt.attached.data.result
local proc_ok = type(result) ~= "table" or result[1]
local ready_to_die = t:status() ~= "running" -- suspended is considered dead to exit
or not proc_ok -- the thread is killed if its attached process has a non zero exit
if ready_to_die then
dieing[#dieing + 1] = t
mortician[t] = true
else
living = true
end
end
if all and not living or not all and #dieing > 0 then
timed_out = false
break
end
-- resume each non dead thread
-- we KNOW all threads are event.pull blocked
event.pull(deadline - computer.uptime())
end
for t in pairs(mortician) do
t:kill()
end
if timed_out then
return nil, "thread join timed out"
end
return true
end
function thread.waitForAny(threads, timeout)
return waitForDeath(threads, timeout, false)
end
function thread.waitForAll(threads, timeout)
return waitForDeath(threads, timeout, true)
end
local box_thread = {}
function box_thread:resume()
local mt = getmetatable(self)
if mt.__status ~= "suspended" then
return nil, "cannot resume " .. mt.__status .. " thread"
end
mt.__status = "running"
-- register the thread to wake up
if coroutine.status(self.pco.root) == "suspended" and not mt.reg then
mt.register(0)
end
return true
end
function box_thread:suspend()
local mt = getmetatable(self)
if mt.__status ~= "running" then
return nil, "cannot suspend " .. mt.__status .. " thread"
end
mt.__status = "suspended"
local pco_status = coroutine.status(self.pco.root)
if pco_status == "running" or pco_status == "normal" then
mt.coma()
end
return true
end
function box_thread:status()
return getmetatable(self).__status
end
function box_thread:join(timeout)
return waitForDeath({self}, timeout, true)
end
function box_thread:kill()
getmetatable(self).close()
end
function box_thread:detach()
return self:attach(init_thread)
end
function box_thread:attach(parent)
local proc = process.info(parent)
local mt = assert(getmetatable(self), "thread panic: no metadata")
if not proc then return nil, "thread failed to attach, process not found" end
if mt.attached == proc then return self end -- already attached
-- remove from old parent
local waiting_handler
if mt.attached then
-- registration happens on the attached proc, unregister before reparenting
waiting_handler = mt.unregister()
process.removeHandle(self, mt.attached)
end
-- fix close
self.close = self.join
-- attach to parent or the current process
mt.attached = proc
process.addHandle(self, proc)
-- register on the new parent
if waiting_handler then -- event-waiting
mt.register(waiting_handler.timeout - computer.uptime())
end
return self
end
function thread.current()
local proc = process.findProcess()
local thread_root
while proc do
if thread_root then
for _,handle in ipairs(proc.data.handles) do
if handle.pco and handle.pco.root == thread_root then
return handle
end
end
else
thread_root = proc.data.coroutine_handler.root
end
proc = proc.parent
end
end
function thread.create(fp, ...)
checkArg(1, fp, "function")
local mt = {__status="suspended",__index=box_thread}
local t = setmetatable({}, mt)
t.pco = pipe.createCoroutineStack(function(...)
mt.__status = "running"
local fp_co = t.pco.create(fp)
-- run fp_co until dead
-- pullSignal will yield_past this point
-- but yield will return here, we pullSignal from here to yield_past
local args = table.pack(...)
while true do
local result = table.pack(t.pco.resume(fp_co, table.unpack(args, 1, args.n)))
if t.pco.status(fp_co) == "dead" then
-- this error handling is VERY much like process.lua
-- maybe one day it'll merge
if not result[1] then
local exit_code
local msg = result[2]
-- msg can be a custom error object
local reason = "crashed"
if type(msg) == "table" then
if type(msg.reason) == "string" then
reason = msg.reason
end
exit_code = tonumber(msg.code)
elseif type(msg) == "string" then
reason = msg
end
if not exit_code then
pcall(event.onError, string.format("[thread] %s", reason))
exit_code = 1
end
os.exit(exit_code)
end
break
end
args = table.pack(event.pull(table.unpack(result, 2, result.n)))
end
end, nil, "thread")
--special resume to keep track of process death
function mt.private_resume(...)
mt.unregister()
-- this thread may have been killed
if t:status() == "dead" then return end
local result = table.pack(t.pco.resume(t.pco.root, ...))
if t.pco.status(t.pco.root) == "dead" then
mt.close()
end
return table.unpack(result, 1, result.n)
end
mt.process = process.list[t.pco.root]
mt.process.data.handlers = {}
function mt.register(timeout)
-- register a timeout handler
mt.id = event.register(
nil, -- nil key matches anything, timers use false keys
mt.private_resume,
timeout, -- wait for the time specified by the caller
1, -- we only want this thread to wake up once
mt.attached.data.handlers) -- optional arg, to specify our own handlers
mt.reg = mt.attached.data.handlers[mt.id]
end
function mt.unregister()
local id = mt.id
local reg = mt.reg
mt.id = nil
mt.reg = nil
-- before just removing a handler, make sure it is still ours
if id and mt.attached.data.handlers[id] == reg then
mt.attached.data.handlers[id] = nil
return reg
end
end
function mt.coma()
mt.unregister() -- we should not wake up again (until resumed)
while mt.__status == "suspended" do
t.pco.yield_past(t.pco.root, 0)
end
end
function mt.process.data.pull(_, timeout)
--[==[
yield_past(root) will yield until out of this thread
registration puts in a callback to resume this thread
Subsequent registrations are necessary in case the thread is suspended
This thread yields when suspended, entering a coma state
-> coma state: yield without registration
resume will regsiter a wakeup call, breaks coma
subsequent yields need not specify a timeout because
we already legitimately resumed only to find out we had been suspended
3 places register for wake up
1. computer.pullSignal [this path]
2. t:attach(proc) will unregister and re-register
3. t:resume() of a suspended thread
]==]
mt.register(timeout)
local event_data = table.pack(t.pco.yield_past(t.pco.root, timeout))
mt.coma()
return table.unpack(event_data, 1, event_data.n)
end
function mt.close()
local old_status = t:status()
mt.__status = "dead"
process.removeHandle(t, mt.attached)
if old_status ~= "dead" then
event.push("thread_exit")
end
end
t:attach() -- the current process
mt.private_resume(...) -- threads start out running
return t
end
do
local handlers = event.handlers
local handlers_mt = getmetatable(handlers)
-- the event library sets a metatable on handlers, but we set threaded=true
if not handlers_mt.threaded then
-- find the root process
local root_data
for t,p in pairs(process.list) do
if not p.parent then
init_thread = t
root_data = p.data
break
end
end
assert(init_thread, "thread library panic: no init thread")
handlers_mt.threaded = true
-- if we don't separate root handlers from thread handlers we see double dispatch
-- because the thread calls dispatch on pull as well
root_data.handlers = {} -- root handlers
root_data.pull = handlers_mt.__call -- the real computer.pullSignal
while true do
local key, value = next(handlers)
if not key then break end
root_data.handlers[key] = value
handlers[key] = nil
end
handlers_mt.__index = function(_, key)
return process.info().data.handlers[key]
end
handlers_mt.__newindex = function(_, key, value)
process.info().data.handlers[key] = value
end
handlers_mt.__pairs = function(_, ...)
return pairs(process.info().data.handlers, ...)
end
handlers_mt.__call = function(tbl, ...)
return process.info().data.pull(tbl, ...)
end
end
end
return thread

View File

@@ -0,0 +1,31 @@
local computer = require("computer")
local fs = require("filesystem")
local shell = require("shell")
local lib = {}
function lib.locate(path)
for _,lookup in ipairs(computer.getProgramLocations()) do
if lookup[1] == path then
return lookup[2]
end
end
end
function lib.reportNotFound(path, reason)
checkArg(1, path, "string")
if fs.isDirectory(shell.resolve(path)) then
io.stderr:write(path .. ": is a directory\n")
return 126
end
local loot = lib.locate(path)
if loot then
io.stderr:write("The program '" .. path .. "' is currently not installed. To install it:\n" ..
"1. Craft the '" .. loot .. "' floppy disk and insert it into this computer.\n" ..
"2. Run `install " .. loot .. "`")
elseif type(reason) == "string" then
io.stderr:write(path .. ": " .. reason .. "\n")
end
return 127
end
return lib

View File

@@ -0,0 +1,272 @@
local fs = require("filesystem")
local shell = require("shell")
local lib = {}
local function perr(ops, format, ...)
if format then
io.stderr:write(ops.cmd .. string.format(": " .. format, ...) .. "\n")
ops.exit_code = 1
return 1
end
end
local function contents_check(arg, options, bMustExist)
if arg == "" then
return perr(options, "cannot create regular file '' No such file or directory")
end
local path = shell.resolve(arg)
local content_pattern = "^(%.*)(.?)"
local contents_of, of_dir = arg:reverse():match(content_pattern)
of_dir = of_dir:match("^/?$")
local dots = contents_of and contents_of:len() or 0
contents_of = of_dir and ({true,true})[dots]
if (not bMustExist or fs.exists(path)) and of_dir and not fs.isDirectory(path) then
perr(options, "'%s' is not a directory", arg)
os.exit(1)
end
return contents_of, path
end
local function areEqual(path1, path2)
local f1, f2 = fs.open(path1, "rb")
local result = true
if f1 then
f2 = fs.open(path2, "rb")
if f2 then
local chunkSize = 4 * 1024
repeat
local s1, s2 = f1:read(chunkSize), f2:read(chunkSize)
if s1 ~= s2 then
result = false
break
end
until not s1 or not s2
f2:close()
end
f1:close()
end
assert(f1 and f2, "could not open files for reading: " .. path1 .. ", " .. path2)
return result
end
local function status(verbose, from, to)
if verbose then
to = to and (" -> " .. to) or ""
io.write(from .. to .. "\n")
end
os.sleep(0) -- allow interrupting
end
local function prompt(message)
io.write(message .. " [Y/n] ")
local result = io.read()
if not result then -- closed pipe
os.exit(1)
end
return result and (result == "" or result:sub(1, 1):lower() == "y")
end
local function stat(path, ops, P)
local real, reason = fs.realPath(path)
if not real and not P then
perr(ops, "cannot read '%s': '%s'", path, reason)
return false
end
local isLink, linkTarget = fs.isLink(path)
return true,
real,
reason,
isLink,
linkTarget,
fs.exists(path),
fs.get(path),
real and fs.isDirectory(real)
end
function lib.recurse(fromPath, toPath, options, origin, top)
fromPath = fromPath:gsub("/+", "/")
toPath = toPath:gsub("/+", "/")
local fromPathFull = shell.resolve(fromPath)
local toPathFull = shell.resolve(toPath)
local mv = options.cmd == "mv"
local verbose = options.v and (not mv or top)
if options.skip[fromPathFull] then
status(verbose, string.format("skipping %s", fromPath))
return true
end
local function release(result, reason)
if result and mv and top then
local rm_result = not fs.get(fromPathFull).isReadOnly() and fs.remove(fromPathFull)
if not rm_result then
perr(options, "cannot remove '%s': filesystem is readonly", fromPath)
result = false
end
end
return result, reason
end
local
ok,
fromReal,
_, --fromError,
fromIsLink,
fromLinkTarget,
fromExists,
fromFs,
fromIsDir = stat(fromPathFull, options, options.P)
if not ok then return nil end
local
ok,
toReal,
_,--toError,
toIsLink,
_,--toLinkTarget,
toExists,
toFs,
toIsDir = stat(toPathFull, options)
if not ok then os.exit(1) end
if toFs.isReadOnly() then
perr(options, "cannot create target '%s': filesystem is readonly", toPath)
return
end
local same_path = fromReal == toReal
local same_fs = fromFs == toFs
local is_mount = origin[fromReal]
if mv and is_mount then
return false, string.format("cannot move '%s', it is a mount point", fromPath)
end
if fromIsLink and options.P and not (toExists and same_path and not toIsLink) then
if toExists and options.n then
return true
end
fs.remove(toPathFull)
if toExists then
status(verbose, string.format("removed '%s'", toPath))
end
status(verbose, fromPath, toPath)
return release(fs.link(fromLinkTarget, toPathFull))
elseif fromIsDir then
if not options.r then
status(true, string.format("omitting directory '%s'", fromPath))
options.exit_code = 1
return true
end
if toExists and not toIsDir then
-- my real cp always does this, even with -f, -n or -i.
return nil, "cannot overwrite non-directory '" .. toPath .. "' with directory '" .. fromPath .. "'"
end
if options.x and not top and is_mount then
return true
end
if same_fs then
if (toReal.."/"):find(fromReal.."/",1,true) then
return nil, "cannot write a directory, '" .. fromPath .. "', into itself, '" .. toPath .. "'"
end
end
if mv then
if fs.list(toReal)() then -- to is NOT empty
return nil, "cannot move '" .. fromPath .. "' to '" .. toPath .. "': Directory not empty"
end
status(verbose, fromPath, toPath)
end
if not toExists then
status(verbose, fromPath, toPath)
fs.makeDirectory(toPathFull)
end
for file in fs.list(fromPathFull) do
local result, reason = lib.recurse(fromPath .."/".. file, toPath.."/"..file, options, origin, false) -- false, no longer top
if not result then
return false, reason
end
end
return release(true)
elseif fromExists then
if toExists then
if same_path then
return nil, "'" .. fromPath .. "' and '" .. toPath .. "' are the same file"
end
if options.n then
return true
end
if options.u and not toIsDir and areEqual(fromReal, toReal) then
return true
end
if options.i then
if not prompt("overwrite '" .. toPath .. "'?") then
return true
end
end
if toIsDir then
return nil, "cannot overwrite directory '" .. toPath .. "' with non-directory"
end
fs.remove(toReal)
end
status(verbose, fromPath, toPath)
return release(fs.copy(fromPathFull, toPathFull))
else
return nil, "'" .. fromPath .. "': No such file or directory"
end
end
function lib.batch(args, options)
options.exit_code = 0
-- standardized options
options.i = options.i and not options.f
options.P = options.P or options.r
local skips = options.skip or {}
options.skip = {}
for _, skip_item in ipairs(skips) do
options.skip[shell.resolve(skip_item)] = true
end
local origin = {}
for dev,path in fs.mounts() do
origin[path] = dev
end
local toArg = table.remove(args)
local _, ok = contents_check(toArg, options)
if not ok then
return 1
end
local originalToIsDir = fs.isDirectory(ok)
for _, fromArg in ipairs(args) do
-- a "contents of" copy is where src path ends in . or ..
-- a source path ending with . is not sufficient - could be the source filename
local contents_of
contents_of, ok = contents_check(fromArg, options, true)
if ok then
-- we do not append fromPath name to toPath in case of contents_of copy
local toPath = toArg
if contents_of and options.cmd == "mv" then
perr(options, "invalid move path '%s'", fromArg)
else
if not contents_of and originalToIsDir then
local fromName = fs.name(fromArg)
if fromName then
toPath = toPath .. "/" .. fromName
end
end
local result, reason = lib.recurse(fromArg, toPath, options, origin, true)
if not result then
perr(options, reason)
end
end
end
end
return options.exit_code
end
return lib

View File

@@ -0,0 +1,79 @@
local lib={}
lib.internal={}
function lib.internal.range_adjust(f,l,s)
checkArg(1,f,'number','nil')
checkArg(2,l,'number','nil')
checkArg(3,s,'number')
if f==nil then f=1 elseif f<0 then f=s+f+1 end
if l==nil then l=s elseif l<0 then l=s+l+1 end
return f,l
end
function lib.internal.table_view(tbl,f,l)
return setmetatable({},
{
__index = function(_, key)
return (type(key) ~= 'number' or (key >= f and key <= l)) and tbl[key] or nil
end,
__len = function(_)
return l
end,
})
end
local adjust=lib.internal.range_adjust
local view=lib.internal.table_view
-- first(p1,p2) searches for the first range in p1 that satisfies p2
function lib.first(tbl,pred,f,l)
checkArg(1,tbl,'table')
checkArg(2,pred,'function','table')
if type(pred)=='table'then
local set;set,pred=pred,function(e,fi,tbl)
for vi=1,#set do
local v=set[vi]
if lib.begins(tbl,v,fi) then return true,#v end
end
end
end
local s=#tbl
f,l=adjust(f,l,s)
tbl=view(tbl,f,l)
for i=f,l do
local si,ei=pred(tbl[i],i,tbl)
if si then
return i,i+(ei or 1)-1
end
end
end
-- returns true if p1 at first p3 equals element for element p2
function lib.begins(tbl,v,f,l)
checkArg(1,tbl,'table')
checkArg(2,v,'table')
local vs=#v
f,l=adjust(f,l,#tbl)
if vs>(l-f+1)then return end
for i=1,vs do
if tbl[f+i-1]~=v[i] then return end
end
return true
end
function lib.concat(...)
local r,rn,k={},0
for _,tbl in ipairs({...})do
if type(tbl)~='table'then
return nil,'parameter '..tostring(_)..' to concat is not a table'
end
local n=tbl.n or #tbl
k=k or tbl.n
for i=1,n do
rn=rn+1;r[rn]=tbl[i]
end
end
r.n=k and rn or nil
return r
end
require("package").delay(lib, "/lib/core/full_transforms.lua")
return lib

284
data/OpenOS/lib/tty.lua Normal file
View File

@@ -0,0 +1,284 @@
local unicode = require("unicode")
local event = require("event")
local component = require("component")
local computer = require("computer")
local tty = {}
tty.window =
{
fullscreen = true,
blink = true,
dx = 0,
dy = 0,
x = 1,
y = 1,
output_buffer = "",
}
tty.stream = {}
local screen_cache = {}
local function screen_reset(gpu, addr)
screen_cache[addr or gpu.getScreen() or false] = nil
end
event.listen("screen_resized", screen_reset)
function tty.getViewport()
local window = tty.window
local screen = tty.screen()
if window.fullscreen and screen and not screen_cache[screen] then
screen_cache[screen] = true
window.width, window.height = window.gpu.getViewport()
end
return window.width, window.height, window.dx, window.dy, window.x, window.y
end
function tty.setViewport(width, height, dx, dy, x, y)
checkArg(1, width, "number")
checkArg(2, height, "number")
local window = tty.window
dx, dy, x, y = dx or 0, dy or 0, x or 1, y or 1
window.width, window.height, window.dx, window.dy, window.x, window.y = width, height, dx, dy, x, y
end
function tty.gpu()
return tty.window.gpu
end
function tty.clear()
tty.stream.scroll(math.huge)
tty.setCursor(1, 1)
end
function tty.isAvailable()
local gpu = tty.gpu()
return not not (gpu and gpu.getScreen())
end
-- PLEASE do not use this method directly, use io.read or term.read
function tty.stream.read()
local core = require("core/cursor")
local cursor = core.new(tty.window.cursor)
-- the window is given the cursor to allow sy updates [needed for wide char wrapping]
-- even if the user didn't set a cursor, we need one to read
tty.window.cursor = cursor
local ok, result, reason = xpcall(core.read, debug.traceback, cursor)
if not ok or not result then
pcall(cursor.update, cursor)
end
return select(2, assert(ok, result, reason))
end
-- PLEASE do not use this method directly, use io.write or term.write
function tty.stream:write(value)
local gpu = tty.gpu()
if not gpu then
return
end
local window = tty.window
local cursor = window.cursor or {}
cursor.sy = cursor.sy or 0
cursor.tails = cursor.tails or {}
local beeped
local uptime = computer.uptime
local last_sleep = uptime()
window.output_buffer = window.output_buffer .. value
while true do
if uptime() - last_sleep > 3 then
os.sleep(0)
last_sleep = uptime()
end
local ansi_print = require("vt100").parse(window)
-- scroll before parsing next line
-- the value may only have been a newline
cursor.sy = cursor.sy + self.scroll()
-- we may have needed to scroll one last time [nowrap adjustments]
-- or the vt100 parse is incomplete, print nothing else
if #window.output_buffer == 0 or not ansi_print then
break
end
local x, y = tty.getCursor()
local _, ei, delim = unicode.sub(window.output_buffer, 1, window.width):find("([\27\t\r\n\a\b\v\15])")
local segment = ansi_print .. (ei and window.output_buffer:sub(1, ei - 1) or window.output_buffer)
if segment ~= "" then
local gpu_x, gpu_y = x + window.dx, y + window.dy
local tail = ""
local wlen_needed = unicode.wlen(segment)
local wlen_remaining = window.width - x + 1
if wlen_remaining < wlen_needed then
segment = unicode.wtrunc(segment, wlen_remaining + 1)
wlen_needed = unicode.wlen(segment)
tail = wlen_needed < wlen_remaining and " " or ""
cursor.tails[gpu_y - cursor.sy] = tail
if not window.nowrap then
-- we have to reparse the delimeter
ei = #segment
-- fake a newline
delim = "\n"
end
end
gpu.set(gpu_x, gpu_y, segment..tail)
x = x + wlen_needed
end
window.output_buffer = ei and window.output_buffer:sub(ei + 1) or
unicode.sub(window.output_buffer, window.width + 1)
if delim == "\t" then
x = ((x-1) - ((x-1) % 8)) + 9
elseif delim == "\r" then
x = 1
elseif delim == "\n" then
x = 1
y = y + 1
elseif delim == "\b" then
x = x - 1
elseif delim == "\v" then
y = y + 1
elseif delim == "\a" and not beeped then
computer.beep()
beeped = true
elseif delim == "\27" then
window.output_buffer = delim .. window.output_buffer
end
tty.setCursor(x, y)
end
return cursor.sy
end
function tty.getCursor()
local window = tty.window
return window.x, window.y
end
function tty.setCursor(x, y)
checkArg(1, x, "number")
checkArg(2, y, "number")
local window = tty.window
window.x, window.y = x, y
end
local gpu_intercept = {}
function tty.bind(gpu)
checkArg(1, gpu, "table")
if not gpu_intercept[gpu] then
gpu_intercept[gpu] = true -- only override a gpu once
-- the gpu can change resolution before we get a chance to call events and handle screen_resized
-- unfortunately, we have to handle viewport changes by intercept
local setr, setv = gpu.setResolution, gpu.setViewport
gpu.setResolution = function(...)
screen_reset(gpu)
return setr(...)
end
gpu.setViewport = function(...)
screen_reset(gpu)
return setv(...)
end
end
local window = tty.window
if window.gpu ~= gpu then
window.gpu = gpu
window.keyboard = nil -- without a keyboard bound, always use the screen's main keyboard (1st)
tty.getViewport()
end
screen_reset(gpu)
end
function tty.keyboard()
-- this method needs to be safe even if there is no terminal window (e.g. no gpu)
local window = tty.window
if window.keyboard then
return window.keyboard
end
local system_keyboard = component.isAvailable("keyboard") and component.keyboard
system_keyboard = system_keyboard and system_keyboard.address or "no_system_keyboard"
local screen = tty.screen()
if not screen then
-- no screen, no known keyboard, use system primary keyboard if any
return system_keyboard
end
-- if we are using a gpu bound to the primary screen, then use the primary keyboard
if component.isAvailable("screen") and component.screen.address == screen then
window.keyboard = system_keyboard
else
-- calling getKeyboards() on the screen is costly (time)
-- changes to this design should avoid this on every key hit
-- this is expensive (slow!)
window.keyboard = component.invoke(screen, "getKeyboards")[1] or system_keyboard
end
return window.keyboard
end
function tty.screen()
local gpu = tty.gpu()
if not gpu then
return nil
end
return gpu.getScreen()
end
function tty.stream.scroll(lines)
local gpu = tty.gpu()
if not gpu then
return 0
end
local width, height, dx, dy, x, y = tty.getViewport()
-- nil lines indicates a request to auto scroll
-- auto scroll is when the cursor has gone below the bottom on the terminal
-- and the text is scroll up, pulling the cursor back into view
-- lines<0 scrolls up (text down)
-- lines>0 scrolls down (text up)
-- no lines count given, the user is asking to auto scroll y back into view
if not lines then
if y < 1 then
lines = y - 1 -- y==0 scrolls back -1
elseif y > height then
lines = y - height -- y==height+1 scroll forward 1
else
return 0 -- do nothing
end
end
lines = math.min(lines, height)
lines = math.max(lines,-height)
-- scroll request can be too large
local abs_lines = math.abs(lines)
local box_height = height - abs_lines
local fill_top = dy + 1 + (lines < 0 and 0 or box_height)
gpu.copy(dx + 1, dy + 1 + math.max(0, lines), width, box_height, 0, -lines)
gpu.fill(dx + 1, fill_top, width, abs_lines, ' ')
tty.setCursor(x, math.max(1, math.min(y, height)))
return lines
end
-- stream methods
local function bfd() return nil, "tty: invalid operation" end
tty.stream.close = bfd
tty.stream.seek = bfd
tty.stream.handle = "tty"
return tty

30
data/OpenOS/lib/uuid.lua Normal file
View File

@@ -0,0 +1,30 @@
local bit32 = require("bit32")
local uuid = {}
function uuid.next()
-- e.g. 3c44c8a9-0613-46a2-ad33-97b6ba2e9d9a
-- 8-4-4-4-12 (halved sizes because bytes make hex pairs)
local sets = {4, 2, 2, 2, 6}
local result = ""
local pos = 0
for _,set in ipairs(sets) do
if result:len() > 0 then
result = result .. "-"
end
for _ = 1,set do
local byte = math.random(0, 255)
if pos == 6 then
byte = bit32.bor(bit32.band(byte, 0x0F), 0x40)
elseif pos == 8 then
byte = bit32.bor(bit32.band(byte, 0x3F), 0x80)
end
result = result .. string.format("%02x", byte)
pos = pos + 1
end
end
return result
end
return uuid

129
data/OpenOS/lib/vt100.lua Normal file
View File

@@ -0,0 +1,129 @@
local text = require("text")
local rules = {}
local vt100 = {rules=rules}
local full
-- colors, blinking, and reverse
-- [%d+;%d+;..%d+m
-- cost: 2,250
rules[{"%[", "[%d;]*", "m"}] = function(window, _, number_text)
-- prefix and suffix ; act as reset
-- e.g. \27[41;m is actually 41 followed by a reset
local colors = {0x0,0xff0000,0x00ff00,0xffff00,0x0000ff,0xff00ff,0x00B6ff,0xffffff}
local fg, bg = window.gpu.setForeground, window.gpu.setBackground
if window.flip then
fg, bg = bg, fg
end
number_text = " _ " .. number_text:gsub("^;$", ""):gsub(";", " _ ") .. " _ "
local parts = text.internal.tokenize(number_text)
local last_was_break
for _,part in ipairs(parts) do
local num = tonumber(part[1].txt)
last_was_break, num = not num, num or last_was_break and 0
local flip = num == 7
if flip then
if not window.flip then
local rgb, pal = bg(window.gpu.getForeground())
fg(pal or rgb, not not pal)
fg, bg = bg, fg
end
elseif num == 5 then
window.blink = true
elseif num == 0 then
bg(colors[1])
fg(colors[8])
elseif num then
num = num - 29
local set = fg
if num > 10 then
num = num - 10
set = bg
end
local color = colors[num]
if color then
set(color)
end
end
window.flip = flip
end
end
local function save_attributes(window, seven, s)
if seven == "7" or s == "s" then
window.saved =
{
window.x,
window.y,
{window.gpu.getBackground()},
{window.gpu.getForeground()},
window.flip,
window.blink
}
else
local data = window.saved or {1, 1, {0x0}, {0xffffff}, window.flip, window.blink}
window.x = data[1]
window.y = data[2]
window.gpu.setBackground(table.unpack(data[3]))
window.gpu.setForeground(table.unpack(data[4]))
window.flip = data[5]
window.blink = data[6]
end
end
-- 7 save cursor position and attributes
-- 8 restore cursor position and attributes
rules[{"[78]"}] = save_attributes
-- s save cursor position
-- u restore cursor position
rules[{"%[", "[su]"}] = save_attributes
-- returns: anything that failed to parse
function vt100.parse(window)
if window.output_buffer:sub(1, 1) ~= "\27" then
return ""
end
local any_valid
for rule,action in pairs(rules) do
local last_index = 1 -- start at 1 to skip the \27
local captures = {}
for _,pattern in ipairs(rule) do
if last_index >= #window.output_buffer then
any_valid = true
break
end
local si, ei, capture = window.output_buffer:find("^(" .. pattern .. ")", last_index + 1)
if not si then
break
end
captures[#captures + 1] = capture
last_index = ei
end
if #captures == #rule then
action(window, table.unpack(captures))
window.output_buffer = window.output_buffer:sub(last_index + 1)
return ""
end
end
if not full then
-- maybe it did satisfy a rule, load more rules
full = true
dofile("/lib/core/full_vt.lua")
return vt100.parse(window)
end
if not any_valid then
-- malformed
window.output_buffer = window.output_buffer:sub(2)
return "\27"
end
-- else, still consuming
end
return vt100