Update.
This commit is contained in:
parent
ad3f46a9ae
commit
f965cc4ecd
src/snek
@ -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, [])
|
||||
|
@ -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">></span>
|
||||
<input type="text" id="input" autocomplete="off" autofocus />
|
||||
<span class="prompt">></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 };
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user