├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Neutron ├── __init__.py └── elements.py ├── README.md ├── TEMPLATE ├── def.css ├── main.py └── render.html ├── TODO.md ├── examples └── todo-app │ ├── def.css │ ├── main.py │ └── render.html └── setup.py /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ianbaldelli@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | Thanks for considering contributing! Below are guidelines for contributing and how to get started. 5 | 6 | ## Getting Started 7 | 8 | A good way to contribute is to work on a to-do. [to-dos](https://github.com/IanTerzo/Neutron/blob/main/TODO.md). 9 | 10 | Another way to get started is to fix issues, you can find new issues in the issues tab. 11 | 12 | 13 | ## Pull Requests And Commits 14 | 15 | If a small fix or update is needed feel free to do a pull request, but keep in mind the following: 16 | 17 | - Have a good and explanatory name for your pull request, e.g. no "Fixed code" or "Added feature". 18 | 19 | - Always test your code before pushing. (the module must work if it is cloned from source) 20 | 21 | If you plan on implementing a new feature or changing a lot of code, please open an issue first to discuss it. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ian Baldelli 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 | -------------------------------------------------------------------------------- /Neutron/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt6.QtCore import QUrl 3 | from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget 4 | from PyQt6.QtWebEngineWidgets import QWebEngineView 5 | from PyQt6.QtCore import QThread 6 | from bs4 import BeautifulSoup 7 | import json 8 | from . import elements 9 | import sys 10 | import os 11 | import logging 12 | import asyncio 13 | import threading 14 | import websockets 15 | from websockets.exceptions import ConnectionClosedOK 16 | from http.server import SimpleHTTPRequestHandler, HTTPServer 17 | 18 | global api_functions 19 | api_functions = {} 20 | 21 | class ListenerHTTPServer(SimpleHTTPRequestHandler): 22 | def log_message(self, format, *args): 23 | # Disable log messages 24 | pass 25 | 26 | def _set_headers(self): 27 | self.send_header('Access-Control-Allow-Origin', '*') 28 | self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') 29 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 30 | 31 | def do_GET(self): 32 | super().do_GET() 33 | 34 | def do_OPTIONS(self): 35 | self.send_response(200) 36 | self._set_headers() 37 | self.end_headers() 38 | 39 | def do_POST(self): 40 | content_length = int(self.headers['Content-Length']) 41 | post_data = self.rfile.read(content_length) 42 | post_data = post_data.decode('utf-8') 43 | data = json.loads(post_data) 44 | 45 | if data["type"] == "bridge": 46 | api_functions[data['function']](*data['parameters']) 47 | self.send_response(200) 48 | self._set_headers() 49 | self.end_headers() 50 | else: 51 | self.send_response(500) 52 | self._set_headers() 53 | self.end_headers() 54 | 55 | 56 | def start_listener_server(port): 57 | httpd = HTTPServer(('', port), ListenerHTTPServer) 58 | httpd.serve_forever() 59 | 60 | class WebSocketSendServer(QThread): 61 | client = None 62 | 63 | def __init__(self, port): 64 | super().__init__() 65 | self.pending_response = None 66 | self.port = port 67 | 68 | async def handler(self, websocket): 69 | self.client = websocket 70 | 71 | try: 72 | while True: 73 | try: 74 | message = await websocket.recv() 75 | if self.pending_response: 76 | self.pending_response.set_result(message) 77 | 78 | except ConnectionClosedOK: 79 | print(f"Client {websocket} disconnected gracefully.") 80 | break 81 | except websockets.exceptions.ConnectionClosed as e: 82 | print(f"Connection closed unexpectedly: {e}") 83 | break 84 | finally: 85 | self.client = None 86 | 87 | async def start_server(self): 88 | async with websockets.serve(self.handler, "localhost", self.port): # Optional max size for messages 89 | await asyncio.Future() # Run forever 90 | 91 | async def send_and_wait(self, message: str) -> str: 92 | if not self.client: 93 | raise RuntimeError("No client connected") 94 | 95 | # Create a future to await the response 96 | self.pending_response = asyncio.get_event_loop().create_future() 97 | 98 | await self.client.send(message) 99 | 100 | # Wait for the future to be done 101 | while True: 102 | if self.pending_response.done(): 103 | result = self.pending_response.result() 104 | self.pending_response = None 105 | return result 106 | 107 | 108 | def run(self): 109 | asyncio.run(self.start_server()) 110 | 111 | def event(function): 112 | if callable(function): 113 | if not str(function) in api_functions: 114 | api_functions.update({str(function): function}) 115 | return f"bridge('{str(function)}')" 116 | else: 117 | raise TypeError("Event attribute is not a function!") 118 | 119 | class Window: 120 | def __init__(self, title, css=None, position=(300, 300), size=(900, 600), listener_port=22943, sender_port=22944): 121 | self.title = title 122 | self.css = css 123 | self.position = position 124 | self.size = size 125 | self.running = False 126 | 127 | self.listener_port = listener_port 128 | self.sender_port = sender_port 129 | 130 | self.view = None 131 | self.qt_window = None; 132 | self.html = "" 133 | self.loop = asyncio.new_event_loop() 134 | 135 | 136 | def run_javascript(self, javascript): 137 | if not self.running: 138 | raise RuntimeError(""""Window.run_javascript()" can only be called while the window is running!""") 139 | 140 | message = json.dumps({"type": "eval", "javascript": javascript}) 141 | response = "" 142 | async def send_and_wait(): 143 | nonlocal response 144 | try: 145 | response = await self.websocket_server.send_and_wait(message) 146 | except TimeoutError: 147 | raise TimeoutError("No response received within the timeout period.") 148 | 149 | asyncio.run(send_and_wait()) 150 | return response 151 | 152 | def display(self, file=None, html=None, pyfunctions=None, encoding="utf-8"): 153 | 154 | if file: 155 | # Check if program is being run as an exe 156 | if getattr(sys, 'frozen', False): 157 | content = str(open(os.path.join(sys._MEIPASS, file), "r", encoding=encoding).read()) 158 | else: 159 | content = str(open(file, "r", encoding=encoding).read()) 160 | elif html: 161 | content = str(html) # Make sure it is a string (could be beutifulsoup element) 162 | 163 | soup = BeautifulSoup(content, "html.parser") 164 | 165 | bridge_html = """ 166 | " 219 | 220 | # add the css 221 | 222 | if self.css: 223 | # Check if program is being run as an exe 224 | if getattr(sys, 'frozen', False): 225 | css = str(open(os.path.join(sys._MEIPASS, self.css), "r", encoding=encoding).read()) 226 | else: 227 | css = str(open(self.css, "r", encoding=encoding).read()) 228 | 229 | bridge_html += f"" 230 | 231 | # Append the new HTML to the section 232 | 233 | if not soup.head: 234 | head = soup.new_tag('head') 235 | soup.insert(0, head) 236 | 237 | soup.head.append(BeautifulSoup(bridge_html, 'html.parser')) 238 | 239 | soupContent = soup.find_all() 240 | 241 | for element in soupContent: 242 | elements.createNeutronId(element) 243 | 244 | # Link all filereads to the http server 245 | base = soup.new_tag('base') 246 | base['href'] = f"http://localhost:{self.listener_port}/" 247 | 248 | self.html = str(soup) 249 | 250 | 251 | def show(self, after=None): 252 | title = self.title 253 | size = self.size 254 | 255 | # Start bridge server 256 | server_thread = threading.Thread(target=start_listener_server, args=(self.listener_port,)) 257 | server_thread.daemon = True # Ensures the thread will exit when the main program exits 258 | server_thread.start() 259 | 260 | # Start websocket server 261 | self.websocket_server = WebSocketSendServer(self.sender_port) 262 | self.websocket_server.start() 263 | 264 | # Create window 265 | app = QApplication(sys.argv) 266 | 267 | window = QMainWindow() 268 | window.setWindowTitle(title) 269 | window.setGeometry(100, 100, size[0], size[1]) 270 | 271 | central_widget = QWidget() 272 | layout = QVBoxLayout(central_widget) 273 | layout.setContentsMargins(0, 0, 0, 0) 274 | layout.setSpacing(0) 275 | window.setCentralWidget(central_widget) 276 | 277 | view = QWebEngineView() 278 | view.setHtml( self.html, QUrl("qrc:///")) 279 | 280 | layout.addWidget(view) 281 | 282 | self.view = view 283 | self.qt_window = window; 284 | 285 | self.running = True 286 | self.qt_window.show() 287 | sys.exit(app.exec()) 288 | 289 | 290 | def close(self): 291 | self.qt_window.close() 292 | 293 | def appendChild(self, html_element): 294 | if self.running: 295 | self.run_javascript(f"""document.body.innerHTML += '{str(html_element)}';""") 296 | html_element.domAttatched = True; 297 | return html_element 298 | else: 299 | raise RuntimeError(""""Window.appendChild()" can only be called while the window is running!""") 300 | 301 | def append(self, html): 302 | if self.running: 303 | self.run_javascript(f"""document.body.innerHTML += '{str(html)}';""") 304 | else: 305 | raise RuntimeError(""""Window.append()" can only be called while the window is running!""") 306 | 307 | def getElementById(self, id): 308 | if self.running: 309 | elementNeutronID = str(self.run_javascript(f""" '' + document.getElementById("{id}").className;""")) 310 | 311 | NeutronID = None 312 | 313 | for classname in elementNeutronID.split(' '): 314 | if "NeutronID_" in classname: 315 | NeutronID = classname 316 | break 317 | 318 | if NeutronID: 319 | return elements.HTMLelement(self, NeutronID, None, True) 320 | else: 321 | logging.warning(f'HTMLelement with id "{id}" was not found!') 322 | return None 323 | else: 324 | soup = BeautifulSoup(self.html, "html.parser") 325 | # check if element exists 326 | element = soup.select(f'#{id}') 327 | if element != []: 328 | NeutronID = element[0].get('class')[0] 329 | return elements.HTMLelement(self, NeutronID, element, True) 330 | else: 331 | logging.warning(f'HTMLelement with id "{id}" was not found!') 332 | return None 333 | 334 | def getElementsByTagName(self, name): 335 | if self.running: 336 | ElementsNeutronID = self.run_javascript("var elementsNeutronID = []; Array.from(document.getElementsByTagName('" + name + "')).forEach(function(item) { elementsNeutronID.push(item.className) }); '' + elementsNeutronID;") 337 | return [elements.HTMLelement(self, NeutronID.split(' ')[0], None, True) for NeutronID in ElementsNeutronID.split(",")] 338 | else: 339 | soup = BeautifulSoup(self.html, "html.parser") 340 | return [elements.HTMLelement(self, element.get('class')[0], element, True) for element in soup.find_all(name)] 341 | 342 | def createElement(self, tag): 343 | soup = BeautifulSoup(self.html, features="html.parser") 344 | elem = soup.new_tag(tag) 345 | 346 | NeutronID = elements.createNeutronId(elem) 347 | 348 | soup.append(elem) 349 | 350 | return elements.HTMLelement(self, NeutronID, elem, False) 351 | -------------------------------------------------------------------------------- /Neutron/elements.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import uuid 3 | 4 | """ 5 | 6 | Neutron passes HTML elements beetween JavaScript and Python using a custom ID system "NeutronID". 7 | The NeutronID is located in an elements classlist, and is generated when display() is first called. 8 | Using this system Neutron can share HTML elements that do not have a regular HTML id, 9 | for example HTML elements returned by getElementsByTagName(). 10 | Every NeutronID is an UUID. 11 | 12 | """ 13 | 14 | def createNeutronId(tag): 15 | NeutronID = f"NeutronID_{uuid.uuid1()}" 16 | 17 | element_classes = tag.get('class') 18 | if element_classes is not None: 19 | tag['class'] = NeutronID + " " + " ".join(element_classes) 20 | else: 21 | tag['class'] = NeutronID 22 | 23 | return NeutronID 24 | 25 | 26 | # Web componets # 27 | 28 | # TODO: Add event Attributes 29 | HTMLelementAttributes = ['value', 'accept', 'action', 'align', 'allow', 'alt', 'autocapitalize', 'autocomplete', 30 | 'autofocus', 'autoplay', 'background', 'bgcolor', 'border', 'buffered', 'capture', 'challenge', 31 | 'charset', 'checked', 'cite', 'id', 'className', 'code', 'codebase', 'color', 'cols', 'colspan', 32 | 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'crossorigin', 'csp ', 33 | 'data', 'datetime', 'decoding', 'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 34 | 'draggable', 'enctype', 'enterkeyhint', 'for_', 'form', 'formaction', 'formenctype', 35 | 'formmethod', 'formnovalidate', 'formtarget', 'headers', 'height', 'hidden', 'high', 'href', 36 | 'hreflang', 'http_equiv', 'icon', 'importance', 'integrity', 'intrinsicsize ', 'inputmode', 37 | 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang', 'language ', 'loading ', 'list', 38 | 'loop', 'low', 'manifest', 'max', 'maxlength', 'minlength', 'media', 'method', 'min', 39 | 'multiple', 'muted', 'name', 'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 40 | 'poster', 'preload', 'radiogroup', 'readonly', 'referrerpolicy', 'rel', 'required', 'reversed', 41 | 'rows', 'rowspan', 'sandbox', 'scope', 'scoped', 'selected', 'shape', 'size', 'sizes', 'slot', 42 | 'span', 'spellcheck', 'src', 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'style', 43 | 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'width', 'wrap', 44 | 'onblur', 'onchange', 'oncontextmenu', 'onfocus', 'oninput', 'oninvalid', 'onreset', 'onsearch', 45 | 'onselect', 'onsubmit', 'onkeydown', 'onkeypress', 'onkeyup', 'onclick', 'ondblclick', 'onmousedown', 46 | 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onwheel', 'ondrag', 'ondragend', 47 | 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onscroll', 'oncopy', 'oncut', 48 | 'onpaste', 'onabort', 'oncanplay', 'oncanplaythrough', 'oncuechange', 'ondurationchange', 'onemptied', 'onended', 49 | 'onerror', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 50 | 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 51 | 'onwaiting', 'ontoggle',] 52 | 53 | 54 | class HTMLelement: 55 | def __init__(self, window, NeutronID, element_soup, domAttatched): 56 | self.window = window 57 | self.element_soup = element_soup # element_soup is None if element is aquired while window is running 58 | self.NeutronID = NeutronID 59 | self.domAttatched = domAttatched; 60 | 61 | if "NeutronID_" not in self.NeutronID: 62 | raise ValueError("NeutronID is invalid") 63 | 64 | def __str__(self): 65 | # element_soup will be set to None if class is called on runtime 66 | if self.window.running and self.domAttatched: 67 | return str(self.window.run_javascript(f""" '' + document.getElementsByClassName("{self.NeutronID}")[0].outerHTML;""")) 68 | else: 69 | return str(self.element_soup) 70 | 71 | 72 | def addEventListener(self, eventHandler, NeutronEvent): 73 | if self.window.running and self.domAttatched: 74 | self.window.run_javascript( 75 | f""" '' + document.getElementsByClassName("{self.NeutronID}")[0].addEventListener("{eventHandler}", {NeutronEvent});"""); 76 | else: 77 | eventHandler = "on" + eventHandler 78 | soup = BeautifulSoup(self.window.html, features="html.parser") 79 | # Create a new attribute for the event (i.e onclick) 80 | soup.find_all(class_=self.NeutronID)[0][eventHandler] = NeutronEvent 81 | 82 | self.window.html = str(soup) 83 | 84 | def appendChild(self, html_element): 85 | if self.window.running and self.domAttatched: 86 | soup = BeautifulSoup(str(html_element), features="html.parser") 87 | bodyContent = soup.find_all() 88 | 89 | for element in bodyContent: 90 | createNeutronId(element) 91 | 92 | self.window.run_javascript(f"""document.getElementsByClassName("{self.NeutronID}")[0].innerHTML += '{str(soup)}';""") 93 | return html_element 94 | else: 95 | self.element_soup.append(BeautifulSoup(str(html_element), 'html.parser')) 96 | 97 | def append(self, html): 98 | if self.window.running and self.domAttatched: 99 | soup = BeautifulSoup(html, features="html.parser") 100 | bodyContent = soup.find_all() 101 | 102 | for element in bodyContent: 103 | createNeutronId(element) 104 | 105 | self.window.run_javascript(f"""document.getElementsByClassName("{self.NeutronID}")[0].innerHTML += '{str(soup)}';""") 106 | else: 107 | self.element_soup.append(BeautifulSoup(html, 'html.parser')) 108 | 109 | # TODO 110 | def remove(self): 111 | if not self.window.running or not self.domAttatched: 112 | raise RuntimeError(""""remove" can only be called while the window is running and element is present on DOM!""") 113 | 114 | self.window.run_javascript(f"""document.getElementsByClassName("{self.NeutronID}")[0].remove();""") 115 | 116 | # Does not work with global event handlers! 117 | def getAttribute(self, attribute): 118 | if self.window.running and self.domAttatched: 119 | return str(self.window.run_javascript(f""" '' + document.getElementsByClassName("{self.NeutronID}")[0].{attribute};""")) 120 | else: 121 | return self.element_soup.attrs 122 | 123 | # Does not work with global event handlers! 124 | def setAttribute(self, attribute, value): 125 | if self.window.running and self.domAttatched: 126 | self.window.run_javascript( 127 | f""" '' + document.getElementsByClassName("{self.NeutronID}")[0].setAttribute("{attribute}", "{value}");""") 128 | else: 129 | self.element_soup[attribute] = value 130 | 131 | def removeAttribute(self, attribute): 132 | if self.window.running and self.domAttatched: 133 | self.window.run_javascript( 134 | f""" '' + document.getElementsByClassName("{self.NeutronID}")[0].removeAttribute("{attribute}");""" 135 | ) 136 | else: 137 | del self.element_soup[attribute] 138 | 139 | 140 | def innerHTML_get(self): 141 | if self.window.running and self.domAttatched: 142 | return str(self.window.run_javascript(f""" '' + document.getElementsByClassName("{self.NeutronID}")[0].innerHTML;""")) 143 | else: 144 | return self.element_soup.decode_contents() 145 | 146 | def innerHTML_set(self, value): 147 | if self.window.running and self.domAttatched: 148 | self.window.run_javascript(f"""document.getElementsByClassName("{self.NeutronID}")[0].innerHTML = "{value}";""") 149 | else: 150 | self.element_soup.clear() 151 | self.element_soup.append(BeautifulSoup(value, 'html.parser')) 152 | 153 | innerHTML = property(innerHTML_get, innerHTML_set) 154 | 155 | for attribute in HTMLelementAttributes: 156 | exec( 157 | f"{attribute} = property(lambda self: self.getAttribute('{attribute}'), lambda self, val: self.setAttribute('{attribute}', val))") 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://i.ibb.co/wC9LxYw/Neutron-nobg.png) 2 | 3 | Neutron is ment to be an intuative way of creating GUI applications using HTML, CSS and Python. It is built on top of PyQt6 and uses the QtWebEngine. 4 | 5 | You can get started contributing via [CONTRIBUTING.md](https://github.com/IanTerzo/Neutron/blob/main/CONTRIBUTING.md). 6 | 7 | ## Installation 8 | 9 | ``` 10 | pip install neutron-web 11 | ``` 12 | 13 | ## Example 14 | For a template project see [TEMPLATE](https://github.com/IanTerzo/Neutron/tree/main/TEMPLATE) or see the [to-do app](https://github.com/IanTerzo/Neutron/tree/main/examples/todo-app). 15 | 16 | ## Usage 17 | The Neutron api is designed to be very similar to the JavaScript DOM api. You can imagine the `Window` class as the `document` object in JavaScript (altough it is still missing a lot of features). Ideally you should be able to use Neutron as you would use the JavaScript DOM api, altough there are some differences. 18 | 19 | ### Neutron features 20 | 21 | ```python 22 | Neutron.event(function : Callable)) -> str 23 | ``` 24 | Use this function when passing a python function to an event listener or javascript method that requires a callable as parameter. Return the new javascript "bridge" function as str. 25 | 26 | ```python 27 | Window(title: str, css: str, position: Tuple[int, int], size: Tuple[int, int]) -> Window 28 | ``` 29 | Create a window. 30 | 31 | ```python 32 | Window.run_javascript(javascript: str) -> str 33 | ``` 34 | Evaluate JavaScript code. 35 | 36 | ```python 37 | Window.display(file: str, html: str, pyfunctions: List[Callable], encoding: str) -> None 38 | ``` 39 | Used to parse your html code. You run it before showing the window. It takes a path to your htlm file or html code (if file is not provided), a list of python functions and an encoding. The encoding is the encoding of your html file. The python functions are the functions you want to able to directly call from your html file. See the to-do app example. 40 | 41 | ```python 42 | Window.show() -> None / Window.close() -> None 43 | ``` 44 | Show and close the window. 45 | 46 | 47 | ## Building your project 48 | 49 | To build a Neutron project you first need pyinstaller, install pyinstaller through pip: `pip install pyinstaller`. Then run the script below in your command prompt/terminal. You can also use other programs to build your project such as py2exe if you prefer. 50 | 51 | > **Note:** If you are on linux use ":" instead of ";" 52 | ``` 53 | pyinstaller YOUR_PYTHON_FILE.py --noconsole --onefile --add-data="YOUR_HTML_FILE.html;." --add-data="YOUR_CSS_FILE.css;." 54 | ``` 55 | 56 | You don't need to use `--add-data` if your project doesn't have a CSS or HTML file 57 | -------------------------------------------------------------------------------- /TEMPLATE/def.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | 4 | } 5 | -------------------------------------------------------------------------------- /TEMPLATE/main.py: -------------------------------------------------------------------------------- 1 | import Neutron 2 | 3 | win = Neutron.Window("Example", size=(600, 400), css="def.css") 4 | win.display(file="render.html") 5 | 6 | 7 | def onClick(): 8 | win.getElementById("title").innerHTML = "Hello:" + win.getElementById("inputName").value 9 | 10 | 11 | win.getElementById("submitName").addEventListener("click", Neutron.event(onClick)) 12 | 13 | win.show() 14 | -------------------------------------------------------------------------------- /TEMPLATE/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Hello:

8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | # Neutron to-dos list 3 | 4 | ### Javascript compatibility 5 | 6 | - [ ] Finish adding all DOM methods 7 | 8 | (Inside the window class in `__init__.py`, DOM method: i.e getElementById) 9 | 10 | - [ ] Finish adding all HTMLelement attributes 11 | 12 | (In `elements.py`, for example HTMLelement.style attribute) 13 | 14 | 15 | ### Other 16 | 17 | - [ ] Improve the HTMLelement attribute system to avoid looping trough all HTMLelementAttributes at creation 18 | 19 | (In `elements.py`) 20 | 21 | ### COMPLETED TO-DOS 22 | - [x] JavaScript-Python bridge 23 | -------------------------------------------------------------------------------- /examples/todo-app/def.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Roboto; 3 | background: -webkit-linear-gradient(90deg, #01b58b 10%, #eaecc6 90%); 4 | } 5 | 6 | ul { 7 | list-style: none; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | h1 { 13 | background: #01b58b; 14 | color: white; 15 | margin: 0; 16 | padding: 10px 20px; 17 | text-transform: uppercase; 18 | font-size: 24px; 19 | font-weight: normal; 20 | } 21 | 22 | .fa-plus { 23 | float: right; 24 | } 25 | 26 | li { 27 | background: #fff; 28 | height: 40px; 29 | line-height: 40px; 30 | color: #666; 31 | } 32 | 33 | li:nth-child(2n) { 34 | background: #f7f7f7; 35 | } 36 | 37 | span { 38 | font-family: Arial, Helvetica, sans-serif; 39 | background: #e74c3c; 40 | height: 40px; 41 | margin-right: 20px; 42 | text-align: center; 43 | color: white; 44 | width: 0; 45 | display: inline-block; 46 | transition: 0.2s linear; 47 | opacity: 0; 48 | margin-bottom: -14px; 49 | } 50 | 51 | li:hover span { 52 | width: 40px; 53 | opacity: 1; 54 | } 55 | 56 | input { 57 | font-size: 18px; 58 | color: #2980b9; 59 | background-color: #f7f7f7; 60 | width: 100%; 61 | padding: 13px 13px 13px 20px; 62 | box-sizing: border-box; 63 | border: 3px solid rgba(0, 0, 0, 0); 64 | } 65 | 66 | input:focus { 67 | background: #fff; 68 | border: 3px solid #2980b9; 69 | outline: none; 70 | } 71 | 72 | #container { 73 | width: 360px; 74 | margin: 100px auto; 75 | background: #f7f7f7; 76 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.1); 77 | } 78 | 79 | .completed { 80 | color: gray; 81 | text-decoration: line-through; 82 | } 83 | -------------------------------------------------------------------------------- /examples/todo-app/main.py: -------------------------------------------------------------------------------- 1 | import Neutron 2 | 3 | # All the CSS and HTML in this example is based on https://bbbootstrap.com/snippets/todo-list-jquery-and-font-awesome-icons-77769811 4 | 5 | win = Neutron.Window("Example", size=(800, 500), css="def.css") 6 | 7 | tasks = [0, 1, 2] 8 | 9 | def CreateTask(key): 10 | if key == "Enter": 11 | taskId = len(tasks) 12 | tasks.append(taskId) 13 | 14 | taskName = win.getElementById("addTask").value 15 | 16 | # Create task element 17 | 18 | taskHtml = f'
  • X {taskName}
  • ' 19 | win.getElementById("tasks").append(taskHtml) 20 | 21 | # Or you can do it like this... 22 | 23 | """ 24 | task = win.createElement("li") 25 | task.id = f"task{taskId}" 26 | 27 | span = win.createElement("span") 28 | span.onclick = "RemoveTask(this.parentNode.id)" 29 | span.innerHTML = 'X' 30 | 31 | task.appendChild(span) 32 | task.append(taskName) 33 | 34 | win.getElementById("tasks").appendChild(task) 35 | """ 36 | 37 | def RemoveTask(taskid): 38 | win.getElementById(taskid).remove() 39 | 40 | win.display(file="render.html", pyfunctions=[CreateTask, RemoveTask]) 41 | 42 | win.show() 43 | -------------------------------------------------------------------------------- /examples/todo-app/render.html: -------------------------------------------------------------------------------- 1 |
    2 |

    To-Do List

    3 | 9 | 10 | 30 |
    31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import codecs 3 | import os 4 | 5 | with codecs.open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md"), encoding="utf-8") as fh: 6 | long_description = "\n" + fh.read() 7 | 8 | # Setting up 9 | setup( 10 | name="neutron-web", 11 | version='0.5', 12 | author="IanTerzo (Ian Baldelli)", 13 | author_email="ian.baldelli@gmail.com", 14 | description="Create modern cross-platform apps in Python using HTML and CSS", 15 | long_description_content_type="text/markdown", 16 | long_description=long_description, 17 | packages=['Neutron'], 18 | install_requires=['PyQt6', 'PyQt6-WebEngine', 'bs4', 'asyncio', 'websockets'], 19 | keywords=['python', 'HTML', 'CSS', 'GUI', 'desktop apps'], 20 | classifiers=[ 21 | "Development Status :: 4 - Beta", 22 | "Intended Audience :: Developers", 23 | "Programming Language :: Python :: 3", 24 | "Operating System :: Unix", 25 | "Operating System :: MacOS :: MacOS X", 26 | "Operating System :: Microsoft :: Windows", 27 | ] 28 | ) 29 | --------------------------------------------------------------------------------