· 8 years ago · Jun 05, 2018, 08:24 PM
1home/bin/cbrowse.lua0000600000175000017500000015002512562645047011653 0ustar -----------------------------------------------------
2--name : bin/cbrowse.lua
3--description: lua command line with advanced value display
4--author : mpmxyz
5--github page: https://github.com/mpmxyz/ocprograms
6--forum page : http://oc.cil.li/index.php?/topic/576-cbrowse-inspecting-lua-components-and-other-objects/
7-----------------------------------------------------
8--[[
9
10
11TABLES:
12/br_reactor/
13Key | |Value
14type| value |ID|
15----+------------+--+----+
16nil | | a|nil |
17bool|true | b|...
18num |123.5 | c|
19strg|"abcdef" | d|...
20"st"|"Long Text >| e|
21func|f0123 | f|...
22usrd|u0123 | g|
23thrd|t0123 | h|...
24tabl|t123456 | i|
25tabl|{a=t123, >| j|
26----+------------+--+
27>
28
29FUNCTIONS CALLS/ COMMANDS:
30/br_reactor/=func(a, b, c)
31ID|type| value
32--+----+
33 a|nil |
34 b|bool|true
35 c|num |123.5
36 d|strg|"abcdef"
37 e|strg|"Long Text >
38 f|func|f0123
39 g|usrd|u0123
40 h|thrd|T0123
41 i|tabl|t123
42 j|tabl|t123
43 k|tabl|t123
44--+----+-------------
45>
46
47STRINGS:
48/test/longstring
49abcdef
50adaea
51asese
52...
53--mark string with special foreground/ background color
54--add special "line break" character
55
56
57NUMBERS:
58/test/number
590
600.105135
610x0
62
63
64
65
66COMMAND LINE:
67 1st: <cmd> -> dostring(cmd:gsub("^=" , "return "))
68 2nd: "sh <cmd>" -> shell.execute(cmd, env)
69
70for lua code:
71 local environment
72 default values
73 _G: global_environment
74 _K: list of keys, accessed by string id (middle column) or row number
75 _V: list of values
76 _OBJ: current object
77 _REG: list of known values, accessed by automatic identifier (e.g. t1234 for a table)
78 - >there is an automaticly updated registry (how to save memory with big strings? which types are registered?)
79 _unpack(t): table.unpack(t, 1, table.maxn(t))
80 metatable
81 __newindex = if __index- value existing then apply there else apply at object end
82 __index = 1st: default values, 2nd: object, 3rd: global_environment
83 stdio, event code
84 redirect to special view, when used
85 ideas to name the 'path':
86 if the value belongs to a string index, use the string index instead of a generic name
87 if it was a direct call of a function with a string index, use this index as a name
88
89for shell code:
90 stdio and event redirect as with lua code
91 use shell.execute(command_line, environment)
92CLICK ON OBJECT:
93 LEFT:
94 add object reference to command
95 RIGHT:
96 execute for non- functions
97 set command line to = func(* ), where * is the cursor
98
99
100
101
102
103DIMENSIONS:
104(width - 4(separators) - 2(ID)) / 2 - 1,4,8(type)
105(50 - 6)/2 - 8 -> 22 - 8 -> 14
106
107FOR LATER:
108 display tables in content table as a serialized string?
109]]
110
111local component = require("component")
112local event = require("event")
113local term = require("term")
114local keyboard = require("keyboard")
115local computer = require("computer")
116local unicode = require("unicode")
117
118local cache = require("mpm.cache")
119local tables = require("mpm.tables")
120local draw_buffer = require("mpm.draw_buffer")
121local component_filter = require("mpm.component_filter")
122local config = require("mpm.config")
123local valuesLib = require("mpm.values")
124
125
126--****DEBUG****
127local function interruptedTraceback(message)
128 if message == "interrupted" or type(message) ~= "string" then
129 return message
130 else
131 --don't add traceback twice - > wrap in table
132 message = debug.traceback(message)
133 return setmetatable({}, {__tostring = function() return message end})
134 end
135end
136local function userError(err)
137 io.stderr:write(err.."\n")
138 error("interrupted", 0)
139end
140local function userAssert(check, err)
141 if not check then
142 userError(err)
143 end
144end
145
146--****CONFIG****
147
148local defaultConfig = [[
149--several lists of type names
150--They are used for the type column.
151dictionaries = {
152 normal = {
153 --type: column header
154 ["type"] = "type ",
155 ["nil"] = "nil ",
156 ["boolean"] = "boolean ",
157 ["number"] = "number ",
158 ["string"] = "string ",
159 ["function"] = "function",
160 ["userdata"] = "userdata",
161 ["thread"] = "thread ",
162 ["table"] = "table ",
163 --"function" and "userdata"
164 width = 8,
165 requiredWidth = 50,
166 },
167 short = {
168 ["type"] = "type",
169 ["nil"] = "nil ",
170 ["boolean"] = "bool",
171 ["number"] = "num ",
172 ["string"] = "strg",
173 ["function"] = "func",
174 ["userdata"] = "usrd",
175 ["thread"] = "thrd",
176 ["table"] = "tabl",
177 width = 4,
178 requiredWidth = 42,
179 },
180 single_char = {
181 ["type"] = "t",
182 ["nil"] = "x",
183 ["boolean"] = "b",
184 ["number"] = "n",
185 ["string"] = "s",
186 ["function"] = "f",
187 ["userdata"] = "u",
188 ["thread"] = "T",
189 ["table"] = "t",
190 width = 1,
191 requiredWidth = 20,
192 },
193}
194colors = {
195 full = {
196 default = 0xFFFFFF,
197 type = {
198 ["nil"] = 0xCCCCCC, --light gray
199 ["number"] = 0x8888FF, --light blue
200 ["boolean"] = 0xFFFFFF, --white
201 ["string"] = 0xFFCC33, --orange
202 ["function"] = 0xFFFF33, --yellow
203 ["thread"] = 0xCC66CC, --purple
204 ["userdata"] = 0xFF6699, --magenta
205 ["table"] = 0x33CC33, --lime
206 },
207 value = {
208 [true] = 0x00FF00,
209 [false] = 0xFF0000,
210 },
211 background = {
212 header = 0x000000,
213 --is alternated to make identifying a line easier
214 content = {
215 [1] = 0x000000,
216 [2] = 0x333333, --gray
217 },
218 command = 0x000000,
219 },
220 requiredDepth = 4,
221 },
222 blackwhite = {
223 default = 0xFFFFFF,
224 type = {},
225 value = {},
226 background = {
227 header = 0x000000,
228 content = {
229 [1] = 0x000000,
230 },
231 command = 0x000000,
232 },
233 requiredDepth = 1,
234 },
235}
236--determines which keys are displayed and how they are sorted
237displayedKeys = {
238 "nil", --I wouldn't expect that, but who knows...
239 "boolean",
240 "string",
241 "function",
242 "userdata",
243 "thread",
244 "table",
245 "number",
246}
247--tells cbrowse which key value pairs it should sort
248sortedKeys = {
249 "string",
250 "number",
251}
252]]
253
254local checkAllTypes = [[
255for _, key in ipairs{"nil", "boolean", "number", "string", "function", "userdata", "thread", "table"} do
256 if value[key] == nil then
257 return false, 'Missing entry \"'..key..'\"!'
258 end
259end
260return true
261]]
262
263--data driven config validation
264local configFormat = {
265 "= type(value.dictionaries) == 'table'",
266 "= type(value.colors) == 'table'",
267 "= type(value.displayedKeys) == 'table'",
268 "= type(value.sortedKeys) == 'table'",
269 --checks the values of the specified keys only
270 --makes 'value' accessible to every check
271 forkeys = {
272 dictionaries = {
273 "= next(value) ~= nil",
274 forpairs = {
275 "= type(value.width) == 'number'",
276 "= type(value.requiredWidth) == 'number'",
277 "= type(value.type) == 'string'",
278 checkAllTypes,
279 forpairs = {
280 "= ({width = true, requiredWidth = true})[key] and ignore() or true",
281 "= type(value) == 'string'",
282 },
283 },
284 },
285 colors = {
286 "= next(value) ~= nil",
287 forpairs = {
288 "= type(value) == 'table'",
289 "= type(value.default) == 'number'",
290 "= type(value.type) == 'table'",
291 "= type(value.value) == 'table'",
292 "= type(value.background) == 'table'",
293 "= type(value.requiredDepth) == 'number'",
294 forkeys = {
295 type = {
296 forpairs = {
297 "= type(key) == 'string'",
298 "= type(value) == 'number'",
299 },
300 },
301 value = {
302 forpairs = {
303 "= type(value) == 'number'",
304 },
305 },
306 background = {
307 "= type(value.header) == 'number'",
308 "= type(value.content) == 'table'",
309 "= type(value.command) == 'number'",
310 forkeys = {
311 content = {
312 "= value[1] ~= nil",
313 foripairs = {
314 "= type(value) == 'number'",
315 },
316 },
317 },
318 },
319 },
320 },
321 },
322 displayedKeys = {
323 foripairs = {
324 "= type(value) == 'string'"
325 },
326 },
327 sortedKeys = {
328 foripairs = {
329 "= type(value) == 'string'"
330 },
331 },
332 },
333}
334
335local CONFIG
336local configDictionary, configColors
337local function loadConfig()
338 CONFIG = config.load("/etc/cbrowse.cfg", configFormat, defaultConfig, _ENV, true)
339
340 configDictionary = cache.wrap(
341 function(availableWidth)
342 local best, bestWidth = nil, 0
343 for _, dict in pairs (CONFIG.dictionaries) do
344 if dict.requiredWidth <= availableWidth then
345 if dict.requiredWidth > bestWidth then
346 best = dict
347 bestWidth = best.requiredWidth
348 end
349 end
350 end
351 return best
352 end
353 )
354 configColors = cache.wrap(
355 function(availableDepth)
356 local best, bestDepth = nil, 0
357 for _, colors in pairs(CONFIG.colors) do
358 if colors.requiredDepth <= availableDepth then
359 if colors.requiredDepth > bestDepth then
360 best = colors
361 bestDepth = best.requiredDepth
362 end
363 end
364 end
365 return best
366 end
367 )
368end
369loadConfig()
370
371local function getValueColor(colors, value)
372 return (value ~= nil) and colors.value[value] or colors.type[type(value)] or colors.default
373end
374local function getTypeColor(colors, typ)
375 return colors.type[typ] or colors.default
376end
377
378
379--****INDEXING****
380local reserved_keywords = {}
381for _, keyword in ipairs{
382 "and", "break", "do", "else", "elseif", "end",
383 "false", "for", "function", "goto", "if", "in",
384 "local", "nil", "not", "or", "repeat", "return",
385 "then", "true", "until", "while",
386 } do
387 reserved_keywords[keyword] = true
388end
389
390local index_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
391--maps number indices to the string indices used for the ID column
392--that way you can access >2500 objects instead of just 100 with 2 characters
393--number - > string, string - > index
394local string_indices = setmetatable({}, {
395 __index = function(t, k)
396 local k_type = type(k)
397 if k_type == "number" and k >= 1 then
398 --number - > string
399 local num = math.floor(k - 1) --1 based indexing...
400 local text = ""
401 repeat
402 local index = (num % #index_chars) + 1
403 num = math.floor(num / #index_chars)
404 text = index_chars:sub(index, index)..text
405 until num <= 0
406 t[text] = k
407 t[k] = text
408 return text
409 elseif k_type == "string" and k ~= "" then
410 --string - > number
411 local num
412 if #k == 1 then
413 --1 based indexing: first string index equals index 1
414 num = index_chars:find(k, 1,true)
415 if num == nil then
416 return
417 end
418 else
419 num = 0
420 for char in k:gmatch(".") do
421 local digitIndex = t[char]
422 if digitIndex == nil then
423 return
424 end
425 num = num * #index_chars + digitIndex - 1
426 end
427 num = num + 1 --1 based indexing...
428 end
429 t[k] = num
430 t[num] = k
431 return num
432 end
433 end,
434})
435
436--****REGISTRY****
437local REG_PREFIXES = {
438 ["function"] = "f",
439 ["userdata"] = "u",
440 ["thread"] = "T",
441 ["table"] = "t",
442}
443--count number of created keys to assure that the same key will always refer to the same value
444local REG_COUNTS = {}
445--the registry table
446--name - > object, object - > name
447--An object is registered when using _REG[object].
448--Entries are removed when the corresponding object is collected by the garbage collector.
449--It depends on the fact that strings are treated as primitive values and
450--therefore are not collected due to weak references.
451local _REG = setmetatable({}, {
452 __index = function(reg, object)
453 local prefix = REG_PREFIXES[type(object)]
454 if prefix then
455 local number = (REG_COUNTS[prefix] or 0) + 1
456 REG_COUNTS[prefix] = number
457 local key = prefix..number
458 reg[key] = object
459 reg[object] = key
460 return key
461 end
462 end,
463 __mode = "kv",
464})
465
466
467local function getDisplayedString(object)
468 if type(object) == "string" then
469 return ("%q"):format(object)
470 else
471 local regKey = _REG[object]
472 if regKey then
473 return regKey
474 else
475 return tostring(object)
476 end
477 end
478end
479
480local identifierPattern = "[%a_][%a%d_]*"
481local function isValidIdentifier(object)
482 if type(object) ~= "string" then
483 return false
484 end
485 return object:match("^"..identifierPattern.."$") ~= nil and not reserved_keywords[object]
486end
487
488local function getDisplayedIndex(object, prefix)
489 if object == nil then
490 return nil
491 end
492 if isValidIdentifier(object) then
493 if prefix then
494 return prefix.."."..object
495 else
496 return object
497 end
498 end
499 prefix = prefix or ""
500 return prefix.."["..getDisplayedString(object).."]"
501end
502
503
504
505--****ENVIRONMENTS****
506--the shared environment used to save values of interest and loaded modules
507local global_environment = setmetatable({}, {
508 __index = function(t, k)
509 if type(k) == "string" then
510 --autoloading modules
511 local ok, module = pcall(require, k)
512 if ok then
513 t[k] = module
514 return module
515 end
516 end
517 end,
518})
519for k, v in pairs(_G) do
520 global_environment[k] = v
521end
522global_environment._G = global_environment
523
524
525
526--wrapper function
527local function read_only(t, text)
528 return setmetatable({}, {
529 __metatable = "read only",
530 __index = t,
531 __newindex = function(t, k,v)
532 error(text:format(k), 3)
533 end,
534 __pairs = function(self)
535 return function(_, k)
536 return next(t, k)
537 end, self, nil
538 end,
539 __ipairs = function(self)
540 return function(_, i)
541 i = i + 1
542 local v = rawget(t, i)
543 if v ~= nil then
544 return i, v
545 end
546 end, self, 0
547 end,
548 __len = function(self)
549 return #t
550 end,
551 })
552end
553
554
555local function localEnvironment(object, keys, values)
556 --This table even overrides the object because you can use this table
557 --to access everything and that is rarely possible with the object you are visiting.
558 local override = read_only({
559 _G = global_environment,
560 _K = keys and read_only(keys , "_K is read only!"),
561 _V = values and read_only(values, "_V is read only!"),
562 _REG = read_only(_REG , "_REG is read only!"),
563 _OBJ = object, --use this value e.g. if you intend to access the object field '_K'
564 }, "_ENV.%s is read only!")
565 --this is a function used to find values for __index and to find it's source for __newindex
566 local function findNonNil(key, raw)
567 local get
568 if raw then
569 get = function(t, k)
570 if type(t) == "table" then
571 --to avoid loading libraries when writing a value to the global environment...
572 return rawget(t, k)
573 else
574 return t[k]
575 end
576 end
577 else
578 get = function(t, k)
579 return t[k]
580 end
581 end
582 --1st: override
583 local value = override[key]
584 if value ~= nil then
585 return value, override, "override"
586 end
587 --2nd: object
588 value = get(object, key)
589 if value ~= nil then
590 return value, object, "_OBJ"
591 end
592 --3rd: global environment
593 value = get(global_environment, key)
594 if value ~= nil then
595 return value, global_environment, "_G"
596 end
597 --no value found: redirect writing access to object
598 return nil, object, "_OBJ"
599 end
600
601 --create an individual environment...
602 local env = setmetatable({}, {
603 __newindex = function(t, k,v)
604 local _, source = findNonNil(k, true)
605 if valuesLib.types_indexable[type(source)] then
606 source[k] = v
607 else
608 --redirect to _G if the object is not indexable (i.e. a string)
609 global_environment[k] = v
610 end
611 end,
612 __index = function(t, k)
613 return (findNonNil(k))
614 end,
615 __pairs = function(t)
616 return coroutine.wrap(function()
617 for k, v in pairs(override) do
618 coroutine.yield(k, v)
619 end
620 for k, v in pairs(object) do
621 if override[k] == nil then
622 coroutine.yield(k, v)
623 end
624 end
625 for k, v in pairs(global_environment) do
626 if override[k] == nil and rawget(object, k) == nil then
627 coroutine.yield(k, v)
628 end
629 end
630 coroutine.yield(nil)
631 end)
632 end,
633 })
634 return env
635end
636
637
638local searchedOverrideNames = {
639 "_OBJ", "_K", "_G",
640}
641
642--findEnvironmentIndex(object, environment) - > key, environment name
643local function findEnvironmentIndex(object, environment)
644 if object == nil then
645 return nil, nil
646 end
647 for _, overrideName in ipairs(searchedOverrideNames) do
648 local sourceList = environment[overrideName]
649 if rawequal(object, sourceList) then
650 return overrideName, "_ENV"
651 end
652 if type(sourceList) == "table" then
653 for k, v in pairs(sourceList) do
654 if rawequal(object, v) then
655 return k, overrideName
656 end
657 end
658 end
659 end
660end
661
662--****OBJECT LOADING****
663local function loadKeyValues(object, fillNil)
664 --Number keys are added last to make large arrays easier to look through.
665 --(non numeric keys first, number keys sorted by numeric value)
666 --That was the first idea. Now it's extended to a general sort by type system...
667 local typeKeys = {}
668 for k, v in pairs(object) do
669 local k_type = type(k)
670 local list = typeKeys[k_type]
671 if list == nil then
672 --no list
673 list = {n = 0}
674 typeKeys[k_type] = list
675 end
676 local n = list.n + 1
677 list.n = n
678 list[n] = k
679 end
680 --sort numbers and strings for better readability
681 for _, k_type in ipairs(CONFIG.sortedKeys) do
682 local keyList = typeKeys[k_type]
683 if keyList then
684 table.sort(keyList)
685 end
686 end
687 --now it's time to assemble the tables...
688 local n = 0
689 local keys, values = {}, {}
690 local lastIndex = 0
691 local function addLast(key, value)
692 n = n + 1
693 keys [n] = key
694 values[n] = value
695 local string_index = string_indices[n]
696 keys [string_index] = key
697 values[string_index] = value
698 end
699 for _, k_type in ipairs(CONFIG.displayedKeys) do
700 local keyList = typeKeys[k_type]
701 if keyList then
702 local nilsBeforeLimit = 1024
703 for _, key in ipairs(keyList) do
704 if fillNil and k_type == "number" then
705 --use fillNil to display nil values between non nil values
706 --That is done for function returns because there are no keys for return values.
707 --Normal tables are not filled because that would make
708 --large sparse arrays unreadable and would use a lot of ressources.
709 --It is filling a maximum of 128 empty indices at once. (and 1024 total)
710 if key - lastIndex <= 129 and nilsBeforeLimit > 0 then
711 for i = lastIndex + 1, key - 1 do
712 --set missing indices
713 addLast(key, nil)
714 nilsBeforeLimit = nilsBeforeLimit - 1
715 if nilsBeforeLimit == 0 then
716 break
717 end
718 end
719 end
720 lastIndex = key
721 end
722 --add key to list
723 addLast(key, object[key])
724 end
725 end
726 end
727 return keys, values, n
728end
729
730local loaders = {
731 ["table"] = function(path, object)
732 local keys, values, length = loadKeyValues(object)
733 local environment = localEnvironment(object, keys, values)
734 return {
735 path = path, --How the path to this object is called. Gives the user some directions.
736 object = object, --the loaded object
737 keys = keys, --list of keys, index(number or string) - > key
738 values = values, --list of values
739 length = length, --number of keys/values, useful if some of them are nil (no ipairs)
740 environment = environment, --used when executing a command
741 typ = "table", --used to get a drawing function
742 }
743 end,
744 ["string"] = function(path, object)
745 local environment = localEnvironment(object)
746 --create an escaped version of the string
747 --[[local text = object:gsub(".", function(old)
748 local byte = old:byte()
749 if byte < 32 and byte ~= 13 and byte ~= 10 then
750 return string.format("\\%03u", byte)
751 end
752 end)]]
753 --separates lines for easier processing
754 local lines = {}
755 local length = 0
756 for line in string.gmatch(object, "([^\r\n]*)\r?\n?") do
757 length = length + 1
758 lines[length] = line
759 end
760 return {
761 path = path,
762 object = object,
763 lines = lines,
764 length = length,
765 environment = environment,
766 typ = "string",
767 }
768 end,
769 ["list"] = function(path, ...)
770 local object = table.pack(...)
771 local length = object.n
772 if length == 0 then
773 return nil
774 end
775 --delete length from list; else it's value would also be shown
776 object.n = nil
777 local keys, values = loadKeyValues(object, true)
778 local environment = localEnvironment(object, nil, values)
779 return {
780 path = path,
781 object = object,
782 values = values,
783 length = length,
784 environment = environment,
785 typ = "list",
786 }
787 end,
788}
789
790local function loadObject(typ, ...)
791 local loader = loaders[typ]
792 local obj = loader and loader(...)
793 return obj
794end
795
796
797--****VIEWS****
798--views are reloaded, when there is a screen width change
799local views = {
800 ["table"] = function(obj, context)
801 local dictionary = context.dictionary
802 local colors = context.colors
803 local content = tables.create{
804 layout = { -dictionary.width, 1, -2, -dictionary.width, 1},
805 alignment = {"l", "la", "r", "l", "la"},
806 empty = " ",
807 index = {"_K", "_K", nil, "_V", "_V"},
808 separator = "|",
809 }
810 for index = 1, obj.length do
811 local key = obj.keys [index]
812 local value = obj.values[index]
813 local bgColor = colors.background.content[((index - 1) % #colors.background.content) + 1]
814 local keyColor = getValueColor(colors, key)
815 local keyTypeColor = getTypeColor(colors, type(key))
816 local valueColor = getValueColor(colors, value)
817 local valueTypeColor = getTypeColor(colors, type(value))
818 content.add{
819 dictionary[type(key)], getDisplayedString(key), string_indices[index], dictionary[type(value)], getDisplayedString(value),
820 foreground = {keyTypeColor, keyColor, nil, valueTypeColor, valueColor},
821 background = bgColor,
822 }
823 end
824 local viewTables = {
825 tables.create{
826 {obj.path},
827 alignment = "la",
828 height = 1,
829 },
830 tables.create{
831 {"Keys", "| |", "Values"},
832 layout = { 1, - 4, 1},
833 alignment = "cl",
834 height = 1,
835 },
836 tables.create{
837 {dictionary.type, "|", "value", "|ID|", dictionary.type, "|", "value"},
838 {"" , "+", "" , "+--+", "" , "+", "",
839 padding = "-",
840 },
841 layout = { -dictionary.width, -1, 1, -4, -dictionary.width, -1, 1},
842 alignment = "l",
843 height = 2,
844 },
845 content,
846 tables.create{
847 {"" , "+", "" , "+--+", "" , "+", ""},
848 layout = { -dictionary.width, -1, 1, -4, -dictionary.width, -1, 1},
849 alignment = "l",
850 padding = "-",
851 height = 1,
852 },
853 }
854 return viewTables, content
855 end,
856 ["list"] = function(obj, context)
857 local dictionary = context.dictionary
858 local colors = context.colors
859 local content = tables.create{
860 layout = { -2, -dictionary.width, 1},
861 empty = " ",
862 index = {nil, "_V", "_V"},
863 separator = "|",
864 alignment = {"r", "l", "la"},
865 }
866 for index = 1, obj.length do
867 local value = obj.object[index]
868 local bgColor = colors.background.content[((index - 1) % #colors.background.content) + 1]
869 local valueColor = getValueColor(colors, value)
870 local valueTypeColor = getTypeColor(colors, type(value))
871 content.add{
872 string_indices[index], dictionary[type(value)], getDisplayedString(value),
873 foreground = {nil, valueTypeColor, valueColor},
874 background = bgColor,
875 }
876 end
877 local viewTables = {
878 tables.create{
879 {obj.path},
880 alignment = "la",
881 height = 1,
882 },
883 tables.create{
884 {"ID|", dictionary.type, "|", "value"},
885 {"--+", "" , "+", "",
886 padding = "-",
887 },
888 layout = { -3, -dictionary.width, -1, 1},
889 alignment = "l",
890 height = 2,
891 },
892 content,
893 tables.create{
894 {"--+", "","+",""},
895 layout = { -3, -dictionary.width, -1, 1},
896 padding = "-",
897 alignment = "l",
898 height = 1,
899 },
900 }
901 return viewTables, content
902 end,
903 ["string"] = function(obj, context)
904 local colors = context.colors
905 local typeColor = getTypeColor(colors, "string")
906 local content = tables.create{
907 alignment = "l",
908 layout = {1, -1},
909 foreground = {typeColor, 0xFF0000},
910 background = bgcolor,
911 }
912 local index = 1
913 for _, line in ipairs(obj.lines) do
914 --line wrapping
915 local writtenPerStep = math.max(context.width - 1, 1)
916 local lineLength = unicode.len(line)
917 for fromIndex = 1, lineLength, writtenPerStep do
918 local bgColor = colors.background.content[((index - 1) % #colors.background.content) + 1]
919 part = unicode.sub(line, fromIndex, fromIndex + writtenPerStep - 1)
920 content.add{
921 part, (fromIndex + writtenPerStep < lineLength) and ">" or " ",
922 }
923 index = index + 1
924 end
925 end
926 local viewTables = {
927 tables.create{
928 {obj.path},
929 {"",
930 padding = "-",
931 },
932 alignment = "la",
933 height = 2,
934 },
935 content,
936 tables.create{
937 {""},
938 padding = "-",
939 alignment = "l",
940 height = 1,
941 },
942 }
943 return viewTables, content
944 end,
945}
946local function initView(obj, context)
947 local viewLoader = views[obj.typ]
948 if not viewLoader then
949 return nil
950 end
951 ---common loading code
952 local view = {}
953 local viewTables, content = viewLoader(obj, context)
954 --positioning of tables
955 local totalHeight = 0
956 for _, tab in ipairs(viewTables) do
957 totalHeight = totalHeight + (tab.height or 0)
958 end
959 content.height = context.height - totalHeight
960 --calculate scrolling boundary
961 view.maxScrollY = math.max(#content - content.height, 0)
962 local y = 1
963 for _, tab in ipairs(viewTables) do
964 tab.y = y
965 y = y + tab.height
966 end
967 --on initialization: draw everything
968 function view.draw(scrollY)
969 local gpu = context.gpu
970 gpu.dirty()
971 for _, tab in ipairs(viewTables) do
972 tab.draw(gpu, 1,tab.y, context.width, tab.height, tab == content and scrollY or 0)
973 end
974 gpu.setForeground(context.colors.default)
975 gpu.setBackground(context.colors.background.command)
976 gpu.flush(true)
977 end
978 --else: only things that could have changed (content)
979 function view.update(scrollY)
980 local gpu = context.gpu
981 gpu.dirty()
982 content.draw(gpu, 1,content.y, context.width, content.height, scrollY)
983 gpu.setForeground(context.colors.default)
984 gpu.setBackground(context.colors.background.command)
985 gpu.flush(true)
986 end
987 --ensures that the scrolling position remains in the range it is allowed to be
988 function view.clipScrolling(scrollY)
989 return math.max(math.min(view.maxScrollY, scrollY), 0)
990 end
991 --updates the table callbacks used for scrolling long strings
992 function view.updateScrollingCallbacks()
993 local gpu = context.gpu
994 gpu.dirty()
995 for _, tab in ipairs(viewTables) do
996 tab.updateScrollingCallbacks()
997 end
998 gpu.setForeground(context.colors.default)
999 gpu.setBackground(context.colors.background.command)
1000 gpu.flush(true)
1001 end
1002 if content.index then
1003 --returns a lua code representing the clicked object when used with the object environment
1004 function view.getClicked(x, y,scrollY)
1005 --determine the clicked column
1006 local column = content.getColumn(x - 1, context.width)
1007 if column then
1008 --get an index which is always able to reference the clicked object
1009 local env_index = content.index[column]
1010 if env_index then
1011 --determine the clicked row
1012 local row = content.getRow(y - content.y, content.height, scrollY)
1013 if row then
1014 --get the value to check for its type and to see if you can write a nicer reference
1015 local value = obj.environment[env_index][row]
1016 --nice references are only possible for values, not for keys
1017 if env_index == "_V" and obj.keys then
1018 local key = obj.keys[row]
1019 --their key has to be a valid identifier
1020 if isValidIdentifier(key) then
1021 --and this identifier has to work in the environment used by the command line
1022 if obj.environment[key] == value then
1023 return key, type(value)
1024 end
1025 end
1026 end
1027 --if everything fails there is still the possibility for _K.a, _V.b etc.
1028 return env_index.."."..string_indices[row], type(value)
1029 end
1030 end
1031 end
1032 end
1033 end
1034 return view
1035end
1036
1037--****GPU FILTERING****
1038local function catchGPUAccess(...)
1039 --This function executes the given function while monitoring the primary gpu for any access.
1040 --If the primary GPU is used, it resets the screen.
1041 --In that case it will also wait for the user after execution.
1042 --has the screen been modified?
1043 local touched = false
1044 --running command is trying to write to the screen, clean up the mess...
1045 local function touch(invoke, address, ...)
1046 touched = true
1047 --get screen size
1048 local width, height = invoke(address, "getResolution")
1049 --reset the screen
1050 local oldBackground = invoke(address, "getBackground")
1051 invoke(address, "setBackground", 0x000000)
1052 invoke(address, "fill", 1,1, width, height, " ")
1053 invoke(address, "setBackground", oldBackground)
1054 return invoke, address, ...
1055 end
1056
1057 --a list of all method names, which are in one way or another
1058 --interacting with the screen and therefore triggering graphics execution mode
1059 --setResolution does not belong in there, because it is not touching the screen content
1060 local touchingMethods = {
1061 --these commands modify
1062 set = true,
1063 fill = true,
1064 bind = true,
1065 get = true,
1066 }
1067 --checks if the current access is doing anything important with the gpu
1068 local function check(invoke, address, method, ...)
1069 if not touched and component.isPrimary(address) then
1070 local f = touchingMethods[method]
1071 if f then
1072 if type(f) == "function" then
1073 --currently unused, could be used to give gpu.get
1074 --a fake output when the screen wasn't touched
1075 return f(invoke, address, method, ...)
1076 else
1077 --we've got a relevant access, reset screen and remember to wait after execution
1078 touch(invoke, address)
1079 end
1080 end
1081 end
1082 return invoke(address, method, ...)
1083 end
1084 local filters = {
1085 gpu = check,
1086 }
1087 --reads a given part of the screen and saves the contents
1088 local function readFrontbuffer(gpu, x,y, width)
1089 --the data structure used to store the old screen content
1090 local data = {
1091 characters = {},
1092 foreground = {},
1093 background = {},
1094 }
1095 local maxX, maxY = gpu.getResolution()
1096 if y > maxY then
1097 --boundary check: failed
1098 return data
1099 end
1100 for i = 1, width do
1101 if x > maxX then
1102 --boundary check: failed
1103 break
1104 end
1105 --get content on position (x, y) and remember it
1106 local char, fg,bg = gpu.get(x, y)
1107 data.characters[i] = char
1108 data.foreground[i] = fg
1109 data.background[i] = bg
1110 --move on
1111 x = x + 1
1112 end
1113 return data
1114 end
1115 --writes the given screen contents back to the screen
1116 local function writeFrontbuffer(gpu, x,y, data)
1117 local maxX, maxY = gpu.getResolution()
1118 if y > maxY then
1119 --boundary check: failed
1120 return
1121 end
1122 for i, char in ipairs(data.characters) do
1123 if x > maxX then
1124 --boundary check: failed
1125 return
1126 end
1127 --setting colors
1128 local fg = data.foreground[i]
1129 if fg then
1130 gpu.setForeground(fg)
1131 end
1132 local bg = data.background[i]
1133 if bg then
1134 gpu.setBackground(bg)
1135 end
1136 --drawing character
1137 gpu.set(x, y,char)
1138 --moving on
1139 x = x + 1
1140 end
1141 end
1142 --if graphics execution mode is active: wait until the user finished reading
1143 local function waitForKeyboardIfTouched(...)
1144 if touched then
1145 --display message to user, alternating between original content and message?
1146 local gpu = draw_buffer.new(component.gpu)
1147 local width, height = gpu.getResolution()
1148 local msg = "Press any key..."
1149 local originalContent = readFrontbuffer(gpu, 1, height, unicode.len(msg))
1150 local showMessage = true
1151 local timerID = event.timer(1.5, function()
1152 local width, height = gpu.getResolution()
1153 if showMessage then
1154 --draw message
1155 gpu.setForeground(0xFFFFFF)
1156 gpu.setBackground(0x000000)
1157 gpu.set(1, height, msg)
1158 else
1159 --restore original screen content
1160 writeFrontbuffer(gpu, 1,height, originalContent)
1161 end
1162 gpu.flush()
1163 showMessage = not showMessage
1164 end, math.huge)
1165 --Don't crash here! It would leave an annoying timer alive.
1166 pcall(function()
1167 --clear events
1168 os.sleep(0.1)
1169 --wait for key press event
1170 event.pull("key_down")
1171 event.pull("key_up")
1172 end)
1173 --cleanup
1174 writeFrontbuffer(gpu, 1, height, originalContent)
1175 gpu.flush()
1176 event.cancel(timerID)
1177 end
1178 return ...
1179 end
1180 return waitForKeyboardIfTouched(component_filter.call(filters, ...))
1181end
1182
1183--****COMMAND LINE****
1184local shell = require("shell")
1185local function runCommand(cmd, environment)
1186 if cmd:match("^sh ") then
1187 --shell
1188 cmd = cmd:gsub("^sh ", "")
1189 return catchGPUAccess(shell.execute, cmd, environment)
1190 else
1191 --lua
1192 cmd = cmd:gsub("^=","return ")
1193 local func, err = load(cmd, nil, "t", environment)
1194 if func then
1195 return catchGPUAccess(xpcall, func, debug.traceback)
1196 else
1197 return false, err
1198 end
1199 end
1200end
1201
1202
1203--****BROWSING****
1204local browseValue, browseList
1205local function browse(typ, pathName, ...)
1206 local loadedObject, view, context
1207 local scrollY = 0
1208 local scrollStep = 1
1209 --marks that a context has been changed
1210 local contextDirty, objectDirty = false, false
1211
1212 local function pressCtrlC()
1213 if event.shouldSoftInterrupt then
1214 --newer OC versions
1215 --(incompatible to old version due to timeouts causing errors)
1216 computer.pushSignal("interrupted", 0.0)
1217 else
1218 --older OC versions
1219 --(faking player input due to missing an "interrupted" event)
1220 computer.pushSignal("key_down", component.keyboard.address, 0, keyboard.keys.lcontrol)
1221 computer.pushSignal("key_down", component.keyboard.address, 99,keyboard.keys.c)
1222 computer.pushSignal("key_up" , component.keyboard.address, 99,keyboard.keys.c)
1223 computer.pushSignal("key_up" , component.keyboard.address, 0, keyboard.keys.lcontrol)
1224 end
1225 end
1226 local function forceContextReset()
1227 if not contextDirty then
1228 --remember the context after term.read fails
1229 contextDirty = true
1230 --send Ctrl + C
1231 pressCtrlC()
1232 end
1233 end
1234 local function forceObjectReload()
1235 if not objectDirty then
1236 --remember the context after term.read fails
1237 objectDirty = true
1238 --send Ctrl + C
1239 pressCtrlC()
1240 end
1241 end
1242 --loaded on context changes...
1243 local function resetContext()
1244 --wait for a gpu and a screen
1245 while not term.isAvailable() do
1246 event.pull("term_available")
1247 end
1248 --acquire data
1249 local gpu = component.gpu
1250 local width, height = gpu.getResolution()
1251 local depth = gpu.getDepth()
1252 --anything less doesn't make sense and could lead to errors
1253 userAssert(width >= 20 and height >= 7, "20x7 resolution required!")
1254 context = {
1255 gpu = draw_buffer.new(gpu),
1256 dictionary = configDictionary(width),
1257 colors = configColors(depth),
1258 width = width,
1259 height = height - 1,
1260 }
1261 userAssert(context.dictionary, "No dictionary found!")
1262 scrollStep = math.max(math.floor((height - 6) / 2), 1)
1263 view = initView(loadedObject, context)
1264 scrollY = view.clipScrolling(scrollY)
1265 view.draw(scrollY)
1266 gpu.fill(1, height, width, 1, " ")
1267 return true
1268 end
1269 local function reload(...)
1270 loadedObject = loadObject(typ, pathName or "/", ...)
1271 if loadedObject == nil then
1272 return false
1273 end
1274 resetContext()
1275 return true
1276 end
1277 --initialization, return if there is nothing to display
1278 if not reload(...) then
1279 return false
1280 end
1281 --sets the main variables to nil to reduce memory usage during recursion
1282 local function clean()
1283 loadedObject = nil --could be changed by command
1284 context = nil --could be changed by command (e.g. setResolution(...))
1285 view = nil --depends on the other two parts
1286 end
1287 ---registering event listeners
1288 local function scroll(dy)
1289 local oldScrollY = scrollY
1290 scrollY = view.clipScrolling(scrollY + dy)
1291 if scrollY ~= oldScrollY then
1292 view.update(scrollY)
1293 end
1294 end
1295 --list of listener functions
1296 local listeners = {
1297 screen_resized = function(_, address, width, height)
1298 if context.gpu.getScreen() == address then
1299 forceContextReset()
1300 end
1301 end,
1302 key_down = function(_, address, char, code, player)
1303 if component.isPrimary(address) then
1304 local keys = keyboard.keys
1305 if code == keys.pageUp then
1306 scroll(-scrollStep)
1307 elseif code == keys.pageDown then
1308 scroll( scrollStep)
1309 elseif code == keys.f5 then
1310 --F5: reloading object
1311 forceObjectReload()
1312 end
1313 end
1314 end,
1315 scroll = function(_, address, x, y, direction, player)
1316 if context.gpu.getScreen() == address then
1317 scroll(-direction * scrollStep)
1318 end
1319 end,
1320 touch = function(_, address, x, y, button, player)
1321 if context.gpu.getScreen() == address then
1322 if view.getClicked then
1323 local clickedIndex, clickedType = view.getClicked(x, y, scrollY)
1324 if clickedIndex then
1325 if button == 0 then
1326 --normal click: just add a reference to the selected object
1327 computer.pushSignal("clipboard", component.keyboard.address, clickedIndex, player)
1328 elseif button == 1 then
1329 --right click: combined functions
1330 if clickedType == "function" then
1331 --types "=index()" and moves cursor between the parenthesis
1332 computer.pushSignal("clipboard", component.keyboard.address, "="..clickedIndex.."()", player)
1333 computer.pushSignal("key_down", component.keyboard.address, 0,keyboard.keys.left, player)
1334 computer.pushSignal("key_up" , component.keyboard.address, 0,keyboard.keys.left, player)
1335 else
1336 --types "=index\n", that should also execute the command
1337 computer.pushSignal("clipboard", component.keyboard.address, "="..clickedIndex.."\n", player)
1338 end
1339 end
1340 end
1341 end
1342 end
1343 end,
1344 }
1345 local function updateScrollingCallbacks()
1346 view.updateScrollingCallbacks()
1347 end
1348 local scrollingTimer
1349 --registering loop
1350 local function listen()
1351 for name, listener in pairs(listeners) do
1352 event.listen(name, listener)
1353 end
1354 if scrollingTimer then
1355 event.cancel(scrollingTimer)
1356 end
1357 scrollingTimer = event.timer(1.5, updateScrollingCallbacks, math.huge)
1358 end
1359 --cleanup code
1360 local function ignore()
1361 for name, listener in pairs(listeners) do
1362 event.ignore(name, listener)
1363 end
1364 if scrollingTimer then
1365 event.cancel(scrollingTimer)
1366 scrollingTimer = nil
1367 end
1368 end
1369 --recursion part
1370 local function checkRecursion(cmd, ok,...)
1371 local nvalues = select("#", ...)
1372 local path = pathName or ""
1373 if nvalues > 0 then
1374 --get new path
1375 cmd = cmd:match("^[^\r\n]*")
1376 if nvalues == 1 then
1377 local index, environmentName = findEnvironmentIndex(..., loadedObject.environment)
1378 if environmentName == "_OBJ" then
1379 environmentName = nil
1380 end
1381 if type(index) == "string" then
1382 --limitting the displayed index size
1383 index = index:match("^[^\r\n]*")
1384 if unicode.len(index) > 32 then
1385 index = unicode.sub(index, 1, 32)
1386 end
1387 end
1388 path = path .. "/" .. (getDisplayedIndex(index, environmentName) or (cmd))
1389 else
1390 path = path .. "/" .. cmd
1391 end
1392 --freeing resources to reduce memory consumption
1393 clean()
1394 --display results
1395 if ok then
1396 if nvalues > 1 or not browseValue(path, ...) then
1397 return browseList(path, ...)
1398 else
1399 return true
1400 end
1401 else
1402 return browseValue(path.." -> Error", ...)
1403 end
1404 end
1405 end
1406 --term.read hints
1407 local function getHint(line, cursor)
1408 if loadedObject.keys == nil then
1409 return nil
1410 end
1411 local firstCode = unicode.sub(line, 1, cursor - 1)
1412 local nextCode = unicode.sub(line, cursor, -1)
1413 local previousCode, searchFilter = firstCode:match("^(.-)("..identifierPattern..")$")
1414 if not previousCode then
1415 --no preexisting identifier part
1416 return nil
1417 end
1418 local parentObject = loadedObject.environment
1419 do
1420 --checking part before dot (current object only)
1421 local prefix, parents = previousCode, {}
1422 while prefix and prefix ~= "" do
1423 local key
1424 prefix, key = prefix:match("^(.-)("..identifierPattern..")%.$")
1425 if key then
1426 parents[#parents + 1] = key
1427 end
1428 end
1429 for i = #parents, 1, -1 do
1430 local key = parents[i]
1431 if not isValidIdentifier(key) then
1432 return nil
1433 end
1434 parentObject = parentObject[key]
1435 if parentObject == nil then
1436 return nil
1437 end
1438 end
1439 end
1440 local list = {}
1441 local searchedLength = unicode.len(searchFilter)
1442 for key in pairs(parentObject) do
1443 if type(key) == "string" and isValidIdentifier(key) then
1444 if unicode.len(key) > searchedLength then
1445 --compare prefix of key to already typed value
1446 if unicode.sub(key, 1, searchedLength) == searchFilter then
1447 table.insert(list, previousCode .. key .. nextCode)
1448 end
1449 end
1450 end
1451 end
1452 table.sort(list)
1453 if list[1] then
1454 return list
1455 else
1456 return nil
1457 end
1458 end
1459 --start event processing
1460 listen()
1461 --history remains local, if you want to reuse code: Use _G!
1462 local history = {}
1463 local ok, err = xpcall(function(...)
1464 while true do
1465 --using pcall for term.read because it has problems with screen resizing
1466 local ok, cmd = pcall(function()
1467 term.setCursor(1, context.height + 1)
1468 return term.read(history, false, getHint)
1469 end)
1470 if not ok then
1471 if cmd == "interrupted" then
1472 error("interrupted", 0)
1473 else
1474 --an error happened in term.read, most likely due to a screen size change
1475 contextDirty = true
1476 end
1477 end
1478
1479 if #history > 20 then
1480 --limit history size
1481 table.remove(history, 1)
1482 end
1483 if cmd == nil and not term.isAvailable() then
1484 contextDirty = true
1485 end
1486 if objectDirty then
1487 --reload object
1488 reload(...)
1489 contextDirty, objectDirty = false, false
1490 elseif contextDirty then
1491 --reload context
1492 resetContext()
1493 contextDirty = false
1494 elseif cmd ~= nil then
1495 --disable event processing because we are leaving this object for a minute...
1496 ignore()
1497 --prepare term: move cursor to top left corner, disable cursor blink
1498 pcall(term.setCursor, 1,1)
1499 pcall(term.setCursorBlink, false)
1500 --reset colors
1501 context.gpu.setForeground(0xFFFFFF)
1502 context.gpu.setForeground(0x000000)
1503 --calling
1504 checkRecursion(cmd, runCommand(cmd, loadedObject.environment))
1505 --ignore automatic context reloading...
1506 event.pull(0.1, "screen_resized")
1507 --due to possible side effects and because term.read shifts everything upwards: always reload everything
1508 reload(...)
1509 --reenable event processing for this object
1510 listen()
1511 else
1512 --Strg/Ctrl + C - > go up
1513 return
1514 end
1515 end
1516 end, interruptedTraceback, ...)
1517 --end event processing
1518 ignore()
1519 if ok then
1520 --everything ok, return
1521 return true
1522 else
1523 --forward error without modification
1524 error(err, 0)
1525 end
1526end
1527browseValue = function(pathName, value)
1528 return browse(type(value), pathName, value)
1529end
1530browseList = function(pathName, ...)
1531 return browse("list", pathName, ...)
1532end
1533
1534--****MAIN****
1535
1536local parameters, options = shell.parse(...)
1537--option: don't load proxies and libraries unless told to do so
1538local doListing = (not options.clean)
1539local doEventListening = not options.noevent
1540
1541
1542--**LOAD PARAMETERS**
1543local parameterValues = {}
1544
1545local function loadParameter(name, index)
1546 if type(name) ~= "string" then
1547 --use raw value if it isn't a string
1548 return name
1549 end
1550 local address = component.get(name)
1551 if address ~= nil then
1552 --1st: component address
1553 return component.proxy(address)
1554 elseif component.isAvailable(name) then
1555 --2nd: component type
1556 return component.getPrimary(name)
1557 else
1558 --3rd: libraries
1559 local ok, lib = pcall(require, name)
1560 if ok then
1561 return lib
1562 else
1563 --4th: run command
1564 local function packResults(ok, ...)
1565 if ok then
1566 local list = table.pack(...)
1567 if list.n > 0 then
1568 return list
1569 end
1570 else
1571 return "No component or library found; error when executing as code: " .. (...)
1572 end
1573 end
1574 return packResults(runCommand(name, global_environment))
1575 end
1576 end
1577end
1578
1579if options.env then
1580 --"--env" uses the first parameter as a replacement to the global environment
1581 local newEnv = parameters[1]
1582 if not options.raw then
1583 --loading from text
1584 newEnv = loadParameter(newEnv, 1)
1585 end
1586 assert(type(newEnv) == "table", "Expected an environment table as first parameter!")
1587 --replacing the global environment
1588 global_environment = newEnv
1589 --removing the first parameter
1590 local newParameters = {}
1591 for i, v in pairs(parameters) do
1592 if i > 1 then
1593 newParameters[i - 1] = v
1594 end
1595 end
1596 parameters = newParameters
1597end
1598
1599
1600if options.raw then
1601 for i, value in pairs(parameters) do
1602 parameterValues[i] = value
1603 end
1604else
1605 for i, name in ipairs(parameters) do
1606 parameterValues[name] = loadParameter(name, i)
1607 end
1608end
1609
1610--**LOAD COMPONENTS AND LIBRARIES**
1611local components
1612local libraries
1613local cleanup
1614if doListing then
1615 --find components
1616 components = component.list()
1617 local primaries = {}
1618 --by address
1619 for k, _ in pairs(components) do
1620 local comp_type = component.type(k)
1621 components[k] = component.proxy(k)
1622 primaries[comp_type] = true
1623 end
1624 --by type
1625 for k, _ in pairs(primaries) do
1626 components[k] = component.getPrimary(k)
1627 end
1628
1629 --find libraries (loaded, preloaded and in lib directory)
1630 local filesystem = require("filesystem")
1631 libraries = {}
1632 --find libraries in files, TODO: use mpm.lib
1633 local libFiles = {}--absolute path -> library (
1634 local function addLibrary(name, path)
1635 local oldName = libFiles[path]
1636 if oldName == nil or #name < #oldName then
1637 libFiles[path] = name
1638 end
1639 end
1640 local function addLibs(path, dir, prefix, ext, subPath, libPrefix)
1641 if path then
1642 dir, prefix, ext, subPath = path:match("^(.-)([^/]*)%?([^/]*)(.-)$")
1643 libPrefix = ""
1644 end
1645 --don't search working dir
1646 if dir and prefix and ext and dir ~= "./" and dir ~= "" then
1647 for file in filesystem.list(dir) do
1648 if file:sub(1, #prefix) == prefix then
1649 if file:sub(-#ext, -1) == ext then
1650 local libname = libPrefix .. file:sub(#prefix + 1, -#ext - 1)
1651 local absolutePath = dir .. file .. subPath:sub(2, -1)
1652 if absolutePath:sub(1, 1) ~= "/" then
1653 absolutePath = fs.concat(os.getenv("PWD") or "/", absolutePath)
1654 end
1655 if filesystem.exists(absolutePath) and not filesystem.isDirectory(absolutePath) then
1656 addLibrary(libname, absolutePath)
1657 end
1658 end
1659 end
1660 if file:sub(-1, -1) == "/" then
1661 --directory: recursion
1662 --(expects "dir" to end with a slash if it isn't empty
1663 addLibs(nil, dir .. file, prefix, ext, subPath, libPrefix .. file:sub(1, -2) .. ".")
1664 end
1665 end
1666 end
1667 end
1668 for path in package.path:gmatch("[^;]+") do
1669 addLibs(path)
1670 end
1671 for path, libname in pairs(libFiles) do
1672 if libraries[libname] == nil then
1673 local ok, lib = pcall(require, libname)
1674 libraries[libname] = lib
1675 end
1676 end
1677 --add preloaded libraries
1678 for libname, loader in pairs(package.preload) do
1679 if libraries[libname] == nil then
1680 local ok, lib = pcall(require, libname)
1681 libraries[libname] = lib
1682 end
1683 end
1684 --add loaded libraries
1685 for libname, library in pairs(package.loaded) do
1686 if libraries[libname] == nil and library ~= false then
1687 libraries[libname] = library
1688 end
1689 end
1690
1691 if doEventListening then
1692 --event listeners for a dynamic component list
1693 local function componentListener(event, key)
1694 if event == "component_added" then
1695 components[key] = component.proxy(key)
1696 elseif event == "component_available" then
1697 components[key] = component.getPrimary(key)
1698 elseif event == "component_removed" then
1699 components[key] = nil
1700 elseif event == "component_unavailable" then
1701 components[key] = nil
1702 end
1703 end
1704 event.listen("component_added", componentListener)
1705 event.listen("component_removed", componentListener)
1706 event.listen("component_available", componentListener)
1707 event.listen("component_unavailable", componentListener)
1708
1709 function cleanup()
1710 event.ignore("component_added", componentListener)
1711 event.ignore("component_removed", componentListener)
1712 event.ignore("component_available", componentListener)
1713 event.ignore("component_unavailable", componentListener)
1714 end
1715 end
1716end
1717
1718--**LAST STEPS TO FINAL ENVIRONMENT**
1719local default_object = {
1720 environment = global_environment,
1721 components = components and read_only(components, "components is read only!"),
1722 libraries = libraries and read_only(libraries, "libraries is read only!"),
1723}
1724local main_object = default_object
1725if #parameters > 0 and next(parameterValues) then
1726 parameterValues["==default=="] = default_object
1727 main_object = parameterValues
1728end
1729
1730--**EXECUTE**
1731
1732local ok, err = xpcall(browseValue, interruptedTraceback, nil, main_object)
1733
1734if cleanup then
1735 cleanup()
1736end
1737
1738if not ok and err ~= "interrupted" then
1739 error(err, 0)
1740end
1741home/lib/mpm/tables.lua0000600000175000017500000002275412562645050012251 0ustar -----------------------------------------------------
1742--name : lib/mpm/tables.lua
1743--description: drawing formatted tables
1744--author : mpmxyz
1745--github page: https://github.com/mpmxyz/ocprograms
1746--forum page : none
1747-----------------------------------------------------
1748
1749local unicode = require("unicode")
1750local values = require("mpm.values")
1751
1752local tables = {}
1753
1754function tables.create(tab)
1755 tab = tab or {}
1756 --layout:
1757 -- dynamic width given with positive weight values
1758 -- constant width given with negative values
1759 function tab.setLayout(newLayout)
1760 tab.layout = newLayout or {1}
1761 tab.totalWeight = 0
1762 tab.sumConstWidth = 0
1763 for i, width in ipairs(tab.layout) do
1764 if width > 0 then
1765 tab.totalWeight = tab.totalWeight + width
1766 elseif width < 0 then
1767 tab.sumConstWidth = tab.sumConstWidth - width
1768 else
1769 error("Zero column width!")
1770 end
1771 end
1772 end
1773 function tab.setColor(foreground, background)
1774 tab.foreground = foreground or 0xFFFFFF
1775 tab.background = background or 0x000000
1776 end
1777 function tab.setAlignment(alignment, padding)
1778 tab.alignment = alignment or "l"
1779 tab.padding = padding or " "
1780 end
1781 function tab.setSeparator(separator)
1782 tab.separator = separator or ""
1783 end
1784 function tab.add(line)
1785 tab[#tab + 1] = line
1786 end
1787 function tab.getColumnWidths(totalWidth)
1788 --calculate column sizes...
1789 local ncolumns = #tab.layout
1790 local variableWidth = totalWidth - tab.sumConstWidth - unicode.len(tab.separator) * (ncolumns - 1)
1791 local widthPerWeight = tab.totalWeight > 0 and (variableWidth / tab.totalWeight) or 0
1792 if widthPerWeight < 0 then
1793 --not enough space to draw...
1794 return nil, tab.sumConstWidth
1795 end
1796 local columnWidths = {}
1797 local carriedWidth = 0
1798 for column, width in ipairs(tab.layout) do
1799 if width > 0 then
1800 local rawWidth = width * widthPerWeight
1801 carriedWidth = carriedWidth + (rawWidth % 1)
1802 if carriedWidth >= 1 then
1803 --if there is a chance for carriedWidth to become 0.999... instead of one
1804 --change carriedWidth >= 1 to carriedWidth >= 0.999
1805 rawWidth = rawWidth + 1
1806 carriedWidth = carriedWidth - 1
1807 end
1808 columnWidths[column] = math.floor(rawWidth)
1809 else
1810 columnWidths[column] = - width
1811 end
1812 end
1813 return columnWidths, tab.sumConstWidth
1814 end
1815 --x_relative is 0 based (x_relative == 0 -> tested x == table x)
1816 function tab.getColumn(x_relative, width)
1817 local columnWidths, minimumWidth = tab.getColumnWidths(width)
1818 if columnWidths == nil then
1819 return nil
1820 end
1821 if x_relative < 0 then
1822 return nil
1823 end
1824 for column, columnWidth in ipairs(columnWidths) do
1825 x_relative = x_relative - columnWidth
1826 if x_relative < 0 then
1827 return column
1828 end
1829 x_relative = x_relative - unicode.len(tab.separator)
1830 end
1831 return nil
1832 end
1833 --y_relative is 0 based (y_relative == 0 -> tested x == table x)
1834 function tab.getRow(y_relative, height, scrollY)
1835 if y_relative < 0 or y_relative >= height then
1836 return nil
1837 end
1838 local index = y_relative + scrollY + 1
1839 if tab[index] ~= nil then
1840 return index
1841 end
1842 end
1843 function tab.getFormattedCellContent(line, column, columnWidth)
1844 local cellContent = line[column] or ""
1845 local alignment = values.get(line.alignment, false, column) or values.get(tab.alignment, false, column)
1846 local padding = values.get(line.padding , false, column) or values.get(tab.padding , false, column)
1847
1848 local extendingAlignment, shorteningAlignment = alignment:match("([lrc])([alr]?)")
1849 assert(extendingAlignment, "Invalid Alignment!")
1850 if shorteningAlignment == "" then
1851 assert(extendingAlignment ~= "c", "Can't use 'c' alignment for shortening!")
1852 shorteningAlignment = extendingAlignment
1853 end
1854
1855 local autoScroller
1856 --adjust content to perfectly fit
1857 if unicode.len(cellContent) > columnWidth then
1858 --content too long: shorten it
1859 if shorteningAlignment == "a" then
1860 local originalContent = cellContent
1861 autoScroller = function(gpu, x,y, foreground, background)
1862 local scrollX = 0
1863 local maxScrollX = unicode.len(originalContent) - columnWidth
1864 local scrollXStep = math.min(math.max(math.floor(columnWidth / 2), 1), 5)
1865 return function()
1866 if scrollX < maxScrollX then
1867 scrollX = math.min(scrollX + scrollXStep, maxScrollX)
1868 else
1869 scrollX = 0
1870 end
1871 gpu.setForeground(foreground)
1872 gpu.setBackground(background)
1873 gpu.set(x, y, unicode.sub(originalContent, 1 + scrollX, columnWidth + scrollX))
1874 end
1875 end
1876 end
1877 --shorten it
1878 if shorteningAlignment == "l" or shorteningAlignment == "a" then
1879 cellContent = unicode.sub(cellContent, 1, columnWidth)
1880 elseif shorteningAlignment == "r" then
1881 cellContent = unicode.sub(cellContent, -columnWidth, -1)
1882 end
1883 elseif unicode.len(cellContent) < columnWidth then
1884 --content too large: add padding
1885 local addition = columnWidth - unicode.len(cellContent)
1886 if extendingAlignment == "l" then
1887 cellContent = cellContent .. padding:rep(addition)
1888 elseif extendingAlignment == "r" then
1889 cellContent = padding:rep(addition) .. cellContent
1890 elseif extendingAlignment == "c" then
1891 cellContent = padding:rep(math.floor(addition / 2)) ..
1892 cellContent ..
1893 padding:rep(math.ceil(addition / 2))
1894 end
1895 end
1896 return cellContent, autoScroller
1897 end
1898 function tab.draw(gpu, x, y, width, height, scrollY)
1899 --assuming that a table is never drawn twice, we can delete known scrolling updaters
1900 tab.autoScrollers = {}
1901 --how much space is each column going to have?
1902 local columnWidths, minimumWidth = tab.getColumnWidths(width)
1903 if columnWidths == nil then
1904 gpu.setForeground(tab.foreground)
1905 gpu.setBackground(tab.background)
1906 gpu.fill(x, y,width, height, tab.padding)
1907 local msg = ""
1908 for draw_y = y, y + height - 1 do
1909 if msg == "" then
1910 msg = ("width>"..minimumWidth.."!")
1911 end
1912 gpu.set(x, draw_y, msg:sub(1, width))
1913 msg = msg:sub(width + 1, - 1)
1914 end
1915 return
1916 end
1917 --drawing loop
1918 local separatorSpaces = (" "):rep(unicode.len(tab.separator)) --draw_buffer optimization: connecting spaces
1919 local draw_y = y
1920 scrollY = scrollY or 0
1921 for index = scrollY + 1, scrollY + height do
1922 local line = tab[index]
1923 if line then
1924 local draw_x = x
1925 for column, columnWidth in ipairs(columnWidths) do
1926 if columnWidth > 0 then
1927 --get colors and formatting
1928 local foreground = values.get(line.foreground, false, column) or values.get(tab.foreground, false, column)
1929 local background = values.get(line.background, false, column) or values.get(tab.background, false, column)
1930 gpu.setForeground(foreground)
1931 gpu.setBackground(background)
1932 --gets displayed string and a callback generator
1933 local cellContent, autoScroller = tab.getFormattedCellContent(line, column, columnWidth)
1934 --draw_buffer optimization: create connecting spaces
1935 if columnWidths[column + 1] and tab.separator ~= "" then
1936 cellContent = cellContent .. separatorSpaces
1937 end
1938 --draw
1939 gpu.set(draw_x, draw_y, cellContent)
1940 --remember scrolling callback
1941 if autoScroller then
1942 table.insert(tab.autoScrollers, autoScroller(gpu, draw_x, draw_y, foreground, background))
1943 end
1944 --move right
1945 draw_x = draw_x + columnWidth + unicode.len(tab.separator)
1946 end
1947 end
1948 else
1949 --simple background
1950 local draw_x = x
1951 for column, columnWidth in ipairs(columnWidths) do
1952 gpu.setForeground(values.get(tab.foreground, false, column))
1953 gpu.setBackground(values.get(tab.background, false, column))
1954 local padding = values.get(tab.empty or tab.padding, false, column)
1955 gpu.fill(draw_x, draw_y, columnWidth, height - (draw_y - y), padding)
1956 draw_x = draw_x + columnWidth + unicode.len(tab.separator)
1957 end
1958 break
1959 end
1960 draw_y = draw_y + 1
1961 end
1962 --draw separators last to allow combining rows to a single drawing call
1963 if tab.separator ~= "" then
1964 local draw_x = x
1965 for column, columnWidth in ipairs(columnWidths) do
1966 gpu.setForeground(values.get(tab.foreground, false, column))
1967 gpu.setBackground(values.get(tab.background, false, column))
1968 if column > 1 then
1969 for char in tab.separator:gmatch("[\0-\x7F\xC2-\xF4][\x80-\xBF]*") do
1970 gpu.fill(draw_x, y,1, height, char)
1971 draw_x = draw_x + 1
1972 end
1973 end
1974 draw_x = draw_x + columnWidth
1975 end
1976 end
1977 end
1978 function tab.updateScrollingCallbacks()
1979 if tab.autoScrollers then
1980 for _, callback in ipairs(tab.autoScrollers) do
1981 callback()
1982 end
1983 end
1984 end
1985 --add convenience to the constructor
1986 tab.setLayout(tab.layout)
1987 tab.setColor(tab.foreground, tab.background)
1988 tab.setAlignment(tab.alignment, tab.padding)
1989 tab.setSeparator(tab.separator)
1990 return tab
1991end
1992
1993return tables
1994home/lib/mpm/draw_buffer.lua0000600000175000017500000001766112562645050013266 0ustar -----------------------------------------------------
1995--name : lib/mpm/draw_buffer.lua
1996--description: speeds up drawing by combining multiple drawing calls
1997--author : mpmxyz
1998--github page: https://github.com/mpmxyz/ocprograms
1999--forum page : none
2000-----------------------------------------------------
2001
2002
2003--[[
2004****DRAW BUFFER****
2005--
2006--it's a simple buffer, which only remembers everything that can be combined into one call
2007--speeds up drawing if you draw strings from left to right, without distance in between
2008--(that is often the case with formatted texts if you explicitly draw space characters)
2009]]
2010--returns true if the given text is only made of space characters
2011local function isSpace(text)
2012 return text:match("^ *$") ~= nil
2013end
2014
2015local flushingFunctions = {
2016 bind = true, --prevent drawing after target change
2017 copy = true, --avoid reading old contents
2018 fill = true, --uses a 2D shape, the buffer is 1D only
2019 get = true, --avoid reading old contents
2020 getBackground = true, --avoid reading old contents
2021 getForeground = true, --avoid reading old contents
2022 setResolution = true, --buffer has to be cleared, else it is drawn after resolution change
2023}
2024
2025local draw_buffer = {}
2026--wraps a gpu proxy to buffer its operations for increased speed
2027function draw_buffer.new(gpu)
2028 assert(gpu~=nil,"GPU required!")
2029
2030 --user side object
2031 local object = {}
2032 --internal buffer object
2033 local buffer = {text="",x=1,y=1}
2034 --now following: functions working like their gpu counterparts
2035 --But buffered!
2036 function object.setForeground(foreground,background)
2037 buffer.next_foreground = math.floor(foreground)
2038 end
2039 function object.setBackground(background)
2040 buffer.next_background = math.floor(background)
2041 end
2042 --an axis independent token pasting function
2043 local function insertToken(relative_position, pastedText)
2044 local text = buffer.text
2045 local remove_from = math.max(1 + relative_position,1)
2046 local remove_to = math.max(#pastedText + relative_position,0)
2047 buffer.text = text:sub(1, remove_from - 1) .. pastedText .. text:sub(remove_to + 1, -1)
2048 end
2049 function object.set(x,y,text,vertical)
2050 --ist: prepare arguments
2051 x = math.floor(x)
2052 y = math.floor(y)
2053 text = tostring(text)
2054 local size = #text
2055 local nextSpace = isSpace(text)
2056 if size == 0 then
2057 return true --?
2058 end
2059 if size > 1 then
2060 vertical = (not not vertical) -->force boolean
2061 else
2062 vertical = nil -->don't care
2063 end
2064 local next_foreground
2065 if nextSpace then
2066 next_foreground = nil -->don't care
2067 else
2068 next_foreground = buffer.next_foreground
2069 end
2070 local next_background = buffer.next_background
2071
2072 --2nd: Is flushing necessary?
2073 --check orientation: flush if unequal unless at least one does not care
2074 --(uses a three values logic: nil == "don't care")
2075 if buffer.vertical ~= nil and vertical ~= nil then
2076 if buffer.vertical ~= vertical then
2077 object.flush()
2078 end
2079 end
2080 --check color: flush if unequal unless next_<color> is nil
2081 --next_<color> indicates the color that should be applied to the given text
2082 --The foreground can be ignored for parts only containing space characters.
2083 --->The foreground check can be ignored if either text or the buffer are full of spaces.
2084 --buffer.next_<color> is the color set by the user, nil when not yet set (nil == "not yet set")
2085 --local next_color the same as above, but nil when text does not need it (nil == "don't care")
2086 --buffer.<color> is the color value used by the buffered operation, nil == "don't care" (spaces) or "not yet set" (empty buffer)
2087 --buffer.current_<color> is the color that has been set by gpu.set<color>, it assumes no interference by the user
2088
2089 --foreground: flushing when already set and the next foreground is different
2090 if buffer.foreground and next_foreground then
2091 if buffer.foreground ~= next_foreground then
2092 object.flush()
2093 end
2094 end
2095 --background: flushing when already set and the next background is different
2096 if buffer.background and next_background then
2097 if buffer.background ~= next_background then
2098 object.flush()
2099 end
2100 end
2101 --check position
2102 if #buffer.text > 0 then
2103 local dx = x - buffer.x
2104 local dy = y - buffer.y
2105 --check that there isn't a diagonal connection
2106 if dx ~= 0 and dy ~= 0 then
2107 object.flush()
2108 elseif dx ~= 0 then
2109 --check compatibility with orientation
2110 if buffer.vertical == true or vertical == true then
2111 object.flush()
2112 --check that the next token connects to the buffer
2113 elseif dx < -size or dx > #buffer.text then
2114 object.flush()
2115 end
2116 elseif dy ~= 0 then
2117 --check compatibility with orientation
2118 if buffer.vertical == false or vertical == false then
2119 object.flush()
2120 --check that the next token connects to the buffer
2121 elseif dy < -size or dy > #buffer.text then
2122 object.flush()
2123 end
2124 end
2125 end
2126
2127 --3rd: add information to buffer
2128 if vertical ~= nil then
2129 buffer.vertical = vertical
2130 end
2131 local dx = x - buffer.x
2132 local dy = y - buffer.y
2133 if #buffer.text > 0 and (dx ~= 0 or dy ~= 0) then
2134 if dx ~= 0 then
2135 --force horizontal
2136 buffer.vertical = false
2137 insertToken(dx, text)
2138 buffer.x = math.min(buffer.x, x)
2139 elseif dy ~= 0 then
2140 --force vertical
2141 buffer.vertical = true
2142 insertToken(dy, text)
2143 buffer.y = math.min(buffer.y, y)
2144 end
2145 else
2146 --first entry / overwrite (starting at single character in buffer)
2147 buffer.text = text
2148 buffer.x = x
2149 buffer.y = y
2150 end
2151 --Checks: Only spaces?
2152 buffer.allSpace = buffer.allSpace and nextSpace
2153 --combine color information, the or operation only receives one non nil argument or two equal arguments
2154 buffer.background = buffer.background or next_background
2155 buffer.foreground = buffer.foreground or next_foreground
2156 return true
2157 end
2158 --forces clearing the buffer
2159 function object.flush(all)
2160 if buffer.foreground and buffer.current_foreground ~= buffer.foreground then
2161 gpu.setForeground(buffer.foreground)
2162 --current_<color> remembers the last color set to avoid even more calls
2163 buffer.current_foreground = buffer.foreground
2164 buffer.foreground = nil
2165 end
2166 if buffer.background and buffer.current_background ~= buffer.background then
2167 gpu.setBackground(buffer.background)
2168 buffer.current_background = buffer.background
2169 buffer.background = nil
2170 end
2171 if #buffer.text > 0 then
2172 if buffer.allSpace then
2173 if buffer.vertical then
2174 gpu.fill(buffer.x,buffer.y,1,#buffer.text," ")
2175 else
2176 gpu.fill(buffer.x,buffer.y,#buffer.text,1," ")
2177 end
2178 else
2179 gpu.set(buffer.x,buffer.y,buffer.text,buffer.vertical or false)
2180 end
2181 --clear buffer
2182 buffer.text = ""
2183 buffer.vertical = nil
2184 buffer.allSpace = true
2185 buffer.background = nil
2186 buffer.foreground = nil
2187 end
2188 if all then
2189 --force setting all colors
2190 buffer.foreground = buffer.next_foreground
2191 buffer.background = buffer.next_background
2192 object.flush()
2193 end
2194 end
2195 --to be called when the stored color state might be wrong
2196 function object.dirty()
2197 buffer.current_foreground = nil
2198 buffer.current_background = nil
2199 end
2200 --copy other gpu methods to make this a perfect drop-in replacement
2201 for k,v in pairs(gpu) do
2202 if object[k] == nil then
2203 if flushingFunctions[k] then
2204 object[k] = function(...)
2205 object.flush(true)
2206 return v(...)
2207 end
2208 else
2209 object[k] = v
2210 end
2211 end
2212 end
2213 return object
2214end
2215return draw_buffer
2216home/lib/mpm/component_filter.lua0000600000175000017500000001447512562645050014347 0ustar -----------------------------------------------------
2217--name : lib/mpm/component_filter.lua
2218--description: intercepts component access to enable modifications
2219--author : mpmxyz
2220--github page: https://github.com/mpmxyz/ocprograms
2221--forum page : none
2222-----------------------------------------------------
2223
2224--[[
2225 "component_filter" allows you to apply filters to component methods.
2226 When you 'apply' a list of filters the component.invoke function is replaced. *TODO: UPDATE DESCRIPTION*
2227 When executed this function searches the given list of filters by address and type.
2228 If it finds a filter, it is executed. Else it executes the original component.invoke.
2229 A filter list is a table with keys representing a component address or type and values representing filter functions.
2230 (The address is prioritized when searching.)
2231 There also is the special key "default" to be used when there is no other filter.
2232 A filter is just a function receiving the original component.invoke and all other parameters.
2233 It should return values similar to those of an unfiltered call since they are returned to the calling code.
2234 Else it might be surprised to get colors from component.filesystem.list().
2235 The following example shows a filesystem filter adding a computer.beep on every filesystem access:
2236
2237 local component_filter = require 'component_filter'
2238 local shell = require 'shell'
2239 local component = require 'component'
2240
2241 local filters = {
2242 filesystem = function(invoke, address, method, ...)
2243 --make some noise
2244 component.computer.beep(2000,0.05)
2245 --but still do what it was supposed to do
2246 return invoke(address, method, ...)
2247 end,
2248 }
2249
2250 local function monitoredShell()
2251 shell.execute("sh")
2252 end
2253
2254 component_filter.call(filters,monitoredShell)
2255]]
2256local component_filter = {}
2257--a table only containing stack states
2258local stack = {}
2259--a table containing every state
2260local allStates = {}
2261
2262local component = require("component")
2263local topInvoke = component.invoke
2264--this is the new component.invoke, TODO: how to order multiple component wrappers?
2265function component.invoke(address, method, ...)
2266 return topInvoke(address, method, ...)
2267end
2268
2269
2270--creates an invoke function for the given state
2271local function newInvoke(state)
2272 return function(address, method, ...)
2273 --get filter list
2274 local filters = state.filters
2275 --filters[address] -> filter, filters[type] -> filter, filters.default -> filter
2276 local filter = filters[address] or filters[component.type(address)] or filters.default
2277 if filter then
2278 --filter(originalInvoke, address, method, ...) -> return values
2279 return filter(state.oldInvoke, address, method, ...)
2280 end
2281 --default: just call original function
2282 return state.oldInvoke(address, method, ...)
2283 end
2284end
2285--creates a new state
2286local function newState(filters, onStack, oldInvoke)
2287 --create a table
2288 local state = {
2289 filters = filters,
2290 onStack = onStack,
2291 oldInvoke = oldInvoke,
2292 }
2293 --add a function
2294 state.invoke = newInvoke(state)
2295 --and there it is...
2296 return state
2297end
2298
2299--removes an entry in the complete list,
2300--also takes care of topInvoke changes if they are necessary
2301local function removeIndex(i)
2302 local state = allStates[i]
2303 local nextState = allStates[i+1]
2304 if nextState then
2305 --connect next state with the previous state
2306 nextState.oldInvoke = state.oldInvoke
2307 else
2308 --removed the top most state: update topInvoke
2309 topInvoke = state.oldInvoke
2310 end
2311 table.remove(allStates, i)
2312end
2313--finds a matching state and removes it
2314local function remove(filters, onStack)
2315 --from top to bottom...
2316 for i = #allStates,1,-1 do
2317 --find a state...
2318 local state = allStates[i]
2319 --which is having the same filter...
2320 if state.filters == filters then
2321 --and belongs to the specified part... (stack / non-stack)
2322 if state.onStack == onStack then
2323 --and remove it.
2324 return removeIndex(i)
2325 end
2326 end
2327 end
2328end
2329local function pop()
2330 --removes the state from the top of the stack
2331 local state = table.remove(stack)
2332 assert(state ~= nil, "Error: 'restore' without matching 'apply'.")
2333 --search in complete list...
2334 for i = #allStates,1,-1 do
2335 if allStates[i] == state then
2336 --and eliminate.
2337 return removeIndex(i)
2338 end
2339 end
2340end
2341
2342--adds a state on top of the stack
2343local function push(state)
2344 table.insert(stack, state)
2345end
2346--creates a new state and remembers it, also pushes it on the stack if necessary
2347--Since the new filters are put on top of the other filters
2348--there will be a topInvoke change.
2349
2350local function add(filters, onStack)
2351 local state = newState(filters, onStack, topInvoke)
2352 --remember state
2353 table.insert(allStates,state)
2354 if onStack then
2355 push(state)
2356 end
2357 --modify topInvoke
2358 topInvoke = state.invoke
2359end
2360
2361--component_filter.apply(filters, noStack)
2362--from now on applies the given list of filters to every component.invoke call
2363--It is also filtering every filter created before.
2364--(adding a layer to the stack unless 'noStack' is true)
2365function component_filter.apply(filters, noStack)
2366 assert(type(filters) == "table", "Invalid Filter Type! (Table required.)")
2367 add(filters, not noStack)
2368end
2369--component_filter.restore()
2370--removes the top layer of the stack
2371function component_filter.restore()
2372 pop()
2373end
2374--component_filter.remove()
2375--removes the given filter list
2376--(removing the topmost non-stack filter list equal to the given one)
2377function component_filter.remove(filters)
2378 remove(filters, false)
2379end
2380
2381
2382--This function is used to store the return values "..." while doing some cleanup / error logic.
2383local function removeAndReturn(filters, ok, ...)
2384 remove(filters, false)
2385 if ok then
2386 return ...
2387 else
2388 return error((...),0)
2389 end
2390end
2391
2392--component_filter.call(filters,func,...)
2393--a useful convenience function:
2394--1st: applies the filter
2395--2nd: calls the function
2396--3rd: reverts the first step (Even if step two throws an error!)
2397--4th: returns anything the function returned (errors are forwarded without change)
2398function component_filter.call(filters, ...)
2399 assert(type(filters) == "table", "Invalid Filter Type! (Table required.)")
2400 add(filters, false)
2401 return removeAndReturn(filters, pcall(...))
2402end
2403
2404return component_filter
2405home/lib/mpm/config.lua0000600000175000017500000001302412562645050012232 0ustar -----------------------------------------------------
2406--name : lib/mpm/config.lua
2407--description: simple reading and verification of config files
2408--author : mpmxyz
2409--github page: https://github.com/mpmxyz/ocprograms
2410--forum page : none
2411-----------------------------------------------------
2412
2413local config = {}
2414
2415--adds some boilerplate text
2416local function onError(name, text)
2417 error("Config validation failed; error in '"..name.."': "..text,0)
2418end
2419--
2420local function toIndex(key)
2421 if type(key) == "string" then
2422 if key:match("^[%a_][%a%d_]*$") then
2423 return "."..key
2424 else
2425 return ("[%q]"):format(key)
2426 end
2427 end
2428 return ("[%s]"):format(tostring(key))
2429end
2430
2431local function validator(key, value, format, configName, formatName, functionCache)
2432 if type(format) ~= "table" then
2433 onError(formatName,"Format description needs to be a table!")
2434 end
2435 --ignored -> don't check this part of format
2436 local ignored = false
2437 local function ignore()
2438 ignored = true
2439 return true
2440 end
2441
2442 for i,check in ipairs(format) do
2443 local func, defaultMessage
2444 if type(check) == "function" then
2445 func = check
2446 defaultMessage = "Custom check failed! (No additional information!)"
2447 elseif type(check) == "string" then
2448 func = functionCache[check]
2449 if func == nil then
2450 local errMsg
2451 func,errMsg = load("local key,value,ignore=...;"..check:gsub("^=","return "))
2452 if func then
2453 functionCache[check] = func
2454 else
2455 onError(formatName..toIndex(i),errMsg or "Loading '"..check.."' failed!")
2456 end
2457 end
2458 defaultMessage = "'"..check.."' failed!"
2459 else
2460 onError(formatName..toIndex(i),"Condition needs to be a string or function!")
2461 end
2462
2463 local ok,err = func(key,value,ignore)
2464 if ignored then
2465 --ignoring the rest of the format rule
2466 return
2467 end
2468 if not ok then
2469 onError(configName, err or defaultMessage)
2470 end
2471 end
2472 if format.oneof then
2473 if type(format.oneof) ~= "table" then
2474 onError(formatName,"'oneof' has to be a table!")
2475 end
2476 local oneOK = false
2477 for i,subFormat in ipairs(format.oneof) do
2478 local ok = pcall(validator, key, value, subFormat, configName, formatName..".oneof"..toIndex(i), functionCache)
2479 if ok then
2480 oneOK = true
2481 break
2482 end
2483 end
2484 if not oneOK then
2485 onError(configName,format.oneof.message or "'oneof' check failed!")
2486 end
2487 end
2488 if format.forkeys then
2489 if type(format.forkeys) ~= "table" then
2490 onError(formatName,"'forkeys' has to be a table!")
2491 end
2492 for nextKey,nextFormat in pairs(format.forkeys) do
2493 validator(nextKey, value[nextKey], nextFormat, configName..toIndex(nextKey), formatName..".forkeys"..toIndex(nextKey), functionCache)
2494 end
2495 end
2496 if format.foripairs then
2497 if type(value) ~= "table" then
2498 onError(configName,"Value has to be iterable! (foripairs)")
2499 end
2500 for nextKey,nextValue in ipairs(value) do
2501 validator(nextKey, nextValue, format.foripairs, configName..toIndex(nextKey), formatName..".foripairs", functionCache)
2502 end
2503 end
2504 if format.forpairs then
2505 if type(value) ~= "table" then
2506 onError(configName,"Value has to be iterable! (forpairs)")
2507 end
2508 for nextKey,nextValue in pairs(value) do
2509 validator(nextKey, nextValue, format.forpairs, configName..toIndex(nextKey), formatName..".forpairs", functionCache)
2510 end
2511 end
2512end
2513
2514
2515function config.check(cfg,format)
2516 assert(type(cfg) == "table","'config' isn't a table!")
2517 validator(nil,cfg,format,"config","format",{})
2518end
2519
2520function config.load(file,format,default,env,autoTables)
2521 if default then
2522 local fs = require "filesystem"
2523 --auto generate config
2524 if not fs.exists(file) then
2525 if type(default) == "string" then
2526 --create new file with <default> as content
2527 local stream = io.open(file,"wb")
2528 stream:write(default)
2529 stream:close()
2530 elseif type(default) == "table" then
2531 return checkConfig(default,format)
2532 elseif type(default) == "function" then
2533 return checkConfig(default(file,format),format)
2534 end
2535 end
2536 end
2537
2538 local cfg
2539 local auto_meta, auto_create
2540 if autoTables then
2541 auto_create = function()
2542 local t = setmetatable({},auto_meta)
2543 if type(autoTables) == "table" then
2544 table.insert(autoTables, t)
2545 elseif type(autoTables) == "function" then
2546 autoTables(t)
2547 end
2548 return t
2549 end
2550 auto_meta = {
2551 __index = function(t,k)
2552 local v = auto_create()
2553 t[k] = v
2554 return v
2555 end,
2556 }
2557 cfg = auto_create()
2558 else
2559 cfg = {}
2560 end
2561
2562 local env_meta = {
2563 __index = function(t,k)
2564 --1st: stay "local" -> config
2565 local v = rawget(cfg,k)
2566 if v ~= nil then
2567 return v
2568 end
2569 --2nd: now searching the given environment
2570 if env then
2571 v = env[k]
2572 if v ~= nil then
2573 return v
2574 end
2575 end
2576 --3rd: create new table if cfg has got a metatable
2577 --Using cfg that way avoids the need for special handling of the environment
2578 --if one wants to disable the 'autoTables' feature after loading.
2579 return cfg[k]
2580 end,
2581 --always writing to the configuration table
2582 __newindex = cfg,
2583 }
2584
2585 local func = assert(loadfile(file, "t", setmetatable({}, env_meta)))
2586 func()
2587 if format then
2588 config.check(cfg, format)
2589 end
2590 return cfg
2591end
2592
2593return config
2594home/lib/mpm/cache.lua0000600000175000017500000001004612562645050012031 0ustar -----------------------------------------------------
2595--name : lib/mpm/cache.lua
2596--description: caching functions made easy
2597--author : mpmxyz
2598--github page: https://github.com/mpmxyz/ocprograms
2599--forum page : none
2600-----------------------------------------------------
2601
2602
2603local cache = {}
2604
2605--uncomment this line to enable logging of your caches
2606--This enables you to see how effective your caches are. (hits vs. misses)
2607--Alternatively you can do this assignment in your code as long as it is before you create caches.
2608--cache.debug = {}
2609
2610local function logCache(cache_table, cache_id)
2611 --if debug table is enabled: add cache statistics
2612 local cacheStats = cache.debug.stats
2613 if cacheStats == nil then
2614 --list of cache statistics missing, adding it now
2615 cacheStats = setmetatable({},{
2616 __index = function(cacheStats, key)
2617 --creates a new cache statistic for the given cache id
2618 local stats = {hit=0, miss=0}
2619 cacheStats[key] = stats
2620 return stats
2621 end,
2622 __tostring = function(cacheStats)
2623 --returns a pretty table of the cache statistics
2624 local list = {[0] = {"id","hit","miss","hit/miss"}}
2625 local maxLength = {2, 3, 4, 8}
2626 for key, data in pairs(cacheStats) do
2627 if type(key) == "string" then
2628 --integer index -> sorting keys
2629 list[#list + 1] = key
2630 --string index -> collecting
2631 local row = {key, ("%u"):format(data.hit), ("%u"):format(data.miss), ("%.2f"):format(data.hit / data.miss)}
2632 for i, text in ipairs(row) do
2633 maxLength[i] = math.max(maxLength[i], #text)
2634 end
2635 list[key] = row
2636 end
2637 end
2638 --sort keys by name
2639 table.sort(list)
2640 --adjust widths, create lines
2641 for i = 0, #list do
2642 local key = i>0 and list[i] or 0
2643 local row = list[key]
2644 for i, text in ipairs(row) do
2645 --adjusting width
2646 row[i] = (" "):rep(maxLength[i] - #text) .. text
2647 end
2648 --create line, separate columns by 2 space characters
2649 list[i] = table.concat(row, " ")
2650 end
2651 --connect lines and return
2652 return table.concat(list,"\n",0,#list)
2653 end
2654 })
2655 cache.debug.stats = cacheStats
2656 end
2657 return setmetatable({},{
2658 __index = function(_,k)
2659 local v = rawget(cache_table, k)
2660 local stats = cacheStats[cache_id]
2661 if v ~= nil then
2662 stats.hit = stats.hit + 1
2663 else
2664 stats.miss = stats.miss + 1
2665 v = cache_table[k]
2666 end
2667 return v
2668 end,
2669 __newindex = cache_table,
2670 __call = function(table, key, next, ...)
2671 if next ~= nil then
2672 return table[key](next, ...)
2673 else
2674 return table[key]
2675 end
2676 end,
2677 })
2678end
2679
2680
2681--****EASY CACHING****
2682
2683local cache_registry = setmetatable({},{__mode="k"})
2684local cache_meta = setmetatable({},{
2685 __index = function(t, mode)
2686 local meta = {
2687 --generates missing keys
2688 __index = function(cache_table, key)
2689 local value = cache_registry[cache_table](key)
2690 cache_table[key] = value
2691 return value
2692 end,
2693 __mode = mode,
2694 __call = function(cache_table, key, next, ...)
2695 if next ~= nil then
2696 --there is support for multiple arguments if you stack caches
2697 return cache_table[key](next, ...)
2698 else
2699 return cache_table[key]
2700 end
2701 end,
2702 }
2703 t[mode] = meta
2704 return meta
2705 end,
2706})
2707
2708--cache.wrap(function(key) - > value) - > (table[key] - > value)
2709--returns a cache table as a proxy to the given function
2710function cache.wrap(func, mode, cache_id)
2711 --create cache table
2712 local cache_table = setmetatable({}, cache_meta[mode or ""])
2713 --register cached function
2714 cache_registry[cache_table] = func
2715 --wrap debug table around cache if wanted
2716 if cache.debug and cache_id ~= nil then
2717 cache_table = logCache(cache_table, cache_id)
2718 end
2719 return cache_table
2720end
2721
2722
2723return cache
2724home/lib/mpm/lib.lua0000600000175000017500000000600312617156153011534 0ustar -----------------------------------------------------
2725--name : lib/mpm/lib.lua
2726--description: allows iteration of all files for a given library path
2727--author : mpmxyz
2728--github page: https://github.com/mpmxyz/ocprograms
2729--forum page : none
2730-----------------------------------------------------
2731local filesystem
2732do
2733 local ok
2734 ok, filesystem = pcall(require, "filesystem")
2735 if not ok then
2736 --compatibility as a standalone script without OpenComputers: requires Lua File System
2737 local lfs = require("lfs")
2738 filesystem = {
2739 list = lfs.dir,
2740 exists = function(path)
2741 return lfs.attributes(path) ~= nil
2742 end,
2743 isDirectory = function(path)
2744 local attributes = lfs.attributes(path)
2745 return attributes and (attributes.mode == "directory") or false
2746 end,
2747 concat = function(a, b)
2748 return a .. "/" .. b
2749 end,
2750 }
2751 end
2752end
2753
2754return {
2755 list = function(path, includeWorkingDir, includeDuplicates)
2756 if checkArg then
2757 checkArg(1, path, "string", "nil")
2758 checkArg(2, includeWorkingDir, "boolean", "nil")
2759 checkArg(3, includeDuplicates, "boolean", "nil")
2760 end
2761 path = path or package.path
2762
2763 local knownLibs = {}
2764
2765 local function findLibs(path, dir, prefix, ext, subPath, libPrefix)
2766 if path then
2767 dir, prefix, ext, subPath = path:match("^(.-)([^/]*)%?([^/]*)(.-)$")
2768 libPrefix = ""
2769 if (dir:sub(1, 2) ~= "./" and dir ~= "") and not (filesystem.exists(dir) and filesystem.isDirectory(dir)) then
2770 return
2771 end
2772 end
2773 --don't search working dir
2774 if dir and prefix and ext and ((dir:sub(1, 2) ~= "./" and dir ~= "") or includeWorkingDir) then
2775 for file in filesystem.list(dir) do
2776 if file:sub(1, #prefix) == prefix then
2777 if file:sub(-#ext, -1) == ext then
2778 local libname = libPrefix .. file:sub(#prefix + 1, -#ext - 1)
2779 local absolutePath = dir .. file .. subPath:sub(2, -1)
2780 if absolutePath:sub(1, 1) ~= "/" then
2781 absolutePath = filesystem.concat(os.getenv("PWD") or "", absolutePath)
2782 end
2783 if filesystem.exists(absolutePath) and not filesystem.isDirectory(absolutePath) then
2784 if not knownLibs[libname] then
2785 if not includeDuplicates then
2786 knownLibs[libname] = true
2787 end
2788 coroutine.yield(libname, absolutePath)
2789 end
2790 end
2791 end
2792 end
2793 if file:sub(-1, -1) == "/" then
2794 --directory: recursion
2795 --(expects "dir" to end with a slash if it isn't empty
2796 findLibs(nil, dir .. file, prefix, ext, subPath, libPrefix .. file:sub(1, -2) .. ".")
2797 end
2798 end
2799 end
2800 end
2801
2802 return coroutine.wrap(function()
2803 for path in path:gmatch("[^;]+") do
2804 findLibs(path)
2805 end
2806 coroutine.yield()
2807 end)
2808 end,
2809}
2810home/lib/mpm/values.lua0000600000175000017500000001107712600123221012252 0ustar -----------------------------------------------------
2811--name : lib/mpm/hashset.lua
2812--description: allows using raw values or getters for the same property
2813--author : mpmxyz
2814--github page: https://github.com/mpmxyz/ocprograms
2815--forum page : none
2816-----------------------------------------------------
2817
2818local values = {}
2819--type sets used to extract values
2820values.types_callable = {
2821 ["function"] = true,
2822 --due to component wrappers:
2823 ["table"] = true,
2824}
2825values.types_indexable = {
2826 ["table"] = true,
2827}
2828--values.get(value, forceCall, key, ...) -> value
2829--If the given value is a primitive value, it is simply returned.
2830--If it is a function it is called with the parameters (key, ...) and the first result is returned.
2831--If it is a table it is called as a function if forceCall is true.
2832--Else it will be indexed using key. The other parameters are applied recursively if they aren't nil.
2833function values.get(value, forceCall, key, ...)
2834 local typ = type(value)
2835 if values.types_indexable[typ] and key ~= nil and not forceCall then
2836 value = value[key]
2837 --if (...) ~= nil then
2838 --multidimensional keys: applied recursively
2839 return values.get(value, false, ...)
2840 --end
2841 elseif values.types_callable[typ] then
2842 return (value(key, ...))
2843 end
2844 return value
2845end
2846
2847
2848function values.set(target, value, forceCall, key, ...)
2849 local typ = type(target)
2850 if values.types_indexable[typ] and key ~= nil and not forceCall then
2851 if (...) ~= nil then
2852 --multidimensional keys: applied recursively
2853 return values.set(target[key], value, false, ...)
2854 else
2855 target[key] = value
2856 return true
2857 end
2858 elseif values.types_callable[typ] then
2859 if key == nil then
2860 target(value)
2861 else
2862 --TODO: better format?
2863 target(value, key, ...)
2864 end
2865 return true
2866 end
2867 return false
2868end
2869
2870--type sets used to check value types
2871values.types_number = {
2872 --raw
2873 ["number"] = true,
2874 --via callable or indexable object
2875 ["function"] = true,
2876 ["table"] = true,
2877}
2878values.types_string = {
2879 --raw
2880 ["string"] = true,
2881 --via callable or indexable object
2882 ["function"] = true,
2883 ["table"] = true,
2884}
2885values.types_table = {
2886 --raw
2887 ["table"] = true,
2888 --via callable object
2889 ["function"] = true,
2890}
2891
2892values.types_raw_number = {
2893 --raw only
2894 ["number"] = true,
2895}
2896values.types_raw_string = {
2897 --raw only
2898 ["string"] = true,
2899}
2900values.types_raw_table = {
2901 --raw only
2902 ["table"] = true,
2903}
2904
2905--values.check(value, name, permitted_types, wrongTypeText, default) -> value or default
2906--This function checks if the given value has a valid type. It returns the value or the given default value if the value is nil.
2907--'value' is the value being checked.
2908--'name' is a name used for error descriptions.
2909--'permitted_types' is a table with type strings as keys. All 'true' values mark a valid type.
2910--'wrongTypeText' is a string appended to the name to get the error message if the value type isn't valid.
2911--'default' is used as the output value if the input value is nil. Throws an error if both the value and 'default' are nil.
2912function values.check(value, name, permittedTypes, wrongTypeText, default)
2913 if not permittedTypes[type(value)] then
2914 if value == nil then
2915 if default == nil then
2916 error("'" .. name .. "' is missing!")
2917 end
2918 return default
2919 else
2920 error("'" .. name .. "' " .. wrongTypeText)
2921 end
2922 end
2923 return value
2924end
2925
2926local checkTables = {
2927 checkNumber = values.types_number,
2928 checkString = values.types_string,
2929 checkTable = values.types_table,
2930 checkRawNumber = values.types_raw_number,
2931 checkRawString = values.types_raw_string,
2932 checkRawTable = values.types_raw_table,
2933 checkCallable = values.types_callable,
2934}
2935local checkMessages = {
2936 checkNumber = "has to be a number or a callable object!",
2937 checkString = "has to be a string or a callable object!",
2938 checkTable = "has to be a table or a callable object!",
2939 checkRawNumber = "has to be a number!",
2940 checkRawString = "has to be a string!",
2941 checkRawTable = "has to be a table!",
2942 checkCallable = "has to be a callable object!",
2943}
2944
2945for name, permittedTypes in pairs(checkTables) do
2946 local msg = checkMessages[name]
2947 --Asserts that the given value has the correct type or can be converted to one by using values.get().
2948 --(throws an error using the given name otherwise)
2949 values[name] = function(value, name, default)
2950 return values.check(value, name, permittedTypes, msg, default)
2951 end
2952end
2953
2954return values
2955usr/man/cbrowse.man0000600000175000017500000001336312530573302011521 0ustar NAME
2956 cbrowse - inspecting lua components and other objects
2957
2958SYNOPSIS
2959 cbrowse [options] [commands...]
2960
2961DESCRIPTION
2962 cbrowse is a development tool with a heavy focus on inspecting Lua objects.
2963
2964 It features a single command line as input that accepts Lua code or - when using the prefix "sh " including the space - shell commands.
2965 (includes tab autocompletion for Lua code)
2966 After hitting enter the command is executed and a list of returned values is displayed if there were any.
2967 You can return to the previous display via Ctrl+C. The program itself is closed by exiting the top level or via an interrupt. (Ctrl+Alt+C)
2968 You can reload the display by hitting F5.
2969 Scrolling is done via Page down/up or via using the mouse wheel.
2970
2971 There are 3 types of displays:
2972 list
2973 This contains a list of values - showing their type in one column and some text to describe their value in another one. (used as default)
2974 Each value has got a 1 or 2 letter identifier shown to ease access from Lua commands.
2975 (-> use _V.a to access the first value via Lua, _V.b for the second etc.)
2976 table
2977 This contains a combination of keys and values and is used to display Lua tables. (used when a single table is returned)
2978 Each key value pair has got a 1 or 2 letter identifier as described in the previous paragraph.
2979 (-> use _K.a to access the first key, _V.b for the second value)
2980 string
2981 This shows a Lua string with character based line wrapping. (used when a single string is returned)
2982
2983 You are able to access a special environment using Lua commands.
2984 It is a proxy merging the currently viewed object with the global environment and some extra values.
2985 Since there may be collisions it is necessary to prioritize the contents:
2986 1st: special values (overrides the other contents)
2987 There are up to 5 of them:
2988 _OBJ: the currently displayed object
2989 _G: the global environment
2990 _K: a list of keys (if available)
2991 _V: a list of values (if available)
2992 _REG: a special registry table used by cbrowse to display names for non primitive values
2993 When using the name as an index you get the object associated with the name.
2994 When using an object as an index you get its name.
2995 You can use the registry for the following types:
2996 type name format
2997 function "f1234"
2998 userdata "u1234"
2999 thread "T1234"
3000 table "t1234"
3001 2nd: current object (a list of values is just a table using integer indices)
3002 3rd: a sandboxed global environment - with automatic require
3003
3004 Writing access uses the same priority but defaults to the current object as a target if the reading operation didn't find a non nil value.
3005 (This implies that there IS a reading operation before every writing operation. Use one of the special values to bypass that behaviour.)
3006
3007 When a command calls a gpu function that might modify the screen it will make cbrowse enter a graphics debug mode:
3008 -The screen is cleared before the function is executed.
3009 -After finishing the command the screen will freeze until you hit a key.
3010 Changes in primary GPU and screen or resolution are also detected. (->The screen is redrawn.)
3011
3012 cbrowse also supports left and right clicks on keys or values.
3013 A left click inserts a reference to the clicked object.
3014 A right click inserts "=reference" and hits enter for you.
3015 That way you can immediately look at an object you are interested in.
3016 Functions behave a bit differently:
3017 "=reference()" is insered and the cursor is moved one character to the left to allow you to type in parameters.
3018
3019OPTIONS
3020 --clean
3021 disables loading libraries and components on startup
3022 This is highly recommended if you don't have a lot of memory installed.
3023
3024 --noevent
3025 disables updates to the list of components
3026
3027 --env
3028 uses the first non option parameter as a global environment (_G)
3029
3030 For non option parameters it tries to find a value by using the parameter:
3031 1st: as a component address
3032 2nd: to get a primary component
3033 3rd: as a library name
3034 4th: as code executed via the cbrowse command line
3035 Using the --raw option disables this behaviour and instead uses arguments without processing. (useful if you call cbrowse for debugging your own program)
3036
3037EXAMPLES
3038 cbrowse
3039 starts cbrowse in the default screen
3040
3041 cbrowse --clean
3042 starts cbrowse without loading all libraries and components available on the computer
3043 sh ls
3044 executes command "ls" in the shell
3045 cbrowse is waiting for a key input after execution because ls printed some output. (You should have a chance to read it after all.)
3046 cbrowse redstone
3047 The initial screen now contains a reference "==default==" to the default object and a reference "redstone" to a redstone component proxy.
3048 =redstone
3049 move to the loaded redstone component
3050 =getInput(sides.north)
3051 show the redstone input at the northern side of the redstone component
3052
3053DEBUGGING
3054 cbrowse can be used as a debugging aid by executing it from within your program:
3055 local a, b, c = "test", 3.14, os
3056 require"cbrowse".view(a, b, c)
3057 Some words of advice though:
3058 1st
3059 It will consume events using event.pull. (like term.read)
3060 If you expect to pull some events after running it, it might change the programs behaviour.
3061 2nd
3062 Due to a limitation of shell.execute you can't have nil values as a parameter. All values after a nil value are just ignored.
3063 (The library is just a shortcut for a shell.execute call.)
3064 If you need that function, consider using a table:
3065 require"cbrowse".view{a, b, c}