forked from izaya/OC-PsychOS2
516 lines
13 KiB
Lua
516 lines
13 KiB
Lua
-- shamelessly stolen from plan9k
|
|
|
|
buffer = {}
|
|
|
|
function buffer.new(mode, stream) -- string table -- table -- create a new buffer in mode *mode* backed by stream object *stream*
|
|
local result = {
|
|
mode = {},
|
|
stream = stream,
|
|
bufferRead = "",
|
|
bufferWrite = "",
|
|
bufferSize = math.max(512, math.min(8 * 1024, computer.freeMemory() / 8)),
|
|
bufferMode = "full",
|
|
readTimeout = math.huge
|
|
}
|
|
mode = mode or "r"
|
|
for i = 1, unicode.len(mode) do
|
|
result.mode[unicode.sub(mode, i, i)] = true
|
|
end
|
|
local metatable = {
|
|
__index = buffer,
|
|
__metatable = "file"
|
|
}
|
|
return setmetatable(result, metatable)
|
|
end
|
|
|
|
local function badFileDescriptor()
|
|
return nil, "bad file descriptor"
|
|
end
|
|
|
|
function buffer:close()
|
|
if self.mode.w or self.mode.a then
|
|
self:flush()
|
|
end
|
|
self.closed = true
|
|
return self.stream:close()
|
|
end
|
|
|
|
function buffer:flush()
|
|
local result, reason = self.stream:write(self.bufferWrite)
|
|
if result then
|
|
self.bufferWrite = ""
|
|
else
|
|
if reason then
|
|
return nil, reason
|
|
else
|
|
return badFileDescriptor()
|
|
end
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
function buffer:lines(...)
|
|
local args = table.pack(...)
|
|
return function()
|
|
local result = table.pack(self:read(table.unpack(args, 1, args.n)))
|
|
if not result[1] and result[2] then
|
|
error(result[2])
|
|
end
|
|
return table.unpack(result, 1, result.n)
|
|
end
|
|
end
|
|
|
|
function buffer:read(...)
|
|
local timeout = computer.uptime() + self.readTimeout
|
|
|
|
local function readChunk()
|
|
if computer.uptime() > timeout then
|
|
error("timeout")
|
|
end
|
|
local result, reason = self.stream:read(self.bufferSize)
|
|
if result then
|
|
self.bufferRead = self.bufferRead .. result
|
|
return self
|
|
else -- error or eof
|
|
return nil, reason
|
|
end
|
|
end
|
|
|
|
local function readBytesOrChars(n)
|
|
n = math.max(n, 0)
|
|
local len, sub
|
|
if self.mode.b then
|
|
len = rawlen
|
|
sub = string.sub
|
|
else
|
|
len = unicode.len
|
|
sub = unicode.sub
|
|
end
|
|
local buffer = ""
|
|
repeat
|
|
if len(self.bufferRead) == 0 then
|
|
local result, reason = readChunk()
|
|
if not result then
|
|
if reason then
|
|
return nil, reason
|
|
else -- eof
|
|
return #buffer > 0 and buffer or nil
|
|
end
|
|
end
|
|
end
|
|
local left = n - len(buffer)
|
|
buffer = buffer .. sub(self.bufferRead, 1, left)
|
|
self.bufferRead = sub(self.bufferRead, left + 1)
|
|
until len(buffer) == n
|
|
|
|
return buffer
|
|
end
|
|
|
|
local function readNumber()
|
|
local len, sub
|
|
if self.mode.b then
|
|
len = rawlen
|
|
sub = string.sub
|
|
else
|
|
len = unicode.len
|
|
sub = unicode.sub
|
|
end
|
|
local buffer = ""
|
|
local first = true
|
|
local decimal = false
|
|
local last = false
|
|
local hex = false
|
|
local pat = "^[0-9]+"
|
|
local minbuf = 3 -- "+0x" (sign + hexadecimal tag)
|
|
-- this function is used to read trailing numbers (1e2, 0x1p2, etc)
|
|
local function readnum(checksign)
|
|
local _buffer = ""
|
|
local sign = ""
|
|
while true do
|
|
if len(self.bufferRead) == 0 then
|
|
local result, reason = readChunk()
|
|
if not result then
|
|
if reason then
|
|
return nil, reason
|
|
else -- eof
|
|
return #_buffer > 0 and (sign .. _buffer) or nil
|
|
end
|
|
end
|
|
end
|
|
if checksign then
|
|
local _sign = sub(self.bufferRead, 1, 1)
|
|
if _sign == "+" or _sign == "-" then
|
|
-- "eat" the sign (Rio Lua behaviour)
|
|
sign = sub(self.bufferRead, 1, 1)
|
|
self.bufferRead = sub(self.bufferRead, 2)
|
|
end
|
|
checksign = false
|
|
else
|
|
local x,y = string.find(self.bufferRead, pat)
|
|
if not x then
|
|
break
|
|
else
|
|
_buffer = _buffer .. sub(self.bufferRead, 1, y)
|
|
self.bufferRead = sub(self.bufferRead, y + 1)
|
|
end
|
|
end
|
|
end
|
|
return #_buffer > 0 and (sign .. _buffer) or nil
|
|
end
|
|
while true do
|
|
if len(self.bufferRead) == 0 or len(self.bufferRead) < minbuf then
|
|
local result, reason = readChunk()
|
|
if not result then
|
|
if reason then
|
|
return nil, reason
|
|
else -- eof
|
|
return #buffer > 0 and tonumber(buffer) or nil
|
|
end
|
|
end
|
|
end
|
|
-- these ifs are here so we run the buffer check above
|
|
if first then
|
|
local sign = sub(self.bufferRead, 1, 1)
|
|
if sign == "+" or sign == "-" then
|
|
-- "eat" the sign (Rio Lua behaviour)
|
|
buffer = buffer .. sub(self.bufferRead, 1, 1)
|
|
self.bufferRead = sub(self.bufferRead, 2)
|
|
end
|
|
local hextag = sub(self.bufferRead, 1, 2)
|
|
if hextag == "0x" or hextag == "0X" then
|
|
pat = "^[0-9A-Fa-f]+"
|
|
-- "eat" the 0x, see https://gist.github.com/SoniEx2/570a363d81b743353151
|
|
buffer = buffer .. sub(self.bufferRead, 1, 2)
|
|
self.bufferRead = sub(self.bufferRead, 3)
|
|
hex = true
|
|
end
|
|
minbuf = 0
|
|
first = false
|
|
elseif decimal then
|
|
local sep = sub(self.bufferRead, 1, 1)
|
|
if sep == "." then
|
|
buffer = buffer .. sep
|
|
self.bufferRead = sub(self.bufferRead, 2)
|
|
local temp = readnum(false) -- no sign
|
|
if temp then
|
|
buffer = buffer .. temp
|
|
end
|
|
end
|
|
if not tonumber(buffer) then break end
|
|
decimal = false
|
|
last = true
|
|
minbuf = 1
|
|
elseif last then
|
|
local tag = sub(self.bufferRead, 1, 1)
|
|
if hex and (tag == "p" or tag == "P") then
|
|
local temp = sub(self.bufferRead, 1, 1)
|
|
self.bufferRead = sub(self.bufferRead, 2)
|
|
local temp2 = readnum(true) -- this eats the next sign if any
|
|
if temp2 then
|
|
buffer = buffer .. temp .. temp2
|
|
end
|
|
elseif tag == "e" or tag == "E" then
|
|
local temp = sub(self.bufferRead, 1, 1)
|
|
self.bufferRead = sub(self.bufferRead, 2)
|
|
local temp2 = readnum(true) -- this eats the next sign if any
|
|
if temp2 then
|
|
buffer = buffer .. temp .. temp2
|
|
end
|
|
end
|
|
break
|
|
else
|
|
local x,y = string.find(self.bufferRead, pat)
|
|
if not x then
|
|
minbuf = 1
|
|
decimal = true
|
|
else
|
|
buffer = buffer .. sub(self.bufferRead, 1, y)
|
|
self.bufferRead = sub(self.bufferRead, y + 1)
|
|
end
|
|
end
|
|
end
|
|
return tonumber(buffer)
|
|
end
|
|
|
|
local function readLine(chop)
|
|
if not self.mode.t then
|
|
local start = 1
|
|
while true do
|
|
local l = self.bufferRead:find("\n", start, true)
|
|
if l then
|
|
local result = self.bufferRead:sub(1, l + (chop and -1 or 0))
|
|
self.bufferRead = self.bufferRead:sub(l + 1)
|
|
return result
|
|
else
|
|
start = #self.bufferRead
|
|
local result, reason = readChunk()
|
|
if not result then
|
|
if reason then
|
|
return nil, reason
|
|
else -- eof
|
|
local result = #self.bufferRead > 0 and self.bufferRead or nil
|
|
self.bufferRead = ""
|
|
return result
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- this whole thing is a house of cards. good luck.
|
|
io.write("\27[s\27[8m\27[6n")
|
|
if not (self.mx or self.my) then
|
|
io.write("\27[9999;9999H\27[6n")
|
|
end
|
|
local pos, buffer, hIndex, sx, sy = 1, "", 0
|
|
self.history = self.history or {}
|
|
local function redraw()
|
|
-- scroll until the buffer will fit on the screen
|
|
while sx and sy and self.mx and self.my and #buffer > (self.mx * ((self.my - sy) + 1)) - sx do
|
|
sy = sy - 1
|
|
io.write("\27[9999;9999H ")
|
|
io.write(string.format("\27[2K\27[%i;%iH\27[s", sx, sy))
|
|
end
|
|
io.write(string.format("\27[u%s \27[u\27[%iC",buffer,(#buffer-pos)+1))
|
|
end
|
|
while true do
|
|
char = readBytesOrChars(1)
|
|
if char == "\27" then
|
|
if readBytesOrChars(1) == "[" then
|
|
local args = {""}
|
|
repeat
|
|
char = readBytesOrChars(1)
|
|
if char:match("%d") then
|
|
args[#args] = args[#args]..char
|
|
else
|
|
args[#args] = tonumber(args[#args])
|
|
args[#args+1] = ""
|
|
end
|
|
until not char:match("[%d;]")
|
|
if char == "C" then -- right
|
|
if pos > 1 then
|
|
pos = pos - 1
|
|
end
|
|
elseif char == "D" then -- left
|
|
if pos <= #buffer then
|
|
pos = pos + 1
|
|
end
|
|
elseif char == "A" then -- up
|
|
hIndex = hIndex + 1
|
|
io.write("\27[u"..(" "):rep(buffer:len()+1))
|
|
buffer = self.history[1+#self.history-hIndex] or buffer
|
|
pos = 1
|
|
elseif char == "B" then -- down
|
|
hIndex = hIndex - 1
|
|
io.write("\27[u"..(" "):rep(buffer:len()+1))
|
|
if hIndex == 0 then
|
|
hIndex = hIndex - 1
|
|
buffer = ""
|
|
end
|
|
buffer = self.history[1+#self.history-hIndex] or buffer
|
|
pos = 1
|
|
elseif char == "R" then -- cursor position report
|
|
self.mx, self.my = sx and math.max(self.mx or 0, args[1]) or self.mx, sy and math.max(self.my or 0, args[2]) or self.my
|
|
sx, sy = sx or args[1], sy or args[2]
|
|
end
|
|
hIndex = math.max(math.min(hIndex,#self.history),0)
|
|
end
|
|
elseif char == "\8" then -- backspace
|
|
if #buffer > 0 and pos <= #buffer then
|
|
buffer = buffer:sub(1, (#buffer - pos)) .. buffer:sub((#buffer - pos) + 2)
|
|
end
|
|
elseif char == "\3" then -- ^C, error
|
|
error("terminated")
|
|
elseif char == "\1" then -- ^A, go to start of line
|
|
pos = buffer:len()+1
|
|
elseif char == "\5" then -- ^E, go to end of line
|
|
pos = 1
|
|
elseif char == "\2" then -- ^B, back one word
|
|
local nc = buffer:reverse():find(" ",pos+1)
|
|
pos = nc or #buffer+1
|
|
elseif char == "\6" then -- ^F, forward one word
|
|
local nc = buffer:find(" ",math.max(#buffer-pos+3,0))
|
|
pos = (nc and #buffer-nc+2) or 1
|
|
elseif char == "\13" or char == "\10" or char == "\n" then -- return / newline
|
|
io.write("\n")
|
|
self.history[#self.history+1] = buffer ~= "" and buffer ~= self.history[#self.history] and buffer or nil
|
|
if #self.history > (self.maxhistory or 16) then table.remove(self.history,1) end
|
|
if chop then buffer = buffer .. "\n" end
|
|
return buffer
|
|
else
|
|
buffer = buffer:sub(1, (#buffer - pos) + 1) .. char .. buffer:sub((#buffer - pos) + 2)
|
|
end
|
|
redraw()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function readAll()
|
|
repeat
|
|
local result, reason = readChunk()
|
|
if not result and reason then
|
|
return nil, reason
|
|
end
|
|
until not result -- eof
|
|
local result = self.bufferRead
|
|
self.bufferRead = ""
|
|
return result
|
|
end
|
|
|
|
local function read(n, format)
|
|
if type(format) == "number" then
|
|
return readBytesOrChars(format)
|
|
else
|
|
if type(format) ~= "string" or unicode.sub(format, 1, 1) ~= "*" then
|
|
error("bad argument #" .. n .. " (invalid option)")
|
|
end
|
|
format = unicode.sub(format, 2, 2)
|
|
if format == "n" then
|
|
return readNumber()
|
|
elseif format == "l" then
|
|
return readLine(true)
|
|
elseif format == "L" then
|
|
return readLine(false)
|
|
elseif format == "a" then
|
|
return readAll()
|
|
else
|
|
error("bad argument #" .. n .. " (invalid format)")
|
|
end
|
|
end
|
|
end
|
|
|
|
if self.mode.w or self.mode.a then
|
|
self:flush()
|
|
end
|
|
|
|
local results = {}
|
|
local formats = table.pack(...)
|
|
if formats.n == 0 then
|
|
return readLine(true)
|
|
end
|
|
for i = 1, formats.n do
|
|
local result, reason = read(i, formats[i])
|
|
if result then
|
|
results[i] = result
|
|
elseif reason then
|
|
return nil, reason
|
|
end
|
|
end
|
|
return table.unpack(results, 1, formats.n)
|
|
end
|
|
|
|
function buffer:seek(whence, offset)
|
|
whence = tostring(whence or "cur")
|
|
assert(whence == "set" or whence == "cur" or whence == "end",
|
|
"bad argument #1 (set, cur or end expected, got " .. whence .. ")")
|
|
offset = offset or 0
|
|
checkArg(2, offset, "number")
|
|
assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")
|
|
|
|
if self.mode.w or self.mode.a then
|
|
self:flush()
|
|
elseif whence == "cur" then
|
|
offset = offset - #self.bufferRead
|
|
end
|
|
local result, reason = self.stream:seek(whence, offset)
|
|
if result then
|
|
self.bufferRead = ""
|
|
return result
|
|
else
|
|
return nil, reason
|
|
end
|
|
end
|
|
|
|
function buffer:setvbuf(mode, size)
|
|
mode = mode or self.bufferMode
|
|
size = size or self.bufferSize
|
|
|
|
assert(mode == "no" or mode == "full" or mode == "line",
|
|
"bad argument #1 (no, full or line expected, got " .. tostring(mode) .. ")")
|
|
assert(mode == "no" or type(size) == "number",
|
|
"bad argument #2 (number expected, got " .. type(size) .. ")")
|
|
|
|
self.bufferMode = mode
|
|
self.bufferSize = size
|
|
|
|
return self.bufferMode, self.bufferSize
|
|
end
|
|
|
|
function buffer:getTimeout()
|
|
return self.readTimeout
|
|
end
|
|
|
|
function buffer:setTimeout(value)
|
|
self.readTimeout = tonumber(value)
|
|
end
|
|
|
|
function buffer:write(...)
|
|
if self.closed then
|
|
return badFileDescriptor()
|
|
end
|
|
local args = table.pack(...)
|
|
for i = 1, args.n do
|
|
if type(args[i]) == "number" then
|
|
args[i] = tostring(args[i])
|
|
end
|
|
checkArg(i, args[i], "string")
|
|
end
|
|
|
|
for i = 1, args.n do
|
|
local arg = args[i]
|
|
local result, reason
|
|
|
|
if self.bufferMode == "full" then
|
|
if self.bufferSize - #self.bufferWrite < #arg then
|
|
result, reason = self:flush()
|
|
if not result then
|
|
return nil, reason
|
|
end
|
|
end
|
|
if #arg > self.bufferSize then
|
|
result, reason = self.stream:write(arg)
|
|
else
|
|
self.bufferWrite = self.bufferWrite .. arg
|
|
result = self
|
|
end
|
|
|
|
elseif self.bufferMode == "line" then
|
|
local l
|
|
repeat
|
|
local idx = arg:find("\n", (l or 0) + 1, true)
|
|
if idx then
|
|
l = idx
|
|
end
|
|
until not idx
|
|
if l or #arg > self.bufferSize then
|
|
result, reason = self:flush()
|
|
if not result then
|
|
return nil, reason
|
|
end
|
|
end
|
|
if l then
|
|
result, reason = self.stream:write(arg:sub(1, l))
|
|
if not result then
|
|
return nil, reason
|
|
end
|
|
arg = arg:sub(l + 1)
|
|
end
|
|
if #arg > self.bufferSize then
|
|
result, reason = self.stream:write(arg)
|
|
else
|
|
self.bufferWrite = self.bufferWrite .. arg
|
|
result = self
|
|
end
|
|
|
|
else -- self.bufferMode == "no"
|
|
result, reason = self.stream:write(arg)
|
|
end
|
|
|
|
if not result then
|
|
return nil, reason
|
|
end
|
|
end
|
|
|
|
return self
|
|
end
|