From 2bf4b17577a54d1a2aaf997f7f8693f61eb60b56 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Fri, 28 Jul 2023 18:16:39 +0200 Subject: [PATCH] CapyOS 0.1.0 update --- Capy64/Assets/Lua/CapyOS/home/.shrc | 7 + Capy64/Assets/Lua/CapyOS/init.lua | 2 +- Capy64/Assets/Lua/CapyOS/sys/bin/alias.lua | 28 ++ Capy64/Assets/Lua/CapyOS/sys/bin/bg.lua | 4 + Capy64/Assets/Lua/CapyOS/sys/bin/cat.lua | 9 + Capy64/Assets/Lua/CapyOS/sys/bin/echo.lua | 4 + Capy64/Assets/Lua/CapyOS/sys/bin/exit.lua | 2 +- Capy64/Assets/Lua/CapyOS/sys/bin/hello.lua | 8 +- Capy64/Assets/Lua/CapyOS/sys/bin/help.lua | 11 + Capy64/Assets/Lua/CapyOS/sys/bin/less.lua | 78 ++++ Capy64/Assets/Lua/CapyOS/sys/bin/ls.lua | 91 ++++- Capy64/Assets/Lua/CapyOS/sys/bin/lua.lua | 105 ++++-- Capy64/Assets/Lua/CapyOS/sys/bin/motd.lua | 20 + Capy64/Assets/Lua/CapyOS/sys/bin/mv.lua | 16 + Capy64/Assets/Lua/CapyOS/sys/bin/programs.lua | 10 + Capy64/Assets/Lua/CapyOS/sys/bin/reboot.lua | 8 - Capy64/Assets/Lua/CapyOS/sys/bin/rm.lua | 14 +- Capy64/Assets/Lua/CapyOS/sys/bin/shell.lua | 76 ++-- Capy64/Assets/Lua/CapyOS/sys/bin/shutdown.lua | 33 +- .../Lua/CapyOS/sys/boot/autorun/02_fs.lua | 28 ++ .../CapyOS/sys/boot/autorun/50_os_manager.lua | 47 +++ .../CapyOS/sys/boot/autorun/999_shutdown.lua | 1 + .../Lua/CapyOS/sys/boot/autorun/99_shell.lua | 15 - .../Assets/Lua/CapyOS/sys/lib/argparser.lua | 91 +++++ Capy64/Assets/Lua/CapyOS/sys/lib/io.lua | 355 +++++++++++++++++- Capy64/Assets/Lua/CapyOS/sys/lib/lib.lua | 3 + .../Assets/Lua/CapyOS/sys/lib/scheduler.lua | 176 +++++++++ .../Assets/Lua/CapyOS/sys/lib/tableutils.lua | 87 +++++ Capy64/Assets/Lua/CapyOS/sys/share/help/index | 2 + .../Assets/Lua/CapyOS/sys/share/help/license | 12 + Capy64/Assets/Lua/CapyOS/sys/share/motd.txt | 8 + 31 files changed, 1227 insertions(+), 124 deletions(-) create mode 100644 Capy64/Assets/Lua/CapyOS/home/.shrc create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/alias.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/bg.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/cat.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/echo.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/help.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/less.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/motd.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/mv.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/programs.lua delete mode 100644 Capy64/Assets/Lua/CapyOS/sys/bin/reboot.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/boot/autorun/02_fs.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/boot/autorun/50_os_manager.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/boot/autorun/999_shutdown.lua delete mode 100644 Capy64/Assets/Lua/CapyOS/sys/boot/autorun/99_shell.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/lib/argparser.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/lib/lib.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/lib/scheduler.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/lib/tableutils.lua create mode 100644 Capy64/Assets/Lua/CapyOS/sys/share/help/index create mode 100644 Capy64/Assets/Lua/CapyOS/sys/share/help/license create mode 100644 Capy64/Assets/Lua/CapyOS/sys/share/motd.txt diff --git a/Capy64/Assets/Lua/CapyOS/home/.shrc b/Capy64/Assets/Lua/CapyOS/home/.shrc new file mode 100644 index 0000000..6b26566 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/home/.shrc @@ -0,0 +1,7 @@ +alias ll "ls -al" +alias la "ls -a" +alias rmdir "rm -r" +alias reboot "shutdown -r" + +# Comment or remove the line below to disable the MOTD +motd \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/init.lua b/Capy64/Assets/Lua/CapyOS/init.lua index ae90150..bae0138 100644 --- a/Capy64/Assets/Lua/CapyOS/init.lua +++ b/Capy64/Assets/Lua/CapyOS/init.lua @@ -1,4 +1,4 @@ -local version = "0.0.3" +local version = "0.1.0" local systemDirectory = "/sys" print("Starting CapyOS") diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/alias.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/alias.lua new file mode 100644 index 0000000..93a3f6c --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/alias.lua @@ -0,0 +1,28 @@ +local argparser = require("argparser") + +local args, options = argparser.parse(...) + + +if options.l or options.list then + for alias, value in pairs(shell.aliases) do + print(string.format("%s = \"%s\"", alias, value)) + end + return +end + +local alias = args[1] + +if not alias or options.h or options.help then + print("Usage: alias [option...] [command]") + print("Options:") + print(" -l --list: List aliases") + return false +end + +local command = table.pack(select(2, ...)) +if #command == 0 then + shell.aliases[alias] = nil + return +end + +shell.aliases[alias] = table.concat(command, " ") \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/bg.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/bg.lua new file mode 100644 index 0000000..234b985 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/bg.lua @@ -0,0 +1,4 @@ +local scheduler = require("scheduler") +scheduler.spawn(function() + shell.run(arg.string) +end) \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/cat.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/cat.lua new file mode 100644 index 0000000..6adc8fa --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/cat.lua @@ -0,0 +1,9 @@ +local fs = require("fs") + +local args = {...} + +local path = shell.resolve(args[1]) + +local f = fs.open(path, "r") +print(f:read("a")) +f:close() \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/echo.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/echo.lua new file mode 100644 index 0000000..a098ee0 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/echo.lua @@ -0,0 +1,4 @@ +local argparser = require("argparser") + +local args, options = argparser.parse(...) +print(table.concat(args, " ")) \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/exit.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/exit.lua index b90092c..0aa454a 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/exit.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/exit.lua @@ -1 +1 @@ -shell.exit() +shell.exit() \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/hello.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/hello.lua index 8f52616..da1207a 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/hello.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/hello.lua @@ -11,7 +11,13 @@ local function slowPrint(text, delay) print() end +local args = {...} +local text = "Hello, World!" +if #args > 0 then + text = table.concat(args, " ") +end + local color = colors[math.random(1, #colors)] term.setForeground(color) -slowPrint("Hello, World!", 0.05) +slowPrint(text, 0.05) diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/help.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/help.lua new file mode 100644 index 0000000..c3b8dd6 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/help.lua @@ -0,0 +1,11 @@ +local fs = require("fs") +local helpPath = "/sys/share/help" + +local topicName = arg[1] or "index" + +if not fs.exists(fs.combine(helpPath, topicName)) then + print(string.format("Topic \"%s\" not found.", topicName)) + return false +end + +shell.run("/sys/bin/less.lua", fs.combine(helpPath, topicName)) \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/less.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/less.lua new file mode 100644 index 0000000..c49605c --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/less.lua @@ -0,0 +1,78 @@ +local term = require("term") +local keys = require("keys") +local event = require("event") +local fs = require("fs") +local timer = require("timer") +local colors = require("colors") + +local filename = shell.resolve(arg[1]) + +local f = fs.open(filename, "r") +local lines = {} +local lineMax = 0 +for line in f:lines() do + table.insert(lines, line) + lineMax = math.max(lineMax, #line) +end +f:close() + + +local width, height = term.getSize() +height = height - 1 +local posx, posy = 0, 0 + +local function redraw() + term.clear() + term.setForeground(colors.white) + for i = 1, height do + if i + posy > #lines then + break + end + term.setPos(-posx + 1, i) + term.write(lines[i + posy]) + end + + term.setForeground(colors.yellow) + term.setPos(1, height + 1) + term.write("Use arrow keys to move or press Q to exit.") +end + +while true do + redraw() + + local _, key = event.pull("key_down") + + if key == keys.enter or key == keys.down then + posy = posy + 1 + elseif key == keys.up then + posy = posy - 1 + elseif key == keys.right then + posx = posx + 1 + elseif key == keys.left then + posx = posx - 1 + elseif key == keys.q or key == keys.escape then + -- Clear event queue + timer.sleep(0) + term.clear() + term.setPos(1, 1) + break + end + + + + if posy > #lines - height then + posy = #lines - height + end + + if posy < 0 then + posy = 0 + end + + if posx + width > lineMax + 1 then + posx = lineMax - width + 1 + end + + if posx < 0 then + posx = 0 + end +end diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/ls.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/ls.lua index 58a0607..8778e1f 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/ls.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/ls.lua @@ -1,25 +1,88 @@ -local args = { ... } local fs = require("fs") local term = require("term") local colors = require("colors") -local dir = shell.getDir() +local argparser = require("argparser") -if args[1] then - dir = shell.resolve(args[1]) +local theme = { + directory = colors.lightBlue, + file = colors.white, + lua = colors.yellow, +} + +local function humanizeBytes(n) + local prefixes = { + [0] = "", + "k", + "M", + "G", + "T", + } + local block = 1024 + local prefixIndex = 0 + + while n >= block do + n = n / 1024 + prefixIndex = prefixIndex + 1 + end + + return string.format("%.0f%s", n, prefixes[prefixIndex]) end -if not fs.isDir(dir) then - error("No such directory: " .. dir, 0) +local args, options = argparser.parse(...) + +if options.h or options.help then + print("Usage: ls [option...] [path]") + print("List files (current directory by default)") + print("Options:") + print(" -a: Include hidden files") + print(" -l: Use long listing format") + return +end +local path = shell.getDir() + +if args[1] then + path = shell.resolve(args[1]) +end + +if not fs.isDir(path) then + error("No such directory: " .. path, 0) return false end -local files = fs.list(dir) -for k, v in ipairs(files) do - if fs.isDir(fs.combine(dir, v)) then - term.setForeground(colors.lightBlue) - print(v .. "/") - else - term.setForeground(colors.white) - print(v) +local entries = fs.list(path) + +if options.l then + print(string.format("total %d", #entries)) +end +local printed = 0 +for i, entry in ipairs(entries) do + if entry:sub(1, 1) ~= "." or options.a then + printed = printed + 1 + local attributes = fs.attributes(fs.combine(path, entry)) + local size = humanizeBytes(attributes.size) + local date = os.date("%x %H:%m", attributes.modified // 1000) + + local entryType + if attributes.isDirectory then + entryType = "directory" + else + entryType = "file" + if string.match(entry, "%.lua$") then + entryType = "lua" + end + end + + if options.l then + term.setForeground(colors.white) + term.write(string.format("%s %5s %s ", attributes.isDirectory and "d" or "-", size, date)) + end + term.setForeground(theme[entryType]) + io.write(entry) + + io.write(options.l and "\n" or "\t") end end + +if not options.l and printed > 0 then + print() +end \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/lua.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/lua.lua index 1fc9038..01bfb3a 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/lua.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/lua.lua @@ -1,49 +1,16 @@ local term = require("term") local io = require("io") local colors = require("colors") -local colours = colors +local argparser = require("argparser") +local tableutils = require("tableutils") -local tArgs = { ... } -if #tArgs > 0 then - print("This is an interactive Lua prompt.") - print("To run a lua program, just type its name.") - return -end - ---local pretty = require "cc.pretty" - -local bRunning = true -local tCommandHistory = {} -local tEnv = { - ["exit"] = setmetatable({}, { - __tostring = function() return "Call exit() to exit." end, - __call = function() bRunning = false end, - }), -} -setmetatable(tEnv, { __index = _ENV }) - -for k, v in pairs(package.loaded) do - tEnv[k] = v -end - -term.setForeground(colours.yellow) -print(_VERSION .. " interactive prompt") -print("Call exit() to exit.") -term.setForeground(colours.white) - -while bRunning do - term.setForeground(colours.yellow) - io.write("> ") - term.setForeground(colours.white) - - local s = io.read(nil, tCommandHistory) - if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then - table.insert(tCommandHistory, s) - end +local args, options = argparser.parse(...) +local function evaluate(str, env, chunkname) + chunkname = chunkname or "=lua" local nForcePrint = 0 - local func, e = load(s, "=lua", "t", tEnv) - local func2 = load("return " .. s, "=lua", "t", tEnv) + local func, e = load(str, chunkname, "t", env) + local func2 = load("return " .. str, chunkname, "t", env) if not func then if func2 then func = func2 @@ -62,14 +29,70 @@ while bRunning do local n = 1 while n < tResults.n or n <= nForcePrint do local value = tResults[n + 1] - print(tostring(value)) + print(tableutils.pretty(value)) n = n + 1 end else io.stderr.print(tResults[2]) + return false end else io.stderr.print(e) + return false + end + return true +end + +local function createEnvironment() + return setmetatable({}, { __index = _ENV }) +end + +local function loadPackages(env) + for k, v in pairs(package.loaded) do + env[k] = v + end +end + +if options.e then + local env = createEnvironment() + loadPackages(env) + return evaluate(table.concat(args, " "), env) +end + +if #args > 0 then + print("This is an interactive Lua prompt.") + print("To run a lua program, just type its name.") + return +end + +--local pretty = require "cc.pretty" + +local bRunning = true +local tCommandHistory = {} + + +local tEnv = createEnvironment() +tEnv.exit = setmetatable({}, { + __tostring = function() return "Call exit() to exit." end, + __call = function() bRunning = false end, +}) +loadPackages(tEnv) + +term.setForeground(colors.yellow) +print(_VERSION .. " interactive prompt") +print("Call exit() to exit.") +term.setForeground(colors.white) + +while bRunning do + term.setForeground(colors.yellow) + io.write("> ") + term.setForeground(colors.white) + + local s = io.read(nil, tCommandHistory) + if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then + table.insert(tCommandHistory, s) end + evaluate(s, tEnv) + end diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/motd.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/motd.lua new file mode 100644 index 0000000..3fe47a3 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/motd.lua @@ -0,0 +1,20 @@ +local fs = require("fs") + +local date = os.date("*t") + +if date.month == 4 and date.day == 28 then + print("Ed Balls") + return +end + +local motdList = {} + +local f = fs.open("/sys/share/motd.txt", "r") +for line in f:lines() do + table.insert(motdList, line) +end +f:close() + +local motdIndex = math.random(1, #motdList) + +print(motdList[motdIndex]) \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/mv.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/mv.lua new file mode 100644 index 0000000..b32b2de --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/mv.lua @@ -0,0 +1,16 @@ +local fs = require("fs") +local argparser = require("argparser") + +local args, options = argparser.parse(...) + +if not args[1] or not args[2] or options.h or options.help then + print("Usage: mv [option...] ") + print("Options:") + print(" -h --help: Display help") + return +end + +local source = shell.resolve(args[1]) +local destination = shell.resolve(args[2]) + +fs.move(source, destination) \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/programs.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/programs.lua new file mode 100644 index 0000000..fa5191c --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/programs.lua @@ -0,0 +1,10 @@ +local fs = require("fs") +local programs = fs.list("/sys/bin", function(name, attr) + return not attr.isDirectory +end) + +for i, v in ipairs(programs) do + programs[i] = string.gsub(v, "%.lua$", "") +end + +print(table.concat(programs, " ")) \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/reboot.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/reboot.lua deleted file mode 100644 index 78cf97d..0000000 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/reboot.lua +++ /dev/null @@ -1,8 +0,0 @@ -local timer = require("timer") -local machine = require("machine") - -print("Goodbye!") - -timer.sleep(1) - -machine.reboot() diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/rm.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/rm.lua index 428fcad..7bc63f4 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/rm.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/rm.lua @@ -1,10 +1,16 @@ local fs = require("fs") +local argparser = require("argparser") -local args = { ... } -if #args == 0 then - print("Usage: rm ") +local args, options = argparser.parse(...) + +if not args[1] or options.h or options.help then + print("Usage: rm [option...] ") + print("Options:") + print(" -r --recursive: Delete non-empty directories") + print(" -h --help: Display help") return end local file = shell.resolve(args[1]) -fs.delete(file, true) + +fs.delete(file, options.recursive or options.r) diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/shell.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/shell.lua index 21d840a..d87c69d 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/shell.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/shell.lua @@ -2,42 +2,31 @@ local term = require("term") local colors = require("colors") local fs = require("fs") local machine = require("machine") +local argparser = require("argparser") +local scheduler = require("scheduler") local exit = false +local parentShell = shell +local isStartupShell = parentShell == nil local shell = {} -shell.path = "./?;./?.lua;/bin/?.lua;/sys/bin/?.lua" -shell.homePath = "/home" +shell.path = parentShell and parentShell.path or "./?;./?.lua;/bin/?.lua;/sys/bin/?.lua" +shell.homePath = parentShell and parentShell.home or "/home" +shell.aliases = parentShell and parentShell.aliases or {} -local currentDir = shell.homePath +local currentDir = parentShell and parentShell.getDir() or shell.homePath -local function buildEnvironment(path, args) +local function buildEnvironment(path, args, argf) local arg = { table.unpack(args, 2) } arg[0] = path - + arg.string = argf + return setmetatable({ shell = shell, - arg = arg + arg = arg, }, { __index = _G }) end -local function tokenise(...) - local sLine = table.concat({ ... }, " ") - local tWords = {} - local bQuoted = false - for match in string.gmatch(sLine .. "\"", "(.-)\"") do - if bQuoted then - table.insert(tWords, match) - else - for m in string.gmatch(match, "[^ \t]+") do - table.insert(tWords, m) - end - end - bQuoted = not bQuoted - end - return tWords -end - function shell.getDir() return currentDir end @@ -72,16 +61,24 @@ function shell.resolveProgram(path) end function shell.run(...) - local args = tokenise(...) + local args = argparser.tokenize(...) + local argf = table.concat({...}, " ") local command = args[1] + + argf = argf:sub(#command + 2) + local path = shell.resolveProgram(command) if not path then - io.stderr.print("Command not found: " .. command) - return false + if shell.aliases[command] then + return shell.run(shell.aliases[command], select(2, table.unpack(args))) + else + io.stderr.print("Command not found: " .. command) + return false + end end - local env = buildEnvironment(command, args) + local env = buildEnvironment(command, args, argf) local func, err = loadfile(path, "t", env) @@ -90,7 +87,15 @@ function shell.run(...) return false end - local ok, err = pcall(func, table.unpack(args, 2)) + local ok, err + local function run() + ok, err = pcall(func, table.unpack(args, 2)) + + end + + local programTask = scheduler.spawn(run) + coroutine.yield("scheduler_task_end") + if not ok then io.stderr.print(err) return false @@ -107,6 +112,21 @@ if not fs.exists(shell.homePath) then fs.makeDir(shell.homePath) end +term.setForeground(colors.white) +term.setBackground(colors.black) + +if isStartupShell then + if fs.exists(fs.combine(shell.homePath, ".shrc")) then + local f = fs.open(fs.combine(shell.homePath, ".shrc"), "r") + for line in f:lines() do + if line:match("%S") and not line:match("^%s-#") then + shell.run(line) + end + end + f:close() + end +end + local history = {} local lastExecSuccess = true while not exit do diff --git a/Capy64/Assets/Lua/CapyOS/sys/bin/shutdown.lua b/Capy64/Assets/Lua/CapyOS/sys/bin/shutdown.lua index cb3016e..04a9d9b 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/bin/shutdown.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/bin/shutdown.lua @@ -1,8 +1,33 @@ -local timer = require("timer") local machine = require("machine") +local scheduler = require("scheduler") +local argparser = require("argparser") -print("Goodbye!") +local args, options = argparser.parse(...) -timer.sleep(1) +if options.h or options.help then + print("Usage: shutdown [option...]") + print("Shutdown or restart Capy64.") + print("Options:") + print(" -s --shutdown: Shutdown and exit Capy64. (default)") + print(" -r --reboot: Restart Capy64.") + print(" -t --time: Time to wait in seconds. (\"now\" is 0 seconds, default)") + return +end -machine.shutdown() +local time = 0 +if options.t or options.time then + time = options.t or options.time +end +if time == "now" then + time = 0 +else + time = tonumber(time) + if not time then + error("Invalid time option: " .. (options.t or options.time), 0) + end +end + +scheduler.ipc(1, "power", { + reboot = options.r or options.reboot, + time = time, +}) diff --git a/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/02_fs.lua b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/02_fs.lua new file mode 100644 index 0000000..58a00c7 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/02_fs.lua @@ -0,0 +1,28 @@ +local fs = require("fs") +local expect = require("expect").expect + +local fsList = fs.list + +function fs.list(path, filter) + expect(1, path, "string") + expect(2, filter, "nil", "function") + + if not fs.isDir(path) then + error("directory not found", 2) + end + + local list = fsList(path) + if not filter then + return list + end + + local filteredList = {} + for i = 1, #list do + local attributes = fs.attributes(fs.combine(path, list[i])) + if filter(list[i], attributes) then + table.insert(filteredList, list[i]) + end + end + + return filteredList +end \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/50_os_manager.lua b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/50_os_manager.lua new file mode 100644 index 0000000..6554a89 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/50_os_manager.lua @@ -0,0 +1,47 @@ +local machine = require("machine") +local scheduler = require("scheduler") + +local term = require("term") +local colors = require("colors") +term.setForeground(0x59c9ff) +term.setBackground(colors.black) +term.clear() +term.setPos(1, 1) + +term.write(os.version()) +term.setPos(1, 2) + +local function spawnShell() + return scheduler.spawn(loadfile("/sys/bin/shell.lua")) +end + +local function main() + local shellTask = spawnShell() + while true do + local ev = {coroutine.yield()} + if ev[1] == "ipc_message" then + local sender = ev[2] + local call = ev[3] + if call == "power" then + local options = ev[4] + --todo: handle time and cancels + if options.reboot then + machine.reboot() + else + machine.shutdown() + end + end + elseif ev[1] == "scheduler_task_end" then + if ev[2].pid == shellTask.pid then + if not ev[3] then + io.stderr.print(ev[4]) + end + shellTask = spawnShell() + end + end + end +end + +scheduler.spawn(main) + +scheduler.init() \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/999_shutdown.lua b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/999_shutdown.lua new file mode 100644 index 0000000..d015449 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/999_shutdown.lua @@ -0,0 +1 @@ +require("machine").shutdown() \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/99_shell.lua b/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/99_shell.lua deleted file mode 100644 index d1619ec..0000000 --- a/Capy64/Assets/Lua/CapyOS/sys/boot/autorun/99_shell.lua +++ /dev/null @@ -1,15 +0,0 @@ -local term = require("term") -local colors = require("colors") -local machine = require("machine") - -term.setForeground(0x59c9ff) -term.setBackground(colors.black) -term.clear() -term.setPos(1, 1) - -term.write(os.version()) -term.setPos(1, 2) - -dofile("/sys/bin/shell.lua") - -machine.shutdown() diff --git a/Capy64/Assets/Lua/CapyOS/sys/lib/argparser.lua b/Capy64/Assets/Lua/CapyOS/sys/lib/argparser.lua new file mode 100644 index 0000000..d0c8171 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/lib/argparser.lua @@ -0,0 +1,91 @@ +local argparser = {} + +function argparser.tokenize(...) + local input = table.concat(table.pack(...), " ") + local tokens = {} + + -- surely there must be a better way + local quoted = false + local escaped = false + local current = "" + for i = 1, #input do + local char = input:sub(i, i) + if escaped then + escaped = false + current = current .. char + else + if char == "\\" then + escaped = true + elseif char == "\"" then + if quoted then + -- close quote + table.insert(tokens, current) + current = "" + end + quoted = not quoted + elseif char == " " and not quoted then + if #current > 0 then + table.insert(tokens, current) + end + current = "" + else + current = current .. char + end + end + end + + if current ~= "" then + table.insert(tokens, current) + end + + return tokens +end + +function argparser.parse(...) + local tokens = { ... } + local args = {} + local options = {} + local ignoreOptions = false + + for i = 1, #tokens do + local token = tokens[i] + if not ignoreOptions then + if token == "--" then + ignoreOptions = true + elseif token:sub(1, 2) == "--" then + local opt, value = token:match("%-%-(.+)=(.+)") + if not opt then + opt = token:sub(3) + if opt:sub(-1) == "=" then + -- next token is value + value = tokens[i + 1] + opt = opt:sub(1, -2) + options[opt] = value + i = i + 1 + else + options[opt] = true + end + else + options[opt] = value + end + elseif token:sub(1, 1) == "-" then + local opts = token:sub(2) + for j = 1, #opts do + options[opts:sub(j, j)] = true + end + else + if #token > 0 then + table.insert(args, token) + end + end + else + if #token > 0 then + table.insert(args, token) + end + end + end + + return args, options +end + +return argparser diff --git a/Capy64/Assets/Lua/CapyOS/sys/lib/io.lua b/Capy64/Assets/Lua/CapyOS/sys/lib/io.lua index 9367689..97f0aaf 100644 --- a/Capy64/Assets/Lua/CapyOS/sys/lib/io.lua +++ b/Capy64/Assets/Lua/CapyOS/sys/lib/io.lua @@ -6,7 +6,347 @@ local machine = require("machine") local io = {} -function io.write(text) +function io.write(sText) + sText = tostring(sText) + + local w, h = term.getSize() + local x, y = term.getPos() + + local nLinesPrinted = 0 + local function newLine() + if y + 1 <= h then + term.setPos(1, y + 1) + else + term.setPos(1, h) + term.scroll(1) + end + x, y = term.getPos() + nLinesPrinted = nLinesPrinted + 1 + end + + -- Print the line with proper word wrapping + sText = tostring(sText) + while #sText > 0 do + local whitespace = string.match(sText, "^[ \t]+") + if whitespace then + -- Print whitespace + term.write(whitespace) + x, y = term.getPos() + sText = string.sub(sText, #whitespace + 1) + end + + local newline = string.match(sText, "^\n") + if newline then + -- Print newlines + newLine() + sText = string.sub(sText, 2) + end + + local text = string.match(sText, "^[^ \t\n]+") + if text then + sText = string.sub(sText, #text + 1) + if #text > w then + -- Print a multiline word + while #text > 0 do + if x > w then + newLine() + end + term.write(text) + text = string.sub(text, w - x + 2) + x, y = term.getPos() + end + else + -- Print a word normally + if x + #text - 1 > w then + newLine() + end + term.write(text) + x, y = term.getPos() + end + end + end + + return nLinesPrinted +end + +function io.read(_sReplaceChar, _tHistory, _fnComplete, _sDefault) + expect(1, _sReplaceChar, "string", "nil") + expect(2, _tHistory, "table", "nil") + expect(3, _fnComplete, "function", "nil") + expect(4, _sDefault, "string", "nil") + + term.setBlink(true) + + local sLine + if type(_sDefault) == "string" then + sLine = _sDefault + else + sLine = "" + end + local nHistoryPos + local nPos, nScroll = #sLine, 0 + if _sReplaceChar then + _sReplaceChar = string.sub(_sReplaceChar, 1, 1) + end + + local tCompletions + local nCompletion + local function recomplete() + if _fnComplete and nPos == #sLine then + tCompletions = _fnComplete(sLine) + if tCompletions and #tCompletions > 0 then + nCompletion = 1 + else + nCompletion = nil + end + else + tCompletions = nil + nCompletion = nil + end + end + + local function uncomplete() + tCompletions = nil + nCompletion = nil + end + + local w = term.getSize() + local sx = term.getPos() + + local function redraw(_bClear) + local cursor_pos = nPos - nScroll + if sx + cursor_pos >= w then + -- We've moved beyond the RHS, ensure we're on the edge. + nScroll = sx + nPos - w + elseif cursor_pos < 0 then + -- We've moved beyond the LHS, ensure we're on the edge. + nScroll = nPos + end + + local _, cy = term.getPos() + term.setPos(sx, cy) + local sReplace = _bClear and " " or _sReplaceChar + if sReplace then + term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0))) + else + term.write(string.sub(sLine, nScroll + 1)) + end + + if nCompletion then + local sCompletion = tCompletions[nCompletion] + local oldText, oldBg + if not _bClear then + oldText = term.getTextColor() + oldBg = term.getBackgroundColor() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.gray) + end + if sReplace then + term.write(string.rep(sReplace, #sCompletion)) + else + term.write(sCompletion) + end + if not _bClear then + term.setTextColor(oldText) + term.setBackgroundColor(oldBg) + end + end + + term.setPos(sx + nPos - nScroll, cy) + end + + local function clear() + redraw(true) + end + + recomplete() + redraw() + + local function acceptCompletion() + if nCompletion then + -- Clear + clear() + + -- Find the common prefix of all the other suggestions which start with the same letter as the current one + local sCompletion = tCompletions[nCompletion] + sLine = sLine .. sCompletion + nPos = #sLine + + -- Redraw + recomplete() + redraw() + end + end + while true do + local sEvent, param, param1, param2 = event.pull() + if sEvent == "char" then + -- Typed key + clear() + sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1) + nPos = nPos + 1 + recomplete() + redraw() + + elseif sEvent == "paste" then + -- Pasted text + clear() + sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1) + nPos = nPos + #param + recomplete() + redraw() + + elseif sEvent == "key_down" then + if param == keys.enter or param == keys.numPadEnter then + -- Enter/Numpad Enter + if nCompletion then + clear() + uncomplete() + redraw() + end + break + + elseif param == keys.left then + -- Left + if nPos > 0 then + clear() + nPos = nPos - 1 + recomplete() + redraw() + end + + elseif param == keys.right then + -- Right + if nPos < #sLine then + -- Move right + clear() + nPos = nPos + 1 + recomplete() + redraw() + else + -- Accept autocomplete + acceptCompletion() + end + + elseif param == keys.up or param == keys.down then + -- Up or down + if nCompletion then + -- Cycle completions + clear() + if param == keys.up then + nCompletion = nCompletion - 1 + if nCompletion < 1 then + nCompletion = #tCompletions + end + elseif param == keys.down then + nCompletion = nCompletion + 1 + if nCompletion > #tCompletions then + nCompletion = 1 + end + end + redraw() + + elseif _tHistory then + -- Cycle history + clear() + if param == keys.up then + -- Up + if nHistoryPos == nil then + if #_tHistory > 0 then + nHistoryPos = #_tHistory + end + elseif nHistoryPos > 1 then + nHistoryPos = nHistoryPos - 1 + end + else + -- Down + if nHistoryPos == #_tHistory then + nHistoryPos = nil + elseif nHistoryPos ~= nil then + nHistoryPos = nHistoryPos + 1 + end + end + if nHistoryPos then + sLine = _tHistory[nHistoryPos] + nPos, nScroll = #sLine, 0 + else + sLine = "" + nPos, nScroll = 0, 0 + end + uncomplete() + redraw() + + end + + elseif param == keys.back then + -- Backspace + if nPos > 0 then + clear() + sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1) + nPos = nPos - 1 + if nScroll > 0 then nScroll = nScroll - 1 end + recomplete() + redraw() + end + + elseif param == keys.home then + -- Home + if nPos > 0 then + clear() + nPos = 0 + recomplete() + redraw() + end + + elseif param == keys.delete then + -- Delete + if nPos < #sLine then + clear() + sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2) + recomplete() + redraw() + end + + elseif param == keys["end"] then + -- End + if nPos < #sLine then + clear() + nPos = #sLine + recomplete() + redraw() + end + + elseif param == keys.tab then + -- Tab (accept autocomplete) + acceptCompletion() + + end + + elseif sEvent == "mouse_down" or sEvent == "mouse_drag" and param == 1 then + local _, cy = term.getPos() + if param1 >= sx and param1 <= w and param2 == cy then + -- Ensure we don't scroll beyond the current line + nPos = math.min(math.max(nScroll + param1 - sx, 0), #sLine) + redraw() + end + + elseif sEvent == "term_resize" then + -- Terminal resized + w = term.getSize() + redraw() + + end + end + + local _, cy = term.getPos() + term.setBlink(false) + term.setPos(w + 1, cy) + print() + + return sLine +end + + +--[[function io.write(text) text = tostring(text) local lines = 0 @@ -28,7 +368,7 @@ function io.write(text) local chunk = text:sub(1, nl) text = text:sub(#chunk + 1) - local has_nl = chunk:sub( -1) == "\n" + local has_nl = chunk:sub(-1) == "\n" if has_nl then chunk = chunk:sub(1, -2) end local cx, cy = term.getPos() @@ -83,7 +423,7 @@ function io.read(replace, history, complete, default) local function clearCompletion() if completions[comp_id] then - write((" "):rep(#completions[comp_id])) + io.write((" "):rep(#completions[comp_id])) end end @@ -142,7 +482,7 @@ function io.read(replace, history, complete, default) elseif cursor_pos == #buffer then buffer = par1 .. buffer else - buffer = buffer:sub(0, -cursor_pos - 1) .. par1 .. buffer:sub( -cursor_pos) + buffer = buffer:sub(0, -cursor_pos - 1) .. par1 .. buffer:sub(-cursor_pos) end elseif evt == "key_down" then if par1 == keys.back and #buffer > 0 then @@ -151,7 +491,7 @@ function io.read(replace, history, complete, default) buffer = buffer:sub(1, -2) clearCompletion() elseif cursor_pos < #buffer then - buffer = buffer:sub(0, -cursor_pos - 2) .. buffer:sub( -cursor_pos) + buffer = buffer:sub(0, -cursor_pos - 2) .. buffer:sub(-cursor_pos) end elseif par1 == keys.delete and cursor_pos > 0 then dirty = true @@ -161,7 +501,7 @@ function io.read(replace, history, complete, default) elseif cursor_pos == 1 then buffer = buffer:sub(1, -2) else - buffer = buffer:sub(0, -cursor_pos - 1) .. buffer:sub( -cursor_pos + 1) + buffer = buffer:sub(0, -cursor_pos - 1) .. buffer:sub(-cursor_pos + 1) end cursor_pos = cursor_pos - 1 elseif par1 == keys.up then @@ -243,7 +583,7 @@ function io.read(replace, history, complete, default) buffer = text .. buffer else buffer = buffer:sub(0, -cursor_pos - 1) .. text .. - buffer:sub( -cursor_pos + (#text - 1)) + buffer:sub(-cursor_pos + (#text - 1)) end end end @@ -255,6 +595,7 @@ function io.read(replace, history, complete, default) return buffer end +]] io.stderr = {} diff --git a/Capy64/Assets/Lua/CapyOS/sys/lib/lib.lua b/Capy64/Assets/Lua/CapyOS/sys/lib/lib.lua new file mode 100644 index 0000000..de0f48c --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/lib/lib.lua @@ -0,0 +1,3 @@ +local x = math.random() + +return x \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/lib/scheduler.lua b/Capy64/Assets/Lua/CapyOS/sys/lib/scheduler.lua new file mode 100644 index 0000000..198704e --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/lib/scheduler.lua @@ -0,0 +1,176 @@ +local expect = require("expect").expect +local tableutils = require("tableutils") +local event = require("event") + +local scheduler = {} + +local function contains(array, value) + for k, v in pairs(array) do + if v == value then + return true + end + end + return false +end + +local tasks = {} +local processes = 0 + +local Task = {} +local TaskMeta = { + __index = Task, + __name = "OS_TASK", + __tostring = function(self) + return string.format("OS_TASK[%s]: %d", self.source or "", self.pid or 0) + end, +} +local function newTask() + local task = {} + return setmetatable(task, TaskMeta) +end + +function Task:queue(eventName, ...) + expect(1, eventName, "string") + event.push("scheduler", self.pid, eventName, ...) +end + +local function findParent() + local i = 3 + + while true do + local info = debug.getinfo(i) + if not info then + break + end + + for pid, task in pairs(tasks) do + if task.uuid == tostring(info.func) then + return task + end + end + + i = i + 1 + end + + return nil +end + +function scheduler.spawn(func, options) + expect(1, func, "function") + expect(2, options, "nil", "table") + + options = options or {} + options.args = options.args or {} + + local source = debug.getinfo(2) + + local task = newTask() + local pid = #tasks + 1 + task.pid = pid + task.options = options + task.source = source.source + task.uuid = tostring(func) + task.thread = coroutine.create(func) + task.started = false + local parent = findParent() + if parent then + task.parent = parent.pid + table.insert(parent.children, pid) + end + task.filters = {} + task.children = {} + task.eventQueue = {} + + tasks[pid] = task + + processes = processes + 1 + + return task +end + +local function cascadeKill(pid, err) + local task = tasks[pid] + if not task then + return + end + for i, cpid in ipairs(task.children) do + cascadeKill(cpid, err) + end + if task.parent then + local parent = tasks[task.parent] + if parent then + local index = tableutils.find(parent.children, task.pid) + table.remove(parent.children, index) + parent:queue("scheduler_task_end", task, err == nil, err) + end + else + if err then + error(err, 0) + end + end + if task then + task.killed = true + coroutine.close(task.thread) + tasks[pid] = nil + processes = processes - 1 + end +end + +function scheduler.kill(pid) + expect(1, pid, "number") + cascadeKill(pid) +end + +function scheduler.ipc(pid, ...) + expect(1, pid, "number") + if not tasks[pid] then + error("process by pid " .. pid .. " does not exist.", 2) + end + + local sender = findParent() + tasks[pid]:queue("ipc_message", sender, ...) +end + +local running = false +function scheduler.init() + if running then + error("scheduler already running", 2) + end + running = true + + local ev = { n = 0 } + while processes > 0 do + for pid, task in pairs(tasks) do + local yieldPars = ev + if ev[1] == "scheduler" and ev[2] == pid then + yieldPars = table.pack(table.unpack(ev, 3)) + end + if yieldPars[1] ~= "scheduler" and not task.filters or #task.filters == 0 or contains(task.filters, yieldPars[1]) or yieldPars[1] == "interrupt" then + if not task.started then + yieldPars = task.options.args + task.started = true + end + local pars = table.pack(coroutine.resume(task.thread, table.unpack(yieldPars))) + if pars[1] then + task.filters = table.pack(table.unpack(pars, 2)) + else + cascadeKill(pid, pars[2]) + end + end + + if coroutine.status(task.thread) == "dead" then + cascadeKill(pid) + end + end + + if processes <= 0 then + break + end + + ev = table.pack(coroutine.yield()) + end + + running = false +end + +return scheduler diff --git a/Capy64/Assets/Lua/CapyOS/sys/lib/tableutils.lua b/Capy64/Assets/Lua/CapyOS/sys/lib/tableutils.lua new file mode 100644 index 0000000..1659751 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/lib/tableutils.lua @@ -0,0 +1,87 @@ +local expect = require("expect").expect +local tableutils = {} + +local function serialize(data, circular) + expect(1, data, "string", "table", "number", "boolean", "nil") + if type(data) == "table" then + if not circular then + circular = {} + end + local output = "{" + for k, v in pairs(data) do + if type(v) == "table" then + local name = tostring(v) + if circular[name] then + error("circular reference in table", 2) + end + circular[name] = true + end + output = output .. string.format("[%q] = %s,", k, serialize(v, circular)) + end + output = output .. "}" + return output + else + return string.format("%q", data) + end +end + +function tableutils.serialize(data) + expect(1, data, "string", "table", "number", "boolean", "nil") + return serialize(data) +end + +function tableutils.deserialize(data) + local func, err = load("return " .. data, "=tableutils", "t", {}) + if not func then + error(err, 2) + end + return func() +end + +local function prettyvalue(value) + if type(value) == "table" or type(value) == "function" or type(value) == "thread" or type(value) == "userdata" or type(value) == "number" then + return tostring(value) + else + return string.format("%q", value) + end +end + +function tableutils.pretty(data) + if type(data) == "table" then + local output = "{" + + local index = 0 + for k, v in pairs(data) do + local value = prettyvalue(v) + + if type(k) == "number" and k - 1 == index then + index = index + 1 + output = output .. string.format("\n %s,", value) + elseif type(k) == "string" and k:match("^[%a_][%w_]*$") then + output = output .. string.format("\n %s = %s,", k, value) + else + output = output .. string.format("\n [%s] = %s,", prettyvalue(k), value) + end + end + if output == "{" then + return "{}" + end + output = output .. "\n}" + + return output + else + return prettyvalue(data) + end +end + +function tableutils.find(tbl, element) + for i = 1, #tbl do + if tbl[i] == element then + return i + end + end + + return nil +end + +return tableutils \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/share/help/index b/Capy64/Assets/Lua/CapyOS/sys/share/help/index new file mode 100644 index 0000000..aa1450d --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/share/help/index @@ -0,0 +1,2 @@ +Welcome to CapyOS! +Run "programs" to get a list of available programs. \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/share/help/license b/Capy64/Assets/Lua/CapyOS/sys/share/help/license new file mode 100644 index 0000000..fccebe4 --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/share/help/license @@ -0,0 +1,12 @@ +CapyOS and Capy64 are licensed under the Apache 2.0 Public License. +https://capy64.alexdevs.me/ +https://github.com/Ale32bit/Capy64 + +Some CapyOS components include code from the CC Recrafted project. +https://github.com/Ocawesome101/recrafted + +Some CapyOS components may include code from CC: Tweaked. +https://github.com/CC-Tweaked/CC-Tweaked + +json.lua by rxi is licensed under the MIT License. +https://github.com/rxi/json.lua \ No newline at end of file diff --git a/Capy64/Assets/Lua/CapyOS/sys/share/motd.txt b/Capy64/Assets/Lua/CapyOS/sys/share/motd.txt new file mode 100644 index 0000000..9a6cf1a --- /dev/null +++ b/Capy64/Assets/Lua/CapyOS/sys/share/motd.txt @@ -0,0 +1,8 @@ +Please do not use this software to flash firmwares. +Now with bitwise operators! +Writing to _G is a war crime. +Tampering with _G in any way is also a war crime. +No, Wojbie, stop! STOP TOUCHING _G! +Stop! You've violated the law! +That's 65% more bullet per bullet. +This software is not responsible for collapses of spacetime. \ No newline at end of file