Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
c53b930554 | |||
1705575985 | |||
0c0742fce7 | |||
803ad3dfc6 | |||
f965cc4ecd | |||
ad3f46a9ae | |||
3ea4918ca2 | |||
26a54848f1 | |||
015337d75c | |||
5f06d7e04c | |||
1b2ad3965b | |||
7adb71efe5 | |||
f35fb12856 | |||
0295108e6b | |||
a2fa976065 | |||
b82db127ae | |||
f5e807cdbf | |||
06f28fd426 | |||
60afefac96 | |||
351fef504c | |||
3a8703d3c6 | |||
1c58165425 | |||
38640d8f75 | |||
bb1f7cdb88 | |||
4bb1b0997a | |||
f8f1235e60 | |||
fbd72f727a | |||
62eb1060d9 |
@ -10,3 +10,9 @@ RUN chmod +x r
|
||||
|
||||
RUN mv r /usr/local/bin/r
|
||||
|
||||
RUN echo 'root:root' | chpasswd
|
||||
|
||||
COPY ./terminal /opt/bootstrap
|
||||
COPY ./terminal /opt/snek
|
||||
RUN cp -r /root /opt/bootstrap/root
|
||||
COPY ./terminal/entry /usr/local/bin/entry
|
||||
|
@ -3,6 +3,7 @@ import logging
|
||||
import pathlib
|
||||
import ssl
|
||||
import uuid
|
||||
import signal
|
||||
from datetime import datetime
|
||||
|
||||
from snek import snode
|
||||
@ -40,9 +41,10 @@ from snek.system.template import (
|
||||
)
|
||||
from snek.view.about import AboutHTMLView, AboutMDView
|
||||
from snek.view.avatar import AvatarView
|
||||
from snek.view.channel import ChannelAttachmentView, ChannelView
|
||||
from snek.view.channel import ChannelAttachmentView,ChannelAttachmentUploadView, ChannelView
|
||||
from snek.view.docs import DocsHTMLView, DocsMDView
|
||||
from snek.view.drive import DriveApiView, DriveView
|
||||
from snek.view.channel import ChannelDriveApiView
|
||||
from snek.view.index import IndexView
|
||||
from snek.view.login import LoginView
|
||||
from snek.view.logout import LogoutView
|
||||
@ -210,6 +212,10 @@ class Application(BaseApplication):
|
||||
# app.loop = asyncio.get_running_loop()
|
||||
app.executor = ThreadPoolExecutor(max_workers=200)
|
||||
app.loop.set_default_executor(self.executor)
|
||||
#for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
#app.loop.add_signal_handler(
|
||||
# sig, lambda: asyncio.create_task(self.services.container.shutdown())
|
||||
#)
|
||||
|
||||
async def create_task(self, task):
|
||||
await self.tasks.put(task)
|
||||
@ -281,6 +287,12 @@ class Application(BaseApplication):
|
||||
self.router.add_view(
|
||||
"/channel/{channel_uid}/attachment.bin", ChannelAttachmentView
|
||||
)
|
||||
self.router.add_view(
|
||||
"/channel/{channel_uid}/drive.json", ChannelDriveApiView
|
||||
)
|
||||
self.router.add_view(
|
||||
"/channel/{channel_uid}/attachment.sock", ChannelAttachmentUploadView
|
||||
)
|
||||
self.router.add_view(
|
||||
"/channel/attachment/{relative_url:.*}", ChannelAttachmentView
|
||||
)
|
||||
|
@ -12,6 +12,9 @@ class ContainerService(BaseService):
|
||||
self.compose = ComposeFileManager(self.compose_path,self.container_event_handler)
|
||||
self.event_listeners = {}
|
||||
|
||||
async def shutdown(self):
|
||||
return await self.compose.shutdown()
|
||||
|
||||
async def add_event_listener(self, name, event,event_handler):
|
||||
if not name in self.event_listeners:
|
||||
self.event_listeners[name] = {}
|
||||
@ -19,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, [])
|
||||
@ -84,7 +94,7 @@ class ContainerService(BaseService):
|
||||
"./"
|
||||
+ str(await self.services.channel.get_home_folder(channel_uid))
|
||||
+ ":"
|
||||
+ "/home/ubuntu"
|
||||
+ "/root"
|
||||
],
|
||||
)
|
||||
return await self.compose.get_instance(name)
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { Schedule } from "./schedule.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
import { Njet } from "./njet.js";
|
||||
export class RESTClient {
|
||||
debug = false;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { app } from "../app.js";
|
||||
|
||||
class ChatInputComponent extends HTMLElement {
|
||||
import { app } from "./app.js";
|
||||
import { NjetComponent } from "./njet.js";
|
||||
import { FileUploadGrid } from "./file-upload-grid.js";
|
||||
class ChatInputComponent extends NjetComponent {
|
||||
autoCompletions = {
|
||||
"example 1": () => {
|
||||
},
|
||||
@ -161,6 +162,11 @@ class ChatInputComponent extends HTMLElement {
|
||||
|
||||
this.classList.add("chat-input");
|
||||
|
||||
this.fileUploadGrid = new FileUploadGrid();
|
||||
this.fileUploadGrid.setAttribute("channel", this.channelUid);
|
||||
this.fileUploadGrid.style.display = "none";
|
||||
this.appendChild(this.fileUploadGrid);
|
||||
|
||||
this.textarea.setAttribute("placeholder", "Type a message...");
|
||||
this.textarea.setAttribute("rows", "2");
|
||||
|
||||
@ -174,7 +180,18 @@ class ChatInputComponent extends HTMLElement {
|
||||
this.uploadButton.addEventListener("uploaded", (e) => {
|
||||
this.dispatchEvent(new CustomEvent("uploaded", e));
|
||||
});
|
||||
this.uploadButton.addEventListener("click", (e) => {
|
||||
// e.preventDefault();
|
||||
// this.fileUploadGrid.openFileDialog()
|
||||
|
||||
})
|
||||
this.subscribe("file-uploading", (e) => {
|
||||
|
||||
this.fileUploadGrid.style.display = "block";
|
||||
|
||||
this.uploadButton.style.display = "none";
|
||||
this.textarea.style.display = "none";
|
||||
})
|
||||
this.appendChild(this.uploadButton);
|
||||
this.textarea.addEventListener("blur", () => {
|
||||
this.updateFromInput("");
|
||||
|
@ -25,7 +25,7 @@ export class Container extends EventHandler{
|
||||
});
|
||||
|
||||
}
|
||||
this.refresh()
|
||||
//this.refresh()
|
||||
this.terminal.focus()
|
||||
}
|
||||
refresh(){
|
||||
@ -36,7 +36,10 @@ export class Container extends EventHandler{
|
||||
this._container.classList.toggle("hidden")
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
fit(){
|
||||
this._fitAddon.fit();
|
||||
|
||||
}
|
||||
constructor(channelUid,log){
|
||||
super()
|
||||
this.terminal = new Terminal({ cursorBlink: true ,theme: {
|
||||
@ -58,7 +61,15 @@ export class Container extends EventHandler{
|
||||
this.terminal.write(new TextDecoder().decode(fixedData));
|
||||
|
||||
})
|
||||
this.ws = new WebSocket(`/container/sock/${channelUid}.json`)
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
const wsUrl = `${protocol}//${host}/container/sock/${this.channelUid}.json`;
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
|
||||
|
||||
this.ws.binaryType = "arraybuffer"; // Support binary data
|
||||
this.ws.onmessage = (event) => {
|
||||
this.emit("stdout", event.data)
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
/* A <file-browser> custom element that talks to /api/files */
|
||||
import { NjetComponent } from "/njet.js";
|
||||
|
||||
class FileBrowser extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
@ -6,9 +8,11 @@ class FileBrowser extends HTMLElement {
|
||||
this.path = ""; // current virtual path ("" = ROOT)
|
||||
this.offset = 0; // pagination offset
|
||||
this.limit = 40; // items per request
|
||||
this.url = '/drive.json'
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.url = this.getAttribute("url") || this.url;
|
||||
this.path = this.getAttribute("path") || "";
|
||||
this.renderShell();
|
||||
this.load();
|
||||
@ -58,7 +62,7 @@ class FileBrowser extends HTMLElement {
|
||||
// ---------- Networking ----------------------------------------------
|
||||
async load() {
|
||||
const r = await fetch(
|
||||
`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`,
|
||||
this.url + `?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`
|
||||
);
|
||||
if (!r.ok) {
|
||||
console.error(await r.text());
|
||||
|
59
src/snek/static/file-upload-grid.css
Normal file
59
src/snek/static/file-upload-grid.css
Normal file
@ -0,0 +1,59 @@
|
||||
.fug-root {
|
||||
background: #181818;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
/*min-height: 100vh;*/
|
||||
}
|
||||
.fug-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 150px);
|
||||
gap: 20px;
|
||||
margin: 30px;
|
||||
}
|
||||
.fug-tile {
|
||||
background: #111;
|
||||
border: 2px solid #ff6600;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 8px 8px 8px;
|
||||
box-shadow: 0 0 4px #333;
|
||||
position: relative;
|
||||
}
|
||||
.fug-icon {
|
||||
width: 48px; height: 48px;
|
||||
margin-bottom: 8px;
|
||||
object-fit: cover;
|
||||
background: #222;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.fug-filename {
|
||||
word-break: break-all;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.95em;
|
||||
text-align: center;
|
||||
}
|
||||
.fug-progressbar {
|
||||
width: 100%;
|
||||
height: 7px;
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: auto;
|
||||
}
|
||||
.fug-progress {
|
||||
height: 100%;
|
||||
background: #ffb200;
|
||||
transition: width 0.15s;
|
||||
width: 0%;
|
||||
}
|
||||
.fug-tile.fug-done {
|
||||
border-color: #0f0;
|
||||
}
|
||||
.fug-fileinput {
|
||||
margin: 24px;
|
||||
font-size: 1.1em;
|
||||
display: none;
|
||||
}
|
||||
|
175
src/snek/static/file-upload-grid.js
Normal file
175
src/snek/static/file-upload-grid.js
Normal file
@ -0,0 +1,175 @@
|
||||
import { NjetComponent, NjetDialog } from '/njet.js';
|
||||
|
||||
const FUG_ICONS = {
|
||||
file: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect x="8" y="8" width="32" height="40" rx="5" fill="white" stroke="%234af" stroke-width="2"/></svg>'
|
||||
};
|
||||
|
||||
class FileUploadGrid extends NjetComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this._grid = null;
|
||||
this._fileInput = null;
|
||||
this.channelUid = null ;
|
||||
this.uploadsDone = 0;
|
||||
this.uploadsStarted = 0;
|
||||
}
|
||||
|
||||
openFileDialog() {
|
||||
if(this.isBusy){
|
||||
const dialog = new NjetDialog({
|
||||
title: 'Upload in progress',
|
||||
content: 'Please wait for the current upload to complete.',
|
||||
primaryButton: {
|
||||
text: 'OK',
|
||||
handler: function () {
|
||||
this.closest('njet-dialog').remove();
|
||||
}
|
||||
}
|
||||
})
|
||||
dialog.open();
|
||||
return false
|
||||
}
|
||||
|
||||
if (this._fileInput) {
|
||||
this._fileInput.value = ''; // Allow same file selection twice
|
||||
this._fileInput.click();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Root wrapper for styling
|
||||
this.classList.add('fug-root');
|
||||
// Clear previous (if rerendered)
|
||||
this.innerHTML = '';
|
||||
// Input
|
||||
this._fileInput = document.createElement('input');
|
||||
this._fileInput.type = 'file';
|
||||
this._fileInput.className = 'fug-fileinput';
|
||||
this._fileInput.multiple = true;
|
||||
// Grid
|
||||
this._grid = document.createElement('div');
|
||||
this._grid.className = 'fug-grid';
|
||||
this.appendChild(this._fileInput);
|
||||
this.appendChild(this._grid);
|
||||
this.uploadsDone = 0;
|
||||
this.uploadsStarted = 0;
|
||||
}
|
||||
|
||||
reset(){
|
||||
this.uploadsDone = 0;
|
||||
this.uploadsStarted = 0;
|
||||
this._grid.innerHTML = '';
|
||||
}
|
||||
|
||||
get isBusy(){
|
||||
return this.uploadsDone != this.uploadsStarted;
|
||||
}
|
||||
|
||||
handleFiles(files) {
|
||||
this.reset()
|
||||
this.uploadsDone = 0;
|
||||
this.uploadsStarted = files.length;
|
||||
[...files].forEach(file => this.createTile(file));
|
||||
}
|
||||
connectedCallback() {
|
||||
this.channelUid = this.getAttribute('channel');
|
||||
this.render();
|
||||
this._fileInput.addEventListener('change', e => this.handleFiles(e.target.files));
|
||||
}
|
||||
|
||||
createTile(file) {
|
||||
const tile = document.createElement('div');
|
||||
tile.className = 'fug-tile';
|
||||
// Icon/Thumbnail
|
||||
let icon;
|
||||
if (file.type.startsWith('image/')) {
|
||||
icon = document.createElement('img');
|
||||
icon.className = 'fug-icon';
|
||||
icon.src = URL.createObjectURL(file);
|
||||
icon.onload = () => URL.revokeObjectURL(icon.src);
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
icon = document.createElement('video');
|
||||
icon.className = 'fug-icon';
|
||||
icon.src = URL.createObjectURL(file);
|
||||
icon.muted = true;
|
||||
icon.playsInline = true;
|
||||
icon.controls = false;
|
||||
icon.preload = 'metadata';
|
||||
icon.onloadeddata = () => {
|
||||
icon.currentTime = 0.5;
|
||||
URL.revokeObjectURL(icon.src);
|
||||
};
|
||||
} else {
|
||||
icon = document.createElement('img');
|
||||
icon.className = 'fug-icon';
|
||||
icon.src = FUG_ICONS.file;
|
||||
}
|
||||
// Filename
|
||||
const name = document.createElement('div');
|
||||
name.className = 'fug-filename';
|
||||
name.textContent = file.name;
|
||||
// Progressbar
|
||||
const progressbar = document.createElement('div');
|
||||
progressbar.className = 'fug-progressbar';
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'fug-progress';
|
||||
progressbar.appendChild(progress);
|
||||
|
||||
// Tile composition
|
||||
tile.appendChild(icon);
|
||||
tile.appendChild(name);
|
||||
tile.appendChild(progressbar);
|
||||
this._grid.appendChild(tile);
|
||||
|
||||
// Start upload
|
||||
this.startUpload(file, tile, progress);
|
||||
|
||||
}
|
||||
|
||||
startUpload(file, tile, progress) {
|
||||
|
||||
this.publish('file-uploading', {file: file, tile: tile, progress: progress});
|
||||
|
||||
const protocol = location.protocol === "https:" ? "wss://" : "ws://";
|
||||
const ws = new WebSocket(`${protocol}${location.host}/channel/${this.channelUid}/attachment.sock`);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
let sent = 0;
|
||||
|
||||
ws.onopen = async () => {
|
||||
ws.send(JSON.stringify({type: 'start', filename: file.name}));
|
||||
const chunkSize = 64*1024;
|
||||
while (sent < file.size) {
|
||||
const chunk = file.slice(sent, sent + chunkSize);
|
||||
ws.send(await chunk.arrayBuffer());
|
||||
sent += chunkSize;
|
||||
}
|
||||
ws.send(JSON.stringify({type: 'end', filename: file.name}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
|
||||
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'progress') {
|
||||
const pct = Math.min(100, Math.round(100 * data.bytes / file.size));
|
||||
progress.style.width = pct + '%';
|
||||
this.publish('file-uploading', {file: file, tile: tile, progress: progress});
|
||||
} else if (data.type === 'done') {
|
||||
|
||||
this.uploadsDone += 1;
|
||||
this.publish('file-uploaded', {file: file, tile: tile, progress: progress});
|
||||
progress.style.width = '100%';
|
||||
tile.classList.add('fug-done');
|
||||
|
||||
ws.close();
|
||||
|
||||
this.reset()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('file-upload-grid', FileUploadGrid);
|
||||
|
||||
export { FileUploadGrid };
|
18
src/snek/static/njet.css
Normal file
18
src/snek/static/njet.css
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
/* CSS version */
|
||||
.njet-dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
min-width: 33px;
|
||||
}
|
||||
.njet-window {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
min-width: 33px;
|
||||
}
|
508
src/snek/static/njet.js
Normal file
508
src/snek/static/njet.js
Normal file
@ -0,0 +1,508 @@
|
||||
|
||||
|
||||
class RestClient {
|
||||
constructor({ baseURL = '', headers = {} } = {}) {
|
||||
this.baseURL = baseURL;
|
||||
this.headers = { ...headers };
|
||||
|
||||
// Interceptor containers
|
||||
this.interceptors = {
|
||||
request: {
|
||||
handlers: [],
|
||||
use: (fn) => {
|
||||
this.interceptors.request.handlers.push(fn);
|
||||
}
|
||||
},
|
||||
response: {
|
||||
handlers: [],
|
||||
use: (successFn, errorFn) => {
|
||||
this.interceptors.response.handlers.push({ success: successFn, error: errorFn });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Core request method
|
||||
request(config) {
|
||||
// Merge defaults
|
||||
const cfg = {
|
||||
method: 'GET',
|
||||
url: '',
|
||||
data: null,
|
||||
headers: {},
|
||||
...config
|
||||
};
|
||||
cfg.headers = { ...this.headers, ...cfg.headers };
|
||||
|
||||
// Apply request interceptors
|
||||
let chain = Promise.resolve(cfg);
|
||||
this.interceptors.request.handlers.forEach((fn) => {
|
||||
chain = chain.then(fn);
|
||||
});
|
||||
|
||||
// Perform fetch
|
||||
chain = chain.then((c) => {
|
||||
const url = this.baseURL + c.url;
|
||||
const options = { method: c.method, headers: c.headers };
|
||||
if (c.data != null) {
|
||||
options.body = JSON.stringify(c.data);
|
||||
if (!options.headers['Content-Type']) {
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
return fetch(url, options).then(async (response) => {
|
||||
const text = await response.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
data = text;
|
||||
}
|
||||
const result = {
|
||||
data,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: RestClient._parseHeaders(response.headers),
|
||||
config: c,
|
||||
request: response
|
||||
};
|
||||
if (!response.ok) {
|
||||
return Promise.reject(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
// Apply response interceptors
|
||||
this.interceptors.response.handlers.forEach(({ success, error }) => {
|
||||
chain = chain.then(success, error);
|
||||
});
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
// Helper methods for HTTP verbs
|
||||
get(url, config) {
|
||||
return this.request({ ...config, method: 'GET', url });
|
||||
}
|
||||
delete(url, config) {
|
||||
return this.request({ ...config, method: 'DELETE', url });
|
||||
}
|
||||
head(url, config) {
|
||||
return this.request({ ...config, method: 'HEAD', url });
|
||||
}
|
||||
options(url, config) {
|
||||
return this.request({ ...config, method: 'OPTIONS', url });
|
||||
}
|
||||
post(url, data, config) {
|
||||
return this.request({ ...config, method: 'POST', url, data });
|
||||
}
|
||||
put(url, data, config) {
|
||||
return this.request({ ...config, method: 'PUT', url, data });
|
||||
}
|
||||
patch(url, data, config) {
|
||||
return this.request({ ...config, method: 'PATCH', url, data });
|
||||
}
|
||||
|
||||
// Utility to parse Fetch headers into an object
|
||||
static _parseHeaders(headers) {
|
||||
const result = {};
|
||||
for (const [key, value] of headers.entries()) {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Njet extends HTMLElement {
|
||||
static _root = null
|
||||
static showDialog = null
|
||||
|
||||
get isRoot() {
|
||||
return Njet._root === this
|
||||
}
|
||||
|
||||
get root() {
|
||||
return Njet._root
|
||||
}
|
||||
|
||||
get rest() {
|
||||
return Njet._root._rest
|
||||
}
|
||||
attach(element) {
|
||||
this._attachedTo = element
|
||||
this._attachedTo.addEventListener("resize", () => {
|
||||
this.updatePosition()
|
||||
})
|
||||
}
|
||||
updatePosition(){
|
||||
if(this._attachedTo)
|
||||
{
|
||||
this.style.width = `${this._attachedTo.offsetWidth}`
|
||||
this.style.height = `${this._attachedTo.offsetHeight}`
|
||||
this.style.left = `${this._attachedTo.offsetLeft}`
|
||||
this.style.top = `${this._attachedTo.offsetTop}`
|
||||
this.style.position = 'fixed'
|
||||
}
|
||||
}
|
||||
_subscriptions = {}
|
||||
_elements = []
|
||||
_rest = null
|
||||
_attachedTo = null
|
||||
match(args) {
|
||||
return Object.entries(args).every(([key, value]) => this[key] === value);
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.dataset[key] = value
|
||||
}
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (this.dataset[key]) {
|
||||
return this.dataset[key]
|
||||
}
|
||||
if (defaultValue === undefined) {
|
||||
return
|
||||
}
|
||||
this.dataset[key] = defaultValue
|
||||
return this.dataset[key]
|
||||
}
|
||||
|
||||
showDialog(args){
|
||||
|
||||
// const dialog = this.createComponent('njet-dialog', args)
|
||||
// dialog.show()
|
||||
// return dialog()
|
||||
}
|
||||
|
||||
find(args) {
|
||||
for (let element of this.root._elements) {
|
||||
if (element.match(args)) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
findAll(args) {
|
||||
return this.root._elements.filter(element => element.match(args))
|
||||
}
|
||||
|
||||
subscribe(event, callback) {
|
||||
if (!this.root._subscriptions[event]) {
|
||||
this.root._subscriptions[event] = []
|
||||
}
|
||||
this.root._subscriptions[event].push(callback)
|
||||
}
|
||||
|
||||
publish(event, data) {
|
||||
if (this.root._subscriptions[event]) {
|
||||
this.root._subscriptions[event].forEach(callback => callback(data))
|
||||
}
|
||||
}
|
||||
|
||||
static registerComponent(name, component) {
|
||||
customElements.define(name, component);
|
||||
}
|
||||
|
||||
constructor(config = {}) {
|
||||
super();
|
||||
if (!Njet._root) {
|
||||
Njet._root = this
|
||||
Njet._rest = new RestClient({ baseURL: config.baseURL || null })
|
||||
}
|
||||
this.root._elements.push(this)
|
||||
this.classList.add('njet');
|
||||
this.config = config;
|
||||
this.render.call(this);
|
||||
this.initProps(config);
|
||||
if (typeof this.construct === 'function')
|
||||
this.construct.call(this)
|
||||
}
|
||||
|
||||
initProps(config) {
|
||||
const props = Object.keys(config)
|
||||
props.forEach(prop => {
|
||||
if (config[prop] !== undefined) {
|
||||
this[prop] = config[prop];
|
||||
}
|
||||
});
|
||||
if (config.classes) {
|
||||
this.classList.add(...config.classes);
|
||||
}
|
||||
}
|
||||
|
||||
duplicate() {
|
||||
const duplicatedConfig = { ...this.config };
|
||||
if (duplicatedConfig.items) {
|
||||
duplicatedConfig.items = duplicatedConfig.items.map(item => {
|
||||
return typeof item.duplicate === 'function' ? item.duplicate() : item;
|
||||
});
|
||||
}
|
||||
return new this.constructor(duplicatedConfig);
|
||||
}
|
||||
|
||||
set width(val) {
|
||||
this.style.width = typeof val === 'number' ? `${val}px` : val;
|
||||
}
|
||||
|
||||
get width() { return this.style.width; }
|
||||
|
||||
set height(val) {
|
||||
this.style.height = typeof val === 'number' ? `${val}px` : val;
|
||||
}
|
||||
|
||||
get height() { return this.style.height; }
|
||||
|
||||
set left(val) {
|
||||
this.style.position = 'absolute';
|
||||
this.style.left = typeof val === 'number' ? `${val}px` : val;
|
||||
}
|
||||
|
||||
get left() { return this.style.left; }
|
||||
|
||||
set top(val) {
|
||||
this.style.position = 'absolute';
|
||||
this.style.top = typeof val === 'number' ? `${val}px` : val;
|
||||
}
|
||||
|
||||
get top() { return this.style.top; }
|
||||
|
||||
set opacity(val) { this.style.opacity = val; }
|
||||
get opacity() { return this.style.opacity; }
|
||||
|
||||
set disabled(val) { this.toggleAttribute('disabled', !!val); }
|
||||
get disabled() { return this.hasAttribute('disabled'); }
|
||||
|
||||
set visible(val) { this.style.display = val ? '' : 'none'; }
|
||||
get visible() { return this.style.display !== 'none'; }
|
||||
|
||||
render() {}
|
||||
}
|
||||
|
||||
class Component extends Njet {}
|
||||
|
||||
class NjetPanel extends Component {
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
const { title, items = [] } = this.config;
|
||||
this.style.border = '1px solid #ccc';
|
||||
this.style.padding = '10px';
|
||||
if (title) {
|
||||
const header = document.createElement('h3');
|
||||
header.textContent = title;
|
||||
this.appendChild(header);
|
||||
}
|
||||
items.forEach(item => this.appendChild(item));
|
||||
}
|
||||
}
|
||||
Njet.registerComponent('njet-panel', NjetPanel);
|
||||
|
||||
class NjetButton extends Component {
|
||||
render() {
|
||||
this.classList.add('njet-button');
|
||||
this.innerHTML = '';
|
||||
const button = document.createElement('button');
|
||||
button.textContent = this.config.text || 'Button';
|
||||
if (typeof this.config.handler === 'function') {
|
||||
button.addEventListener('click', (event) => this.config.handler.call(this));
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
button.disabled = this.disabled;
|
||||
});
|
||||
observer.observe(this, { attributes: true, attributeFilter: ['disabled'] });
|
||||
button.disabled = this.disabled;
|
||||
this.appendChild(button);
|
||||
}
|
||||
}
|
||||
Njet.registerComponent('njet-button', NjetButton);
|
||||
|
||||
class NjetDialog extends Component {
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
const { title, content, primaryButton, secondaryButton } = this.config;
|
||||
this.classList.add('njet-dialog');
|
||||
if (title) {
|
||||
const header = document.createElement('h2');
|
||||
header.textContent = title;
|
||||
this.appendChild(header);
|
||||
}
|
||||
if (content) {
|
||||
const body = document.createElement('div');
|
||||
body.innerHTML = content;
|
||||
this.appendChild(body);
|
||||
}
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.marginTop = '20px';
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.justifyContent = 'flenjet-end';
|
||||
buttonContainer.style.gap = '10px';
|
||||
if (secondaryButton) {
|
||||
const secondary = new NjetButton(secondaryButton);
|
||||
buttonContainer.appendChild(secondary);
|
||||
}
|
||||
if (primaryButton) {
|
||||
const primary = new NjetButton(primaryButton);
|
||||
buttonContainer.appendChild(primary);
|
||||
}
|
||||
this.appendChild(buttonContainer);
|
||||
}
|
||||
|
||||
show(){
|
||||
document.body.appendChild(this)
|
||||
}
|
||||
}
|
||||
Njet.registerComponent('njet-dialog', NjetDialog);
|
||||
|
||||
class NjetWindow extends Component {
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
const { title, content, primaryButton, secondaryButton } = this.config;
|
||||
this.classList.add('njet-window');
|
||||
|
||||
if (title) {
|
||||
const header = document.createElement('h2');
|
||||
header.textContent = title;
|
||||
this.appendChild(header);
|
||||
}
|
||||
this.config.items.forEach(item => this.appendChild(item));
|
||||
|
||||
}
|
||||
|
||||
show(){
|
||||
document.body.appendChild(this)
|
||||
}
|
||||
}
|
||||
Njet.registerComponent('njet-window', NjetWindow);
|
||||
|
||||
|
||||
|
||||
|
||||
class NjetGrid extends Component {
|
||||
render() {
|
||||
this.classList.add('njet-grid');
|
||||
this.innerHTML = '';
|
||||
const table = document.createElement('table');
|
||||
table.style.width = '100%';
|
||||
table.style.borderCollapse = 'collapse';
|
||||
const data = this.config.data || [];
|
||||
data.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
Object.values(row).forEach(cell => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = cell;
|
||||
td.style.border = '1px solid #ddd';
|
||||
td.style.padding = '4px';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
table.appendChild(tr);
|
||||
});
|
||||
this.appendChild(table);
|
||||
}
|
||||
}
|
||||
Njet.registerComponent('njet-grid', NjetGrid);
|
||||
/*
|
||||
const button = new NjetButton({
|
||||
classes: ['my-button'],
|
||||
text: 'Shared',
|
||||
tag: 'shared',
|
||||
width: 120,
|
||||
height: 30,
|
||||
handler() {
|
||||
this.root.findAll({ tag: 'shared' }).forEach(e => {
|
||||
e.disabled = !e.disabled;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const button2 = new NjetButton({
|
||||
classes: ['my-button'],
|
||||
text: 'Single',
|
||||
iValue: 0,
|
||||
width: 120,
|
||||
height: 30,
|
||||
handler() {
|
||||
this.iValue++;
|
||||
const panel = this.closest('njet-panel');
|
||||
if (panel) {
|
||||
const h3 = panel.querySelector('h3');
|
||||
if (h3) h3.textContent = `Click ${this.iValue}`;
|
||||
}
|
||||
this.publish("btn2Click", `Click ${this.iValue}`);
|
||||
}
|
||||
});
|
||||
|
||||
const grid = new NjetGrid({
|
||||
data: [
|
||||
{ id: 1, name: 'John' },
|
||||
{ id: 2, name: 'Jane' }
|
||||
],
|
||||
width: '100%',
|
||||
visible: true
|
||||
});
|
||||
|
||||
const panel = new NjetPanel({
|
||||
title: 'My Panel',
|
||||
items: [button, grid, button2],
|
||||
left: 50,
|
||||
top: 50,
|
||||
construct: function () {
|
||||
this.subscribe('btn2Click', (data) => {
|
||||
this._title = data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(panel);
|
||||
|
||||
const panelClone = panel.duplicate();
|
||||
const panell = panelClone.duplicate();
|
||||
panell.left = 120;
|
||||
panell.width = 300;
|
||||
panelClone.appendChild(panell);
|
||||
panelClone.left = 300;
|
||||
panelClone.top = 50;
|
||||
document.body.appendChild(panelClone);
|
||||
|
||||
const dialog = new NjetDialog({
|
||||
title: 'Confirm Action',
|
||||
content: 'Are you sure you want to continue?',
|
||||
primaryButton: {
|
||||
text: 'Yes',
|
||||
handler: function () {
|
||||
alert('Confirmed');
|
||||
this.closest('njet-dialog').remove();
|
||||
}
|
||||
},
|
||||
secondaryButton: {
|
||||
text: 'Cancel',
|
||||
handler: function () {
|
||||
this.closest('njet-dialog').remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
*/
|
||||
|
||||
class NjetComponent extends Component {}
|
||||
const njet = Njet;
|
||||
njet.showDialog = function(args){
|
||||
const dialog = new NjetDialog(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, NjetWindow };
|
@ -17,6 +17,19 @@ class ComposeFileManager:
|
||||
self.running_instances = {}
|
||||
self.event_handler = event_handler
|
||||
|
||||
async def shutdown(self):
|
||||
print("Stopping all sessions")
|
||||
|
||||
tasks = []
|
||||
for name in self.list_instances():
|
||||
proc = self.running_instances.get(name)
|
||||
if not proc:
|
||||
continue
|
||||
if proc['proc'].returncode == None:
|
||||
print("Stopping",name)
|
||||
tasks.append(asyncio.create_task(proc['proc'].stop()))
|
||||
print("Stopped",name,"gracefully")
|
||||
return tasks
|
||||
def _load(self):
|
||||
try:
|
||||
with open(self.compose_path) as f:
|
||||
@ -62,7 +75,13 @@ class ComposeFileManager:
|
||||
volumes=None,
|
||||
):
|
||||
service = {
|
||||
"image": image,
|
||||
"build": {
|
||||
"context": ".",
|
||||
"dockerfile": "DockerfileUbuntu",
|
||||
},
|
||||
"user":"root",
|
||||
"working_dir": "/home/retoor/projects/snek",
|
||||
"environment": [f"SNEK_UID={name}"],
|
||||
}
|
||||
service["command"] = command or "tail -f /dev/null"
|
||||
if cpus or memory:
|
||||
@ -153,15 +172,21 @@ class ComposeFileManager:
|
||||
)
|
||||
if name in self.running_instances:
|
||||
del self.running_instances[name]
|
||||
stdout, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Failed to stop {name}: {stderr.decode()}")
|
||||
stdout, stderr = None, None
|
||||
while proc.returncode is None:
|
||||
try:
|
||||
stdout, stderr = await proc.communicate()
|
||||
except Exception as ex:
|
||||
stdout = b''
|
||||
stderr = str(ex).encode()
|
||||
await self.event_handler(name,"stdout",stdout or stderr)
|
||||
print("Return code", proc.returncode)
|
||||
if stdout:
|
||||
await self.event_handler(name,"stdout",stdout)
|
||||
return stdout.decode(errors="ignore")
|
||||
await self.event_handler(name,"stdout",stdout or b'')
|
||||
return stdout and stdout.decode(errors="ignore") or ""
|
||||
|
||||
await self.event_handler(name,"stdout",stderr)
|
||||
return stderr.decode(errors="ignore")
|
||||
await self.event_handler(name,"stdout",stderr or b'')
|
||||
return stderr and stderr.decode(errors="ignore") or ""
|
||||
|
||||
async def start(self, name):
|
||||
"""Asynchronously start a container by doing 'docker compose up -d [name]'."""
|
||||
@ -176,17 +201,27 @@ class ComposeFileManager:
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker", "compose", "-f", self.compose_path, "up", name, "-d",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout,stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
print(f"Failed to start {name}: {stderr.decode(errors='ignore')}")
|
||||
return False
|
||||
stdout, stderr = None, None
|
||||
while proc.returncode is None:
|
||||
try:
|
||||
stdout, stderr = await proc.communicate()
|
||||
except Exception as ex:
|
||||
stdout = b''
|
||||
stderr = str(ex).encode()
|
||||
if stdout:
|
||||
print(stdout.decode(errors="ignore"))
|
||||
if stderr:
|
||||
print(stderr.decode(errors="ignore"))
|
||||
await self.event_handler(name,"stdout",stdout or stderr)
|
||||
print("Return code", proc.returncode)
|
||||
|
||||
master, slave = pty.openpty()
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker", "compose", "-f", self.compose_path, "exec", name, "/bin/bash",
|
||||
"docker", "compose", "-f", self.compose_path, "exec", name, "/usr/local/bin/entry",
|
||||
stdin=slave,
|
||||
stdout=slave,
|
||||
stderr=slave,
|
||||
|
@ -6,7 +6,12 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Snek</title>
|
||||
<style>{{highlight_styles}}</style>
|
||||
<link rel="stylesheet" href="/file-upload-grid.css">
|
||||
|
||||
<script src="/njet.js" type="module"></script>
|
||||
<script src="/file-upload-grid.js" type="module"></script>
|
||||
<script src="/polyfills/Promise.withResolvers.js" type="module"></script>
|
||||
|
||||
<script src="/push.js" type="module"></script>
|
||||
<script src="/fancy-button.js" type="module"></script>
|
||||
<script src="/upload-button.js" type="module"></script>
|
||||
@ -18,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">
|
||||
@ -55,7 +61,59 @@
|
||||
import { app } from "/app.js";
|
||||
import { Container } from "/container.js";
|
||||
let prevKey = null;
|
||||
document.addEventListener("keydown", () => {
|
||||
|
||||
function toggleDevelopmentMode(){
|
||||
const headerElement = document.querySelector('header');
|
||||
headerElement.style.display = 'none';
|
||||
const sidebarElement = document.querySelector('aside');
|
||||
|
||||
sidebarElement.style.position = 'fixed'
|
||||
sidebarElement.style.width= '10%'
|
||||
sidebarElement.style.top = '0px'
|
||||
sidebarElement.style.left='0px'
|
||||
sidebarElement.style.height='100%'
|
||||
|
||||
// sidebarElement.style.display = 'none';
|
||||
const containerElement = document.querySelector('#terminal');
|
||||
containerElement.style.position = 'fixed';
|
||||
containerElement.style.width = '50%';
|
||||
containerElement.style.height = '100%';
|
||||
containerElement.style.left = '10%';
|
||||
containerElement.style.top = '0px';
|
||||
//window.container.resizeToPercentage(document.body,'50%','100%')
|
||||
|
||||
const messagesElement = document.querySelector('.chat-area');
|
||||
messagesElement.style.position = 'fixed';
|
||||
messagesElement.style.width = '40%';
|
||||
messagesElement.style.height = '100%';
|
||||
messagesElement.style.left = '60%';
|
||||
messagesElement.style.top = '0px';
|
||||
|
||||
const messageList = document.querySelector('message-list')
|
||||
messageList.scrollToBottom()
|
||||
|
||||
window.container.fit()
|
||||
|
||||
app.starField.renderWord("H4x0r 1337")
|
||||
}
|
||||
|
||||
{% if channel %}
|
||||
app.channelUid = '{{ channel.uid.value }}'
|
||||
window.getContainer = async function (){
|
||||
if(window.c) return window.c
|
||||
window.c = new Container(app.channelUid,false)
|
||||
window.c.start()
|
||||
window.t = document.querySelector("#terminal")
|
||||
window.t.classList.toggle("hidden")
|
||||
window.c.render(window.t)
|
||||
|
||||
|
||||
return window.c
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
document.addEventListener("keydown", async() => {
|
||||
if(prevKey == "Escape"){
|
||||
document.querySelector("chat-input").querySelector("textarea").value = "";
|
||||
}
|
||||
@ -63,25 +121,32 @@
|
||||
app.starField.shuffleAll(5000)
|
||||
|
||||
}
|
||||
if(event.key == "." && event.ctrlKey){
|
||||
event.preventDefault();
|
||||
if(!window.c)
|
||||
{
|
||||
window.getContainer()
|
||||
}
|
||||
if(window.c){
|
||||
|
||||
toggleDevelopmentMode()
|
||||
//window.container.terminal.element.hidden = !window.container.terminal.element.hidden
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
prevKey = event.key
|
||||
if(event.key == "," && event.ctrlKey){
|
||||
event.preventDefault();
|
||||
let textAreas = document.querySelectorAll("textarea")
|
||||
textAreas.forEach(textArea => {
|
||||
if(document.activeElement != textArea)
|
||||
setTimeout(() => textArea.focus(), 300)
|
||||
setTimeout(() => textArea.focus(), 10)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
{% if channel %}
|
||||
app.channelUid = '{{ channel.uid.value }}'
|
||||
|
||||
window.getContainer = function(){
|
||||
return new Container(app.channelUid,false)
|
||||
}
|
||||
{% endif %}
|
||||
let installPrompt = null
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
//e.preventDefault();
|
||||
|
40
src/snek/templates/channel.html
Normal file
40
src/snek/templates/channel.html
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
<script type="module">
|
||||
import { njet } from "/njet.js"
|
||||
|
||||
app.channel = {}
|
||||
|
||||
|
||||
app.channel.remove = function(channelUid){
|
||||
|
||||
const dialog = new njet.showDialog({
|
||||
title: 'Upload in progress',
|
||||
content: 'Please wait for the current upload to complete.',
|
||||
primaryButton: {
|
||||
text: 'OK',
|
||||
handler: function () {
|
||||
dialog.remove();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.channel._fileManager = null
|
||||
app.channel.toggleDrive = function(channelUid){
|
||||
if(app.channel._fileManager){
|
||||
app.channel._fileManager.remove()
|
||||
app.channel._fileManager = null
|
||||
document.querySelector('message-list').style.display = 'block'
|
||||
return
|
||||
}
|
||||
app.channel._fileManager = document.createElement("file-manager")
|
||||
app.channel._fileManager.style.padding = '10px'
|
||||
app.channel._fileManager.setAttribute("url",`/channel/${app.channelUid}/drive.json`)
|
||||
document.querySelector(".chat-area").insertBefore(app.channel._fileManager,document.querySelector(".chat-area").firstChild)
|
||||
document.querySelector("message-list").style.display = 'none'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
@ -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 } = {}) {
|
||||
@ -12,6 +20,250 @@ class StarField {
|
||||
this.originalColor = getComputedStyle(document.documentElement).getPropertyValue("--star-color").trim();
|
||||
this._createStars();
|
||||
window.stars = this.positionMap;
|
||||
this.patchConsole()
|
||||
this.starSignal = (() => {
|
||||
const positionMap = this.positionMap;
|
||||
|
||||
const areaMap = {
|
||||
Center: 'Center',
|
||||
Corner: 'Corner',
|
||||
Edge: 'Edge',
|
||||
North: 'North',
|
||||
South: 'South',
|
||||
East: 'East',
|
||||
West: 'West',
|
||||
All: Object.keys(positionMap),
|
||||
};
|
||||
|
||||
const applyEffect = (stars, effectFn, duration = 2000) => {
|
||||
const active = new Set();
|
||||
stars.forEach((star, i) => {
|
||||
const { cleanup } = effectFn(star, i);
|
||||
active.add(cleanup);
|
||||
});
|
||||
setTimeout(() => {
|
||||
active.forEach(fn => fn && fn());
|
||||
}, duration);
|
||||
};
|
||||
|
||||
const effects = {
|
||||
pulseColor: (color, size = 5) => (star) => {
|
||||
const orig = {
|
||||
color: star.style.backgroundColor,
|
||||
width: star.style.width,
|
||||
height: star.style.height,
|
||||
shadow: star.style.boxShadow
|
||||
};
|
||||
star.style.backgroundColor = color;
|
||||
star.style.width = `${size}px`;
|
||||
star.style.height = `${size}px`;
|
||||
star.style.boxShadow = `0 0 ${size * 2}px ${color}`;
|
||||
return {
|
||||
cleanup: () => {
|
||||
star.style.backgroundColor = orig.color;
|
||||
star.style.width = orig.width;
|
||||
star.style.height = orig.height;
|
||||
star.style.boxShadow = orig.shadow;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
flicker: (color) => (star) => {
|
||||
let visible = true;
|
||||
const orig = {
|
||||
color: star.style.backgroundColor,
|
||||
shadow: star.style.boxShadow
|
||||
};
|
||||
const flick = setInterval(() => {
|
||||
star.style.backgroundColor = visible ? color : '';
|
||||
star.style.boxShadow = visible ? `0 0 4px ${color}` : '';
|
||||
visible = !visible;
|
||||
}, 200);
|
||||
return {
|
||||
cleanup: () => {
|
||||
clearInterval(flick);
|
||||
star.style.backgroundColor = orig.color;
|
||||
star.style.boxShadow = orig.shadow;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
shimmer: (colors) => (star, i) => {
|
||||
const orig = star.style.backgroundColor;
|
||||
let idx = 0;
|
||||
const interval = setInterval(() => {
|
||||
star.style.backgroundColor = colors[idx % colors.length];
|
||||
idx++;
|
||||
}, 100 + (i % 5) * 10);
|
||||
return {
|
||||
cleanup: () => {
|
||||
clearInterval(interval);
|
||||
star.style.backgroundColor = orig;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const trigger = (signalName) => {
|
||||
switch (signalName) {
|
||||
// --- Notifications ---
|
||||
case 'notif.newDM':
|
||||
applyEffect(positionMap[areaMap.Corner], effects.pulseColor('white'));
|
||||
break;
|
||||
case 'notif.groupMsg':
|
||||
applyEffect(positionMap[areaMap.Edge], effects.flicker('blue'), 1000);
|
||||
break;
|
||||
case 'notif.mention':
|
||||
applyEffect(positionMap[areaMap.Center], effects.pulseColor('magenta'));
|
||||
break;
|
||||
case 'notif.react':
|
||||
applyEffect(positionMap[areaMap.South], effects.pulseColor('gold'), 1200);
|
||||
break;
|
||||
case 'notif.typing':
|
||||
applyEffect(positionMap[areaMap.West], effects.pulseColor('teal'), 1500);
|
||||
break;
|
||||
case 'notif.msgSent':
|
||||
applyEffect(positionMap[areaMap.East], effects.pulseColor('lightgreen'), 800);
|
||||
break;
|
||||
case 'notif.msgRead':
|
||||
applyEffect(positionMap[areaMap.North], effects.pulseColor('lightblue'), 1000);
|
||||
break;
|
||||
|
||||
// --- User Status ---
|
||||
case 'status.online':
|
||||
applyEffect(positionMap[areaMap.Center], effects.pulseColor('green'), 3000);
|
||||
break;
|
||||
case 'status.offline':
|
||||
applyEffect(positionMap[areaMap.Center], effects.pulseColor('gray'), 3000);
|
||||
break;
|
||||
case 'status.idle':
|
||||
applyEffect(positionMap[areaMap.Center], effects.pulseColor('orange'), 3000);
|
||||
break;
|
||||
case 'status.typing':
|
||||
applyEffect(positionMap[areaMap.Center], effects.flicker('lightblue'), 2000);
|
||||
break;
|
||||
case 'status.join':
|
||||
applyEffect(positionMap[areaMap.Edge], effects.pulseColor('cyan'), 1500);
|
||||
break;
|
||||
case 'status.leave':
|
||||
applyEffect(positionMap[areaMap.Edge], effects.pulseColor('black'), 1500);
|
||||
break;
|
||||
|
||||
// --- System ---
|
||||
case 'sys.broadcast':
|
||||
areaMap.All.forEach(area => {
|
||||
applyEffect(positionMap[area], effects.pulseColor('gold'), 1000);
|
||||
});
|
||||
break;
|
||||
case 'sys.warning':
|
||||
areaMap.All.forEach(area => {
|
||||
applyEffect(positionMap[area], effects.flicker('red'), 2000);
|
||||
});
|
||||
break;
|
||||
case 'sys.down':
|
||||
areaMap.All.forEach(area => {
|
||||
applyEffect(positionMap[area], effects.flicker('darkred'), 3000);
|
||||
});
|
||||
break;
|
||||
case 'sys.update':
|
||||
applyEffect(positionMap[areaMap.North], effects.shimmer(['cyan', 'violet', 'lime']), 2500);
|
||||
break;
|
||||
case 'sys.ping':
|
||||
areaMap.All.forEach(area => {
|
||||
applyEffect(positionMap[area], effects.pulseColor('white'), 500);
|
||||
});
|
||||
break;
|
||||
|
||||
// --- Activity & Reactions ---
|
||||
case 'activity.surge':
|
||||
applyEffect(positionMap[areaMap.Center], effects.shimmer(['white', 'blue', 'purple']), 1500);
|
||||
break;
|
||||
case 'reaction.burst':
|
||||
applyEffect(positionMap[areaMap.South], effects.pulseColor('hotpink'), 1000);
|
||||
break;
|
||||
case 'reaction.lol':
|
||||
applyEffect(positionMap[areaMap.South], effects.flicker('yellow'), 800);
|
||||
break;
|
||||
case 'reaction.likes':
|
||||
applyEffect(positionMap[areaMap.Center], effects.pulseColor('deeppink'), 1000);
|
||||
break;
|
||||
|
||||
// --- Focus & Movement ---
|
||||
case 'focus.zone':
|
||||
applyEffect(positionMap[areaMap.Center], effects.pulseColor('white'), 1500);
|
||||
break;
|
||||
case 'focus.enter':
|
||||
applyEffect(positionMap[areaMap.West], effects.pulseColor('cyan'), 1000);
|
||||
break;
|
||||
case 'focus.exit':
|
||||
applyEffect(positionMap[areaMap.East], effects.pulseColor('gray'), 1000);
|
||||
break;
|
||||
case 'focus.idle':
|
||||
applyEffect(positionMap[areaMap.North], effects.flicker('gray'), 2000);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown star signal:', signalName);
|
||||
}
|
||||
this.trigger = this.starSignal.trigger;
|
||||
};
|
||||
|
||||
return { trigger };
|
||||
})();
|
||||
}
|
||||
showLogEvent(...args) {
|
||||
this.showNotify(...args)
|
||||
}
|
||||
|
||||
mapLogLevel(level) {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
return { category: 'Info', color: 'skyblue' };
|
||||
case 'warn':
|
||||
return { category: 'Warning', color: 'orange' };
|
||||
case 'error':
|
||||
return { category: 'Error', color: 'crimson' };
|
||||
case 'log':
|
||||
default:
|
||||
return { category: 'Log', color: 'gold' };
|
||||
}
|
||||
}
|
||||
patchConsole(){
|
||||
|
||||
|
||||
|
||||
|
||||
const originalConsole = {
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
log: console.log,
|
||||
};
|
||||
const me = this
|
||||
// Override console methods
|
||||
console.info = function(...args) {
|
||||
me.showLogEvent('info', args);
|
||||
me.createConsoleStar('info', args);
|
||||
originalConsole.info.apply(console, args);
|
||||
};
|
||||
|
||||
console.warn = function(...args) {
|
||||
me.showLogEvent('warn', args);
|
||||
me.createConsoleStar('warn', args);
|
||||
originalConsole.warn.apply(console, args);
|
||||
};
|
||||
|
||||
console.error = function(...args) {
|
||||
me.showLogEvent('error', args);
|
||||
me.createConsoleStar('error', args);
|
||||
originalConsole.error.apply(console, args);
|
||||
};
|
||||
|
||||
console.log = function(...args) {
|
||||
me.showLogEvent('log', args);
|
||||
me.createConsoleStar('log', args);
|
||||
originalConsole.log.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
_getStarPosition(star) {
|
||||
@ -48,6 +300,21 @@ class StarField {
|
||||
star.shuffle = () => this._randomizeStar(star);
|
||||
star.position = this._getStarPosition(star);
|
||||
}
|
||||
createConsoleStar(level, args) {
|
||||
const { category, color } = this.mapLogLevel(level);
|
||||
const message = args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
|
||||
|
||||
this.addSpecialStar({
|
||||
title: category.toUpperCase(),
|
||||
content: message,
|
||||
category,
|
||||
color,
|
||||
onClick: () => {
|
||||
this.showNotify(level, [`[${category}]`, message]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
_placeStar(star) {
|
||||
const pos = star.position;
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block header_text %}<h2 style="color:#fff">{{ name }}</h2>{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% include "channel.html" %}
|
||||
<section class="chat-area">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
||||
@ -47,7 +47,6 @@ const messagesContainer = document.querySelector(".chat-messages");
|
||||
const chatArea = document.querySelector(".chat-area");
|
||||
const channelUid = "{{ channel.uid.value }}";
|
||||
const username = "{{ user.username.value }}";
|
||||
let container = null
|
||||
// --- Command completions ---
|
||||
chatInputField.autoCompletions = {
|
||||
"/online": showOnline,
|
||||
@ -55,14 +54,7 @@ chatInputField.autoCompletions = {
|
||||
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
||||
"/help": showHelp,
|
||||
"/container": async() =>{
|
||||
if(container == null){
|
||||
window.c = await window.getContainer()
|
||||
await window.c.start()
|
||||
window.t = document.querySelector("#terminal")
|
||||
window.t.classList.toggle("hidden")
|
||||
window.c.render(window.t)
|
||||
}
|
||||
//containerDialog.openWithStatus()
|
||||
containerDialog.openWithStatus()
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -13,6 +13,17 @@ from snek.system.view import BaseView
|
||||
|
||||
register_heif_opener()
|
||||
|
||||
from snek.view.drive import DriveApiView
|
||||
|
||||
class ChannelDriveApiView(DriveApiView):
|
||||
async def get_target(self):
|
||||
target = await self.services.channel.get_home_folder(self.request.match_info.get("channel_uid"))
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
return target
|
||||
|
||||
async def get_download_url(self, rel):
|
||||
return f"/channel/{self.request.match_info.get('channel_uid')}/drive/{urllib.parse.quote(rel)}"
|
||||
|
||||
class ChannelAttachmentView(BaseView):
|
||||
async def get(self):
|
||||
relative_path = self.request.match_info.get("relative_url")
|
||||
@ -146,6 +157,56 @@ class ChannelAttachmentView(BaseView):
|
||||
)
|
||||
|
||||
|
||||
class ChannelAttachmentUploadView(BaseView):
|
||||
|
||||
|
||||
async def get(self):
|
||||
|
||||
channel_uid = self.request.match_info.get("channel_uid")
|
||||
user_uid = self.request.session.get("uid")
|
||||
|
||||
channel_member = await self.services.channel_member.get(
|
||||
user_uid=user_uid, channel_uid=channel_uid, deleted_at=None, is_banned=False
|
||||
)
|
||||
|
||||
if not channel_member:
|
||||
return web.HTTPNotFound()
|
||||
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(self.request)
|
||||
|
||||
file = None
|
||||
filename = None
|
||||
|
||||
|
||||
msg = await ws.receive()
|
||||
if not msg.type == web.WSMsgType.TEXT:
|
||||
return web.HTTPBadRequest()
|
||||
|
||||
data = msg.json()
|
||||
if not data.get('type') == 'start':
|
||||
return web.HTTPBadRequest()
|
||||
|
||||
filename = data['filename']
|
||||
attachment = await self.services.channel_attachment.create_file(
|
||||
channel_uid=channel_uid, name=filename, user_uid=user_uid
|
||||
)
|
||||
pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True)
|
||||
async with aiofiles.open(attachment["path"], "wb") as f:
|
||||
async for msg in ws:
|
||||
if msg.type == web.WSMsgType.BINARY:
|
||||
if file is not None:
|
||||
await file.write(msg.data)
|
||||
await ws.send_json({"type": "progress", "filename": filename, "bytes": file.tell()})
|
||||
elif msg.type == web.WSMsgType.TEXT:
|
||||
data = msg.json()
|
||||
if data.get('type') == 'end':
|
||||
await ws.send_json({"type": "done", "filename": filename})
|
||||
elif msg.type == web.WSMsgType.ERROR:
|
||||
break
|
||||
return ws
|
||||
|
||||
|
||||
class ChannelView(BaseView):
|
||||
async def get(self):
|
||||
channel_name = self.request.match_info.get("channel")
|
||||
|
@ -35,7 +35,7 @@ class ContainerView(BaseView):
|
||||
|
||||
if not container['status'] == 'running':
|
||||
resp = await self.services.container.start(channel_uid)
|
||||
await ws.send_str(str(resp))
|
||||
await ws.send_bytes(b'Container is starting\n\n')
|
||||
|
||||
container_name = await self.services.container.get_container_name(channel_uid)
|
||||
|
||||
|
@ -28,8 +28,19 @@ class DriveView(BaseView):
|
||||
|
||||
|
||||
class DriveApiView(BaseView):
|
||||
async def get(self):
|
||||
|
||||
login_required = True
|
||||
|
||||
async def get_target(self):
|
||||
target = await self.services.user.get_home_folder(self.session.get("uid"))
|
||||
return target
|
||||
|
||||
async def get_download_url(self, rel):
|
||||
return f"/drive/{urllib.parse.quote(rel)}"
|
||||
|
||||
async def get(self):
|
||||
target = await self.get_target()
|
||||
original_target = target
|
||||
rel = self.request.query.get("path", "")
|
||||
offset = int(self.request.query.get("offset", 0))
|
||||
limit = int(self.request.query.get("limit", 20))
|
||||
@ -40,6 +51,9 @@ class DriveApiView(BaseView):
|
||||
if not target.exists():
|
||||
return web.json_response({"error": "Not found"}, status=404)
|
||||
|
||||
if not target.relative_to(original_target):
|
||||
return web.json_response({"error": "Not found"}, status=404)
|
||||
|
||||
if target.is_dir():
|
||||
entries = []
|
||||
for p in sorted(
|
||||
@ -79,7 +93,7 @@ class DriveApiView(BaseView):
|
||||
}
|
||||
)
|
||||
|
||||
url = self.request.url.with_path(f"/drive/{urllib.parse.quote(rel)}")
|
||||
url = self.request.url.with_path(await self.get_download_url(rel))
|
||||
return web.json_response(
|
||||
{
|
||||
"name": target.name,
|
||||
|
@ -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__)
|
||||
|
||||
|
||||
@ -202,7 +202,43 @@ class RPCView(BaseView):
|
||||
}
|
||||
)
|
||||
return channels
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
if not channel_member:
|
||||
raise Exception("Not allowed")
|
||||
|
||||
container_name = await self.services.container.get_container_name(channel_uid)
|
||||
|
||||
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()
|
||||
channel_member = await self.services.channel_member.get(
|
||||
@ -217,7 +253,7 @@ class RPCView(BaseView):
|
||||
"name": await self.services.container.get_container_name(channel_uid),
|
||||
"cpus": container["deploy"]["resources"]["limits"]["cpus"],
|
||||
"memory": container["deploy"]["resources"]["limits"]["memory"],
|
||||
"image": container["image"],
|
||||
"image": "ubuntu:latest",
|
||||
"volumes": [],
|
||||
"status": container["status"]
|
||||
}
|
||||
@ -389,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()
|
||||
|
||||
@ -496,8 +535,7 @@ class RPCView(BaseView):
|
||||
async for msg in ws:
|
||||
if msg.type == web.WSMsgType.TEXT:
|
||||
try:
|
||||
async with Profiler():
|
||||
await rpc(msg.json())
|
||||
await rpc(msg.json())
|
||||
except Exception as ex:
|
||||
print("Deleting socket", ex, flush=True)
|
||||
logger.exception(ex)
|
||||
|
47
terminal/.welcome.txt
Normal file
47
terminal/.welcome.txt
Normal file
@ -0,0 +1,47 @@
|
||||
π Welcome to your custom Ubuntu development environment!
|
||||
|
||||
This container is pre-configured with a rich set of tools to help you hit the ground running:
|
||||
|
||||
π§ Development Libraries & Tools:
|
||||
|
||||
Full support for building and debugging C/C++ and Python code
|
||||
|
||||
Libraries for SSL, readline, SQLite, zlib, bz2, ffi, lzma, and JSON-C
|
||||
|
||||
Valgrind for memory debugging and profiling
|
||||
|
||||
π Python:
|
||||
|
||||
Python 3 with pip and virtual environment support (venv)
|
||||
|
||||
π¦ Rust:
|
||||
|
||||
Rust installed with the nightly toolchain via rustup
|
||||
|
||||
π Command-line Essentials:
|
||||
|
||||
vim, tmux, htop, git, curl, wget, xterm, ack, and more
|
||||
|
||||
π¬ Networking & Communication:
|
||||
|
||||
irssi for IRC and lynx for browsing from the terminal
|
||||
|
||||
π¦ Custom Tool Installed:
|
||||
|
||||
Latest version of AI vibe coding tool r is installed from the
|
||||
retoor.molodetz.nl repository.
|
||||
|
||||
To get started, open a new terminal and type "r" to launch the tool.
|
||||
|
||||
Configure ~/.rcontext.txt to describe it's behavior. It can be a friend
|
||||
or a whatever you like.
|
||||
|
||||
π Credentials:
|
||||
|
||||
Default root password is set to: root (please change it if needed)
|
||||
|
||||
π Custom Terminal Files:
|
||||
|
||||
Files from ./terminal/ have been copied to your home directory (/root/)
|
||||
|
||||
Enjoy hacking in your personalized command-line workspace!
|
26
terminal/entry
Executable file
26
terminal/entry
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
import os
|
||||
|
||||
os.chdir("/root")
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, "/root/bin")
|
||||
sys.path.insert(0, "/root/bin/local/bin")
|
||||
|
||||
if not pathlib.Path(".welcome.txt").exists():
|
||||
os.system("python3 -m venv --prompt '' .venv")
|
||||
os.system("cp -r /opt/bootstrap/root/.* /root")
|
||||
os.system("cp /opt/bootstrap/.welcome.txt /root/.welcome.txt")
|
||||
pathlib.Path(".bashrc").write_text(pathlib.Path(".bashrc").read_text() + "\n" + "source .venv/bin/activate")
|
||||
os.environ["SNEK"] = "1"
|
||||
|
||||
if pathlib.Path(".welcome.txt").exists():
|
||||
with open(".welcome.txt") as f:
|
||||
print(f.read())
|
||||
|
||||
os.system("bash")
|
Loadingβ¦
Reference in New Issue
Block a user