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:
payonel 2017-05-13 23:44:09 -07:00
parent bcadcdb21e
commit 79e8f863ea
53 changed files with 1843 additions and 1890 deletions

View File

@ -1,3 +1,2 @@
local term = require("term")
term.clear()
local tty = require("tty")
tty.clear()

View File

@ -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

View File

@ -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")

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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}))

View File

@ -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")

View File

@ -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]))

View File

@ -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()

View File

@ -1,5 +1,3 @@
local text = require("text")
local args = {...}
if #args < 1 then

View File

@ -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

View File

@ -1,5 +1,5 @@
local computer = require("computer")
local term = require("term")
local tty = require("tty")
term.clear()
tty.clear()
computer.shutdown()

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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$","")

View File

@ -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)

View 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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 "/"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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...")

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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