This commit is contained in:
retoor 2025-06-11 20:20:34 +02:00
parent ad3f46a9ae
commit f965cc4ecd
6 changed files with 215 additions and 36 deletions

View File

@ -22,6 +22,13 @@ class ContainerService(BaseService):
self.event_listeners[name][event] = [] self.event_listeners[name][event] = []
self.event_listeners[name][event].append(event_handler) self.event_listeners[name][event].append(event_handler)
async def remove_event_listener(self, name, event, event_handler):
if name in self.event_listeners and event in self.event_listeners[name]:
try:
self.event_listeners[name][event].remove(event_handler)
except ValueError:
pass
async def container_event_handler(self, name, event, data): async def container_event_handler(self, name, event, data):
event_listeners = self.event_listeners.get(name, {}) event_listeners = self.event_listeners.get(name, {})
handlers = event_listeners.get(event, []) handlers = event_listeners.get(event, [])

View File

@ -1,13 +1,32 @@
class DumbTerminal extends HTMLElement { import { NjetComponent } from "/njet.js";
constructor() { class WebTerminal extends NjetComponent {
super();
this.attachShadow({ mode: "open" }); commands = {
"clear": () => {
this.outputEl.innerHTML = "";
return "";
},
"help": () => {
return "Available commands: help, clear, date";
},
"date": () => {
return new Date().toString();
} }
};
help = {
"clear": "Clear the terminal",
"help": "Show available commands",
"date": "Show the current date"
};
connectedCallback() { connectedCallback() {
this.shadowRoot.innerHTML = `
this.innerHTML = `
<style> <style>
:host { .web-terminal {
--terminal-bg: #111; --terminal-bg: #111;
--terminal-fg: #0f0; --terminal-fg: #0f0;
--terminal-accent: #0ff; --terminal-accent: #0ff;
@ -23,7 +42,7 @@ class DumbTerminal extends HTMLElement {
max-height: 500px; max-height: 500px;
} }
.output { .web-terminal-output {
white-space: pre-wrap; white-space: pre-wrap;
margin-bottom: 1em; margin-bottom: 1em;
} }
@ -37,7 +56,7 @@ class DumbTerminal extends HTMLElement {
margin-right: 0.5em; margin-right: 0.5em;
} }
input { .web-terminal-input {
background: transparent; background: transparent;
border: none; border: none;
color: var(--terminal-fg); color: var(--terminal-fg);
@ -57,21 +76,22 @@ class DumbTerminal extends HTMLElement {
padding: 2rem; padding: 2rem;
} }
</style> </style>
<div class="web-terminal">
<div class="output" id="output"></div> <div class="web-terminal-output" id="output"></div>
<div class="input-line"> <div class="input-line">
<span class="prompt">&gt;</span> <span class="prompt">&gt;</span>
<input type="text" id="input" autocomplete="off" autofocus /> <input type="text" class="web-terminal-input" id="input" autocomplete="off" autofocus />
</div>
</div> </div>
`; `;
this.container = this.querySelector(".web-terminal");
this.outputEl = this.shadowRoot.getElementById("output"); this.outputEl = this.querySelector(".web-terminal-output");
this.inputEl = this.shadowRoot.getElementById("input"); this.inputEl = this.querySelector(".web-terminal-input");
this.history = []; this.history = [];
this.historyIndex = -1; this.historyIndex = -1;
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e)); this.inputEl.addEventListener("keyup", (e) => this.onKeyDown(e));
} }
onKeyDown(event) { onKeyDown(event) {
@ -116,17 +136,89 @@ class DumbTerminal extends HTMLElement {
this.outputEl.scrollTop = this.outputEl.scrollHeight; this.outputEl.scrollTop = this.outputEl.scrollHeight;
} }
parseCommand(input) {
const args = [];
let current = '';
let inSingleQuote = false;
let inDoubleQuote = false;
let inTemplate = false; // For ${...}
let templateBuffer = '';
for (let i = 0; i < input.length; i++) {
const char = input[i];
// Handle template expressions
if (!inSingleQuote && !inDoubleQuote && char === '$' && input[i + 1] === '{') {
inTemplate = true;
i++; // Skip '{'
templateBuffer = '${';
continue;
}
if (inTemplate) {
templateBuffer += char;
if (char === '}') {
// End of template
args.push(eval(templateBuffer));
inTemplate = false;
templateBuffer = '';
}
continue; // Continue to next char
}
// Handle quotes
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue; // Skip quote
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue; // Skip quote
}
// Handle spaces outside quotes and templates
if (char === ' ' && !inSingleQuote && !inDoubleQuote) {
if (current.length > 0) {
args.push(current);
current = '';
}
} else {
current += char;
}
}
if (current.length > 0) {
args.push(current);
}
return args;
}
executeScript(script) {
const scriptElement = document.createElement("script");
scriptElement.textContent = script;
this.appendChild(scriptElement);
}
mockExecute(command) { mockExecute(command) {
switch (command.trim()) { let args;
case "help": try {
return "Available commands: help, clear, date"; args = this.parseCommand(command);
case "date": } catch (e) {
return new Date().toString(); return e.toString();
case "clear": }
this.outputEl.innerHTML = ""; console.info({ef:this})
return ""; console.info({af:this})
default: console.info({gf:args})
return `Unknown command: ${command}`; let cmdName = args.shift();
let commandHandler = this.commands[cmdName];
if (commandHandler) {
return commandHandler.apply(this, args);
}
try {
// Try to eval as JS
return this.executeScript(command);
} catch (e) {
return e.toString();
} }
} }
@ -134,7 +226,7 @@ class DumbTerminal extends HTMLElement {
* Static method to create a modal dialog with the terminal * Static method to create a modal dialog with the terminal
* @returns {HTMLDialogElement} * @returns {HTMLDialogElement}
*/ */
static createModal() { show() {
const dialog = document.createElement("dialog"); const dialog = document.createElement("dialog");
dialog.innerHTML = ` dialog.innerHTML = `
<div class="dialog-backdrop"> <div class="dialog-backdrop">
@ -147,4 +239,12 @@ class DumbTerminal extends HTMLElement {
} }
} }
window.showTerm = function (options) {
const term = new WebTerminal(options);
term.show();
return term;
}
customElements.define("web-terminal", WebTerminal); customElements.define("web-terminal", WebTerminal);
export { WebTerminal };

View File

@ -353,6 +353,38 @@ class NjetDialog extends Component {
} }
Njet.registerComponent('njet-dialog', NjetDialog); Njet.registerComponent('njet-dialog', NjetDialog);
class NjetWindow extends Component {
render() {
this.innerHTML = '';
const { title, content, primaryButton, secondaryButton } = this.config;
this.classList.add('njet-dialog');
this.style.position = 'fixed';
this.style.top = '50%';
this.style.left = '50%';
this.style.transform = 'translate(-50%, -50%)';
this.style.padding = '20px';
this.style.border = '1px solid #444';
this.style.backgroundColor = '#fff';
this.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
this.style.minWidth = '300px';
if (title) {
const header = document.createElement('h2');
header.textContent = title;
this.appendChild(header);
}
}
show(){
document.body.appendChild(this)
}
}
Njet.registerComponent('njet-window', NjetWindow);
class NjetGrid extends Component { class NjetGrid extends Component {
render() { render() {
this.classList.add('njet-grid'); this.classList.add('njet-grid');
@ -467,7 +499,12 @@ njet.showDialog = function(args){
dialog.show() dialog.show()
return dialog return dialog
} }
njet.showWindow = function(args) {
const w = new NjetWindow(args)
w.show()
return w
}
window.njet = njet window.njet = njet
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet}; export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow };

View File

@ -23,6 +23,7 @@
<script src="/message-list.js" type="module"></script> <script src="/message-list.js" type="module"></script>
<script src="/chat-input.js" type="module"></script> <script src="/chat-input.js" type="module"></script>
<script src="/container.js" type="module"></script> <script src="/container.js" type="module"></script>
<script src="/dumb-term.js" type="module"></script>
<link rel="stylesheet" href="/sandbox.css"> <link rel="stylesheet" href="/sandbox.css">
<link rel="stylesheet" href="/user-list.css"> <link rel="stylesheet" href="/user-list.css">
<link rel="stylesheet" href="/fa640.min.css"> <link rel="stylesheet" href="/fa640.min.css">

View File

@ -2,6 +2,14 @@
<div id="star-popup" class="star-popup"></div> <div id="star-popup" class="star-popup"></div>
<script type="module"> <script type="module">
import { app } from "/app.js"; import { app } from "/app.js";
import {WebTerminal} from "/dumb-term.js";
function showTerm(options){
const term = new WebTerminal(options);
term.show();
}
class StarField { class StarField {
constructor({ count = 200, container = document.body } = {}) { constructor({ count = 200, container = document.body } = {}) {
@ -204,7 +212,7 @@ class StarField {
})(); })();
} }
showLogEvent(...args) { showLogEvent(...args) {
//this.showNotify(...args) this.showNotify(...args)
} }
mapLogLevel(level) { mapLogLevel(level) {

View File

@ -17,7 +17,7 @@ from aiohttp import web
from snek.system.model import now from snek.system.model import now
from snek.system.profiler import Profiler from snek.system.profiler import Profiler
from snek.system.view import BaseView from snek.system.view import BaseView
import time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -203,7 +203,7 @@ class RPCView(BaseView):
) )
return channels return channels
async def write_container(self, channel_uid, content): async def write_container(self, channel_uid, content,timeout=3):
self._require_login() self._require_login()
channel_member = await self.services.channel_member.get( channel_member = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=self.user_uid channel_uid=channel_uid, user_uid=self.user_uid
@ -212,9 +212,32 @@ class RPCView(BaseView):
raise Exception("Not allowed") raise Exception("Not allowed")
container_name = await self.services.container.get_container_name(channel_uid) container_name = await self.services.container.get_container_name(channel_uid)
await self.services.container.write_stdin(channel_uid, content)
return "Written to terminal, response of terminal is not implemented yet." class SessionCall:
def __init__(self, app,channel_uid_uid, container_name):
self.app = app
self.channel_uid = channel_uid
self.container_name = container_name
self.time_last_output = time.time()
self.output = b''
async def stdout_event_handler(self, data):
self.time_last_output = time.time()
self.output += data
return True
async def communicate(self,content, timeout=3):
await self.app.services.container.add_event_listener(self.container_name, "stdout", self.stdout_event_handler)
await self.app.services.container.write_stdin(self.channel_uid, content)
while time.time() - self.time_last_output < timeout:
await asyncio.sleep(0.1)
await self.app.services.container.remove_event_listener(self.container_name, "stdout", self.stdout_event_handler)
return self.output
sc = SessionCall(self, channel_uid,container_name)
return (await sc.communicate(content)).decode("utf-8","ignore")
async def get_container(self, channel_uid): async def get_container(self, channel_uid):
self._require_login() self._require_login()
@ -402,8 +425,11 @@ class RPCView(BaseView):
) )
async def _send_json(self, obj): async def _send_json(self, obj):
if not self.ws.closed: try:
await self.ws.send_str(json.dumps(obj, default=str)) await self.ws.send_str(json.dumps(obj, default=str))
except Exception as ex:
await self.services.socket.delete(self.ws)
await self.ws.close()
async def get_online_users(self, channel_uid): async def get_online_users(self, channel_uid):
self._require_login() self._require_login()