🔧 master: work in progress [might break]

This commit is contained in:
Leon van Kammen 2026-07-23 15:20:25 +02:00
parent 9dd86155c6
commit 21152b128c
21 changed files with 363 additions and 66 deletions

3
.env
View file

@ -1,7 +1,8 @@
# aliases
build(){ $(pwd)/util/xrforge.sh build; }
rebuild(){ $(pwd)/util/xrforge.sh rebuild; }
# server hook
# forgejo server hook
#test "$GITEA_ROOT_URL" = "https://xrforge.isvery.ninja/" && {
# cp -r example/assets/* .
#}

5
.gitignore vendored
View file

@ -1,2 +1,5 @@
experiences
experienceA
experienceB
result
src/v
janusweb*

View file

@ -1,10 +1,3 @@
```
$ source .env # not needed if your system has autoenv
$ util/xrforge.sh build # fast: repeatable build
$ nix-build # slow: reproducable nix build (for experts)
```
NOTES:
```
@ -44,6 +37,21 @@ curl -L -X 'GET' 'http://xrforge.isvery.ninja/models?page=1&order=name' -H
curl -L -X 'GET' 'http://xrforge.isvery.ninja/models?page=1&order=recent' -H 'accept: application/vnd.manyfold.v0+json'
```
## Developing
```bash
$ source .env # not needed if your system has autoenv
$ build # fast: repeatable build
$ rebuild # faster: lua-repeatable build
$ nix-build # slow: reproducable nix build (for experts)
```
Then run with debug flag to see output of `trace()` calls
```bash
$ DEBUG=1 result/bin/xrforge.com
```
## Permacomputing Spirit
* developed on [Dillo](https://dillo-browser.github.io/) as lowest common denominator / compatibility

View file

@ -1,8 +1,8 @@
-H
"Access-Control-Allow-Origin: *"
Access-Control-Allow-Origin: *
-H
"Access-Control-Allow-Method: GET, POST, OPTIONS"
Access-Control-Allow-Method: GET, POST, OPTIONS
-H
"Allow: GET, POST, OPTIONS"
Allow: GET, POST, OPTIONS
-D
.

View file

@ -2,6 +2,7 @@ package.path = package.path .. ";.lua/?.lua"
package.path = package.path .. ";src/.lua/?.lua"
package.path = package.path .. ";middleware/?.lua"
package.path = package.path .. ";cmd/?.lua"
package.path = package.path .. ";plugin/?.lua"
print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on
@ -12,27 +13,30 @@ json = require "json"
HidePath('/usr/share/zoneinfo/')
HidePath('/usr/share/ssl/')
dir = require "opendirectory"
-- create
app = require("soakbean") {
bin = "./xrforge.com",
bindir = arg[-1]:gsub("xrforge.com",""),
cache = {},
cache = { items = {} },
plugin = {},
opts = {},
cmd={
help = {file="cmd/help.lua", info="display all possible flags"},
server = {file="cmd/server.lua", info="starts the webserver"}
server = {file="cmd/server.lua", info="starts the webserver"},
createplugin = {file="cmd/createplugin.lua", info="generates skeleton plugin"}
}
}
-- api skeleton (implemented via soakbean's app.on(...))
app.set = function(k,v) end
app.init = function() end
app.scan = require('scan').scan
-- load config
app.opts = require('config')(app)
-- load plugins
app.plugin.janusxr = require('plugin/janusxr')
app.runcmd(app) -- run clicmds
@ -62,16 +66,17 @@ app --
.use( app.response() ) -- try serve app response (if any)
.use( function(req,next) Route() end) -- fallback default redbean fileserver
function OnHttpRequest() app.run() end
app.on('init', function()
if assert(unix.fork()) == 0 then
os.execute("ls ; sleep 5")
--os.execute("ls ; sleep 5")
app.scan()
print("ok")
--local ok, headers, body = Fetch('http://127.0.0.1:8080/test')
end
end)
function OnHttpRequest() app.run() end
OnServerStart = function()
app.init() -- this triggers init event
end

View file

@ -36,7 +36,23 @@ return function(app)
if k == 'app.opts' then
app.opts = v
app.opts.updated = os.time()
local newconfig = "app.on('init', function()\n\tprint('config loaded')\nend)\n\nreturn " .. util.dump(app.opts)
local newconfig = [[
-- TIP: run 'xrforge.com createplugin' to start writing plugins
app.on('init', function()
print('config loaded')
local scan = app.scan
app.scan = function()
os.execute("echo scan before-hook")
scan()
os.execute("echo scan after-hook")
end
end)
]] .. "\n\nreturn " .. util.dump(app.opts)
newconfig = newconfig:gsub("^}$"," init = function() print('xrforge.lua config loaded') end\n}")
util.writefile( app.bindir .. configfile, newconfig )
print("stored xrforge.lua")

View file

@ -46,9 +46,10 @@ dir.foreach = function(path,cb)
if name == "." then goto continue end
attr,err = unix.stat(path..'/'..name, unix.AT_SYMLINK_NOFOLLOW)
if err then print("error: ".. name .. ": " ..err) end
cb(path,stat,state,name,kind,ino,off,attr)
cb(path,stat,state,name,kind,ino,off,attr, path .. "/" ..name)
::continue::
end
return state
end
dir.serve = function(folder) -- raw redbean function (use dir.foreach in soakbean)
@ -97,7 +98,7 @@ dir.serve = function(folder) -- raw redbean function (use dir.foreach in soakbea
Write('<tbody>\r\n')
local stat, state = dir.scan(path)
dir.foreach(path, function(path,stat,state,name,kind,ino,off,attr)
dir.foreach(path, function(path,stat,state,name,kind,ino,off,attr,fullpath)
Write('<tr>\r\n')
Write('<td>')
local link = EscapeHtml(name)

1
src/.lua/plugin Symbolic link
View file

@ -0,0 +1 @@
../plugin

50
src/.lua/scan.lua Normal file
View file

@ -0,0 +1,50 @@
local dir = require "opendirectory"
local scan
scan = {
shouldHideFile = function(name)
return name == ".."
or name == "util" -- repository
or name == ".git" -- files
or name == "src" --
or name == "result" --
end,
scan = function()
-- scan dirs
local dref = dir.foreach( app.opts.disk_dir, function(path,stat,state,name,kind,ino,off,attr,fullpath)
if kind == unix.DT_DIR and not scan.shouldHideFile(name) then
local experience = {
path = fullpath,
name = name,
tag = {},
file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
}
app.pub("scan_experience", nil, experience)
print(" ".. fullpath)
-- scan files
local fref = dir.foreach(fullpath, function(path,stat,state,name,kind,ino,off,attr, fullpath)
if kind ~= unix.DT_DIR and not scan.shouldHideFile(name) then
experience.file = {stat=stat,state=state,kind=kind,ino=ino,off=off,attr=attr,fullpath=fullpath,name=name,path=path}
app.pub("scan_experience_file", nil, experience)
local size = attr:size()
trace(" ".. fullpath)
end
end)
fref:close()
end
end)
dref:close()
foreach( app.cache.items, function(k,v)
-- remove file attributes (which are closed already)
v.file.stat = nil
v.file.attr = nil
v.file.state = nil
end)
end
}
return scan

View file

@ -197,9 +197,9 @@ sb = {
for k,v in pairs(argv) do
app.opts[ v:gsub("=.*","") ] = v:gsub(".*=","")
end
for k,v in pairs(app.opts) do
if app.cmd[k] then
local file = app.cmd[k].file
for k,v in pairs(argv) do
if app.cmd[v] then
local file = app.cmd[v].file
return require( file:sub(0,-5) )(app,argv)
end
end

View file

@ -69,6 +69,16 @@ util.template = function(content,data)
return app.tpl( content, mdata )
end
util.contains = function(tab,k_or_v)
local result = false
foreach( tab, function(k,v)
if k == k_or_v or v == k_or_v then
result = true
end
end)
return result
end
util.memoize = function(fn, ttl_seconds)
local cached_results = nil
local last_executed = 0

48
src/cmd/createplugin.lua Normal file
View file

@ -0,0 +1,48 @@
return function(app,argv)
print( [[
--
-- 1. put the code below in a file ('xrforge.myplugin.lua' e.g.)
-- 2. make sure to require it at the top of xrforge.lua (require('xrforge.myplugin') e.g.)
-- 3. profit!
--
app.on('scan_experience', function(exp)
print_r(exp)
end)
app.on('scan_experience_file', function(exp)
print_r(exp)
end)
app.on('set', function(k,v)
print("content was updated")
end)
-- plugin UI
local plugin = {
name = "MyPlugin",
info = "description / usage of your plugin goes here (html supported)",
opts = {
{
key='app.opts.plugin_myplugin_foo',
title='foo title',
info='Enable or disable foo',
default="yes",
values={yes="yes",no="no",maybe="maaaaybe"}
},
{
key='app.opts.plugin_myplugin_bar',
title='foo title',
info='Name your bar',
default="bar",
values=""
}
}
}
return plugin
]] )
os.exit()
end

View file

@ -235,6 +235,15 @@ img.icon{ display:none; }
font-weight: normal;
}
b.tag{
display:inline-block;
background: #77f;
color: white;
font-size: 12px;
padding: 1px 4px;
border-radius: 5px;
}
/* progressive enhancement quirks */
.spectrum{ display:none }

View file

@ -39,11 +39,12 @@ body{
}
.grid .tile .text{
padding: 10px 20px 10px 20px !important;
padding: 10px 20px 10px 15px !important;
font-size:14px;
}
.grid .tile img.icon{
transform: translate(0,5px);
transform: translate(0,-3px);
}
.clear{
@ -69,11 +70,11 @@ img.icon{
iframe#viewer{
position: fixed;
width: 100%;
height: calc( 100vh - 59px);
height: calc( 99.7vh - 59px);
margin-top: 0px;
background: #FFF;
left: 0;
top: 70px;
top: 62px;
}
/* telescopic text */

View file

@ -56,7 +56,7 @@ ${notification}
Keywords
</td>
<td >
<input type="text" value="${opts.hide_keywords}"></input>
<input type="text" name="opts.hide_keywords" value="${opts.hide_keywords}"></input>
</td>
<td class="col3">
Space-separated, <a href="http://lua-users.org/wiki/PatternsTutorial" target="_blank">lua patterns</a> are allowed
@ -145,13 +145,17 @@ ${notification}
</td>
</tr>
</table>
<div style="margin-left:165px">
</div>
<br/>
<br/>
${plugins}
<div style="margin-left:187px">
<div class="btn">
<input type="submit" class="btn secondary" value="Save" style=""/>
</div>
</div>
</div>
</form>
${footer}

View file

@ -1,7 +1,57 @@
local util = require("util")
local data = util.merge({notification=''},{ opts = app.opts })
data.plugins = ""
function pluginInput(opt)
local html = ""
html = html .. [[
<tr>
<td>
]]
html = html .. opt.title
html = html .. [[
</td>
<td>
]]
if type(opt.values) == 'table' then
html = html .. util.template('<select name="${key}" value="${value}">\n', {
key = opt.key,
value = app.opts[key]
})
foreach( opt.values, function(k,v)
html = html .. util.template('<option value="${v}">${k}</option>\n',{
k=k,
v=v
})
end)
html = html .. "</select>\n"
else
html = html .. util.template('<input type="text" name="${key}" value="${value}"></input>',{
key = opt.key,
value = app.opts[key]
})
end
html = html .. util.template([[
</td>
<td class="col3">${info}</td>
</td>
</tr>
]],opt)
return html
end
function pluginUI(name,plugin)
local plugindata = util.flatten(plugin,"plugin")
plugindata.opts = ""
foreach(plugin.opts, function(k,opt)
plugindata.opts = plugindata.opts .. pluginInput(opt)
end)
print_r(plugindata)
data.plugins = data.plugins .. util.template( LoadAsset('/page/admin/plugin.html'), plugindata)
end
function saveData()
if GetMethod() == 'POST' then
local params = GetParams()
local saveconfig = false
@ -18,7 +68,10 @@ if GetMethod() == 'POST' then
data.notification = '<div class="notification">no changes detected</div>'
end
end
end
foreach( app.plugin, pluginUI )
saveData()
local html = util.template( LoadAsset("page/admin/index.html"),data)

View file

@ -0,0 +1,21 @@
<div class="tab">
<div class="label col2">${plugin.name} <b class="tag">plugin</b></div>
</div>
<div class="margin1">
<table width="100%">
<tr>
<th width="165"/>
<th />
<th />
</tr>
<tr>
<td></td>
<td></td>
<td>${plugin.info}<br/><br/></td>
</tr>
${opts}
</table>
</div>
<br/>
<br/>

View file

@ -6,21 +6,6 @@ ${header}
</div>
<hr style="margin-top:0">
<div class="btn">
<a href="#foo">button</a>
</div>
<div class="btn">
<a href="#foo" class="primary">button primary <img src="img/arrow-down.svg" class="icon"/></a>
</div>
<div class="btn">
<a href="https://foo.com" target="_blank" class="secondary">button secondary <img src="img/arrow-right.svg" class="icon"/></a>
</div>
<br>
<hr>
<br>
<div class="grid">
<div class="tile">
<div class="img" style="background-image:url(https://xrforge.isvery.ninja/models/dpjnj8pt1cjc/model_files/bzjvdm7l523l.png?derivative=preview);"></div>

View file

@ -1,5 +1,5 @@
${header}
<iframe border="0" frameborder="0" src="https://web.janusxr.org" id="viewer"/>
<iframe border="0" frameborder="0" src="/v/index.html#janus.url=/" id="viewer"/>
${footer}

67
src/plugin/janusxr.lua Normal file
View file

@ -0,0 +1,67 @@
local util = require('util')
local pattern_fireboxroom = "[Ff][Ii][Rr][Ee][Bb][Oo][Xx][Rr][Oo][Oo][Mm][ ]?.*>"
local pattern_title = "<[Tt][Ii][Tt][Ll][Ee]>(.*)<"
app.on('scan_experience', function(exp)
--print_r(exp)
end)
app.on('scan_experience_file', function(exp)
local size = exp.file.attr:size()
if exp.file.name:match(".glb") and not util.contains(exp.tag,"xrfragment") then
print("✅ └─ plugin/janusxr: generating janusxr.xml")
elseif size < 1000 then -- skip large binary files
local data = util.readfile( exp.file.fullpath)
if data:match(pattern_fireboxroom) then
table.insert(exp.tag, "janusxr")
local title = data:match(pattern_title)
if title then exp.title = title:gsub("<.*","") end
app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins
table.insert( app.cache.items, exp)
print("✅ └─ plugin/janusxr: " .. exp.file.name)
end
end
end)
app.on('set', function(k,v)
print("content was updated")
end)
-- this registers the plugin UI
local plugin = {
name = "JanusXR",
info = '<a href="https://coderofsalvation.github.io/janus-guide" target="_blank">janusxr</a> allows composing (interconnected) 3D experiences (via portals) via its JML markup language. It also supports AFRAME (geometry)markup.',
opts = {
{
key='app.opts.plugin_janusxr_wrap_image',
title='wrap images',
info='Generate <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> to embed images into an XR experience',
default="yes",
values={yes="yes",no="no"}
},
{
key='app.opts.plugin_janusxr_wrap_video',
title='wrap videos',
info='Generate <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> to embed videos into an XR experience',
default="yes",
values={yes="yes",no="no"}
},
{
key='app.opts.plugin_janusxr_wrap_aframe',
title='wrap AFRAME',
info='Generate <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> to embed AFRAME markup into an XR experience',
default="yes",
values={yes="yes",no="no"}
},
{
key='app.opts.plugin_janusxr_wrap_template',
title='template',
info='What kind of space should images/videos be rendered into?',
default="random",
values={random="random room",plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"}
},
}
}
return plugin

View file

@ -19,6 +19,15 @@ download(){
verify
}
janusweb(){
version=1.7.4
test -f janusweb-${version}.tar.gz || {
wget "https://github.com/jbaicoianu/janusweb/releases/download/v${version}/janusweb-${version}.tar.gz"
tar -xvf janusweb-${version}.tar.gz
mv janusweb-${version} src/v
}
}
"$@"
}
@ -28,4 +37,9 @@ build(){
zip -r ../result/bin/xrforge.com * .lua .init.lua .args
}
rebuild(){
cd src
zip -r ../result/bin/xrforge.com .lua page plugin cmd css .init.lua .args
}
"$@"