mirror of
https://github.com/MightyPirates/OpenComputers.git
synced 2025-09-12 16:57:32 -04:00
code cleanup, memory optimizations, slim term code, and system file organization
notable changes ### TTY New /lib/tty library that provides the core readKeyboard and drawText functionality. /lib/term should still be used by user code and the tty api should be considered alpha. The intent in this separation of the code is to allow term api to remain unchanged and allow tty to adapt as improvements are made. ### delayloaded and now splitting The delayloaded packaging system has been removed. The delayload system allowed for library object to appear loaded when in fact they were not. But this significantly increased boot time. The code now imploys "library splitting", saves tremendous memory, shortens boot time, and all libraries are fully usable even when partially loaded. In other words, it is all transparent to the user. ### boot memory cost Numerous micro optimizations have been made. "boot to shell prompt" now idles at 156k allocated, or 40k free, up from ~26k free before this change. Yes, that's 14k less in this update.
This commit is contained in:
parent
bcadcdb21e
commit
79e8f863ea
@ -1,3 +1,2 @@
|
||||
local term = require("term")
|
||||
|
||||
term.clear()
|
||||
local tty = require("tty")
|
||||
tty.clear()
|
@ -1,8 +1,8 @@
|
||||
local event = require "event"
|
||||
local term = require "term"
|
||||
local event = require("event")
|
||||
local tty = require("tty")
|
||||
|
||||
local args = {...}
|
||||
local gpu = term.gpu()
|
||||
local gpu = tty.gpu()
|
||||
local interactive = io.output().tty
|
||||
local color, isPal, evt
|
||||
if interactive then
|
||||
|
@ -2,7 +2,7 @@ local event = require("event")
|
||||
local fs = require("filesystem")
|
||||
local keyboard = require("keyboard")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local term = require("term") -- TODO use tty and cursor position instead of global area and gpu
|
||||
local text = require("text")
|
||||
local unicode = require("unicode")
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
local computer = require("computer")
|
||||
local total = computer.totalMemory()
|
||||
local max = 0
|
||||
for i=1,40 do
|
||||
max = math.max(max, computer.freeMemory())
|
||||
os.sleep(0) -- invokes gc
|
||||
end
|
||||
print(string.format("Total%12d\nUsed%13d\nFree%13d", total, total - max, max))
|
@ -8,13 +8,13 @@ https://raw.githubusercontent.com/OpenPrograms/Wobbo-Programs/master/grep/grep.l
|
||||
|
||||
local fs = require("filesystem")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
|
||||
-- Process the command line arguments
|
||||
|
||||
local args, options = shell.parse(...)
|
||||
|
||||
local gpu = term.gpu()
|
||||
local gpu = tty.gpu()
|
||||
|
||||
local function printUsage(ostream, msg)
|
||||
local s = ostream or io.stdout
|
||||
@ -106,7 +106,7 @@ local m_only = pop('o','only-matching')
|
||||
local quiet = pop('q','quiet','silent')
|
||||
|
||||
local print_count = pop('c','count')
|
||||
local colorize = pop('color','colour') and io.output().tty and term.isAvailable()
|
||||
local colorize = pop('color','colour') and io.output().tty and tty.isAvailable()
|
||||
|
||||
local noop = function(...)return ...;end
|
||||
local setc = colorize and gpu.setForeground or noop
|
||||
|
@ -4,7 +4,7 @@ local shell = require("shell")
|
||||
local options
|
||||
|
||||
do
|
||||
local basic, reason = loadfile(package.searchpath("tools/install_basics", package.path), "bt", _G)
|
||||
local basic, reason = loadfile("/opt/core/install_basics.lua", "bt", _G)
|
||||
if not basic then
|
||||
io.stderr:write("failed to load install: " .. tostring(reason) .. "\n")
|
||||
return 1
|
||||
|
@ -1,6 +1,6 @@
|
||||
local keyboard = require("keyboard")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local term = require("term") -- using term for negative scroll feature
|
||||
local text = require("text")
|
||||
local unicode = require("unicode")
|
||||
local computer = require("computer")
|
||||
|
@ -1,6 +1,5 @@
|
||||
local fs = require("filesystem")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
|
||||
local args, ops = shell.parse(...)
|
||||
if #args == 0 then
|
||||
|
@ -1,8 +1,7 @@
|
||||
-- load complex, if we can (might be low on memory)
|
||||
|
||||
local ok, why = pcall(function(...)
|
||||
local full_ls_path = package.searchpath("tools/full_ls", package.path)
|
||||
return loadfile(full_ls_path, "bt", _G)(...)
|
||||
return loadfile("/opt/core/full_ls.lua", "bt", _G)(...)
|
||||
end, ...)
|
||||
|
||||
if not ok then
|
||||
|
@ -2,7 +2,7 @@ local shell = require("shell")
|
||||
local args = shell.parse(...)
|
||||
|
||||
if #args == 0 then
|
||||
args = {"/lib/tools/lua_shell.lua"}
|
||||
args = {"/opt/core/lua_shell.lua"}
|
||||
end
|
||||
|
||||
local script, reason = loadfile(args[1], nil, setmetatable({},{__index=_ENV}))
|
||||
|
@ -1,6 +1,6 @@
|
||||
local keyboard = require("keyboard")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local term = require("term") -- TODO use tty and cursor position instead of global area and gpu
|
||||
local text = require("text")
|
||||
local unicode = require("unicode")
|
||||
|
||||
|
@ -55,7 +55,7 @@ else
|
||||
io.stderr:write(reason,"\n")
|
||||
return 1
|
||||
elseif ops.r then
|
||||
proxy = require("tools/ro_wrapper").wrap(proxy)
|
||||
proxy = dofile("/opt/core/ro_wrapper.lua").wrap(proxy)
|
||||
end
|
||||
|
||||
local result, reason = fs.mount(proxy, shell.resolve(args[2]))
|
||||
|
@ -1,8 +1,8 @@
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
|
||||
local args = shell.parse(...)
|
||||
local gpu = term.gpu()
|
||||
local gpu = tty.gpu()
|
||||
|
||||
if #args == 0 then
|
||||
local w, h = gpu.getViewport()
|
||||
@ -29,4 +29,4 @@ if not result then
|
||||
end
|
||||
return 1
|
||||
end
|
||||
term.clear()
|
||||
tty.clear()
|
||||
|
@ -1,5 +1,3 @@
|
||||
local text = require("text")
|
||||
|
||||
local args = {...}
|
||||
|
||||
if #args < 1 then
|
||||
|
@ -1,6 +1,6 @@
|
||||
local event = require("event")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
local text = require("text")
|
||||
local sh = require("sh")
|
||||
|
||||
@ -10,39 +10,28 @@ if input[2] then
|
||||
table.insert(args, 1, input[2])
|
||||
end
|
||||
|
||||
local history = {}
|
||||
local history = {hint = sh.hintHandler}
|
||||
shell.prime()
|
||||
|
||||
if #args == 0 and (io.stdin.tty or options.i) and not options.c then
|
||||
-- interactive shell.
|
||||
-- source profile
|
||||
if not term.isAvailable() then event.pull("term_available") end
|
||||
if not tty.isAvailable() then event.pull("term_available") end
|
||||
loadfile(shell.resolve("source","lua"))("/etc/profile")
|
||||
while true do
|
||||
if not term.isAvailable() then -- don't clear unless we lost the term
|
||||
while not term.isAvailable() do
|
||||
if not tty.isAvailable() then -- don't clear unless we lost the term
|
||||
while not tty.isAvailable() do
|
||||
event.pull("term_available")
|
||||
end
|
||||
term.clear()
|
||||
tty.clear()
|
||||
end
|
||||
local gpu = term.gpu()
|
||||
while term.isAvailable() do
|
||||
local gpu = tty.gpu()
|
||||
while tty.isAvailable() do
|
||||
local foreground = gpu.setForeground(0xFF0000)
|
||||
term.write(sh.expand(os.getenv("PS1") or "$ "))
|
||||
tty.write(sh.expand(os.getenv("PS1") or "$ "))
|
||||
gpu.setForeground(foreground)
|
||||
term.setCursorBlink(true)
|
||||
local ok, command = pcall(term.read, history, nil, sh.hintHandler)
|
||||
if not ok then
|
||||
if command == "interrupted" then -- hard interrupt
|
||||
io.write("^C\n")
|
||||
break
|
||||
elseif not term.isAvailable() then
|
||||
break
|
||||
else -- crash?
|
||||
io.stderr:write("\nshell crashed: " .. tostring(command) .. "\n")
|
||||
break
|
||||
end
|
||||
end
|
||||
tty.setCursorBlink(true)
|
||||
local command = tty.read(history)
|
||||
if not command then
|
||||
if command == false then
|
||||
break -- soft interrupt
|
||||
@ -55,7 +44,7 @@ if #args == 0 and (io.stdin.tty or options.i) and not options.c then
|
||||
return
|
||||
elseif command ~= "" then
|
||||
local result, reason = sh.execute(_ENV, command)
|
||||
if term.getCursor() > 1 then
|
||||
if tty.getCursor() > 1 then
|
||||
print()
|
||||
end
|
||||
if not result then
|
||||
|
@ -1,5 +1,5 @@
|
||||
local computer = require("computer")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
|
||||
term.clear()
|
||||
tty.clear()
|
||||
computer.shutdown()
|
@ -1,5 +1,5 @@
|
||||
local buffer = require("buffer")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
|
||||
local io_open = io.open
|
||||
function io.open(path, mode)
|
||||
@ -21,26 +21,24 @@ end
|
||||
stdoutStream.close = stdinStream.close
|
||||
stderrStream.close = stdinStream.close
|
||||
|
||||
function stdinStream:read(n, dobreak)
|
||||
stdinHistory.dobreak = dobreak
|
||||
local result = term.readKeyboard(stdinHistory)
|
||||
return result
|
||||
function stdinStream:read()
|
||||
return tty.read(stdinHistory)
|
||||
end
|
||||
|
||||
function stdoutStream:write(str)
|
||||
term.drawText(str, self.wrap ~= false)
|
||||
tty.drawText(str, self.nowrap)
|
||||
return self
|
||||
end
|
||||
|
||||
function stderrStream:write(str)
|
||||
local gpu = term.gpu()
|
||||
local gpu = tty.gpu()
|
||||
local set_depth = gpu and gpu.getDepth() and gpu.getDepth() > 1
|
||||
|
||||
if set_depth then
|
||||
set_depth = gpu.setForeground(0xFF0000)
|
||||
end
|
||||
|
||||
term.drawText(str, true)
|
||||
tty.drawText(str)
|
||||
|
||||
if set_depth then
|
||||
gpu.setForeground(set_depth)
|
||||
@ -70,30 +68,17 @@ core_stdin.close = stdinStream.close
|
||||
core_stdout.close = stdinStream.close
|
||||
core_stderr.close = stdinStream.close
|
||||
|
||||
local fd_map =
|
||||
{
|
||||
-- key name => method name
|
||||
stdin = 'input',
|
||||
stdout = 'output',
|
||||
stderr = 'error'
|
||||
}
|
||||
|
||||
local io_mt = getmetatable(io) or {}
|
||||
io_mt.__index = function(t, k)
|
||||
if fd_map[k] then
|
||||
return io[fd_map[k]]()
|
||||
end
|
||||
end
|
||||
io_mt.__newindex = function(t, k, v)
|
||||
if fd_map[k] then
|
||||
io[fd_map[k]](v)
|
||||
else
|
||||
rawset(io, k, v)
|
||||
end
|
||||
return
|
||||
k == 'stdin' and io.input() or
|
||||
k == 'stdout' and io.output() or
|
||||
k == 'stderr' and io.error() or
|
||||
nil
|
||||
end
|
||||
|
||||
setmetatable(io, io_mt)
|
||||
|
||||
io.stdin = core_stdin
|
||||
io.stdout = core_stdout
|
||||
io.stderr = core_stderr
|
||||
io.input(core_stdin)
|
||||
io.output(core_stdout)
|
||||
io.error(core_stderr)
|
||||
|
@ -1,20 +1,17 @@
|
||||
local component = require("component")
|
||||
local computer = require("computer")
|
||||
local event = require("event")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
local process = require("process")
|
||||
|
||||
-- this should be the init level process
|
||||
process.info().data.window = term.internal.open()
|
||||
|
||||
event.listen("gpu_bound", function(ename, gpu)
|
||||
gpu=component.proxy(gpu)
|
||||
term.bind(gpu)
|
||||
gpu = component.proxy(gpu)
|
||||
tty.bind(gpu)
|
||||
computer.pushSignal("term_available")
|
||||
end)
|
||||
|
||||
local function components_changed(ename, address, type)
|
||||
local window = term.internal.window()
|
||||
local window = tty.window
|
||||
if not window then
|
||||
return
|
||||
end
|
||||
@ -47,7 +44,7 @@ local function components_changed(ename, address, type)
|
||||
window.keyboard = nil
|
||||
end
|
||||
|
||||
if (type == "screen" or type == "gpu") and not term.isAvailable() then
|
||||
if (type == "screen" or type == "gpu") and not tty.isAvailable() then
|
||||
computer.pushSignal("term_unavailable")
|
||||
end
|
||||
end
|
||||
|
@ -4,7 +4,7 @@ local component = require("component")
|
||||
local computer = require("computer")
|
||||
local text = require("text")
|
||||
local unicode = require("unicode")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
|
||||
if not component.isAvailable("gpu") then
|
||||
return
|
||||
@ -20,7 +20,7 @@ if f then
|
||||
f:close()
|
||||
local greeting = greetings[math.random(1, #greetings)]
|
||||
if greeting then
|
||||
local width = math.max(10, term.getViewport())
|
||||
local width = math.max(10, tty.getViewport())
|
||||
for line in text.wrappedLines(greeting, width - 4, width - 4) do
|
||||
table.insert(lines, line)
|
||||
maxWidth = math.max(maxWidth, unicode.len(line))
|
||||
|
@ -13,7 +13,7 @@ do
|
||||
invoke(addr, "close", handle)
|
||||
return load(buffer, "=" .. file, "bt", _G)
|
||||
end]], "=loadfile", "bt", _G)()
|
||||
loadfile("/lib/tools/boot.lua")(loadfile)
|
||||
loadfile("/opt/core/boot.lua")(loadfile)
|
||||
end
|
||||
|
||||
while true do
|
||||
|
@ -153,7 +153,7 @@ inject_dynamic_pairs = function(fsnode, path, bStoreUse)
|
||||
})
|
||||
end
|
||||
|
||||
local label_lib = dofile("/lib/tools/device_labeling.lua")
|
||||
local label_lib = dofile("/opt/core/device_labeling.lua")
|
||||
label_lib.loadRules()
|
||||
api.getDeviceLabel = label_lib.getDeviceLabel
|
||||
api.setDeviceLabel = label_lib.setDeviceLabel
|
||||
@ -163,7 +163,7 @@ function api.register(public_proxy)
|
||||
if registered then return end
|
||||
registered = true
|
||||
|
||||
local start_path = "/lib/tools/devfs/"
|
||||
local start_path = "/opt/core/devfs/"
|
||||
for starter in fs.list(start_path) do
|
||||
local full_path = start_path .. starter
|
||||
local _,matched = starter:gsub("%.lua$","")
|
||||
|
@ -47,6 +47,7 @@ end
|
||||
function io.stream(fd,file,mode)
|
||||
checkArg(1,fd,'number')
|
||||
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
|
||||
local result, reason = io.open(file, mode)
|
||||
@ -57,9 +58,9 @@ function io.stream(fd,file,mode)
|
||||
elseif not io.type(file) then
|
||||
error("bad argument #1 (string or file expected, got " .. type(file) .. ")", 2)
|
||||
end
|
||||
require("process").info().data.io[fd] = file
|
||||
dio[fd] = file
|
||||
end
|
||||
return require("process").info().data.io[fd]
|
||||
return dio[fd]
|
||||
end
|
||||
|
||||
function io.input(file)
|
||||
|
@ -45,7 +45,7 @@ setmetatable(keyboard.keys,
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local function getKeyboardAddress(address)
|
||||
return address or require("term").keyboard()
|
||||
return address or require("tty").keyboard()
|
||||
end
|
||||
|
||||
local function getPressedCodes(address)
|
||||
|
@ -19,9 +19,6 @@ package.loaded = loaded
|
||||
local preload = {}
|
||||
package.preload = preload
|
||||
|
||||
local delayed = {}
|
||||
package.delayed = delayed
|
||||
|
||||
package.searchers = {}
|
||||
|
||||
function package.searchpath(name, path, sep, rep)
|
||||
@ -58,30 +55,6 @@ local function preloadSearcher(module)
|
||||
end
|
||||
end
|
||||
|
||||
local delay_data = {}
|
||||
local delay_tools = setmetatable({},{__mode="v"})
|
||||
|
||||
function delay_data.__index(tbl,key)
|
||||
local lookup = delay_data.lookup or loadfile(package.searchpath("tools/delayLookup", package.path), "bt", _G)
|
||||
delay_data.lookup = lookup
|
||||
return lookup(delay_data, tbl, key)
|
||||
end
|
||||
delay_data.__pairs = delay_data.__index -- nil key acts like pairs
|
||||
|
||||
local function delaySearcher(module)
|
||||
if not delayed[module] then
|
||||
return "\tno field package.delayed['" .. module .. "']"
|
||||
end
|
||||
local filepath, reason = package.searchpath(module, package.path)
|
||||
if not filepath then
|
||||
return reason
|
||||
end
|
||||
local parser = delay_tools.parser or loadfile(package.searchpath("tools/delayParse", package.path), "bt", _G)
|
||||
delay_tools.parser = parser
|
||||
local loader, reason = parser(filepath,delay_data)
|
||||
return loader, reason
|
||||
end
|
||||
|
||||
local function pathSearcher(module)
|
||||
local filepath, reason = package.searchpath(module, package.path)
|
||||
if filepath then
|
||||
@ -97,7 +70,6 @@ local function pathSearcher(module)
|
||||
end
|
||||
|
||||
table.insert(package.searchers, preloadSearcher)
|
||||
table.insert(package.searchers, delaySearcher)
|
||||
table.insert(package.searchers, pathSearcher)
|
||||
|
||||
function require(module)
|
||||
|
@ -20,13 +20,6 @@ function process.findProcess(co)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
local function parent_data(pre, tbl, k, ...)
|
||||
if tbl and k then
|
||||
return parent_data(pre, tbl[k], ...)
|
||||
end
|
||||
return setmetatable(pre, {__index=tbl})
|
||||
end
|
||||
|
||||
function process.load(path, env, init, name)
|
||||
checkArg(1, path, "string", "function")
|
||||
checkArg(2, env, "table", "nil")
|
||||
@ -106,19 +99,25 @@ function process.load(path, env, init, name)
|
||||
end
|
||||
return select(2, table.unpack(result))
|
||||
end, true)
|
||||
process.list[thread] = {
|
||||
local new_proc =
|
||||
{
|
||||
path = path,
|
||||
command = name,
|
||||
env = env,
|
||||
data = parent_data(
|
||||
data =
|
||||
{
|
||||
handles = {},
|
||||
io = parent_data({}, p, "data", "io"),
|
||||
coroutine_handler = parent_data({}, p, "data", "coroutine_handler"),
|
||||
}, p, "data"),
|
||||
io = {},
|
||||
coroutine_handler = {}
|
||||
},
|
||||
parent = p,
|
||||
instances = setmetatable({}, {__mode="v"}),
|
||||
}
|
||||
setmetatable(new_proc.data.io, {__index=p.data.io})
|
||||
setmetatable(new_proc.data.coroutine_handler, {__index=p.data.coroutine_handler})
|
||||
setmetatable(new_proc.data, {__index=p.data})
|
||||
process.list[thread] = new_proc
|
||||
|
||||
return thread
|
||||
end
|
||||
|
||||
|
@ -2,7 +2,6 @@ local event = require("event")
|
||||
local fs = require("filesystem")
|
||||
local process = require("process")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local text = require("text")
|
||||
local tx = require("transforms")
|
||||
local unicode = require("unicode")
|
||||
@ -10,11 +9,11 @@ local unicode = require("unicode")
|
||||
local sh = {}
|
||||
sh.internal = {}
|
||||
|
||||
-- --[[@@]] are not just comments, but custom annotations for delayload methods.
|
||||
-- See package.lua and the api wiki for more information
|
||||
local function isWordOf(w, vs) return w and #w == 1 and not w[1].qr and tx.first(vs,{{w[1].txt}}) ~= nil end
|
||||
local function isWord(w,v) return isWordOf(w,{v}) end
|
||||
local local_env = {event=event,fs=fs,process=process,shell=shell,term=term,text=text,tx=tx,unicode=unicode,isWordOf=isWordOf,isWord=isWord}
|
||||
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
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
@ -172,25 +171,23 @@ function sh.internal.evaluate(word)
|
||||
elseif #word == 1 and word[1].qr then
|
||||
return sh.internal.expand(word)
|
||||
end
|
||||
local function make_pattern(seg)
|
||||
local result = seg
|
||||
for _,glob_rule in ipairs(sh.internal.globbers) do
|
||||
result = result:gsub("%%%"..glob_rule[1], glob_rule[2])
|
||||
local reduced = result
|
||||
repeat
|
||||
result = reduced
|
||||
reduced = result:gsub(text.escapeMagic(glob_rule[2]):rep(2), glob_rule[2])
|
||||
until reduced == result
|
||||
end
|
||||
return result
|
||||
end
|
||||
local glob_pattern = ''
|
||||
local has_globits = false
|
||||
for i=1,#word do local part = word[i]
|
||||
local next = part.txt
|
||||
if not part.qr then
|
||||
local escaped = text.escapeMagic(next)
|
||||
next = make_pattern(escaped)
|
||||
next = escaped
|
||||
for _,glob_rule in ipairs(sh.internal.globbers) do
|
||||
next = next:gsub("%%%"..glob_rule[1], glob_rule[2])
|
||||
while true do
|
||||
local prev = next
|
||||
next = next:gsub(text.escapeMagic(glob_rule[2]):rep(2), glob_rule[2])
|
||||
if prev == next then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if next ~= escaped then
|
||||
has_globits = true
|
||||
end
|
||||
@ -353,538 +350,16 @@ function sh.internal.concatn(apack, bpack, bn)
|
||||
apack.n = an + bn
|
||||
end
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.handleThreadYield(result)
|
||||
local action = result[2]
|
||||
if action == nil or type(action) == "number" then
|
||||
return table.pack(pcall(event.pull, table.unpack(result, 2, result.n)))
|
||||
else
|
||||
return table.pack(coroutine.yield(table.unpack(result, 2, result.n)))
|
||||
setmetatable(sh,
|
||||
{
|
||||
__index = function(tbl, key)
|
||||
setmetatable(sh.internal, nil)
|
||||
setmetatable(sh, nil)
|
||||
dofile("/opt/core/full_sh.lua")
|
||||
return rawget(tbl, key)
|
||||
end
|
||||
end --[[@delayloaded-end@]]
|
||||
})
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.buildCommandRedirects(args, thread)
|
||||
local data = process.info(thread).data
|
||||
local tokens, ios, handles = args, data.io, data.handles
|
||||
args = {}
|
||||
local from_io, to_io, mode
|
||||
for i = 1, #tokens do
|
||||
local token = tokens[i]
|
||||
if token == "<" then
|
||||
from_io = 0
|
||||
mode = "r"
|
||||
else
|
||||
local first_index, last_index, from_io_txt, mode_txt, to_io_txt = token:find("(%d*)(>>?)(.*)")
|
||||
if mode_txt then
|
||||
mode = mode_txt == ">>" and "a" or "w"
|
||||
from_io = from_io_txt and tonumber(from_io_txt) or 1
|
||||
if to_io_txt ~= "" then
|
||||
to_io = tonumber(to_io_txt:sub(2))
|
||||
ios[from_io] = ios[to_io]
|
||||
mode = nil
|
||||
end
|
||||
else -- just an arg
|
||||
if not mode then
|
||||
table.insert(args, token)
|
||||
else
|
||||
local file, reason = io.open(shell.resolve(token), mode)
|
||||
if not file then
|
||||
io.stderr:write("could not open '" .. token .. "': " .. reason .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
table.insert(handles, file)
|
||||
ios[from_io] = file
|
||||
end
|
||||
mode = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
setmetatable(sh.internal, getmetatable(sh))
|
||||
|
||||
return args
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.buildPipeChain(threads)
|
||||
local prev_pipe
|
||||
for i=1,#threads do
|
||||
local thread = threads[i]
|
||||
local data = process.info(thread).data
|
||||
local pio = data.io
|
||||
|
||||
local pipe
|
||||
if i < #threads then
|
||||
pipe = require("buffer").new("rw", sh.internal.newMemoryStream())
|
||||
pipe:setvbuf("no", 0)
|
||||
-- buffer close flushes the buffer, but we have no buffer
|
||||
-- also, when the buffer is closed, read and writes don't pass through
|
||||
-- simply put, we don't want buffer:close
|
||||
pipe.close = function(self) self.stream:close() end
|
||||
pipe.stream.redirect[1] = rawget(pio, 1)
|
||||
pio[1] = pipe
|
||||
table.insert(data.handles, pipe)
|
||||
end
|
||||
|
||||
if prev_pipe then
|
||||
prev_pipe.stream.redirect[0] = rawget(pio, 0)
|
||||
prev_pipe.stream.next = thread
|
||||
pio[0] = prev_pipe
|
||||
end
|
||||
|
||||
prev_pipe = pipe
|
||||
end
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.glob(glob_pattern)
|
||||
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(sh.internal.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 break end
|
||||
relative_separator = "/"
|
||||
end
|
||||
-- if no next_paths were hit here, the ENTIRE glob value is not a path
|
||||
return paths
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.getMatchingPrograms(baseName)
|
||||
local result = {}
|
||||
local result_keys = {} -- cache for fast value lookup
|
||||
-- TODO only matching files with .lua extension for now, might want to
|
||||
-- extend this to other extensions at some point? env var? file attrs?
|
||||
if not baseName or #baseName == 0 then
|
||||
baseName = "^(.*)%.lua$"
|
||||
else
|
||||
baseName = "^(" .. text.escapeMagic(baseName) .. ".*)%.lua$"
|
||||
end
|
||||
for basePath in string.gmatch(os.getenv("PATH"), "[^:]+") do
|
||||
for file in fs.list(shell.resolve(basePath)) do
|
||||
local match = file:match(baseName)
|
||||
if match and not result_keys[match] then
|
||||
table.insert(result, match)
|
||||
result_keys[match] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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, simple = 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 --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
-- verifies that no pipes are doubled up nor at the start nor end of words
|
||||
function --[[@delayloaded-start@]] 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.find(text.syntax, {";"}) -- all symbols before ; in syntax CAN be repeated
|
||||
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, "parse error near " .. state
|
||||
else
|
||||
return true
|
||||
end
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.boolean_executor(chains, predicator)
|
||||
local function not_gate(result)
|
||||
return sh.internal.command_passed(result) and 1 or 0
|
||||
end
|
||||
|
||||
local last = true
|
||||
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 = not_gate(prev(n,i))
|
||||
predicator = prev
|
||||
return result
|
||||
end
|
||||
end
|
||||
if chomped then
|
||||
stage = negation_stage
|
||||
end
|
||||
if #next > 0 then
|
||||
last = 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
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.splitStatements(words, semicolon)
|
||||
checkArg(1, words, "table")
|
||||
checkArg(2, semicolon, "string", "nil")
|
||||
semicolon = semicolon or ";"
|
||||
|
||||
return tx.partition(words, function(g, i, t)
|
||||
if isWord(g,semicolon) then
|
||||
return i, i
|
||||
end
|
||||
end, true)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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 isWord(w,pc) then
|
||||
return true
|
||||
end
|
||||
end, true) -- drop |s
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.groupChains(s)
|
||||
checkArg(1,s,"table")
|
||||
return tx.partition(s,function(w)return isWordOf(w,{"&&","||"})end)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.remove_negation(chain)
|
||||
if isWord(chain[1],"!") then
|
||||
table.remove(chain, 1)
|
||||
return true and not sh.internal.remove_negation(chain)
|
||||
end
|
||||
return false
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.newMemoryStream()
|
||||
local memoryStream = {}
|
||||
|
||||
function memoryStream:close()
|
||||
self.closed = true
|
||||
self.redirect = {}
|
||||
end
|
||||
|
||||
function memoryStream:seek()
|
||||
return nil, "bad file descriptor"
|
||||
end
|
||||
|
||||
function memoryStream:read(n)
|
||||
if self.closed then
|
||||
return nil -- eof
|
||||
end
|
||||
if self.redirect[0] then
|
||||
-- popen could be using this code path
|
||||
-- if that is the case, it is important to leave stream.buffer alone
|
||||
return self.redirect[0]:read(n)
|
||||
elseif self.buffer == "" then
|
||||
coroutine.yield()
|
||||
end
|
||||
local result = string.sub(self.buffer, 1, n)
|
||||
self.buffer = string.sub(self.buffer, n + 1)
|
||||
return result
|
||||
end
|
||||
|
||||
function memoryStream:write(value)
|
||||
if not self.redirect[1] and 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
|
||||
elseif self.redirect[1] then
|
||||
return self.redirect[1]:write(value)
|
||||
elseif not self.closed then
|
||||
self.buffer = self.buffer .. value
|
||||
local result = table.pack(coroutine.resume(self.next))
|
||||
if coroutine.status(self.next) == "dead" then
|
||||
self:close()
|
||||
end
|
||||
if not result[1] then
|
||||
io.stderr:write(tostring(result[2]) .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
return self
|
||||
end
|
||||
os.exit(0) -- abort the current process: SIGPIPE
|
||||
end
|
||||
|
||||
local stream = {closed = false, buffer = "",
|
||||
redirect = {}, result = {}}
|
||||
local metatable = {__index = memoryStream,
|
||||
__metatable = "memorystream"}
|
||||
return setmetatable(stream, metatable)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.execute_complex(statements, eargs, env)
|
||||
for si=1,#statements do local s = statements[si]
|
||||
local chains = sh.internal.groupChains(s)
|
||||
local last_code = 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)
|
||||
end
|
||||
return true
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
|
||||
function --[[@delayloaded-start@]] sh.internal.parse_sub(input)
|
||||
-- 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
|
||||
|
||||
local sub = io.popen(capture)
|
||||
local result = input:sub(i, fi - 1) .. sub:read("*a")
|
||||
sub:close()
|
||||
-- all whitespace is replaced by single spaces
|
||||
-- we requote the result because tokenize will respect this as text
|
||||
table.insert(packed, (text.trim(result):gsub("%s+"," ")))
|
||||
|
||||
i = si+1
|
||||
end
|
||||
|
||||
return table.concat(packed)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
return sh, local_env
|
||||
return sh
|
||||
|
@ -1,5 +1,4 @@
|
||||
local fs = require("filesystem")
|
||||
local text = require("text")
|
||||
local unicode = require("unicode")
|
||||
local process = require("process")
|
||||
|
||||
@ -107,25 +106,6 @@ function shell.aliases()
|
||||
return pairs(process.info().data.aliases)
|
||||
end
|
||||
|
||||
function shell.resolveAlias(command, args)
|
||||
checkArg(1, command, "string")
|
||||
checkArg(2, args, "table", "nil")
|
||||
args = args or {}
|
||||
local program, lastProgram = command, nil
|
||||
while true do
|
||||
local tokens = text.tokenize(shell.getAlias(program) or program)
|
||||
program = tokens[1]
|
||||
if program == lastProgram then
|
||||
break
|
||||
end
|
||||
lastProgram = program
|
||||
for i = #tokens, 2, -1 do
|
||||
table.insert(args, 1, tokens[i])
|
||||
end
|
||||
end
|
||||
return program, args
|
||||
end
|
||||
|
||||
function shell.getWorkingDirectory()
|
||||
-- if no env PWD default to /
|
||||
return os.getenv("PWD") or "/"
|
||||
|
@ -1,606 +1,78 @@
|
||||
local tty = require("tty")
|
||||
local unicode = require("unicode")
|
||||
local event = require("event")
|
||||
local process = require("process")
|
||||
local kb = require("keyboard")
|
||||
local component = require("component")
|
||||
local computer = require("computer")
|
||||
local process = require("process")
|
||||
|
||||
local kb = require("keyboard")
|
||||
local keys = kb.keys
|
||||
|
||||
local term = {}
|
||||
term.internal = {}
|
||||
local term = setmetatable({internal={}}, {__index=tty})
|
||||
|
||||
function term.internal.window()
|
||||
return process.info().data.window
|
||||
end
|
||||
|
||||
local W = term.internal.window
|
||||
local local_env = {unicode=unicode,event=event,process=process,W=W,kb=kb}
|
||||
|
||||
local gpu_intercept = {}
|
||||
local function update_viewport(window, width, height)
|
||||
window = window or W()
|
||||
local gpu = window.gpu
|
||||
if not gpu then return end
|
||||
if not gpu_intercept[gpu] then
|
||||
gpu_intercept[gpu] = {} -- 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(...)
|
||||
gpu_intercept[gpu] = {}
|
||||
return setr(...)
|
||||
end
|
||||
gpu.setViewport = function(...)
|
||||
gpu_intercept[gpu] = {}
|
||||
return setv(...)
|
||||
end
|
||||
local function as_window(window, func, ...)
|
||||
local data = process.info().data
|
||||
if not data.window then
|
||||
return func(...)
|
||||
end
|
||||
if not width and not gpu_intercept[gpu][window] then
|
||||
width, height = gpu.getViewport()
|
||||
end
|
||||
if width then
|
||||
window:resize(width, height)
|
||||
gpu_intercept[gpu][window] = true
|
||||
end
|
||||
end
|
||||
|
||||
local function resize(window, width, height)
|
||||
window.w, window.h = width, height
|
||||
local prev = rawget(data, "window")
|
||||
data.window = window
|
||||
local ret = table.pack(func(...))
|
||||
data.window = prev
|
||||
return table.unpack(ret, ret.n)
|
||||
end
|
||||
|
||||
function term.internal.open(...)
|
||||
local dx, dy, w, h = ...
|
||||
local window = {x=1,y=1,fullscreen=select("#",...)==0,dx=dx or 0,dy=dy or 0,w=w,h=h,blink=true,resize=resize}
|
||||
event.listen("screen_resized", function(_,addr,w,h)
|
||||
if term.isAvailable(window) and term.screen(window) == addr and window.fullscreen then
|
||||
update_viewport(window, w, h)
|
||||
local window = {fullscreen=select("#",...) == 0, blink = true}
|
||||
|
||||
-- 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
|
||||
end)
|
||||
})
|
||||
|
||||
-- first time we open a pty the current tty.window must become the process window
|
||||
if not term.internal.window() then
|
||||
local init_index = 2
|
||||
while process.info(init_index) do
|
||||
init_index = init_index + 1
|
||||
end
|
||||
process.info(init_index - 1).data.window = tty.window
|
||||
tty.window = nil
|
||||
setmetatable(tty,
|
||||
{
|
||||
__index = function(tbl, key)
|
||||
if key == "window" then
|
||||
return term.internal.window()
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
as_window(window, tty.setViewport, w, h, dx, dy, 1, 1)
|
||||
return window
|
||||
end
|
||||
|
||||
function term.getViewport(window)
|
||||
window = window or W()
|
||||
update_viewport(window)
|
||||
return window.w, window.h, window.dx, window.dy, window.x, window.y
|
||||
end
|
||||
|
||||
function term.setViewport(w,h,dx,dy,x,y,window)
|
||||
window = window or W()
|
||||
|
||||
dx,dy,x,y = dx or 0,dy or 0,x or 1,y or 1
|
||||
if not w or not h then
|
||||
local gw,gh = window.gpu.getViewport()
|
||||
w,h = w or gw, h or gh
|
||||
end
|
||||
|
||||
window.dx,window.dy,window.x,window.y,window.gw,window.gh = dx, dy, x, y, gw, gh
|
||||
update_viewport(window, w, h)
|
||||
end
|
||||
|
||||
function term.gpu(window)
|
||||
window = window or W()
|
||||
return window.gpu
|
||||
end
|
||||
|
||||
function term.clear()
|
||||
local w = W()
|
||||
local gpu = w.gpu
|
||||
if not gpu then return end
|
||||
gpu.fill(1+w.dx,1+w.dy,w.w,w.h," ")
|
||||
w.x,w.y=1,1
|
||||
end
|
||||
|
||||
function term.isAvailable(w)
|
||||
w = w or W()
|
||||
return w and not not (w.gpu and w.gpu.getScreen())
|
||||
end
|
||||
|
||||
function term.internal.pull(input, timeout, ...)
|
||||
timeout = timeout or math.huge
|
||||
|
||||
local w = W()
|
||||
local d, h, dx, dy, x, y = term.getViewport(w)
|
||||
local out = (x<1 or x>d or y<1 or y>h)
|
||||
|
||||
if input and out then
|
||||
input:move(0)
|
||||
y = w.y
|
||||
input:scroll()
|
||||
end
|
||||
|
||||
x, y = w.x + dx, w.y + dy
|
||||
local gpu = (input or not out) and w.gpu
|
||||
|
||||
local bgColor, bgIsPalette
|
||||
local fgColor, fgIsPalette
|
||||
local char_at_cursor
|
||||
local blinking
|
||||
if gpu then
|
||||
bgColor, bgIsPalette = gpu.getBackground()
|
||||
-- it can happen during a type of race condition when a screen is removed
|
||||
if not bgColor then
|
||||
return nil, "interrupted"
|
||||
end
|
||||
|
||||
fgColor, fgIsPalette = gpu.getForeground()
|
||||
char_at_cursor = gpu.get(x, y)
|
||||
|
||||
blinking = w.blink
|
||||
if input then
|
||||
blinking = input.blink
|
||||
end
|
||||
end
|
||||
|
||||
-- get the next event
|
||||
local blinked = false
|
||||
local done = false
|
||||
local signal
|
||||
while true do
|
||||
if gpu then
|
||||
if not blinked and not done then
|
||||
gpu.setForeground(bgColor, bgIsPalette)
|
||||
gpu.setBackground(fgColor, fgIsPalette)
|
||||
gpu.set(x, y, char_at_cursor)
|
||||
gpu.setForeground(fgColor, fgIsPalette)
|
||||
gpu.setBackground(bgColor, bgIsPalette)
|
||||
blinked = true
|
||||
elseif blinked then
|
||||
gpu.set(x, y, char_at_cursor)
|
||||
blinked = false
|
||||
end
|
||||
end
|
||||
|
||||
if done then
|
||||
return table.unpack(signal, 1, signal.n)
|
||||
end
|
||||
|
||||
signal = table.pack(event.pull(math.min(.5, timeout), ...))
|
||||
timeout = timeout - .5
|
||||
done = signal.n > 1 or timeout < .5
|
||||
end
|
||||
end
|
||||
|
||||
function term.pull(...)
|
||||
local args = table.pack(...)
|
||||
local timeout = nil
|
||||
if type(args[1]) == "number" then
|
||||
timeout = table.remove(args, 1)
|
||||
args.n = args.n - 1
|
||||
end
|
||||
return term.internal.pull(nil, timeout, table.unpack(args, 1, args.n))
|
||||
end
|
||||
|
||||
function term.read(history,dobreak,hintHandler,pwchar,filter)
|
||||
if not io.stdin.tty then return io.read() end
|
||||
local ops = history or {}
|
||||
ops.dobreak = ops.dobreak
|
||||
if ops.dobreak==nil then ops.dobreak = dobreak end
|
||||
ops.hintHandler = ops.hintHandler or hintHandler
|
||||
ops.pwchar = ops.pwchar or pwchar
|
||||
ops.filter = ops.filter or filter
|
||||
return term.readKeyboard(ops)
|
||||
end
|
||||
|
||||
function term.internal.split(input)
|
||||
local data,index=input.data,input.index
|
||||
local dlen = unicode.len(data)
|
||||
index=math.max(0,math.min(index,dlen))
|
||||
local tail=dlen-index
|
||||
return unicode.sub(data,1,index),tail==0 and""or unicode.sub(data,-tail)
|
||||
end
|
||||
|
||||
function term.internal.build_vertical_reader(input)
|
||||
input.sy = 0
|
||||
input.scroll = function(_)
|
||||
_.sy = _.sy + term.internal.scroll(_.w)
|
||||
_.w.y = math.min(_.w.y,_.w.h)
|
||||
end
|
||||
input.move = function(_,n)
|
||||
local w=_.w
|
||||
_.index = math.min(math.max(0,_.index+n),unicode.len(_.data))
|
||||
local s1,s2 = term.internal.split(_)
|
||||
s2 = unicode.sub(s2.." ",1,1)
|
||||
local data_remaining = ("_"):rep(_.promptx-1)..s1..s2
|
||||
w.y = _.prompty - _.sy
|
||||
while true do
|
||||
local wlen_remaining = unicode.wlen(data_remaining)
|
||||
if wlen_remaining > w.w then
|
||||
local line_cut = unicode.wtrunc(data_remaining, w.w+1)
|
||||
data_remaining = unicode.sub(data_remaining,unicode.len(line_cut)+1)
|
||||
w.y=w.y+1
|
||||
else
|
||||
w.x = wlen_remaining-unicode.wlen(s2)+1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
input.clear_tail = function(_)
|
||||
local win=_.w
|
||||
local oi,w,h,dx,dy,ox,oy = _.index,term.getViewport(win)
|
||||
_:move(math.huge)
|
||||
_:move(-1)
|
||||
local ex,ey=win.x,win.y
|
||||
win.x,win.y,_.index=ox,oy,oi
|
||||
x=oy==ey and ox or 1
|
||||
win.gpu.fill(x+dx,ey+dy,w-x+1,1," ")
|
||||
end
|
||||
input.update = function(_,arg)
|
||||
local w,cursor,suffix=_.w
|
||||
local s1,s2=term.internal.split(_)
|
||||
if type(arg) == "number" then
|
||||
local ndata
|
||||
if arg < 0 then if _.index<=0 then return end
|
||||
_:move(-1)
|
||||
ndata=unicode.sub(s1,1,-2)..s2
|
||||
else if _.index>=unicode.len(_.data) then return end
|
||||
s2=unicode.sub(s2,2)
|
||||
ndata=s1..s2
|
||||
end
|
||||
suffix=s2
|
||||
input:clear_tail()
|
||||
_.data = ndata
|
||||
else
|
||||
_.data=s1..arg..s2
|
||||
_.index=_.index+unicode.len(arg)
|
||||
cursor,suffix=arg,s2
|
||||
end
|
||||
if cursor then _:draw(_.mask(cursor)) end
|
||||
if suffix and suffix~="" then
|
||||
local px,py,ps=w.x,w.y,_.sy
|
||||
_:draw(_.mask(suffix))
|
||||
w.x,w.y=px,py-(_.sy-ps)
|
||||
end
|
||||
end
|
||||
input.clear = function(_)
|
||||
_:move(-math.huge)
|
||||
_:draw((" "):rep(unicode.wlen(_.data)))
|
||||
_:move(-math.huge)
|
||||
_.index=0
|
||||
_.data=""
|
||||
end
|
||||
input.draw = function(_,text)
|
||||
_.sy = _.sy + term.drawText(text,true)
|
||||
end
|
||||
end
|
||||
|
||||
function term.internal.read_history(history,input,change)
|
||||
if not change then
|
||||
if unicode.wlen(input.data) > 0 then
|
||||
table.insert(history.list,1,input.data)
|
||||
history.list[(tonumber(os.getenv("HISTSIZE")) or 10)+1]=nil
|
||||
history.list[0]=nil
|
||||
end
|
||||
else
|
||||
local ni = history.index + change
|
||||
if ni >= 0 and ni <= #history.list then
|
||||
history.list[history.index]=input.data
|
||||
history.index = ni
|
||||
input:clear()
|
||||
input:update(history.list[ni])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function term.readKeyboard(ops)
|
||||
checkArg(1,ops,"table")
|
||||
local filter = ops.filter and function(i) return term.internal.filter(ops.filter,i) end or term.internal.nop
|
||||
local pwchar = ops.pwchar and function(i) return term.internal.mask(ops.pwchar,i) end or term.internal.nop
|
||||
local history,db,hints={list=ops,index=0},ops.dobreak,{handler=ops.hintHandler}
|
||||
local w=W()
|
||||
local draw=io.stdin.tty and term.drawText or term.internal.nop
|
||||
local input={w=w,promptx=w.x,prompty=w.y,index=0,data="",mask=pwchar}
|
||||
input.blink = ops.blink
|
||||
if input.blink == nil then
|
||||
input.blink = w.blink
|
||||
end
|
||||
|
||||
-- two wrap types currently supported, vertical and hortizontal
|
||||
if ops.nowrap then term.internal.build_horizontal_reader(input)
|
||||
else term.internal.build_vertical_reader(input)
|
||||
end
|
||||
|
||||
while true do
|
||||
local name, address, char, code = term.internal.pull(input)
|
||||
if not term.isAvailable() then
|
||||
return
|
||||
end
|
||||
|
||||
-- we have to keep checking what kb is active in case it is switching during use
|
||||
-- we could have multiple screens, each with keyboards active
|
||||
local main_kb = term.keyboard(w)
|
||||
local main_sc = term.screen(w)
|
||||
local c
|
||||
local backup_cache = hints.cache
|
||||
if name == "interrupted" then
|
||||
draw("^C\n",true)
|
||||
return false
|
||||
elseif address == main_kb or address == main_sc then
|
||||
if name == "touch" or name == "drag" then
|
||||
term.internal.onTouch(input,char,code)
|
||||
elseif name == "clipboard" then
|
||||
c = term.internal.clipboard(char)
|
||||
hints.cache = nil
|
||||
elseif name == "key_down" then
|
||||
hints.cache = nil
|
||||
local ctrl = kb.isControlDown(address)
|
||||
if ctrl and code == keys.d then return
|
||||
elseif code == keys.tab then
|
||||
hints.cache = backup_cache
|
||||
term.internal.tab(input,hints)
|
||||
elseif (code == keys.enter or code == keys.numpadenter)
|
||||
and filter(input) then
|
||||
input:move(math.huge)
|
||||
if db ~= false then
|
||||
draw("\n")
|
||||
end
|
||||
term.internal.read_history(history,input)
|
||||
return input.data .. "\n"
|
||||
elseif code == keys.up then term.internal.read_history(history, input, 1)
|
||||
elseif code == keys.down then term.internal.read_history(history, input, -1)
|
||||
elseif code == keys.left then input:move(ctrl and term.internal.ctrl_movement(input, -1) or -1)
|
||||
elseif code == keys.right then input:move(ctrl and term.internal.ctrl_movement(input, 1) or 1)
|
||||
elseif code == keys.home then input:move(-math.huge)
|
||||
elseif code == keys["end"] then input:move( math.huge)
|
||||
elseif code == keys.back then c = -1
|
||||
elseif code == keys.delete then c = 0
|
||||
elseif ctrl and char == "w"then -- TODO: cut word
|
||||
elseif char >= 32 then c = unicode.char(char)
|
||||
else hints.cache = backup_cache -- ignored chars shouldn't clear hint cache
|
||||
end
|
||||
end
|
||||
-- if we obtained something (c) to handle
|
||||
if c then
|
||||
input:update(c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- cannot use term.write = io.write because io.write invokes metatable
|
||||
function term.write(value,wrap)
|
||||
local stdout = io.output()
|
||||
local stream = stdout and stdout.stream
|
||||
local previous_wrap = stream.wrap
|
||||
stream.wrap = wrap == nil and true or wrap
|
||||
stdout:write(value)
|
||||
stdout:flush()
|
||||
stream.wrap = previous_wrap
|
||||
end
|
||||
|
||||
function term.getCursor()
|
||||
local w = W()
|
||||
return w.x,w.y
|
||||
end
|
||||
|
||||
function term.setCursor(x,y)
|
||||
local w = W()
|
||||
w.x,w.y=x,y
|
||||
end
|
||||
|
||||
function term.drawText(value, wrap, window)
|
||||
window = window or W()
|
||||
if not window then return end
|
||||
local gpu = window.gpu
|
||||
if not gpu then return end
|
||||
local w,h,dx,dy,x,y = term.getViewport(window)
|
||||
local sy = 0
|
||||
local vlen = #value
|
||||
local index = 1
|
||||
local cr_last,beeped = false,false
|
||||
local function scroll(_sy,_y)
|
||||
return _sy + term.internal.scroll(window,_y-h), math.min(_y,h)
|
||||
end
|
||||
local uptime = computer.uptime
|
||||
local last_sleep = uptime()
|
||||
while index <= vlen do
|
||||
if uptime() - last_sleep > 4 then
|
||||
os.sleep(0)
|
||||
last_sleep = uptime()
|
||||
end
|
||||
local si,ei = value:find("[\t\r\n\a]", index)
|
||||
si = si or vlen+1
|
||||
if index==si then
|
||||
local delim = value:sub(index, index)
|
||||
if delim=="\t" then
|
||||
x=((x-1)-((x-1)%8))+9
|
||||
elseif delim=="\r" or (delim=="\n" and not cr_last) then
|
||||
x,y=1,y+1
|
||||
sy,y = scroll(sy,y)
|
||||
elseif delim=="\a" and not beeped then
|
||||
computer.beep()
|
||||
beeped = true
|
||||
end
|
||||
cr_last = delim == "\r"
|
||||
else
|
||||
sy,y = scroll(sy,y)
|
||||
si = si - 1
|
||||
local next = value:sub(index, si)
|
||||
local wlen_needed = unicode.wlen(next)
|
||||
local slen = #next
|
||||
local wlen_remaining = w - x + 1
|
||||
local clean_end = ""
|
||||
if wlen_remaining < wlen_needed then
|
||||
next = unicode.wtrunc(next, wlen_remaining + 1)
|
||||
wlen_needed = unicode.wlen(next)
|
||||
clean_end = (" "):rep(wlen_remaining-wlen_needed)
|
||||
if not wrap then
|
||||
si = math.huge
|
||||
end
|
||||
end
|
||||
gpu.set(x+dx,y+dy,next..clean_end)
|
||||
x = x + wlen_needed
|
||||
if wrap and slen ~= #next then
|
||||
si = si - (slen - #next)
|
||||
x = 1
|
||||
y = y + 1
|
||||
end
|
||||
end
|
||||
index = si + 1
|
||||
end
|
||||
|
||||
window.x,window.y = x,y
|
||||
return sy
|
||||
end
|
||||
|
||||
function term.internal.scroll(w,n)
|
||||
w = w or W()
|
||||
local gpu,d,h,dx,dy,x,y = w.gpu,term.getViewport(w)
|
||||
n = n or (y-h)
|
||||
if n <= 0 then return 0 end
|
||||
gpu.copy(dx+1,dy+n+1,d,h-n,0,-n)
|
||||
gpu.fill(dx+1,dy+h-n+1,d,n," ")
|
||||
return n
|
||||
end
|
||||
|
||||
function term.internal.nop(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
function term.setCursorBlink(enabled)
|
||||
W().blink=enabled
|
||||
end
|
||||
|
||||
function term.getCursorBlink()
|
||||
return W().blink
|
||||
end
|
||||
|
||||
function term.bind(gpu, window)
|
||||
window = window or W()
|
||||
window.gpu = gpu or window.gpu
|
||||
window.keyboard = nil -- without a keyboard bound, always use the screen's main keyboard (1st)
|
||||
if window.fullscreen then
|
||||
term.setViewport(nil,nil,nil,nil,window.x,window.y,window)
|
||||
end
|
||||
end
|
||||
|
||||
function term.keyboard(window)
|
||||
window = window or W() or {} -- this method needs to be safe even if there is no terminal window (e.g. no gpu)
|
||||
|
||||
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 = term.screen(window)
|
||||
|
||||
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 scren, 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)
|
||||
-- custom terminals should avoid designs that require
|
||||
-- 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 term.screen(window)
|
||||
window = window or W()
|
||||
local gpu = window.gpu
|
||||
if not gpu then
|
||||
return nil
|
||||
end
|
||||
return gpu.getScreen()
|
||||
end
|
||||
|
||||
function --[[@delayloaded-start@]] term.scroll(number, window)
|
||||
-- if zero scroll length is requested, do nothing
|
||||
if number == 0 then return end
|
||||
-- window is optional, default to current active terminal
|
||||
window = window or W()
|
||||
-- gpu works with global coordinates
|
||||
local gpu,width,height,dx,dy,x,y = window.gpu,term.getViewport(w)
|
||||
|
||||
-- scroll request can be too large
|
||||
local abs_number = math.abs(number)
|
||||
if (abs_number >= height) then
|
||||
term.clear()
|
||||
return
|
||||
end
|
||||
|
||||
-- box positions to shift
|
||||
local box_height = height - abs_number
|
||||
local top = 0
|
||||
if number > 0 then
|
||||
top = number -- (e.g. 1 scroll moves box at 2)
|
||||
end
|
||||
|
||||
gpu.copy(dx + 1, dy + top + 1, width, box_height, 0, -number)
|
||||
|
||||
local fill_top = 0
|
||||
if number > 0 then
|
||||
fill_top = box_height
|
||||
end
|
||||
|
||||
gpu.fill(dx + 1, dy + fill_top + 1, width, abs_number, " ")
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.ctrl_movement(input, dir)
|
||||
local index, data = input.index, input.data
|
||||
|
||||
local function isEdge(char)
|
||||
return char == "" or not not char:find("%s")
|
||||
end
|
||||
|
||||
local last=dir<0 and 0 or unicode.len(data)
|
||||
local start=index+dir+1
|
||||
for i=start,last,dir do
|
||||
local a,b = unicode.sub(data, i-1, i-1), unicode.sub(data, i, i)
|
||||
if isEdge(a) and not isEdge(b) then return i-(index+1) end
|
||||
end
|
||||
return last - index
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.onTouch(input,gx,gy)
|
||||
if input.data == "" then return end
|
||||
input:move(-math.huge)
|
||||
local w = W()
|
||||
gx,gy=gx-w.dx,gy-w.dy
|
||||
local x2,y2,d = input.w.x,input.w.y,input.w.w
|
||||
local char_width_to_move = ((gy*d+gx)-(y2*d+x2))
|
||||
if char_width_to_move <= 0 then return end
|
||||
local total_wlen = unicode.wlen(input.data)
|
||||
if char_width_to_move >= total_wlen then
|
||||
input:move(math.huge)
|
||||
else
|
||||
local chars_to_move = unicode.wtrunc(input.data, char_width_to_move + 1)
|
||||
input:move(unicode.len(chars_to_move))
|
||||
end
|
||||
-- fake white space can make the index off, redo adjustment for alignment
|
||||
x2,y2,d = input.w.x,input.w.y,input.w.w
|
||||
char_width_to_move = ((gy*d+gx)-(y2*d+x2))
|
||||
if (char_width_to_move < 0) then
|
||||
-- using char_width_to_move as a type of index is wrong, but large enough and helps to speed this up
|
||||
local up_to_cursor = unicode.sub(input.data, input.index+char_width_to_move, input.index)
|
||||
local full_wlen = unicode.wlen(up_to_cursor)
|
||||
local without_tail = unicode.wtrunc(up_to_cursor, full_wlen + char_width_to_move + 1)
|
||||
local chars_cut = unicode.len(up_to_cursor) - unicode.len(without_tail)
|
||||
input:move(-chars_cut)
|
||||
end
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.build_horizontal_reader(input)
|
||||
term.internal.build_vertical_reader(input)
|
||||
input.clear_tail = function(_)
|
||||
local w,h,dx,dy,x,y = term.getViewport(_.w)
|
||||
local s1,s2=term.internal.split(_)
|
||||
local function build_horizontal_reader(cursor)
|
||||
cursor.clear_tail = function(_)
|
||||
local w,h,dx,dy,x,y = tty.getViewport()
|
||||
local s1,s2=tty.internal.split(_)
|
||||
local wlen = math.min(unicode.wlen(s2),w-x+1)
|
||||
_.w.gpu.fill(x+dx,y+dy,wlen,1," ")
|
||||
tty.gpu().fill(x+dx,y+dy,wlen,1," ")
|
||||
end
|
||||
input.move = function(_,n)
|
||||
local win = _.w
|
||||
cursor.move = function(_,n)
|
||||
local win = tty.window
|
||||
local a = _.index
|
||||
local b = math.max(0,math.min(unicode.len(_.data),_.index+n))
|
||||
_.index = b
|
||||
@ -609,13 +81,13 @@ function --[[@delayloaded-start@]] term.internal.build_horizontal_reader(input)
|
||||
win.x = win.x + wlen_moved * (n<0 and -1 or 1)
|
||||
_:scroll()
|
||||
end
|
||||
input.draw = function(_,text)
|
||||
term.drawText(text,false)
|
||||
cursor.draw = function(_, text)
|
||||
tty.drawText(text, true)
|
||||
end
|
||||
input.scroll = function(_)
|
||||
local win = _.w
|
||||
cursor.scroll = function(_)
|
||||
local win = tty.window
|
||||
local gpu,data,px,i = win.gpu,_.data,_.promptx,_.index
|
||||
local w,h,dx,dy,x,y = term.getViewport(win)
|
||||
local w,h,dx,dy,x,y = tty.getViewport()
|
||||
win.x = math.max(_.promptx, math.min(w, x))
|
||||
local len = unicode.len(data)
|
||||
local available,sx,sy,last = w-px+1,px+dx,y+dy,i==len
|
||||
@ -640,94 +112,118 @@ function --[[@delayloaded-start@]] term.internal.build_horizontal_reader(input)
|
||||
gpu.set(sx,sy,data)
|
||||
end
|
||||
end
|
||||
input.clear = function(_)
|
||||
local win = _.w
|
||||
cursor.clear = function(_)
|
||||
local win = tty.window
|
||||
local gpu,data,px=win.gpu,_.data,_.promptx
|
||||
local w,h,dx,dy,x,y = term.getViewport(win)
|
||||
local w,h,dx,dy,x,y = tty.getViewport()
|
||||
_.index,_.data,win.x=0,"",px
|
||||
gpu.fill(px+dx,y+dy,w-px+1-dx,1," ")
|
||||
end
|
||||
end --[[@delayloaded-end@]]
|
||||
end
|
||||
|
||||
function --[[@delayloaded-start@]] term.clearLine(window)
|
||||
window = window or W()
|
||||
local w,h,dx,dy,x,y = term.getViewport(window)
|
||||
window.gpu.fill(dx+1,dy+math.max(1,math.min(y,h)),w,1," ")
|
||||
window.x=1
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.mask(mask,input)
|
||||
if not mask then return input end
|
||||
if type(mask) == "function" then return mask(input) end
|
||||
return mask:rep(unicode.wlen(input))
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.filter(filter,input)
|
||||
if not filter then return true
|
||||
elseif type(filter) == "string" then return input.data:match(filter)
|
||||
elseif filter(input.data) then return true
|
||||
else require("computer").beep(2000, 0.1) end
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.tab(input,hints)
|
||||
if not hints.handler then return end
|
||||
local main_kb = term.keyboard()
|
||||
-- term may not have a keyboard
|
||||
-- in which case, we shouldn't be handling tab events
|
||||
if not main_kb then
|
||||
return
|
||||
end
|
||||
if not hints.cache then
|
||||
local data = hints.handler
|
||||
hints.handler = function(...)
|
||||
if type(data) == "table" then
|
||||
return data
|
||||
else
|
||||
return data(...) or {}
|
||||
local function inject_filter(handler, filter)
|
||||
if filter then
|
||||
if type(filter) == "string" then
|
||||
local filter_text = filter
|
||||
filter = function(text)
|
||||
return text:match(filter_text)
|
||||
end
|
||||
end
|
||||
hints.cache = hints.handler(input.data, input.index + 1)
|
||||
hints.cache.i = -1
|
||||
|
||||
local mt =
|
||||
{
|
||||
__newindex = function(tbl, key, value)
|
||||
if key == "key_down" then
|
||||
local tty_key_down = value
|
||||
value = function(handler, cursor, char, code)
|
||||
if code == keys.enter or code == keys.numpadenter then
|
||||
if not filter(cursor.data) then
|
||||
computer.beep(2000, 0.1)
|
||||
return false -- ignore
|
||||
end
|
||||
end
|
||||
return tty_key_down(handler, cursor, char, code)
|
||||
end
|
||||
end
|
||||
rawset(tbl, key, value)
|
||||
end
|
||||
}
|
||||
setmetatable(handler, mt)
|
||||
end
|
||||
end
|
||||
|
||||
local function inject_mask(cursor, dobreak, pwchar)
|
||||
if not pwchar and dobreak ~= false then
|
||||
return
|
||||
end
|
||||
|
||||
local cache = hints.cache
|
||||
local cache_size = #cache
|
||||
|
||||
if cache_size == 1 and cache.i == 0 then
|
||||
-- there was only one solution, and the user is asking for the next
|
||||
hints.cache = hints.handler(cache[1], input.index + 1)
|
||||
hints.cache.i = -1
|
||||
cache = hints.cache
|
||||
cache_size = #cache
|
||||
end
|
||||
|
||||
local change = kb.isShiftDown(main_kb) 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(input.data) - input.index
|
||||
input:clear()
|
||||
input:update(next)
|
||||
input:move(-tail)
|
||||
end
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.getGlobalArea(window)
|
||||
local w,h,dx,dy = term.getViewport(window)
|
||||
return dx+1,dy+1,w,h
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] term.internal.clipboard(char)
|
||||
local first_line, end_index = char:find("\13?\10")
|
||||
if first_line then
|
||||
local after = char:sub(end_index + 1)
|
||||
if after ~= "" then
|
||||
require("computer").pushSignal("key_down", term.keyboard(), 13, 28)
|
||||
require("computer").pushSignal("clipboard", term.keyboard(), after)
|
||||
if pwchar then
|
||||
if type(pwchar) == "string" then
|
||||
local pwchar_text = pwchar
|
||||
pwchar = function(text)
|
||||
return text:gsub(".", pwchar_text)
|
||||
end
|
||||
end
|
||||
char = char:sub(1, first_line - 1)
|
||||
end
|
||||
return char
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
return term, local_env
|
||||
local cursor_draw = cursor.draw
|
||||
cursor.draw = function(cursor, text)
|
||||
local pre, newline = text:match("(.-)(\n?)$")
|
||||
if dobreak == false then
|
||||
newline = ""
|
||||
end
|
||||
if pwchar then
|
||||
pre = pwchar(pre)
|
||||
end
|
||||
return cursor_draw(cursor, pre .. newline)
|
||||
end
|
||||
end
|
||||
|
||||
function term.read(history, dobreak, hint, pwchar, filter)
|
||||
if not io.stdin.tty then
|
||||
return io.read()
|
||||
end
|
||||
local handler = history or {}
|
||||
handler.hint = handler.hint or hint
|
||||
|
||||
local cursor = tty.internal.build_vertical_reader()
|
||||
if handler.nowrap then
|
||||
build_horizontal_reader(cursor)
|
||||
end
|
||||
|
||||
inject_filter(handler, filter)
|
||||
inject_mask(cursor, dobreak, pwchar)
|
||||
-- todo, make blinking work from here
|
||||
-- handler.blink or w.blink
|
||||
|
||||
return tty.read(handler, cursor)
|
||||
end
|
||||
|
||||
function term.getGlobalArea(window)
|
||||
local w,h,dx,dy = as_window(window, tty.getViewport)
|
||||
return dx+1,dy+1,w,h
|
||||
end
|
||||
|
||||
function term.clearLine(window)
|
||||
window = window or tty.window
|
||||
local w,h,dx,dy,x,y = as_window(window, tty.getViewport)
|
||||
window.gpu.fill(dx+1,dy+math.max(1,math.min(y,h)),w,1," ")
|
||||
window.x=1
|
||||
end
|
||||
|
||||
function term.pull(...)
|
||||
local args = table.pack(...)
|
||||
local timeout = nil
|
||||
if type(args[1]) == "number" then
|
||||
timeout = table.remove(args, 1)
|
||||
args.n = args.n - 1
|
||||
end
|
||||
return tty.pull(nil, timeout, table.unpack(args, 1, args.n))
|
||||
end
|
||||
|
||||
function term.bind(gpu, window)
|
||||
return as_window(window, tty.bind, gpu)
|
||||
end
|
||||
|
||||
return term
|
||||
|
||||
|
@ -5,23 +5,14 @@ local tx = require("transforms")
|
||||
-- See package.lua and the api wiki for more information
|
||||
|
||||
local text = {}
|
||||
local local_env = {tx=tx,unicode=unicode}
|
||||
|
||||
text.internal = {}
|
||||
|
||||
text.syntax = {"^%d?>>?&%d+$",";","&&","||?","^%d?>>?",">>?","<"}
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
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 in motd
|
||||
function text.padRight(value, length)
|
||||
@ -34,21 +25,6 @@ function text.padRight(value, length)
|
||||
end
|
||||
end
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
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
|
||||
|
||||
function text.wrap(value, width, maxWidth)
|
||||
checkArg(1, value, "string")
|
||||
checkArg(2, width, "number")
|
||||
@ -78,29 +54,7 @@ function text.wrappedLines(value, width, maxWidth)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- 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
|
||||
|
||||
-- used by lib/sh
|
||||
function text.escapeMagic(txt)
|
||||
return txt:gsub('[%(%)%.%%%+%-%*%?%[%^%$]', '%%%1')
|
||||
end
|
||||
@ -109,50 +63,6 @@ function text.removeEscapes(txt)
|
||||
return txt:gsub("%%([%(%)%.%%%+%-%*%?%[%^%$])","%1")
|
||||
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 --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
function text.internal.tokenize(value, options)
|
||||
checkArg(1, value, "string")
|
||||
checkArg(2, options, "table", "nil")
|
||||
@ -237,160 +147,16 @@ function text.internal.words(input, options)
|
||||
return tokens
|
||||
end
|
||||
|
||||
-- splits each word into words at delimiters
|
||||
-- delimiters are kept as their own words
|
||||
-- quoted word parts are not split
|
||||
function --[[@delayloaded-start@]] 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, spi)
|
||||
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
|
||||
setmetatable(text,
|
||||
{
|
||||
__index = function(tbl, key)
|
||||
setmetatable(text.internal, nil)
|
||||
setmetatable(text, nil)
|
||||
dofile("/opt/core/full_text.lua")
|
||||
return rawget(tbl, key)
|
||||
end
|
||||
})
|
||||
|
||||
return split_words
|
||||
end --[[@delayloaded-end@]]
|
||||
setmetatable(text.internal, getmetatable(text))
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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:match("b"))})
|
||||
|
||||
return require("buffer").new("r", reader)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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 i,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:match("b"))})
|
||||
|
||||
return require("buffer").new("w", writer)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
return text, local_env
|
||||
return text
|
||||
|
@ -1,33 +0,0 @@
|
||||
local data,tbl,key = ...
|
||||
local z = data[tbl]
|
||||
|
||||
if key then -- index
|
||||
local method = z.methods[key]
|
||||
local cache = z.cache[key]
|
||||
if method and not cache then
|
||||
local file = io.open(z.path,"r")
|
||||
if file then
|
||||
file:seek("set", method[1])
|
||||
local loaded = load("return function"..file:read(method[2]), "=delayed-"..key,"t",z.env)
|
||||
file:close()
|
||||
assert(loaded,"failed to load "..key)
|
||||
cache = loaded()
|
||||
--lazy_protect(key, cache)
|
||||
z.cache[key] = cache
|
||||
end
|
||||
end
|
||||
return cache
|
||||
else -- pairs
|
||||
local set,k,v = {}
|
||||
while true do
|
||||
k,v = next(tbl,k)
|
||||
if not k then break end
|
||||
set[k] = v
|
||||
end
|
||||
for k in pairs(z.methods) do
|
||||
if not set[k] then
|
||||
set[k] = function(...)return tbl[k](...)end
|
||||
end
|
||||
end
|
||||
return pairs(set)
|
||||
end
|
@ -1,67 +0,0 @@
|
||||
local filepath,delay_data = ...
|
||||
local file, reason = io.open(filepath, "r")
|
||||
if not file then
|
||||
return reason
|
||||
end
|
||||
|
||||
local methods = {}
|
||||
local delay_start_pattern = "^%s*function%s*%-%-%[%[@delayloaded%-start@%]%]%s*(.*)$"
|
||||
local delay_end_pattern = "^%s*end%s*%-%-%[%[@delayloaded%-end@%]%]%s*$"
|
||||
local n,buffer,lib_name,current_method,open = 0,{}
|
||||
|
||||
while true do
|
||||
local line = file:readLine(false)
|
||||
if current_method then
|
||||
local closed = not line or line:match(delay_end_pattern)
|
||||
if closed then
|
||||
local path,method_name,args = open:match("^(.-)([^%.]+)(%(.*)$")
|
||||
current_method = current_method-#args
|
||||
methods[path] = methods[path] or {}
|
||||
methods[path][method_name] = {current_method,n+#line-current_method}
|
||||
current_method=nil
|
||||
end
|
||||
elseif line then
|
||||
open = line:match(delay_start_pattern)
|
||||
if open then
|
||||
lib_name,open = open:match("^([^%.]+)%.(.*)$")
|
||||
current_method = n+#line
|
||||
else
|
||||
buffer[#buffer+1] = line
|
||||
end
|
||||
else
|
||||
file:close()
|
||||
break
|
||||
end
|
||||
n = n + #line
|
||||
end
|
||||
|
||||
if not next(methods) or current_method or not lib_name then
|
||||
return "no methods found or unclosed marker for delayed load"
|
||||
end
|
||||
|
||||
buffer = table.concat(buffer)
|
||||
local loader, reason = load(buffer, "="..filepath, "t", _G)
|
||||
local library, local_env = loader()
|
||||
if library then
|
||||
local_env = local_env or {}
|
||||
local_env[lib_name] = library
|
||||
|
||||
local env = setmetatable(local_env, {__index=_G})
|
||||
|
||||
for path,pack in pairs(methods) do
|
||||
local target = library
|
||||
for name in path:gmatch("[^%.]+") do target = target[name] end
|
||||
delay_data[target] =
|
||||
{
|
||||
methods = pack,
|
||||
cache = {},
|
||||
env = env,
|
||||
path = filepath
|
||||
}
|
||||
setmetatable(target, delay_data)
|
||||
end
|
||||
|
||||
return function()return library end, filepath
|
||||
end
|
||||
|
||||
return reason
|
@ -54,109 +54,6 @@ function lib.begins(tbl,v,f,l)
|
||||
return true
|
||||
end
|
||||
|
||||
-- works like string.sub but on elements of an indexed table
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
-- if value was made by lib.sub then find can find from whence
|
||||
function --[[@delayloaded-start@]] lib.find(tbl, sub, first, last)
|
||||
checkArg(1, tbl, 'table')
|
||||
checkArg(2, sub, 'table')
|
||||
local sub_len = #sub
|
||||
return lib.first(tbl, function(element, index, projected_table)
|
||||
for n=0,sub_len-1 do
|
||||
if projected_table[n + index] ~= sub[n + 1] then return nil end
|
||||
end
|
||||
return 1, sub_len
|
||||
end, first, last)
|
||||
end --[[@delayloaded-end@]]
|
||||
|
||||
-- Returns a list of subsets of tbl where partitioner acts as a delimiter.
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
-- calls callback(e,i,tbl) for each ith element e in table tbl from first
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
function --[[@delayloaded-start@]] 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 --[[@delayloaded-end@]]
|
||||
|
||||
function lib.concat(...)
|
||||
local r,rn,k={},0
|
||||
for _,tbl in ipairs({...})do
|
||||
@ -173,19 +70,13 @@ function lib.concat(...)
|
||||
return r
|
||||
end
|
||||
|
||||
-- works with pairs on tables
|
||||
-- returns the kv pair, or nil and the number of pairs iterated
|
||||
function --[[@delayloaded-start@]] 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
|
||||
setmetatable(lib,
|
||||
{
|
||||
__index = function(tbl, key)
|
||||
setmetatable(tbl, nil)
|
||||
dofile("/opt/core/full_transforms.lua")
|
||||
return rawget(tbl, key)
|
||||
end
|
||||
return nil, current_index - 1 -- went one too far
|
||||
end --[[@delayloaded-end@]]
|
||||
})
|
||||
|
||||
return lib,{adjust=adjust,view=view}
|
||||
return lib
|
||||
|
549
src/main/resources/assets/opencomputers/loot/openos/lib/tty.lua
Normal file
549
src/main/resources/assets/opencomputers/loot/openos/lib/tty.lua
Normal file
@ -0,0 +1,549 @@
|
||||
local unicode = require("unicode")
|
||||
local event = require("event")
|
||||
local process = require("process")
|
||||
local kb = require("keyboard")
|
||||
local component = require("component")
|
||||
local computer = require("computer")
|
||||
local keys = kb.keys
|
||||
|
||||
local tty = {}
|
||||
tty.window =
|
||||
{
|
||||
fullscreen = true,
|
||||
blink = true,
|
||||
dx = 0,
|
||||
dy = 0,
|
||||
x = 1,
|
||||
y = 1,
|
||||
}
|
||||
|
||||
tty.internal = {}
|
||||
|
||||
local function ctrl_movement(cursor, dir)
|
||||
local index, data = cursor.index, cursor.data
|
||||
|
||||
local last=dir<0 and 0 or unicode.len(data)
|
||||
local start=index+dir+1
|
||||
for i=start,last,dir do
|
||||
local a,b = unicode.sub(data, i-1, i-1), unicode.sub(data, i, i)
|
||||
a = a == "" or not not a:find("%s")
|
||||
b = b == "" or not not b:find("%s")
|
||||
if a and not b then return i - (index + 1) end
|
||||
end
|
||||
return last - index
|
||||
end
|
||||
|
||||
local function read_history(handler, cursor, change)
|
||||
local ni = handler.index + change
|
||||
if ni >= 0 and ni <= #handler then
|
||||
handler[handler.index] = cursor.data
|
||||
handler.index = ni
|
||||
cursor:clear()
|
||||
cursor:update(handler[ni])
|
||||
end
|
||||
end
|
||||
|
||||
local function tab_handler(handler, cursor)
|
||||
local hints = handler.hint
|
||||
if not hints then return end
|
||||
local main_kb = tty.keyboard()
|
||||
-- tty may not have a keyboard
|
||||
-- in which case, we shouldn't be handling tab events
|
||||
if not main_kb then
|
||||
return
|
||||
end
|
||||
if not handler.cache then
|
||||
handler.cache = type(hints) == "table" and hints or hints(cursor.data, cursor.index + 1) or {}
|
||||
handler.cache.i = -1
|
||||
end
|
||||
|
||||
local cache = handler.cache
|
||||
local cache_size = #cache
|
||||
|
||||
if cache_size == 1 and cache.i == 0 then
|
||||
-- there was only one solution, and the user is asking for the next
|
||||
handler.cache = hints(cache[1], cursor.index + 1)
|
||||
handler.cache.i = -1
|
||||
cache = handler.cache
|
||||
cache_size = #cache
|
||||
end
|
||||
|
||||
local change = kb.isShiftDown(main_kb) 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:clear()
|
||||
cursor:update(next)
|
||||
cursor:move(-tail)
|
||||
end
|
||||
end
|
||||
|
||||
local function key_down_handler(handler, cursor, char, code)
|
||||
local c = false
|
||||
local backup_cache = handler.cache
|
||||
handler.cache = nil
|
||||
local ctrl = kb.isControlDown(tty.keyboard())
|
||||
if ctrl and code == keys.d then
|
||||
return --close
|
||||
elseif code == keys.tab then
|
||||
handler.cache = backup_cache
|
||||
tab_handler(handler, cursor)
|
||||
elseif code == keys.enter or code == keys.numpadenter then
|
||||
cursor:move(math.huge)
|
||||
cursor:draw("\n")
|
||||
if #cursor.data > 0 then
|
||||
table.insert(handler, 1, cursor.data)
|
||||
handler[(tonumber(os.getenv("HISTSIZE")) or 10)+1]=nil
|
||||
handler[0]=nil
|
||||
end
|
||||
return nil, cursor.data .. "\n"
|
||||
elseif code == keys.up then read_history(handler, cursor, 1)
|
||||
elseif code == keys.down then read_history(handler, cursor, -1)
|
||||
elseif code == keys.left then cursor:move(ctrl and ctrl_movement(cursor, -1) or -1)
|
||||
elseif code == keys.right then cursor:move(ctrl and ctrl_movement(cursor, 1) or 1)
|
||||
elseif code == keys.home then cursor:move(-math.huge)
|
||||
elseif code == keys["end"] then cursor:move( math.huge)
|
||||
elseif code == keys.back then c = -1
|
||||
elseif code == keys.delete then c = 1
|
||||
elseif ctrl and char == "w"then -- TODO: cut word
|
||||
elseif char >= 32 then c = unicode.char(char)
|
||||
else handler.cache = backup_cache -- ignored chars shouldn't clear hint cache
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
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)
|
||||
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.scroll(math.huge)
|
||||
tty.setCursor(1, 1)
|
||||
end
|
||||
|
||||
function tty.isAvailable()
|
||||
local gpu = tty.gpu()
|
||||
return not not (gpu and gpu.getScreen())
|
||||
end
|
||||
|
||||
function tty.pull(cursor, timeout, ...)
|
||||
timeout = timeout or math.huge
|
||||
|
||||
local width, height, dx, dy, x, y = tty.getViewport()
|
||||
local out = (x<1 or x>width or y<1 or y>height)
|
||||
local blink = tty.getCursorBlink()
|
||||
|
||||
if cursor and out then
|
||||
cursor:move(0)
|
||||
cursor:scroll()
|
||||
out = false
|
||||
end
|
||||
|
||||
x, y = tty.getCursor()
|
||||
x, y = x + dx, y + dy
|
||||
local gpu = not out and tty.gpu()
|
||||
|
||||
local bgColor, bgIsPalette
|
||||
local fgColor, fgIsPalette
|
||||
local char_at_cursor
|
||||
if gpu then
|
||||
bgColor, bgIsPalette = gpu.getBackground()
|
||||
-- it can happen during a type of race condition when a screen is removed
|
||||
if not bgColor then
|
||||
return nil, "interrupted"
|
||||
end
|
||||
|
||||
fgColor, fgIsPalette = gpu.getForeground()
|
||||
char_at_cursor = gpu.get(x, y)
|
||||
end
|
||||
|
||||
-- get the next event
|
||||
local blinked = false
|
||||
local done = false
|
||||
local signal
|
||||
while true do
|
||||
if gpu then
|
||||
if not blinked and not done then
|
||||
gpu.setForeground(bgColor, bgIsPalette)
|
||||
gpu.setBackground(fgColor, fgIsPalette)
|
||||
gpu.set(x, y, char_at_cursor)
|
||||
gpu.setForeground(fgColor, fgIsPalette)
|
||||
gpu.setBackground(bgColor, bgIsPalette)
|
||||
blinked = true
|
||||
elseif blinked and (done or blink) then
|
||||
gpu.set(x, y, char_at_cursor)
|
||||
blinked = false
|
||||
end
|
||||
end
|
||||
|
||||
if done then
|
||||
return table.unpack(signal, 1, signal.n)
|
||||
end
|
||||
|
||||
signal = table.pack(event.pull(math.min(.5, timeout), ...))
|
||||
timeout = timeout - .5
|
||||
done = signal.n > 1 or timeout < .5
|
||||
end
|
||||
end
|
||||
|
||||
function tty.internal.split(cursor)
|
||||
local data, index = cursor.data, cursor.index
|
||||
local dlen = unicode.len(data)
|
||||
index = math.max(0, math.min(index, dlen))
|
||||
local tail = dlen - index
|
||||
return unicode.sub(data, 1, index), tail == 0 and "" or unicode.sub(data, -tail)
|
||||
end
|
||||
|
||||
function tty.internal.build_vertical_reader()
|
||||
local x, y = tty.getCursor()
|
||||
return
|
||||
{
|
||||
promptx = x,
|
||||
prompty = y,
|
||||
index = 0,
|
||||
data = "",
|
||||
sy = 0,
|
||||
scroll = function(_)
|
||||
_.sy = _.sy + tty.scroll()
|
||||
end,
|
||||
move = function(_,n)
|
||||
local win = tty.window
|
||||
_.index = math.min(math.max(0,_.index + n), unicode.len(_.data))
|
||||
local s1, s2 = tty.internal.split(_)
|
||||
s2 = unicode.sub(s2.." ", 1, 1)
|
||||
local data_remaining = ("_"):rep(_.promptx - 1)..s1..s2
|
||||
win.y = _.prompty - _.sy
|
||||
while true do
|
||||
local wlen_remaining = unicode.wlen(data_remaining)
|
||||
if wlen_remaining > win.width then
|
||||
local line_cut = unicode.wtrunc(data_remaining, win.width + 1)
|
||||
data_remaining = unicode.sub(data_remaining, unicode.len(line_cut) + 1)
|
||||
win.y = win.y + 1
|
||||
else
|
||||
win.x = wlen_remaining - unicode.wlen(s2) + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
end,
|
||||
clear_tail = function(_)
|
||||
local oi, width, height, dx, dy, ox, oy = _.index, tty.getViewport()
|
||||
_:move(math.huge)
|
||||
_:move(-1)
|
||||
local ex, ey = tty.getCursor()
|
||||
tty.setCursor(ox, oy)
|
||||
_.index = oi
|
||||
local x = oy == ey and ox or 1
|
||||
tty.gpu().fill(x + dx, ey + dy, width - x + 1, 1, " ")
|
||||
end,
|
||||
update = function(_, arg)
|
||||
local s1, s2 = tty.internal.split(_)
|
||||
if type(arg) == "string" then
|
||||
_.data = s1 .. arg .. s2
|
||||
_.index = _.index + unicode.len(arg)
|
||||
_:draw(arg)
|
||||
else -- number
|
||||
if arg < 0 then
|
||||
-- backspace? ignore if at start
|
||||
if _.index <= 0 then return end
|
||||
_:move(arg)
|
||||
s1 = unicode.sub(s1, 1, -1 + arg)
|
||||
else
|
||||
-- forward? ignore if at end
|
||||
if _.index >= unicode.len(_.data) then return end
|
||||
s2 = unicode.sub(s2, 1 + arg)
|
||||
end
|
||||
_:clear_tail()
|
||||
_.data = s1 .. s2
|
||||
end
|
||||
|
||||
-- redraw suffix
|
||||
if s2 ~= "" then
|
||||
local ps, px, py = _.sy, tty.getCursor()
|
||||
_:draw(s2)
|
||||
tty.setCursor(px, py - (_.sy - ps))
|
||||
end
|
||||
end,
|
||||
clear = function(_)
|
||||
_:move(-math.huge)
|
||||
_:draw((" "):rep(unicode.wlen(_.data)))
|
||||
_:move(-math.huge)
|
||||
_.index = 0
|
||||
_.data = ""
|
||||
end,
|
||||
draw = function(_, text)
|
||||
_.sy = _.sy + tty.drawText(text)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
function tty.read(handler, cursor)
|
||||
if not io.stdin.tty then return io.read() end
|
||||
|
||||
checkArg(1, handler, "table")
|
||||
checkArg(2, cursor, "table", "nil")
|
||||
|
||||
handler.index = 0
|
||||
handler.touch = handler.touch or "touch_handler"
|
||||
handler.drag = handler.drag or "touch_handler"
|
||||
handler.clipboard = handler.clipboard or "clipboard_handler"
|
||||
handler.key_down = handler.key_down or key_down_handler
|
||||
|
||||
cursor = cursor or tty.internal.build_vertical_reader()
|
||||
|
||||
while true do
|
||||
local name, address, char, code = tty.pull(cursor)
|
||||
-- we may have lost tty during the pull
|
||||
if not tty.isAvailable() then
|
||||
return
|
||||
end
|
||||
|
||||
-- we have to keep checking what kb is active in case it is switching during use
|
||||
-- we could have multiple screens, each with keyboards active
|
||||
local main_kb = tty.keyboard()
|
||||
local main_sc = tty.screen()
|
||||
if name == "interrupted" then
|
||||
tty.drawText("^C\n")
|
||||
return false
|
||||
elseif address == main_kb or address == main_sc then
|
||||
local handler_method = handler[name]
|
||||
if handler_method then
|
||||
if type(handler_method) == "string" then -- special hack to delay loading tty stuff
|
||||
handler_method = tty[handler_method]
|
||||
end
|
||||
-- nil to end (close)
|
||||
-- false to ignore
|
||||
-- true-thy updates cursor
|
||||
local c, ret = handler_method(handler, cursor, char, code)
|
||||
if c == nil then
|
||||
return ret
|
||||
elseif c then
|
||||
-- if we obtained something (c) to handle
|
||||
cursor:update(c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- cannot use tty.write = io.write because io.write invokes metatable
|
||||
function tty.write(value, wrap)
|
||||
local stdout = io.output()
|
||||
local stream = stdout and stdout.stream
|
||||
local previous_nowrap = stream.nowrap
|
||||
stream.nowrap = wrap == false
|
||||
stdout:write(value)
|
||||
stdout:flush()
|
||||
stream.nowrap = previous_wrap
|
||||
end
|
||||
|
||||
function tty.getCursor()
|
||||
local window = tty.window
|
||||
return window.x, window.y
|
||||
end
|
||||
|
||||
function tty.setCursor(x, y)
|
||||
local window = tty.window
|
||||
window.x, window.y = x, y
|
||||
end
|
||||
|
||||
function tty.drawText(value, nowrap)
|
||||
local gpu = tty.gpu()
|
||||
if not gpu then
|
||||
return
|
||||
end
|
||||
local sy = 0
|
||||
local cr_last, beeped
|
||||
local uptime = computer.uptime
|
||||
local last_sleep = uptime()
|
||||
local last_index = 1
|
||||
local width, height, dx, dy = tty.getViewport()
|
||||
while true do
|
||||
if uptime() - last_sleep > 1 then
|
||||
os.sleep(0)
|
||||
last_sleep = uptime()
|
||||
end
|
||||
|
||||
-- scroll before parsing next line
|
||||
-- the value may only have been a newline
|
||||
sy = sy + tty.scroll()
|
||||
local x, y = tty.getCursor()
|
||||
|
||||
local si, ei, segment, delim = value:find("([^\t\r\n\a]*)([\t\r\n\a]?)", last_index)
|
||||
if si > ei then
|
||||
break
|
||||
end
|
||||
last_index = ei + 1
|
||||
|
||||
if segment ~= "" then
|
||||
local gpu_x, gpu_y = x + dx, y + dy
|
||||
local tail = ""
|
||||
local wlen_needed = unicode.wlen(segment)
|
||||
local wlen_remaining = width - x + 1
|
||||
if wlen_remaining < wlen_needed then
|
||||
segment = unicode.wtrunc(segment, wlen_remaining + 1)
|
||||
wlen_needed = unicode.wlen(segment)
|
||||
-- we can clear the line because we already know remaining < needed
|
||||
tail = (" "):rep(wlen_remaining - wlen_needed)
|
||||
-- we have to reparse the delimeter
|
||||
last_index = si + #segment
|
||||
-- fake a newline
|
||||
if not nowrap then
|
||||
delim = "\n"
|
||||
end
|
||||
end
|
||||
gpu.set(gpu_x, gpu_y, segment..tail)
|
||||
x = x + wlen_needed
|
||||
end
|
||||
|
||||
if delim == "\t" then
|
||||
x = ((x-1) - ((x-1) % 8)) + 9
|
||||
elseif delim == "\r" or (delim == "\n" and not cr_last) then
|
||||
x = 1
|
||||
y = y + 1
|
||||
elseif delim == "\a" and not beeped then
|
||||
computer.beep()
|
||||
beeped = true
|
||||
end
|
||||
|
||||
tty.setCursor(x, y)
|
||||
cr_last = delim == "\r"
|
||||
end
|
||||
return sy
|
||||
end
|
||||
|
||||
function tty.setCursorBlink(enabled)
|
||||
tty.window.blink = enabled
|
||||
end
|
||||
|
||||
function tty.getCursorBlink()
|
||||
return tty.window.blink
|
||||
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
|
||||
window.gpu = gpu
|
||||
window.keyboard = nil -- without a keyboard bound, always use the screen's main keyboard (1st)
|
||||
screen_reset(gpu)
|
||||
tty.getViewport()
|
||||
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 scren, 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.scroll(number)
|
||||
local gpu = tty.gpu()
|
||||
if not gpu then
|
||||
return 0
|
||||
end
|
||||
local width, height, dx, dy, x, y = tty.getViewport()
|
||||
|
||||
local lines = number or (y - height)
|
||||
if lines == 0 -- if zero scroll length is requested, do nothing
|
||||
or not number and lines < 0 then -- do not auto scroll back up, only down
|
||||
return 0
|
||||
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.min(y, height))
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
setmetatable(tty,
|
||||
{
|
||||
__index = function(tbl, key)
|
||||
setmetatable(tbl, nil)
|
||||
dofile("/opt/core/full_tty.lua")
|
||||
return rawget(tbl, key)
|
||||
end
|
||||
})
|
||||
|
||||
return tty
|
@ -1,7 +1,7 @@
|
||||
-- called from /init.lua
|
||||
local raw_loadfile = ...
|
||||
|
||||
_G._OSVERSION = "OpenOS 1.6.1"
|
||||
_G._OSVERSION = "OpenOS 1.6.2"
|
||||
|
||||
local component = component
|
||||
local computer = computer
|
||||
@ -33,20 +33,21 @@ _G.boot_screen = screen
|
||||
local gpu = component.list("gpu", true)()
|
||||
local w, h
|
||||
if gpu and screen then
|
||||
component.invoke(gpu, "bind", screen)
|
||||
w, h = component.invoke(gpu, "maxResolution")
|
||||
component.invoke(gpu, "setResolution", w, h)
|
||||
component.invoke(gpu, "setBackground", 0x000000)
|
||||
component.invoke(gpu, "setForeground", 0xFFFFFF)
|
||||
component.invoke(gpu, "fill", 1, 1, w, h, " ")
|
||||
gpu = component.proxy(gpu)
|
||||
gpu.bind(screen)
|
||||
w, h = gpu.maxResolution()
|
||||
gpu.setResolution(w, h)
|
||||
gpu.setBackground(0x000000)
|
||||
gpu.setForeground(0xFFFFFF)
|
||||
gpu.fill(1, 1, w, h, " ")
|
||||
end
|
||||
local y = 1
|
||||
local function status(msg)
|
||||
if gpu and screen then
|
||||
component.invoke(gpu, "set", 1, y, msg)
|
||||
gpu.set(1, y, msg)
|
||||
if y == h then
|
||||
component.invoke(gpu, "copy", 1, 2, w, h - 1, 0, -1)
|
||||
component.invoke(gpu, "fill", 1, h, w, 1, " ")
|
||||
gpu.copy(1, 2, w, h - 1, 0, -1)
|
||||
gpu.fill(1, h, w, 1, " ")
|
||||
else
|
||||
y = y + 1
|
||||
end
|
||||
@ -100,12 +101,6 @@ do
|
||||
|
||||
-- Inject the io modules
|
||||
_G.io = loadfile("/lib/io.lua")()
|
||||
|
||||
--mark modules for delay loaded api
|
||||
package.delayed["text"] = true
|
||||
package.delayed["sh"] = true
|
||||
package.delayed["transforms"] = true
|
||||
package.delayed["term"] = true
|
||||
end
|
||||
|
||||
status("Initializing file system...")
|
@ -4,7 +4,7 @@ local text = require("text")
|
||||
|
||||
local dcache = {}
|
||||
local pcache = {}
|
||||
local adapter_pwd = "/lib/tools/devfs/adapters/"
|
||||
local adapter_pwd = "/opt/core/devfs/adapters/"
|
||||
|
||||
local adapter_api = {}
|
||||
|
||||
@ -87,25 +87,12 @@ return
|
||||
-- first sort the addr, primaries first, then sorted by address lexigraphically
|
||||
local hw_addresses = {}
|
||||
for addr,type in comp.list() do
|
||||
table.insert(hw_addresses, {addr,type})
|
||||
local isPrim = comp.isPrimary(addr)
|
||||
table.insert(hw_addresses, select(isPrim and 1 or 2, 1, {type,addr}))
|
||||
end
|
||||
|
||||
table.sort(hw_addresses, function(a, b)
|
||||
local aaddr, atype = table.unpack(a)
|
||||
local baddr, btype = table.unpack(b)
|
||||
|
||||
if atype == btype then
|
||||
local aprim = comp.isPrimary(aaddr)
|
||||
local bprim = comp.isPrimary(baddr)
|
||||
if aprim then return true end
|
||||
if bprim then return false end
|
||||
end
|
||||
|
||||
return aaddr < baddr
|
||||
end)
|
||||
|
||||
for _,pair in ipairs(hw_addresses) do
|
||||
local addr, type = table.unpack(pair)
|
||||
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)
|
@ -1,6 +1,6 @@
|
||||
local fs = require("filesystem")
|
||||
local shell = require("shell")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
local unicode = require("unicode")
|
||||
|
||||
local dirsArg, ops = shell.parse(...)
|
||||
@ -32,8 +32,8 @@ if #dirsArg == 0 then
|
||||
end
|
||||
|
||||
local ec = 0
|
||||
local gpu = term.gpu()
|
||||
local fOut = term.isAvailable() and io.output().tty
|
||||
local gpu = tty.gpu()
|
||||
local fOut = tty.isAvailable() and io.output().tty
|
||||
local function perr(msg) io.stderr:write(msg,"\n") ec = 2 end
|
||||
local function stat(names, index)
|
||||
local name = names[index]
|
||||
@ -272,7 +272,7 @@ local function display(names)
|
||||
return {{color = colorize(info), name = info.name}}
|
||||
end
|
||||
else -- columns
|
||||
local num_columns, items_per_column, width = 0, 0, term.getViewport() - 1
|
||||
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
|
@ -0,0 +1,549 @@
|
||||
local event = require("event")
|
||||
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.handleThreadYield(result)
|
||||
local action = result[2]
|
||||
if action == nil or type(action) == "number" then
|
||||
return table.pack(pcall(event.pull, table.unpack(result, 2, result.n)))
|
||||
else
|
||||
return table.pack(coroutine.yield(table.unpack(result, 2, result.n)))
|
||||
end
|
||||
end
|
||||
|
||||
function sh.internal.buildCommandRedirects(args, thread)
|
||||
local data = process.info(thread).data
|
||||
local tokens, ios, handles = args, data.io, data.handles
|
||||
args = {}
|
||||
local from_io, to_io, mode
|
||||
for i = 1, #tokens do
|
||||
local token = tokens[i]
|
||||
if token == "<" then
|
||||
from_io = 0
|
||||
mode = "r"
|
||||
else
|
||||
local first_index, last_index, from_io_txt, mode_txt, to_io_txt = token:find("(%d*)(>>?)(.*)")
|
||||
if mode_txt then
|
||||
mode = mode_txt == ">>" and "a" or "w"
|
||||
from_io = from_io_txt and tonumber(from_io_txt) or 1
|
||||
if to_io_txt ~= "" then
|
||||
to_io = tonumber(to_io_txt:sub(2))
|
||||
ios[from_io] = ios[to_io]
|
||||
mode = nil
|
||||
end
|
||||
else -- just an arg
|
||||
if not mode then
|
||||
table.insert(args, token)
|
||||
else
|
||||
local file, reason = io.open(shell.resolve(token), mode)
|
||||
if not file then
|
||||
io.stderr:write("could not open '" .. token .. "': " .. reason .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
table.insert(handles, file)
|
||||
ios[from_io] = file
|
||||
end
|
||||
mode = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return args
|
||||
end
|
||||
|
||||
function sh.internal.buildPipeChain(threads)
|
||||
local prev_pipe
|
||||
for i=1,#threads do
|
||||
local thread = threads[i]
|
||||
local data = process.info(thread).data
|
||||
local pio = data.io
|
||||
|
||||
local pipe
|
||||
if i < #threads then
|
||||
pipe = require("buffer").new("rw", sh.internal.newMemoryStream())
|
||||
pipe:setvbuf("no", 0)
|
||||
-- buffer close flushes the buffer, but we have no buffer
|
||||
-- also, when the buffer is closed, read and writes don't pass through
|
||||
-- simply put, we don't want buffer:close
|
||||
pipe.close = function(self) self.stream:close() end
|
||||
pipe.stream.redirect[1] = rawget(pio, 1)
|
||||
pio[1] = pipe
|
||||
table.insert(data.handles, pipe)
|
||||
end
|
||||
|
||||
if prev_pipe then
|
||||
prev_pipe.stream.redirect[0] = rawget(pio, 0)
|
||||
prev_pipe.stream.next = thread
|
||||
pio[0] = prev_pipe
|
||||
end
|
||||
|
||||
prev_pipe = pipe
|
||||
end
|
||||
end
|
||||
|
||||
function sh.internal.glob(glob_pattern)
|
||||
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(sh.internal.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 break end
|
||||
relative_separator = "/"
|
||||
end
|
||||
-- if no next_paths were hit here, the ENTIRE glob value is not a path
|
||||
return paths
|
||||
end
|
||||
|
||||
function sh.getMatchingPrograms(baseName)
|
||||
local result = {}
|
||||
local result_keys = {} -- cache for fast value lookup
|
||||
-- TODO only matching files with .lua extension for now, might want to
|
||||
-- extend this to other extensions at some point? env var? file attrs?
|
||||
if not baseName or #baseName == 0 then
|
||||
baseName = "^(.*)%.lua$"
|
||||
else
|
||||
baseName = "^(" .. text.escapeMagic(baseName) .. ".*)%.lua$"
|
||||
end
|
||||
for basePath in string.gmatch(os.getenv("PATH"), "[^:]+") do
|
||||
for file in fs.list(shell.resolve(basePath)) do
|
||||
local match = file:match(baseName)
|
||||
if match and not result_keys[match] then
|
||||
table.insert(result, match)
|
||||
result_keys[match] = true
|
||||
end
|
||||
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, simple = 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.find(text.syntax, {";"}) -- all symbols before ; in syntax CAN be repeated
|
||||
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, "parse error near " .. state
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function sh.internal.boolean_executor(chains, predicator)
|
||||
local function not_gate(result)
|
||||
return sh.internal.command_passed(result) and 1 or 0
|
||||
end
|
||||
|
||||
local last = true
|
||||
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 = not_gate(prev(n,i))
|
||||
predicator = prev
|
||||
return result
|
||||
end
|
||||
end
|
||||
if chomped then
|
||||
stage = negation_stage
|
||||
end
|
||||
if #next > 0 then
|
||||
last = 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
|
||||
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, t)
|
||||
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 true and not sh.internal.remove_negation(chain)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function sh.internal.newMemoryStream()
|
||||
local memoryStream = {}
|
||||
|
||||
function memoryStream:close()
|
||||
self.closed = true
|
||||
self.redirect = {}
|
||||
end
|
||||
|
||||
function memoryStream:seek()
|
||||
return nil, "bad file descriptor"
|
||||
end
|
||||
|
||||
function memoryStream:read(n)
|
||||
if self.closed then
|
||||
return nil -- eof
|
||||
end
|
||||
if self.redirect[0] then
|
||||
-- popen could be using this code path
|
||||
-- if that is the case, it is important to leave stream.buffer alone
|
||||
return self.redirect[0]:read(n)
|
||||
elseif self.buffer == "" then
|
||||
coroutine.yield()
|
||||
end
|
||||
local result = string.sub(self.buffer, 1, n)
|
||||
self.buffer = string.sub(self.buffer, n + 1)
|
||||
return result
|
||||
end
|
||||
|
||||
function memoryStream:write(value)
|
||||
if not self.redirect[1] and 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
|
||||
elseif self.redirect[1] then
|
||||
return self.redirect[1]:write(value)
|
||||
elseif not self.closed then
|
||||
self.buffer = self.buffer .. value
|
||||
local result = table.pack(coroutine.resume(self.next))
|
||||
if coroutine.status(self.next) == "dead" then
|
||||
self:close()
|
||||
end
|
||||
if not result[1] then
|
||||
io.stderr:write(tostring(result[2]) .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
return self
|
||||
end
|
||||
os.exit(0) -- abort the current process: SIGPIPE
|
||||
end
|
||||
|
||||
local stream = {closed = false, buffer = "",
|
||||
redirect = {}, result = {}}
|
||||
local metatable = {__index = memoryStream,
|
||||
__metatable = "memorystream"}
|
||||
return setmetatable(stream, metatable)
|
||||
end
|
||||
|
||||
function sh.internal.execute_complex(statements, eargs, env)
|
||||
for si=1,#statements do local s = statements[si]
|
||||
local chains = sh.internal.groupChains(s)
|
||||
local last_code = 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)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function sh.internal.parse_sub(input)
|
||||
-- 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
|
||||
|
||||
local sub = io.popen(capture)
|
||||
local result = input:sub(i, fi - 1) .. sub:read("*a")
|
||||
sub:close()
|
||||
-- all whitespace is replaced by single spaces
|
||||
-- we requote the result because tokenize will respect this as text
|
||||
table.insert(packed, (text.trim(result):gsub("%s+"," ")))
|
||||
|
||||
i = si+1
|
||||
end
|
||||
|
||||
return table.concat(packed)
|
||||
end
|
||||
|
||||
|
@ -0,0 +1,246 @@
|
||||
local text = require("text")
|
||||
local tx = require("transforms")
|
||||
local unicode = require("unicode")
|
||||
|
||||
-- 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, spi)
|
||||
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:match("b"))})
|
||||
|
||||
return require("buffer").new("r", 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 i,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:match("b"))})
|
||||
|
||||
return require("buffer").new("w", 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
|
@ -0,0 +1,122 @@
|
||||
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
|
||||
|
||||
-- if value was made by lib.sub then find can find from whence
|
||||
function lib.find(tbl, sub, first, last)
|
||||
checkArg(1, tbl, 'table')
|
||||
checkArg(2, sub, 'table')
|
||||
local sub_len = #sub
|
||||
return lib.first(tbl, function(element, index, projected_table)
|
||||
for n=0,sub_len-1 do
|
||||
if projected_table[n + index] ~= sub[n + 1] then return nil end
|
||||
end
|
||||
return 1, sub_len
|
||||
end, first, last)
|
||||
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
|
@ -0,0 +1,51 @@
|
||||
local tty = require("tty")
|
||||
local unicode = require("unicode")
|
||||
|
||||
function tty.touch_handler(handler, cursor, gx, gy)
|
||||
if cursor.data == "" then
|
||||
return false
|
||||
end
|
||||
cursor:move(-math.huge)
|
||||
local win = tty.window
|
||||
gx, gy = gx - win.dx, gy - win.dy
|
||||
local x2, y2, d = win.x, win.y, win.width
|
||||
local char_width_to_move = ((gy*d+gx)-(y2*d+x2))
|
||||
if char_width_to_move <= 0 then
|
||||
return false
|
||||
end
|
||||
local total_wlen = unicode.wlen(cursor.data)
|
||||
if char_width_to_move >= total_wlen then
|
||||
cursor:move(math.huge)
|
||||
else
|
||||
local chars_to_move = unicode.wtrunc(cursor.data, char_width_to_move + 1)
|
||||
cursor:move(unicode.len(chars_to_move))
|
||||
end
|
||||
-- fake white space can make the index off, redo adjustment for alignment
|
||||
x2, y2, d = win.x, win.y, win.width
|
||||
char_width_to_move = ((gy*d+gx)-(y2*d+x2))
|
||||
if (char_width_to_move < 0) then
|
||||
-- using char_width_to_move as a type of index is wrong, but large enough and helps to speed this up
|
||||
local up_to_cursor = unicode.sub(cursor.data, cursor.index+char_width_to_move, cursor.index)
|
||||
local full_wlen = unicode.wlen(up_to_cursor)
|
||||
local without_tail = unicode.wtrunc(up_to_cursor, full_wlen + char_width_to_move + 1)
|
||||
local chars_cut = unicode.len(up_to_cursor) - unicode.len(without_tail)
|
||||
cursor:move(-chars_cut)
|
||||
end
|
||||
return false -- no further cursor update
|
||||
end
|
||||
|
||||
function tty.clipboard_handler(handler, cursor, char, code)
|
||||
handler.cache = nil
|
||||
local first_line, end_index = char:find("\13?\10")
|
||||
if first_line then
|
||||
local after = char:sub(end_index + 1)
|
||||
if after ~= "" then
|
||||
-- todo look at postponing the text on cursor
|
||||
require("computer").pushSignal("key_down", tty.keyboard(), 13, 28)
|
||||
require("computer").pushSignal("clipboard", tty.keyboard(), after)
|
||||
end
|
||||
char = char:sub(1, first_line - 1)
|
||||
end
|
||||
return char
|
||||
end
|
||||
|
@ -156,7 +156,7 @@ end
|
||||
|
||||
local source = options.sources[1]
|
||||
local target = options.targets[1]
|
||||
local utils_path = package.searchpath("tools/install_utils", package.path)
|
||||
local utils_path = "/opt/core/install_utils.lua"
|
||||
|
||||
if #options.sources ~= 1 or #options.targets ~= 1 then
|
||||
source, target = loadfile(utils_path, "bt", _G)('select', options)
|
@ -31,6 +31,7 @@ local function select_prompt(devs, prompt)
|
||||
choice = devs[number]
|
||||
else
|
||||
io.write("Invalid input, please try again: ")
|
||||
os.sleep(0)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
local package = require("package")
|
||||
local term = require("term")
|
||||
local tty = require("tty")
|
||||
|
||||
local gpu = term.gpu()
|
||||
local gpu = tty.gpu()
|
||||
|
||||
local function optrequire(...)
|
||||
local success, module = pcall(require, ...)
|
||||
@ -34,8 +34,6 @@ env = setmetatable({}, {
|
||||
})
|
||||
env._PROMPT = tostring(env._PROMPT or "lua> ")
|
||||
|
||||
local history = {}
|
||||
|
||||
local function findTable(t, path)
|
||||
if type(t) ~= "table" then return nil end
|
||||
if not path or #path == 0 then return t end
|
||||
@ -72,7 +70,8 @@ local function findKeys(t, r, prefix, name)
|
||||
end
|
||||
end
|
||||
|
||||
local function hint(line, index)
|
||||
local read_handler = {}
|
||||
function read_handler.hint(line, index)
|
||||
line = (line or "")
|
||||
local tail = line:sub(index)
|
||||
line = line:sub(1, index - 1)
|
||||
@ -92,18 +91,18 @@ local function hint(line, index)
|
||||
end
|
||||
|
||||
gpu.setForeground(0xFFFFFF)
|
||||
term.write(_VERSION .. " Copyright (C) 1994-2015 Lua.org, PUC-Rio\n")
|
||||
tty.write(_VERSION .. " Copyright (C) 1994-2015 Lua.org, PUC-Rio\n")
|
||||
gpu.setForeground(0xFFFF00)
|
||||
term.write("Enter a statement and hit enter to evaluate it.\n")
|
||||
term.write("Prefix an expression with '=' to show its value.\n")
|
||||
term.write("Press Ctrl+D to exit the interpreter.\n")
|
||||
tty.write("Enter a statement and hit enter to evaluate it.\n")
|
||||
tty.write("Prefix an expression with '=' to show its value.\n")
|
||||
tty.write("Press Ctrl+D to exit the interpreter.\n")
|
||||
gpu.setForeground(0xFFFFFF)
|
||||
|
||||
while term.isAvailable() do
|
||||
while tty.isAvailable() do
|
||||
local foreground = gpu.setForeground(0x00FF00)
|
||||
term.write(env._PROMPT)
|
||||
tty.write(env._PROMPT)
|
||||
gpu.setForeground(foreground)
|
||||
local command = term.read(history, nil, hint)
|
||||
local command = tty.read(read_handler)
|
||||
if not command then -- eof
|
||||
return
|
||||
end
|
||||
@ -122,10 +121,10 @@ while term.isAvailable() do
|
||||
io.stderr:write(tostring(result[2]) .. "\n")
|
||||
else
|
||||
for i = 2, result.n do
|
||||
term.write(require("serialization").serialize(result[i], true) .. "\t", true)
|
||||
tty.write(require("serialization").serialize(result[i], true) .. "\t", true)
|
||||
end
|
||||
if term.getCursor() > 1 then
|
||||
term.write("\n")
|
||||
if tty.getCursor() > 1 then
|
||||
tty.write("\n")
|
||||
end
|
||||
end
|
||||
else
|
Loading…
x
Reference in New Issue
Block a user