testing version of LuaBIOS and OpenOS
people were having issues getting them to work so now we promote consistency
This commit is contained in:
102
data/OpenOS/lib/bit32.lua
Normal file
102
data/OpenOS/lib/bit32.lua
Normal 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
182
data/OpenOS/lib/buffer.lua
Normal 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
|
||||
30
data/OpenOS/lib/colors.lua
Normal file
30
data/OpenOS/lib/colors.lua
Normal 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
|
||||
150
data/OpenOS/lib/core/boot.lua
Normal file
150
data/OpenOS/lib/core/boot.lua
Normal 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
|
||||
270
data/OpenOS/lib/core/cursor.lua
Normal file
270
data/OpenOS/lib/core/cursor.lua
Normal 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
|
||||
131
data/OpenOS/lib/core/devfs/01_hw.lua
Normal file
131
data/OpenOS/lib/core/devfs/01_hw.lua
Normal 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
|
||||
},
|
||||
}
|
||||
57
data/OpenOS/lib/core/devfs/02_utils.lua
Normal file
57
data/OpenOS/lib/core/devfs/02_utils.lua
Normal 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
|
||||
},
|
||||
}
|
||||
9
data/OpenOS/lib/core/devfs/adapters/computer.lua
Normal file
9
data/OpenOS/lib/core/devfs/adapters/computer.lua
Normal 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
|
||||
22
data/OpenOS/lib/core/devfs/adapters/eeprom.lua
Normal file
22
data/OpenOS/lib/core/devfs/adapters/eeprom.lua
Normal 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
|
||||
25
data/OpenOS/lib/core/devfs/adapters/filesystem.lua
Normal file
25
data/OpenOS/lib/core/devfs/adapters/filesystem.lua
Normal 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
|
||||
17
data/OpenOS/lib/core/devfs/adapters/gpu.lua
Normal file
17
data/OpenOS/lib/core/devfs/adapters/gpu.lua
Normal 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
|
||||
7
data/OpenOS/lib/core/devfs/adapters/internet.lua
Normal file
7
data/OpenOS/lib/core/devfs/adapters/internet.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
return function(proxy)
|
||||
return
|
||||
{
|
||||
httpEnabled = {proxy.isHttpEnabled()},
|
||||
tcpEnabled = {proxy.isTcpEnabled()},
|
||||
}
|
||||
end
|
||||
11
data/OpenOS/lib/core/devfs/adapters/modem.lua
Normal file
11
data/OpenOS/lib/core/devfs/adapters/modem.lua
Normal 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
|
||||
18
data/OpenOS/lib/core/devfs/adapters/screen.lua
Normal file
18
data/OpenOS/lib/core/devfs/adapters/screen.lua
Normal 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
|
||||
101
data/OpenOS/lib/core/device_labeling.lua
Normal file
101
data/OpenOS/lib/core/device_labeling.lua
Normal 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
|
||||
|
||||
238
data/OpenOS/lib/core/full_buffer.lua
Normal file
238
data/OpenOS/lib/core/full_buffer.lua
Normal 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
|
||||
123
data/OpenOS/lib/core/full_cursor.lua
Normal file
123
data/OpenOS/lib/core/full_cursor.lua
Normal 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 })
|
||||
75
data/OpenOS/lib/core/full_event.lua
Normal file
75
data/OpenOS/lib/core/full_event.lua
Normal 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
|
||||
363
data/OpenOS/lib/core/full_filesystem.lua
Normal file
363
data/OpenOS/lib/core/full_filesystem.lua
Normal 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
|
||||
143
data/OpenOS/lib/core/full_keyboard.lua
Normal file
143
data/OpenOS/lib/core/full_keyboard.lua
Normal 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
|
||||
})
|
||||
366
data/OpenOS/lib/core/full_ls.lua
Normal file
366
data/OpenOS/lib/core/full_ls.lua
Normal 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
|
||||
|
||||
604
data/OpenOS/lib/core/full_sh.lua
Normal file
604
data/OpenOS/lib/core/full_sh.lua
Normal 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
|
||||
|
||||
|
||||
25
data/OpenOS/lib/core/full_shell.lua
Normal file
25
data/OpenOS/lib/core/full_shell.lua
Normal 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
|
||||
286
data/OpenOS/lib/core/full_text.lua
Normal file
286
data/OpenOS/lib/core/full_text.lua
Normal 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
|
||||
|
||||
109
data/OpenOS/lib/core/full_transforms.lua
Normal file
109
data/OpenOS/lib/core/full_transforms.lua
Normal 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
|
||||
93
data/OpenOS/lib/core/full_vt.lua
Normal file
93
data/OpenOS/lib/core/full_vt.lua
Normal 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
|
||||
231
data/OpenOS/lib/core/install_basics.lua
Normal file
231
data/OpenOS/lib/core/install_basics.lua
Normal 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
|
||||
68
data/OpenOS/lib/core/install_utils.lua
Normal file
68
data/OpenOS/lib/core/install_utils.lua
Normal 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
|
||||
129
data/OpenOS/lib/core/lua_shell.lua
Normal file
129
data/OpenOS/lib/core/lua_shell.lua
Normal 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
345
data/OpenOS/lib/devfs.lua
Normal 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
165
data/OpenOS/lib/event.lua
Normal 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
|
||||
313
data/OpenOS/lib/filesystem.lua
Normal file
313
data/OpenOS/lib/filesystem.lua
Normal 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
|
||||
130
data/OpenOS/lib/internet.lua
Normal file
130
data/OpenOS/lib/internet.lua
Normal 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
123
data/OpenOS/lib/io.lua
Normal 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
|
||||
63
data/OpenOS/lib/keyboard.lua
Normal file
63
data/OpenOS/lib/keyboard.lua
Normal 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
126
data/OpenOS/lib/note.lua
Normal 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
119
data/OpenOS/lib/package.lua
Normal 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
230
data/OpenOS/lib/pipe.lua
Normal 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
195
data/OpenOS/lib/process.lua
Normal 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
7
data/OpenOS/lib/rc.lua
Normal 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
|
||||
|
||||
145
data/OpenOS/lib/serialization.lua
Normal file
145
data/OpenOS/lib/serialization.lua
Normal 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
222
data/OpenOS/lib/sh.lua
Normal 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
144
data/OpenOS/lib/shell.lua
Normal 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
62
data/OpenOS/lib/sides.lua
Normal 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
172
data/OpenOS/lib/term.lua
Normal 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
109
data/OpenOS/lib/text.lua
Normal 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
320
data/OpenOS/lib/thread.lua
Normal 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
|
||||
31
data/OpenOS/lib/tools/programLocations.lua
Normal file
31
data/OpenOS/lib/tools/programLocations.lua
Normal 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
|
||||
272
data/OpenOS/lib/tools/transfer.lua
Normal file
272
data/OpenOS/lib/tools/transfer.lua
Normal 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
|
||||
79
data/OpenOS/lib/transforms.lua
Normal file
79
data/OpenOS/lib/transforms.lua
Normal 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
284
data/OpenOS/lib/tty.lua
Normal 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
30
data/OpenOS/lib/uuid.lua
Normal 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
129
data/OpenOS/lib/vt100.lua
Normal 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
|
||||
Reference in New Issue
Block a user