├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app.mojo ├── app2.mojo ├── app3.mojo ├── index.html ├── mojoproject.toml ├── style.css ├── ui.mojo ├── ui_html_elements.mojo └── ui_websocket.mojo /.gitattributes: -------------------------------------------------------------------------------- 1 | # GitHub syntax highlighting 2 | pixi.lock linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pixi environments 2 | .pixi 3 | *.egg-info 4 | # magic environments 5 | .magic 6 | magic.lock 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 rd4com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 🐣 mojo-new-web-framework 4 | 5 | > ⏭️ See at least [Server](#server) (if don't feel like reading the whole page now) 6 | 7 | "The back-end and the front-end in the same structs!" 8 | 9 | Work in progress, don't use in production. 10 | 11 | **The hope is to give this an opportunity to grow, 12 | feel free to fork, also to contribute fixes to bugs or suggestions.** 13 | 14 | 15 | 16 | ✨ mojoproject.toml: 17 | 🔮 nightly 2️⃣0️⃣2️⃣4️⃣🟪1️⃣0️⃣🟪2️⃣4️⃣0️⃣6️⃣ 18 | 19 | 20 | **MAX and Mojo usage and distribution are licensed under the [MAX & Mojo Community License](https://www.modular.com/legal/max-mojo-license)** 21 | 22 | 23 | 24 | ### 🥚Summary 25 | The idea is to build pages with structs instances stored on the server, 26 | 27 | connected visitors remotely mutate their own instances with events. 28 | 29 | The render method send the result page back. 30 | 31 | The structs can be nested in html and html can be nested in structs. 32 | 33 | It is done by abstracting html tags as functions that returns a tree. 34 | 35 | The instances have an id so they are either created, retrieved or deleted. 36 | 37 | An instance is deleted if it is not rendered anymore. 38 | 39 | All of this is done on the server. 40 | 41 | The json is rendered into HtmlElement in the browser! 42 | 43 | 44 | 45 | ### 🐣 Examples: 46 | - [app.mojo](./app.mojo) is a small websocket chat (`appdata()`) 47 | - [app2.mojo](./app2.mojo) is a simple login system with a page router 48 | - [app3.mojo](./app3.mojo) is an example for `sessiondata()` 49 | 50 | 51 | 52 | **The `ui.mojo` need a lot of work,** 53 | **it is using too much `UnsafePointer`.** 54 | 55 | **`ui.mojo` should instead use the latest features of mojo.** 56 | 57 | **`Variant` which always existed would simplify the `ui.mojo` a lot too 👍** 58 | 59 | Next commits will be about making `ui.mojo` user-friendly! 60 | 61 | 62 | 63 | # More about 64 | 65 | Components: 66 | They are one or more instances of a struct. 67 | Web-frameworks use them to store states on the client-side, 68 | here, we create and store them on the server-side. 69 | That way, we can create them in mojo🔥 and Python🐍. 70 | The client-side send events and render html elements from json. 71 | 72 | Sessions: 73 | They are websocket connections, 74 | each "visitor" have one to communicate with the server. 75 | When the session is closed, it's component instances are removed. 76 | 77 | Appdata: 78 | It is a `PythonObject` dictionnary available to all sessions. 79 | That way, any Component of any session can have things in common. 80 | It is quite a simple way to have an in-memory simple DB. 81 | For example, multiple sessions can increment the same counter. 82 | 83 | Sessiondata: 84 | It is a `PythonObject` dictionnary only available to one session. 85 | That way, any Component of one session can have things in common. 86 | 87 | 88 | ## Server 89 | Because it is a work in progress, it should not be used in production. 90 | (there could be a bug or an unexpected behaviour) 91 | 92 | There is an additional safeguard for the server but it is untested: 93 | - `serve_app` takes a list of hosts as an argument, 94 | server check if new connections are from a host within the list. 95 | If it is not, the app should exit as a safeguard. 96 | 97 | See [./ui_websocket.mojo](./ui_websocket.mojo) for default host and port: 98 | ```mojo 99 | alias host="127.0.0.1" 100 | alias port=8080 101 | ``` 102 | 103 | ## Next Todos 104 | 105 | - `Variant` for `ui.mojo` 106 | 107 | 108 | ## Design 109 | - `return Action.UpdateSessions` but what if page is currently interacted ? 110 | - how browser can remember focus of elements between new pages 111 | - `**kwargs` props for `Render` or `__init__` or both ? 112 | - ✅ Added for `Render` 113 | - `Event("increment")` with any instance_name ? 114 | - url to event ?: `/api/?event_name='change_page'&value='home'` 115 | url->json->`T._event` 116 | - let's let events bubble up trough their outer components if unhandled? 117 | - that way, nested components can send event to outer components 118 | 119 | ## Features 120 | - ✅ each websocket has it's session and instances of `T:Component` 121 | - ✅ garbage collection of `Component` instances 122 | - for example: 123 | ```mojo 124 | if rand()>0.5: 125 | AppendElement( 126 | Render[Counter]("instance_name") 127 | to = page 128 | ) 129 | ``` 130 | - ✅ render dom from a json tree 131 | - ✅ an app-wide dictionary usable by all websocket sessions. 132 | (`self.session.appdata()`) 133 | - ✅ A client-wide dictionary for all components in the current session 134 | (`self.session.sessiondata()`) 135 | - ✅ hybrid events mixing js dom value with `**kwargs` ! 136 | ```mojo 137 | return Input( 138 | type='colorpicker', 139 | value = self.input_value, 140 | change = Event("changed_input", lets_go=True) 141 | ) 142 | ``` 143 | - ✅ Add a new html element easily 144 | ```python 145 | # my_elements.mojo 146 | alias H3 = CreateHtmlElement["h3"]() 147 | # Somewhere else: 148 | from my_elements import H3 149 | Div(H3(Text("Hello world"))) 150 | ``` 151 | 152 | - ✅ `**kwargs` goes to html elements attributes 153 | 154 | Inspired by [lsx](https://github.com/lsh/lsx): 155 | thanks to @ lukas 💯 156 | ```mojo 157 | return Div( 158 | Button(Text("Hello world")), 159 | `class` = "container", 160 | style = "border 1px blue", 161 | `id` = "MainDiv" 162 | ) 163 | ``` 164 | 165 | - ✅ `style.css` 166 | -------------------------------------------------------------------------------- /app.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | 3 | def main(): 4 | appdata = Python.dict() 5 | appdata["messages"] = [] 6 | exit_if_client_not_in = PythonObject(["127.0.0.1"]) 7 | serve_app[MainComponent](appdata, exit_if_client_not_in) 8 | 9 | @value 10 | struct MainComponent: #(Component): 11 | var current_message: String 12 | var session: Session 13 | def __init__(inout self, session: Session): 14 | self.current_message = "Hello websocket" 15 | self.session = session 16 | 17 | def Event(inout self, data: PythonObject)->Action: 18 | if data["EventName"] == "change_current_message": 19 | self.current_message = str(data["value"]) 20 | if data["EventName"] == "add_message": 21 | self.session.appdata()["messages"].append( 22 | self.current_message 23 | ) 24 | return Action.Render 25 | 26 | def Render(inout self, props: PythonObject)->PythonObject: 27 | all_messages = Div(style="background-color: lightblue;") 28 | for m in self.session.appdata()["messages"]: 29 | AppendElement(Text(m), to=all_messages) 30 | AppendElement(Br(), to=all_messages) 31 | 32 | return Div( 33 | Input( 34 | type='text', value=self.current_message, 35 | change=Event("change_current_message"), 36 | ), 37 | Button( 38 | Text("add message"), 39 | `class`= "btn", 40 | click = Event("add_message"), 41 | ), 42 | self.session.Render[Counter]("counter"), 43 | self.session.Render[Counter]("counter2"), 44 | Br(), 45 | Button( 46 | Text("Messages"), 47 | HtmlSpan( 48 | Text(len(self.session.appdata()["messages"])), 49 | `class` = "badge" 50 | ), 51 | `class` = "btn btn-primary", type="button" 52 | ), 53 | all_messages, 54 | ) 55 | 56 | @value 57 | struct Counter: #(Component): 58 | var count: Int 59 | var session: Session 60 | 61 | def __init__(inout self, session: Session): 62 | self.count = 0 63 | self.session = session 64 | 65 | def Event(inout self, data: PythonObject)->Action: 66 | if data["EventName"] == "increment": 67 | if data.__contains__("amount"): 68 | self.count += int(data["amount"]) 69 | else: 70 | self.count+=1 71 | return Action.Render 72 | 73 | def Render(inout self, props: PythonObject)->PythonObject: 74 | return Div( 75 | H1( 76 | Text(self.session.InstanceName + " " + str(self.count)) 77 | ), 78 | Button( 79 | Text("Increment"), 80 | `class` = "btn btn-success", 81 | click = Event("increment", amount = 2), 82 | # mouseover = Event("increment", amount = 1), 83 | ) 84 | ) 85 | 86 | -------------------------------------------------------------------------------- /app2.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | 3 | # Example of creating routing and login system with components, 4 | # (don't use it as a login system, it's not complete) 5 | 6 | def main(): 7 | appdata = Python.dict() 8 | appdata["users"] = ["user1", "user2"] 9 | exit_if_client_not_in = PythonObject(["127.0.0.1"]) 10 | serve_app[MainComponent](appdata, exit_if_client_not_in) 11 | 12 | @value 13 | struct MainComponent: #(Component): 14 | var input_login: String 15 | var connected: Optional[String] 16 | var session: Session 17 | def __init__(inout self, session: Session): 18 | self.connected = None 19 | self.input_login = "user name (user1 or user2)" 20 | self.session = session 21 | 22 | def Event(inout self, data: PythonObject)->Action: 23 | if data["EventName"] == "change_input_login": 24 | self.input_login = str(data["value"]) 25 | if data["EventName"] == "connect": 26 | if self.session.appdata()["users"].__contains__(self.input_login): 27 | self.connected = str(self.input_login) 28 | if data["EventName"] == "disconnect": 29 | self.connected = None 30 | return Action.Render 31 | 32 | def Render(inout self, props: PythonObject)->PythonObject: 33 | if self.connected: 34 | return Div( 35 | H1( 36 | Text("Connected! " + self.connected.value()), 37 | `class`= "bg-green" 38 | ), 39 | Button( 40 | Text("Disconnect"), 41 | click = Event("disconnect"), 42 | ), 43 | self.session.Render[Router]("simple_router"), 44 | ) 45 | else: return Div( 46 | Text("login:"), 47 | Input( 48 | type='text', value=self.input_login, 49 | change=Event("change_input_login"), 50 | ), 51 | Button( 52 | Text("connect"), 53 | `class`= "btn", 54 | click = Event("connect"), 55 | ), 56 | `class` = "bg-blue" 57 | ) 58 | 59 | @value 60 | struct Router: #(Component): 61 | var current_page: String 62 | var session: Session 63 | def __init__(inout self, session: Session): 64 | self.current_page = "home" 65 | self.session = session 66 | 67 | def Event(inout self, data: PythonObject)->Action: 68 | if data["EventName"] == "change_route": 69 | self.current_page = str(data["page_name"]) 70 | return Action.Render 71 | 72 | def Render(inout self, props: PythonObject)->PythonObject: 73 | page = Div( 74 | Button(Text("home"), click=Event("change_route",page_name="home")), 75 | Button(Text("test"), click=Event("change_route",page_name="test")) 76 | ) 77 | if self.current_page == "home": 78 | AppendElement( 79 | Div( 80 | H1(Text("home")), 81 | Text("Welcome to the home page!"), 82 | ), to = page 83 | ) 84 | else: 85 | AppendElement(Div(H1(Text(self.current_page))), to=page) 86 | return page -------------------------------------------------------------------------------- /app3.mojo: -------------------------------------------------------------------------------- 1 | from ui import * 2 | 3 | # Example for `session.sessiondata()` 4 | # (A client-wide dictionary for all components in the current session) 5 | 6 | def main(): 7 | appdata = Python.dict() 8 | exit_if_client_not_in = PythonObject(["127.0.0.1"]) 9 | serve_app[MainComponent](appdata, exit_if_client_not_in) 10 | 11 | @value 12 | struct MainComponent: #(Component): 13 | var session: Session 14 | def __init__(inout self, session: Session): 15 | self.session = session 16 | self.session.sessiondata()["count"]=0 17 | 18 | def Event(inout self, data: PythonObject)->Action: 19 | return Action.Render 20 | 21 | def Render(inout self, props: PythonObject)->PythonObject: 22 | return Div( 23 | H1( 24 | Text(str(self.session.sessiondata()["count"])), 25 | ), 26 | self.session.Render[Counter]("counter"), 27 | self.session.Render[Counter]("counter2"), 28 | ) 29 | 30 | @value 31 | struct Counter: #(Component): 32 | var session: Session 33 | 34 | def __init__(inout self, session: Session): 35 | self.session = session 36 | 37 | def Event(inout self, data: PythonObject)->Action: 38 | if data["EventName"] == "increment": 39 | self.session.sessiondata()["count"] += 1 40 | return Action.Render 41 | 42 | def Render(inout self, props: PythonObject)->PythonObject: 43 | return Div( 44 | H1(Text(self.session.sessiondata()["count"])), 45 | Button( 46 | Text("Increment"), 47 | click = Event("increment"), 48 | ) 49 | ) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /mojoproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = ["rd4com <144297616+rd4com@users.noreply.github.com>"] 3 | channels = ["conda-forge", "https://conda.modular.com/max-nightly"] 4 | description = "Add a short description here" 5 | name = "mojo-new-web-framework" 6 | platforms = ["linux-64"] 7 | version = "0.1.0" 8 | 9 | [tasks] 10 | 11 | [dependencies] 12 | max = ">=24.6.0.dev2024102406,<25" 13 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 4px; 4 | } 5 | 6 | button { 7 | background-color: yellow; 8 | color: blue 9 | } 10 | 11 | .bg-green { 12 | background-color: lightgreen; 13 | } 14 | .bg-blue { 15 | background-color: lightblue; 16 | } -------------------------------------------------------------------------------- /ui.mojo: -------------------------------------------------------------------------------- 1 | from python import PythonObject, Python 2 | from pathlib import Path 3 | from collections import Dict 4 | from memory import UnsafePointer 5 | from ui_websocket import * 6 | from ui_html_elements import * 7 | 8 | @value 9 | struct Session: 10 | var local_instances: UnsafePointer[Dict[String, Instance]] 11 | var InstanceName: String 12 | var _appdata: UnsafePointer[PythonObject] 13 | var _sessiondata: UnsafePointer[PythonObject] 14 | 15 | fn appdata(inout self)->ref[self]PythonObject: 16 | """An app-wide dictionary usable by all websocket sessions.""" 17 | return self._appdata[] 18 | 19 | fn sessiondata(inout self)->ref[self]PythonObject: 20 | """A client-wide dictionary for all components in the current session.""" 21 | return self._sessiondata[] 22 | 23 | def Render[C: Component]( 24 | inout self, 25 | instance_name: String, 26 | **kwargs: PythonObject 27 | )->PythonObject: 28 | cpy = self 29 | cpy.InstanceName = instance_name 30 | var _ref = Pointer.address_of(cpy.local_instances[]) 31 | if instance_name not in _ref[]: 32 | #TODO: print create component: instance_name 33 | _ref[][instance_name] = Instance(C(cpy)^) 34 | try: 35 | _ref[][instance_name].rendered[] = True 36 | tmp = _ref[][instance_name] 37 | _ref2 = Pointer.address_of(tmp.ptr.bitcast[C]()[]) 38 | var tmp_props = Python.evaluate("{}") 39 | for k in kwargs: 40 | tmp_props[k[]] = kwargs[k[]] 41 | #TODO: print render component: instance_name 42 | tmp2 = _ref2[].Render(tmp_props) 43 | tmp2["data-instance_name"]= instance_name 44 | return tmp2 45 | except e: print(e) 46 | raise "error rendering:"+instance_name 47 | 48 | def _event( 49 | inout self, 50 | props: PythonObject 51 | )->Action: 52 | var _ref = Pointer.address_of(self.local_instances[]) 53 | if not props.__contains__("instance_name"): 54 | raise "Event: instance_name not in props" 55 | if str(props["instance_name"]) not in _ref[]: 56 | raise "Event: instance_name" 57 | instance = _ref[][str(props["instance_name"])] 58 | instance.rendered[] = True 59 | #TODO: print component event instance_name: props 60 | return instance._event(instance.ptr, props) 61 | 62 | def serve_app[ 63 | L: MutableOrigin, 64 | L2: ImmutableOrigin, 65 | //, 66 | T:Component 67 | ]( 68 | ref[L]appdata: PythonObject, 69 | ref[L2]exit_if_client_not_in: PythonObject 70 | ): 71 | """Start the HTTP server and serve the app. 72 | (`index.html`, `style.css`, `websockets`). 73 | 74 | Parameters: 75 | L: The lifetime of appdata. 76 | L2: The lifetime of exit_if_client_not_in. 77 | T: The main component. 78 | 79 | Args: 80 | appdata: An app-wide dictionary for all websocket sessions. 81 | exit_if_client_not_in: Exit the app if new connection host not in it. 82 | """ 83 | http_server = HttpServer() 84 | client_states = Dict[ 85 | String, Dict[String,Instance] 86 | ]() 87 | session_data = Dict[String,PythonObject]() 88 | todel = PythonObject([]) 89 | 90 | index_html = OpenOrRaise("index.html") 91 | style_css = OpenOrRaise("style.css") 92 | 93 | while True: 94 | new_websocket = http_server.handle_one_request( 95 | index_html, 96 | style_css, 97 | exit_if_client_not_in 98 | ) 99 | if new_websocket: 100 | print("new websocket", new_websocket.value()) 101 | client_states[new_websocket.value()] = Dict[String, Instance]() 102 | session_data[new_websocket.value()] = Python.dict() 103 | 104 | app = Session( 105 | UnsafePointer.address_of( 106 | client_states._find_ref(new_websocket.value()) 107 | ), 108 | "main_component", 109 | UnsafePointer.address_of(appdata), 110 | UnsafePointer.address_of(session_data[new_websocket.value()]) 111 | ) 112 | resp = IntoJson(app.Render[T]("main_component")) 113 | 114 | ws = http_server.websockets[new_websocket.value()] 115 | try: 116 | if not Python.import_module("select").select([], [ws], [], 0)[1]: 117 | raise WebSocketFrame.NotConnected 118 | WebSocketFrame.send_message(ws, str(resp)) 119 | except e: 120 | todel.append(new_websocket.value()) 121 | print(e) 122 | # print(resp) 123 | 124 | for w in http_server.websockets: 125 | ws = http_server.websockets[w[]] 126 | try: 127 | tmp = WebSocketFrame.read_message(ws) 128 | print("------\nevent", w[]) 129 | as_json = Python.import_module("json").loads(tmp) 130 | print("event value", as_json,"\n------") 131 | 132 | if not as_json.__contains__("instance_name"): 133 | raise "instance_name" 134 | 135 | for i_ in client_states[w[]]: 136 | client_states[w[]][i_[]].rendered[]=False 137 | 138 | app = Session( 139 | UnsafePointer.address_of(client_states._find_ref(w[])), 140 | str(as_json["instance_name"]), 141 | UnsafePointer.address_of(appdata), 142 | UnsafePointer.address_of(session_data[w[]]) 143 | ) 144 | res_from_event = app._event(as_json) 145 | if res_from_event.value == Action.Error: 146 | raise ("error: _event") 147 | 148 | #TODO: while unhandledevents: outer_component_event(e) 149 | 150 | resp = IntoJson(app.Render[T]("main_component")) 151 | WebSocketFrame.send_message(ws, str(resp)) 152 | 153 | #TODO: Move into a new function: 154 | instances_to_del = PythonObject([]) 155 | for instance_ in client_states[w[]]: 156 | if not client_states[w[]][instance_[]].rendered[]: 157 | instances_to_del.append(instance_[]) 158 | 159 | print("instances to del:", len(instances_to_del)) 160 | for instance_to_del in instances_to_del: 161 | tmp_instance_to_del = client_states._find_ref(w[]).pop( 162 | str(instance_to_del) 163 | ) 164 | tmp_instance_to_del.rendered.free() 165 | tmp_instance_to_del._del(tmp_instance_to_del.ptr) 166 | print("instance del: ", str(instance_to_del)) 167 | 168 | except e: 169 | if str(e) == WebSocketFrame.NoMessage: 170 | ... 171 | else: 172 | ws.close() 173 | todel.append(w[]) 174 | print(e) 175 | 176 | #TODO: Move into an instance: 177 | if todel: 178 | for w in todel: 179 | if str(w) in http_server.websockets: 180 | http_server.websockets.pop(str(w)) 181 | # TODO: check if closed 182 | if str(w) in session_data: 183 | _ = session_data.pop(str(w)) 184 | if str(w) in client_states: 185 | tmp_client_state = client_states.pop(str(w)) 186 | for s in tmp_client_state: 187 | tmp_instance = tmp_client_state[s[]] 188 | tmp_instance._del(tmp_instance.ptr) 189 | tmp_instance.rendered.free() 190 | _ = tmp_client_state^ 191 | todel = PythonObject([]) 192 | 193 | #FIXME: improve the PythonObject.__init__([http_socket,...websockets]) 194 | # (creating a list on each iteration is slow) 195 | # block until new http request or existing websocket event: 196 | tmp_slow_all_sockets = PythonObject([]) 197 | tmp_slow_all_sockets.append(http_server.socket) 198 | for s in http_server.websockets.values(): 199 | tmp_slow_all_sockets.append(s[]) 200 | print("connected:", len(http_server), len(client_states)) 201 | Python.import_module("select").select(tmp_slow_all_sockets, [], []) 202 | 203 | @value 204 | struct Instance: 205 | var ptr: UnsafePointer[NoneType] 206 | var _del: fn(UnsafePointer[NoneType])->None 207 | var _event: fn(ptr: UnsafePointer[NoneType], props: PythonObject)->Action 208 | var rendered: UnsafePointer[Bool] 209 | 210 | fn __init__[T:Component](inout self, owned arg: T): 211 | self.rendered = UnsafePointer[Bool].alloc(1) 212 | self.rendered[] = False 213 | var tmp_ptr = UnsafePointer[T].alloc(1) 214 | tmp_ptr.init_pointee_move(arg^) 215 | self.ptr = tmp_ptr.bitcast[NoneType]() 216 | fn tmp_del(ptr: UnsafePointer[NoneType]): 217 | ptr.bitcast[T]().destroy_pointee() 218 | ptr.free() 219 | self._del = tmp_del 220 | fn tmp_event(ptr: UnsafePointer[NoneType], props: PythonObject)->Action: 221 | _ref = Pointer.address_of(ptr.bitcast[T]()[]) 222 | try: 223 | return _ref[].Event(props) 224 | except e: print(e) 225 | return Action.Error 226 | self._event = tmp_event 227 | 228 | fn __getitem__[T:Component](inout self)->ref[__origin_of(self)]T: 229 | return self.ptr.bitcast[T]()[] 230 | 231 | trait Component(CollectionElement): 232 | def __init__(inout self, session: Session): ... 233 | def Render(inout self, props: PythonObject)->PythonObject: 234 | ... 235 | def Event(inout self, data: PythonObject)->Action: 236 | ... 237 | 238 | @value 239 | struct Action: 240 | alias Render = 1 # update ui on current websocket 241 | alias Error = 2 242 | alias RenderSessions = 3 #TODO: update ui on all websockets 243 | var value: Int 244 | 245 | def IntoJson(owned dom_tree: PythonObject) -> PythonObject: 246 | resp_py = Python.evaluate("{}") 247 | resp_py["event_type"] = "render" 248 | resp_py["dom_tree"] = dom_tree 249 | return Python.import_module("json").dumps(resp_py) 250 | 251 | def AppendElement(el: PythonObject,*, to:PythonObject): 252 | to["element_nested"].append(el) 253 | 254 | def Event( 255 | event_name: String, 256 | **kwargs: PythonObject 257 | )->PythonObject: 258 | 259 | var tmp_py = Python.evaluate("{}") 260 | tmp_py["attribute_type"] = "event" 261 | tmp_py["EventName"] = event_name 262 | # default is event go to current component 263 | # TODO: if InstanceName in kwargs: js event there (example: "main_component") 264 | # (changes in index.html render_dom) 265 | for k in kwargs: 266 | tmp_py[k[]] = kwargs[k[]] 267 | return tmp_py 268 | 269 | def OpenOrRaise(arg: String)->String: 270 | if (not Path(arg).exists()): 271 | raise "index.html and style.css" 272 | 273 | tmp_handler = open(arg, "rb") 274 | tmp_result = tmp_handler.read() 275 | tmp_handler.close() 276 | return tmp_result 277 | -------------------------------------------------------------------------------- /ui_html_elements.mojo: -------------------------------------------------------------------------------- 1 | from python import PythonObject, Python 2 | 3 | fn CreateHtmlElement[name: String]()->fn(*args: PythonObject, **kwargs: PythonObject) raises ->PythonObject: 4 | """Makes a new html tag usable in `Render`. 5 | Example: 6 | ```mojo 7 | # my_elements.mojo 8 | alias H3 = CreateHtmlElement["h3"]() 9 | # Somewhere else: 10 | from my_elements import H3 11 | Div(H3(Text("Hello world"))) 12 | ``` 13 | (`H3` in the example is defined once and then can be reused). 14 | """ 15 | fn tmp(*args: PythonObject, **kwargs: PythonObject) raises ->PythonObject: 16 | var tmp_return = Python.evaluate("{}") 17 | tmp_return["element_type"] = name 18 | tmp_return["element_nested"] = [] 19 | for arg in args: 20 | if arg[] is not None: 21 | tmp_return["element_nested"].append(arg[]) 22 | for k in kwargs: 23 | tmp_return[k[]] = kwargs[k[]] 24 | return tmp_return 25 | return tmp 26 | 27 | alias Div = CreateHtmlElement["div"]() 28 | alias Button = CreateHtmlElement["button"]() 29 | alias H1 = CreateHtmlElement["h1"]() 30 | alias H2 = CreateHtmlElement["h2"]() 31 | alias Text = CreateHtmlElement["text"]() 32 | alias Input = CreateHtmlElement["input"]() 33 | alias Br = CreateHtmlElement["br"]() 34 | alias HtmlSpan = CreateHtmlElement["span"]() 35 | 36 | # def main(): 37 | # var x = Div( 38 | # Input(value="hello world"), 39 | # Button(Text("Add")) 40 | # ) 41 | # print(x) 42 | -------------------------------------------------------------------------------- /ui_websocket.mojo: -------------------------------------------------------------------------------- 1 | from python import PythonObject, Python 2 | from collections import OptionalReg, Dict, Optional 3 | from sys.param_env import is_defined 4 | from time import sleep 5 | 6 | struct HttpServer: 7 | alias host="127.0.0.1" 8 | alias port=8080 9 | 10 | var socket: PythonObject 11 | var websockets: Dict[String, PythonObject] 12 | 13 | fn __len__(self)->Int: return len(self.websockets) 14 | fn __init__(inout self): 15 | self.socket = PythonObject(None) 16 | self.websockets = __type_of(self.websockets)() 17 | try: 18 | py_socket = Python.import_module("socket") 19 | self.socket = py_socket.socket(py_socket.AF_INET, py_socket.SOCK_STREAM) 20 | self.socket.setsockopt(py_socket.SOL_SOCKET, py_socket.SO_REUSEADDR, 1) 21 | self.socket.bind((Self.host, Self.port)) 22 | self.socket.listen(1) 23 | print("http://"+str(Self.host)+":"+str(Self.port)) 24 | except e: 25 | self.socket = PythonObject(None) 26 | print(e) 27 | 28 | def handle_one_request( 29 | inout self, 30 | ref[_]index_html:String, 31 | ref[_]style_css:String, 32 | ref[_]exit_if_client_not_in: PythonObject 33 | )->Optional[String]: 34 | try: 35 | if not Python.import_module("select").select([self.socket], [], [], 0)[0]: 36 | raise "No request" 37 | py_base64 = Python.import_module("base64") 38 | py_sha1 = Python.import_module("hashlib").sha1 39 | client = PythonObject(None) 40 | client = self.socket.accept() 41 | 42 | if not exit_if_client_not_in.__contains__(client[1][0]): 43 | print("Exit, request from: "+str(client[1][0])) 44 | raise "exit_app" 45 | 46 | if not Python.import_module("select").select([client[0]], [], [], 0)[0]: 47 | raise "No request" 48 | request = client[0].recv(1024).decode() 49 | request_header = Dict[String,String]() 50 | 51 | var end_header = int(request.find("\r\n\r\n")) 52 | if end_header == -1: 53 | raise "end_header == -1, no \\r\\n\\r\\n" 54 | var request_split = str(request)[:end_header].split("\r\n") 55 | if len(request_split) == 0: 56 | raise "error: len(request_split) == 0" 57 | if request_split[0] != "GET / HTTP/1.1": 58 | if request_split[0] == "GET /style.css HTTP/1.1": 59 | var tmp_response = String("HTTP/1.1 200 OK\r\n") 60 | tmp_response+= "Content-Type: text/css; charset=UTF-8\r\n" 61 | tmp_response+= "\r\n" 62 | tmp_response += style_css 63 | tmp_response+= "\r\n" 64 | if not Python.import_module("select").select([], [client[0]], [], 0)[1]: 65 | client[0].close() 66 | raise "No request" 67 | client[0].send(PythonObject(tmp_response).encode("utf-8")) 68 | client[0].close() 69 | return 70 | if not request_split[0].startswith("GET /api/"): 71 | var tmp_response = String("HTTP/1.1 404 Not Found\r\n") 72 | tmp_response+= "\r\n" 73 | if not Python.import_module("select").select([], [client[0]], [], 0)[1]: 74 | client[0].close() 75 | raise "No request" 76 | client[0].send(PythonObject(tmp_response).encode("utf-8")) 77 | client[0].close() 78 | print(request_split[0]) 79 | raise "request_split[0] not GET / HTTP/1.1" 80 | url = request_split.pop(0).split(" ")[1] 81 | print(url) 82 | if len(request_split) == 0: 83 | raise "error: no headers" 84 | 85 | for e in request_split: 86 | var header_pos = e[].find(":") 87 | if header_pos == -1: 88 | raise "header_pos == -1" 89 | if len(e[]) == header_pos+2: 90 | raise "len(e[]) == header_pos+2" 91 | var k = e[][:header_pos] 92 | var v = e[][header_pos+2:] 93 | request_header[k^]=v^ 94 | 95 | for h in request_header: 96 | print(h[], request_header[h[]]) 97 | 98 | if "Upgrade" not in request_header: 99 | var tmp_response = String("HTTP/1.1 200 OK\r\n") 100 | tmp_response+= "Content-Type: text/html; charset=UTF-8\r\n" 101 | tmp_response+= "\r\n" 102 | tmp_response += index_html 103 | tmp_response+= "\r\n" 104 | if not Python.import_module("select").select([], [client[0]], [], 0)[1]: 105 | client[0].close() 106 | raise "No request" 107 | client[0].send(PythonObject(tmp_response).encode("utf-8")) 108 | client[0].close() 109 | return 110 | 111 | if request_header["Upgrade"] != "websocket": 112 | raise "Not an upgrade to websocket" 113 | 114 | if "Sec-WebSocket-Key" not in request_header: 115 | raise "No Sec-WebSocket-Key for upgrading to websocket" 116 | 117 | if str(request_header["Sec-WebSocket-Key"]) in self.websockets: 118 | raise "Already connected" 119 | 120 | var accept = PythonObject(request_header["Sec-WebSocket-Key"]) 121 | accept += PythonObject(WebSocketFrame.MAGIC_CONSTANT) 122 | accept = accept.encode() 123 | accept = py_base64.b64encode(py_sha1(accept).digest()) 124 | 125 | var response = String("HTTP/1.1 101 Switching Protocols\r\n") 126 | response += "Upgrade: websocket\r\n" 127 | response += "Connection: Upgrade\r\n" 128 | response += "Sec-WebSocket-Accept: " 129 | response += str(accept.decode("utf-8")) 130 | response += String("\r\n\r\n") 131 | 132 | print(response) 133 | 134 | if not Python.import_module("select").select([], [client[0]], [], 0)[1]: 135 | client[0].close() 136 | raise "No request" 137 | client[0].send(PythonObject(response).encode()) 138 | self.websockets[str(request_header["Sec-WebSocket-Key"])]=client[0] 139 | return str(request_header["Sec-WebSocket-Key"]) 140 | 141 | except e: 142 | print(e) 143 | if str(e) == "exit_app": raise e 144 | return None 145 | 146 | struct WebSocketFrame: 147 | alias storage_type = List[UInt8] 148 | 149 | # constants 150 | alias byte_0_text: UInt8 = 1 151 | alias byte_0_fin: UInt8 = 128 152 | 153 | alias byte_1_uint8: UInt8 = 125 154 | alias byte_1_uint16: UInt8 = 126 155 | alias byte_1_uint64: UInt8 = 127 156 | alias byte_1_mask: UInt8 = 128 157 | 158 | alias MAGIC_CONSTANT = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 159 | 160 | alias NotConnected = "NotConnected" 161 | alias NoMessage = "NoMessage" 162 | alias NoMask = "NoMask" 163 | alias NotSupported = "NotSupported" 164 | 165 | @staticmethod 166 | fn read_message(ws: PythonObject) raises -> String: 167 | """ 168 | If raises any error: 169 | 1. Need to close the connection. 170 | 2. Remove the websocket from the List. 171 | """ 172 | data = Self.storage_type() 173 | select = Python.import_module("select").select 174 | if not select([ws],[],[], 0)[0]: 175 | raise Self.NoMessage 176 | data.append(Self.read_byte(ws)) 177 | if not data[0]&Self.byte_0_fin: 178 | raise Self.NotSupported 179 | if not data[0]&Self.byte_0_text: 180 | raise Self.NotSupported 181 | 182 | data.append(Self.read_byte(ws)) 183 | size = data[1].cast[DType.uint64]() 184 | if not size.cast[DType.uint8]()&Self.byte_1_mask.cast[DType.uint8](): 185 | raise Self.NoMask 186 | else: 187 | size^=Self.byte_1_mask.cast[DType.uint64]() 188 | if size <= Self.byte_1_uint8.cast[DType.uint64](): 189 | ... 190 | elif size == Self.byte_1_uint16.cast[DType.uint64](): 191 | size = Self.read_byte(ws).cast[DType.uint64]() << 8 192 | size |= Self.read_byte(ws).cast[DType.uint64]() 193 | elif size == Self.byte_1_uint64.cast[DType.uint64](): 194 | size = 0 195 | for i in range(0,8): 196 | pos = (7-i)*8 197 | size |= Self.read_byte(ws).cast[DType.uint64]()<