From 1c8d0a5ad8496feb6e1ff8299b8e7fb7292b6326 Mon Sep 17 00:00:00 2001 From: Sebbe Blokhuizen Date: Fri, 6 Mar 2026 11:22:39 +0100 Subject: [PATCH] add option to run as standalone application --- pyproject.toml | 2 ++ waveform_editor/cli.py | 25 +++++++++++++++----- waveform_editor/gui/main.py | 46 ++++++++++++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ca057ecb..cdf37099 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ dependencies = [ "kaleido", # required for exporting PNG to disk "asteval", "muscle3", + "pywebview[qt]", + "pyinstaller", ] dynamic = ["version"] diff --git a/waveform_editor/cli.py b/waveform_editor/cli.py index 7cd186f8..e7ac06dc 100644 --- a/waveform_editor/cli.py +++ b/waveform_editor/cli.py @@ -74,7 +74,13 @@ def parse_linspace(ctx, param, value): @click.option( "-p", "--port", type=int, default=0, help="Specify port to host application." ) -def launch_gui(file, port): +@click.option( + "--standalone", + is_flag=True, + default=False, + help="Launch as a standalone desktop window using pywebview instead of a browser.", +) +def launch_gui(file, port, standalone): """Launch the Waveform Editor GUI using Panel. \b @@ -85,13 +91,20 @@ def launch_gui(file, port): # cases: import panel as pn - from waveform_editor.gui.main import WaveformEditorGui + from waveform_editor.gui.main import PanelDesktop, WaveformEditorGui try: - app = WaveformEditorGui() - if file is not None: - app.load_yaml_from_file(Path(file)) - pn.serve(app, port=port, threaded=True) + + def create_app(): + app = WaveformEditorGui() + if file is not None: + app.load_yaml_from_file(Path(file)) + return app.__panel__() + + if standalone: + PanelDesktop().serve(create_app, port=port) + else: + pn.serve(create_app, port=port, threaded=True) except Exception as e: logger.error(f"Failed to launch GUI: {e}") diff --git a/waveform_editor/gui/main.py b/waveform_editor/gui/main.py index bfa8a1f0..8a7565af 100644 --- a/waveform_editor/gui/main.py +++ b/waveform_editor/gui/main.py @@ -1,8 +1,11 @@ import logging +import socket +import threading import imas import panel as pn import param +import webview import waveform_editor from waveform_editor.configuration import WaveformConfiguration @@ -195,6 +198,43 @@ def serve(self): return self.template.servable() -# Allow serving with `panel serve waveform_editor/gui/main.py` -if "bokeh" in __name__: - WaveformEditorGui().serve() +class PanelDesktop: + def __init__(self, title="Waveform Editor", width: int = 1920, height: int = 1080): + self.title = title + self.width = width + self.height = height + + @staticmethod + def _find_free_port(): + """Find a free port on localhost.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + s.listen(1) + port = s.getsockname()[1] + return port + + def serve(self, app_factory, port: int = 0): + if not port: + port = self._find_free_port() + + server_thread = threading.Thread( + target=lambda: pn.serve( + app_factory, + port=port, + show=False, + autoreload=False, + ), + daemon=True, + ) + server_thread.start() + + webview.create_window( + self.title, + f"http://localhost:{port}", + resizable=True, + fullscreen=False, + width=self.width, + height=self.height, + text_select=True, + ) + webview.start()