· 4 years ago · Aug 05, 2021, 10:42 PM
1package.preload["argparse"] = function(...)
2 local function errorf(msg, ...)
3 error(msg:format(...), 0)
4 end
5 local function setter(arg, result, value)
6 result[arg.name] = value or true
7 end
8 local parser = { __name = "ArgParser" }
9 parser.__index = parser
10 function parser:add(names, arg)
11 if type(names) == "string" then names = { names } end
12 arg.names = names
13 for i = 1, #names do
14 local name = names[i]
15 if name:sub(1, 2) == "--" then self.options[name:sub(3)] = arg
16 elseif name:sub(1, 1) == "-" then self.flags[name:sub(2)] = arg
17 else self.arguments[#self.arguments + 1] = arg; arg.argument = true end
18 end
19 table.insert(self.list, #self.list, arg)
20 if arg.action == nil then arg.action = setter end
21 if arg.required == nil then arg.required = names[1]:sub(1, 1) ~= "-" end
22 if arg.name == nil then arg.name = names[1]:gsub("^-+", "") end
23 if arg.mvar == nil then arg.mvar = arg.name:upper() end
24 end
25 function parser:parse(...)
26 local args = table.pack(...)
27 local i, n = 1, #args
28 local arg_idx = 1
29 local result = {}
30 while i <= n do
31 local arg = args[i]
32 i = i + 1
33 if arg:find("^%-%-([^=]+)=(.+)$") then
34 local name, value = arg:match("^%-%-([^=]+)=(.+)$")
35 local arg = self.options[name]
36 if not arg then errorf("Unknown argument %q", name) end
37 if not arg.many and result[arg.name] ~= nil then errorf("%s has already been set", name) end
38 if not arg.value then errorf("%s does not accept a value", name) end
39 arg:action(result, value)
40 elseif arg:find("^%-%-(.*)$") then
41 local name = arg:match("^%-%-(.*)$")
42 local arg = self.options[name]
43 if not arg then errorf("Unknown argument %q", name) end
44 if not arg.many and result[arg.name] ~= nil then errorf("%s has already been set", name) end
45 if arg.value then
46 local value = args[i]
47 i = i + 1
48 if not value then errorf("%s needs a value", name) end
49 arg:action(result, value)
50 else
51 arg:action(result)
52 end
53 elseif arg:find("^%-(.+)$") then
54 local flags = arg:match("^%-(.+)$")
55 for j = 1, #flags do
56 local name = flags:sub(j, j)
57 local arg = self.flags[name]
58 if not arg then errorf("Unknown argument %q", name) end
59 if not arg.many and result[arg.name] ~= nil then errorf("%s has already been set", name) end
60 if arg.value then
61 local value
62 if j == #flags then
63 value = args[i]
64 i = i + 1
65 else
66 value = flags:sub(j + 1)
67 end
68 if not value then errorf("%s expects a value", name) end
69 arg:action(result, value)
70 break
71 else
72 arg:action(result)
73 end
74 end
75 else
76 local argument = self.arguments[arg_idx]
77 if argument then
78 argument:action(result, arg)
79 arg_idx = arg_idx + 1
80 else
81 errorf("Unexpected argument %q", arg)
82 end
83 end
84 end
85 for i = 1, #self.list do
86 local arg = self.list[i]
87 if arg and arg.required and result[arg.name] == nil then
88 errorf("%s is required (use -h to see usage)", arg.name)
89 end
90 end
91 return result
92 end
93 local function get_usage(arg)
94 local name
95 if arg.argument then name = arg.mvar
96 elseif arg.value then name = arg.names[1] .. "=" .. arg.mvar
97 else name = arg.names[1]
98 end
99 if #arg.names > 1 then name = name .. "," .. table.concat(arg.names, ",", 2) end
100 return name
101 end
102 local function create(prefix)
103 local parser = setmetatable({
104 options = {},
105 flags = {},
106 arguments = {},
107 list = {},
108 }, parser)
109 parser:add({ "-h", "--help", "-?" }, {
110 value = false, required = false,
111 doc = "Show this help message",
112 action = function()
113 if prefix then print(prefix) print() end
114 print("USAGE")
115 local max = 0
116 for i = 1, #parser.list do max = math.max(max, #get_usage(parser.list[i])) end
117 local format = " %-" .. max .. "s %s"
118 for i = 1, #parser.list do
119 local arg = parser.list[i]
120 print(format:format(get_usage(arg), arg.doc or ""))
121 end
122 error("", 0)
123 end,
124 })
125 return parser
126 end
127 local function is_help(cmd)
128 return cmd == "help" or cmd == "--help" or cmd == "-h" or cmd == "-?"
129 end
130 return { create = create, is_help = is_help }
131end
132package.preload["framebuffer"] = function(...)
133 local stringify = require("json").stringify
134 local colour_lookup = {}
135 for i = 0, 15 do
136 colour_lookup[2 ^ i] = string.format("%x", i)
137 end
138 local void = function() end
139 local function empty(colour, width, height)
140 local function is_colour() return colour end
141 return {
142 write = void, blit = void, clear = void, clearLine = void,
143 setCursorPos = void, setCursorBlink = void,
144 setPaletteColour = void, setPaletteColor = void,
145 setTextColour = void, setTextColor = void, setBackgroundColour = void, setBackgroundColor = void,
146 getTextColour = void, getTextColor = void, getBackgroundColour = void, getBackgroundColor = void,
147 scroll = void,
148 isColour = is_colour, isColor = is_colour,
149 getSize = function() return width, height end,
150 getPaletteColour = term.native().getPaletteColour, getPaletteColor = term.native().getPaletteColor,
151 }
152 end
153 local function buffer(original)
154 local text = {}
155 local text_colour = {}
156 local back_colour = {}
157 local palette = {}
158 local palette_24 = {}
159 local cursor_x, cursor_y = 1, 1
160 local cursor_blink = false
161 local cur_text_colour = "0"
162 local cur_back_colour = "f"
163 local sizeX, sizeY = original.getSize()
164 local color = original.isColor()
165 local dirty = false
166 local redirect = {}
167 if original.getPaletteColour then
168 for i = 0, 15 do
169 local c = 2 ^ i
170 palette[c] = { original.getPaletteColour( c ) }
171 palette_24[colour_lookup[c]] = colours.rgb8(original.getPaletteColour( c ))
172 end
173 end
174 function redirect.write(writeText)
175 writeText = tostring(writeText)
176 original.write(writeText)
177 dirty = true
178 if cursor_y > sizeY or cursor_y < 1 or cursor_x + #writeText <= 1 or cursor_x > sizeX then
179 cursor_x = cursor_x + #writeText
180 return
181 end
182 if cursor_x < 1 then
183 writeText = writeText:sub(-cursor_x + 2)
184 cursor_x = 1
185 elseif cursor_x + #writeText > sizeX then
186 writeText = writeText:sub(1, sizeX - cursor_x + 1)
187 end
188 local lineText = text[cursor_y]
189 local lineColor = text_colour[cursor_y]
190 local lineBack = back_colour[cursor_y]
191 local preStop = cursor_x - 1
192 local preStart = math.min(1, preStop)
193 local postStart = cursor_x + #writeText
194 local postStop = sizeX
195 local sub, rep = string.sub, string.rep
196 text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
197 text_colour[cursor_y] = sub(lineColor, preStart, preStop)..rep(cur_text_colour, #writeText)..sub(lineColor, postStart, postStop)
198 back_colour[cursor_y] = sub(lineBack, preStart, preStop)..rep(cur_back_colour, #writeText)..sub(lineBack, postStart, postStop)
199 cursor_x = cursor_x + #writeText
200 end
201 function redirect.blit(writeText, writeFore, writeBack)
202 original.blit(writeText, writeFore, writeBack)
203 dirty = true
204 if cursor_y > sizeY or cursor_y < 1 or cursor_x + #writeText <= 1 or cursor_x > sizeX then
205 cursor_x = cursor_x + #writeText
206 return
207 end
208 if cursor_x < 1 then
209 writeText = writeText:sub(-cursor_x + 2)
210 writeFore = writeFore:sub(-cursor_x + 2)
211 writeBack = writeBack:sub(-cursor_x + 2)
212 cursor_x = 1
213 elseif cursor_x + #writeText > sizeX then
214 writeText = writeText:sub(1, sizeX - cursor_x + 1)
215 writeFore = writeFore:sub(1, sizeX - cursor_x + 1)
216 writeBack = writeBack:sub(1, sizeX - cursor_x + 1)
217 end
218 local lineText = text[cursor_y]
219 local lineColor = text_colour[cursor_y]
220 local lineBack = back_colour[cursor_y]
221 local preStop = cursor_x - 1
222 local preStart = math.min(1, preStop)
223 local postStart = cursor_x + #writeText
224 local postStop = sizeX
225 local sub = string.sub
226 text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
227 text_colour[cursor_y] = sub(lineColor, preStart, preStop)..writeFore..sub(lineColor, postStart, postStop)
228 back_colour[cursor_y] = sub(lineBack, preStart, preStop)..writeBack..sub(lineBack, postStart, postStop)
229 cursor_x = cursor_x + #writeText
230 end
231 function redirect.clear()
232 for i = 1, sizeY do
233 text[i] = string.rep(" ", sizeX)
234 text_colour[i] = string.rep(cur_text_colour, sizeX)
235 back_colour[i] = string.rep(cur_back_colour, sizeX)
236 end
237 dirty = true
238 return original.clear()
239 end
240 function redirect.clearLine()
241 if cursor_y > sizeY or cursor_y < 1 then
242 return
243 end
244 text[cursor_y] = string.rep(" ", sizeX)
245 text_colour[cursor_y] = string.rep(cur_text_colour, sizeX)
246 back_colour[cursor_y] = string.rep(cur_back_colour, sizeX)
247 dirty = true
248 return original.clearLine()
249 end
250 function redirect.getCursorPos()
251 return cursor_x, cursor_y
252 end
253 function redirect.setCursorPos(x, y)
254 if type(x) ~= "number" then error("bad argument #1 (expected number, got " .. type(x) .. ")", 2) end
255 if type(y) ~= "number" then error("bad argument #2 (expected number, got " .. type(y) .. ")", 2) end
256 if x ~= cursor_x or y ~= cursor_y then
257 cursor_x = math.floor(x)
258 cursor_y = math.floor(y)
259 dirty = true
260 end
261 return original.setCursorPos(x, y)
262 end
263 function redirect.setCursorBlink(b)
264 if type(b) ~= "boolean" then error("bad argument #1 (expected boolean, got " .. type(b) .. ")", 2) end
265 if cursor_blink ~= b then
266 cursor_blink = b
267 dirty = true
268 end
269 return original.setCursorBlink(b)
270 end
271 function redirect.getSize()
272 return sizeX, sizeY
273 end
274 function redirect.scroll(n)
275 if type(n) ~= "number" then error("bad argument #1 (expected number, got " .. type(n) .. ")", 2) end
276 local empty_text = string.rep(" ", sizeX)
277 local empty_text_colour = string.rep(cur_text_colour, sizeX)
278 local empty_back_colour = string.rep(cur_back_colour, sizeX)
279 if n > 0 then
280 for i = 1, sizeY do
281 text[i] = text[i + n] or empty_text
282 text_colour[i] = text_colour[i + n] or empty_text_colour
283 back_colour[i] = back_colour[i + n] or empty_back_colour
284 end
285 elseif n < 0 then
286 for i = sizeY, 1, -1 do
287 text[i] = text[i + n] or empty_text
288 text_colour[i] = text_colour[i + n] or empty_text_colour
289 back_colour[i] = back_colour[i + n] or empty_back_colour
290 end
291 end
292 dirty = true
293 return original.scroll(n)
294 end
295 function redirect.setTextColour(clr)
296 if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
297 local new_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
298 if new_colour ~= cur_text_colour then
299 dirty = true
300 cur_text_colour = new_colour
301 end
302 return original.setTextColour(clr)
303 end
304 redirect.setTextColor = redirect.setTextColour
305 function redirect.setBackgroundColour(clr)
306 if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
307 local new_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
308 if new_colour ~= cur_back_colour then
309 dirty = true
310 cur_back_colour = new_colour
311 end
312 return original.setBackgroundColour(clr)
313 end
314 redirect.setBackgroundColor = redirect.setBackgroundColour
315 function redirect.isColour()
316 return color == true
317 end
318 redirect.isColor = redirect.isColour
319 function redirect.getTextColour()
320 return 2 ^ tonumber(cur_text_colour, 16)
321 end
322 redirect.getTextColor = redirect.getTextColour
323 function redirect.getBackgroundColour()
324 return 2 ^ tonumber(cur_back_colour, 16)
325 end
326 redirect.getBackgroundColor = redirect.getBackgroundColour
327 if original.getPaletteColour then
328 function redirect.setPaletteColour(colour, r, g, b)
329 local palcol = palette[colour]
330 if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
331 if type(r) == "number" and g == nil and b == nil then
332 palcol[1], palcol[2], palcol[3] = colours.rgb8(r)
333 palette_24[colour_lookup[colour]] = r
334 else
335 if type(r) ~= "number" then error("bad argument #2 (expected number, got " .. type(r) .. ")", 2) end
336 if type(g) ~= "number" then error("bad argument #3 (expected number, got " .. type(g) .. ")", 2) end
337 if type(b) ~= "number" then error("bad argument #4 (expected number, got " .. type(b ) .. ")", 2 ) end
338 palcol[1], palcol[2], palcol[3] = r, g, b
339 palette_24[colour_lookup[colour]] = colours.rgb8(r, g, b)
340 end
341 dirty = true
342 return original.setPaletteColour(colour, r, g, b)
343 end
344 redirect.setPaletteColor = redirect.setPaletteColour
345 function redirect.getPaletteColour(colour)
346 local palcol = palette[colour]
347 if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
348 return palcol[1], palcol[2], palcol[3]
349 end
350 redirect.getPaletteColor = redirect.getPaletteColour
351 end
352 function redirect.is_dirty() return dirty end
353 function redirect.clear_dirty() dirty = false end
354 function redirect.serialise()
355 return stringify {
356 packet = 0x10,
357 width = sizeX, height = sizeY,
358 cursorX = cursor_x, cursorY = cursor_y, cursorBlink = cursor_blink,
359 curFore = cur_text_colour, curBack = cur_back_colour,
360 palette = palette_24,
361 text = text, fore = text_colour, back = back_colour
362 }
363 end
364 redirect.setCursorPos(1, 1)
365 redirect.setBackgroundColor(colours.black)
366 redirect.setTextColor(colours.white)
367 redirect.clear()
368 return redirect
369 end
370 return { buffer = buffer, empty = empty }
371end
372package.preload["encode"] = function(...)
373 local function fletcher_32(str)
374 local s1, s2, byte = 0, 0, string.byte
375 if #str % 2 ~= 0 then str = str .. "\0" end
376 for i = 1, #str, 2 do
377 local c1, c2 = byte(str, i, i + 1)
378 s1 = (s1 + c1 + (c2 * 0x100)) % 0xFFFF
379 s2 = (s2 + s1) % 0xFFFF
380 end
381 return s2 * 0x10000 + s1
382 end
383 return {
384 fletcher_32 = fletcher_32
385 }
386end
387package.preload["json"] = function(...)
388 local tonumber = tonumber
389 local function skip_delim(str, pos, delim, err_if_missing)
390 pos = pos + #str:match('^%s*', pos)
391 if str:sub(pos, pos) ~= delim then
392 if err_if_missing then error('Expected ' .. delim) end
393 return pos, false
394 end
395 return pos + 1, true
396 end
397 local esc_map = { b = '\b', f = '\f', n = '\n', r = '\r', t = '\t' }
398 local function parse_str_val(str, pos)
399 local out, n = {}, 0
400 if pos > #str then error("Malformed JSON (in string)") end
401 while true do
402 local c = str:sub(pos, pos)
403 if c == '"' then return table.concat(out, "", 1, n), pos + 1 end
404 n = n + 1
405 if c == '\\' then
406 local nextc = str:sub(pos + 1, pos + 1)
407 if not nextc then error("Malformed JSON (in string)") end
408 if nextc == "u" then
409 local num = tonumber(str:sub(pos + 2, pos + 5), 16)
410 if not num then error("Malformed JSON (in unicode string) ") end
411 if num <= 255 then
412 pos, out[n] = pos + 6, string.char(num)
413 else
414 pos, out[n] = pos + 6, "?"
415 end
416 else
417 pos, out[n] = pos + 2, esc_map[nextc] or nextc
418 end
419 else
420 pos, out[n] = pos + 1, c
421 end
422 end
423 end
424 local function parse_num_val(str, pos)
425 local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
426 local val = tonumber(num_str)
427 if not val then error('Error parsing number at position ' .. pos .. '.') end
428 return val, pos + #num_str
429 end
430 local null = {}
431 local literals = {['true'] = true, ['false'] = false, ['null'] = null }
432 local escapes = {}
433 for i = 0, 255 do
434 local c = string.char(i)
435 if i >= 32 and i <= 126
436 then escapes[c] = c
437 else escapes[c] = ("\\u00%02x"):format(i)
438 end
439 end
440 escapes["\t"], escapes["\n"], escapes["\r"], escapes["\""], escapes["\\"] = "\\t", "\\n", "\\r", "\\\"", "\\\\"
441 local function parse(str, pos, end_delim)
442 pos = pos or 1
443 if pos > #str then error('Reached unexpected end of input.') end
444 local pos = pos + #str:match('^%s*', pos)
445 local first = str:sub(pos, pos)
446 if first == '{' then
447 local obj, key, delim_found = {}, true, true
448 pos = pos + 1
449 while true do
450 key, pos = parse(str, pos, '}')
451 if key == nil then return obj, pos end
452 if not delim_found then error('Comma missing between object items.') end
453 pos = skip_delim(str, pos, ':', true)
454 obj[key], pos = parse(str, pos)
455 pos, delim_found = skip_delim(str, pos, ',')
456 end
457 elseif first == '[' then
458 local arr, val, delim_found = {}, true, true
459 pos = pos + 1
460 while true do
461 val, pos = parse(str, pos, ']')
462 if val == nil then return arr, pos end
463 if not delim_found then error('Comma missing between array items.') end
464 arr[#arr + 1] = val
465 pos, delim_found = skip_delim(str, pos, ',')
466 end
467 elseif first == '"' then
468 return parse_str_val(str, pos + 1)
469 elseif first == '-' or first:match('%d') then
470 return parse_num_val(str, pos)
471 elseif first == end_delim then
472 return nil, pos + 1
473 else
474 for lit_str, lit_val in pairs(literals) do
475 local lit_end = pos + #lit_str - 1
476 if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end
477 end
478 local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)
479 error('Invalid json syntax starting at ' .. pos_info_str)
480 end
481 end
482 local format, gsub, tostring, pairs, next, type, concat
483 = string.format, string.gsub, tostring, pairs, next, type, table.concat
484 local function stringify_impl(t, out, n)
485 local ty = type(t)
486 if ty == "table" then
487 local first_ty = type(next(t))
488 if first_ty == "nil" then
489 out[n], n = "{}", n + 1
490 return n
491 elseif first_ty == "string" then
492 out[n], n = "{", n + 1
493 local first = true
494 for k, v in pairs(t) do
495 if first then first = false else out[n], n = ",", n + 1 end
496 out[n] = format("\"%s\":", k)
497 n = stringify_impl(v, out, n + 1)
498 end
499 out[n], n = "}", n + 1
500 return n
501 elseif first_ty == "number" then
502 out[n], n = "[", n + 1
503 for i = 1, #t do
504 if i > 1 then out[n], n = ",", n + 1 end
505 n = stringify_impl(t[i], out, n)
506 end
507 out[n], n = "]", n + 1
508 return n
509 else
510 error("Cannot serialize key " .. first_ty)
511 end
512 elseif ty == "string" then
513 if t:match("^[ -~]*$") then
514 out[n], n = gsub(format("%q", t), "\n", "n"), n + 1
515 else
516 out[n], n = "\"" .. gsub(t, ".", escapes) .. "\"", n + 1
517 end
518 return n
519 elseif ty == "number" or ty == "boolean" then
520 out[n],n = tostring(t), n + 1
521 return n
522 else error("Cannot serialize type " .. ty)
523 end
524 end
525 local function stringify(object)
526 local buffer = {}
527 local n = stringify_impl(object, buffer, 1)
528 return concat(buffer, "", 1, n - 1)
529 end
530 local function try_parse(msg)
531 local ok, res = pcall(parse, msg)
532 if ok then return res else return nil, res end
533 end
534 return {
535 stringify = stringify,
536 try_parse = try_parse,
537 parse = parse,
538 null = null
539 }
540end
541local tonumber, type, keys = tonumber, type, keys
542local argparse = require "argparse"
543local framebuffer = require "framebuffer"
544local encode = require "encode"
545local json = require "json"
546if _G.cloud_catcher then
547 local usage = ([[
548 cloud: <subcommand> [args]
549 Communicate with the cloud-catcher session.
550 Subcommands:
551 edit <file> Open a file on the remote server.
552 token Display the token for this
553 connection.
554 ]]):gsub("^%s+", ""):gsub("%s+$", ""):gsub("\n ", "\n")
555 local subcommand = ...
556 if subcommand == "edit" or subcommand == "e" then
557 local arguments = argparse.create("cloud edit: Edit a file in the remote viewer")
558 arguments:add({ "file" }, { doc = "The file to upload", required = true })
559 local result = arguments:parse(select(2, ...))
560 local file = result.file
561 local resolved = shell.resolve(file)
562 if not fs.exists(resolved) and not resolved:find("%.") then
563 local extension = settings.get("edit.default_extension", "")
564 if extension ~= "" and type(extension) == "string" then
565 resolved = resolved .. "." .. extension
566 end
567 end
568 if fs.isDir(resolved) then error(("%q is a directory"):format(file), 0) end
569 if fs.isReadOnly(resolved) then
570 if fs.exists(resolved) then
571 print(("%q is read only, will not be able to modify"):format(file))
572 else
573 error(("%q does not exist"):format(file), 0)
574 end
575 end
576 local ok, err = _G.cloud_catcher.edit(resolved)
577 if not ok then error(err, 0) end
578 elseif subcommand == "token" or subcommand == "t" then
579 print(_G.cloud_catcher.token())
580 elseif argparse.is_help(subcommand) then
581 print(usage)
582 elseif subcommand == nil then
583 printError(usage)
584 error()
585 else
586 error(("%q is not a cloud catcher subcommand, run with --h for more info"):format(subcommand), 0)
587 end
588 return
589end
590local current_path = shell.getRunningProgram()
591local current_name = fs.getName(current_path)
592local arguments = argparse.create(current_name .. ": Interact with this computer remotely")
593arguments:add({ "token" }, { doc = "The token to use when connecting" })
594arguments:add({ "--term", "-t" }, { value = true, doc = "Terminal dimensions or none to hide" })
595arguments:add({ "--dir", "-d" }, { value = true, doc = "The directory to sync to. Defaults to the current one." })
596arguments:add({ "--http", "-H" }, { value = false, doc = "Use HTTP instead of HTTPs" })
597local args = arguments:parse(...)
598local token = args.token
599if #token ~= 32 or token:find("[^%a%d]") then
600 error("Invalid token (must be 32 alpha-numeric characters)", 0)
601end
602local capabilities = {}
603local term_opts = args.term
604local previous_term, parent_term = term.current()
605if term_opts == nil then
606 parent_term = previous_term
607else if term_opts == "none" then
608 parent_term = nil
609elseif term_opts == "hide" then
610 parent_term = framebuffer.empty(true, term.getSize())
611elseif term_opts:find("^(%d+)x(%d+)$") then
612 local w, h = term_opts:match("^(%d+)x(%d+)$")
613 if w == 0 or h == 0 then error("Terminal cannot have 0 size", 0) end
614 parent_term = framebuffer.empty(true, tonumber(w), tonumber(h))
615else
616 error("Unknown format for term: expected \"none\", \"hide\" or \"wxh\"", 0)
617 end
618end
619if parent_term then
620 table.insert(capabilities, "terminal:host")
621 local w, h = parent_term.getSize()
622 if w * h > 5000 then error("Terminal is too large to handle", 0) end
623end
624local sync_dir = shell.resolve(args.dir or "./")
625if not fs.isDir(sync_dir) then error(("%q is not a directory"):format(sync_dir), 0) end
626table.insert(capabilities, "file:host")
627local url = ("%s://cloud-catcher.squiddev.cc/connect?id=%s&capabilities=%s"):format(
628 args.http and "ws" or "wss", token, table.concat(capabilities, ","))
629local remote, err = http.websocket(url)
630if not remote then error("Cannot connect to cloud-catcher server: " .. err, 0) end
631local server_term, server_file_edit, server_file_host = false, false, false
632do
633 local max_packet_size = 16384
634 _G.cloud_catcher = {
635 token = function() return token end,
636 edit = function(file, force)
637 if not server_file_edit then
638 return false, "There are no editors connected"
639 end
640 local contents, exists
641 local handle = fs.open(file, "rb")
642 if handle then
643 contents = handle.readAll()
644 handle.close()
645 exists = true
646 else
647 contents = ""
648 exists = false
649 end
650 if #file + #contents + 5 > max_packet_size then
651 return false, "This file is too large to be edited remotely"
652 end
653 local check = encode.fletcher_32(contents)
654 local flag = 0x04
655 if fs.isReadOnly(file) then flag = flag + 0x01 end
656 if not exists then flag = flag + 0x08 end
657 remote.send(json.stringify {
658 packet = 0x22,
659 id = 0,
660 actions = {
661 { file = file, checksum = check, flags = flag, action = 0, contents = contents }
662 }
663 })
664 return true
665 end
666 }
667 shell.setAlias("cloud", "/" .. current_path)
668 local function complete_multi(text, options)
669 local results = {}
670 for i = 1, #options do
671 local option, add_spaces = options[i][1], options[i][2]
672 if #option + (add_spaces and 1 or 0) > #text and option:sub(1, #text) == text then
673 local result = option:sub(#text + 1)
674 if add_spaces then table.insert( results, result .. " " )
675 else table.insert( results, result )
676 end
677 end
678 end
679 return results
680 end
681 local subcommands = { { "edit", true }, { "token", false } }
682 shell.setCompletionFunction(current_path, function(shell, index, text, previous_text)
683 if _G.cloud_catcher == nil then return end
684 if index == 1 then
685 return complete_multi(text, subcommands)
686 elseif index == 2 and previous_text[2] == "edit" then
687 return fs.complete(text, shell.dir(), true, false)
688 end
689 end)
690end
691local co, buffer
692if parent_term ~= nil then
693 buffer = framebuffer.buffer(parent_term)
694 co = coroutine.create(shell.run)
695 term.redirect(buffer)
696end
697local info_dirty, last_label, get_label = true, nil, os.getComputerLabel
698local function send_info()
699 last_label = get_label()
700 info_dirty = false
701 remote.send(json.stringify {
702 packet = 0x12,
703 id = os.getComputerID(),
704 label = last_label,
705 })
706end
707local ok, res = true
708if co then ok, res = coroutine.resume(co, "shell") end
709local last_change, last_timer = os.clock(), nil
710local pending_events, pending_n = {}, 0
711local function push_event(event)
712 pending_n = pending_n + 1
713 pending_events[pending_n] = event
714end
715while ok and (not co or coroutine.status(co) ~= "dead") do
716 if not info_dirty and last_label ~= get_label() then info_dirty = true end
717 if server_term and last_timer == nil and (buffer.is_dirty() or info_dirty) then
718 local now = os.clock()
719 if now - last_change < 0.04 then
720 last_timer = os.startTimer(0)
721 else
722 last_change = os.clock()
723 if buffer.is_dirty() then
724 remote.send(buffer.serialise())
725 buffer.clear_dirty()
726 end
727 if info_dirty then send_info() end
728 end
729 end
730 local event
731 if pending_n >= 1 then
732 event = table.remove(pending_events, 1)
733 pending_n = pending_n - 1
734 else
735 event = table.pack(coroutine.yield())
736 end
737 if event[1] == "timer" and event[2] == last_timer then
738 last_timer = nil
739 if server_term then
740 last_change = os.clock()
741 if buffer.is_dirty() then remote.send(buffer.serialise()) buffer.clear_dirty() end
742 if info_dirty then send_info() end
743 end
744 elseif event[1] == "websocket_closed" and event[2] == url then
745 ok, res = false, "Connection lost"
746 remote = nil
747 elseif event[1] == "websocket_message" and event[2] == url then
748 local packet = json.try_parse(event[3])
749 local code = packet and packet.packet
750 if type(code) ~= "number" then code = - 1 end
751 if code >= 0x00 and code < 0x10 then
752 if code == 0x00 then -- ConnectionUpdate
753 server_term, server_file_edit, server_file_host = false, false, false
754 for _, cap in ipairs(packet.capabilities) do
755 if cap == "terminal:view" and buffer ~= nil then
756 server_term = true
757 remote.send(buffer.serialise()) buffer.clear_dirty()
758 send_info()
759 last_change = os.clock()
760 elseif cap == "file:host" then
761 server_file_host = true
762 elseif cap == "file:edit" then
763 server_file_edit = true
764 end
765 end
766 elseif code == 0x02 then -- ConnectionPing
767 remote.send([[{"packet":2}]])
768 end
769 elseif server_term and code >= 0x10 and code < 0x20 then
770 if code == 0x11 then -- TerminalEvents
771 for _, event in ipairs(packet.events) do
772 if event.name == "cloud_catcher_key" then
773 local key = keys[event.args[1]]
774 if type(key) == "number" then push_event { n = 3, "key", key, event.args[2] } end
775 elseif event.name == "cloud_catcher_key_up" then
776 local key = keys[event.args[1]]
777 if type(key) == "number" then push_event { n = 2, "key_up", key } end
778 else
779 push_event(table.pack(event.name, table.unpack(event.args)))
780 end
781 end
782 end
783 elseif code >= 0x20 and code < 0x30 then
784 if code == 0x22 then -- FileAction
785 local result = {}
786 for i, action in pairs(packet.actions) do
787 local ok = bit32.band(action.flags, 0x1) == 1
788 local expected_checksum = 0
789 local handle = fs.open(action.file, "rb")
790 if handle then
791 local contents = handle.readAll()
792 handle.close()
793 expected_checksum = encode.fletcher_32(contents)
794 end
795 if not ok then
796 ok = expected_checksum == 0 or action.checksum == expected_checksum
797 end
798 if not ok then
799 result[i] = { file = action.file, checksum = expected_checksum, result = 2 }
800 elseif action.action == 0x0 then -- Replace
801 handle = fs.open(action.file, "wb")
802 if handle then
803 handle.write(action.contents)
804 handle.close()
805 result[i] = { file = action.file, checksum = encode.fletcher_32(action.contents), result = 1 }
806 else
807 result[i] = { file = action.file, checksum = expected_checksum, result = 3 }
808 end
809 elseif action.action == 0x1 then -- Patch
810 handle = fs.open(action.file, "rb")
811 if handle then
812 local out, n = {}, 0
813 for _, delta in pairs(action.delta) do
814 if delta.kind == 0 then -- Same
815 n = n + 1
816 out[n] = handle.read(delta.length)
817 elseif delta.kind == 1 then -- Added
818 n = n + 1
819 out[n] = delta.contents
820 elseif delta.kind == 2 then -- Removed
821 handle.read(delta.length)
822 end
823 end
824 handle.close()
825 handle = fs.open(action.file, "wb")
826 if handle then
827 local contents = table.concat(out)
828 handle.write(contents)
829 handle.close()
830 result[i] = { file = action.file, checksum = encode.fletcher_32(contents), result = 1 }
831 else
832 result[i] = { file = action.file, checksum = expected_checksum, result = 3 }
833 end
834 else
835 result[i] = { file = action.file, checksum = expected_checksum, result = 2 }
836 end
837 elseif action.action == 0x02 then -- Delete
838 local ok = fs.delete(action.file)
839 result[i] = { file = action.file, checksum = action.checksum, result = ok and 1 or 3 }
840 end
841 end
842 remote.send(json.stringify {
843 packet = 0x23,
844 id = packet.id,
845 files = result,
846 })
847 end
848 end
849 elseif res == nil or event[1] == res or event[1] == "terminate" then
850 if co then
851 ok, res = coroutine.resume(co, table.unpack(event, 1, event.n))
852 elseif event[1] == "terminate" then
853 ok, res = false, "Terminated"
854 end
855 end
856end
857term.redirect(previous_term)
858if previous_term == parent_term then
859 term.clear()
860 term.setCursorPos(1, 1)
861 if previous_term.endPrivateMode then previous_term.endPrivateMode() end
862end
863_G.cloud_catcher = nil
864shell.clearAlias("cloud")
865shell.getCompletionInfo()[current_path] = nil
866if remote ~= nil then remote.close() end
867if not ok then error(res, 0) end
868