Compare commits
16 Commits
7d7febea39
...
966d14a5b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 966d14a5b1 | |||
| 4fe61ee773 | |||
| 2b66107866 | |||
| 24a17f2b46 | |||
| 00603452bf | |||
| 6fcffc65f9 | |||
| a87564b082 | |||
| e8e94a4e3a | |||
| e7b4d47456 | |||
| 0f49cbeabd | |||
| 740abc9f2c | |||
| eec06c1a24 | |||
| da61426fc9 | |||
| ba44f8e65b | |||
| f598beb406 | |||
| b90a76ebc8 |
|
|
@ -83,8 +83,10 @@ https://wakamaifondue.com/
|
||||||
- [x] public tunnel for your NOSE webapps
|
- [x] public tunnel for your NOSE webapps
|
||||||
- [x] public tunnel lives through reboots
|
- [x] public tunnel lives through reboots
|
||||||
- [ ] tunnel to the terminal
|
- [ ] tunnel to the terminal
|
||||||
|
- [x] web browser
|
||||||
- [x] remember your "mode"
|
- [x] remember your "mode"
|
||||||
- [x] status bar on terminal UX
|
- [x] status bar on terminal UX
|
||||||
|
- [ ] quickly open the current webapp
|
||||||
- [x] "project"-based rehaul
|
- [x] "project"-based rehaul
|
||||||
- [x] self updating NOSE server
|
- [x] self updating NOSE server
|
||||||
- [x] `pub/` static hosting in webapps
|
- [x] `pub/` static hosting in webapps
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Reboot the whole computer! Careful!
|
// Reboot the whole computer! Careful!
|
||||||
export default async function reboot() {
|
export default async function reboot() {
|
||||||
setTimeout(async () => await Bun.$`reboot`, 1000)
|
setTimeout(async () => await Bun.$`sudo reboot`, 1000)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: "Rebooting... This will take about 10 seconds.",
|
text: "Rebooting... This will take about 10 seconds.",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export default function () {
|
||||||
const sec = Math.floor(ms / 1000)
|
const sec = Math.floor(ms / 1000)
|
||||||
const min = Math.floor(sec / 60)
|
const min = Math.floor(sec / 60)
|
||||||
const hrs = Math.floor(min / 60)
|
const hrs = Math.floor(min / 60)
|
||||||
|
const days = Math.floor(hrs / 24)
|
||||||
|
|
||||||
return `${hrs}h ${min % 60}m ${sec % 60}s`
|
return `${days}d ${hrs % 24}h ${min % 60}m ${sec % 60}s`
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
<div class="text-content">
|
<div class="text-content">
|
||||||
<h1>@defunkt</h1>
|
<h1>@defunkt</h1>
|
||||||
<p>This is my website. I am Chris.</p>
|
<p>This is my website. I am Chris.</p>
|
||||||
|
<p><a href="/other.html">Other.html</a></p>
|
||||||
|
<p><a href="http://corey.localhost:3000">corey</a></p>
|
||||||
|
<p><a href="https://google.com">google</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile burger">
|
<div class="tile burger">
|
||||||
|
|
|
||||||
4
nose/chris/pub/other.html
Normal file
4
nose/chris/pub/other.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<html>
|
||||||
|
<head><title>other</title></head>
|
||||||
|
<body><h1>other</h1></body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export default () =>
|
|
||||||
"pong"
|
|
||||||
2
nose/ping/index.tsx
Normal file
2
nose/ping/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export default () =>
|
||||||
|
<html><head><title>pong</title></head><body>pong - {Date.now()}</body></html>
|
||||||
|
|
@ -4,17 +4,15 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"check": "bunx tsc --noEmit",
|
||||||
"dev": "env BUN_HOT=1 bun --hot src/server.tsx",
|
"dev": "env BUN_HOT=1 bun --hot src/server.tsx",
|
||||||
"start": "bun src/server.tsx",
|
"start": "bun src/server.tsx",
|
||||||
"prod": "env NODE_ENV=production bun src/server.tsx",
|
"prod": "env NODE_ENV=production bun src/server.tsx",
|
||||||
"prod-nodns": "env NO_DNS=1 NODE_ENV=production bun src/server.tsx",
|
"prod-nodns": "env NO_DNS=1 NODE_ENV=production bun src/server.tsx",
|
||||||
"build": "./scripts/build.sh",
|
"build": "./scripts/build.sh",
|
||||||
|
|
||||||
"runner": "env NODE_ENV=production bun run src/runner.ts",
|
"runner": "env NODE_ENV=production bun run src/runner.ts",
|
||||||
|
|
||||||
"deploy": "./scripts/deploy.sh",
|
"deploy": "./scripts/deploy.sh",
|
||||||
"push": "./scripts/deploy.sh",
|
"push": "./scripts/deploy.sh",
|
||||||
|
|
||||||
"remote:install": "./scripts/remote-install.sh",
|
"remote:install": "./scripts/remote-install.sh",
|
||||||
"remote:start": "./scripts/remote-start.sh",
|
"remote:start": "./scripts/remote-start.sh",
|
||||||
"remote:stop": "./scripts/remote-stop.sh",
|
"remote:stop": "./scripts/remote-stop.sh",
|
||||||
|
|
@ -26,4 +24,4 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"kleur": "^4.1.5"
|
"kleur": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
110
public/browser-nav.js
Normal file
110
public/browser-nav.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// browser-nav.js - Injected into NOSE apps to get the UI browser working.
|
||||||
|
(function () {
|
||||||
|
const ALLOWED_DOMAINS = ['localhost', '.local', '.nose.space']
|
||||||
|
|
||||||
|
function isAllowedOrigin(url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url, window.location.href)
|
||||||
|
return ALLOWED_DOMAINS.some(domain =>
|
||||||
|
urlObj.hostname === domain || urlObj.hostname.endsWith(domain)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept navigation attempts
|
||||||
|
function interceptNavigation(e) {
|
||||||
|
const target = e.target.closest('a')
|
||||||
|
if (!target || !target.href) return
|
||||||
|
|
||||||
|
// Allow relative URLs and hash links
|
||||||
|
if (target.getAttribute('href').startsWith('#')) return
|
||||||
|
if (target.getAttribute('href').startsWith('/')) return
|
||||||
|
|
||||||
|
// Check if external
|
||||||
|
if (!isAllowedOrigin(target.href)) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'NAV_BLOCKED',
|
||||||
|
data: { url: target.href, reason: 'External domain not allowed' }
|
||||||
|
}, '*')
|
||||||
|
} else {
|
||||||
|
alert('Navigation to external sites is not allowed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for clicks on links
|
||||||
|
document.addEventListener('click', interceptNavigation, true)
|
||||||
|
|
||||||
|
// Intercept programmatic navigation
|
||||||
|
const originalPushState = history.pushState
|
||||||
|
const originalReplaceState = history.replaceState
|
||||||
|
|
||||||
|
history.pushState = function (state, title, url) {
|
||||||
|
if (url && !isAllowedOrigin(url)) {
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'NAV_BLOCKED',
|
||||||
|
data: { url, reason: 'External domain not allowed' }
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return originalPushState.apply(this, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replaceState = function (state, title, url) {
|
||||||
|
if (url && !isAllowedOrigin(url)) {
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'NAV_BLOCKED',
|
||||||
|
data: { url, reason: 'External domain not allowed' }
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return originalReplaceState.apply(this, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for navigation commands from parent
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
console.log(event)
|
||||||
|
if (event.data.type === 'NAV_COMMAND') {
|
||||||
|
switch (event.data.action) {
|
||||||
|
case 'back': history.back(); break
|
||||||
|
case 'forward': history.forward(); break
|
||||||
|
case 'reload': window.location.reload(); break
|
||||||
|
case 'stop': window.stop(); break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send all keydown events to parent
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'KEYDOWN',
|
||||||
|
data: {
|
||||||
|
key: e.key,
|
||||||
|
ctrlKey: e.ctrlKey,
|
||||||
|
shiftKey: e.shiftKey,
|
||||||
|
altKey: e.altKey,
|
||||||
|
metaKey: e.metaKey
|
||||||
|
}
|
||||||
|
}, '*')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({ type: 'NAV_READY' }, '*')
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'URL_CHANGED',
|
||||||
|
data: { url: location.href }
|
||||||
|
}, '*')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
////
|
////
|
||||||
// version: 66c75ff
|
// version: 7d7febe
|
||||||
|
|
||||||
// src/js/dom.ts
|
// src/js/dom.ts
|
||||||
var content2 = $("content");
|
var content2 = $("content");
|
||||||
|
|
@ -1058,6 +1058,25 @@ async function runCommand(input) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/js/statusbar.ts
|
||||||
|
var STATUS_MSG_LENGTH = 3000;
|
||||||
|
var statusbar = $("statusbar");
|
||||||
|
var statusbarMsg = $("statusbar-msg");
|
||||||
|
var timer;
|
||||||
|
function status(msg) {
|
||||||
|
showStatusMsg();
|
||||||
|
statusbarMsg.textContent = msg;
|
||||||
|
if (timer)
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(hideStatusMsg, STATUS_MSG_LENGTH);
|
||||||
|
}
|
||||||
|
function showStatusMsg() {
|
||||||
|
statusbar.classList.add("showing-msg");
|
||||||
|
}
|
||||||
|
function hideStatusMsg() {
|
||||||
|
statusbar.className = "";
|
||||||
|
}
|
||||||
|
|
||||||
// src/js/hyperlink.ts
|
// src/js/hyperlink.ts
|
||||||
function initHyperlink() {
|
function initHyperlink() {
|
||||||
window.addEventListener("click", handleClick);
|
window.addEventListener("click", handleClick);
|
||||||
|
|
@ -1076,6 +1095,9 @@ async function handleClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await runCommand(href.slice(1));
|
await runCommand(href.slice(1));
|
||||||
focusInput();
|
focusInput();
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
status(href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1133,7 +1155,7 @@ function clearInput() {
|
||||||
// src/js/vram.ts
|
// src/js/vram.ts
|
||||||
var vramCounter = $("vram-size");
|
var vramCounter = $("vram-size");
|
||||||
var startVramCounter = () => {
|
var startVramCounter = () => {
|
||||||
const timer = setInterval(() => {
|
const timer2 = setInterval(() => {
|
||||||
const count = parseInt(vramCounter.textContent) + 1;
|
const count = parseInt(vramCounter.textContent) + 1;
|
||||||
let val = count + "KB";
|
let val = count + "KB";
|
||||||
if (count < 10)
|
if (count < 10)
|
||||||
|
|
@ -1141,7 +1163,7 @@ var startVramCounter = () => {
|
||||||
vramCounter.textContent = val;
|
vramCounter.textContent = val;
|
||||||
if (count >= 64) {
|
if (count >= 64) {
|
||||||
vramCounter.textContent += " OK";
|
vramCounter.textContent += " OK";
|
||||||
clearInterval(timer);
|
clearInterval(timer2);
|
||||||
}
|
}
|
||||||
}, 15);
|
}, 15);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
66
src/css/browser.css
Normal file
66
src/css/browser.css
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
:root {
|
||||||
|
--browser-bar-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.browser {
|
||||||
|
display: block;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 10;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.browser.fullscreen {
|
||||||
|
margin-top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mode="tall"] iframe.browser.active {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser.active {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin-top: var(--browser-bar-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
#browser-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
z-index: 15;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--browser-bar-height);
|
||||||
|
background-color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
#browser-controls span,
|
||||||
|
#browser-controls a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: var(--gray);
|
||||||
|
color: var(--c64-light-blue);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#forward-button {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#fullscreen-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 15;
|
||||||
|
top: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-button {
|
||||||
|
position: absolute;
|
||||||
|
right: -10;
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
--purple: #7C3AED;
|
--purple: #7C3AED;
|
||||||
--blue: #1565C0;
|
--blue: #1565C0;
|
||||||
--magenta: #ff66cc;
|
--magenta: #ff66cc;
|
||||||
|
--gray: #BEBEBE;
|
||||||
|
--grey: #BEBEBE;
|
||||||
|
|
||||||
--c64-light-blue: #6C6FF6;
|
--c64-light-blue: #6C6FF6;
|
||||||
--c64-dark-blue: #40318D;
|
--c64-dark-blue: #40318D;
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#statusline {
|
#statusbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -177,7 +177,24 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
#statusline a {
|
#statusbar .line-msg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusbar.showing-msg .line-cwd {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusbar.showing-msg {
|
||||||
|
background: var(--purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusbar.showing-msg .line-msg {
|
||||||
|
color: var(--cyan);
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusbar a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
@ -11,13 +11,15 @@ export async function dispatchMessage(ws: any, msg: Message) {
|
||||||
console.log("<- receive", msg)
|
console.log("<- receive", msg)
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "input":
|
case "input":
|
||||||
await inputMessage(ws, msg as InputMessage); break
|
await inputMessage(ws, msg); break
|
||||||
|
|
||||||
case "save-file":
|
case "save-file":
|
||||||
await saveFileMessage(ws, msg as SaveFileMessage); break
|
await saveFileMessage(ws, msg); break
|
||||||
|
|
||||||
case "ui:mode":
|
case "session:update":
|
||||||
setState("ui:mode", msg.data); break
|
for (const key of Object.keys(msg.data))
|
||||||
|
setState(key, msg.data[key]);
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
send(ws, { type: "error", data: `unknown message: ${msg.type}` })
|
send(ws, { type: "error", data: `unknown message: ${msg.type}` })
|
||||||
|
|
|
||||||
15
src/html/browser/controls.tsx
Normal file
15
src/html/browser/controls.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { FC } from "hono/jsx"
|
||||||
|
|
||||||
|
export const BrowserControls: FC = async () => (
|
||||||
|
<>
|
||||||
|
<div id="browser-controls" style="display:none">
|
||||||
|
<a id="back-button" href="#"></a>
|
||||||
|
<a id="forward-button" href="#"></a>
|
||||||
|
<a id="stop-button" href="#"></a>
|
||||||
|
<a id="reload-button" href="#">@</a>
|
||||||
|
<a id="fullscreen-button" href="#"></a>
|
||||||
|
<a id="close-button" href="#">╳</a>
|
||||||
|
<span id="browser-address">your-app.nose-pluto.local</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import type { FC } from "hono/jsx"
|
import type { FC } from "hono/jsx"
|
||||||
|
import { BrowserControls } from "./browser/controls"
|
||||||
|
|
||||||
export const Terminal: FC = async () => (
|
export const Terminal: FC = async () => (
|
||||||
<>
|
<>
|
||||||
<link rel="stylesheet" href="/css/terminal.css" />
|
<link rel="stylesheet" href="/css/terminal.css" />
|
||||||
<link rel="stylesheet" href="/css/editor.css" />
|
<link rel="stylesheet" href="/css/editor.css" />
|
||||||
|
<link rel="stylesheet" href="/css/browser.css" />
|
||||||
|
|
||||||
|
<BrowserControls />
|
||||||
|
|
||||||
<div id="command-line">
|
<div id="command-line">
|
||||||
<span id="command-prompt">></span>
|
<span id="command-prompt">></span>
|
||||||
|
|
@ -35,6 +39,9 @@ export const Terminal: FC = async () => (
|
||||||
<li class="center">VRAM <span id="vram-size">000KB</span></li>
|
<li class="center">VRAM <span id="vram-size">000KB</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div id="statusline"><div><a href="#projects" id="project-name">root</a>: <a href="#ls" id="project-cwd">/</a></div></div>
|
<div id="statusbar">
|
||||||
|
<div class="line-cwd"><a href="#projects" id="project-name">root</a>: <a href="#ls" id="project-cwd">/</a></div>
|
||||||
|
<div class="line-msg"><span id="statusbar-msg"></span></div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
178
src/js/browser.ts
Normal file
178
src/js/browser.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
////
|
||||||
|
// Our tiny browser, only for NOSE apps.
|
||||||
|
// Check /public/browser-nav.js for the 2nd part.
|
||||||
|
|
||||||
|
import { $, $$ } from "./dom"
|
||||||
|
import { focusInput } from "./focus"
|
||||||
|
import { scrollback } from "./dom"
|
||||||
|
import { addInput } from "./scrollback"
|
||||||
|
import { randomId } from "../shared/utils"
|
||||||
|
|
||||||
|
const HEIGHT = 540
|
||||||
|
const WIDTH = 960
|
||||||
|
|
||||||
|
const controls = $("browser-controls") as HTMLDivElement
|
||||||
|
const address = $("browser-address") as HTMLSpanElement
|
||||||
|
let iframe: HTMLIFrameElement
|
||||||
|
let realUrl = ""
|
||||||
|
let showInput = true
|
||||||
|
|
||||||
|
export function isBrowsing(): boolean {
|
||||||
|
return document.querySelector("iframe.browser.active") !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openBrowser(url: string, openedVia: "click" | "command" = "click") {
|
||||||
|
showInput = openedVia === "click"
|
||||||
|
|
||||||
|
iframe = $$("iframe.browser.active") as HTMLIFrameElement
|
||||||
|
iframe.src = url
|
||||||
|
iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms")
|
||||||
|
iframe.height = String(HEIGHT)
|
||||||
|
iframe.width = String(WIDTH)
|
||||||
|
iframe.tabIndex = 0
|
||||||
|
|
||||||
|
window.addEventListener("message", handleAppMessage)
|
||||||
|
window.addEventListener("keydown", handleBrowserKeydown)
|
||||||
|
controls.addEventListener("click", handleClick)
|
||||||
|
iframe.addEventListener("load", handlePageLoad)
|
||||||
|
|
||||||
|
controls.style.display = ""
|
||||||
|
const main = document.querySelector("#content")
|
||||||
|
main?.prepend(iframe)
|
||||||
|
|
||||||
|
setAddress(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBrowser() {
|
||||||
|
window.removeEventListener("keydown", handleBrowserKeydown)
|
||||||
|
window.removeEventListener("message", handleAppMessage)
|
||||||
|
controls.removeEventListener("click", handleClick)
|
||||||
|
iframe.removeEventListener("load", handlePageLoad)
|
||||||
|
|
||||||
|
const id = randomId()
|
||||||
|
if (showInput)
|
||||||
|
addInput(id, "browse " + realUrl, "ok")
|
||||||
|
|
||||||
|
iframe.height = String(HEIGHT / 2)
|
||||||
|
iframe.width = String(WIDTH / 2)
|
||||||
|
iframe.style.pointerEvents = "none"
|
||||||
|
iframe.tabIndex = -1
|
||||||
|
iframe.classList.remove("fullscreen", "active")
|
||||||
|
scrollback.append(iframe)
|
||||||
|
|
||||||
|
controls.style.display = "none"
|
||||||
|
focusInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAppMessage(event: MessageEvent) {
|
||||||
|
const origin = event.origin
|
||||||
|
if (!origin.includes('localhost') && !origin.match(/\.local$/)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, data } = event.data
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'NAV_READY':
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'URL_CHANGED':
|
||||||
|
setAddress(data.url)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'TITLE_CHANGED':
|
||||||
|
// TODO: browser titles
|
||||||
|
console.log('Page title:', data.title)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'NAV_BLOCKED':
|
||||||
|
showNavigationError(data.url, data.reason)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'KEYDOWN':
|
||||||
|
const keyEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: data.key,
|
||||||
|
ctrlKey: data.ctrlKey,
|
||||||
|
shiftKey: data.shiftKey,
|
||||||
|
altKey: data.altKey,
|
||||||
|
metaKey: data.metaKey,
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
window.dispatchEvent(keyEvent)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBrowserKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) {
|
||||||
|
e.preventDefault()
|
||||||
|
closeBrowser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(e: MouseEvent) {
|
||||||
|
const target = e.target
|
||||||
|
if (!(target instanceof HTMLElement)) return
|
||||||
|
|
||||||
|
switch (target.id) {
|
||||||
|
case "back-button":
|
||||||
|
navigateBack(); break
|
||||||
|
case "forward-button":
|
||||||
|
navigateForward(); break
|
||||||
|
case "stop-button":
|
||||||
|
stopLoading(); break
|
||||||
|
case "reload-button":
|
||||||
|
reloadBrowser(); break
|
||||||
|
case "fullscreen-button":
|
||||||
|
fullscreenBrowser(); break
|
||||||
|
case "close-button":
|
||||||
|
closeBrowser(); break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageLoad() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAddress(url: string) {
|
||||||
|
realUrl = url
|
||||||
|
address.textContent = url.replace(/https?:\/\//, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateBack() {
|
||||||
|
sendNavCommand('back')
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateForward() {
|
||||||
|
sendNavCommand('forward')
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadBrowser() {
|
||||||
|
sendNavCommand('reload')
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopLoading() {
|
||||||
|
sendNavCommand('stop')
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullscreenBrowser() {
|
||||||
|
controls.style.display = 'none'
|
||||||
|
iframe.classList.add('fullscreen')
|
||||||
|
document.body.append(iframe)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendNavCommand(action: 'back' | 'forward' | 'reload' | 'stop') {
|
||||||
|
if (!iframe.contentWindow) return
|
||||||
|
|
||||||
|
iframe.contentWindow.postMessage({
|
||||||
|
type: 'NAV_COMMAND',
|
||||||
|
action
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNavigationError(url: string, reason: string) {
|
||||||
|
alert(`NAVIGATION BLOCKED\n\n${url}\n\n${reason}`)
|
||||||
|
}
|
||||||
|
|
@ -2,16 +2,17 @@
|
||||||
// temporary hack for browser commands
|
// temporary hack for browser commands
|
||||||
|
|
||||||
import type { CommandOutput } from "../shared/types"
|
import type { CommandOutput } from "../shared/types"
|
||||||
|
import { openBrowser } from "./browser"
|
||||||
import { scrollback, content } from "./dom"
|
import { scrollback, content } from "./dom"
|
||||||
|
import { focusInput } from "./focus"
|
||||||
import { resize } from "./resize"
|
import { resize } from "./resize"
|
||||||
import { autoScroll } from "./scrollback"
|
|
||||||
import { sessionId } from "./session"
|
import { sessionId } from "./session"
|
||||||
import { send } from "./websocket"
|
import { send } from "./websocket"
|
||||||
import { focusInput } from "./focus"
|
|
||||||
|
|
||||||
export const commands: string[] = []
|
export const commands: string[] = []
|
||||||
|
|
||||||
export const browserCommands: Record<string, (...args: string[]) => void | Promise<void> | CommandOutput> = {
|
export const browserCommands: Record<string, (...args: string[]) => void | Promise<void> | CommandOutput> = {
|
||||||
|
browse: (url: string) => openBrowser(url, "command"),
|
||||||
"browser-session": () => sessionId,
|
"browser-session": () => sessionId,
|
||||||
clear: () => scrollback.innerHTML = "",
|
clear: () => scrollback.innerHTML = "",
|
||||||
commands: () => {
|
commands: () => {
|
||||||
|
|
@ -21,13 +22,12 @@ export const browserCommands: Record<string, (...args: string[]) => void | Promi
|
||||||
mode: (mode?: string) => {
|
mode: (mode?: string) => {
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"
|
mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"
|
||||||
send({ type: "ui:mode", data: mode })
|
send({ type: "session:update", data: { "ui:mode": mode } })
|
||||||
}
|
}
|
||||||
|
|
||||||
content.style.display = ""
|
content.style.display = ""
|
||||||
document.body.dataset.mode = mode
|
document.body.dataset.mode = mode
|
||||||
resize()
|
resize()
|
||||||
autoScroll()
|
|
||||||
focusInput()
|
focusInput()
|
||||||
},
|
},
|
||||||
reload: () => window.location.reload(),
|
reload: () => window.location.reload(),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { SaveFileMessage } from "../shared/types"
|
||||||
import { scrollback } from "./dom"
|
import { scrollback } from "./dom"
|
||||||
import { send } from "./websocket"
|
import { send } from "./websocket"
|
||||||
import { focusInput } from "./focus"
|
import { focusInput } from "./focus"
|
||||||
|
|
@ -49,10 +50,10 @@ function keydownHandler(e: KeyboardEvent) {
|
||||||
} else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) {
|
} else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
send({
|
send({
|
||||||
id: editor.dataset.path,
|
id: editor.dataset.path || "/tmp.txt",
|
||||||
type: "save-file",
|
type: "save-file",
|
||||||
data: editor.value
|
data: editor.value
|
||||||
})
|
} as SaveFileMessage)
|
||||||
} else if (e.key === "{") {
|
} else if (e.key === "{") {
|
||||||
if (editor.selectionStart !== editor.selectionEnd) {
|
if (editor.selectionStart !== editor.selectionEnd) {
|
||||||
insertAroundSelection(editor, '{', '}')
|
insertAroundSelection(editor, '{', '}')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Message } from "../shared/types"
|
import type { GameStartMessage } from "../shared/types"
|
||||||
import { GameContext, type InputState } from "../shared/game"
|
import { GameContext, type InputState } from "../shared/game"
|
||||||
import { focusInput } from "./focus"
|
import { focusInput } from "./focus"
|
||||||
import { $$ } from "./dom"
|
import { $$ } from "./dom"
|
||||||
|
|
@ -9,7 +9,7 @@ import { sessionId } from "./session"
|
||||||
|
|
||||||
const FPS = 30
|
const FPS = 30
|
||||||
const HEIGHT = 540
|
const HEIGHT = 540
|
||||||
const WIDTH = 980
|
const WIDTH = 960
|
||||||
|
|
||||||
type Game = { init?: () => void, update?: (delta: number, input: InputState) => void, draw?: (ctx: GameContext) => void }
|
type Game = { init?: () => void, update?: (delta: number, input: InputState) => void, draw?: (ctx: GameContext) => void }
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ let pressed: InputState = {
|
||||||
justReleased: new Set(),
|
justReleased: new Set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGameStart(msg: Message) {
|
export async function handleGameStart(msg: GameStartMessage) {
|
||||||
const msgId = msg.id as string
|
const msgId = msg.id as string
|
||||||
const name = msg.data as string
|
const name = msg.data as string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
////
|
||||||
|
// Links can be #command (will run in the prompt) or real links, which run in
|
||||||
|
// a NOSE browser so you always stay in the UI's safe embrace.
|
||||||
|
|
||||||
import { runCommand } from "./shell"
|
import { runCommand } from "./shell"
|
||||||
import { focusInput } from "./focus"
|
import { focusInput } from "./focus"
|
||||||
|
import { openBrowser, isBrowsing } from "./browser"
|
||||||
|
|
||||||
export function initHyperlink() {
|
export function initHyperlink() {
|
||||||
window.addEventListener("click", handleClick)
|
window.addEventListener("click", handleClick)
|
||||||
|
|
@ -16,9 +21,12 @@ async function handleClick(e: MouseEvent) {
|
||||||
const href = a.getAttribute("href")
|
const href = a.getAttribute("href")
|
||||||
if (!href) return
|
if (!href) return
|
||||||
|
|
||||||
if (href.startsWith("#")) {
|
if (href.startsWith("#") && href.length > 1) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
await runCommand(href.slice(1))
|
await runCommand(href.slice(1))
|
||||||
focusInput()
|
focusInput()
|
||||||
|
} else if (!isBrowsing()) {
|
||||||
|
e.preventDefault()
|
||||||
|
openBrowser(href)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,25 +7,26 @@ import { scrollback, cmdInput, $$ } from "./dom"
|
||||||
import { randomId } from "../shared/utils"
|
import { randomId } from "../shared/utils"
|
||||||
|
|
||||||
type InputStatus = "waiting" | "streaming" | "ok" | "error"
|
type InputStatus = "waiting" | "streaming" | "ok" | "error"
|
||||||
|
const statusColors = {
|
||||||
|
waiting: "yellow",
|
||||||
|
streaming: "purple",
|
||||||
|
ok: "green",
|
||||||
|
error: "red"
|
||||||
|
}
|
||||||
|
|
||||||
export function initScrollback() {
|
export function initScrollback() {
|
||||||
window.addEventListener("click", handleInputClick)
|
window.addEventListener("click", handleInputClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function autoScroll() {
|
|
||||||
// requestAnimationFrame(() => scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight)
|
|
||||||
// scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insert(node: HTMLElement) {
|
export function insert(node: HTMLElement) {
|
||||||
scrollback.append(node)
|
scrollback.append(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addInput(id: string, input: string) {
|
export function addInput(id: string, input: string, status?: InputStatus) {
|
||||||
const parent = $$("li.input")
|
const parent = $$("li.input")
|
||||||
const status = $$("span.status.yellow", "•")
|
const statusSpan = $$(`span.status.${statusColors[status || "waiting"]}`, "•")
|
||||||
const content = $$("span.content", input)
|
const content = $$("span.content", input)
|
||||||
parent.append(status, content)
|
parent.append(statusSpan, content)
|
||||||
parent.dataset.id = id
|
parent.dataset.id = id
|
||||||
|
|
||||||
insert(parent)
|
insert(parent)
|
||||||
|
|
@ -36,15 +37,8 @@ export function setStatus(id: string, status: InputStatus) {
|
||||||
const statusEl = document.querySelector(`[data-id="${id}"].input .status`)
|
const statusEl = document.querySelector(`[data-id="${id}"].input .status`)
|
||||||
if (!statusEl) return
|
if (!statusEl) return
|
||||||
|
|
||||||
const colors = {
|
statusEl.classList.remove(...Object.values(statusColors))
|
||||||
waiting: "yellow",
|
statusEl.classList.add(statusColors[status])
|
||||||
streaming: "purple",
|
|
||||||
ok: "green",
|
|
||||||
error: "red"
|
|
||||||
}
|
|
||||||
|
|
||||||
statusEl.classList.remove(...Object.values(colors))
|
|
||||||
statusEl.classList.add(colors[status])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addOutput(id: string, output: CommandOutput) {
|
export function addOutput(id: string, output: CommandOutput) {
|
||||||
|
|
@ -65,8 +59,6 @@ export function addOutput(id: string, output: CommandOutput) {
|
||||||
} else {
|
} else {
|
||||||
insert(item)
|
insert(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
autoScroll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addErrorMessage(message: string) {
|
export function addErrorMessage(message: string) {
|
||||||
|
|
@ -87,8 +79,6 @@ export function appendOutput(id: string, output: CommandOutput) {
|
||||||
item.innerHTML += content
|
item.innerHTML += content
|
||||||
else
|
else
|
||||||
item.textContent += content
|
item.textContent += content
|
||||||
|
|
||||||
autoScroll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceOutput(id: string, output: CommandOutput) {
|
export function replaceOutput(id: string, output: CommandOutput) {
|
||||||
|
|
@ -105,8 +95,6 @@ export function replaceOutput(id: string, output: CommandOutput) {
|
||||||
item.innerHTML = content
|
item.innerHTML = content
|
||||||
else
|
else
|
||||||
item.textContent = content
|
item.textContent = content
|
||||||
|
|
||||||
autoScroll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processOutput(output: CommandOutput): ["html" | "text", string] {
|
function processOutput(output: CommandOutput): ["html" | "text", string] {
|
||||||
|
|
@ -145,6 +133,7 @@ function handleInputClick(e: MouseEvent) {
|
||||||
|
|
||||||
export function handleOutput(msg: Message) {
|
export function handleOutput(msg: Message) {
|
||||||
const result = msg.data as CommandResult
|
const result = msg.data as CommandResult
|
||||||
setStatus(msg.id!, result.status)
|
const id = "id" in msg ? msg.id || "" : ""
|
||||||
addOutput(msg.id!, result.output)
|
setStatus(id, result.status)
|
||||||
|
addOutput(id, result.output)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
////
|
////
|
||||||
// The shell runs on the server and processes input, returning output.
|
// The shell runs on the server and processes input, returning output.
|
||||||
|
|
||||||
|
import type { InputMessage } from "../shared/types"
|
||||||
import { addInput, setStatus, addOutput } from "./scrollback"
|
import { addInput, setStatus, addOutput } from "./scrollback"
|
||||||
import { send } from "./websocket"
|
import { send } from "./websocket"
|
||||||
import { randomId } from "../shared/utils"
|
import { randomId } from "../shared/utils"
|
||||||
|
|
@ -27,7 +28,7 @@ export async function runCommand(input: string) {
|
||||||
if (result) addOutput(id, result)
|
if (result) addOutput(id, result)
|
||||||
setStatus(id, "ok")
|
setStatus(id, "ok")
|
||||||
} else {
|
} else {
|
||||||
send({ id, type: "input", data: input })
|
send({ id, type: "input", data: input } as InputMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
27
src/js/statusbar.ts
Normal file
27
src/js/statusbar.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
////
|
||||||
|
// Temporarily display a message to the user in the status bar.
|
||||||
|
|
||||||
|
import { $ } from "./dom"
|
||||||
|
|
||||||
|
const STATUS_MSG_LENGTH = 3000
|
||||||
|
|
||||||
|
const statusbar = $("statusbar") as HTMLDivElement
|
||||||
|
const statusbarMsg = $("statusbar-msg") as HTMLSpanElement
|
||||||
|
|
||||||
|
let timer: NodeJS.Timeout
|
||||||
|
|
||||||
|
export function status(msg: string) {
|
||||||
|
showStatusMsg()
|
||||||
|
statusbarMsg.textContent = msg
|
||||||
|
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
timer = setTimeout(hideStatusMsg, STATUS_MSG_LENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatusMsg() {
|
||||||
|
statusbar.classList.add("showing-msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStatusMsg() {
|
||||||
|
statusbar.className = ""
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import type { Message, CommandOutput } from "@/shared/types"
|
import type { StreamMessage } from "@/shared/types"
|
||||||
import { addOutput, appendOutput, replaceOutput } from "./scrollback"
|
import { addOutput, appendOutput, replaceOutput } from "./scrollback"
|
||||||
|
|
||||||
export function handleStreamStart(msg: Message) {
|
export function handleStreamStart(msg: StreamMessage) {
|
||||||
const id = msg.id!
|
const id = msg.id
|
||||||
|
|
||||||
const status = document.querySelector(`[data-id="${id}"].input .status`)
|
const status = document.querySelector(`[data-id="${id}"].input .status`)
|
||||||
if (!status) return
|
if (!status) return
|
||||||
|
|
||||||
addOutput(id, msg.data as CommandOutput)
|
addOutput(id, msg.data)
|
||||||
|
|
||||||
status.classList.remove("yellow")
|
status.classList.remove("yellow")
|
||||||
status.classList.add("purple")
|
status.classList.add("purple")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleStreamAppend(msg: Message) {
|
export function handleStreamAppend(msg: StreamMessage) {
|
||||||
appendOutput(msg.id!, msg.data as CommandOutput)
|
appendOutput(msg.id, msg.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleStreamReplace(msg: Message) {
|
export function handleStreamReplace(msg: StreamMessage) {
|
||||||
replaceOutput(msg.id!, msg.data as CommandOutput)
|
replaceOutput(msg.id, msg.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleStreamEnd(_msg: Message) {
|
export function handleStreamEnd(_msg: StreamMessage) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { addErrorMessage } from "./scrollback"
|
||||||
const MAX_RETRIES = 5
|
const MAX_RETRIES = 5
|
||||||
let retries = 0
|
let retries = 0
|
||||||
let connected = false
|
let connected = false
|
||||||
let msgQueue: Message[] = []
|
let msgQueue: Omit<Message, "session">[] = []
|
||||||
|
|
||||||
let ws: WebSocket | null = null
|
let ws: WebSocket | null = null
|
||||||
|
|
||||||
|
|
@ -30,14 +30,15 @@ export function startConnection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// send any message
|
// send any message
|
||||||
export function send(msg: Message) {
|
export function send(msg: Omit<Message, "session">) {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
msgQueue.push(msg)
|
msgQueue.push(msg)
|
||||||
startConnection()
|
startConnection()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!msg.session) msg.session = sessionId
|
if (!(msg as any).session) (msg as any).session = sessionId
|
||||||
|
|
||||||
ws?.readyState === 1 && ws.send(JSON.stringify(msg))
|
ws?.readyState === 1 && ws.send(JSON.stringify(msg))
|
||||||
console.log("-> send", msg)
|
console.log("-> send", msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,39 @@ app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
|
||||||
// app routes
|
// app routes
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// inject browser-nav.js into NOSE apps when loaded in a NOSE iframe
|
||||||
|
app.use("*", async (c, next) => {
|
||||||
|
await next()
|
||||||
|
|
||||||
|
const url = new URL(c.req.url)
|
||||||
|
const domains = url.hostname.split(".")
|
||||||
|
const isSubdomain = domains.length > (url.hostname.endsWith("localhost") ? 1 : 2)
|
||||||
|
|
||||||
|
if (!isSubdomain) return
|
||||||
|
|
||||||
|
const contentType = c.res.headers.get('content-type')
|
||||||
|
if (!contentType?.includes('text/html')) return
|
||||||
|
|
||||||
|
const secFetchDest = c.req.header('Sec-Fetch-Dest')
|
||||||
|
const isIframe = secFetchDest === 'iframe'
|
||||||
|
if (!isIframe) return
|
||||||
|
|
||||||
|
const originalBody = await c.res.text()
|
||||||
|
const shimScript = `<script src="/browser-nav.js"></script>`
|
||||||
|
|
||||||
|
let modifiedBody = originalBody
|
||||||
|
if (originalBody.includes('</head>')) {
|
||||||
|
modifiedBody = originalBody.replace('</head>', `${shimScript}\n</head>`)
|
||||||
|
} else if (originalBody.includes('<body>')) {
|
||||||
|
modifiedBody = originalBody.replace('<body>', `<body>\n${shimScript}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.res = new Response(modifiedBody, {
|
||||||
|
status: c.res.status,
|
||||||
|
headers: c.res.headers
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
app.use("*", async (c, next) => {
|
app.use("*", async (c, next) => {
|
||||||
const url = new URL(c.req.url)
|
const url = new URL(c.req.url)
|
||||||
const localhost = url.hostname.endsWith("localhost")
|
const localhost = url.hostname.endsWith("localhost")
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
export type Message = {
|
export type Message =
|
||||||
session?: string
|
| ErrorMessage
|
||||||
id?: string
|
|
||||||
type: MessageType
|
|
||||||
data?: CommandResult | CommandOutput
|
|
||||||
}
|
|
||||||
| InputMessage
|
| InputMessage
|
||||||
|
| OutputMessage
|
||||||
| SaveFileMessage
|
| SaveFileMessage
|
||||||
| SessionStartMessage
|
| SessionStartMessage
|
||||||
| SessionUpdateMessage
|
| SessionUpdateMessage
|
||||||
|
| GameStartMessage
|
||||||
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
|
| StreamMessage
|
||||||
| "game:start"
|
| CommandsMessage
|
||||||
| "stream:start" | "stream:end" | "stream:append" | "stream:replace"
|
|
||||||
| "ui:mode"
|
|
||||||
|
|
||||||
export type CommandOutput = string | string[]
|
export type CommandOutput = string | string[]
|
||||||
| { text: string, script?: string }
|
| { text: string, script?: string }
|
||||||
|
|
@ -25,6 +20,22 @@ export type CommandResult = {
|
||||||
output: CommandOutput
|
output: CommandOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ErrorMessage = {
|
||||||
|
type: "error"
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandsMessage = {
|
||||||
|
type: "commands"
|
||||||
|
data: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutputMessage = {
|
||||||
|
type: "output"
|
||||||
|
id?: string
|
||||||
|
data: CommandResult
|
||||||
|
}
|
||||||
|
|
||||||
export type InputMessage = {
|
export type InputMessage = {
|
||||||
type: "input"
|
type: "input"
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -53,3 +64,16 @@ export type SessionUpdateMessage = {
|
||||||
type: "session:update"
|
type: "session:update"
|
||||||
data: Record<string, string>
|
data: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GameStartMessage = {
|
||||||
|
type: "game:start"
|
||||||
|
id: string
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StreamMessage = {
|
||||||
|
type: "stream:start" | "stream:end" | "stream:append" | "stream:replace"
|
||||||
|
id: string
|
||||||
|
session: string
|
||||||
|
data: CommandOutput
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { send as sendWs } from "./websocket"
|
||||||
import { sessionGet } from "./session"
|
import { sessionGet } from "./session"
|
||||||
import { processExecOutput } from "./shell"
|
import { processExecOutput } from "./shell"
|
||||||
import type { Child } from "hono/jsx"
|
import type { Child } from "hono/jsx"
|
||||||
import type { CommandOutput, Message } from "./shared/types"
|
import type { CommandOutput, StreamMessage } from "./shared/types"
|
||||||
|
|
||||||
type StreamFn = (output: Child) => Promise<void>
|
type StreamFn = (output: Child) => Promise<void>
|
||||||
type StreamFns = { replace: StreamFn, append: StreamFn }
|
type StreamFns = { replace: StreamFn, append: StreamFn }
|
||||||
|
|
@ -16,7 +16,7 @@ export async function stream(initOrFn: StreamParamFn | string | any, fn?: Stream
|
||||||
const taskId = state.taskId
|
const taskId = state.taskId
|
||||||
const session = state.sessionId
|
const session = state.sessionId
|
||||||
|
|
||||||
const send = (msg: Message) => {
|
const send = (msg: Omit<StreamMessage, "id" | "session">) => {
|
||||||
sendWs(state.ws, { ...msg, id: taskId, session })
|
sendWs(state.ws, { ...msg, id: taskId, session })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user