Last active
November 20, 2022 00:26
-
-
Save MCJack123/50b211c55ceca4376e51d33435026006 to your computer and use it in GitHub Desktop.
CraftOS-PC raw mode client/server API for ComputerCraft with test program
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- rawterm.lua - CraftOS-PC raw mode protocol client/server API | |
-- By JackMacWindows | |
-- | |
-- @module rawterm | |
-- | |
-- This API provides the ability to host terminals accessible from remote | |
-- systems, as well as to render those terminals on the screen. It uses the raw | |
-- mode protocol defined by CraftOS-PC to communicate between client and server. | |
-- This means that this API can be used to host and connect to a CraftOS-PC | |
-- instance running over a WebSocket connection (using an external server | |
-- application). | |
-- | |
-- In addition, this API supports raw mode version 1.1, which includes support | |
-- for filesystem access. This lets the server send and receive files and query | |
-- file information over the raw connection. | |
-- | |
-- To allow the ability to use any type of connection medium to send/receive | |
-- data, a delegate object is used for communication. This must have a send and | |
-- receive method, and may also have additional methods as mentioned below. | |
-- Built-in delegate constructors are provided for WebSockets and Rednet. | |
-- | |
-- See the adjacent rawtermtest.lua file for an example of how to use this API. | |
-- MIT License | |
-- | |
-- Copyright (c) 2021 JackMacWindows | |
-- | |
-- Permission is hereby granted, free of charge, to any person obtaining a copy | |
-- of this software and associated documentation files (the "Software"), to deal | |
-- in the Software without restriction, including without limitation the rights | |
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
-- copies of the Software, and to permit persons to whom the Software is | |
-- furnished to do so, subject to the following conditions: | |
-- | |
-- The above copyright notice and this permission notice shall be included in all | |
-- copies or substantial portions of the Software. | |
-- | |
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
-- SOFTWARE. | |
local expect = require "cc.expect" | |
setmetatable(expect, {__call = function(_, ...) return expect.expect(...) end}) | |
local rawterm = {} | |
local b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | |
local keymap = { | |
[1] = 0, | |
[2] = keys.one, | |
[3] = keys.two, | |
[4] = keys.three, | |
[5] = keys.four, | |
[6] = keys.five, | |
[7] = keys.six, | |
[8] = keys.seven, | |
[9] = keys.eight, | |
[10] = keys.nine, | |
[11] = keys.zero, | |
[12] = keys.minus, | |
[13] = keys.equals, | |
[14] = keys.backspace, | |
[15] = keys.tab, | |
[16] = keys.q, | |
[17] = keys.w, | |
[18] = keys.e, | |
[19] = keys.r, | |
[20] = keys.t, | |
[21] = keys.y, | |
[22] = keys.u, | |
[23] = keys.i, | |
[24] = keys.o, | |
[25] = keys.p, | |
[26] = keys.leftBracket, | |
[27] = keys.rightBracket, | |
[28] = keys.enter, | |
[29] = keys.leftCtrl, | |
[30] = keys.a, | |
[31] = keys.s, | |
[32] = keys.d, | |
[33] = keys.f, | |
[34] = keys.g, | |
[35] = keys.h, | |
[36] = keys.j, | |
[37] = keys.k, | |
[38] = keys.l, | |
[39] = keys.semiColon, | |
[40] = keys.apostrophe, | |
[41] = keys.grave, | |
[42] = keys.leftShift, | |
[43] = keys.backslash, | |
[44] = keys.z, | |
[45] = keys.x, | |
[46] = keys.c, | |
[47] = keys.v, | |
[48] = keys.b, | |
[49] = keys.n, | |
[50] = keys.m, | |
[51] = keys.comma, | |
[52] = keys.period, | |
[53] = keys.slash, | |
[54] = keys.rightShift, | |
[55] = keys.multiply, | |
[56] = keys.leftAlt, | |
[57] = keys.space, | |
[58] = keys.capsLock, | |
[59] = keys.f1, | |
[60] = keys.f2, | |
[61] = keys.f3, | |
[62] = keys.f4, | |
[63] = keys.f5, | |
[64] = keys.f6, | |
[65] = keys.f7, | |
[66] = keys.f8, | |
[67] = keys.f9, | |
[68] = keys.f10, | |
[69] = keys.numLock, | |
[70] = keys.scrollLock, | |
[71] = keys.numPad7, | |
[72] = keys.numPad8, | |
[73] = keys.numPad9, | |
[74] = keys.numPadSubtract, | |
[75] = keys.numPad4, | |
[76] = keys.numPad5, | |
[77] = keys.numPad6, | |
[78] = keys.numPadAdd, | |
[79] = keys.numPad1, | |
[80] = keys.numPad2, | |
[81] = keys.numPad3, | |
[82] = keys.numPad0, | |
[83] = keys.numPadDecimal, | |
[87] = keys.f11, | |
[88] = keys.f12, | |
[100] = keys.f13, | |
[101] = keys.f14, | |
[102] = keys.f15, | |
[111] = keys.kana, | |
[121] = keys.convert, | |
[123] = keys.noconvert, | |
[125] = keys.yen, | |
[141] = keys.numPadEquals, | |
[144] = keys.cimcumflex, | |
[145] = keys.at, | |
[146] = keys.colon, | |
[147] = keys.underscore, | |
[148] = keys.kanji, | |
[149] = keys.stop, | |
[150] = keys.ax, | |
[156] = keys.numPadEnter, | |
[157] = keys.rightCtrl, | |
[179] = keys.numPadComma, | |
[181] = keys.numPadDivide, | |
[184] = keys.rightAlt, | |
[197] = keys.pause, | |
[199] = keys.home, | |
[200] = keys.up, | |
[201] = keys.pageUp, | |
[203] = keys.left, | |
[205] = keys.right, | |
[207] = keys["end"], | |
[208] = keys.down, | |
[209] = keys.pageDown, | |
[210] = keys.insert, | |
[211] = keys.delete | |
} | |
local keymap_rev = { | |
[0] = 1, | |
[keys.one] = 2, | |
[keys.two] = 3, | |
[keys.three] = 4, | |
[keys.four] = 5, | |
[keys.five] = 6, | |
[keys.six] = 7, | |
[keys.seven] = 8, | |
[keys.eight] = 9, | |
[keys.nine] = 10, | |
[keys.zero] = 11, | |
[keys.minus] = 12, | |
[keys.equals] = 13, | |
[keys.backspace] = 14, | |
[keys.tab] = 15, | |
[keys.q] = 16, | |
[keys.w] = 17, | |
[keys.e] = 18, | |
[keys.r] = 19, | |
[keys.t] = 20, | |
[keys.y] = 21, | |
[keys.u] = 22, | |
[keys.i] = 23, | |
[keys.o] = 24, | |
[keys.p] = 25, | |
[keys.leftBracket] = 26, | |
[keys.rightBracket] = 27, | |
[keys.enter] = 28, | |
[keys.leftCtrl] = 29, | |
[keys.a] = 30, | |
[keys.s] = 31, | |
[keys.d] = 32, | |
[keys.f] = 33, | |
[keys.g] = 34, | |
[keys.h] = 35, | |
[keys.j] = 36, | |
[keys.k] = 37, | |
[keys.l] = 38, | |
[keys.semicolon or keys.semiColon] = 39, | |
[keys.apostrophe] = 40, | |
[keys.grave] = 41, | |
[keys.leftShift] = 42, | |
[keys.backslash] = 43, | |
[keys.z] = 44, | |
[keys.x] = 45, | |
[keys.c] = 46, | |
[keys.v] = 47, | |
[keys.b] = 48, | |
[keys.n] = 49, | |
[keys.m] = 50, | |
[keys.comma] = 51, | |
[keys.period] = 52, | |
[keys.slash] = 53, | |
[keys.rightShift] = 54, | |
[keys.leftAlt] = 56, | |
[keys.space] = 57, | |
[keys.capsLock] = 58, | |
[keys.f1] = 59, | |
[keys.f2] = 60, | |
[keys.f3] = 61, | |
[keys.f4] = 62, | |
[keys.f5] = 63, | |
[keys.f6] = 64, | |
[keys.f7] = 65, | |
[keys.f8] = 66, | |
[keys.f9] = 67, | |
[keys.f10] = 68, | |
[keys.numLock] = 69, | |
[keys.scollLock or keys.scrollLock] = 70, | |
[keys.numPad7] = 71, | |
[keys.numPad8] = 72, | |
[keys.numPad9] = 73, | |
[keys.numPadSubtract] = 74, | |
[keys.numPad4] = 75, | |
[keys.numPad5] = 76, | |
[keys.numPad6] = 77, | |
[keys.numPadAdd] = 78, | |
[keys.numPad1] = 79, | |
[keys.numPad2] = 80, | |
[keys.numPad3] = 81, | |
[keys.numPad0] = 82, | |
[keys.numPadDecimal] = 83, | |
[keys.f11] = 87, | |
[keys.f12] = 88, | |
[keys.f13] = 100, | |
[keys.f14] = 101, | |
[keys.f15] = 102, | |
[keys.numPadEquals or keys.numPadEqual] = 141, | |
[keys.numPadEnter] = 156, | |
[keys.rightCtrl] = 157, | |
[keys.rightAlt] = 184, | |
[keys.pause] = 197, | |
[keys.home] = 199, | |
[keys.up] = 200, | |
[keys.pageUp] = 201, | |
[keys.left] = 203, | |
[keys.right] = 205, | |
[keys["end"]] = 207, | |
[keys.down] = 208, | |
[keys.pageDown] = 209, | |
[keys.insert] = 210, | |
[keys.delete] = 211 | |
} | |
local function minver(version) | |
local res | |
if _CC_VERSION then res = version <= _CC_VERSION | |
elseif not _HOST then res = version <= os.version():gsub("CraftOS ", "") | |
elseif _HOST:match("ComputerCraft 1%.1%d+") ~= version:match("1%.1%d+") then | |
version = version:gsub("(1%.)([02-9])", "%10%2") | |
local host = _HOST:gsub("(ComputerCraft 1%.)([02-9])", "%10%2") | |
res = version <= host:match("ComputerCraft ([0-9%.]+)") | |
else res = version <= _HOST:match("ComputerCraft ([0-9%.]+)") end | |
assert(res, "This program requires ComputerCraft " .. version .. " or later.") | |
end | |
local function base64encode(str) | |
local retval = "" | |
for s in str:gmatch "..." do | |
local n = s:byte(1) * 65536 + s:byte(2) * 256 + s:byte(3) | |
local a, b, c, d = bit32.extract(n, 18, 6), bit32.extract(n, 12, 6), bit32.extract(n, 6, 6), bit32.extract(n, 0, 6) | |
retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. b64str:sub(d+1, d+1) | |
end | |
if #str % 3 == 1 then | |
local n = str:byte(-1) | |
local a, b = bit32.rshift(n, 2), bit32.lshift(bit32.band(n, 3), 4) | |
retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. "==" | |
elseif #str % 3 == 2 then | |
local n = str:byte(-2) * 256 + str:byte(-1) | |
local a, b, c, d = bit32.extract(n, 10, 6), bit32.extract(n, 4, 6), bit32.lshift(bit32.extract(n, 0, 4), 2) | |
retval = retval .. b64str:sub(a+1, a+1) .. b64str:sub(b+1, b+1) .. b64str:sub(c+1, c+1) .. "=" | |
end | |
return retval | |
end | |
local function base64decode(str) | |
local retval = "" | |
for s in str:gmatch "...." do | |
if s:sub(3, 4) == '==' then | |
retval = retval .. string.char(bit32.bor(bit32.lshift(b64str:find(s:sub(1, 1)) - 1, 2), bit32.rshift(b64str:find(s:sub(2, 2)) - 1, 4))) | |
elseif s:sub(4, 4) == '=' then | |
local n = (b64str:find(s:sub(1, 1))-1) * 4096 + (b64str:find(s:sub(2, 2))-1) * 64 + (b64str:find(s:sub(3, 3))-1) | |
retval = retval .. string.char(bit32.extract(n, 10, 8)) .. string.char(bit32.extract(n, 2, 8)) | |
else | |
local n = (b64str:find(s:sub(1, 1))-1) * 262144 + (b64str:find(s:sub(2, 2))-1) * 4096 + (b64str:find(s:sub(3, 3))-1) * 64 + (b64str:find(s:sub(4, 4))-1) | |
retval = retval .. string.char(bit32.extract(n, 16, 8)) .. string.char(bit32.extract(n, 8, 8)) .. string.char(bit32.extract(n, 0, 8)) | |
end | |
end | |
return retval | |
end | |
local crctable | |
local function crc32(str) | |
-- calculate CRC-table | |
if not crctable then | |
crctable = {} | |
for i = 0, 0xFF do | |
local rem = i | |
for j = 1, 8 do | |
if bit32.band(rem, 1) == 1 then | |
rem = bit32.rshift(rem, 1) | |
rem = bit32.bxor(rem, 0xEDB88320) | |
else rem = bit32.rshift(rem, 1) end | |
end | |
crctable[i] = rem | |
end | |
end | |
local crc = 0xFFFFFFFF | |
for x = 1, #str do crc = bit32.bxor(bit32.rshift(crc, 8), crctable[bit32.bxor(bit32.band(crc, 0xFF), str:byte(x))]) end | |
return bit32.bxor(crc, 0xFFFFFFFF) | |
end | |
local function decodeIBT(data, pos) | |
local ptyp = data:byte(pos) | |
pos = pos + 1 | |
local pat | |
if ptyp == 0 then pat = "<j" | |
elseif ptyp == 1 then pat = "<n" | |
elseif ptyp == 2 then pat = "<B" | |
elseif ptyp == 3 then pat = "<z" | |
elseif ptyp == 4 then | |
local t, keys = {}, {} | |
local nent = data:byte(pos) | |
pos = pos + 1 | |
for i = 1, nent do keys[i], pos = decodeIBT(data, pos) end | |
for i = 1, nent do t[keys[i]], pos = decodeIBT(data, pos) end | |
return t, pos | |
else return nil, pos end | |
local d = string.unpack(pat, data, pos) | |
if ptyp == 2 then d = d ~= 0 end | |
pos = pos + string.packsize(pat) | |
return d, pos | |
end | |
local function encodeIBT(val) | |
if type(val) == "number" then | |
if val % 1 == 0 and val >= -0x80000000 and val < 0x80000000 then return string.pack("<Bj", 0, val) | |
else return string.pack("<Bn", 1, val) end | |
elseif type(val) == "boolean" then return string.pack("<BB", 2, val and 1 or 0) | |
elseif type(val) == "string" then return string.pack("<Bz", 3, val) | |
elseif type(val) == "nil" then return "\5" | |
elseif type(val) == "table" then | |
local keys, vals = {}, {} | |
local i = 1 | |
for k,v in pairs(val) do keys[i], vals[i], i = k, v, i + 1 end | |
local s = string.pack("<BB", 4, i - 1) | |
for j = 1, i - 1 do s = s .. encodeIBT(keys[j]) end | |
for j = 1, i - 1 do s = s .. encodeIBT(vals[j]) end | |
return s | |
else error("Cannot encode type " .. type(val)) end | |
end | |
local mouse_events = {[0] = "mouse_click", "mouse_up", "mouse_scroll", "mouse_drag"} | |
local fsFunctions = {[0] = fs.exists, fs.isDir, fs.isReadOnly, fs.getSize, fs.getDrive, fs.getCapacity, fs.getFreeSpace, fs.list, fs.attributes, fs.find, fs.makeDir, fs.delete, fs.copy, fs.move, function() end, function() end} | |
local openModes = {[0] = "r", "w", "r", "a", "rb", "wb", "rb", "ab"} | |
local localEvents = {key = true, key_up = true, char = true, mouse_click = true, mouse_up = true, mouse_drag = true, mouse_scroll = true, mouse_move = true, term_resize = true, paste = true} | |
minver "1.91.0" | |
--- Creates a new server window object with the specified properties. | |
-- This object functions like an object from the window API, and can be used as | |
-- a redirection target. It also has a few additional functions to control the | |
-- client and connection. | |
-- @param delegate The delegate object. This must have two methods named | |
-- `:send(data)` and `:receive()`, and may additionally have a `:close()` method | |
-- that's called when the server is closed. Every method is called with the `:` | |
-- operator, meaning its first argument is the delegate object itself. | |
-- @param width The width of the new window. | |
-- @param height The height of the new window. | |
-- @param id The ID of the window. Multiple window IDs can be sent over one | |
-- connection. This defaults to 0. | |
-- @param title The title of the window. This defaults to "CraftOS Raw Terminal". | |
-- @param parent The parent window to draw to. This allows rendering on both the | |
-- screen and a remote terminal. If unspecified, output will only be sent to the | |
-- remote terminal. | |
-- @param x If parent is specified, the X coordinate to start at. This defaults to 1. | |
-- @param y If parent is specified, the Y coordinate to start at. This defaults to 1. | |
-- @param blockFSAccess Set this to true to disable filesystem access for clients. | |
-- @return The new window object. | |
function rawterm.server(delegate, width, height, id, title, parent, x, y, blockFSAccess) | |
expect(1, delegate, "table") | |
expect(2, width, "number") | |
expect(3, height, "number") | |
expect(4, id, "number", "nil") | |
expect(5, title, "string", "nil") | |
expect(6, parent, "table", "nil") | |
expect(7, x, "number", "nil") | |
expect(8, y, "number", "nil") | |
expect.field(delegate, "send", "function") | |
expect.field(delegate, "receive", "function") | |
expect.field(delegate, "close", "function", "nil") | |
title = title or "CraftOS Raw Terminal" | |
x = x or 1 | |
y = y or 1 | |
local win, mode, cursorX, cursorY, current_colors, visible, canBlink, isClosed, changed = {}, 0, 1, 1, 0xF0, true, false, false, true | |
local screen, colors, pixels, palette, fileHandles = {}, {}, {}, {}, {} | |
local flags = { | |
isVersion11 = false, | |
filesystem = false, | |
binaryChecksum = false | |
} | |
for i = 1, height do screen[i], colors[i] = (" "):rep(width), ("\xF0"):rep(width) end | |
for i = 1, height*9 do pixels[i] = ("\x0F"):rep(width*6) end | |
for i = 0, 15 do palette[i] = {(parent or term).getPaletteColor(2^i)} end | |
for i = 16, 255 do palette[i] = {0, 0, 0} end | |
local function makePacket(type, id, data) | |
local payload = base64encode(string.char(type) .. string.char(id or 0) .. data) | |
local d | |
if #data > 65535 and flags.isVersion11 then d = "!CPD" .. string.format("%012X", #payload) | |
else d = "!CPC" .. string.format("%04X", #payload) end | |
d = d .. payload | |
if flags.binaryChecksum then d = d .. ("%08X"):format(crc32(string.char(type) .. string.char(id or 0) .. data)) | |
else d = d .. ("%08X"):format(crc32(payload)) end | |
return d .. "\n" | |
end | |
-- Term functions | |
function win.write(text) | |
text = tostring(text) | |
expect(1, text, "string") | |
if cursorY < 1 or cursorY > height then return | |
elseif cursorX > width or cursorX + #text < 1 then | |
cursorX = cursorX + #text | |
return | |
elseif cursorX < 1 then | |
text = text:sub(-cursorX + 2) | |
cursorX = 1 | |
end | |
local ntext = #text | |
if cursorX + #text > width then text = text:sub(1, width - cursorX + 1) end | |
screen[cursorY] = screen[cursorY]:sub(1, cursorX - 1) .. text .. screen[cursorY]:sub(cursorX + #text) | |
colors[cursorY] = colors[cursorY]:sub(1, cursorX - 1) .. string.char(current_colors):rep(#text) .. colors[cursorY]:sub(cursorX + #text) | |
cursorX = cursorX + ntext | |
changed = true | |
win.redraw() | |
end | |
function win.blit(text, fg, bg) | |
text = tostring(text) | |
expect(1, text, "string") | |
expect(2, fg, "string") | |
expect(3, bg, "string") | |
if #text ~= #fg or #fg ~= #bg then error("Arguments must be the same length", 2) end | |
if cursorY < 1 or cursorY > height then return | |
elseif cursorX > width or cursorX < 1 - #text then | |
cursorX = cursorX + #text | |
win.redraw() | |
return | |
elseif cursorX < 1 then | |
text, fg, bg = text:sub(-cursorX + 2), fg:sub(-cursorX + 2), bg:sub(-cursorX + 2) | |
cursorX = 1 | |
win.redraw() | |
end | |
local ntext = #text | |
if cursorX + #text > width then text, fg, bg = text:sub(1, width - cursorX + 1), fg:sub(1, width - cursorX + 1), bg:sub(1, width - cursorX + 1) end | |
local col = "" | |
for i = 1, #text do col = col .. string.char((tonumber(bg:sub(i, i), 16) or 0) * 16 + (tonumber(fg:sub(i, i), 16) or 0)) end | |
screen[cursorY] = screen[cursorY]:sub(1, cursorX - 1) .. text .. screen[cursorY]:sub(cursorX + #text) | |
colors[cursorY] = colors[cursorY]:sub(1, cursorX - 1) .. col .. colors[cursorY]:sub(cursorX + #text) | |
cursorX = cursorX + ntext | |
changed = true | |
win.redraw() | |
end | |
function win.clear() | |
if mode == 0 then | |
for i = 1, height do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end | |
else | |
for i = 1, height*9 do pixels[i] = ("\x0F"):rep(width*6) end | |
end | |
changed = true | |
win.redraw() | |
end | |
function win.clearLine() | |
if cursorY >= 1 and cursorY <= height then | |
screen[cursorY], colors[cursorY] = (" "):rep(width), string.char(current_colors):rep(width) | |
changed = true | |
win.redraw() | |
end | |
end | |
function win.getCursorPos() | |
return cursorX, cursorY | |
end | |
function win.setCursorPos(cx, cy) | |
expect(1, cx, "number") | |
expect(2, cy, "number") | |
if cx == cursorX and cy == cursorY then return end | |
cursorX, cursorY = cx, cy | |
changed = true | |
win.redraw() | |
end | |
function win.getCursorBlink() | |
return canBlink | |
end | |
function win.setCursorBlink(b) | |
expect(1, b, "boolean") | |
canBlink = b | |
if parent then parent.setCursorBlink(b) end | |
win.redraw() | |
end | |
function win.isColor() | |
if parent then return parent.isColor() end | |
return true | |
end | |
function win.getSize(m) | |
if (type(m) == "number" and m > 1) or (type(m) == "boolean" and m == true) then return width * 6, height * 9 | |
else return width, height end | |
end | |
function win.scroll(lines) | |
expect(1, lines, "number") | |
if math.abs(lines) >= width then | |
for i = 1, height do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end | |
elseif lines > 0 then | |
for i = lines + 1, height do screen[i - lines], colors[i - lines] = screen[i], colors[i] end | |
for i = height - lines + 1, height do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end | |
elseif lines < 0 then | |
for i = 1, height + lines do screen[i - lines], colors[i - lines] = screen[i], colors[i] end | |
for i = 1, -lines do screen[i], colors[i] = (" "):rep(width), string.char(current_colors):rep(width) end | |
else return end | |
changed = true | |
win.redraw() | |
end | |
function win.getTextColor() | |
return 2^bit32.band(current_colors, 0x0F) | |
end | |
function win.setTextColor(color) | |
expect(1, color, "number") | |
current_colors = bit32.band(current_colors, 0xF0) + bit32.band(math.floor(math.log(color, 2)), 0x0F) | |
end | |
function win.getBackgroundColor() | |
return 2^bit32.rshift(current_colors, 4) | |
end | |
function win.setBackgroundColor(color) | |
expect(1, color, "number") | |
current_colors = bit32.band(current_colors, 0x0F) + bit32.band(math.floor(math.log(color, 2)), 0x0F) * 16 | |
end | |
function win.getPaletteColor(color) | |
expect(1, color, "number") | |
if mode == 2 then if color < 0 or color > 255 then error("bad argument #1 (value out of range)", 2) end | |
else color = bit32.band(math.floor(math.log(color, 2)), 0x0F) end | |
return table.unpack(palette[color]) | |
end | |
function win.setPaletteColor(color, r, g, b) | |
expect(1, color, "number") | |
expect(2, r, "number") | |
expect(3, g, "number", "nil") | |
expect(4, b, "number", "nil") | |
if g == nil and b == nil then r, g, b = bit32.extract(r, 16, 8) / 255, bit32.extract(r, 8, 8) / 255, bit32.extract(r, 0, 8) / 255 end | |
if r < 0 or r > 1 then error("bad argument #2 (value out of range)", 2) end | |
if g < 0 or g > 1 then error("bad argument #3 (value out of range)", 2) end | |
if b < 0 or b > 1 then error("bad argument #4 (value out of range)", 2) end | |
if mode == 2 then if color < 0 or color > 255 then error("bad argument #1 (value out of range)", 2) end | |
else color = bit32.band(math.floor(math.log(color, 2)), 0x0F) end | |
palette[color] = {r, g, b} | |
changed = true | |
win.redraw() | |
end | |
-- Graphics functions | |
function win.getGraphicsMode() | |
if mode == 0 then return false | |
else return mode end | |
end | |
function win.setGraphicsMode(m) | |
expect(1, m, "boolean", "number") | |
local om = mode | |
if m == false then mode = 0 | |
elseif m == true then mode = 1 | |
elseif m >= 0 and m <= 2 then mode = math.floor(m) | |
else error("bad argument #1 (invalid mode)", 2) end | |
if mode ~= om then changed = true win.redraw() end | |
end | |
function win.getPixel(px, py) | |
expect(1, px, "number") | |
expect(2, py, "number") | |
if px < 0 or px >= width or py < 0 or py >= height then return nil end | |
local c = pixels[py + 1]:byte(px + 1, px + 1) | |
return mode == 2 and c or 2^c | |
end | |
function win.setPixel(px, py, color) | |
expect(1, px, "number") | |
expect(2, py, "number") | |
expect(3, color, "number") | |
if mode == 2 then if color < 0 or color > 255 then error("bad argument #3 (value out of range)", 2) end | |
else color = bit32.band(math.floor(math.log(color, 2)), 0x0F) end | |
pixels[py + 1] = pixels[py + 1]:sub(1, px) .. string.char(color) .. pixels[py + 1]:sub(px + 2) | |
changed = true | |
win.redraw() | |
end | |
function win.drawPixels(px, py, pix, pw, ph) | |
expect(1, px, "number") | |
expect(2, py, "number") | |
expect(3, pix, "table", "number") | |
expect(4, pw, "number", type(pix) ~= "number" and "nil" or nil) | |
expect(5, ph, "number", type(pix) ~= "number" and "nil" or nil) | |
if type(pix) == "number" then | |
if mode == 2 then if pix < 0 or pix > 255 then error("bad argument #3 (value out of range)", 2) end | |
else pix = bit32.band(math.floor(math.log(pix, 2)), 0x0F) end | |
for cy = py + 1, py + ph do pixels[cy] = pixels[cy]:sub(1, px) .. string.char(pix):rep(pw) .. pixels[cy]:sub(px + pw + 1) end | |
else | |
for cy = py + 1, py + (ph or #pix) do | |
local row = pix[cy - py] | |
if type(row) == "string" then | |
pixels[cy] = pixels[cy]:sub(1, px) .. row:sub(1, pw or -1) .. pixels[cy]:sub(px + (pw or #row) + 1) | |
elseif type(row) == "table" then | |
local str = "" | |
for cx = 1, pw or #row do str = str .. string.char(row[cx] or pixels[cy]:byte(px + cx)) end | |
pixels[cy] = pixels[cy]:sub(1, px) .. str .. pixels[cy]:sub(px + #str + 1) | |
end | |
end | |
end | |
changed = true | |
win.redraw() | |
end | |
function win.getPixels(px, py, pw, ph, str) | |
expect(1, px, "number") | |
expect(2, py, "number") | |
expect(3, pw, "number") | |
expect(4, ph, "number") | |
expect(5, str, "boolean", "nil") | |
local retval = {} | |
for cy = py + 1, py + ph do | |
if str then retval[cy - py] = pixels[cy]:sub(px + 1, px + pw) else | |
retval[cy - py] = {pixels[cy]:byte(px + 1, px + pw)} | |
if mode < 2 then for i = 1, pw do retval[cy - py][i] = 2^retval[cy - py][i] end end | |
end | |
end | |
return retval | |
end | |
win.isColour = win.isColor | |
win.getTextColour = win.getTextColor | |
win.setTextColour = win.setTextColor | |
win.getBackgroundColour = win.getBackgroundColor | |
win.setBackgroundColour = win.setBackgroundColor | |
win.getPaletteColour = win.getPaletteColor | |
win.setPaletteColour = win.setPaletteColor | |
-- Window functions | |
function win.getLine(cy) | |
if cy < 1 or cy > height then return nil end | |
local fg, bg = "", "" | |
for c in colors[cy]:gmatch "." do | |
fg, bg = fg .. ("%x"):format(bit32.band(c:byte(), 0x0F)), bg .. ("%x"):format(bit32.rshift(c:byte(), 4)) | |
end | |
return screen[cy], fg, bg | |
end | |
function win.isVisible() | |
return visible | |
end | |
function win.setVisible(v) | |
expect(1, v, "boolean") | |
visible = v | |
win.redraw() | |
end | |
function win.redraw() | |
if visible and changed then | |
-- Draw to parent screen | |
if parent then | |
-- This is NOT efficient, but it's not really supposed to be anyway. | |
if parent.getGraphicsMode and (parent.getGraphicsMode() or 0) ~= mode then parent.setGraphicsMode(mode) end | |
for i = 0, (mode == 2 and parent.getGraphicsMode and 255 or 15) do parent.setPaletteColor((mode == 2 and i or 2^i), palette[i][1], palette[i][2], palette[i][3]) end | |
if mode == 0 then | |
local b = parent.getCursorBlink() | |
parent.setCursorBlink(false) | |
for cy = 1, height do | |
parent.setCursorPos(x, y + cy - 1) | |
parent.blit(win.getLine(cy)) | |
end | |
parent.setCursorBlink(b) | |
win.restoreCursor() | |
elseif parent.drawPixels then | |
parent.drawPixels((x - 1) * 6, (y - 1) * 9, pixels, width, height) | |
end | |
end | |
-- Draw to raw target | |
if not isClosed then | |
local rleText = "" | |
if mode == 0 then | |
local c, n = screen[1]:sub(1, 1), 0 | |
for cy = 1, height do | |
for ch in screen[cy]:gmatch "." do | |
if ch ~= c or n == 255 then | |
rleText = rleText .. c .. string.char(n) | |
c, n = ch, 0 | |
end | |
n=n+1 | |
end | |
end | |
if n > 0 then rleText = rleText .. c .. string.char(n) end | |
c, n = colors[1]:sub(1, 1), 0 | |
for cy = 1, height do | |
for ch in colors[cy]:gmatch "." do | |
if ch ~= c or n == 255 then | |
rleText = rleText .. c .. string.char(n) | |
c, n = ch, 0 | |
end | |
n=n+1 | |
end | |
end | |
if n > 0 then rleText = rleText .. c .. string.char(n) end | |
else | |
local c, n = pixels[1]:sub(1, 1), 0 | |
for cy = 1, height * 9 do | |
for ch in pixels[cy]:gmatch "." do | |
if ch ~= c or n == 255 then | |
rleText = rleText .. c .. string.char(n) | |
c, n = ch, 0 | |
end | |
n=n+1 | |
end | |
end | |
end | |
for i = 0, (mode == 2 and 255 or 15) do rleText = rleText .. string.char(palette[i][1] * 255) .. string.char(palette[i][2] * 255) .. string.char(palette[i][3] * 255) end | |
delegate:send(makePacket(0, id, string.pack("<BBHHHHBxxx", mode, canBlink and 1 or 0, width, height, math.min(math.max(cursorX - 1, 0), 0xFFFFFFFF), math.min(math.max(cursorY - 1, 0), 0xFFFFFFFF), parent and (parent.isColor() and 0 or 1) or 0) .. rleText)) | |
end | |
changed = false | |
end | |
end | |
function win.restoreCursor() | |
if parent then parent.setCursorPos(x + cursorX - 1, y + cursorY - 1) end | |
end | |
function win.getPosition() | |
return x, y | |
end | |
function win.reposition(nx, ny, nwidth, nheight, nparent) | |
expect(1, nx, "number", "nil") | |
expect(2, ny, "number", "nil") | |
expect(3, nwidth, "number", "nil") | |
expect(4, nheight, "number", "nil") | |
expect(5, nparent, "table", "nil") | |
x, y, parent = nx or x, ny or y, nparent or parent | |
local resized = (nwidth and nwidth ~= width) or (nheight and nheight ~= height) | |
if nwidth then | |
if nwidth < width then | |
for cy = 1, height do | |
screen[cy], colors[cy] = screen[cy]:sub(1, nwidth), colors[cy]:sub(1, nwidth) | |
for i = 1, 9 do pixels[(cy - 1)*9 + i] = pixels[(cy - 1)*9 + i]:sub(1, nwidth * 6) end | |
end | |
elseif nwidth > width then | |
for cy = 1, height do | |
screen[cy], colors[cy] = screen[cy] .. (" "):rep(nwidth - width), colors[cy] .. string.char(current_colors):rep(nwidth - width) | |
for i = 1, 9 do pixels[(cy - 1)*9 + i] = pixels[(cy - 1)*9 + i] .. ("\x0F"):rep((nwidth - width) * 6) end | |
end | |
end | |
width = nwidth | |
end | |
if nheight then | |
if nheight < height then | |
for cy = nheight + 1, height do | |
screen[cy], colors[cy] = nil | |
for i = 1, 9 do pixels[(cy - 1)*9 + i] = nil end | |
end | |
elseif nheight > height then | |
for cy = height + 1, nheight do | |
screen[cy], colors[cy] = (" "):rep(width), string.char(current_colors):rep(width) | |
for i = 1, 9 do pixels[(cy - 1)*9 + i] = ("\x0F"):rep(width * 6) end | |
end | |
end | |
height = nheight | |
end | |
if resized and not isClosed then delegate:send(makePacket(4, id, string.pack("<BBHHz", 0, os.computerID(), width, height, title))) end | |
changed = true | |
win.redraw() | |
end | |
-- Raw functions | |
--- A wrapper for os.pullEvent() that also listens for raw events, and returns | |
-- them if found. | |
-- @param filter A filter for the event. | |
-- @param ignoreLocalEvents Set this to a truthy value to ignore receiving | |
-- input events from the local computer, making the terminal otherwise | |
-- isolated from the rest of the system. | |
-- @return The event name and arguments. | |
function win.pullEvent(filter, ignoreLocalEvents) | |
expect(1, filter, "string", "nil") | |
local ev | |
parallel.waitForAny(function() | |
if isClosed then while true do coroutine.yield() end end | |
while true do | |
local msg = delegate:receive() | |
if not msg then | |
isClosed = true | |
while true do coroutine.yield() end | |
end | |
if msg:sub(1, 3) == "!CP" then | |
local off = 8 | |
if msg:sub(4, 4) == 'D' then off = 16 end | |
local size = tonumber(msg:sub(5, off), 16) | |
local payload = msg:sub(off + 1, off + size) | |
local expected = tonumber(msg:sub(off + size + 1, off + size + 8), 16) | |
local data = base64decode(payload) | |
if crc32(flags.binaryChecksum and data or payload) == expected then | |
local typ, wid = data:byte(1, 2) | |
if wid == id then | |
if typ == 1 then | |
local ch, flags = data:byte(3, 4) | |
if bit32.btest(flags, 8) then ev = {"char", string.char(ch)} | |
elseif bit32.btest(flags, 1) then ev = {"key", keymap[ch], bit32.btest(flags, 2)} | |
else ev = {"key_up", keymap[ch]} end | |
if not filter or ev[1] == filter then return else ev = nil end | |
elseif typ == 2 then | |
local evt, button, mx, my = string.unpack("<BBII", data, 3) | |
ev = {mouse_events[evt], evt == 2 and button * 2 - 1 or button, mx, my} | |
if not filter or ev[1] == filter then return else ev = nil end | |
elseif typ == 3 then | |
local nparam, name = string.unpack("<Bz", data, 3) | |
ev = {name} | |
local pos = #name + 5 | |
for i = 2, nparam + 1 do ev[i], pos = decodeIBT(data, pos) end | |
if not filter or ev[1] == filter then return else ev = nil end | |
elseif typ == 4 then | |
local flags, _, w, h = string.unpack("<BBHH", data, 3) | |
if flags == 0 then | |
if w ~= 0 and h ~= 0 then | |
win.reposition(nil, nil, w, h, nil) | |
ev = {"term_resize"} | |
end | |
elseif flags == 1 or flags == 2 then | |
win.close() | |
ev = {"win_close"} | |
end | |
if not filter or ev[1] == filter then return else ev = nil end | |
elseif typ == 7 and flags.filesystem then | |
local reqtype, reqid, path, path2 = string.unpack("<BBz", data, 3) | |
if reqtype == 12 or reqtype == 13 then path2 = string.unpack("<z", data, path2) else path2 = nil end | |
if bit32.band(reqtype, 0xF0) == 0 then | |
local ok, val = pcall(fsFunctions[reqtype], path, path2) | |
if ok then | |
if type(val) == "boolean" then delegate:send(makePacket(8, id, string.pack("<BBB", reqtype, reqid, val and 1 or 0))) | |
elseif type(val) == "number" then delegate:send(makePacket(8, id, string.pack("<BBI4", reqtype, reqid, val))) | |
elseif type(val) == "string" then delegate:send(makePacket(8, id, string.pack("<BBz", reqtype, reqid, val))) | |
elseif reqtype == 8 then | |
if val then delegate:send(makePacket(8, id, string.pack("<BBI4I8I8BBBB", reqtype, reqid, val.size, val.created, val.modified, val.isDir and 1 or 0, val.isReadOnly and 1 or 0, 0, 0))) | |
else delegate:send(makePacket(8, id, string.pack("<BBI4I8I8BBBB", reqtype, reqid, 0, 0, 0, 0, 0, 1, 0))) end | |
elseif type(val) == "table" then | |
local list = "" | |
for i = 1, #val do list = list .. val[i] .. "\0" end | |
delegate:send(makePacket(8, id, string.pack("<BBI4", reqtype, reqid, #val) .. list)) | |
else delegate:send(makePacket(8, id, string.pack("<BBB", reqtype, reqid, 0))) end | |
else | |
if reqtype == 0 or reqtype == 1 or reqtype == 2 then delegate:send(makePacket(8, id, string.pack("<BBB", reqtype, reqid, 2))) | |
elseif reqtype == 3 or reqtype == 5 or reqtype == 6 then delegate:send(makePacket(8, id, string.pack("<BBI4", reqtype, reqid, 0xFFFFFFFF))) | |
elseif reqtype == 4 or reqtype == 7 or reqtype == 9 then delegate:send(makePacket(8, id, string.pack("<BBz", reqtype, reqid, ""))) | |
elseif reqtype == 8 then delegate:send(makePacket(8, id, string.pack("<BBI4I8I8BBBB", reqtype, reqid, 0, 0, 0, 0, 0, 2, 0))) | |
else delegate:send(makePacket(8, id, string.pack("<BBz", reqtype, reqid, val))) end | |
end | |
elseif bit32.band(reqtype, 0xF0) == 0x10 then | |
local file, err = fs.open(path, openModes[bit32.band(reqtype, 7)]) | |
if file then | |
if bit32.btest(reqtype, 1) then fileHandles[reqid] = file else | |
delegate:send(makePacket(9, id, string.pack("<BBs4", 0, reqid, file.readAll()))) | |
file.close() | |
end | |
else | |
if bit32.btest(reqtype, 1) then delegate:send(makePacket(8, id, string.pack("<BBz", reqtype, reqid, err))) | |
else delegate:send(makePacket(9, id, string.pack("<BBs4", 1, reqid, err))) end | |
end | |
end | |
elseif typ == 9 and flags.filesystem then | |
local _, reqid, size = string.unpack("<BBI4", data, 3) | |
local str = data:sub(9, size + 8) | |
if fileHandles[reqid] ~= nil then | |
fileHandles[reqid].write(str) | |
fileHandles[reqid].close() | |
fileHandles[reqid] = nil | |
delegate:send(makePacket(8, id, string.pack("<BBB", 17, reqid, 0))) | |
else delegate:send(makePacket(8, id, string.pack("<BBz", 17, reqid, "Unknown request ID"))) end | |
end | |
end | |
if typ == 6 then | |
flags.isVersion11 = true | |
local f = string.unpack("<H", data, 3) | |
if wid == id then delegate:send(makePacket(6, wid, string.pack("<H", 1 + (blockFSAccess and 0 or 2)))) end | |
if bit32.btest(f, 0x01) then flags.binaryChecksum = true end | |
if bit32.btest(f, 0x02) and not blockFSAccess then flags.filesystem = true end | |
if bit32.btest(f, 0x04) then delegate:send(makePacket(4, id, string.pack("<BBHHz", 0, os.computerID(), width, height, title))) changed = true end | |
end | |
end | |
end | |
end | |
end, function() | |
repeat | |
ev = nil | |
ev = table.pack(os.pullEventRaw(filter)) | |
until not ignoreLocalEvents or not localEvents[ev[1]] | |
end) | |
return table.unpack(ev, 1, ev.n or #ev) | |
end | |
--- Sets the window's title and sends a message to the client. | |
-- @param t The new title of the window. | |
function win.setTitle(t) | |
expect(1, title, "string") | |
title = t | |
if isClosed then return end | |
delegate:send(makePacket(4, id, string.pack("<BBHHz", 0, os.computerID(), width, height, title))) | |
end | |
--- Sends a message to the client. | |
-- @param type Either "error", "warning", or "info" to specify an icon to show. | |
-- @param title The title of the message. | |
-- @param message The message to display. | |
function win.sendMessage(type, title, message) | |
expect(1, title, "string") | |
expect(2, message, "string") | |
expect(3, type, "string", "nil") | |
if isClosed then return end | |
local flags = 0 | |
if type == "error" then type = 0x10 | |
elseif type == "warning" then type = 0x20 | |
elseif type == "info" then type = 0x40 | |
elseif type then error("bad argument #3 (invalid type '" .. type .. "')", 2) end | |
delegate:send(makePacket(5, id, string.pack("<Izz", flags, title, message))) | |
end | |
--- Closes the window connection. Any changes made to the screen will still | |
-- show on the parent window if defined. | |
function win.close() | |
if isClosed then return end | |
delegate:send(makePacket(4, id, string.pack("<BBHHz", 1, 0, 0, 0, ""))) | |
if delegate.close then delegate:close() end | |
isClosed = true | |
end | |
delegate:send(makePacket(4, id, string.pack("<BBHHz", 0, os.computerID() % 256, width, height, title))) | |
return win | |
end | |
--- Creates a new client handle that listens for the specified window ID, and | |
-- renders to a window. | |
-- @param delegate The delegate object. This must have two methods named | |
-- `:send(data)` and `:receive()`, and may additionally have a `:close()` method | |
-- that's called when the server is closed. It may also have `:setTitle(title)` | |
-- to set the title of the window, `:showMessage(type, title, message)` to show | |
-- a message on the screen (type may be `error`, `warning`, or `info`), and | |
-- `:windowNotification(id, width, height, title)` which is called when an | |
-- unknown window ID gets a window update (this may be used to discover new | |
-- window alerts from the server). Every method is called with the `:` operator, | |
-- meaning its first argument is the delegate object itself. | |
-- @param id The ID of the window to listen for. (If in doubt, use 0.) | |
-- @param window The window to render to. | |
-- @return The new client handle. | |
function rawterm.client(delegate, id, window) | |
expect(1, delegate, "table") | |
expect(2, id, "number") | |
expect(3, window, "table", "nil") | |
expect.field(delegate, "send", "function") | |
expect.field(delegate, "receive", "function") | |
expect.field(delegate, "close", "function", "nil") | |
expect.field(delegate, "setTitle", "function", "nil") | |
expect.field(delegate, "showMessage", "function", "nil") | |
expect.field(delegate, "windowNotification", "function", "nil") | |
local handle = {} | |
local flags = { | |
isVersion11 = false, | |
binaryChecksum = false, | |
filesystem = false | |
} | |
local isClosed = false | |
local nextFSID = 0 | |
local function makePacket(type, id, data) | |
local payload = base64encode(string.char(type) .. string.char(id or 0) .. data) | |
local d | |
if #data > 65535 and flags.isVersion11 then d = "!CPD" .. string.format("%012X", #payload) | |
else d = "!CPC" .. string.format("%04X", #payload) end | |
d = d .. payload | |
if flags.binaryChecksum then d = d .. ("%08X"):format(crc32(string.char(type) .. string.char(id or 0) .. data)) | |
else d = d .. ("%08X"):format(crc32(payload)) end | |
return d .. "\n" | |
end | |
local function makeFSFunction(fid, type, p2) | |
local f = function(path, path2) | |
expect(1, path, "string") | |
if p2 then expect(2, path, "string") end | |
local n = nextFSID | |
delegate:send(makePacket(7, id, string.pack(p2 and "<BBzz" or "<BBz", fid, n, path, path2))) | |
nextFSID = (nextFSID + 1) % 256 | |
local data | |
while not data or data:byte(4) ~= n do data = handle.update(delegate:receive()) end | |
if type == "nil" then | |
local v = string.unpack("z", data, 5) | |
if v ~= "" then error(v, 2) | |
else return end | |
elseif type == "boolean" then | |
local v = data:byte(5) | |
if v == 2 then error("Failure", 2) | |
else return v ~= 0 end | |
elseif type == "number" then | |
local v = string.unpack("<I4", data, 5) | |
if v == 0xFFFFFFFF then error("Failure", 2) | |
else return v end | |
elseif type == "string" then | |
local v = string.unpack("<I4", data, 5) | |
if v == "" then error("Failure", 2) | |
else return v end | |
elseif type == "table" then | |
local size = string.unpack("<I4", data, 5) | |
if size == 0xFFFFFFFF then error("Failure", 2) end | |
local retval, pos = {}, 9 | |
for i = 1, size do retval[i], pos = string.unpack("z", data, pos) end | |
return retval | |
elseif type == "attributes" then | |
local attr, err = {} | |
attr.size, attr.created, attr.modified, attr.isDir, attr.isReadOnly, err = string.unpack("<I4I8I8BBB", data, 5) | |
if err == 1 then return nil | |
elseif err == 2 then error("Failure", 2) | |
else return attr end | |
end | |
end | |
if p2 then return f else return function(path) return f(path) end end | |
end | |
local fsHandle = { | |
exists = makeFSFunction(0, "boolean"), | |
isDir = makeFSFunction(1, "boolean"), | |
isReadOnly = makeFSFunction(2, "boolean"), | |
getSize = makeFSFunction(3, "number"), | |
getDrive = makeFSFunction(4, "string"), | |
getCapacity = makeFSFunction(5, "number"), | |
getFreeSpace = makeFSFunction(6, "number"), | |
list = makeFSFunction(7, "table"), | |
attributes = makeFSFunction(8, "attributes"), | |
find = makeFSFunction(9, "table"), | |
makeDir = makeFSFunction(10, "nil"), | |
delete = makeFSFunction(11, "nil"), | |
copy = makeFSFunction(12, "nil", true), | |
move = makeFSFunction(13, "nil", true), | |
open = function(path, mode) | |
expect(1, path, "string") | |
expect(2, mode, "string") | |
local m | |
for i = 0, 7 do if openModes[i] == mode then m = i break end end | |
if not m then error("Invalid mode", 2) end | |
if bit32.btest(m, 1) then | |
local buf, closed = "", false | |
return { | |
write = function(d) | |
if closed then error("attempt to use closed file", 2) end | |
if bit32.btest(m, 4) and type(d) == "number" then buf = buf .. string.char(d) | |
else buf = buf .. tostring(d) end | |
end, | |
writeLine = function(d) | |
if closed then error("attempt to use closed file", 2) end | |
buf = buf .. tostring(d) .. "\n" | |
end, | |
flush = function() | |
if closed then error("attempt to use closed file", 2) end | |
local n = nextFSID | |
delegate:send(makePacket(7, id, string.pack("<BBz", 16 + m, n, path))) | |
delegate:send(makePacket(9, id, string.pack("<BBs4", 0, n, buf))) | |
nextFSID = (nextFSID + 1) % 256 | |
buf, m = "", bit32.bor(m, 2) | |
local d | |
while not d or d:byte(4) ~= n do d = handle.update(delegate:receive()) end | |
local v = string.unpack("z", d, 5) | |
if v ~= "" then error(v, 2) end | |
end, | |
close = function() | |
if closed then error("attempt to use closed file", 2) end | |
closed = true | |
local n = nextFSID | |
delegate:send(makePacket(7, id, string.pack("<BBz", 16 + m, n, path))) | |
delegate:send(makePacket(9, id, string.pack("<BBs4", 0, n, buf))) | |
nextFSID = (nextFSID + 1) % 256 | |
buf, m = "", bit32.bor(m, 2) | |
local d | |
while not d or d:byte(4) ~= n do d = handle.update(delegate:receive()) end | |
local v = string.unpack("z", d, 5) | |
if v ~= "" then error(v, 2) end | |
end | |
} | |
else | |
local n = nextFSID | |
delegate:send(makePacket(7, id, string.pack("<BBz", 16 + m, n, path))) | |
nextFSID = (nextFSID + 1) % 256 | |
local d | |
while not d or d:byte(4) ~= n do d = handle.update(delegate:receive()) end | |
local size = string.unpack("<I4", d, 5) | |
local data = d:sub(9, 8 + size) | |
if d:byte(3) ~= 0 then return nil, data end | |
local pos, closed = 1, false | |
return { | |
read = function(n) | |
expect(1, n, "number", "nil") | |
if closed then error("attempt to use closed file", 2) end | |
if pos >= #data then return nil end | |
if n == nil then | |
if bit32.btest(m, 4) then | |
pos = pos + 1 | |
return data:byte(pos - 1) | |
else n = 1 end | |
end | |
pos = pos + n | |
return data:sub(pos - n, pos - 1) | |
end, | |
readLine = function(strip) | |
if closed then error("attempt to use closed file", 2) end | |
if pos >= #data then return nil end | |
local oldpos, line = pos | |
line, pos = data:match("([^\n]" .. (strip and "+)\n" or "*\n)") .. "()", pos) | |
if not pos then | |
line = data:sub(pos) | |
pos = #data | |
end | |
return line | |
end, | |
readAll = function() | |
if closed then error("attempt to use closed file", 2) end | |
if pos >= #data then return nil end | |
local d = data:sub(pos) | |
pos = #data | |
return d | |
end, | |
close = function() | |
if closed then error("attempt to use closed file", 2) end | |
closed = true | |
end, | |
seek = bit32.btest(m, 4) and function(whence, offset) | |
expect(1, whence, "string", "nil") | |
expect(2, offset, "number", "nil") | |
whence = whence or "cur" | |
offset = offset or 0 | |
if closed then error("attempt to use closed file", 2) end | |
if whence == "set" then pos = offset | |
elseif whence == "cur" then pos = pos + offset | |
elseif whence == "end" then pos = #data - offset | |
else error("Invalid whence", 2) end | |
return pos | |
end or nil | |
} | |
end | |
end | |
} | |
--- Updates the window with the raw message provided. | |
-- @param message A raw message to parse. | |
function handle.update(message) | |
expect(1, message, "string") | |
if message:sub(1, 3) == "!CP" then | |
local off = 8 | |
if message:sub(4, 4) == 'D' then off = 16 end | |
local size = tonumber(message:sub(5, off), 16) | |
local payload = message:sub(off + 1, off + size) | |
local expected = tonumber(message:sub(off + size + 1, off + size + 8), 16) | |
local data = base64decode(payload) | |
if crc32(flags.binaryChecksum and data or payload) == expected then | |
local typ, wid = data:byte(1, 2) | |
if wid == id then | |
if typ == 0 and window then | |
local mode, blink, width, height, cursorX, cursorY, grayscale = string.unpack("<BBHHHHB", data, 3) | |
local c, n, pos = string.unpack("c1B", data, 17) | |
window.setCursorBlink(false) | |
if window.setVisible then window.setVisible(false) end | |
if window.getGraphicsMode and window.getGraphicsMode() ~= mode then window.setGraphicsMode(mode) end | |
window.clear() | |
-- These RLE routines could probably be optimized with string.rep. | |
if mode == 0 then | |
local text = {} | |
for y = 1, height do | |
text[y] = "" | |
for x = 1, width do | |
text[y] = text[y] .. c | |
n = n - 1 | |
if n == 0 then c, n, pos = string.unpack("c1B", data, pos) end | |
end | |
end | |
c = c:byte() | |
for y = 1, height do | |
local fg, bg = "", "" | |
for x = 1, width do | |
fg, bg = fg .. ("%x"):format(bit32.band(c, 0x0F)), bg .. ("%x"):format(bit32.rshift(c, 4)) | |
n = n - 1 | |
if n == 0 then c, n, pos = string.unpack("BB", data, pos) end | |
end | |
window.setCursorPos(1, y) | |
window.blit(text[y], fg, bg) | |
end | |
else | |
local pixels = {} | |
for y = 1, height * 9 do | |
pixels[y] = "" | |
for x = 1, width * 6 do | |
pixels[y] = pixels[y] .. c | |
n = n - 1 | |
if n == 0 then c, n, pos = string.unpack("c1B", data, pos) end | |
end | |
end | |
if window.drawPixels then window.drawPixels(0, 0, pixels) end | |
end | |
pos = pos - 2 | |
local r, g, b | |
if mode ~= 2 then | |
for i = 0, 15 do | |
r, g, b, pos = string.unpack("BBB", data, pos) | |
window.setPaletteColor(2^i, r / 255, g / 255, b / 255) | |
end | |
else | |
for i = 0, 255 do | |
r, g, b, pos = string.unpack("BBB", data, pos) | |
window.setPaletteColor(i, r / 255, g / 255, b / 255) | |
end | |
end | |
window.setCursorBlink(blink ~= 0) | |
window.setCursorPos(cursorX + 1, cursorY + 1) | |
if window.setVisible then window.setVisible(true) end | |
elseif typ == 4 then | |
local flags, _, w, h, title = string.unpack("<BBHHz", data, 3) | |
if flags == 0 then | |
if w ~= 0 and h ~= 0 and window and window.reposition then | |
local x, y = window.getPosition() | |
window.reposition(x, y, w, h) | |
end | |
if delegate.setTitle then delegate:setTitle(title) end | |
elseif flags == 1 or flags == 2 then | |
if not isClosed then | |
delegate:send("\n") | |
if delegate.close then delegate:close() end | |
isClosed = true | |
end | |
end | |
elseif typ == 5 then | |
local flags, title, msg = string.unpack("<Izz", data, 3) | |
local mtyp | |
if bit32.btest(flags, 0x10) then mtyp = "error" | |
elseif bit32.btest(flags, 0x20) then mtyp = "warning" | |
elseif bit32.btest(flags, 0x40) then mtyp = "info" end | |
if delegate.showMessage then delegate:showMessage(mtyp, title, msg) end | |
elseif typ == 8 or typ == 9 then | |
return data | |
end | |
elseif typ == 4 then | |
local flags, _, w, h, title = string.unpack("<BBHHz", data, 3) | |
if flags == 0 and delegate.windowNotification then delegate:windowNotification(wid, w, h, title) end | |
end | |
if typ == 6 then | |
flags.isVersion11 = true | |
local f = string.unpack("<H", data, 3) | |
if bit32.btest(f, 0x01) then flags.binaryChecksum = true end | |
if bit32.btest(f, 0x02) then flags.filesystem = true handle.fs = fsHandle end | |
end | |
end | |
end | |
end | |
--- Sends an event to the server. This functions like os.queueEvent. | |
-- @param ev The name of the event to send. | |
-- @param ... The event parameters. This must not contain any functions, | |
-- coroutines, or userdata. | |
function handle.queueEvent(ev, ...) | |
expect(1, ev, "string") | |
if isClosed then return end | |
local params = table.pack(...) | |
if ev == "key" then delegate:send(makePacket(1, id, string.pack("<BB", keymap_rev[params[1]], bit32.bor(1, params[2] and 2 or 0)))) | |
elseif ev == "key_up" then delegate:send(makePacket(1, id, string.pack("<BB", keymap_rev[params[1]], 0))) | |
elseif ev == "char" then delegate:send(makePacket(1, id, string.pack("<BB", params[1]:byte(), 9))) | |
elseif ev == "mouse_click" then delegate:send(makePacket(2, id, string.pack("<BBII", 0, params[1], params[2], params[3]))) | |
elseif ev == "mouse_up" then delegate:send(makePacket(2, id, string.pack("<BBII", 1, params[1], params[2], params[3]))) | |
elseif ev == "mouse_scroll" then delegate:send(makePacket(2, id, string.pack("<BBII", 2, params[1] < 0 and 0 or 1, params[2], params[3]))) | |
elseif ev == "mouse_drag" then delegate:send(makePacket(2, id, string.pack("<BBII", 3, params[1], params[2], params[3]))) | |
elseif ev == "term_resize" then | |
if window then | |
local w, h = window.getSize() | |
delegate:send(makePacket(4, id, string.pack("<BBHHz", 0, 0, w, h, ""))) | |
end | |
else | |
local s = "" | |
for i = 1, params.n do s = s .. encodeIBT(params[i]) end | |
delegate:send(makePacket(3, id, string.pack("<Bz", params.n, ev) .. s)) | |
end | |
end | |
--- Sends a resize request to the server and resizes the window. | |
-- @param w The width of the window. | |
-- @param h The height of the window. | |
function handle.resize(w, h) | |
expect(1, w, "number") | |
expect(2, h, "number") | |
if window and window.reposition then | |
local x, y = window.getPosition() | |
window.reposition(x, y, w, h) | |
end | |
if isClosed then return end | |
delegate:send(makePacket(4, id, string.pack("<BBHHz", 0, 0, w, h, ""))) | |
end | |
--- Closes the window connection. | |
function handle.close() | |
if isClosed then return end | |
delegate:send(makePacket(4, id, string.pack("<BBHHz", 1, 0, 0, 0, ""))) | |
delegate:send("\n") | |
if delegate.close then delegate:close() end | |
isClosed = true | |
end | |
--- A simple function that sends input events to the server, as well as | |
-- updating the window with messages from the server. | |
function handle.run() | |
parallel.waitForAny(function() while not isClosed do | |
local msg = delegate:receive() | |
if msg == nil then isClosed = true | |
else handle.update(msg) end | |
end end, | |
function() while true do | |
local ev = table.pack(os.pullEventRaw()) | |
if ev[1] == "key" or ev[1] == "key_up" or ev[1] == "char" or | |
ev[1] == "mouse_click" or ev[1] == "mouse_up" or ev[1] == "mouse_scroll" or ev[1] == "mouse_drag" or | |
ev[1] == "paste" or ev[1] == "terminate" or ev[1] == "term_resize" then | |
handle.queueEvent(table.unpack(ev, 1, ev.n)) | |
end | |
end end) | |
end | |
-- This field is normally left empty, but if the remote server supports | |
-- filesystem transfers it becomes a table with various functions for | |
-- accessing the remote filesystem. The functions are a subset of the FS API | |
-- as implemented by the raw mode protocol. | |
handle.fs = nil | |
delegate:send(makePacket(6, id, string.pack("<H", 7))) | |
return handle | |
end | |
local wsDelegate, rednetDelegate = {}, {} | |
wsDelegate.__index, rednetDelegate.__index = wsDelegate, rednetDelegate | |
function wsDelegate:send(data) return self._ws.send(data) end | |
function wsDelegate:receive(timeout) return self._ws.receive(timeout) end | |
function wsDelegate:close() return self._ws.close() end | |
function rednetDelegate:send(data) return rednet.send(self._id, data, self._protocol) end | |
function rednetDelegate:receive(timeout) | |
local tm = os.startTimer(timeout) | |
repeat | |
local ev = {os.pullEvent()} | |
if ev[1] == "rednet_message" and ev[2] == self._id and (not self._protocol or ev[4] == self._protocol) then | |
os.cancelTimer(tm) | |
return ev[3] | |
end | |
until ev[1] == "timer" and ev[2] == tm | |
end | |
--- Creates a basic delegate object that connects to a WebSocket server. | |
-- @param url The URL of the WebSocket to connect to. | |
-- @return The new delegate, or nil on error. | |
-- @return If error, the error message. | |
function rawterm.wsDelegate(url) | |
expect(1, url, "string") | |
local ws, err = http.websocket(url) | |
if not ws then return nil, err end | |
return setmetatable({_ws = ws}, wsDelegate) | |
end | |
--- Creates a basic delegate object that communicates over Rednet. | |
-- @param id The ID of the computer to connect to. | |
-- @param protocol The protocol to communicate over. Defaults to "ccpc_raw_terminal". | |
function rawterm.rednetDelegate(id, protocol) | |
expect(1, id, "number") | |
expect(2, protocol, "string", "nil") | |
return setmetatable({_id = id, _protocol = protocol or "ccpc_raw_terminal"}, rednetDelegate) | |
end | |
return rawterm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local rawterm = require "rawterm" | |
if ... == "server" then | |
local conn, err = rawterm.wsDelegate("ws://127.0.0.1:3000/") | |
if not conn then error(err) end | |
local oldClose = conn.close | |
local isOpen = true | |
function conn:close() isOpen = false return oldClose(self) end | |
local w, h = term.getSize() | |
local win = rawterm.server(conn, w, h, 0, "ComputerCraft Remote Terminal: " .. (os.computerLabel() or ("Computer " .. os.computerID())), term.current()) | |
win.setVisible(false) | |
local oldterm = term.redirect(win) | |
local ok, tm | |
ok, err = pcall(parallel.waitForAny, function() | |
local coro = coroutine.create(shell.run) | |
local ok, filter = coroutine.resume(coro, "shell") | |
while ok and coroutine.status(coro) == "suspended" do | |
local ev = table.pack(win.pullEvent(filter)) | |
if ev[1] ~= "timer" or ev[2] ~= tm then ok, filter = coroutine.resume(coro, table.unpack(ev, 1, ev.n)) end | |
end | |
if not ok then err = filter end | |
end, function() | |
while isOpen do | |
win.setVisible(true) | |
win.setVisible(false) | |
tm = os.startTimer(0.05) | |
repeat local ev, p = os.pullEvent("timer") until p == tm | |
end | |
end) | |
term.redirect(oldterm) | |
win.close() | |
shell.run("clear") | |
if type(err) == "string" then printError(err) end | |
elseif ... == "client" then | |
local conn, err = rawterm.wsDelegate("ws://127.0.0.1:3001/") | |
if not conn then error(err) end | |
local handle = rawterm.client(conn, 0, term.current()) | |
local ok, err = pcall(handle.run) | |
term.current().setVisible(true) | |
handle.close() | |
shell.run("clear") | |
if not ok then printError(err) end | |
else error("Unknown option " .. ...) end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment