From ad2166831b61f5e2db8b5108aab21718a7d4b5e3 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Sun, 15 Jan 2023 16:44:42 +0100 Subject: [PATCH] Added paste event. Added proper shell Added shell commands --- Capy64/Assets/Lua/bin/cd.lua | 17 ++++ Capy64/Assets/Lua/bin/clear.lua | 5 + Capy64/Assets/Lua/bin/exit.lua | 1 + Capy64/Assets/Lua/bin/ls.lua | 32 ++++++ Capy64/Assets/Lua/bin/lua.lua | 4 +- Capy64/Assets/Lua/bin/mkdir.lua | 14 +++ Capy64/Assets/Lua/bin/pwd.lua | 1 + Capy64/Assets/Lua/bin/reboot.lua | 7 ++ Capy64/Assets/Lua/bin/rm.lua | 10 ++ Capy64/Assets/Lua/bin/shell.lua | 119 ++++++++++++++++++++-- Capy64/Assets/Lua/bin/shutdown.lua | 7 ++ Capy64/Assets/Lua/bin/wget.lua | 32 ++++++ Capy64/Assets/Lua/boot/99_shell.lua | 20 +++- Capy64/BIOS/RuntimeInputEvents.cs | 17 +++- Capy64/Core/SDL.cs | 21 ++++ Capy64/Extensions/Bindings/SDL2.cs | 78 +++++++++++++- Capy64/LuaRuntime/Libraries/FileSystem.cs | 13 ++- Capy64/LuaRuntime/Libraries/HTTP.cs | 5 +- Capy64/LuaRuntime/Runtime.cs | 9 +- 19 files changed, 389 insertions(+), 23 deletions(-) create mode 100644 Capy64/Assets/Lua/bin/cd.lua create mode 100644 Capy64/Assets/Lua/bin/clear.lua create mode 100644 Capy64/Assets/Lua/bin/exit.lua create mode 100644 Capy64/Assets/Lua/bin/ls.lua create mode 100644 Capy64/Assets/Lua/bin/mkdir.lua create mode 100644 Capy64/Assets/Lua/bin/pwd.lua create mode 100644 Capy64/Assets/Lua/bin/reboot.lua create mode 100644 Capy64/Assets/Lua/bin/rm.lua create mode 100644 Capy64/Assets/Lua/bin/shutdown.lua create mode 100644 Capy64/Assets/Lua/bin/wget.lua create mode 100644 Capy64/Core/SDL.cs diff --git a/Capy64/Assets/Lua/bin/cd.lua b/Capy64/Assets/Lua/bin/cd.lua new file mode 100644 index 0000000..db4323f --- /dev/null +++ b/Capy64/Assets/Lua/bin/cd.lua @@ -0,0 +1,17 @@ +local args = {...} +local fs = require("fs") + +local dir = args[1] + +if #args == 0 then + dir = shell.homePath +end + +dir = fs.combine(shell.getDir(), dir) + +if not fs.exists(dir) or not fs.getAttributes(dir).isDirectory then + error("No such directory: " .. dir, 0) + return false +end + +shell.setDir(dir) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/clear.lua b/Capy64/Assets/Lua/bin/clear.lua new file mode 100644 index 0000000..5cb133d --- /dev/null +++ b/Capy64/Assets/Lua/bin/clear.lua @@ -0,0 +1,5 @@ +local term = require("term") +local colors = require("colors") +term.setBackground(colors.black) +term.clear() +term.setPos(1, 1) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/exit.lua b/Capy64/Assets/Lua/bin/exit.lua new file mode 100644 index 0000000..0aa454a --- /dev/null +++ b/Capy64/Assets/Lua/bin/exit.lua @@ -0,0 +1 @@ +shell.exit() \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/ls.lua b/Capy64/Assets/Lua/bin/ls.lua new file mode 100644 index 0000000..f30f6eb --- /dev/null +++ b/Capy64/Assets/Lua/bin/ls.lua @@ -0,0 +1,32 @@ +local args = { ... } +local fs = require("fs") +local term = require("term") +local colors = require("colors") +local dir = shell.getDir() + +if args[1] then + dir = fs.combine(shell.getDir(), args[1]) +end + +if not fs.exists(dir) then + error("No such directory: " .. dir, 0) + return false +end + +local attr = fs.getAttributes(dir) +if not attr.isDirectory then + error("No such directory: " .. dir, 0) + return false +end + +local files = fs.list(dir) +for k,v in ipairs(files) do + local attr = fs.getAttributes(fs.combine(dir, v)) + if attr.isDirectory then + term.setForeground(colors.lightBlue) + print(v .. "/") + else + term.setForeground(colors.white) + print(v) + end +end \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/lua.lua b/Capy64/Assets/Lua/bin/lua.lua index 7b86518..6a44e18 100644 --- a/Capy64/Assets/Lua/bin/lua.lua +++ b/Capy64/Assets/Lua/bin/lua.lua @@ -30,13 +30,13 @@ for k, v in pairs(package.loaded) do end term.setForeground(colours.yellow) -print("Interactive Lua prompt.") +print(_VERSION .. " interactive prompt") print("Call exit() to exit.") term.setForeground(colours.white) while bRunning do term.setForeground( colours.yellow ) - write("lua> ") + write("> ") term.setForeground( colours.white ) local s = io.read(nil, tCommandHistory) diff --git a/Capy64/Assets/Lua/bin/mkdir.lua b/Capy64/Assets/Lua/bin/mkdir.lua new file mode 100644 index 0000000..f0e57c1 --- /dev/null +++ b/Capy64/Assets/Lua/bin/mkdir.lua @@ -0,0 +1,14 @@ +local fs = require("fs") +local args = {...} + +if #args == 0 then + print("Usage: mkdir ") + return +end + +local dir = fs.combine(shell.getDir(), args[1]) +if fs.exists(dir) then + error("Path already exists", 0) +end + +fs.makeDir(dir) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/pwd.lua b/Capy64/Assets/Lua/bin/pwd.lua new file mode 100644 index 0000000..6a30176 --- /dev/null +++ b/Capy64/Assets/Lua/bin/pwd.lua @@ -0,0 +1 @@ +print(shell.getDir()) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/reboot.lua b/Capy64/Assets/Lua/bin/reboot.lua new file mode 100644 index 0000000..5a82260 --- /dev/null +++ b/Capy64/Assets/Lua/bin/reboot.lua @@ -0,0 +1,7 @@ +local timer = require("timer") + +print("Goodbye!") + +timer.sleep(1000) + +os.shutdown(true) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/rm.lua b/Capy64/Assets/Lua/bin/rm.lua new file mode 100644 index 0000000..51f5de8 --- /dev/null +++ b/Capy64/Assets/Lua/bin/rm.lua @@ -0,0 +1,10 @@ +local fs = require("fs") + +local args = {...} +if #args == 0 then + print("Usage: rm ") + return +end + +local file = fs.combine(shell.getDir(), args[1]) +fs.delete(file, true) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/shell.lua b/Capy64/Assets/Lua/bin/shell.lua index a2fcf49..c640036 100644 --- a/Capy64/Assets/Lua/bin/shell.lua +++ b/Capy64/Assets/Lua/bin/shell.lua @@ -1,32 +1,131 @@ local term = require("term") local colors = require("colors") -local event = require("event") local io = require("io") +local fs = require("fs") +local exit = false local shell = {} -shell.path = "/bin/?.lua" +shell.path = "./?.lua;./?;/bin/?.lua" +shell.homePath = "/home" + +local currentDir = shell.homePath + +local function buildEnvironment() + return setmetatable({ + shell = shell, + }, { __index = _G }) +end + +local function printError(...) + local cfg = {term.getForeground()} + local cbg = {term.getBackground()} + + term.setForeground(colors.red) + term.setBackground(colors.black) + print(...) + term.setForeground(table.unpack(cfg)) + term.setBackground(table.unpack(cbg)) +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 function shell.setDir(path) - + currentDir = path end function shell.resolve(path) + if path:sub(1, 1) == "/" then + return path + end + for seg in shell.path:gmatch("[^;]+") do + local resolved = seg:gsub("%?", path) + if fs.exists(resolved) then + return resolved + end + end end -function shell.run(path, args) +function shell.run(...) + local args = tokenise(...) + local command = args[1] + local path = shell.resolve(command) + if not path then + printError("Command not found: " .. command) + return false + end + + local env = buildEnvironment() + + local func, err = loadfile(path, "t", env) + + if not func then + printError(err) + return false + end + + local ok, err = pcall(func, table.unpack(args, 2)) + if not ok then + printError(err) + return false + end + + return true end +function shell.exit() + exit = true +end -while true do - term.setForeground(colors.yellow) - write("$ ") +local history = {} +local lastExecSuccess = true +while not exit do term.setForeground(colors.white) - local line = io.read() -end \ No newline at end of file + write(":") + term.setForeground(colors.lightBlue) + local currentDir = shell.getDir() + if currentDir == shell.homePath then + write("~") + else + write(currentDir) + end + + if lastExecSuccess then + term.setForeground(colors.yellow) + else + term.setForeground(colors.red) + end + write("$ ") + + term.setForeground(colors.white) + local line = io.read(nil, history) + + if line:match("%S") and history[#history] ~= line then + table.insert(history, line) + end + + if line:match("%S") then + lastExecSuccess = shell.run(line) + end +end diff --git a/Capy64/Assets/Lua/bin/shutdown.lua b/Capy64/Assets/Lua/bin/shutdown.lua new file mode 100644 index 0000000..6bcb27c --- /dev/null +++ b/Capy64/Assets/Lua/bin/shutdown.lua @@ -0,0 +1,7 @@ +local timer = require("timer") + +print("Goodbye!") + +timer.sleep(1000) + +os.shutdown(false) \ No newline at end of file diff --git a/Capy64/Assets/Lua/bin/wget.lua b/Capy64/Assets/Lua/bin/wget.lua new file mode 100644 index 0000000..63ab7a4 --- /dev/null +++ b/Capy64/Assets/Lua/bin/wget.lua @@ -0,0 +1,32 @@ +local http = require("http") +local fs = require("fs") + +local args = {...} + +if not http then + error("HTTP is not enabled", 0) +end + +if #args == 0 then + print("Usage: wget [outputPath]") + return false +end + +local outputName = args[2] or fs.getName(args[1]) +local outputPath = fs.combine(shell.getDir(), outputName) + +if not http.checkURL(args[1]) then + error("Invalid URL", 0) +end + +local response, err = http.get(args[1]) +if not response then + error(err, 0) +end + +local file = fs.open(outputPath, "w") +file:write(response:readAll()) +file:close() +response:close() + +print("File written to " .. outputPath) \ No newline at end of file diff --git a/Capy64/Assets/Lua/boot/99_shell.lua b/Capy64/Assets/Lua/boot/99_shell.lua index 4156a43..1e939bf 100644 --- a/Capy64/Assets/Lua/boot/99_shell.lua +++ b/Capy64/Assets/Lua/boot/99_shell.lua @@ -6,8 +6,24 @@ term.setSize(51, 19) term.setForeground(0x59c9ff) term.setBackground(colors.black) term.clear() -term.setPos(1,1) +term.setPos(1, 1) print(os.version()) -dofile("/bin/lua.lua") \ No newline at end of file +local func, err = loadfile("/bin/shell.lua") +if func then + while true do + local ok, err = pcall(func) + if not ok then + print(err) + end + end +else + print(err) +end + + + +print("Press any key to continue...") +coroutine.yield("key_down") +os.shutdown(false) \ No newline at end of file diff --git a/Capy64/BIOS/RuntimeInputEvents.cs b/Capy64/BIOS/RuntimeInputEvents.cs index 7730cf1..691f25a 100644 --- a/Capy64/BIOS/RuntimeInputEvents.cs +++ b/Capy64/BIOS/RuntimeInputEvents.cs @@ -1,4 +1,5 @@ -using Capy64.Eventing; +using Capy64.Core; +using Capy64.Eventing; using Capy64.Eventing.Events; using Capy64.LuaRuntime; using Microsoft.Xna.Framework.Input; @@ -109,6 +110,20 @@ internal class RuntimeInputEvents BypassFilter = true, }); } + else if (e.Key == Keys.V) + { + if (SDL.HasClipboardText()) + { + var text = SDL.GetClipboardText(); + _runtime.PushEvent(new LuaEvent() + { + Name = "paste", + Parameters = new[] { + text, + }, + }); + } + } } } diff --git a/Capy64/Core/SDL.cs b/Capy64/Core/SDL.cs new file mode 100644 index 0000000..dd7b7b4 --- /dev/null +++ b/Capy64/Core/SDL.cs @@ -0,0 +1,21 @@ +using Capy64.Extensions.Bindings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Capy64.Core; + +public class SDL +{ + public static string GetClipboardText() + { + return SDL2.UTF8_ToManaged(SDL2.Native_SDL_GetClipboardText(), true); + } + + public static bool HasClipboardText() + { + return SDL2.SDL_HasClipboardText() == 1; + } +} diff --git a/Capy64/Extensions/Bindings/SDL2.cs b/Capy64/Extensions/Bindings/SDL2.cs index 1769827..89c2cfa 100644 --- a/Capy64/Extensions/Bindings/SDL2.cs +++ b/Capy64/Extensions/Bindings/SDL2.cs @@ -9,9 +9,83 @@ public partial class SDL2 [LibraryImport(SDL)] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] - public static partial void SDL_MaximizeWindow(IntPtr window); + internal static partial void SDL_MaximizeWindow(IntPtr window); [LibraryImport(SDL)] [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] - public static partial uint SDL_GetWindowFlags(IntPtr window); + internal static partial uint SDL_GetWindowFlags(IntPtr window); + + [LibraryImport(SDL, EntryPoint = "SDL_GetClipboardText")] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + internal static partial IntPtr Native_SDL_GetClipboardText(); + + /// + /// + /// 0 is false; 1 is true + [LibraryImport(SDL)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + internal static partial int SDL_HasClipboardText(); + + [LibraryImport(SDL)] + [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + internal static partial void SDL_free(IntPtr memblock); + + /// + /// https://github.com/flibitijibibo/SDL2-CS/blob/master/src/SDL2.cs#L128 + /// + /// + /// + /// + public static unsafe string UTF8_ToManaged(IntPtr s, bool freePtr = false) + { + if (s == IntPtr.Zero) + { + return null; + } + + /* We get to do strlen ourselves! */ + byte* ptr = (byte*)s; + while (*ptr != 0) + { + ptr++; + } + + /* TODO: This #ifdef is only here because the equivalent + * .NET 2.0 constructor appears to be less efficient? + * Here's the pretty version, maybe steal this instead: + * + string result = new string( + (sbyte*) s, // Also, why sbyte??? + 0, + (int) (ptr - (byte*) s), + System.Text.Encoding.UTF8 + ); + * See the CoreCLR source for more info. + * -flibit + */ +#if NETSTANDARD2_0 + /* Modern C# lets you just send the byte*, nice! */ + string result = System.Text.Encoding.UTF8.GetString( + (byte*) s, + (int) (ptr - (byte*) s) + ); +#else + /* Old C# requires an extra memcpy, bleh! */ + int len = (int)(ptr - (byte*)s); + if (len == 0) + { + return string.Empty; + } + char* chars = stackalloc char[len]; + int strLen = System.Text.Encoding.UTF8.GetChars((byte*)s, len, chars, len); + string result = new string(chars, 0, strLen); +#endif + + /* Some SDL functions will malloc, we have to free! */ + if (freePtr) + { + SDL_free(s); + } + return result; + } } diff --git a/Capy64/LuaRuntime/Libraries/FileSystem.cs b/Capy64/LuaRuntime/Libraries/FileSystem.cs index f06caf9..26020b6 100644 --- a/Capy64/LuaRuntime/Libraries/FileSystem.cs +++ b/Capy64/LuaRuntime/Libraries/FileSystem.cs @@ -117,7 +117,16 @@ public class FileSystem : IPlugin var absolutePath = Path.GetFullPath(path, rootPath); // Trim root from path - return absolutePath.Remove(0, rootPath.Length); + string localPath; + try + { + localPath = absolutePath.Remove(0, rootPath.Length); + } + catch + { + localPath = absolutePath; + } + return Path.Join("/", localPath); } public static string Resolve(string path) @@ -210,7 +219,7 @@ public class FileSystem : IPlugin parts.Add(pathPart); } - var result = Path.Join(parts.ToArray()); + var result = Path.Combine(parts.ToArray()); if (string.IsNullOrEmpty(result)) { L.PushString(""); diff --git a/Capy64/LuaRuntime/Libraries/HTTP.cs b/Capy64/LuaRuntime/Libraries/HTTP.cs index a47792f..323fa64 100644 --- a/Capy64/LuaRuntime/Libraries/HTTP.cs +++ b/Capy64/LuaRuntime/Libraries/HTTP.cs @@ -16,6 +16,7 @@ public class HTTP : IPlugin private static HttpClient _client; private static long RequestId; + private readonly IConfiguration _configuration; private readonly LuaRegister[] HttpLib = new LuaRegister[] { new() @@ -36,11 +37,13 @@ public class HTTP : IPlugin RequestId = 0; _client = new(); _client.DefaultRequestHeaders.Add("User-Agent", $"Capy64/{Capy64.Version}"); + _configuration = configuration; } public void LuaInit(Lua L) { - L.RequireF("http", Open, false); + if (_configuration.GetValue("HTTP:Enable")) + L.RequireF("http", Open, false); } private int Open(IntPtr state) diff --git a/Capy64/LuaRuntime/Runtime.cs b/Capy64/LuaRuntime/Runtime.cs index 61fb3fe..14ff38e 100644 --- a/Capy64/LuaRuntime/Runtime.cs +++ b/Capy64/LuaRuntime/Runtime.cs @@ -120,10 +120,13 @@ public class Runtime { if (Disposing) return false; - filters = new string[pars]; - for (int i = 0; i < pars; i++) + if (status == LuaStatus.Yield) { - filters[i] = Thread.OptString(i + 1, null); + filters = new string[pars]; + for (int i = 0; i < pars; i++) + { + filters[i] = Thread.OptString(i + 1, null); + } } Thread.Pop(pars); return status == LuaStatus.Yield;