diff --git a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py index 81a87d41..a667d638 100644 --- a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +++ b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py @@ -249,7 +249,7 @@ class Alignment1D(BECWidget, QWidget): box.ui.position_indicator.setFixedHeight(20) -if __name__ == "__main__": # pragma: no cover +def main(): import sys from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports @@ -263,3 +263,7 @@ if __name__ == "__main__": # pragma: no cover window = Alignment1D() window.show() sys.exit(app.exec_()) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/bec_widgets/applications/bec_app.py b/bec_widgets/applications/bec_app.py new file mode 100644 index 00000000..b4fa01fb --- /dev/null +++ b/bec_widgets/applications/bec_app.py @@ -0,0 +1,84 @@ +""" +Launcher for BEC GUI Applications + +Application must be located in bec_widgets/applications ; +in order for the launcher to find the application, it has to be put in +a subdirectory with the same name as the main Python module: + +/bec_widgets/applications + ├── alignment + │ └── alignment_1d + │ └── alignment_1d.py + ├── other_app + └── other_app.py + +The tree above would contain 2 applications, alignment_1d and other_app. + +The Python module for the application must have `if __name__ == "__main__":` +in order for the launcher to execute it (it is run with `python -m`). +""" + +import argparse +import os +import sys + +MODULE_PATH = os.path.dirname(__file__) + + +def find_apps(base_dir: str) -> list[str]: + matching_modules = [] + + for root, dirs, files in os.walk(base_dir): + parent_dir = os.path.basename(root) + + for file in files: + if file.endswith(".py") and file != "__init__.py": + file_name_without_ext = os.path.splitext(file)[0] + + if file_name_without_ext == parent_dir: + rel_path = os.path.relpath(root, base_dir) + module_path = rel_path.replace(os.sep, ".") + + module_name = f"{module_path}.{file_name_without_ext}" + matching_modules.append((file_name_without_ext, module_name)) + + return matching_modules + + +def main(): + parser = argparse.ArgumentParser(description="BEC application launcher") + + parser.add_argument("-m", "--module", type=str, help="The module to run (string argument).") + + # Add a positional argument for the module, which acts as a fallback if -m is not provided + parser.add_argument( + "positional_module", + nargs="?", # This makes the positional argument optional + help="Positional argument that is treated as module if -m is not specified.", + ) + + args = parser.parse_args() + # If the -m/--module is not provided, fallback to the positional argument + module = args.module if args.module else args.positional_module + + if module: + for app_name, app_module in find_apps(MODULE_PATH): + if module in (app_name, app_module): + print("Starting:", app_name) + python_executable = sys.executable + + # Replace the current process with the new Python module + os.execvp( + python_executable, + [python_executable, "-m", f"bec_widgets.applications.{app_module}"], + ) + print(f"Error: cannot find application {module}") + + # display list of apps + print("Available applications:") + for app, _ in find_apps(MODULE_PATH): + print(f" - {app}") + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/pyproject.toml b/pyproject.toml index d1ea8c36..50f3cbc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ Homepage = "https://gitlab.psi.ch/bec/bec_widgets" bw-generate-cli = "bec_widgets.cli.generate_cli:main" bec-gui-server = "bec_widgets.cli.server:main" bec-designer = "bec_widgets.utils.bec_designer:main" +bec-app = "bec_widgets.applications.bec_app:main" [tool.hatch.build.targets.wheel] include = ["*"]