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].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):
event_listeners = self.event_listeners.get(name, {})
handlers = event_listeners.get(event, [])

View File

@ -1,13 +1,32 @@
class DumbTerminal extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
import { NjetComponent } from "/njet.js";
class WebTerminal extends NjetComponent {
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() {
this.shadowRoot.innerHTML = `
this.innerHTML = `
<style>
:host {
.web-terminal {
--terminal-bg: #111;
--terminal-fg: #0f0;
--terminal-accent: #0ff;
@ -23,7 +42,7 @@ class DumbTerminal extends HTMLElement {
max-height: 500px;
}
.output {
.web-terminal-output {
white-space: pre-wrap;
margin-bottom: 1em;
}
@ -37,7 +56,7 @@ class DumbTerminal extends HTMLElement {
margin-right: 0.5em;
}
input {
.web-terminal-input {
background: transparent;
border: none;
color: var(--terminal-fg);
@ -57,21 +76,22 @@ class DumbTerminal extends HTMLElement {
padding: 2rem;
}
</style>
<div class="output" id="output"></div>
<div class="web-terminal">
<div class="web-terminal-output" id="output"></div>
<div class="input-line">
<span class="prompt">&gt;</span>
<input type="text" id="input" autocomplete="off" autofocus />
<span class="prompt">&gt;</span>
<input type="text" class="web-terminal-input" id="input" autocomplete="off" autofocus />
</div>
</div>
`;
this.outputEl = this.shadowRoot.getElementById("output");
this.inputEl = this.shadowRoot.getElementById("input");
this.container = this.querySelector(".web-terminal");
this.outputEl = this.querySelector(".web-terminal-output");
this.inputEl = this.querySelector(".web-terminal-input");
this.history = [];
this.historyIndex = -1;
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e));
this.inputEl.addEventListener("keyup", (e) => this.onKeyDown(e));
}
onKeyDown(event) {
@ -116,17 +136,89 @@ class DumbTerminal extends HTMLElement {
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) {
switch (command.trim()) {
case "help":
return "Available commands: help, clear, date";
case "date":
return new Date().toString();
case "clear":
this.outputEl.innerHTML = "";
return "";
default:
return `Unknown command: ${command}`;
let args;
try {
args = this.parseCommand(command);
} catch (e) {
return e.toString();
}
console.info({ef:this})
console.info({af:this})
console.info({gf:args})
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
* @returns {HTMLDialogElement}
*/
static createModal() {
show() {
const dialog = document.createElement("dialog");
dialog.innerHTML = `
<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);
export { WebTerminal };

View File

@ -353,6 +353,38 @@ class NjetDialog extends Component {
}
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 {
render() {
this.classList.add('njet-grid');
@ -467,7 +499,12 @@ njet.showDialog = function(args){
dialog.show()
return dialog
}
njet.showWindow = function(args) {
const w = new NjetWindow(args)
w.show()
return w
}
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="/chat-input.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="/user-list.css">
<link rel="stylesheet" href="/fa640.min.css">

View File

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

View File

@ -17,7 +17,7 @@ from aiohttp import web
from snek.system.model import now
from snek.system.profiler import Profiler
from snek.system.view import BaseView
import time
logger = logging.getLogger(__name__)
@ -203,7 +203,7 @@ class RPCView(BaseView):
)
return channels
async def write_container(self, channel_uid, content):
async def write_container(self, channel_uid, content,timeout=3):
self._require_login()
channel_member = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=self.user_uid
@ -212,9 +212,32 @@ class RPCView(BaseView):
raise Exception("Not allowed")
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):
self._require_login()
@ -402,9 +425,12 @@ class RPCView(BaseView):
)
async def _send_json(self, obj):
if not self.ws.closed:
try:
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):
self._require_login()