From 02531670d97ece0316b53a077723e6d1dfaef794 Mon Sep 17 00:00:00 2001 From: payonel Date: Sun, 28 Feb 2016 00:45:12 -0800 Subject: [PATCH] new term library, faster, light-weight, better widechars --- .../opencomputers/loot/OpenOS/bin/alias.lua | 2 +- .../opencomputers/loot/OpenOS/bin/dmesg.lua | 18 +- .../opencomputers/loot/OpenOS/bin/edit.lua | 59 +- .../opencomputers/loot/OpenOS/bin/grep.lua | 8 +- .../opencomputers/loot/OpenOS/bin/ls.lua | 10 +- .../opencomputers/loot/OpenOS/bin/lua.lua | 25 +- .../opencomputers/loot/OpenOS/bin/more.lua | 25 +- .../loot/OpenOS/bin/resolution.lua | 9 +- .../opencomputers/loot/OpenOS/bin/sh.lua | 13 +- .../opencomputers/loot/OpenOS/bin/source.lua | 6 +- .../opencomputers/loot/OpenOS/boot/02_os.lua | 10 - .../opencomputers/loot/OpenOS/boot/03_io.lua | 6 +- .../opencomputers/loot/OpenOS/boot/91_gpu.lua | 1 + .../loot/OpenOS/boot/93_term.lua | 111 +- .../assets/opencomputers/loot/OpenOS/etc/motd | 3 +- .../opencomputers/loot/OpenOS/etc/profile | 11 + .../assets/opencomputers/loot/OpenOS/init.lua | 5 +- .../opencomputers/loot/OpenOS/lib/sh.lua | 749 +++++----- .../opencomputers/loot/OpenOS/lib/term.lua | 1225 ++++++----------- .../opencomputers/loot/OpenOS/lib/text.lua | 16 +- .../loot/OpenOS/lib/transforms.lua | 4 +- 21 files changed, 935 insertions(+), 1381 deletions(-) diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/alias.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/alias.lua index de2965b92..a9594a447 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/alias.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/alias.lua @@ -1,7 +1,7 @@ local shell = require("shell") local args, options = shell.parse(...) -local ec = 0 +local ec, error_prefix = 0, "alias:" if options.help then print(string.format("Usage: alias: [name[=value] ... ]", cmd_name)) diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/dmesg.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/dmesg.lua index d4664013c..038b3d7aa 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/dmesg.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/dmesg.lua @@ -1,16 +1,12 @@ local event = require "event" local term = require "term" -local keyboard = require "keyboard" local args = {...} - +local gpu = term.gpu() local interactive = io.output().tty -local function gpu() - return select(2, term.getGPU()) -end local color, isPal, evt if interactive then - color, isPal = gpu().getForeground() + color, isPal = gpu.getForeground() end io.write("Press 'Ctrl-C' to exit\n") pcall(function() @@ -20,13 +16,13 @@ pcall(function() else evt = table.pack(event.pull()) end - if interactive then gpu().setForeground(0xCC2200) end + if interactive then gpu.setForeground(0xCC2200) end io.write("[" .. os.date("%T") .. "] ") - if interactive then gpu().setForeground(0x44CC00) end + if interactive then gpu.setForeground(0x44CC00) end io.write(tostring(evt[1]) .. string.rep(" ", math.max(10 - #tostring(evt[1]), 0) + 1)) - if interactive then gpu().setForeground(0xB0B00F) end + if interactive then gpu.setForeground(0xB0B00F) end io.write(tostring(evt[2]) .. string.rep(" ", 37 - #tostring(evt[2]))) - if interactive then gpu().setForeground(0xFFFFFF) end + if interactive then gpu.setForeground(0xFFFFFF) end if evt.n > 2 then for i = 3, evt.n do io.write(" " .. tostring(evt[i])) @@ -37,6 +33,6 @@ pcall(function() until evt[1] == "interrupted" end) if interactive then - gpu().setForeground(color, isPal) + gpu.setForeground(color, isPal) end diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/edit.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/edit.lua index 9e80411a4..bb988aa84 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/edit.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/edit.lua @@ -9,12 +9,7 @@ local unicode = require("unicode") if not term.isAvailable() then return end - -local function gpu() - return select(2, term.getGPU()) -end - - +local gpu = term.gpu() local args, options = shell.parse(...) if #args == 0 then io.write("Usage: edit ") @@ -121,7 +116,7 @@ local function setStatus(value) local x, y, w, h = term.getGlobalArea() value = unicode.wlen(value) > w - 10 and unicode.wtrunc(value, w - 9) or value value = text.padRight(value, w - 10) - gpu().set(x, y + h - 1, value) + gpu.set(x, y + h - 1, value) end local function getArea() @@ -170,7 +165,7 @@ local function drawLine(x, y, w, h, lineNr) local str = removePrefix(buffer[lineNr] or "", scrollX) str = unicode.wlen(str) > w and unicode.wtrunc(str, w + 1) or str str = text.padRight(str, w) - gpu().set(x, y - 1 + lineNr - scrollY, str) + gpu.set(x, y - 1 + lineNr - scrollY, str) end end @@ -204,7 +199,7 @@ local function setCursor(nbx, nby) local dy = math.abs(scrollY - sy) scrollY = sy if h > dy then - gpu().copy(x, y + dy, w, h - dy, 0, -dy) + gpu.copy(x, y + dy, w, h - dy, 0, -dy) end for lineNr = nby - (math.min(dy, h) - 1), nby do drawLine(x, y, w, h, lineNr) @@ -215,7 +210,7 @@ local function setCursor(nbx, nby) local dy = math.abs(scrollY - sy) scrollY = sy if h > dy then - gpu().copy(x, y, w, h - dy, 0, dy) + gpu.copy(x, y, w, h - dy, 0, dy) end for lineNr = nby, nby + (math.min(dy, h) - 1) do drawLine(x, y, w, h, lineNr) @@ -242,7 +237,7 @@ local function setCursor(nbx, nby) term.setCursor(nbx - scrollX, nby - scrollY) --update with term lib nbx, nby = getCursor() - gpu().set(x + w - 10, y + h, text.padLeft(string.format("%d,%d", nby, nbx), 10)) + gpu.set(x + w - 10, y + h, text.padLeft(string.format("%d,%d", nby, nbx), 10)) end local function highlight(bx, by, length, enabled) @@ -252,21 +247,21 @@ local function highlight(bx, by, length, enabled) cy = math.max(1, math.min(h, cy)) length = math.max(1, math.min(w - cx, length)) - local fg, fgp = gpu().getForeground() - local bg, bgp = gpu().getBackground() + local fg, fgp = gpu.getForeground() + local bg, bgp = gpu.getBackground() if enabled then - gpu().setForeground(bg, bgp) - gpu().setBackground(fg, fgp) + gpu.setForeground(bg, bgp) + gpu.setBackground(fg, fgp) end local indexFrom = lengthToChars(buffer[by], bx) local value = unicode.sub(buffer[by], indexFrom) if unicode.wlen(value) > length then value = unicode.wtrunc(value, length + 1) end - gpu().set(x - 1 + cx, y - 1 + cy, value) + gpu.set(x - 1 + cx, y - 1 + cy, value) if enabled then - gpu().setForeground(fg, fgp) - gpu().setBackground(bg, bgp) + gpu.setForeground(fg, fgp) + gpu.setBackground(bg, bgp) end end @@ -336,7 +331,7 @@ local function delete(fullRow) local content = table.remove(buffer, row) local rcy = cy + (row - cby) if rcy <= h then - gpu().copy(x, y + rcy, w, h - rcy, 0, -1) + gpu.copy(x, y + rcy, w, h - rcy, 0, -1) drawLine(x, y, w, h, row + (h - rcy)) end return content @@ -347,7 +342,7 @@ local function delete(fullRow) deleteRow(cby) else buffer[cby] = "" - gpu().fill(x, y - 1 + cy, w, 1, " ") + gpu.fill(x, y - 1 + cy, w, 1, " ") end setCursor(1, cby) elseif cbx <= unicode.wlen(line()) then @@ -395,7 +390,7 @@ local function enter() drawLine(x, y, w, h, cby) if cy < h then if cy < h - 1 then - gpu().copy(x, y + cy, w, h - (cy + 1), 0, 1) + gpu.copy(x, y + cy, w, h - (cy + 1), 0, 1) end drawLine(x, y, w, h, cby + 1) end @@ -434,10 +429,9 @@ local function find() term.setCursor(7 + unicode.wlen(findText), h + 1) setStatus("Find: " .. findText) - local _, address, char, code = event.pull("key_down") - local inFocus, screenAddress, keyboardAddress = term.hasFocus() - if inFocus and address == keyboardAddress then - local handler, name = getKeyBindHandler(code, keyboardAddress) + local _, address, char, code = term.pull("key_down") + if address == term.keyboard().address then + local handler, name = getKeyBindHandler(code) highlight(cbx, cby, unicode.wlen(findText), false) if name == "newline" then break @@ -547,7 +541,7 @@ local keyBindHandlers = { findnext = find } -getKeyBindHandler = function(code, keyboardAddress) +getKeyBindHandler = function(code) if type(config.keybinds) ~= "table" then return end -- Look for matches, prefer more 'precise' keybinds, e.g. prefer -- ctrl+del over del. @@ -563,6 +557,7 @@ getKeyBindHandler = function(code, keyboardAddress) elseif value == "shift" then shift = true else key = value end end + local keyboardAddress = term.keyboard().address if (not alt or keyboard.isAltDown(keyboardAddress)) and (not control or keyboard.isControlDown(keyboardAddress)) and (not shift or keyboard.isShiftDown(keyboardAddress)) and @@ -582,8 +577,8 @@ end ------------------------------------------------------------------------------- -local function onKeyDown(char, code, keyboardAddress) - local handler = getKeyBindHandler(code, keyboardAddress) +local function onKeyDown(char, code) + local handler = getKeyBindHandler(code) if handler then handler() elseif readonly and code == keyboard.keys.q then @@ -660,12 +655,11 @@ do end while running do - local event, address, arg1, arg2, arg3 = event.pull() - local inFocus, screenAddress, keyboardAddress = term.hasFocus() - if inFocus and (address == screenAddress or address == keyboardAddress) then + local event, address, arg1, arg2, arg3 = term.pull() + if address == term.keyboard().address or address == term.screen().address then local blink = true if event == "key_down" then - onKeyDown(arg1, arg2, keyboardAddress) + onKeyDown(arg1, arg2) elseif event == "clipboard" and not readonly then onClipboard(arg1) elseif event == "touch" or event == "drag" then @@ -682,7 +676,6 @@ while running do end if blink then term.setCursorBlink(true) - term.setCursorBlink(true) -- force toggle to caret end end end diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/grep.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/grep.lua index 579cc4470..ba0eafa67 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/grep.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/grep.lua @@ -14,9 +14,7 @@ local term = require("term") local args, options = shell.parse(...) -local function gpu() - return select(2, term.getGPU()) -end +local gpu = term.gpu() local function printUsage(ostream, msg) local s = ostream or io.stdout @@ -111,8 +109,8 @@ local print_count = pop('c','count') local colorize = pop('color','colour') and io.output().tty and term.isAvailable() local noop = function(...)return ...;end -local setc = colorize and gpu().setForeground or noop -local getc = colorize and gpu().getForeground or noop +local setc = colorize and gpu.setForeground or noop +local getc = colorize and gpu.getForeground or noop local trim = pop('t','trim') local trim_front = trim and function(s)return s:gsub('^%s+','')end or noop diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/ls.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/ls.lua index 1902a2e9b..cdeea8316 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/ls.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/ls.lua @@ -29,7 +29,7 @@ For more info run: man ls]]) end local ec = 0 -local gpu = select(2, term.getGPU()) +local gpu = term.gpu() local fOut = term.isAvailable() and io.output().tty local function perr(msg) io.stderr:write(msg,"\n") ec = 2 end local function _path(n,i) return n[i]:sub(1, 1) == '/' and "" or n.path end @@ -53,7 +53,7 @@ if fOut and not ops["no-color"] then LSC = require("serialization").unserialize(LSC) end if not LSC then - perr("ls: unparsable value for LSC environment variable") + perr("ls: unparsable value for LS_COLORS environment variable") else prev_color = gpu.getForeground() restore_color = function() gpu.setForeground(prev_color) end @@ -215,12 +215,12 @@ local function display(n) end return max end - local function measure() + local function measure(_cols) local t=0 - for c=1,cols do t=t+max_name(c)+(c>1 and 2 or 0) end + for c=1,_cols do t=t+max_name(c)+(c>1 and 2 or 0) end return t end - repeat d=d+1 cols=math.ceil(#n/d) until d>=#n or measure() (tonumber(os.getenv("HISTSIZE")) or 10) do - table.remove(history, 1) - end command = text.trim(command) if command == "exit" then return diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/source.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/source.lua index 4738d3d60..fe854b8c6 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/source.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/source.lua @@ -17,15 +17,15 @@ if not file then end return 1 else - local status, reason = pcall(function() + local status, reason = xpcall(function() repeat local line = file:read("*L") if line then sh.execute(nil, line) end until not line - end) + end, function(msg) return {msg, debug.traceback()} end) file:close() - assert(status, reason) + if not status and reason then assert(false, tostring(reason[1]) .."\n".. tostring(reason[2])) end end diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/02_os.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/02_os.lua index 3b77db6cc..b6104260a 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/02_os.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/02_os.lua @@ -73,19 +73,9 @@ function os.tmpname() end end -os.setenv("EDITOR", "/bin/edit") -os.setenv("HISTSIZE", "10") -os.setenv("HOME", "/home") -os.setenv("IFS", " ") -os.setenv("MANPATH", "/usr/man:.") -os.setenv("PAGER", "/bin/more") os.setenv("PATH", "/bin:/usr/bin:/home/bin:.") -os.setenv("PS1", "$PWD# ") -os.setenv("PWD", "/") -os.setenv("SHELL", "/bin/sh") os.setenv("TMP", "/tmp") -- Deprecated os.setenv("TMPDIR", "/tmp") -os.setenv("LS_COLORS",[[{FILE=0xFFFFFF,DIR=0x66CCFF,LINK=0xFFAA00,["*.lua"]=0x00FF00}]]) if computer.tmpAddress() then fs.mount(computer.tmpAddress(), os.getenv("TMPDIR") or "/tmp") diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua index 33e707316..945ae2fa0 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/03_io.lua @@ -22,10 +22,8 @@ stdoutStream.close = stdinStream.close stderrStream.close = stdinStream.close function stdinStream:read(n, dobreak) - local result = term.read(stdinHistory, dobreak) - while #stdinHistory > 10 do - table.remove(stdinHistory, 1) - end + stdinHistory.dobreak = dobreak + local result = term.readKeyboard(stdinHistory) return result end diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/91_gpu.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/91_gpu.lua index 4f4717a94..7475b11c9 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/91_gpu.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/91_gpu.lua @@ -8,6 +8,7 @@ local function onComponentAvailable(_, componentType) component.gpu.bind(component.screen.address) local depth = 2^(component.gpu.getDepth()) os.setenv("TERM", "term-"..depth.."color") + require("computer").pushSignal("gpu_bound", component.gpu.address, component.screen.address) end end diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/93_term.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/93_term.lua index 637bfbe7a..f376f3257 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/boot/93_term.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/boot/93_term.lua @@ -1,104 +1,29 @@ local component = require("component") local computer = require("computer") local event = require("event") -local keyboard = require("keyboard") local term = require("term") +local process = require("process") -local gpuAvailable, screenAvailable = false, false +-- this should be the init level process +process.info().data.window = term.internal.open() -local function isAvailable() - return gpuAvailable and screenAvailable -end +event.listen("gpu_bound", function(ename, gpu, screen) + gpu=component.proxy(gpu) + screen=component.proxy(screen) + term.bind(gpu, screen) + computer.pushSignal("term_available") +end) -local function onComponentAvailable(_, componentType) - local wasAvailable = isAvailable() - if componentType == "gpu" then - gpuAvailable = true - elseif componentType == "screen" then - screenAvailable = true - end - if not wasAvailable and isAvailable() then - computer.pushSignal("term_available") - end -end - -local function onComponentUnavailable(_, componentType) - local wasAvailable = isAvailable() - if componentType == "gpu" then - gpuAvailable = false - elseif componentType == "screen" then - screenAvailable = false - end - if wasAvailable and not isAvailable() then - computer.pushSignal("term_unavailable") - end -end - -local function onTermFocus(event, screenAddress) - term.__focus[screenAddress] = term.__nextFocus[screenAddress] -end - -local function updateKeyboards(event, _, typ) - if event == nil or typ == "screen" or typ == "keyboard" then - term.__keyboardToScreen = {} - term.__screenToKeyboard = {} - for screenAddress in component.list("screen", true) do - local keyboardAddress = component.invoke(screenAddress, "getKeyboards")[1] - if keyboardAddress then - term.__keyboardToScreen[keyboardAddress] = screenAddress - term.__screenToKeyboard[screenAddress] = keyboardAddress +event.listen("component_unavailable", function(_,type) + if type == "screen" or type == "gpu" then + if term.isAvailable() then + local window = term.internal.window() + if window[type] and not component.proxy(window[type].address) then + window[type] = nil end end - end -end - -local function onKeyDown(event, keyboardAddress, char, code) - local screenAddress = term.__keyboardToScreen[keyboardAddress] - if screenAddress then - local self = term.__focus[screenAddress] - if self then - if code == keyboard.keys.tab then - if keyboard.isControlDown(keyboardAddress) then - if keyboard.isShiftDown(keyboardAddress) then - self:focusPrevious() - else - self:focusNext() - end - end - end + if not term.isAvailable() then + computer.pushSignal("term_unavailable") end end -end - -local function onTouch(event, screenAddress, x, y) - local list = term.__knownWindows[screenAddress] - if list[1] then - local _, gpu = list[1]:getGPU() - if gpu and gpu.getScreen() == screenAddress then - local w, h = gpu.getViewport() - if w then - local nextWindow - for _, window in ipairs(list) do - local xWin, yWin, wWin, hWin = window:getGlobalArea(w, h) - local xOffset, yOffset = x - xWin, y - yWin - if xOffset >= 0 and xOffset < wWin and yOffset >= 0 and yOffset < hWin then - nextWindow = window - end - end - if nextWindow then - nextWindow:focus() - end - end - end - end -end - -event.listen("component_available", onComponentAvailable) -event.listen("component_unavailable", onComponentUnavailable) -event.listen("component_added", updateKeyboards) -event.listen("component_removed", updateKeyboards) -event.listen("term_focus", onTermFocus) -event.listen("key_down", onKeyDown) -event.listen("touch", onTouch) - - +end) diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/etc/motd b/src/main/resources/assets/opencomputers/loot/OpenOS/etc/motd index b3d6db74e..5d011ae70 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/etc/motd +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/etc/motd @@ -4,6 +4,7 @@ local component = require("component") local computer = require("computer") local text = require("text") local unicode = require("unicode") +local term = require("term") if not component.isAvailable("gpu") then return @@ -19,7 +20,7 @@ if f then f:close() local greeting = greetings[math.random(1, #greetings)] if greeting then - local width = math.max(10, component.gpu.getViewport()) + local width = math.max(10, term.getViewport()) for line in text.wrappedLines(greeting, width - 4, width - 4) do table.insert(lines, line) maxWidth = math.max(maxWidth, unicode.len(line)) diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/etc/profile b/src/main/resources/assets/opencomputers/loot/OpenOS/etc/profile index 749edb6c2..3bfbe6c21 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/etc/profile +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/etc/profile @@ -12,6 +12,17 @@ alias view=edit\ -r alias help=man alias cp=cp\ -i +set EDITOR=/bin/edit +set HISTSIZE=10 +set HOME=/home +set IFS=\ +set MANPATH=/usr/man:. +set PAGER=/bin/more +set PS1='$PWD# ' +set PWD=/ +set SHELL=/bin/sh +set LS_COLORS="{FILE=0xFFFFFF,DIR=0x66CCFF,LINK=0xFFAA00,['*.lua']=0x00FF00}" + cd $HOME clear /etc/motd diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/init.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/init.lua index 06f52d17d..b5e39025a 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/init.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/init.lua @@ -123,6 +123,7 @@ do package.delayed["text"] = true package.delayed["sh"] = true package.delayed["transforms"] = true + package.delayed["term"] = true end status("Initializing file system...") @@ -169,7 +170,9 @@ do end while true do - local result, reason = require("shell").execute() + local result, reason = xpcall(require("shell").getShell(), function(msg) + return tostring(msg).."\n"..debug.traceback() + end) if not result then io.stderr:write((reason ~= nil and tostring(reason) or "unknown error") .. "\n") io.write("Press any key to continue.\n") diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua index 696b76991..f552c7317 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/sh.lua @@ -24,63 +24,6 @@ local local_env = {event=event,fs=fs,process=process,shell=shell,term=term,text= ------------------------------------------------------------------------------- -function --[[@delayloaded-start@]] sh.internal.newMemoryStream() -local memoryStream = {} - -function memoryStream:close() - self.closed = true -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.read then - -- popen could be using this code path - -- if that is the case, it is important to leave stream.buffer alone - return self.redirect.read:read(n) - elseif self.buffer == "" then - self.args = table.pack(coroutine.yield(table.unpack(self.result))) - 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.write and self.closed then - -- if next is dead, ignore all writes - if coroutine.status(self.next) ~= "dead" then - error("attempt to use a closed stream") - end - elseif self.redirect.write then - return self.redirect.write:write(value) - elseif not self.closed then - self.buffer = self.buffer .. value - self.result = table.pack(coroutine.resume(self.next, table.unpack(self.args))) - if coroutine.status(self.next) == "dead" then - self:close() - end - if not self.result[1] then - error(self.result[2], 0) - end - table.remove(self.result, 1) - return self - end - return nil, 'stream closed' -end - - local stream = {closed = false, buffer = "", - redirect = {}, result = {}, args = {}} - local metatable = {__index = memoryStream, - __metatable = "memorystream"} - return setmetatable(stream, metatable) -end --[[@delayloaded-end@]] - --SH API sh.internal.globbers = {{"*",".*"},{"?","."}} @@ -109,107 +52,6 @@ function sh.internal.command_result_as_code(ec) end 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 sh.internal.resolveActions(input, resolver, resolved) checkArg(1, input, "string") checkArg(2, resolver, "function", "nil") @@ -281,46 +123,8 @@ function sh.internal.statements(input) return statements 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 - - pipes = pipes or tx.sub(text.syntax, 2) -- first text syntax is ; which CAN be repeated - - local pies = tx.select(words, function(parts, i, t) - return (#parts == 1 and tx.first(pipes, {{parts[1].txt}}) and true or false), i - end) - - local bad_pipe - local last = 0 - for k,v in ipairs(pies) do - if v then - if k-last == 1 then - bad_pipe = words[k][1].txt - break - end - last=k - end - end - - if not bad_pipe and last == #pies then - bad_pipe = words[last][1].txt - end - - if bad_pipe then - return false, "parse error near " .. bad_pipe - else - return true - end -end --[[@delayloaded-end@]] - -- returns true if key is a string that represents a valid command line identifier -function sh.isIdentifier(key) +function sh.internal.isIdentifier(key) if type(key) ~= "string" then return false end @@ -328,77 +132,32 @@ function sh.isIdentifier(key) return key:match("^[%a_][%w_]*$") == key end --- returns the environment stored value of key if it exists else an empty string -function sh.expandKey(key) - -- reserved alias $? is last error codes - if key == "?" then - return tostring(sh.getLastExitCode()) - end - - return os.getenv(key) or '' -end - -function sh.keySubstitution(key) - if sh.isIdentifier(key) then - return sh.expandKey(key) - end - - error("${" .. key .. "}: bad substitution") -end - function sh.expand(value) - local result = value:gsub("%$([_%w%?]+)", sh.expandKey):gsub("%${(.*)}", sh.keySubstitution) - return result -- we return just result because gsub returns multiple to the stack + return value + :gsub("%$([_%w%?]+)", function(key) + if key == "?" then + return tostring(sh.getLastExitCode()) + end + return os.getenv(key) or '' end) + :gsub("%${(.*)}", function(key) + if sh.internal.isIdentifier(key) then + return sh.internal.expandKey(key) + end + error("${" .. key .. "}: bad substitution") + end) end -function --[[@delayloaded-start@]] sh.internal.glob(glob_pattern) - local segments = text.split(glob_pattern, {"/"}, true) - local hiddens = tx.select(segments,function(e)return e:match("^%%%.")==nil end) - local function is_visible(s,i) - return not hiddens[i] or s:match("^%.") == nil +function sh.internal.expand(word) + if #word == 0 then return {} end + + local result = '' + for i=1,#word do + local part = word[i] + result = result .. (not (part.qr and part.qr[3]) and sh.expand(part.txt) or part.txt) 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() - 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@]] + return {result} +end -- expand to files in path, or try key substitution -- word is a list of metadata-filled word parts @@ -408,7 +167,7 @@ function sh.internal.evaluate(word) if #word == 0 then return {} elseif #word == 1 and word[1].qr then - return {sh.expand(word[1].txt)} + return sh.internal.expand(word) end local function make_pattern(seg) local result = seg @@ -422,12 +181,10 @@ function sh.internal.evaluate(word) end return result end - local normalized = '' local glob_pattern = '' local has_globits = false for i=1,#word do local part = word[i] local next = part.txt - normalized = normalized .. next if not part.qr then local escaped = text.escapeMagic(next) next = make_pattern(escaped) @@ -438,114 +195,12 @@ function sh.internal.evaluate(word) glob_pattern = glob_pattern .. next end if not has_globits then - return {sh.expand(normalized)} + return sh.internal.expand(word) end local globs = sh.internal.glob(glob_pattern) - return #globs == 0 and {sh.expand(normalized)} or globs + return #globs == 0 and sh.internal.expand(word) or globs 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(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(basePath, name) - 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:len() == 0 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) - 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) - if line:sub(-1):find("%s") then - return '', line - end - local splits = text.internal.tokenize(line) - if not splits then -- parse error, e.g. unclosed quotes - return nil -- no split, no hints - end - local num_splits = #splits - if num_splits == 1 or not isWordOf(splits[num_splits-1],{";","&&","||","|"}) then - return '', line - end - local l = text.internal.normalize({splits[num_splits]})[1] - return line:sub(1,-l:len()-1), l -end --[[@delayloaded-end@]] - -function --[[@delayloaded-start@]] sh.internal.hintHandlerImpl(full_line, cursor) - local line = unicode.sub(full_line, 1, cursor - 1) - local suffix = unicode.sub(full_line, cursor) - if not line or #line < 1 then - return {} - end - local prev,line = sh.internal.hintHandlerSplit(line) - if not prev then -- failed to parse, e.g. unclosed quote, no hints - return {} - end - local result - local prefix, partial = string.match(line, "^(.+%s+)(.*)$") - local partialPrefix = (partial or line) - local name = partialPrefix:gsub(".*/", "") - partialPrefix = partialPrefix:sub(1, -name:len() - 1) - local searchInPath = not prefix and not partialPrefix:find("/") - if searchInPath then - result = sh.getMatchingPrograms(line) - else - result = sh.getMatchingFiles(partialPrefix, name) - end - 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 or not prefix) then - resultSuffix = " " .. resultSuffix - end - prefix = prev .. (prefix or "") - table.sort(result) - for i = 1, #result do - result[i] = prefix .. result[i] .. resultSuffix - end - return result -end --[[@delayloaded-end@]] - function sh.hintHandler(full_line, cursor) return sh.internal.hintHandlerImpl(full_line, cursor) end @@ -764,6 +419,358 @@ function sh.execute(env, command, ...) return true end + return sh.internal.execute_complex(statements) +end + +function --[[@delayloaded-start@]] sh.internal.glob(glob_pattern) + local segments = text.split(glob_pattern, {"/"}, true) + local hiddens = tx.select(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() + 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(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(basePath, name) + 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) + 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) + if line:sub(-1):find("%s") then + return '', line + end + local splits = text.internal.tokenize(line) + if not splits then -- parse error, e.g. unclosed quotes + return nil -- no split, no hints + end + local num_splits = #splits + if num_splits == 1 or not isWordOf(splits[num_splits-1],{";","&&","||","|"}) then + return '', line + end + local l = text.internal.normalize({splits[num_splits]})[1] + return line:sub(1,-unicode.len(l)-1), l +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] sh.internal.hintHandlerImpl(full_line, cursor) + local line = unicode.sub(full_line, 1, cursor - 1) + local suffix = unicode.sub(full_line, cursor) + if not line or #line < 1 then + return {} + end + local prev,line = sh.internal.hintHandlerSplit(line) + if not prev then -- failed to parse, e.g. unclosed quote, no hints + return {} + end + local result + local prefix, partial = line:match("^(.*=)(.*)$") + if not prefix then prefix, partial = line:match("^(.+%s+)(.*)$") end + local partialPrefix = (partial or line) + local name = partialPrefix:gsub(".*/", "") + partialPrefix = partialPrefix:sub(1, -unicode.len(name) - 1) + local searchInPath = not prefix and not partialPrefix:find("/") + if searchInPath then + result = sh.getMatchingPrograms(line) + else + result = sh.getMatchingFiles(partialPrefix, name) + end + 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 or not prefix) then + resultSuffix = " " .. resultSuffix + end + prefix = prev .. (prefix or "") + table.sort(result) + for i = 1, #result do + result[i] = prefix .. 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 + + pipes = pipes or tx.sub(text.syntax, 2) -- first text syntax is ; which CAN be repeated + + local pies = tx.select(words, function(parts, i, t) + return (#parts == 1 and tx.first(pipes, {{parts[1].txt}}) and true or false), i + end) + + local bad_pipe + local last = 0 + for k,v in ipairs(pies) do + if v then + if k-last == 1 then + bad_pipe = words[k][1].txt + break + end + last=k + end + end + + if not bad_pipe and last == #pies then + bad_pipe = words[last][1].txt + end + + if bad_pipe then + return false, "parse error near " .. bad_pipe + 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 + 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.read then + -- popen could be using this code path + -- if that is the case, it is important to leave stream.buffer alone + return self.redirect.read:read(n) + elseif self.buffer == "" then + self.args = table.pack(coroutine.yield(table.unpack(self.result))) + 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.write and self.closed then + -- if next is dead, ignore all writes + if coroutine.status(self.next) ~= "dead" then + error("attempt to use a closed stream") + end + elseif self.redirect.write then + return self.redirect.write:write(value) + elseif not self.closed then + self.buffer = self.buffer .. value + self.result = table.pack(coroutine.resume(self.next, table.unpack(self.args))) + if coroutine.status(self.next) == "dead" then + self:close() + end + if not self.result[1] then + error(self.result[2], 0) + end + table.remove(self.result, 1) + return self + end + return nil, 'stream closed' + end + + local stream = {closed = false, buffer = "", + redirect = {}, result = {}, args = {}} + local metatable = {__index = memoryStream, + __metatable = "memorystream"} + return setmetatable(stream, metatable) +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] sh.internal.execute_complex(statements) for si=1,#statements do local s = statements[si] local chains = sh.internal.groupChains(s) local last_code,br = sh.internal.boolean_executor(chains, function(chain, chain_index) @@ -777,6 +784,6 @@ function sh.execute(env, command, ...) sh.internal.ec.last = sh.internal.command_result_as_code(last_code) end return true, br -end +end --[[@delayloaded-end@]] return sh, local_env diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua index 18ea8b4b2..759440469 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua @@ -1,833 +1,490 @@ -local component = require("component") -local computer = require("computer") -local event = require("event") -local keyboard = require("keyboard") -local text = require("text") local unicode = require("unicode") +local event = require("event") +local process = require("process") +local kb = require("keyboard") +local keys = kb.keys local term = {} -local methods = {} +term.internal = {} -local validateFocus -local metatable = { - __index = methods, - __pairs = function(self) - return function(_, k) - return next(methods, k) - end, self, nil - end -} - ---allows collection of windows with enabled cursor blink ---timer id -> window -term.__blinking = setmetatable({}, {__mode = "v"}) - ---screen address -> window -term.__focus = setmetatable({}, {__mode = "v"}) -term.__nextFocus = setmetatable({}, {__mode = "v"}) ---window -> screen address -term.__usedScreen = setmetatable({}, {__mode = "k"}) --- -term.__knownWindows = setmetatable({}, { - __mode = "v", - __index = function(t, k) - local v = setmetatable({}, {__mode = "v"}) - t[k] = v - return v - end, -}) - -local function register(self, address) - local state = self.__state - if address then - state.neighbors = term.__knownWindows[address] - table.insert(state.neighbors, self) - else - state.neighbors = {self} - end +function term.internal.window() + return process.info().data.window end -local function unregister(self, address) - local state = self.__state - local i, n = 1, #state.neighbors - for i = 1, n do - if state.neighbors[i] == self then - table.remove(state.neighbors, i) - break - end - end - state.neighbors = nil -end +local W = term.internal.window -local function setFocus(self, screenAddress) - if screenAddress and term.__focus[screenAddress] ~= self and term.__nextFocus[screenAddress] ~= self then - term.__focus[screenAddress] = nil - term.__nextFocus[screenAddress] = self - computer.pushSignal("term_focus", screenAddress) - end -end +local local_env = {unicode=unicode,event=event,process=process,W=W,kb=kb} -validateFocus = function(self, address) - local oldAddress = term.__usedScreen[self] - if oldAddress ~= address then - self:unfocus() - unregister(self, oldAddress) - term.__usedScreen[self] = address - register(self, address) - end - if address then - if term.__focus[address] == nil and term.__nextFocus[address] == nil then - setFocus(self, address) - end - end -end - -local function gpu(self) - local state = self.__state - return state.gpu or (component.isAvailable("gpu") and component.gpu) or nil -end - -local function getScreenAddress(self) - if gpu(self) then - local ok, address = pcall(gpu(self).getScreen) - if ok then - validateFocus(self, address) - return address - end - validateFocus(self, nil) - end -end - -local function toggleBlink(self) - local state = self.__state - local cursorBlink = state.cursorBlink - local screenAddress = getScreenAddress(self) - if screenAddress then - local currentFocus = term.__focus[screenAddress] - local isInFocus = (currentFocus == self or currentFocus == nil) - cursorBlink.state = not cursorBlink.state - local x, y, w, h = self:getGlobalArea() - local cursorX, cursorY = state.cursorX, state.cursorY - if cursorX < 1 or cursorY < 1 or cursorX > w or cursorY > h then - return - end - local xAbs, yAbs = x + cursorX - 1, y + cursorY - 1 - if cursorBlink.state then - local char = isInFocus and unicode.char(0x2588) or unicode.char(0x2592) -- solid block or medium shade - cursorBlink.alt = gpu(self).get(xAbs, yAbs) or cursorBlink.alt - gpu(self).set(xAbs, yAbs, string.rep(char, unicode.charWidth(cursorBlink.alt))) - else - gpu(self).set(xAbs, yAbs, cursorBlink.alt) - end - end -end - -term.__keyboardToScreen = {} -term.__screenToKeyboard = {} - -------------------------------------------------------------------------------- - -function term.newWindow(x, y, width, height, gpu) - checkArg(1, x, "number", "nil") - checkArg(2, y, "number", "nil") - checkArg(3, width, "number", "nil") - checkArg(4, height, "number", "nil") - checkArg(5, gpu, "table", "nil") - local window = setmetatable({}, metatable) - window.__state = { - x = x or 1, - y = y or 1, - width = width or -1, - height = height or -1, - gpu = gpu, - cursorX = 1, cursorY = 1, - cursorBlink = nil, - neighbors = {window}, - } +function term.internal.open(dx, dy, w, h) + local window = {x=1,y=1,dx=dx or 0,dy=dy or 0,w=w,h=h,blink=true} return window end -local defaultWindow = term.newWindow() +function term.getViewport(window) + window = window or W() + return window.w, window.h, window.dx, window.dy, window.x, window.y +end -function term.setWindow(newWindow) - checkArg(1, newWindow, "table") - local info = require("process").info() - local oldWindow - if info then - local data = info.data - oldWindow = data.term_window - data.term_window = newWindow - else - oldWindow = defaultWindow - defaultWindow = newWindow +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 not not (w.gpu and w.screen) +end + +function term.internal.pull(input, c, off, p, ...) + 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 not w.blink or (not input and out) or type(p) == "number" then + return event.pull(p,...) end - return oldWindow -end - -function term.getWindow() - local info = require("process").info() - if info then - local data = info.data - if data.term_window then - return data.term_window - end + local gpu=w.gpu + if out then + input:move(0) + y=w.y + input:scroll() end - return defaultWindow -end - - -function methods:setArea(x, y, width, height) - checkArg(1, x, "number") - checkArg(2, y, "number") - checkArg(3, width, "number") - checkArg(4, height, "number") - assert(x ~= 0 and y ~= 0 and width ~= 0 and height ~= 0, "Nonzero arguments only!") - - local state = self.__state - local oldX, oldY, oldWidth, oldHeight = state.x, state.y, state.width, state.height - state.x, state.y, state.width, state.height = x, y, width, height - return oldX, oldY, oldWidth, oldHeight -end - -function methods:getArea() - local state = self.__state - return state.x, state.y, state.width, state.height -end - -function methods:setGPU(newGPU) - local state = self.__state - local oldGPU = state.gpu - state.gpu = newGPU - return oldGPU -end - -function methods:getGPU() - local state = self.__state - return state.gpu, gpu(self) -end - -local function getAbsolute(pos, size) - if pos < 0 then - pos = size + pos + 1 + x,y=w.x+dx,w.y+dy + c=c or {gpu.getBackground(),gpu.getForeground(),gpu.get(x,y)} + if not off then + gpu.setForeground(c[1]) + gpu.setBackground(c[2]) end - return math.min(math.max(pos, 1), size) + gpu.set(x,y,c[3]) + gpu.setForeground(c[2]) + gpu.setBackground(c[1]) + local a={pcall(event.pull,0.5,p,...)} + if #a>1 then + gpu.set(x,y,c[3]) + return select(2,table.unpack(a)) + end + return term.internal.pull(input,c,not off,p,...) end -function methods:getGlobalArea(w, h) - checkArg(1, w, "number", "nil") - checkArg(2, h, "number", "nil") - local state = self.__state - if not w or not h then - if gpu(self) then - w, h = gpu(self).getViewport() - validateFocus(self, gpu(self).getScreen()) - end - end - if not w then - return - end - local x, y = getAbsolute(state.x, w), getAbsolute(state.y, h) - local wMax, hMax = w - x + 1, h - y + 1 - return x, y, getAbsolute(state.width, wMax), getAbsolute(state.height, hMax) +function term.pull(...) + return term.internal.pull(nil,nil,nil,...) end - -function methods:toGlobal(x, y) - checkArg(1, x, "number") - checkArg(2, y, "number") - local dx, dy = self:getGlobalArea() - if dx then - return x + dx - 1, y + dy - 1 - end -end -function methods:toLocal(x, y) - checkArg(1, x, "number") - checkArg(2, y, "number") - local dx, dy = self:getGlobalArea() - if dx then - return x - dx + 1, y - dy + 1 - 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 or dobreak + ops.hintHandler = ops.hintHandler or hintHandler + ops.pwchar = ops.pwchar or pwchar + ops.filter = ops.filter or filter + return term.readKeyboard(ops) end -function methods:clear() - local state = self.__state - local x, y, w, h = self:getGlobalArea() - if x then - gpu(self).fill(x, y, w, h, " ") - end - state.cursorX, state.cursorY = 1, 1 +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 methods:reset() - if self:isAvailable() then - local maxw, maxh = gpu(self).maxResolution() - gpu(self).setResolution(maxw, maxh) - gpu(self).setBackground(0x000000) - gpu(self).setForeground(0xFFFFFF) - self:clear() +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 -end - -function methods:clearLine() - local state = self.__state - local x, y, w, h = self:getGlobalArea() - if x then - gpu(self).fill(x, state.cursorY + y - 1, w, 1, " ") - end - state.cursorX = 1 -end - -function methods:getCursor() - local state = self.__state - return state.cursorX, state.cursorY -end - -function methods:setCursor(col, row) - checkArg(1, col, "number") - checkArg(2, row, "number") - local state = self.__state - local cursorBlink = state.cursorBlink - if cursorBlink and cursorBlink.state then - toggleBlink(self) - end - state.cursorX = math.floor(col) - state.cursorY = math.floor(row) - local wide, right = self:isWide(state.cursorX, state.cursorY) - if wide and right then - state.cursorX = state.cursorX - 1 - end -end - -function methods:getCursorBlink() - local state = self.__state - return state.cursorBlink ~= nil -end - -function methods:setCursorBlink(enabled) - checkArg(1, enabled, "boolean") - local state = self.__state - local cursorBlink = state.cursorBlink - if enabled then - if not cursorBlink then - cursorBlink = {} - cursorBlink.id = event.timer(0.5, function() toggleBlink(term.__blinking[cursorBlink.id]) end, math.huge) - term.__blinking[cursorBlink.id] = self - cursorBlink.state = false - elseif not cursorBlink.state then - toggleBlink(self) - end - elseif cursorBlink then - term.__blinking[cursorBlink.id] = nil - event.cancel(cursorBlink.id) - if cursorBlink.state then - toggleBlink(self) - end - cursorBlink = nil - end - state.cursorBlink = cursorBlink -end - -function methods:isWide(x, y) - local xWin, yWin, wWin, hWin = self:getGlobalArea() - if xWin == nil then - return false - end - if x < 1 or x > wWin or y < 1 or y > hWin then - return false - end - x = x + xWin - 1 - y = y + yWin - 1 - local char = gpu(self).get(x, y) - if unicode.isWide(char) then - -- The char at the specified position is a wide char. - return true - end - if char == " " and x > xWin then - local charLeft = gpu(self).get(x - 1, y) - if charLeft and unicode.isWide(charLeft) then - -- The char left to the specified position is a wide char. - return true, true - end - end - -- Not a wide char. - return false -end - -function methods:isAvailable() - if getScreenAddress(self) then - return true - end - return false -end - -function methods:focus() - setFocus(self, getScreenAddress(self)) -end - -function methods:unfocus() - local screenAddress = term.__usedScreen[self] - if screenAddress then - if term.__focus[screenAddress] == self or term.__nextFocus[screenAddress] == self then - term.__focus[screenAddress] = nil - term.__nextFocus[screenAddress] = nil - self:focusNext() - end - end -end - -function methods:focusNext() - local state = self.__state - local i, n = 1, #state.neighbors - if n > 1 then - for i = 1, n do - if state.neighbors[i] == self then - state.neighbors[(i % n) + 1]:focus() + 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 -end - -function methods:focusPrevious() - local state = self.__state - local i, n = 1, #state.neighbors - if n > 1 then - for i = 1, n do - if state.neighbors[i] == self then - state.neighbors[((i - 2) % n) + 1]:focus() - break + input.clear_tail = function(_) + local win=_.w + local oi,w,h,dx,dy,ox,oy = _.index,term.getViewport(win) + _:move(math.huge) + 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.wtrunc(s1,unicode.wlen(s1))..s2 + else if _.index>=unicode.len(_.data) then return end + s2=unicode.sub(s2,2) + ndata=s1..s2 end - end - end -end - -function methods:hasFocus(screenAddress) - checkArg(1, screenAddress, "string", "nil") - screenAddress = screenAddress or getScreenAddress(self) - local keyboardAddress - if screenAddress then - keyboardAddress = term.__screenToKeyboard[screenAddress] - if not next(term.__focus) then - term.__focus[screenAddress] = self - end - if term.__focus[screenAddress] == self then - return true, screenAddress, keyboardAddress - end - end - return false, screenAddress, keyboardAddress -end - -function methods:read(history, dobreak, hint, pwchar, filter) - checkArg(1, history, "table", "nil") - checkArg(3, hint, "function", "table", "nil") - checkArg(4, pwchar, "string", "nil") - checkArg(5, filter, "string", "function", "nil") - history = history or {} - table.insert(history, "") - - local state = self.__state - local offset = self:getCursor() - 1 - local scrollX = 0 - local textCursorX = 1 - local historyIndex = #history - - if type(hint) == "table" then - local hintTable = hint - hint = function() - return hintTable - end - end - local hintCache, hintIndex - local redraw - - if pwchar and unicode.len(pwchar) > 0 then - pwchar = unicode.sub(pwchar, 1, 1) - end - - if type(filter) == "string" then - local pattern = filter - filter = function(line) - return line:match(pattern) - end - end - - local function masktext(str) - return pwchar and pwchar:rep(unicode.len(str)) or str - end - - local function getTextCursor() - return textCursorX - end - - local function getHistoryIndex() - return historyIndex - end - - local function setHistoryIndex(newIndex) - historyIndex = newIndex - end - - local function line() - return history[getHistoryIndex()] - end - - local function clearHint() - hintCache = nil - end - - local function setTextCursor(newCursor) - local x, y, w, h = self:getGlobalArea() - local termCursorX, termCursorY = self:getCursor() - local str = line() .. " " - - textCursorX = math.max(1, math.min(unicode.len(str), newCursor)) - - while offset + unicode.wlen(unicode.sub(str, 1 + scrollX, textCursorX)) > w do - scrollX = scrollX + 1 - end - if textCursorX <= scrollX then - scrollX = textCursorX - 1 - end - termCursorX = offset + unicode.wlen(unicode.sub(str, 1 + scrollX, textCursorX - 1)) + 1 - - self:setCursor(termCursorX, termCursorY) - redraw() - clearHint() - end - - local function copyIfNecessary() - local index = getHistoryIndex() - if index ~= #history then - history[#history] = line() - setHistoryIndex(#history) - end - end - - redraw = function() - local _, termCursorY = self:getCursor() - local x, y, w, h = self:getGlobalArea() - local l = w - offset - local str = history[getHistoryIndex()] - str = masktext(unicode.sub(str, scrollX + 1, scrollX + l)) - str = (unicode.wlen(str) > l) and unicode.wtrunc(str, l + 1) or str - str = text.padRight(str, l) - gpu(self).set(x + offset, termCursorY + y - 1, str) - end - - local function home() - setTextCursor(1) - end - - local function ende() - setTextCursor(unicode.len(line()) + 1) - end - - local function left() - local cursorX = getTextCursor() - if cursorX > 1 then - setTextCursor(cursorX - 1) - return true -- for backspace - end - end - - local function right(n) - n = n or 1 - local cursorX = getTextCursor() - local maxX = unicode.len(line()) + 1 - if cursorX < maxX then - setTextCursor(math.min(maxX, cursorX + n)) - end - end - - local function up() - local index = getHistoryIndex() - if index > 1 then - setHistoryIndex(index - 1) - redraw() - ende() - end - end - - local function down() - local index = getHistoryIndex() - if index < #history then - setHistoryIndex(index + 1) - redraw() - ende() - end - end - - local function delete() - copyIfNecessary() - clearHint() - local cursorX = getTextCursor() - local index = getHistoryIndex() - if cursorX <= unicode.len(line()) then - history[index] = unicode.sub(line(), 1, cursorX - 1) .. - unicode.sub(line(), cursorX + 1) - redraw() - end - end - - local function insert(value) - copyIfNecessary() - clearHint() - local cursorX = getTextCursor() - local index = getHistoryIndex() - history[index] = unicode.sub(line(), 1, cursorX - 1) .. - value .. - unicode.sub(line(), cursorX) - right(unicode.len(value)) - end - - local function tab(direction) - local cursorX = getTextCursor() - local historyIndex = getHistoryIndex() - if not hintCache then -- hint is never nil, see onKeyDown - local full_line = line() - hintCache = hint(full_line, cursorX) - hintIndex = 0 - if type(hintCache) == "string" then - hintCache = {hintCache} - end - if type(hintCache) ~= "table" or #hintCache < 1 then - hintCache = nil -- invalid hint - else - hintCache.trail = full_line:len() - cursorX -- zero when at end of line - end - end - if hintCache then - hintIndex = (hintIndex + direction + #hintCache - 1) % #hintCache + 1 - history[historyIndex] = tostring(hintCache[hintIndex]) - -- because all other cases of the cursor being moved will result - -- in the hint cache getting invalidated we do that in setCursor, - -- so we have to back it up here to restore it after moving. - local savedCache = hintCache - redraw() - ende() - setTextCursor(getTextCursor()-savedCache.trail-1) - if #savedCache > 1 then -- stop if only one hint exists. - hintCache = savedCache - end - end - end - - local function onKeyDown(keyboardAddress, char, code) - self:setCursorBlink(false) - if code == keyboard.keys.back then - if left() then delete() end - elseif code == keyboard.keys.delete then - delete() - elseif code == keyboard.keys.left then - left() - elseif code == keyboard.keys.right then - right() - elseif code == keyboard.keys.home then - home() - elseif code == keyboard.keys["end"] then - ende() - elseif code == keyboard.keys.up then - up() - elseif code == keyboard.keys.down then - down() - elseif code == keyboard.keys.tab then - if not keyboard.isControlDown(keyboardAddress) and hint then - tab(keyboard.isShiftDown(keyboardAddress) and -1 or 1) - end - elseif code == keyboard.keys.enter then - if not filter or filter(line() or "") then - local index = getHistoryIndex() - if index ~= #history then -- bring entry to front - history[#history] = line() - table.remove(history, index) - end - return true, history[#history] .. "\n" - else - computer.beep(2000, 0.1) - end - elseif keyboard.isControlDown(keyboardAddress) and code == keyboard.keys.d then - if line() == "" then - history[#history] = "" - return true, nil - end - elseif not keyboard.isControl(char) then - insert(unicode.char(char)) - end - self:setCursorBlink(true) - self:setCursorBlink(true) -- force toggle to caret - end - - local function onClipboard(value) - copyIfNecessary() - self:setCursorBlink(false) - local cursorX = getTextCursor() - local index = getHistoryIndex() - local l = value:find("\n", 1, true) - if l then - history[index] = unicode.sub(line(), 1, cursorX - 1) - redraw() - insert(unicode.sub(value, 1, l - 1)) - return true, line() .. "\n" + suffix=s2 + input:clear_tail() + _.data = ndata else - insert(value) - self:setCursorBlink(true) - self:setCursorBlink(true) -- force toggle to caret + _.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 - - local function onTouch(x, y) - local xWin, yWin, wWin, hWin = self:getGlobalArea() - x = x - xWin + 1 - y = y - yWin + 1 - if x > 0 and y > 0 and x <= wWin and y <= hWin then - local _, termCursorY = self:getCursor() - if y == termCursorY and x > offset then - local l = wWin - offset - local str = history[getHistoryIndex()] - str = masktext(unicode.sub(str, scrollX + 1, scrollX + l)) - str = (unicode.wlen(str) >= x - offset) and unicode.wtrunc(str, x - offset) or str - setTextCursor(scrollX + 1 + unicode.len(str)) - end - end + input.clear = function(_) + _:move(-math.huge) + _:draw((" "):rep(unicode.wlen(_.data))) + _:move(-math.huge) + _.index=0 + _.data="" end - - local function cleanup() - if history[#history] == "" then - table.remove(history) - end - self:setCursorBlink(false) - if self:getCursor() > 1 and dobreak ~= false then - self:write("\n") - end + input.draw = function(_,text) + _.sy = _.sy + term.drawText(text,true) end - - self:setCursorBlink(true) - while self:isAvailable() do - local ok, name, address, charOrValueOrX, codeOrY = pcall(event.pull) - if not ok then - cleanup() - error("interrupted", 0) - end - if name == "interrupted" then - cleanup() - return nil - end - local isInFocus, screenAddress, keyboardAddress = self:hasFocus() - --screen may have changed since pull - if isInFocus and type(address) == "string" then - local done, result - if address == screenAddress then - if name == "touch" then - done, result = onTouch(charOrValueOrX, codeOrY) - end - elseif address == keyboardAddress then - if isInFocus then - if name == "key_down" then - done, result = onKeyDown(address, charOrValueOrX, codeOrY) - elseif name == "clipboard" then - done, result = onClipboard(charOrValueOrX) - end - end - end - - if done then - cleanup() - return result - end - end - end - cleanup() - return nil -- fail the read if term becomes unavailable end -function methods:write(value, wrap) +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,cache={}} + term.setCursorBlink(true) + 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} + if ops.nowrap then + term.internal.build_horizontal_reader(input) + else + term.internal.build_vertical_reader(input) + end + while true do + local c = nil + local name, address, char, code = term.internal.pull(input) + hints.cache=char==9 and hints.cache or nil + if name =="interrupted" then draw("^C\n",true) return "" + elseif name=="touch" or name=="drag" then term.internal.onTouch(input,char,code) + elseif name=="clipboard" then c=char + elseif name=="key_down" then + if kb.isControlDown(address) and code == keys.d then return + elseif char==9 then term.internal.tab(input,hints) + elseif char==13 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 char==8 then + input:update(-1) + elseif code==keys.left then + input:move(-1) + elseif code==keys.right then + input:move(1) + 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.home then + input:move(-math.huge) + elseif code==keys["end"] then + input:move(math.huge) + elseif code==keys.delete then + input:update(0) + elseif char>=32 then + c=unicode.char(char) + end + end + if c then input:update(c) 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 - if stream then - local previous_wrap = stream.wrap - stream.wrap = wrap == nil and true or wrap - stdout:write(value) - stdout:flush() - stream.wrap = previous_wrap - end + local previous_wrap = stream.wrap + stream.wrap = wrap == nil and true or wrap + stdout:write(value) + stdout:flush() + stream.wrap = previous_wrap end -function methods:debug(...) - local args = {...} - for _,v in ipairs(args) do - local s = v - if type(s) ~= 'string' then - local r = require('serialization').serialize(s,10000) - if type(s) == 'thread' then - r = string.format('%s{%s}',r,coroutine.status(s)) +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() + 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 + while index <= vlen do + 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 + require("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 + if type(wrap)=="number" then + next,wlen_needed,slen = term.internal.horizontal_push(x,y,window,wrap,next) + else + next = unicode.wtrunc(next, wlen_remaining + 1) + wlen_needed = unicode.wlen(next) + clean_end = (" "):rep(wlen_remaining-wlen_needed) + 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 - s = r - end - self:drawText(s,true) - if _ < #args then - self:drawText('\t',true) end + index = si + 1 end - self:drawText('\n',true) + + window.x,window.y = x,y + return sy end -function methods:drawText(value, wrap) - if not self:isAvailable() then - return - end - value = tostring(value):gsub("\0", "") - value = text.detab(value) - if unicode.wlen(value) == 0 then - return - end - do - local noBell = value:gsub("\a", "") - if #noBell ~= #value then - value = noBell - computer.beep() - end - end - local x, y, w, h = self:getGlobalArea() - if not w then - return -- gpu lost its screen but the signal wasn't processed yet. - end - local blink = self:getCursorBlink() - local state = self.__state - self:setCursorBlink(false) - local line, nl - repeat - local remainingWidth = w - (state.cursorX - 1) - local wrapAfter, margin = math.huge, math.huge - if wrap then - wrapAfter, margin = remainingWidth, w - end - line, value, nl = text.wrap(value, wrapAfter, margin) - line = (unicode.wlen(line) > remainingWidth) and unicode.wtrunc(line, remainingWidth + 1) or line - gpu(self).set(state.cursorX + x - 1, state.cursorY + y - 1, line) - state.cursorX = state.cursorX + unicode.wlen(line) - if nl or (state.cursorX > w and wrap) then - state.cursorX = 1 - state.cursorY = state.cursorY + 1 - end - if state.cursorY > h then - gpu(self).copy(x, y + 1, w, h - 1, 0, -1) - gpu(self).fill(x, h + y - 1, w, 1, " ") - state.cursorY = h - end - until not value - self:setCursorBlink(blink) +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 -------------------------------------------------------------------------------- - ---Automaticly generate term library functions from window methods -for k, v in pairs(methods) do - if type(v) == "function" then - term[k] = function(...) - local self = term.getWindow() - return self[k](self, ...) - end - end +function term.internal.nop(...) + return ... end -return term +function term.setCursorBlink(enabled) + W().blink=enabled +end + +function term.bind(gpu, screen, kb, window) + checkArg(1,gpu,"table") + checkArg(2,screen,"table") + checkArg(3,kb,"table","nil") + checkArg(4,window,"table","nil") + window = window or W() + window.gpu = gpu + window.screen = screen + window.keyboard = kb + window.gw,window.gh = gpu.getViewport() + window.w = math.min(window.gw - window.dx, window.w or window.gw) + window.h = math.min(window.gh - window.dy, window.h or window.gh) +end + +function --[[@delayloaded-start@]] term.internal.horizontal_push(x,y,win,wrap,next) + local gpu,w,h,dx,dy = win.gpu,term.getViewport(win) + local wlen_needed = unicode.wlen(next) + local next_width = math.min(wlen_needed, w - wrap) + next = unicode.sub(next, -next_width) + wlen_needed = unicode.wlen(next) + local xdiff = x - (w - wlen_needed) + gpu.copy(wrap+xdiff+dx,y+dy,x-(wrap+xdiff),1,-xdiff,0) + return next,wlen_needed,#next +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] term.internal.onTouch(input,gx,gy) + input:move(math.huge) + local x2,y2,d = input.w.x,input.w.y-input.sy,input.w.w + input:move((gy*d+gx)-(y2*d+x2)) +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 wlen = math.min(unicode.wlen(s2),w-x+1) + _.w.gpu.fill(x,y,wlen,1," ") + end + input.move = function(_,n) + local win = _.w + local a = _.index + local b = math.max(0,math.min(unicode.len(_.data),_.index+n)) + _.index = b + a,b = a w then + local blank + if i == unicode.len(data) then + available,blank=available-1," " + else + i,blank=i+1,"" + end + data = unicode.sub(data,1,i) + local rev = unicode.reverse(data) + local ending = unicode.wtrunc(rev, available+1) + local cut_wlen = unicode.wlen(data) - unicode.wlen(ending) + data = unicode.reverse(ending) + gpu.set(sx,sy,data..blank:rep(cut_wlen)) + win.x=math.min(w,_.promptx+unicode.wlen(data)) + elseif x < _.promptx then + data = unicode.sub(data,_.index+1) + if unicode.wlen(data) > available then + data = unicode.wtrunc(data,available+1) + end + gpu.set(sx,sy,data) + end + end + input.clear = function(_) + local win = _.w + local gpu,data,px=win.gpu,_.data,_.promptx + local w,h,dx,dy,x,y = term.getViewport(win) + _.index,_.data,win.x=0,"",px + gpu.fill(px+dx,y+dy,w-px+1,1," ") + end +end --[[@delayloaded-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:match(filter) + elseif filter(input) 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 + if not hints.cache then + hints.cache = type(hints.handler)=="table" and hints.handler + or hints.handler(input.data,input.index + 1) or {} + hints.cache.i=-1 + end + local c=hints.cache + c.i=(c.i+1)%#c + local next=c[c.i+1] + if next then + local tail = unicode.wlen(input.data) - input.index - 1 + 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.screen(window) + return (window or W()).screen +end --[[@delayloaded-end@]] + +function --[[@delayloaded-start@]] term.keyboard(window) + window = window or W() + local kba = window.keyboard and window.keyboard.address + if kba and kb.pressedCodes[kba] then return window.keyboard end + window.keyboard=nil + local component = require("component") + if not window.screen or not component.proxy(window.screen.address) then window.screen = nil return end + local kba = window.screen.getKeyboards()[1] + if not kba then return end + window.keyboard = component.proxy(kba) + return window.keyboard +end --[[@delayloaded-end@]] + +return term, local_env diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua index 3e5839bcf..2c7e97d4c 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/text.lua @@ -17,7 +17,7 @@ setmetatable(text.internal, text.syntax = {";","&&","||","|",">>",">","<"} -function text.detab(value, tabWidth) +function --[[@delayloaded-start@]] text.detab(value, tabWidth) checkArg(1, value, "string") checkArg(2, tabWidth, "number", "nil") tabWidth = tabWidth or 8 @@ -27,8 +27,9 @@ function text.detab(value, tabWidth) end local result = value:gsub("([^\n]-)\t", rep) -- truncate results return result -end +end --[[@delayloaded-end@]] +-- used in motd function text.padRight(value, length) checkArg(1, value, "string", "nil") checkArg(2, length, "number") @@ -39,7 +40,7 @@ function text.padRight(value, length) end end -function text.padLeft(value, length) +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 @@ -47,7 +48,7 @@ function text.padLeft(value, length) else return string.rep(" ", length - unicode.wlen(value)) .. value end -end +end --[[@delayloaded-end@]] function text.trim(value) -- from http://lua-users.org/wiki/StringTrim local from = string.match(value, "^%s*()") @@ -176,13 +177,6 @@ function text.internal.tokenize(value, quotes, delimiters) return text.internal.splitWords(words, delimiters) end -function --[[@delayloaded-start@]] text.internal.table_view(str) - checkArg(1, str, 'string') - return setmetatable({s=str}, - { __index = function(_,k) return unicode.sub(_.s,k,k) end, - __len = function(_) return unicode.len(_.s) end}) -end --[[@delayloaded-end@]] - -- tokenize input by quotes and whitespace function text.internal.words(input, quotes) checkArg(1, input, "string") diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/transforms.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/transforms.lua index 518a00816..18dfcf977 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/transforms.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/transforms.lua @@ -133,7 +133,9 @@ 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') + 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)