ctrl+d for soft and ctrl+c for hard abort in term.read, resulting in 'nil' being read, signalling an eof and thus allowing the lua program to be quit that way; removed the exit() function from the lua function; added shell.parse for rudimentary argument parsing (anything starting with - is treated as an option or a list of option (each option name must be exactly one char); added key state tracking and keyboard library

This commit is contained in:
Florian Nücke 2013-10-23 17:10:56 +02:00
parent e9f2fac861
commit a8288b2623
21 changed files with 276 additions and 178 deletions

View File

@ -135,6 +135,6 @@ for k, v in pairs(driver.keyboard.keys) do
driver.keyboard.keys[v] = k
end
function driver.keyboard.keys.isControl(char)
function driver.keyboard.isControl(char)
return type(char) == "number" and (char < 0x20 or (char >= 0x7F and char <= 0x9F))
end

View File

@ -47,11 +47,8 @@ local sandbox = {
coroutine = {
create = coroutine.create,
resume = coroutine.resume,
running = coroutine.running,
status = coroutine.status,
wrap = coroutine.wrap,
yield = coroutine.yield
status = coroutine.status
},
math = {
@ -136,8 +133,9 @@ local sandbox = {
}
sandbox._G = sandbox
function sandbox.load(code, source, env)
return load(code, source, "t", env or sandbox)
function sandbox.load(ld, source, mode, env)
assert((mode or "t") == "t", "unsupported mode")
return load(ld, source, "t", env or sandbox)
end
function sandbox.checkArg(n, have, ...)
@ -151,7 +149,7 @@ function sandbox.checkArg(n, have, ...)
end
if not check(...) then
local msg = string.format("bad argument #%d (%s expected, got %s)", n, table.concat({...}, " or "), have)
--error(debug.traceback(msg, 2), 2)
--error(debug.traceback(msg, 2), 0)
error(msg, 2)
end
end
@ -170,6 +168,100 @@ local function checkDeadline()
end
end
function sandbox.coroutine.resume(co, ...)
local args = table.pack(...)
while true do
if not debug.gethook(co) then -- don't reset counter
debug.sethook(co, checkDeadline, "", 10000)
end
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
checkDeadline()
if result[1] then
local isSystemYield = coroutine.status(co) ~= "dead" and result[2] ~= nil
if isSystemYield then
args = table.pack(coroutine.yield(result[2]))
else
return true, table.unpack(result, 3, result.n)
end
else -- error: result = (bool, string)
return table.unpack(result, 1, result.n)
end
end
end
function sandbox.coroutine.yield(...)
return coroutine.yield(nil, ...)
end
function sandbox.coroutine.wrap(f) -- for sandbox's coroutine.resume
local co = sandbox.coroutine.create(f)
return function(...)
local result = table.pack(sandbox.coroutine.resume(co, ...))
if result[1] then
return table.unpack(result, 2, result.n)
else
error(result[2], 0)
end
end
end
function sandbox.pcall(...)
local result = table.pack(pcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
function sandbox.xpcall(...)
local result = table.pack(xpcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
-------------------------------------------------------------------------------
function sandbox.os.shutdown(reboot)
coroutine.yield(reboot ~= nil and reboot ~= false)
end
function sandbox.os.signal(name, timeout)
local waitUntil = os.uptime() + (type(timeout) == "number" and timeout or math.huge)
repeat
local signal = table.pack(coroutine.yield(waitUntil - os.uptime()))
if signal.n > 0 and (name == signal[1] or name == nil) then
return table.unpack(signal, 1, signal.n)
end
until os.uptime() >= waitUntil
end
-------------------------------------------------------------------------------
sandbox.driver = {}
function sandbox.driver.componentType(address)
return nodeName(address)
end
do
local env = setmetatable({ send = sendToAddress },
{ __index = sandbox, __newindex = sandbox })
for name, code in pairs(drivers()) do
local driver, reason = load(code, "=" .. name, "t", env)
if not driver then
print("Failed loading driver '" .. name .. "': " .. reason)
else
local result, reason = xpcall(driver, function(msg)
return debug.traceback(msg, 2)
end)
if not result then
print("Failed initializing driver '" .. name .. "': " ..
(reason or "unknown error"))
end
end
end
end
-------------------------------------------------------------------------------
local function main(args)
local function bootstrap()
local fs = sandbox.driver.filesystem
@ -235,88 +327,6 @@ local function main(args)
end
end
function sandbox.coroutine.resume(co, ...)
local args = table.pack(...)
while true do
if not debug.gethook(co) then -- don't reset counter
debug.sethook(co, checkDeadline, "", 10000)
end
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
checkDeadline()
if result[1] then
local isSystemYield = coroutine.status(co) ~= "dead" and result[2] ~= nil
if isSystemYield then
args = table.pack(coroutine.yield(result[2]))
else
return true, table.unpack(result, 3, result.n)
end
else -- error: result = (bool, string)
return table.unpack(result, 1, result.n)
end
end
end
function sandbox.coroutine.yield(...)
return coroutine.yield(nil, ...)
end
function sandbox.pcall(...)
local result = table.pack(pcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
function sandbox.xpcall(...)
local result = table.pack(xpcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
-------------------------------------------------------------------------------
function sandbox.os.shutdown(reboot)
coroutine.yield(reboot ~= nil and reboot ~= false)
end
function sandbox.os.signal(name, timeout)
local waitUntil = os.uptime() + (type(timeout) == "number" and timeout or math.huge)
repeat
local signal = table.pack(coroutine.yield(waitUntil - os.uptime()))
if signal.n > 0 and (name == signal[1] or name == nil) then
return table.unpack(signal, 1, signal.n)
end
until os.uptime() >= waitUntil
end
-------------------------------------------------------------------------------
sandbox.driver = {}
function sandbox.driver.componentType(address)
return nodeName(address)
end
do
local env = setmetatable({ send = sendToAddress },
{ __index = sandbox, __newindex = sandbox })
for name, code in pairs(drivers()) do
local driver, reason = load(code, "=" .. name, "t", env)
if not driver then
print("Failed loading driver '" .. name .. "': " .. reason)
else
local result, reason = xpcall(driver, function(msg)
return debug.traceback(msg, 2)
end)
if not result then
print("Failed initializing driver '" .. name .. "': " ..
(reason or "unknown error"))
end
end
end
end
-------------------------------------------------------------------------------
-- JNLua converts the coroutine to a string immediately, so we can't get the
-- traceback later. Because of that we have to do the error handling here.
-- Also, yield once to allow initializing up to here to get a memory baseline.

View File

@ -1,10 +1,10 @@
local args = table.pack(...)
local args = shell.parse(...)
if args.n == 0 then
for name, value in shell.aliases() do
print(name, value)
end
elseif args.n == 1 then
elseif #args == 1 then
local value = shell.alias(args[1])
if value then
print(value)

View File

@ -1,10 +1,10 @@
local args = table.pack(...)
if args.n == 0 then
local args = shell.parse(...)
if #args == 0 then
print("Usage: cat <filename1> [<filename2> [...]]")
return
end
for i = 1, args.n do
for i = 1, #args do
local file, reason = io.open(shell.resolve(args[i]), "r")
if not file then
print(reason)

View File

@ -1,5 +1,5 @@
local args = table.pack(...)
if args.n == 0 then
local args = shell.parse(...)
if #args == 0 then
print("Usage: cd <dirname>")
else
local result, reason = shell.cwd(shell.resolve(args[1]))

View File

@ -1,5 +1,5 @@
local args = table.pack(...)
if args.n < 2 then
local args = shell.parse(...)
if #args < 2 then
print("Usage: cp <from> <to>")
return
end

View File

@ -1,33 +1,29 @@
local args = table.pack(...)
local options = {}
local dirs = {}
for i = 1, args.n do
if args[i]:usub(1, 1) == "-" then
if args[i]:ulen() > 1 then
options[args[i]:usub(2)] = true
end
else
table.insert(dirs, args[i])
end
end
local dirs, options = shell.parse(...)
if #dirs == 0 then
table.insert(dirs, ".")
end
for i = 1, #dirs do
local path = shell.resolve(dirs[i])
if #dirs > 1 then
if i > 1 then print() end
print("/" .. path .. ":")
end
local list, reason = fs.dir(path)
if not list then
print(reason)
else
for f in list do
if options.l then
print(f, fs.size(fs.concat(path, f)))
else
io.write(f .. "\t")
if options.a or f:usub(1, 1) ~= "." then
if options.l then
print(f, fs.size(fs.concat(path, f)))
else
io.write(f .. "\t")
end
end
end
print()
if not options.l then
print()
end
end
end
end

View File

@ -1,14 +1,17 @@
print("Lua 5.2.2 Copyright (C) 1994-2013 Lua.org, PUC-Rio")
local running = true
local env = setmetatable({exit=function() running = false end}, {__index=_ENV})
while running and term.isAvailable() do
io.write("lua> ")
local command = io.read()
if not command then
return -- eof
local history = {}
local env = setmetatable({}, {__index=_ENV})
while term.isAvailable() do
term.write("lua> ")
local command = term.read(history)
if command == nil then -- eof
return
end
local statement, result = load(command, "=stdin", env)
local expression = load("return " .. command, "=stdin", env)
while #history > 10 do
table.remove(history, 1)
end
local statement, result = load(command, "=stdin", "t", env)
local expression = load("return " .. command, "=stdin", "t", env)
local code = expression or statement
if code then
local result = table.pack(pcall(code))

View File

@ -1,10 +1,10 @@
local args = table.pack(...)
if args.n == 0 then
local args = shell.parse(...)
if #args == 0 then
print("Usage: mkdir <dirname1> [<dirname2> [...]]")
return
end
for i = 1, args.n do
for i = 1, #args do
local path = shell.resolve(args[i])
local result, reason = fs.makeDirectory(path)
if not result then

View File

@ -1,5 +1,5 @@
local args = table.pack(...)
if args.n < 2 then
local args = shell.parse(...)
if #args < 2 then
print("Usage: mv <from> <to>")
return
end

View File

@ -1,10 +1,10 @@
local args = table.pack(...)
if args.n == 0 then
local args = shell.parse(...)
if #args == 0 then
print("Usage: rm <filename1> [<filename2> [...]]")
return
end
for i = 1, args.n do
for i = 1, #args do
local path = shell.resolve(args[i])
if not fs.remove(path) then
print(path .. ": no such file, or permission denied")

View File

@ -1,3 +1,9 @@
local args = table.pack(...)
if args.n > 0 then
os.execute(table.concat(args, " ", 1, args.n))
return
end
local function trim(s) -- from http://lua-users.org/wiki/StringTrim
local from = s:match"^%s*()"
return from > #s and "" or s:match(".*%S", from)

View File

@ -1,5 +1,5 @@
local args = table.pack(...)
if args.n < 1 then
local args = shell.parse(...)
if #args < 1 then
print("Usage: unalias <name>")
return
end

View File

@ -1,8 +1,9 @@
local args = table.pack(...)
if args.n == 0 then
local args = shell.parse(...)
if #args == 0 then
print("Usage: which <program>")
return
end
local result, reason = shell.which(args[1])
if result then
print(result)

View File

@ -6,7 +6,7 @@ function dofile(filename)
return program()
end
function loadfile(filename, env)
function loadfile(filename, mode, env)
local file, reason = io.open(filename)
if not file then
return nil, reason
@ -16,7 +16,7 @@ function loadfile(filename, env)
if not source then
return nil, reason
end
return load(source, "=" .. filename, env)
return load(source, "=" .. filename, mode, env)
end
function print(...)

View File

@ -20,6 +20,14 @@ function component.list(filter)
end
end
function component.isPrimary(address)
local componentType = component.type(address)
if componentType then
return primaries[componentType] == address
end
return false
end
function component.primary(componentType, ...)
checkArg(1, componentType, "string")
local args = table.pack(...)

View File

@ -95,13 +95,13 @@ function event.cancel(timerId)
timers[timerId] = nil
end
function event.interval(timeout, callback)
function event.interval(frequency, callback)
local interval = {}
local function onTimer()
interval.id = event.timer(timeout, onTimer)
interval.id = event.timer(frequency, onTimer)
callback()
end
interval.id = event.timer(timeout, onTimer)
interval.id = event.timer(frequency, onTimer)
return interval
end

View File

@ -0,0 +1,52 @@
keyboard = setmetatable({}, {__index = driver.keyboard})
local pressedChars = {}
local pressedCodes = {}
-------------------------------------------------------------------------------
function keyboard.isKeyDown(charOrCode)
checkArg(1, charOrCode, "string", "number")
if type(charOrCode) == "string" then
return pressedChars[charOrCode]
elseif type(charOrCode) == "number" then
return pressedCodes[charOrCode]
end
end
function keyboard.isControlDown()
return (pressedCodes[keyboard.keys.lcontrol] or pressedCodes[keyboard.keys.rcontrol]) ~= nil
end
function keyboard.isShiftDown()
return (pressedCodes[keyboard.keys.lshift] or pressedCodes[keyboard.keys.rshift]) ~= nil
end
-------------------------------------------------------------------------------
local function onKeyDown(_, address, char, code)
if component.isPrimary(address) then
pressedChars[char] = true
pressedCodes[code] = true
end
end
local function onKeyUp(_, address, char, code)
if component.isPrimary(address) then
pressedChars[char] = nil
pressedCodes[code] = nil
end
end
local function onComponentUnavailable(_, componentType)
if componentType == "keyboard" then
pressedChars = {}
pressedCodes = {}
end
end
return function()
event.listen("key_down", onKeyDown)
event.listen("key_up", onKeyUp)
event.listen("component_unavailable", onComponentUnavailable)
end

View File

@ -25,6 +25,8 @@ os.remove = driver.filesystem.remove
os.rename = driver.filesystem.rename
os.sleep = event.wait
function os.tmpname()
if driver.filesystem.exists("tmp") then
for i = 1, 10 do

View File

@ -146,7 +146,7 @@ function shell.execute(program, ...)
pevent = setmetatable(pevent, {__index = event, __metatable = {}})
local env = setmetatable({event = pevent}, {__index = _ENV, __metatable = {}})
program, reason = loadfile(where, env)
program, reason = loadfile(where, "t", env)
if not program then
return nil, reason
end
@ -170,6 +170,23 @@ function shell.execute(program, ...)
return table.unpack(result, 1, result.n)
end
function shell.parse(...)
local params = table.pack(...)
local args = {}
local options = {}
for i = 1, params.n do
local param = params[i]
if param:usub(1, 1) == "-" then
for j = 2, param:ulen() do
options[param:usub(j, j)] = true
end
else
table.insert(args, param)
end
end
return args, options
end
function shell.resolve(path)
if path:usub(1, 1) == "/" then
return fs.canonical(path)

View File

@ -1,14 +1,6 @@
local cursorX, cursorY = 1, 1
local cursorBlink = nil
local function toggleBlink()
cursorBlink.state = not cursorBlink.state
if term.isAvailable() then
local char = cursorBlink.state and cursorBlink.solid or cursorBlink.alt
gpu.set(cursorX, cursorY, char)
end
end
-------------------------------------------------------------------------------
term = {}
@ -43,6 +35,13 @@ function term.cursor(col, row)
end
function term.cursorBlink(enabled)
local function toggleBlink()
cursorBlink.state = not cursorBlink.state
if term.isAvailable() then
local char = cursorBlink.state and cursorBlink.solid or cursorBlink.alt
gpu.set(cursorX, cursorY, char)
end
end
local function start(alt)
if not cursorBlink then
cursorBlink = event.interval(0.5, toggleBlink)
@ -74,6 +73,8 @@ function term.cursorBlink(enabled)
else
stop()
end
elseif enabled and cursorBlink then
toggleBlink()
end
return cursorBlink ~= nil
end
@ -84,11 +85,11 @@ function term.read(history)
history = history or {}
table.insert(history, "")
local current = #history
local keys = driver.keyboard.keys
local start = term.cursor()
local cursor, scroll = 1, 0
local keyRepeat = nil
local result = nil
local forceReturn = false -- for eof where result is nil
local function remove()
local x = start - 1 + cursor - scroll
local _, y = term.cursor()
@ -160,7 +161,7 @@ function term.read(history)
term.cursorBlink(cursor <= history[current]:ulen() and
history[current]:usub(cursor, cursor) or " ")
if blink then
toggleBlink()
term.cursorBlink(true)
end
end
local function handleKeyPress(char, code)
@ -168,7 +169,7 @@ function term.read(history)
local w, h = gpu.resolution()
local cancel, blink = false, false
term.cursorBlink(false)
if code == keys.back then
if code == keyboard.keys.back then
if cursor > 1 then
copyIfNecessary()
history[current] = history[current]:usub(1, cursor - 2) ..
@ -180,7 +181,7 @@ function term.read(history)
remove()
end
cancel = cursor == 1
elseif code == keys.delete then
elseif code == keyboard.keys.delete then
if cursor <= history[current]:ulen() then
copyIfNecessary()
history[current] = history[current]:usub(1, cursor - 1) ..
@ -188,7 +189,7 @@ function term.read(history)
remove()
end
cancel = cursor == history[current]:ulen() + 1
elseif code == keys.left then
elseif code == keyboard.keys.left then
if cursor > 1 then
cursor = cursor - 1
if cursor - scroll < 1 then
@ -197,7 +198,7 @@ function term.read(history)
end
cancel = cursor == 1
blink = true
elseif code == keys.right then
elseif code == keyboard.keys.right then
if cursor < history[current]:ulen() + 1 then
cursor = cursor + 1
if cursor - scroll > w - (start - 1) then
@ -206,34 +207,34 @@ function term.read(history)
end
cancel = cursor == history[current]:ulen() + 1
blink = true
elseif code == keys.home then
elseif code == keyboard.keys.home then
if cursor > 1 then
cursor, scroll = 1, 0
render()
end
cancel = true
blink = true
elseif code == keys["end"] then
elseif code == keyboard.keys["end"] then
if cursor < history[current]:ulen() + 1 then
scrollEnd()
end
cancel = true
blink = true
elseif code == keys.up then
elseif code == keyboard.keys.up then
if current > 1 then
current = current - 1
scrollEnd()
end
cancel = current == 1
blink = true
elseif code == keys.down then
elseif code == keyboard.keys.down then
if current < #history then
current = current + 1
scrollEnd()
end
cancel = current == #history
blink = true
elseif code == keys.enter then
elseif code == keyboard.keys.enter then
if current ~= #history then -- bring entry to front
history[#history] = history[current]
table.remove(history, current)
@ -244,7 +245,18 @@ function term.read(history)
table.remove(history, current)
end
return true
elseif not keys.isControl(char) then
elseif keyboard.isControlDown() then
if code == keyboard.keys.d then
if history[current] == "" then
forceReturn = true
return true
end
elseif code == keyboard.keys.c then
history[current] = ""
forceReturn = true
return true
end
elseif not keyboard.isControl(char) then
copyIfNecessary()
history[current] = history[current]:usub(1, cursor - 1) ..
string.uchar(char) ..
@ -259,9 +271,7 @@ function term.read(history)
return cancel
end
local function onKeyDown(_, address, char, code)
if not component.isAvailable("keyboard") or
address ~= component.primary("keyboard")
then
if not component.isPrimary(address) then
return
end
if keyRepeat then
@ -277,19 +287,12 @@ function term.read(history)
end
end
local function onKeyUp(_, address, char, code)
if not component.isAvailable("keyboard") or
address ~= component.primary("keyboard")
then
return
end
if keyRepeat then
if component.isPrimary(address) and keyRepeat then
keyRepeat = event.cancel(keyRepeat)
end
end
local function onClipboard(_, address, value)
if not component.isAvailable("keyboard") or
address ~= component.primary("keyboard")
then
if not component.isPrimary(address) then
return
end
copyIfNecessary()
@ -308,7 +311,7 @@ function term.read(history)
event.listen("key_up", onKeyUp)
event.listen("clipboard", onClipboard)
term.cursorBlink(true)
while term.isAvailable() and not result do
while term.isAvailable() and not result and not forceReturn do
event.wait()
end
if history[#history] == "" then