-- 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\27[u") 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