diff --git a/setup.cfg b/setup.cfg index 42a92f3..ca4e69d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] -name = boeh +name = rwebgui version = 1.0.0 -description = Service that says boeh when Joe talks. +description = RWebGui author = retoor author_email = retoor@molodetz.nl license = MIT @@ -15,11 +15,10 @@ package_dir = python_requires = >=3.7 install_requires = app @ git+https://retoor.molodetz.nl/retoor/app - matrix-nio [options.packages.find] where = src [options.entry_points] console_scripts = - boeh = boeh.__main__:main + rwebgui.serve = rwebgui.__main__:main diff --git a/src/rwebgui/__init__.py b/src/rwebgui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/rwebgui/__main__.py b/src/rwebgui/__main__.py new file mode 100644 index 0000000..7a4f64e --- /dev/null +++ b/src/rwebgui/__main__.py @@ -0,0 +1,17 @@ +from rwebgui.app import Application +from aiohttp import web +import asyncio + +from concurrent.futures import ThreadPoolExecutor as Executor + + +def main(): + app = Application() + executor = Executor(max_workers=20) + loop = asyncio.get_event_loop() + loop.set_default_executor(executor) + web.run_app(app, loop=loop) + + +if __name__ == "__main__": + main() diff --git a/src/rwebgui/app.py b/src/rwebgui/app.py new file mode 100644 index 0000000..a1fbc32 --- /dev/null +++ b/src/rwebgui/app.py @@ -0,0 +1,172 @@ +import pathlib +from aiohttp import web +import uuid +from app.app import Application as BaseApplication +from rwebgui.component import Component +import traceback +import time +import asyncio +import json + +class EvalBox(Component): + + async def on_change(self, value): + + try: + if value and value.strip().endswith("="): + value = value.strip()[:-1] + try: + result = eval(value) + value = value + "= " + str(result) + await self.set_attr("value",value) + except: + pass + except AttributeError as ex: + print(value) + return value + + + +class Button(Component): + + async def on_click(self, event): + component = self.app.search + await component.set_attr("value","Woeiii") + +class Button1(Component): + + async def on_click(self,event): + field = self.app.search + await field.toggle() + value = await field.get_style("display","block") + await self.set_attr("innerText", value) + +class RandomString(Component): + + + async def task_random(self): + import random + rand_bytes = [random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(15)] + random_data = "".join(rand_bytes) + while True: + remember = random_data[0] + random_data = random_data[1:] + remember + await self.set_attr("innerHTML",random_data) + await asyncio.sleep(0.01) + +class Counter(Component): + + async def task_test(self): + while True: + await asyncio.sleep(10) + print("Slow task") + + + + async def task_increment(self): + if not self.value: + self.value = 0 + while True: + try: + self.value = int(self.value) + except: + self.value = 0 + self.value += 1 + await self.set_attr("value",self.value) + await asyncio.sleep(1) + +class GPT(Component): + + class Children: + prompt = Component + answer = Component + class submit(Component): + async def trigger(self, id_, event, data): + print("GOGOG",event,data) + return await super().trigger(id_, event, data) + async def on_click(self,data): + from xmlrpc.client import ServerProxy + client = ServerProxy("https://api.molodetz.nl/rpc") + prompt = await self.app.prompt.get_attr("value") + print(prompt) + exit(0) + + await self.answer.set_attr("value",client.gpt4o(prompt)) + return {"event_id":data['event_id'],"success":True} + +class SpeedMeter(Component): + + def __init__(self, app, id_, description=None, ws = None): + self.time_start = time.time() + self.bytes_received = 0 + super().__init__(app, id_, description, ws) + + async def task_update(self): + while True: + bytes_received = self.bytes_received + self.bytes_received = 0 + + await self.set_attr("value","{} kb/s".format(bytes_received / 1000)) + await asyncio.sleep(1) + + async def trigger(self, id_, event, data): + super().trigger(id_, event, data) + print("JAA") + self.bytes_received += len(json.dumps(data)) + + +class App(Component): + + class Children: + eval_box = EvalBox + search = Component + teller1 = Counter + teller2 = Counter + teller3 = Counter + teller4 = Counter + teller5 = Counter + teller6 = Counter + teller7 = Counter + link1 = Button + random1 = RandomString + speed = SpeedMeter + toggle = Button1 + gpt = GPT + + _service = None + + +class Application(BaseApplication): + def __init__(self): + self.location = pathlib.Path(__file__).parent + self.location_static = self.location.joinpath("static") + self.template_path = self.location.joinpath("templates") + super().__init__(template_path=self.template_path) + self.router.add_static('/static', self.location_static) + self.router.add_get("/", self.index_handler) + self.router.add_get("/ws/{uuid}", self.websocket_handler) + + async def websocket_handler(self, request): + # Extract the UUID from the route + uuid_value = request.match_info['uuid'] + + # Validate if it's a valid UUID + try: + uuid_obj = uuid.UUID(uuid_value) + except ValueError: + return web.Response(text="Invalid UUID", status=400) + + # Upgrade the connection to WebSocket + ws = web.WebSocketResponse() + await ws.prepare(request) + + print(f"WebSocket connection established with UUID: {uuid_obj}") + component = App(self, "app", ws=ws) + await component.service() + + return ws + + + + async def index_handler(self, request): + return await self.render_template("index.html",request,{}) diff --git a/src/rwebgui/component.py b/src/rwebgui/component.py new file mode 100644 index 0000000..4ca54de --- /dev/null +++ b/src/rwebgui/component.py @@ -0,0 +1,239 @@ +import uuid +import json +import time +import aiohttp +import asyncio +from aiohttp import web + +class Component: + + @classmethod + def define(cls): + return cls + + def __init__(self,app, id_, description=None,ws: web.WebSocketResponse=None): + + self.id = id_ + self.ws = ws + self.app = app + self.description = description + self.children = [] + self._callbacks = {} + self.value = None + self._running = False + if not hasattr(self,"Children"): + return + for name in dir(self.Children): + if name.startswith("__"): + continue + + obj = getattr(self.Children, name) + + instance = obj(app=self.app,id_=name,ws=ws ) + self.add_child(instance) + instance.app = self + instance.ws = self.ws + setattr(self, name, instance) + @classmethod + def from_json(cls, json): + obj = cls(None, None) + obj.__dict__ = json + return obj + + @classmethod + def to_json(cls): + obj = cls.__dict__ .copy() + return obj + + @classmethod + def clone(cls): + return cls.from_json(cls.to_json()) + + @property + def running(self): + if not hasattr(self.app, "_running"): + return self._running + return self.app._running + + @property + def tasks(self): + tasks_ = [getattr(self, name) for name in dir(self) if name.startswith("task_") and hasattr(self, name)] + for child in self.children: + tasks_ += child.tasks#.extend(await child.get_tasks()) + return tasks_ + + async def communicate(self, event_id=None): + + async for msg in self.ws: + if msg.type == web.WSMsgType.TEXT: + # Echo the message back to the client + #print(f"Received message: {msg.data}") + data = msg.json() + if not event_id: + pass + #return data + else: + if data.get("event_id") == event_id: + return data + + @property + def callbacks(self): + return hasattr(self.app, "callbacks") and self.app.callbacks or self._callbacks + + + async def trigger(self,id_, event,data): + if self.id == id_: + method_name = "on_"+event + if hasattr(self, method_name): + method = getattr(self, method_name) + await method(data) + print("JAAJ") + for child in self.children: + await child.trigger(id_,event,data) + + async def register_callback(self, event_id, callback): + self.callbacks[event_id] = callback + + async def call(self, method, args=None,id_=None, callback=True): + while not self.running: + await asyncio.sleep(0.1) + if not args: + args= [] + event_id = str(uuid.uuid4()) + loop = asyncio.get_running_loop() + future = loop.create_future() + + self.callbacks[event_id] = lambda data: future.set_result(data) + await self.ws.send_json({ + "event_id": event_id, + "event": "call", + "id": id_ and id_ or self.id, + "method": method, + "args": args, + "callback": callback + }) + if callback: + response = await self.communicate(event_id=event_id) + return response['result'] + #print("GLUKT") + #return response['result'] + + return True + #return await future + + + async def get_attr(self, key, default=None): + result = await self.call("getAttr", [self.id, key],True) + return result or default + + async def set_attr(self, key, value): + result = await self.call("setAttr", [self.id,key,value],callback=False) + return result + + async def get(self, id_): + if self.id == id_: + return self + for child in self.children: + child = await child.get(id_) + if child: + return child + + async def set_data(self, key, value): + result = await self.call("setData", [self.id, key,value], callback=False) + return result + + async def get_data(self, key, default=None): + result = await self.call("getData", [self.id,key], default,True) + return result or default + + async def set_style(self, key, value): + result = await self.call("setStyle", [self.id, key,value], callback=False) + return result + + async def toggle(self): + value = await self.get_style("display", "block") + + if value == "none": + value = "" + else: + value = "none" + await self.set_style("display", value) + + async def get_style(self, key, default=None): + result = await self.call("getStyle", [self.id,key], default) + return result or default + + + + + async def on_keyup(self,event): + value = await self.get_attr("value") + if self.value != value: + if hasattr(self, "on_change"): + value = await self.on_change(value) + self.value = value + return self.value + + + async def get_tasks(self): + tasks = self.tasks + for child in self.children: + tasks += child.tasks#.extend(await child.get_tasks()) + return tasks + + async def set_running(self): + self._running = True + + async def get_message(self): + async for msg in self.ws: + return msg + + async def service(self): + tasks = self.tasks + tasks.append(self.set_running) + + async def events(): + try: + async for msg in self.ws: + if msg.type == web.WSMsgType.TEXT: + # Echo the message back to the client + #print(f"Received message: {msg.data}") + data = msg.json() + response = {"event_id":data['event_id'],"success":True} + response['time_start'] = time.time() + if self.callbacks.get(data['event_id']): + self.callbacks[data['event_id']](data['result']) + elif data.get('data') and not data['data'].get('id'): + response['handled'] = False + elif data.get('data'): + response['handled'] = True + response['data'] = await self.trigger(data['data']['id'], data['event'],data['data']) + response['cancel'] = True + + response['time_end'] = time.time() + response['time_duration'] = response['time_end'] - response['time_start'] + await self.ws.send_json(response) + + #await ws.send_str(f"Echo: {msg.data}") + elif msg.type == web.WSMsgType.ERROR: + print(f"WebSocket error: {self.ws.exception()}") + except Exception as ex: + print(ex) + pass + + + #async def the_task(): + # while True: + # time.sleep(1) + #while True: + tasks.append(events) + await asyncio.gather(*[task() for task in tasks]) + #await asyncio.create_task(asyncio.gather(*[task() for task in tasks])) + #await tasks() + print("AFTERR") + + + def add_child(self, child): + child.app = self.app + child.ws = self.ws + self.children.append(child) diff --git a/src/rwebgui/static/rwebgui.js b/src/rwebgui/static/rwebgui.js new file mode 100644 index 0000000..8a638da --- /dev/null +++ b/src/rwebgui/static/rwebgui.js @@ -0,0 +1,295 @@ + +function elToObject(el){ + obj = {} + if(el.targetElement) + el = el.targetElement + el.getAttributeNames().forEach(name => { + obj[name] = el.getAttribute(name) + if(el[name]) + obj[name] = el[name] + }) + return obj +} + +function wrapElement(app, el){ +const allEvents = [ + 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', + 'mouseleave', 'mouseover', 'mouseout', 'keydown', 'keyup', 'keypress', + 'focus', 'blur', 'change', 'input', 'submit', 'reset', 'resize', + 'contextmenu', 'drag', 'drop', 'dragstart', 'dragend', 'dragover', + 'dragenter', 'dragleave', 'touchstart', 'touchmove', 'touchend', + 'touchcancel', 'pointerdown', 'pointerup', 'pointermove', 'pointerover', + 'pointerout', 'pointerenter', 'pointerleave', 'wheel'/*'scroll',*/ + // Add more as needed +]; + const props = [ + 'data', + 'id', + 'isTrusted', + 'altKey', + 'ctrlKey', + 'layerX', + 'layerY', + 'movementX', + 'movementY', + 'offsetX', + 'offsetY', + 'pageX', + 'pageY', + 'screenX', + 'screenY', + 'shiftKey', + 'metaKey', + 'value', + 'code', + 'keyCode', + 'key' + ] + + el.app = app + allEvents.forEach(event => { + + el.addEventListener(event, async(e) => { + if(el.app.suppress) + return + obj = {} + obj["id"] = el.id ? el.id : el._uuid + obj["uuid"] = el._uuid + obj["event"] = event + + obj['attrs'] = elToObject(el) + obj['data'] = {} + props.forEach(prop => { + if(e[prop] != undefined){ + obj['data'][prop] = e[prop] + } + if(e["targetElement"] && e["targetElement"][prop] != undefined){ + obj['data'][prop] = e["targetElement"][prop] + } + }) + + //obj["data"] = JSON.stringify(e) + obj["nr"] = el.app.inc() + response = await el.app.emit(event, obj); + + },false) + }) + +} + + +HTMLElement.prototype.rWebGui = function(){ + + const rWebGui = this + +const config = { attributes: true, childList: true, subtree: true }; +const callback = (mutationList, observer) => { + for (const mutation of mutationList) { + + if (mutation.type === "childList") { + mutation.addedNodes.forEach(child => { + + wrapElement(rWebGui, child) + if(!child._uuid){ + child._uuid = rWebGui.createUUID() + } + + }) + } else if (mutation.type === "attributes") { + obj = {} + obj["uuid"] = mutation.target._uuid + + obj[mutation.attributeName] = mutation.target.getAttribute(mutation.attributeName) + rWebGui.emit("attributeChanged", obj) + } + } +}; +// Create an observer instance linked to the callback function +const observer = new MutationObserver(callback); + +// Start observing the target node for configured mutations +observer.observe(rWebGui, config); + +// Later, you can stop observing +//observer.disconnect(); + +} + + +class MyCustomElement extends HTMLElement { + static observedAttributes = ["color", "size"]; + _ready = false + _uuid = null + ws = null + _inc = 0; + connected = false + callbacks = {} + suppress = false + inc() { + this._inc++; + return this._inc + } + + get isReady() { + return this.app._ready && this.app.connected + } + + constructor() { + // Always call super first in constructor + super(); + if(!this.parent || !this.parent.app){ + this.ws = new WebSocket(`ws://${window.location.host}/ws/${this.uuid}`) + const me = this + this.ws.onopen = ()=>{ + me.connected = true; + } + this.ws.onmessage = (e)=>{ + const data = JSON.parse(e.data) + + if(data.event_id){ + if(me.callbacks[data.event_id]) + { + me.callbacks[data.event_id](data) + }else if(data.event == "call"){ + const method = me[data.method] + if(!method){ + data['success'] = false; + data['result'] = null; + }else{ + let response = method(...data.args) + data['result'] = response + data['success'] = true + } + if(data['callback']) + me.ws.send(JSON.stringify( + data + )) + }else { + if(data.event == "set_attr"){ + const el = document.getElementById(data['id']) + if(el){ + data['data'].forEach(attr=>{ + el.setAttribute(attr['name'], attr['value']) + el[attr['name']] = attr['value'] + })} + } + } + } + + } + } + } + + getAttr(id,key){ + const el = document.getElementById(id) + if (el[key] != undefined) + return el[key] + return el.getAttribute(key) + } + getData(id,key){ + const el = document.getElementById(id) + return el.dataset[key] + } + setData(id,key,value){ + const el = document.getElementById(id) + el.dataset[key] = value + return true + } + setStyle(id, key, value){ + const el = document.getElementById(id) + el.style[key] = value + } + getStyle(id, key){ + return document.getElementById(id).style[key] + } + setAttr(id, key, value) { + try{ + document.getElementById(id).setAttribute(key, value) + document.getElementById(id)[key] = value + }catch(e){ + console.error("Element not found:", key) + console.error("Failed to set value:", value) + } + return true + } + get isApp(){ + return !this.parent || !this.parent.app + } + + emit(event, data) { + if(!this.app.isReady) + + return false; + const me = this + return new Promise(resolve => { + data["event_id"] = me.inc() + me.callbacks[data["event_id"]] = resolve + me.app.ws.send(JSON.stringify({"uuid":obj.uuid,"event_id":data["event_id"], "event": event, "data": data})) + + }).then(res => { + return res + }) + } + + get uuid() { + if(!this._uuid){ + this._uuid = this.createUUID() + } + return this._uuid + } + + createUUID(){ + const uuid = crypto.randomUUID(); + return uuid + } + + get app() { + if(!this.parent) + return this + if(!this.parent.app) + return this + return this.parent.app + } + + connectedCallback() { + console.log("Custom element added to page."); + + + this.rWebGui() + if(!this.isApp){ + this._uuid = this.generateUUID() + this._ready = true; + return; + } + + + // this.querySelectorAll("*").forEach(child => { +// child.rWebGui() + // }) + /// this.dataset.uuid = this.generateUUID(); + this._ready = true + } + + disconnectedCallback() { + console.log("Custom element removed from page."); + } + + adoptedCallback() { + console.log("Custom element moved to new page."); + } + + attributeChangedCallback(name, oldValue, newValue) { + this.emit("attributeChanged", {aa:123}) + console.log(`Attribute ${name} has changed.`); + } +} + +customElements.define("rwebgui-app", MyCustomElement); + +/* +document.addEventListener("DOMContentLoaded", () => { + console.log("DOM fully loaded and parsed"); + document.querySelectorAll("*").forEach(child => { + child.rWebGui() + }) +})*/; diff --git a/src/rwebgui/templates/index.html b/src/rwebgui/templates/index.html new file mode 100644 index 0000000..967bd91 --- /dev/null +++ b/src/rwebgui/templates/index.html @@ -0,0 +1,38 @@ + + + + + + + + +

I do stuff

+

I do stuff

+

I do stuff

+

I do stuff

+ + + + + + + + + + I do stuff + I do stuff + + +
+ +
+ + + + +
+ +
+ + +