Compare commits

...

17 commits

Author SHA1 Message Date
2445175248
Fix wget using obsolete file and HTTP Lua methods
Signed-off-by: Alessandro Proto <alex@alexdevs.me>
2024-05-27 18:03:31 +02:00
c19fb78b1f Update dependencies 2024-03-13 22:34:07 +01:00
5d0b6e864a Bump to .net 8 2024-03-13 22:33:09 +01:00
b2c3158d72 Bump version 2024-03-13 22:30:46 +01:00
1ba7185939 Replace MOTD lines with useful ones.
Add 2 new fun programs and improve Mandelbrot plotter
2024-03-13 21:00:59 +01:00
e0afdf177c Bugfix crash on invalid path chars 2023-08-31 16:24:36 +02:00
993b7f43f3
Add bios alert and colors
Signed-off-by: Alessandro Proto <alex@alexdevs.me>
2023-08-16 16:31:07 +02:00
74c9e25ad6 Disable scheduler due to bug 2023-08-15 20:28:21 +02:00
f5d6bbfaae Isolated shell environments 2023-08-15 18:04:17 +02:00
2bf4b17577 CapyOS 0.1.0 update 2023-07-28 18:16:39 +02:00
990b30b7bb Bugfix wrong directory for plugins 2023-07-28 18:15:30 +02:00
3326d0e73d Moved plugins folder to appdata.
Added config to disable plugins
2023-07-27 20:17:42 +02:00
f7d4b728f5 Add some details to appdata.xml 2023-07-24 21:52:21 +02:00
bf24c6e989 Add resource files for Linux and AppStream 2023-07-24 19:00:48 +02:00
Alessandro Proto
2b8eb54a8b Bugfix not using binary directory path for assets 2023-07-24 14:16:41 +02:00
Alessandro Proto
b85adb960b Remove dependency injection and hosting dependencies 2023-07-24 13:01:10 +02:00
d8d6e76729 Remove tools dependencies.
Add WIP Nix package
2023-07-24 06:54:27 +00:00
71 changed files with 1948 additions and 429 deletions

View file

@ -1,36 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.1.303",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.1.303",
"commands": [
"mgcb-editor-mac"
]
}
}
}

View file

@ -14,13 +14,10 @@
// limitations under the License.
using KeraLua;
using Microsoft.Extensions.DependencyInjection;
namespace Capy64.API;
public interface IComponent
{
void ConfigureServices(IServiceCollection services) { }
void LuaInit(Lua L) { }
}

View file

@ -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

View file

@ -1,4 +1,4 @@
local version = "0.0.3"
local version = "0.1.1"
local systemDirectory = "/sys"
print("Starting CapyOS")

View file

@ -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...] <alias> [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, " ")

View file

@ -0,0 +1,4 @@
local scheduler = require("scheduler")
scheduler.spawn(function()
shell.run(arg.string)
end)

View file

@ -0,0 +1,9 @@
local fs = require("fs")
local args = {...}
local path = shell.resolve(args[1])
local f<close> = fs.open(path, "r")
print(f:read("a"))
f:close()

View file

@ -0,0 +1,4 @@
local argparser = require("argparser")
local args, options = argparser.parse(...)
print(table.concat(args, " "))

View file

@ -1 +1 @@
shell.exit()
shell.exit()

View file

@ -0,0 +1,40 @@
local gpu = require("gpu")
local event = require("event")
local donuts = {}
local limit = 100
local w, h = gpu.getSize()
local function insert()
local donut = {
x = math.random(-20, w + 20),
y = math.random(-20, h + 20),
d = math.random() * math.pi*2,
dir = math.random(0, 1),
c = math.random(0xffffff),
life = math.random(100, 1000),
}
table.insert(donuts, donut)
end
while true do
if #donuts < limit then
insert()
end
gpu.clear(0)
for k, donut in ipairs(donuts) do
if donut.life <= 0 then
table.remove(donuts, k)
end
local doReverse = math.random(0, 1000) > 950
donut.x = donut.x + math.cos(donut.d) * 4
donut.y = donut.y + math.sin(donut.d) * 4
donut.d = donut.d + (donut.dir == 1 and 0.05 or -0.05)
gpu.drawCircle(donut.x, donut.y, 20, donut.c, 10)
if doReverse then
donut.dir = donut.dir == 1 and 0 or 1
end
donut.life = donut.life - 1
end
event.push("donuts")
event.pull("donuts")
end

View file

@ -38,15 +38,19 @@ local function iter(cr, ci)
end
local function draw()
local buffer <close> = gpu.newBuffer()
local size = w * h
local canvas = { string.unpack(("B"):rep(size), ("\0"):rep(size)) }
canvas[#canvas] = nil
for y = 0, h - 1 do
for x = 0, w - 1 do
local _, _, i = iter((x - cx + dx * pscale) * px, (y - cy + dy * pscale) * px)
buffer[y * w + x] = colorUnit * (iterations - i)
canvas[y * w + x] = colorUnit * (iterations - i)
end
end
local buffer <close> = gpu.bufferFrom(canvas, w, h)
gpu.setBuffer(buffer)
end

View file

@ -0,0 +1,209 @@
local event = require("event")
local gpu = require("gpu")
local colors = require("colors")
local term = require("term")
local timer = require("timer")
local w, h = gpu.getSize()
local tw, th = term.getSize()
local selectedColor = 1
local thickness = 4
local canvasW, canvasH = term.toRealPos(tw - 1, th + 1)
canvasW = canvasW - 3
local size = canvasW * canvasH
local canvas = {string.unpack(("B"):rep(size), ("\0"):rep(size))}
canvas[#canvas] = nil
local function drawCircle(buffer, x, y, radius, color)
radius = math.max(0, radius)
if radius == 0 then
buffer[x + buffer.width * y] = color
return
end
local width = buffer.width
local height = buffer.height
local index = function(x, y)
return y * width + x
end
local isValid = function(x, y)
return x >= 0 and x < width and y >= 0 and y < height
end
local setPixel = function(x, y, color)
if isValid(x, y) then
buffer[index(x, y)] = color
end
end
local drawFilledCirclePoints = function(cx, cy, x, y)
for dx = -x, x do
for dy = -y, y do
setPixel(cx + dx, cy + dy, color)
end
end
end
local drawCircleBresenham = function(cx, cy, radius)
local x = 0
local y = radius
local d = 3 - 2 * radius
drawFilledCirclePoints(cx, cy, x, y)
while y >= x do
x = x + 1
if d > 0 then
y = y - 1
d = d + 4 * (x - y) + 10
else
d = d + 4 * x + 6
end
drawFilledCirclePoints(cx, cy, x, y)
end
end
drawCircleBresenham(x, y, radius)
end
local function drawLine(buffer, x0, y0, x1, y1, color, thickness)
local width = canvasW
local height = canvasH
local index = function(x, y)
return y * width + x
end
local isValid = function(x, y)
return x >= 0 and x < width and y >= 0 and y < height
end
local setPixel = function(x, y)
if isValid(x, y) then
buffer[index(x, y)] = color
end
end
local drawLineBresenham = function()
local i = 0
local dx = math.abs(x1 - x0)
local dy = math.abs(y1 - y0)
local sx = x0 < x1 and 1 or -1
local sy = y0 < y1 and 1 or -1
local err = dx - dy
local majorAxis = dx > dy
while x0 ~= x1 or y0 ~= y1 do
for i = 0, thickness - 1 do
if majorAxis then
setPixel(x0, y0 + i)
else
setPixel(x0 + i, y0)
end
end
local err2 = 2 * err
if err2 > -dy then
err = err - dy
x0 = x0 + sx
end
if err2 < dx then
err = err + dx
y0 = y0 + sy
end
if i % 1024 == 0 then
--event.push("paint")
--event.pull("paint")
--timer.sleep(0)
end
end
end
drawLineBresenham()
end
local function drawUI()
term.setBackground(0)
term.clear()
for y = 1, 16 do
term.setPos(tw - 1, y)
term.setBackground(0)
term.setForeground(colors[y])
if selectedColor == y then
term.setBackground(colors[y])
term.write(" ")
else
term.write("##")
end
end
term.setPos(tw - 1, 17)
if selectedColor == 0 then
term.setBackground(colors.white)
term.setForeground(0)
else
term.setBackground(0)
term.setForeground(colors.white)
end
term.write("XX")
term.setPos(tw - 1, 18)
term.setBackground(colors.black)
term.setForeground(colors.white)
term.write(thickness)
gpu.drawLine(canvasW + 1, 0, canvasW, canvasH, colors.gray, 2)
local b<close> = gpu.bufferFrom(canvas, canvasW, canvasH)
gpu.drawBuffer(b, 0, 0, {
source = {
0, 0, canvasW, canvasH
}
})
end
local function contains(arr, val)
for i, v in ipairs(arr) do
if v == val then
return true
end
end
return false
end
local oldX, oldY
while true do
drawUI()
local ev, b, x, y = event.pull("mouse_down", "mouse_up", "mouse_move", "mouse_scroll")
local tx, ty = term.fromRealPos(x, y)
if ev == "mouse_up" then
if x >= canvasW then
if ty <= 16 then
selectedColor = ty
elseif ty == 17 then
selectedColor = 0
end
end
oldX, oldY = nil, nil
elseif ev == "mouse_down" or (ev == "mouse_move" and contains(b, 1)) then
if x < canvasW and y < canvasH then
--canvas[x + y * canvasW] = colors[selectedColor] or 0
--drawCircle(canvas, x, y, thickness - 2, colors[selectedColor])
drawLine(canvas, x, y, oldX or x, oldY or y, colors[selectedColor] or 0, thickness)
--gpu.drawLine(x, y, oldX or x, oldY or y, colors[selectedColor] or 0)
--canvas = gpu.getBuffer()
oldX, oldY = x, y
end
elseif ev == "mouse_scroll" then
local x, y, b = b, x, y
local tx, ty = term.fromRealPos(x, y)
if x >= canvasW and ty == 18 then
thickness = math.min(99, math.max(0, thickness - b))
end
end
end

View file

@ -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)

View file

@ -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))

View file

@ -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<close> = 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

View file

@ -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

View file

@ -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

View file

@ -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<close> = 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])

View file

@ -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...] <source> <target>")
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)

View file

@ -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, " "))

View file

@ -1,8 +0,0 @@
local timer = require("timer")
local machine = require("machine")
print("Goodbye!")
timer.sleep(1)
machine.reboot()

View file

@ -1,10 +1,16 @@
local fs = require("fs")
local argparser = require("argparser")
local args = { ... }
if #args == 0 then
print("Usage: rm <file>")
local args, options = argparser.parse(...)
if not args[1] or options.h or options.help then
print("Usage: rm [option...] <path>")
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)

View file

@ -2,40 +2,35 @@ 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 createPackageEnvironment = require("shell.package")
local useScheduler = false
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(command, filepath, args, argf)
local arg = { table.unpack(args, 2) }
arg[0] = path
arg[0] = command
arg.string = argf
local envPackage = createPackageEnvironment(filepath)
envPackage.loaded.scheduler = scheduler
return setmetatable({
shell = shell,
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
arg = arg,
scheduler = scheduler,
}, { __index = envPackage.loaded._G })
end
function shell.getDir()
@ -72,16 +67,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, path, args, argf)
local func, err = loadfile(path, "t", env)
@ -90,7 +93,21 @@ 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
if useScheduler then
local programTask, yielded = scheduler.spawn(run)
if yielded then
coroutine.yield("scheduler_task_end")
end
else
run()
end
if not ok then
io.stderr.print(err)
return false
@ -107,6 +124,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 <close> = 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

View file

@ -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,
})

View file

@ -19,6 +19,8 @@ if not http.checkURL(args[1]) then
error("Invalid URL", 0)
end
print("Connecting...")
local response, err = http.get(args[1], nil, {
binary = true,
})
@ -27,8 +29,8 @@ if not response then
end
local file <close> = fs.open(outputPath, "wb")
file:write(response:readAll())
file:write(response.content:read("a"))
file:close()
response:close()
response.content:close()
print("File written to " .. outputPath)
print("Downloaded to " .. outputPath)

View file

@ -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

View file

@ -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()

View file

@ -0,0 +1 @@
require("machine").shutdown()

View file

@ -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()

View file

@ -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

View file

@ -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 = {}

View file

@ -0,0 +1,3 @@
local x = math.random()
return x

View file

@ -0,0 +1,182 @@
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
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
local function resumeTask(task, yieldPars)
local pars = table.pack(coroutine.resume(task.thread, table.unpack(yieldPars)))
if pars[1] then
task.filters = table.pack(table.unpack(pars, 2))
return coroutine.status(task.thread) ~= "dead"
else
cascadeKill(task.pid, pars[2])
return false
end
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)
local parent = findParent()
if parent then
task.parent = parent.pid
table.insert(parent.children, pid)
end
task.filters = {}
task.children = {}
task.eventQueue = {}
task.skip = true
tasks[pid] = task
processes = processes + 1
return task, resumeTask(task, task.options.args)
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 task.skip then
task.skip = false
else
resumeTask(task, yieldPars)
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

View file

@ -0,0 +1,112 @@
local expect = require("expect").expect
local fs = require("fs")
local nativePackage = package
local function copyTable(source, target)
target = target or {}
for k, v in pairs(source) do
target[k] = v
end
return target
end
local hostPackages = copyTable(nativePackage._host)
local function createPreloadSearcher(envPackage)
return function(name)
if not envPackage.preload[name] then
return string.format("no field package.preload['%s']", name)
end
return envPackage.preload[name], ":preload:"
end
end
local function createLoaderSearcher(envPackage)
return function(name)
local path, err = envPackage.searchpath(name, envPackage.path)
if not path then
return err
end
local func, err = loadfile(path)
if not func then
return string.format("error loading module '%s' from file '%s':\t%s", name, path, err)
end
return func, path
end
end
local function createEnvironment(filePath)
local envPackage = {
cpath = nativePackage.cpath,
searchpath = nativePackage.searchpath,
config = nativePackage.config,
searchers = {},
loaded = {},
preload = {},
}
local dirName = fs.getDir(filePath)
--envPackage.path = string.format("%s/?.lua;%s/?/init.lua;", dirName, dirName) .. nativePackage.path
envPackage.path = nativePackage.path
envPackage.searchers[1] = createPreloadSearcher(envPackage)
envPackage.searchers[2] = createLoaderSearcher(envPackage)
local function envRequire(modname)
expect(1, modname, "string", "number")
modname = tostring(modname)
if envPackage.loaded[modname] then
return envPackage.loaded[modname]
end
local errorOutput = ""
local libFunction, libPath
for i = 1, #envPackage.searchers do
local par, path = envPackage.searchers[i](modname)
if type(par) == "function" then
libFunction, libPath = par, path
break
else
errorOutput = errorOutput .. "\n\t" .. par
end
end
if not libFunction then
error(string.format("module '%s' not found:%s", modname, errorOutput), 2)
end
local ok, par = pcall(libFunction)
if not ok then
error(par, 0)
end
if par == nil then
envPackage.loaded[modname] = true
return true
end
envPackage.loaded[modname] = par
return par, libPath
end
copyTable(hostPackages, envPackage.loaded)
envPackage.loaded.package = envPackage
local env_G = copyTable(envPackage.loaded._G or _G)
envPackage.loaded._G = env_G
env_G._G = env_G
envPackage.loaded._G.package = envPackage
envPackage.loaded._G.require = envRequire
return envPackage, envRequire
end
return createEnvironment

View file

@ -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

View file

@ -0,0 +1,2 @@
Welcome to CapyOS!
Run "programs" to get a list of available programs.

View file

@ -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

View file

@ -0,0 +1,3 @@
You can get started with Lua by following this manual: https://www.lua.org/manual/
Learn more about Capy64 by following the documentation: https://capy64.alexdevs.me/
Found a bug or would like to suggest a feature? https://github.com/Ale32bit/Capy64/issues

View file

@ -1,4 +1,4 @@
-- This file is part of Capy64 - https://github.com/Capy64/Capy64
-- This file is part of Capy64 - https://github.com/Capy64/Capy64
-- Copyright 2023 Alessandro "AlexDevs" Proto
--
-- Licensed under the Apache License, Version 2.0 (the "License").
@ -24,6 +24,9 @@ local event = require("event")
local bootSleep = 2
local bg = 0x0
local fg = 0xffffff
local setupbg = 0x0608a6
local setupfg = 0xffffff
local accent = 0xffea00
term.setForeground(fg)
term.setBackground(bg)
@ -97,11 +100,60 @@ local function promptKey()
event.pull("key_down")
end
local function alert(...)
local args = {...}
table.insert(args, "[ OK ]")
local lines = {}
local width = 0
local padding = 1
for k, v in ipairs(args) do
lines[k] = tostring(v)
width = math.max(width, #tostring(v))
end
lines[#lines] = nil
local okPad = string.rep(" ", (width - #args[#args]) // 2)
local okLine = string.format("%s%s", okPad, args[#args])
local w, h = term.getSize()
local cx, cy = w//2, h//2
local dx, dy = cx - (width + padding * 2) // 2, cy - #lines//2 - 1
local pad = string.rep(" ", padding)
local emptyLine = string.format("\u{258C}%s%s%s\u{2590}", pad, string.rep(" ", width), pad)
term.setPos(dx, dy)
print("\u{259B}" .. string.rep("\u{2580}", width + padding * 2) .. "\u{259C}")
for k, v in ipairs(lines) do
term.setPos(dx, dy + k)
local space = string.rep(" ", width - #v)
print(string.format("\u{258C}%s%s%s%s\u{2590}", pad, v, space, pad))
end
term.setPos(dx, dy + #lines + 1)
print(emptyLine)
term.setPos(dx, dy + #lines + 2)
local space = string.rep(" ", width - #okLine)
term.write(string.format("\u{258C}%s", pad))
term.setForeground(accent)
term.write(okLine)
term.setForeground(setupfg)
print(string.format("%s%s\u{2590}", space, pad))
term.setPos(dx, dy + #lines + 3)
print("\u{2599}" .. string.rep("\u{2584}", width + padding * 2) .. "\u{259F}")
local _, key, keyname
repeat
_, key, keyname = event.pull("key_down")
until keyname == "enter" or keyname == "escape"
end
local function installDefaultOS()
if fs.exists("/sys") then
fs.delete("/sys", true)
end
installOS()
alert("Default OS installed!")
end
term.setBlink(false)
@ -116,6 +168,7 @@ local function setupScreen()
"Install default OS",
installDefaultOS,
},
{},
{
"Exit setup",
exit,
@ -127,26 +180,40 @@ local function setupScreen()
}
local selection = 1
local function redraw()
term.setForeground(fg)
term.setBackground(bg)
local function redraw(noDrawSel)
local w, h = term.getSize()
term.setForeground(setupfg)
term.setBackground(setupbg)
term.clear()
term.setPos(1,2)
writeCenter("Capy64 Setup")
term.setPos(1,3)
term.setForeground(accent)
for k, v in ipairs(options) do
local _, y = term.getPos()
term.setPos(1, y + 1)
term.clearLine()
if selection == k then
writeCenter("[ " .. v[1] .. " ]")
else
writeCenter(v[1])
if #v > 0 then
if selection == k and not noDrawSel then
writeCenter("[ " .. v[1] .. " ]")
else
writeCenter(v[1])
end
end
end
term.setForeground(setupfg)
term.setPos(1, h - 2)
term.write("\u{2190}\u{2191}\u{2192}\u{2193}: Move")
term.setPos(1, h - 1)
term.write("Escape: Exit")
term.setPos(1, h)
term.write("Enter: Select")
end
while true do
@ -154,9 +221,16 @@ local function setupScreen()
local ev = { coroutine.yield("key_down") }
if ev[3] == "up" then
selection = selection - 1
if options[selection] and #options[selection] == 0 then
selection = selection - 1
end
elseif ev[3] == "down" then
selection = selection + 1
if options[selection] and #options[selection] == 0 then
selection = selection + 1
end
elseif ev[3] == "enter" then
redraw(true)
options[selection][2]()
elseif ev[3] == "escape" then
exit()
@ -177,7 +251,7 @@ local function bootScreen()
term.setPos(1,2)
writeCenter("Capy64")
term.setPos(1,4)
writeCenter("Powered by Capybaras")
writeCenter("(c) 2023 AlexDevs")
term.setPos(1, h - 1)
writeCenter("Press F2 to open setup")

View file

@ -1,5 +1,6 @@
{
"EngineMode": 0,
"SafeMode": false,
"Window": {
"Scale": 2
},

View file

@ -21,7 +21,6 @@ using Capy64.Integrations;
using Capy64.PluginManager;
using Capy64.Runtime;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
@ -30,6 +29,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using static Capy64.Utils;
@ -41,9 +41,9 @@ public enum EngineMode
Free
}
public class Capy64 : Game, IGame
public class Capy64 : Game
{
public const string Version = "1.1.0-beta";
public const string Version = "1.1.2-beta";
public static class DefaultParameters
{
@ -57,21 +57,24 @@ public class Capy64 : Game, IGame
public const int FreeTickrate = 60;
}
public static readonly string AssemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
public static readonly string AssetsPath = Path.Combine(AssemblyPath, "Assets");
public static string AppDataPath {
get {
public static string AppDataPath
{
get
{
string baseDir =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
?
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create)
: RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.Create)
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.Create)
:
"./";
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData,
Environment.SpecialFolderOption.Create) :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ?
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.Create) :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.Create) :
"./";
return Path.Combine(baseDir, "Capy64");
}
@ -91,10 +94,12 @@ public class Capy64 : Game, IGame
public Eventing.EventEmitter EventEmitter { get; private set; }
public DiscordIntegration Discord { get; set; }
public int TickRate => tickrate;
public IConfiguration Configuration { get; private set; }
public Color BorderColor { get; set; } = Color.Black;
public Borders Borders = new() {
public Borders Borders = new()
{
Top = 0,
Bottom = 0,
Left = 0,
@ -107,7 +112,6 @@ public class Capy64 : Game, IGame
private readonly InputManager _inputManager;
private RenderTarget2D renderTarget;
private readonly GraphicsDeviceManager _graphics;
private IServiceProvider _serviceProvider;
private ulong _totalTicks = 0;
private int tickrate = 0;
private int everyTick => 60 / tickrate;
@ -117,7 +121,7 @@ public class Capy64 : Game, IGame
Instance = this;
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
//Content.RootDirectory = "Content";
IsMouseVisible = true;
EventEmitter = new();
@ -126,11 +130,6 @@ public class Capy64 : Game, IGame
Drawing = new();
}
public void ConfigureServices(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void SetEngineMode(EngineMode mode)
{
switch (mode)
@ -181,7 +180,8 @@ public class Capy64 : Game, IGame
private void OnWindowSizeChange(object sender, EventArgs e)
{
if (EngineMode == EngineMode.Classic) {
if (EngineMode == EngineMode.Classic)
{
UpdateSize(true);
return;
}
@ -214,7 +214,8 @@ public class Capy64 : Game, IGame
private void ResetBorder()
{
var size = (int)(Scale * DefaultParameters.BorderMultiplier);
Borders = new Borders {
Borders = new Borders
{
Top = size,
Bottom = size,
Left = size,
@ -224,11 +225,26 @@ public class Capy64 : Game, IGame
protected override void Initialize()
{
var configuration = _serviceProvider.GetService<IConfiguration>();
var configBuilder = new ConfigurationBuilder();
var settingsPath = Path.Combine(AppDataPath, "settings.json");
if (!Directory.Exists(AppDataPath))
{
Directory.CreateDirectory(AppDataPath);
}
if (!File.Exists(settingsPath))
{
File.Copy(Path.Combine(AssetsPath, "default.json"), settingsPath);
}
configBuilder.AddJsonFile(Path.Combine(AssetsPath, "default.json"), false);
configBuilder.AddJsonFile(settingsPath, false);
Configuration = configBuilder.Build();
Window.Title = "Capy64 " + Version;
Scale = configuration.GetValue("Window:Scale", DefaultParameters.Scale);
Scale = Configuration.GetValue("Window:Scale", DefaultParameters.Scale);
ResetBorder();
UpdateSize();
@ -238,12 +254,14 @@ public class Capy64 : Game, IGame
InactiveSleepTime = new TimeSpan(0);
SetEngineMode(configuration.GetValue<EngineMode>("EngineMode", DefaultParameters.EngineMode));
SetEngineMode(Configuration.GetValue<EngineMode>("EngineMode", DefaultParameters.EngineMode));
Audio = new Audio();
NativePlugins = GetNativePlugins();
Plugins = PluginLoader.LoadAllPlugins("plugins", _serviceProvider);
var safeMode = Configuration.GetValue("SafeMode", false);
if (!safeMode)
Plugins = PluginLoader.LoadAllPlugins(Path.Combine(AppDataPath, "plugins"));
EventEmitter.RaiseInit();
@ -261,7 +279,7 @@ public class Capy64 : Game, IGame
foreach (var type in types)
{
var instance = (IComponent)ActivatorUtilities.CreateInstance(_serviceProvider, type)!;
var instance = (IComponent)Activator.CreateInstance(type, this);
plugins.Add(instance);
}
@ -281,7 +299,8 @@ public class Capy64 : Game, IGame
// Register user input
_inputManager.Update(IsActive);
EventEmitter.RaiseTick(new() {
EventEmitter.RaiseTick(new()
{
GameTime = gameTime,
TotalTicks = _totalTicks,
IsActiveTick = (int)_totalTicks % everyTick == 0,
@ -306,7 +325,8 @@ public class Capy64 : Game, IGame
SpriteBatch.Draw(renderTarget, new(Borders.Left, Borders.Top), null, Color.White, 0f, Vector2.Zero, Scale,
SpriteEffects.None, 0);
EventEmitter.RaiseOverlay(new() {
EventEmitter.RaiseOverlay(new()
{
GameTime = gameTime,
TotalTicks = _totalTicks,
});

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
@ -37,19 +37,17 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
<PackageReference Include="FontStashSharp.MonoGame" Version="1.2.8" />
<PackageReference Include="KeraLua" Version="1.3.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="FontStashSharp.MonoGame" Version="1.3.6" />
<PackageReference Include="KeraLua" Version="1.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="MonoGame.Extended.Graphics" Version="3.8.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
<PackageReference Include="System.ComponentModel.Composition" Version="7.0.0" />
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
<Message Text="Restoring dotnet tools" Importance="High" />
<Exec Command="dotnet tool restore" />
</Target>
<ItemGroup>
<EditorConfigFiles Remove="C:\Users\Alex\source\repos\Capy64\Capy64\Capy64\.editorconfig" />
</ItemGroup>

View file

@ -1,15 +0,0 @@
#----------------------------- Global Properties ----------------------------#
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:DesktopGL
/config:
/profile:Reach
/compress:False
#-------------------------------- References --------------------------------#
#---------------------------------- Content ---------------------------------#

View file

@ -54,7 +54,7 @@ public class Drawing : IDisposable
public Drawing()
{
_fontSystem = new FontSystem();
_fontSystem.AddFont(File.ReadAllBytes(@"Assets/font.ttf"));
_fontSystem.AddFont(File.ReadAllBytes(Path.Combine(Capy64.AssetsPath, "font.ttf")));
}
public void Begin()

View file

@ -1,50 +0,0 @@
// This file is part of Capy64 - https://github.com/Ale32bit/Capy64
// Copyright 2023 Alessandro "AlexDevs" Proto
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Capy64.API;
using Capy64.Core;
using Capy64.Integrations;
using Capy64.Runtime;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Capy64;
public interface IGame
{
Capy64 Game { get; }
EngineMode EngineMode { get; }
IList<IComponent> NativePlugins { get; }
IList<IComponent> Plugins { get; }
GameWindow Window { get; }
Drawing Drawing { get; }
Audio Audio { get; }
LuaState LuaRuntime { get; set; }
Eventing.EventEmitter EventEmitter { get; }
void ConfigureServices(IServiceProvider serviceProvider);
int Width { get; set; }
int Height { get; set; }
float Scale { get; set; }
void UpdateSize(bool resize = true);
event EventHandler<EventArgs> Exiting;
void Run();
void Exit();
// Integrations
DiscordIntegration Discord { get; }
}

View file

@ -28,9 +28,9 @@ public class DiscordIntegration : IComponent
public readonly bool Enabled;
private readonly IConfiguration _configuration;
public DiscordIntegration(IConfiguration configuration)
public DiscordIntegration(Capy64 game)
{
_configuration = configuration;
_configuration = game.Configuration;
var discordConfig = _configuration.GetSection("Integrations:Discord");
Enabled = discordConfig.GetValue("Enable", false);

View file

@ -14,7 +14,6 @@
// limitations under the License.
using Capy64.API;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
@ -33,7 +32,7 @@ internal class PluginLoader
return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(path)));
}
public static List<IComponent> LoadAllPlugins(string pluginsPath, IServiceProvider provider)
public static List<IComponent> LoadAllPlugins(string pluginsPath)
{
if (!Directory.Exists(pluginsPath))
Directory.CreateDirectory(pluginsPath);
@ -47,7 +46,7 @@ internal class PluginLoader
{
if (typeof(IComponent).IsAssignableFrom(type))
{
IComponent result = ActivatorUtilities.CreateInstance(provider, type) as IComponent;
IComponent result = Activator.CreateInstance(type, Capy64.Instance) as IComponent;
plugins.Add(result);
}
}

View file

@ -13,34 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using Capy64;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.IO;
using var game = new Capy64.Capy64();
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, c) =>
{
var settingsPath = Path.Combine(Capy64.Capy64.AppDataPath, "settings.json");
if (!Directory.Exists(Capy64.Capy64.AppDataPath))
{
Directory.CreateDirectory(Capy64.Capy64.AppDataPath);
}
if (!File.Exists(settingsPath))
{
File.Copy("Assets/default.json", settingsPath);
}
c.AddJsonFile("Assets/default.json", false);
c.AddJsonFile(settingsPath, false);
})
.ConfigureServices((hostContext, services) =>
{
services.AddSingleton<IGame>(game);
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
game.Run();

View file

@ -25,8 +25,8 @@ public class AudioLib : IComponent
{
private const int queueLimit = 8;
private static IGame _game;
public AudioLib(IGame game)
private static Capy64 _game;
public AudioLib(Capy64 game)
{
_game = game;
_game.EventEmitter.OnClose += OnClose;

View file

@ -29,8 +29,8 @@ public class EventLib : IComponent
private static bool FrozenTaskAwaiter = false;
private static IGame _game;
public EventLib(IGame game)
private static Capy64 _game;
public EventLib(Capy64 game)
{
_game = game;
}

View file

@ -111,6 +111,8 @@ public class FileSystemLib : IComponent
new(), // NULL
};
public FileSystemLib(Capy64 _) { }
public void LuaInit(Lua state)
{
// Add "fs" library to lua, not global (uses require())
@ -132,6 +134,9 @@ public class FileSystemLib : IComponent
// Get drive root (C:\ for Windows, / for *nix)
var rootPath = Path.GetFullPath(Path.GetPathRoot("/") ?? "/");
var invalidPathChars = Path.GetInvalidPathChars();
path = invalidPathChars.Aggregate(path, (current, invalidChar) => current.Replace(invalidChar, ' '));
// Join path to rootPath and resolves to absolute path
// Relative paths are resolved here (es. ../ and ./)
var absolutePath = Path.GetFullPath(path, rootPath);

View file

@ -28,8 +28,8 @@ namespace Capy64.Runtime.Libraries;
public class GPULib : IComponent
{
private static IGame _game;
public GPULib(IGame game)
private static Capy64 _game;
public GPULib(Capy64 game)
{
_game = game;
}

View file

@ -30,7 +30,7 @@ namespace Capy64.Runtime.Libraries;
#nullable enable
public class HTTPLib : IComponent
{
private static IGame _game = null!;
private static Capy64 _game = null!;
private static HttpClient _httpClient = null!;
private static long _requestId;
public static readonly HashSet<WebSocketClient.Client> WebSocketConnections = new();
@ -57,13 +57,13 @@ public class HTTPLib : IComponent
},
new(),
};
public HTTPLib(IGame game, IConfiguration configuration)
public HTTPLib(Capy64 game)
{
_game = game;
_requestId = 0;
_httpClient = new();
_httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
_configuration = configuration;
_configuration = game.Configuration;
}
public void LuaInit(Lua L)

View file

@ -24,8 +24,8 @@ namespace Capy64.Runtime.Libraries;
public class MachineLib : IComponent
{
private static IGame _game;
public MachineLib(IGame game)
private static Capy64 _game;
public MachineLib(Capy64 game)
{
_game = game;
}

View file

@ -49,11 +49,11 @@ internal class TermLib : IComponent
public static Color BackgroundColor { get; set; }
private static Char?[] CharGrid;
private static IGame _game;
private static Capy64 _game;
private static bool cursorState = false;
private static bool enableCursor = true;
private static Texture2D cursorTexture;
public TermLib(IGame game)
public TermLib(Capy64 game)
{
_game = game;

View file

@ -50,11 +50,11 @@ class TimerLib : IComponent
new(),
};
private static IGame _game;
private static Capy64 _game;
private static uint _timerId = 0;
private static readonly ConcurrentDictionary<uint, Timer> timers = new();
public TimerLib(IGame game)
public TimerLib(Capy64 game)
{
_game = game;

View file

@ -25,8 +25,8 @@ public class ObjectManager : IComponent
{
private static readonly ConcurrentDictionary<nint, object> _objects = new();
private static IGame _game;
public ObjectManager(IGame game)
private static Capy64 _game;
public ObjectManager(Capy64 game)
{
_game = game;
_game.EventEmitter.OnClose += OnClose;

View file

@ -88,6 +88,8 @@ public class FileHandle : IComponent
new(),
};
public FileHandle(Capy64 _) { }
public void LuaInit(Lua L)
{
CreateMeta(L);

View file

@ -66,8 +66,8 @@ public class GPUBufferMeta : IComponent
new(),
};
private static IGame _game;
public GPUBufferMeta(IGame game)
private static Capy64 _game;
public GPUBufferMeta(Capy64 game)
{
_game = game;
}

View file

@ -29,8 +29,8 @@ public class Socket : IDisposable
public class SocketLib : IComponent
{
private static IGame _game = null!;
public SocketLib(IGame game)
private static Capy64 _game = null!;
public SocketLib(Capy64 game)
{
_game = game;
}

View file

@ -21,8 +21,8 @@ namespace Capy64.Runtime.Objects;
public class TaskMeta : IComponent
{
private static IGame _game;
public TaskMeta(IGame game)
private static Capy64 _game;
public TaskMeta(Capy64 game)
{
_game = game;
}

View file

@ -73,6 +73,8 @@ public class WebSocketClient : IComponent
new(),
};
public WebSocketClient(Capy64 _) { }
public void LuaInit(Lua L)
{
CreateMeta(L);

View file

@ -32,8 +32,8 @@ internal class RuntimeManager : IComponent
private static bool close = false;
private static bool inPanic = false;
private static IGame _game;
public RuntimeManager(IGame game)
private static Capy64 _game;
public RuntimeManager(Capy64 game)
{
_game = game;
@ -124,7 +124,7 @@ internal class RuntimeManager : IComponent
luaState.Thread.PushCFunction(L_Exit);
luaState.Thread.SetGlobal("exit");
var status = luaState.Thread.LoadFile("Assets/Lua/bios.lua");
var status = luaState.Thread.LoadFile(Path.Combine(Capy64.AssetsPath, "Lua/bios.lua"));
if (status != LuaStatus.OK)
{
throw new LuaException(luaState.Thread.ToString(-1));
@ -136,6 +136,7 @@ internal class RuntimeManager : IComponent
luaState = new LuaState();
_game.LuaRuntime = luaState;
luaState.Init();
CopyHostLibraries();
emitter = new(_game.EventEmitter, luaState);
@ -162,11 +163,31 @@ internal class RuntimeManager : IComponent
}
}
private void CopyHostLibraries()
{
// table that will contain host libraries
luaState.Thread.NewTable();
luaState.Thread.GetGlobal("package");
luaState.Thread.GetField(-1, "loaded");
luaState.Thread.PushNil();
while (luaState.Thread.Next(-2))
{
var libname = luaState.Thread.ToString(-2);
luaState.Thread.SetField(1, libname);
}
luaState.Thread.Rotate(1, -1);
luaState.Thread.SetField(1, "_host");
luaState.Thread.SetTop(0);
}
private void LoadFirmware()
{
var firmwareContent = File.ReadAllText("Assets/Lua/firmware.lua");
var firmwareContent = File.ReadAllText(Path.Combine(Capy64.AssetsPath, "Lua/firmware.lua"));
var errored = luaState.Thread.DoString(firmwareContent);
if(errored)
if (errored)
{
throw new LuaException(luaState.Thread.ToString(-1));
}
@ -194,7 +215,7 @@ internal class RuntimeManager : IComponent
var installedFilePath = Path.Combine(Capy64.AppDataPath, ".installed");
if (!File.Exists(installedFilePath) || force)
{
FileSystemLib.CopyDirectory("Assets/Lua/CapyOS", FileSystemLib.DataPath, true, true);
FileSystemLib.CopyDirectory(Path.Combine(Capy64.AssetsPath, "Lua/CapyOS"), FileSystemLib.DataPath, true, true);
File.Create(installedFilePath).Dispose();
}
}

View file

@ -1,73 +0,0 @@
// This file is part of Capy64 - https://github.com/Ale32bit/Capy64
// Copyright 2023 Alessandro "AlexDevs" Proto
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Capy64;
public class Worker : IHostedService
{
private readonly IGame _game;
private readonly IHostApplicationLifetime _appLifetime;
private readonly IServiceProvider _serviceProvider;
public Worker(IGame game, IHostApplicationLifetime appLifetime, IServiceProvider serviceProvider)
{
_game = game;
_appLifetime = appLifetime;
_serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
_game.Exiting += OnGameExiting;
_game.ConfigureServices(_serviceProvider);
return Task.CompletedTask;
}
private void OnGameExiting(object sender, EventArgs e)
{
StopAsync(new CancellationToken());
}
public Task StopAsync(CancellationToken cancellationToken)
{
_appLifetime.StopApplication();
return Task.CompletedTask;
}
private void OnStarted()
{
_game.Run();
}
private void OnStopping()
{
}
private void OnStopped()
{
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>

View file

@ -1,13 +1,12 @@
using Capy64;
using Capy64.API;
using Capy64.API;
using KeraLua;
namespace ExamplePlugin;
public class MyPlugin : IComponent
{
private static IGame _game;
public MyPlugin(IGame game)
private static Capy64.Capy64 _game;
public MyPlugin(Capy64.Capy64 game)
{
_game = game;
}

BIN
Resources/256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

11
Resources/Capy64.desktop Normal file
View file

@ -0,0 +1,11 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Capy64
Comment=Lua Fantasy Computer
Categories=Game;Emulator;
Icon=me.alexdevs.Capy64
Exec=Capy64
Terminal=false

37
Resources/appdata.xml Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>me.alexdevs.Capy64</id>
<launchable type="desktop-id">me.alexdevs.Capy64.desktop</launchable>
<name>Capy64</name>
<summary>Lua Fantasy Computer</summary>
<developer_name>AlexDevs</developer_name>
<url>https://capy64.alexdevs.me/</url>
<metadata_license>MIT</metadata_license>
<project_license>Apache-2.0</project_license>
<content_rating type="oars-1.1" />
<supports>
<control>pointing</control>
<control>keyboard</control>
<control>gamepad</control>
</supports>
<description>
<p>
Capy64 is a fantasy console that runs sandbox Lua 5.4.
</p>
<p>
Create anything, from Hello Worlds to games!
</p>
</description>
<releases>
<release version="1.1.2" date="2023-07-24" type="beta">
<url>https://github.com/Ale32bit/Capy64/releases/tag/v1.1.2-beta</url>
</release>
</releases>
</component>

19
default.nix Normal file
View file

@ -0,0 +1,19 @@
{ pkgs ? import <nixpkgs> { system = builtins.currentSystem; } }:
pkgs.buildDotnetModule rec {
pname = "Capy64";
version = "1.1.0-beta";
src = ./.;
projectFile = "Capy64/Capy64.csproj";
nugetDeps = ./deps.nix;
dotnet-sdk = pkgs.dotnetCorePackages.sdk_7_0;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_7_0;
meta = with pkgs.lib; {
homepage = "https://github.com/Ale32bit/Capy64";
description = "Capy64";
license = with licenses; [ asl20 ];
};
}

53
deps.nix Normal file
View file

@ -0,0 +1,53 @@
# This file was automatically generated by passthru.fetch-deps.
# Please dont edit it manually, your changes might get overwritten!
{ fetchNuGet }: [
(fetchNuGet { pname = "Cyotek.Drawing.BitmapFont"; version = "2.0.4"; sha256 = "04n0lq9sqfjzyvkqav6qrc8v9fb7jv1n7jk0j4r6ivfbmffzq8if"; })
(fetchNuGet { pname = "DiscordRichPresence"; version = "1.1.3.18"; sha256 = "0p4bhaggjjfd4gl06yiphqgncxgcq2bws4sjkrw0n2ldf3hgrps3"; })
(fetchNuGet { pname = "FontStashSharp.Base"; version = "1.1.8"; sha256 = "00b4x3hcldp1f6g32khhyv32z127ab6rcgmd4b88k1534vv128bp"; })
(fetchNuGet { pname = "FontStashSharp.MonoGame"; version = "1.2.8"; sha256 = "0qgp1i54zm27qxilxbpznv9s3mgf831x7xz5z3w66zcp53qgyk93"; })
(fetchNuGet { pname = "FontStashSharp.Rasterizers.StbTrueTypeSharp"; version = "1.1.8"; sha256 = "1rnnwx748378hdbfvldxzacwc710ivvlcjzm9qi2pdh8qcz4v6db"; })
(fetchNuGet { pname = "KeraLua"; version = "1.3.3"; sha256 = "1y76f6582fld1jpbkawv26344p6xwdk07xj3im25p8yf2qcw8yln"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration"; version = "7.0.0"; sha256 = "0n1grglxql9llmrsbbnlz5chx8mxrb5cpvjngm0hfyrkgzcwz90d"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.Abstractions"; version = "7.0.0"; sha256 = "1as8cygz0pagg17w22nsf6mb49lr2mcl1x8i3ad1wi8lyzygy1a3"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.Binder"; version = "7.0.3"; sha256 = "1n59jk6kqqy5f0gfx99b4j2m2clylznvxj1dwm1fjn3gmh2pi35v"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.CommandLine"; version = "7.0.0"; sha256 = "1pmgjrvwdzqrxjb24cg3fd624r64lgywbqc9symd5hyl4175pwk8"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.EnvironmentVariables"; version = "7.0.0"; sha256 = "0nhh7rnh45s39x8sjn88czg7nyfpry85pkm0g619j8b468zj8nb4"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.FileExtensions"; version = "7.0.0"; sha256 = "1fk7dcz6gfhd1k1d8ksz22rnjvj1waqjzk29ym4i3dz73rsq8j1i"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.Json"; version = "7.0.0"; sha256 = "05zjmrpp99l128wijp1fy8asskc11ls871qaqr4mjnz3gbfycxnj"; })
(fetchNuGet { pname = "Microsoft.Extensions.Configuration.UserSecrets"; version = "7.0.0"; sha256 = "0ks7lcyvfvr3ar36f5gp89bnnblxzic5vawppfcrvhw1ivas4mp1"; })
(fetchNuGet { pname = "Microsoft.Extensions.DependencyInjection"; version = "7.0.0"; sha256 = "121zs4jp8iimgbpzm3wsglhjwkc06irg1pxy8c1zcdlsg34cfq1p"; })
(fetchNuGet { pname = "Microsoft.Extensions.DependencyInjection.Abstractions"; version = "7.0.0"; sha256 = "181d7mp9307fs17lyy42f8cxnjwysddmpsalky4m0pqxcimnr6g7"; })
(fetchNuGet { pname = "Microsoft.Extensions.FileProviders.Abstractions"; version = "7.0.0"; sha256 = "0ff20yklyjgyjzdyv7sybczgqhgd557m05dbwxzjznr0x41b180d"; })
(fetchNuGet { pname = "Microsoft.Extensions.FileProviders.Physical"; version = "7.0.0"; sha256 = "1f1h0l47abw0spssd64qkhgd7b54pyzslyb586zp21milimcfmgv"; })
(fetchNuGet { pname = "Microsoft.Extensions.FileSystemGlobbing"; version = "7.0.0"; sha256 = "1812vnkn8n0i4yr3k5azcxcfx1bbpcsmms95rdyxjfrzfksr05ai"; })
(fetchNuGet { pname = "Microsoft.Extensions.Hosting"; version = "7.0.1"; sha256 = "1044pm14ark2d7xiqll1cykawmp6m2f3ba9w6nfw0r3a2wfbmnxn"; })
(fetchNuGet { pname = "Microsoft.Extensions.Hosting.Abstractions"; version = "7.0.0"; sha256 = "1h5szfsr1dalsvdj9c18y6362853chisfns0hfpsq44hz0pr8j9q"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging"; version = "7.0.0"; sha256 = "1bqd3pqn5dacgnkq0grc17cgb2i0w8z1raw12nwm3p3zhrfcvgxf"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Abstractions"; version = "7.0.0"; sha256 = "1gn7d18i1wfy13vrwhmdv1rmsb4vrk26kqdld4cgvh77yigj90xs"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Configuration"; version = "7.0.0"; sha256 = "1f5fhpvzwyrwxh3g1ry027s4skmklf6mbm2w0p13h0x6fbmxcb24"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Console"; version = "7.0.0"; sha256 = "1m8ri2m3vlv9vzk0068jkrx0vkk4sqmk1kxmn8pc3wys38d38qaf"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Debug"; version = "7.0.0"; sha256 = "14p7hrhdd58fxdhjbyjwmlzr00vs03bmns3sf2f6alsgpvbf2h1i"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.EventLog"; version = "7.0.0"; sha256 = "0q1cgi456shngxs70ar0ibshpm5qk8whw369jrl6xdxnf1vxkkq8"; })
(fetchNuGet { pname = "Microsoft.Extensions.Logging.EventSource"; version = "7.0.0"; sha256 = "11rskmrijf6xv78slm38zywj6l3wjlm017kijhan1kfg56f1kvdk"; })
(fetchNuGet { pname = "Microsoft.Extensions.Options"; version = "7.0.1"; sha256 = "0ghz4y4gxnf2vw8yvhz9nkw21p6q2qqwh19phkk1xwxywyilr3mq"; })
(fetchNuGet { pname = "Microsoft.Extensions.Options.ConfigurationExtensions"; version = "7.0.0"; sha256 = "1liyprh0zha2vgmqh92n8kkjz61zwhr7g16f0gmr297z2rg1j5pj"; })
(fetchNuGet { pname = "Microsoft.Extensions.Primitives"; version = "7.0.0"; sha256 = "1b4km9fszid9vp2zb3gya5ni9fn8bq62bzaas2ck2r7gs0sdys80"; })
(fetchNuGet { pname = "Microsoft.NETCore.Platforms"; version = "2.0.0"; sha256 = "1fk2fk2639i7nzy58m9dvpdnzql4vb8yl8vr19r2fp8lmj9w2jr0"; })
(fetchNuGet { pname = "Microsoft.Win32.Registry"; version = "4.5.0"; sha256 = "1zapbz161ji8h82xiajgriq6zgzmb1f3ar517p2h63plhsq5gh2q"; })
(fetchNuGet { pname = "MonoGame.Content.Builder.Task"; version = "3.8.1.303"; sha256 = "17r9aamc3wlxnvjf8s5ndsz9p6qynyklkwhawjkxsij2cw1x1syp"; })
(fetchNuGet { pname = "MonoGame.Extended"; version = "3.8.0"; sha256 = "0xgk6z3d091wd6naqiwlhp649qxvp113w5f0nddm02mh5f63mmxw"; })
(fetchNuGet { pname = "MonoGame.Extended.Graphics"; version = "3.8.0"; sha256 = "0kj4adbhds2mvd9z1sym35ms0kr4f4b62gnpbws2whc29nixs5fi"; })
(fetchNuGet { pname = "MonoGame.Framework.DesktopGL"; version = "3.8.1.303"; sha256 = "12x7ajkh254x6l1s3i4q0j3pm5p7n95xysix19wvx3x2y6pp10nd"; })
(fetchNuGet { pname = "Newtonsoft.Json"; version = "12.0.3"; sha256 = "17dzl305d835mzign8r15vkmav2hq8l6g7942dfjpnzr17wwl89x"; })
(fetchNuGet { pname = "Newtonsoft.Json"; version = "13.0.1"; sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb"; })
(fetchNuGet { pname = "StbImageSharp"; version = "2.27.13"; sha256 = "0zqnzgnkx3c9ycyk25jjcrl8wrg3pcgk06lxf3hvsplwxwldc8fa"; })
(fetchNuGet { pname = "StbTrueTypeSharp"; version = "1.26.11"; sha256 = "1wh2jmym3pvyw8ywrwas3qgqswc6kr4az1bxbn9vb1m3vba0qjn4"; })
(fetchNuGet { pname = "System.ComponentModel.Composition"; version = "7.0.0"; sha256 = "1gkn56gclkn6qnsvaw5fzw6qb45pa7rffxph1gyqhq7ywvmm0nc3"; })
(fetchNuGet { pname = "System.Diagnostics.DiagnosticSource"; version = "7.0.1"; sha256 = "1ajh5y33apcypz5pnvzxsx44n95ciskzpvbxk0kfg7n9li3nfs1v"; })
(fetchNuGet { pname = "System.Diagnostics.EventLog"; version = "7.0.0"; sha256 = "16p8z975dnzmncfifa9gw9n3k9ycpr2qvz7lglpghsvx0fava8k9"; })
(fetchNuGet { pname = "System.Security.AccessControl"; version = "4.5.0"; sha256 = "1wvwanz33fzzbnd2jalar0p0z3x0ba53vzx1kazlskp7pwyhlnq0"; })
(fetchNuGet { pname = "System.Security.Principal.Windows"; version = "4.5.0"; sha256 = "0rmj89wsl5yzwh0kqjgx45vzf694v9p92r4x4q6yxldk1cv1hi86"; })
(fetchNuGet { pname = "System.Text.Encodings.Web"; version = "7.0.0"; sha256 = "1151hbyrcf8kyg1jz8k9awpbic98lwz9x129rg7zk1wrs6vjlpxl"; })
(fetchNuGet { pname = "System.Text.Json"; version = "7.0.0"; sha256 = "0scb0lp7wbgcinaa4kqiqs7b8i5nx4ppfad81138jiwd1sl37pyp"; })
]