saving more memory and improving the vt100 library

move all vt100 code to vt100 library
delay load event rare code
fix shell parse for %d>&%d not followed by whitespace
remove weird tty blink code and use vt100 codes
bump openos patch version
This commit is contained in:
payonel 2017-11-23 13:02:55 -08:00
parent 5b0c085c2d
commit 7ec85999da
10 changed files with 325 additions and 329 deletions

View File

@ -1,35 +1,38 @@
local component = require("component")
local computer = require("computer")
local text = require("text")
local unicode = require("unicode")
local tty = require("tty")
if not component.isAvailable("gpu") then
return
end
local lines = {_OSVERSION .. " (" .. math.floor(computer.totalMemory() / 1024) .. "k RAM)"}
local maxWidth = unicode.len(lines[1])
local f = io.open("/usr/misc/greetings.txt")
local lines = {_OSVERSION .. " (" .. math.floor(computer.totalMemory() / 1024) .. "k RAM)"}
local greeting = ""
if f then
local greetings = {}
pcall(function()
for line in f:lines() do table.insert(greetings, line) end
end)
f:close()
local greeting = greetings[math.random(1, #greetings)]
if greeting then
local width = math.max(10, tty.getViewport())
for line in text.wrappedLines(greeting, width - 4, width - 4) do
table.insert(lines, line)
maxWidth = math.max(maxWidth, unicode.len(line))
end
end
greeting = greetings[math.random(1, math.max(#greetings, 1))] or ""
end
local width = math.min(#greeting, tty.getViewport() - 5)
local maxLine = #lines[1]
while #greeting > 0 do
local si, ei = greeting:sub(1, width):find("%s%S*$")
local line = #greeting <= width and greeting or greeting:sub(1, si or width)
lines[#lines + 1] = line
maxLine = math.max(maxLine, #line)
greeting = greeting:sub(#line + 1)
end
local borders = {{unicode.char(0x2552), unicode.char(0x2550), unicode.char(0x2555)},
{unicode.char(0x2502), nil, unicode.char(0x2502)},
{unicode.char(0x2514), unicode.char(0x2500), unicode.char(0x2518)}}
io.write(borders[1][1] .. string.rep(borders[1][2], maxWidth + 2) .. borders[1][3] .. "\n")
for _, line in ipairs(lines) do
io.write(borders[2][1] .. " " .. text.padRight(line, maxWidth) .. " " .. borders[2][3] .. "\n")
io.write(borders[1][1], string.rep(borders[1][2], maxLine + 2), borders[1][3], "\n")
for _,line in ipairs(lines) do
io.write(borders[2][1], " ", line, (" "):rep(maxLine - #line + 1), borders[2][3], " \n")
end
io.write(borders[3][1] .. string.rep(borders[3][2], maxWidth + 2) .. borders[3][3] .. "\n")
io.write(borders[3][1] .. string.rep(borders[3][2], maxLine + 2) .. borders[3][3] .. "\n")

View File

@ -39,14 +39,8 @@ function buffer:flush()
local tmp = self.bufferWrite
self.bufferWrite = ""
local result, reason = self.stream:write(tmp)
if result then
self.bufferWrite = ""
else
if reason then
return nil, reason
else
return nil, "bad file descriptor"
end
if not result then
return nil, reason or "bad file descriptor"
end
end

View File

@ -1,7 +1,7 @@
-- called from /init.lua
local raw_loadfile = ...
_G._OSVERSION = "OpenOS 1.7.0"
_G._OSVERSION = "OpenOS 1.7.1"
local component = component
local computer = computer

View File

@ -0,0 +1,61 @@
local event = require("event")
local function createMultipleFilter(...)
local filter = table.pack(...)
if filter.n == 0 then
return nil
end
return function(...)
local signal = table.pack(...)
if type(signal[1]) ~= "string" then
return false
end
for i = 1, filter.n do
if filter[i] ~= nil and signal[1]:match(filter[i]) then
return true
end
end
return false
end
end
function event.pullMultiple(...)
local seconds
local args
if type(...) == "number" then
seconds = ...
args = table.pack(select(2,...))
for i=1,args.n do
checkArg(i+1, args[i], "string", "nil")
end
else
args = table.pack(...)
for i=1,args.n do
checkArg(i, args[i], "string", "nil")
end
end
return event.pullFiltered(seconds, createMultipleFilter(table.unpack(args, 1, args.n)))
end
function event.cancel(timerId)
checkArg(1, timerId, "number")
if handlers[timerId] then
handlers[timerId] = nil
return true
end
return false
end
function event.ignore(name, callback)
checkArg(1, name, "string")
checkArg(2, callback, "function")
for id, handler in pairs(handlers) do
if handler.key == name and handler.callback == callback then
handlers[id] = nil
return true
end
end
return false
end

View File

@ -244,3 +244,43 @@ function text.padLeft(value, length)
return string.rep(" ", length - unicode.wlen(value)) .. value
end
end
function text.padRight(value, length)
checkArg(1, value, "string", "nil")
checkArg(2, length, "number")
if not value or unicode.wlen(value) == 0 then
return string.rep(" ", length)
else
return value .. string.rep(" ", length - unicode.wlen(value))
end
end
function text.wrap(value, width, maxWidth)
checkArg(1, value, "string")
checkArg(2, width, "number")
checkArg(3, maxWidth, "number")
local line, nl = value:match("([^\r\n]*)(\r?\n?)") -- read until newline
if unicode.wlen(line) > width then -- do we even need to wrap?
local partial = unicode.wtrunc(line, width)
local wrapped = partial:match("(.*[^a-zA-Z0-9._()'`=])")
if wrapped or unicode.wlen(line) > maxWidth then
partial = wrapped or partial
return partial, unicode.sub(value, unicode.len(partial) + 1), true
else
return "", value, true -- write in new line.
end
end
local start = unicode.len(line) + unicode.len(nl) + 1
return line, start <= unicode.len(value) and unicode.sub(value, start) or nil, unicode.len(nl) > 0
end
function text.wrappedLines(value, width, maxWidth)
local line
return function()
if value then
line, value = text.wrap(value, width, maxWidth)
return line
end
end
end

View File

@ -0,0 +1,93 @@
local vt100 = require("vt100")
local rules = vt100.rules
-- [?7[hl] wrap mode
rules[{"%[", "%?", "7", "[hl]"}] = function(window, _, _, _, nowrap)
window.nowrap = nowrap == "l"
end
-- helper scroll function
local function set_cursor(window, x, y)
window.x = math.min(math.max(x, 1), window.width)
window.y = math.min(math.max(y, 1), window.height)
end
-- -- These DO NOT SCROLL
-- [(%d+)A move cursor up n lines
-- [(%d+)B move cursor down n lines
-- [(%d+)C move cursor right n lines
-- [(%d+)D move cursor left n lines
rules[{"%[", "%d+", "[ABCD]"}] = function(window, _, n, dir)
local dx, dy = 0, 0
n = tonumber(n)
if dir == "A" then
dy = -n
elseif dir == "B" then
dy = n
elseif dir == "C" then
dx = n
else -- D
dx = -n
end
set_cursor(window, window.x + dx, window.y + dy)
end
-- [Line;ColumnH Move cursor to screen location v,h
-- [Line;Columnf ^ same
rules[{"%[", "%d+", ";", "%d+", "[Hf]"}] = function(window, _, y, _, x)
set_cursor(window, tonumber(x), tonumber(y))
end
-- [K clear line from cursor right
-- [0K ^ same
-- [1K clear line from cursor left
-- [2K clear entire line
local function clear_line(window, _, n)
n = tonumber(n) or 0
local x = n == 0 and window.x or 1
local rep = n == 1 and window.x or window.width
window.gpu.set(x, window.y, (" "):rep(rep))
end
rules[{"%[", "[012]?", "K"}] = clear_line
-- [J clear screen from cursor down
-- [0J ^ same
-- [1J clear screen from cursor up
-- [2J clear entire screen
rules[{"%[", "[012]?", "J"}] = function(window, _, n)
clear_line(window, _, n)
n = tonumber(n) or 0
local y = n == 0 and (window.y + 1) or 1
local rep = n == 1 and (window.y - 1) or window.height
window.gpu.fill(1, y, window.width, rep, " ")
end
-- [H move cursor to upper left corner
-- [;H ^ same
-- [f ^ same
-- [;f ^ same
rules[{"%[;?", "[Hf]"}] = function(window)
set_cursor(window, 1, 1)
end
-- [6n get the cursor position [ EscLine;ColumnR Response: cursor is at v,h ]
rules[{"%[", "6", "n"}] = function(window)
-- this solution puts the response on stdin, but it isn't echo'd
-- I'm personally fine with the lack of echo
io.stdin.bufferRead = string.format("%s%s%d;%dR", io.stdin.bufferRead, string.char(0x1b), window.y, window.x)
end
-- D scroll up one line -- moves cursor down
-- E move to next line (acts the same ^, but x=1)
-- M scroll down one line -- moves cursor up
rules[{"[DEM]"}] = function(window, dir)
if dir == "D" then
window.y = window.y + 1
elseif dir == "E" then
window.y = window.y + 1
window.x = 1
else -- M
window.y = window.y - 1
end
end

View File

@ -89,48 +89,8 @@ local function createPlainFilter(name, ...)
end
end
local function createMultipleFilter(...)
local filter = table.pack(...)
if filter.n == 0 then
return nil
end
return function(...)
local signal = table.pack(...)
if type(signal[1]) ~= "string" then
return false
end
for i = 1, filter.n do
if filter[i] ~= nil and signal[1]:match(filter[i]) then
return true
end
end
return false
end
end
-------------------------------------------------------------------------------
function event.cancel(timerId)
checkArg(1, timerId, "number")
if handlers[timerId] then
handlers[timerId] = nil
return true
end
return false
end
function event.ignore(name, callback)
checkArg(1, name, "string")
checkArg(2, callback, "function")
for id, handler in pairs(handlers) do
if handler.key == name and handler.callback == callback then
handlers[id] = nil
return true
end
end
return false
end
function event.listen(name, callback)
checkArg(1, name, "string")
checkArg(2, callback, "function")
@ -161,24 +121,6 @@ function event.pull(...)
end
end
function event.pullMultiple(...)
local seconds
local args
if type(...) == "number" then
seconds = ...
args = table.pack(select(2,...))
for i=1,args.n do
checkArg(i+1, args[i], "string", "nil")
end
else
args = table.pack(...)
for i=1,args.n do
checkArg(i, args[i], "string", "nil")
end
end
return event.pullFiltered(seconds, createMultipleFilter(table.unpack(args, 1, args.n)))
end
function event.pullFiltered(...)
local args = table.pack(...)
local seconds, filter
@ -217,6 +159,8 @@ end
-- users may expect to find event.push to exist
event.push = computer.pushSignal
require("package").delay(event, "/lib/core/full_event.lua")
-------------------------------------------------------------------------------
return event

View File

@ -4,53 +4,13 @@ local tx = require("transforms")
local text = {}
text.internal = {}
text.syntax = {"^%d?>>?&%d+$","^%d?>>?",">>?","<%&%d+","<",";","&&","||?"}
text.syntax = {"^%d?>>?&%d+","^%d?>>?",">>?","<%&%d+","<",";","&&","||?"}
function text.trim(value) -- from http://lua-users.org/wiki/StringTrim
local from = string.match(value, "^%s*()")
return from > #value and "" or string.match(value, ".*%S", from)
end
-- used in motd
function text.padRight(value, length)
checkArg(1, value, "string", "nil")
checkArg(2, length, "number")
if not value or unicode.wlen(value) == 0 then
return string.rep(" ", length)
else
return value .. string.rep(" ", length - unicode.wlen(value))
end
end
function text.wrap(value, width, maxWidth)
checkArg(1, value, "string")
checkArg(2, width, "number")
checkArg(3, maxWidth, "number")
local line, nl = value:match("([^\r\n]*)(\r?\n?)") -- read until newline
if unicode.wlen(line) > width then -- do we even need to wrap?
local partial = unicode.wtrunc(line, width)
local wrapped = partial:match("(.*[^a-zA-Z0-9._()'`=])")
if wrapped or unicode.wlen(line) > maxWidth then
partial = wrapped or partial
return partial, unicode.sub(value, unicode.len(partial) + 1), true
else
return "", value, true -- write in new line.
end
end
local start = unicode.len(line) + unicode.len(nl) + 1
return line, start <= unicode.len(value) and unicode.sub(value, start) or nil, unicode.len(nl) > 0
end
function text.wrappedLines(value, width, maxWidth)
local line
return function()
if value then
line, value = text.wrap(value, width, maxWidth)
return line
end
end
end
-- used by lib/sh
function text.escapeMagic(txt)
return txt:gsub('[%(%)%.%%%+%-%*%?%[%^%$]', '%%%1')

View File

@ -101,49 +101,23 @@ function tty.isAvailable()
return not not (gpu and gpu.getScreen())
end
function tty.stream:blink(done)
local width, height, dx, dy, x, y = tty.getViewport()
local gpu = tty.gpu()
if not gpu or x < 1 or x > width or y < 1 or y > height then
return true
end
x, y = x + dx, y + dy
local blinked, bgColor, bgIsPalette, fgColor, fgIsPalette, char_at_cursor = table.unpack(self.blink_cache or {})
if done == nil then -- reset
blinked = false
bgColor, bgIsPalette = gpu.getBackground()
-- it can happen during a type of race condition when a screen is removed
if not bgColor then
return
end
fgColor, fgIsPalette = gpu.getForeground()
char_at_cursor = gpu.get(x, y)
end
if not blinked and not done then
gpu.setForeground(bgColor, bgIsPalette)
gpu.setBackground(fgColor, fgIsPalette)
gpu.set(x, y, char_at_cursor)
gpu.setForeground(fgColor, fgIsPalette)
gpu.setBackground(bgColor, bgIsPalette)
blinked = true
elseif blinked and (done or tty.window.blink) then
gpu.set(x, y, char_at_cursor)
blinked = false
end
self.blink_cache = table.pack(blinked, bgColor, bgIsPalette, fgColor, fgIsPalette, char_at_cursor)
return true
end
function tty.stream:pull(timeout, ...)
timeout = timeout or math.huge
local blink_timeout = tty.window.blink and .5 or math.huge
-- it can happen during a type of race condition when a screen is removed
if not self:blink() then
return nil, "interrupted"
local width, height, dx, dy, x, y = tty.getViewport()
local gpu = tty.gpu()
if x < 1 or x > width or y < 1 or y > height then
gpu = nil
end
local char_at_cursor
local blinked
if gpu then
blinked, char_at_cursor = pcall(gpu.get, x + dx, y + dy)
if not blinked then
return nil, "interrupted"
end
io.write("\0277\27[7m", char_at_cursor, "\0278")
end
-- get the next event
@ -152,7 +126,15 @@ function tty.stream:pull(timeout, ...)
timeout = timeout - blink_timeout
local done = signal.n > 1 or timeout < blink_timeout
self:blink(done)
if gpu then
if not blinked and not done then
io.write("\0277\27[7m", char_at_cursor, "\0278")
blinked = true
elseif blinked and (done or tty.window.blink) then
io.write("\0277", char_at_cursor, "\0278")
blinked = false
end
end
if done then
return table.unpack(signal, 1, signal.n)
@ -336,27 +318,7 @@ function tty.stream:write(value)
-- parse the instruction in segment
-- [ (%d+;)+ %d+m
window.ansi_escape = window.ansi_escape .. value
local color_attributes = {tonumber(window.ansi_escape:match("^%[(%d%d)m"))}
if not color_attributes[1] then
color_attributes, ansi_print, value = require("vt100").parse(window)
else
value = window.ansi_escape:sub(5)
end
for _,catt in ipairs(color_attributes) do
-- B6 is closer to cyan in 4 bit color
local colors = {0x0,0xff0000,0x00ff00,0xffff00,0x0000ff,0xff00ff,0x00B6ff,0xffffff}
catt = catt - 29
local method = "setForeground"
if catt > 10 then
method = "setBackground"
catt = catt - 10
end
local c = colors[catt]
if c then
gpu[method](c)
end
window.ansi_escape = nil -- might happen multiple times, that's fine
end
value, ansi_print = require("vt100").parse(window)
end
-- scroll before parsing next line

View File

@ -1,190 +1,129 @@
local text = require("text")
local vt100 = {}
-- runs patterns on ansi until failure
-- returns valid:boolean, completed_index:nil|number
function vt100.validate(ansi, patterns)
local last_index = 0
local captures = {}
for _,pattern in ipairs(patterns) do
if last_index >= #ansi then
return true
end
local si, ei, capture = ansi:find("^(" .. pattern .. ")", last_index + 1)
if not si then -- failed to match
return
end
captures[#captures + 1] = capture
last_index = ei
end
return true, last_index, captures
end
local rules = {}
local vt100 = {rules=rules}
local full
-- colors
-- colors, blinking, and reverse
-- [%d+;%d+;..%d+m
rules[{"%[", "[%d;]*", "m"}] = function(_, _, number_text)
local numbers = {}
-- cost: 2,250
rules[{"%[", "[%d;]*", "m"}] = function(window, _, number_text)
-- prefix and suffix ; act as reset
-- e.g. \27[41;m is actually 41 followed by a reset
number_text = ";" .. number_text:gsub("^;$","") .. ";"
local parts = text.split(number_text, {";"})
local colors = {0x0,0xff0000,0x00ff00,0xffff00,0x0000ff,0xff00ff,0x00B6ff,0xffffff}
local fg, bg = window.gpu.setForeground, window.gpu.setBackground
if window.flip then
fg, bg = bg, fg
end
number_text = " _ " .. number_text:gsub("^;$", ""):gsub(";", " _ ") .. " _ "
local parts = text.internal.tokenize(number_text)
local last_was_break
for _,part in ipairs(parts) do
local num = tonumber(part)
if not num then
num = last_was_break and 0
last_was_break = true
else
last_was_break = false
end
if num == 0 then
numbers[#numbers + 1] = 40
numbers[#numbers + 1] = 37
local num = tonumber(part[1].txt)
last_was_break, num = not num, num or last_was_break and 0
if num == 7 then
if not window.flip then
fg(bg(window.gpu.getForeground()))
fg, bg = bg, fg
end
window.flip = true
elseif num == 5 then
window.blink = true
elseif num == 0 then
bg(colors[1])
fg(colors[8])
elseif num then
numbers[#numbers + 1] = num
num = num - 29
local set = fg
if num > 10 then
num = num - 10
set = bg
end
local color = colors[num]
if color then
set(color)
end
end
end
return numbers
end
-- [?7[hl] wrap mode
rules[{"%[", "%?", "7", "[hl]"}] = function(window, _, _, _, nowrap)
window.nowrap = nowrap == "l"
end
-- helper scroll function
local function set_cursor(window, x, y)
window.x = math.min(math.max(x, 1), window.width)
window.y = math.min(math.max(y, 1), window.height)
end
-- -- These DO NOT SCROLL
-- [(%d+)A move cursor up n lines
-- [(%d+)B move cursor down n lines
-- [(%d+)C move cursor right n lines
-- [(%d+)D move cursor left n lines
rules[{"%[", "%d+", "[ABCD]"}] = function(window, _, n, dir)
local dx, dy = 0, 0
n = tonumber(n)
if dir == "A" then
dy = -n
elseif dir == "B" then
dy = n
elseif dir == "C" then
dx = n
else -- D
dx = -n
end
set_cursor(window, window.x + dx, window.y + dy)
end
-- [Line;ColumnH Move cursor to screen location v,h
-- [Line;Columnf ^ same
rules[{"%[", "%d+", ";", "%d+", "[Hf]"}] = function(window, _, y, _, x)
set_cursor(window, tonumber(x), tonumber(y))
end
-- [K clear line from cursor right
-- [0K ^ same
-- [1K clear line from cursor left
-- [2K clear entire line
local function clear_line(window, _, n)
n = tonumber(n) or 0
local x = n == 0 and window.x or 1
local rep = n == 1 and window.x or window.width
window.gpu.set(x, window.y, (" "):rep(rep))
end
rules[{"%[", "[012]?", "K"}] = clear_line
-- [J clear screen from cursor down
-- [0J ^ same
-- [1J clear screen from cursor up
-- [2J clear entire screen
rules[{"%[", "[012]?", "J"}] = function(window, _, n)
clear_line(window, _, n)
n = tonumber(n) or 0
local y = n == 0 and (window.y + 1) or 1
local rep = n == 1 and (window.y - 1) or window.height
window.gpu.fill(1, y, window.width, rep, " ")
end
-- [H move cursor to upper left corner
-- [;H ^ same
-- [f ^ same
-- [;f ^ same
rules[{"%[;?", "[Hf]"}] = function(window)
set_cursor(window, 1, 1)
end
-- [6n get the cursor position [ EscLine;ColumnR Response: cursor is at v,h ]
rules[{"%[", "6", "n"}] = function(window)
-- this solution puts the response on stdin, but it isn't echo'd
-- I'm personally fine with the lack of echo
io.stdin.bufferRead = string.format("%s%s%d;%dR", io.stdin.bufferRead, string.char(0x1b), window.y, window.x)
end
-- D scroll up one line -- moves cursor down
-- E move to next line (acts the same ^, but x=1)
-- M scroll down one line -- moves cursor up
rules[{"[DEM]"}] = function(window, dir)
if dir == "D" then
window.y = window.y + 1
elseif dir == "E" then
window.y = window.y + 1
window.x = 1
else -- M
window.y = window.y - 1
end
end
local function save_attributes(window, save)
if save then
local data = window.saved or {1, 1, {0x0}, {0xffffff}}
local function save_attributes(window, seven, s)
if seven == "7" or s == "s" then
window.saved =
{
window.x,
window.y,
{window.gpu.getBackground()},
{window.gpu.getForeground()},
window.flip,
window.blink
}
else
local data = window.saved or {1, 1, {0x0}, {0xffffff}, window.flip, window.blink}
window.x = data[1]
window.y = data[2]
window.gpu.setBackground(table.unpack(data[3]))
window.gpu.setForeground(table.unpack(data[4]))
else
window.saved = {window.x, window.y, {window.gpu.getBackground()}, {window.gpu.getForeground()}}
window.flip = data[5]
window.blink = data[6]
end
end
-- 7 save cursor position and attributes
-- 8 restore cursor position and attributes
rules[{"[78]"}] = function(window, restore)
save_attributes(window, restore == "8")
end
rules[{"[78]"}] = save_attributes
-- s save cursor position
-- u restore cursor position
rules[{"%[", "[su]"}] = function(window, _, restore)
save_attributes(window, restore == "u")
end
rules[{"%[", "[su]"}] = save_attributes
-- returns 2 values
-- value: parsed text
-- ansi_print: failed to parse
function vt100.parse(window)
local ansi = window.ansi_escape
window.ansi_escape = nil
local any_valid
for rule,action in pairs(rules) do
local ok, completed, captures = vt100.validate(ansi, rule)
if completed then
return action(window, table.unpack(captures)) or {}, "", ansi:sub(completed + 1)
elseif ok then
any_valid = true
local last_index = 0
local captures = {}
for _,pattern in ipairs(rule) do
if last_index >= #ansi then
any_valid = true
break
end
local si, ei, capture = ansi:find("^(" .. pattern .. ")", last_index + 1)
if not si then
break
end
captures[#captures + 1] = capture
last_index = ei
end
if #captures == #rule then
action(window, table.unpack(captures))
return ansi:sub(last_index + 1), ""
end
end
if not full then
-- maybe it did satisfy a rule, load more rules
full = true
dofile("/lib/core/full_vt.lua")
window.ansi_escape = ansi
return vt100.parse(window)
end
if not any_valid then
-- malformed
return {}, string.char(0x1b), ansi
return ansi, "\27"
end
-- else, still consuming
window.ansi_escape = ansi
return {}, "", ""
return "", ""
end
return vt100