Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# file writer data
**.h5

# Qt Design Studio files
**.qtds

# ignore recover_config files
recovery_config*

Expand Down
Empty file added bec_launcher/gui/__init__.py
Empty file.
305 changes: 305 additions & 0 deletions bec_launcher/gui/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
"""
Backend for the BEC Launcher QML application.
Provides deployment data and launch actions to the QML frontend.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import List

from PySide6.QtCore import Property, QObject, QSettings, Signal, Slot

from bec_launcher.deployments import get_available_deployments, launch_deployment

# Default to the deployments folder in bec_launcher package
DEFAULT_DEPLOYMENTS_PATH = str(Path(sys.prefix).parent.parent.parent / "config" / "bec")

# Settings keys
SETTINGS_REMEMBER_CHOICE = "launcher/remember_choice"
SETTINGS_LAST_DEPLOYMENT = "launcher/last_deployment"
SETTINGS_LAST_ACTION = "launcher/last_action" # "terminal", "dock", or "app"


class Backend(QObject):
"""Backend providing deployment data and actions for the QML UI."""

# Signals for property changes
deploymentNamesChanged = Signal()
deploymentPathsChanged = Signal()
selectedIndexChanged = Signal()
deploymentConfirmedChanged = Signal()
rememberChoiceChanged = Signal()
autoLaunchTriggered = Signal(str) # Emits "terminal", "dock", or "app" for auto-launch
quitApplication = Signal() # Signal to quit the application

def __init__(self, base_path: str | None = None, fresh_start: bool = False):
super().__init__()

self._base_path = base_path or DEFAULT_DEPLOYMENTS_PATH
self._fresh_start = fresh_start
self._settings = QSettings("PSI", "BECLauncher")

print(f"[Backend] Using deployments path: {self._base_path}")
if fresh_start:
print("[Backend] Fresh start requested - ignoring saved preferences")

self._deployment_names: List[str] = []
self._deployment_paths_list: List[str] = []
self._deployment_paths: dict[str, str] = {}
self._selected_index: int = -1
self._deployment_confirmed: bool = False
self._remember_choice: bool = False
self._should_auto_launch: bool = False
self._auto_launch_action: str = ""

# Load deployments
self._load_deployments()

# Load saved preferences (unless fresh start)
if not fresh_start:
self._load_saved_preferences()

self._auto_select_single_deployment()

def _load_saved_preferences(self) -> None:
"""Load saved preferences from QSettings."""
self._remember_choice = self._settings.value(SETTINGS_REMEMBER_CHOICE, False, type=bool)

if self._remember_choice:
saved_deployment = self._settings.value(SETTINGS_LAST_DEPLOYMENT, "", type=str)
saved_action = self._settings.value(SETTINGS_LAST_ACTION, "", type=str)

if saved_deployment and saved_action:
if saved_action == "gui":
saved_action = "dock"

# Find the index of the saved deployment
try:
idx = self._deployment_names.index(saved_deployment)
self._selected_index = idx
self._deployment_confirmed = True
self._should_auto_launch = True
self._auto_launch_action = saved_action
print(f"[Backend] Auto-selecting saved deployment: {saved_deployment}")
print(f"[Backend] Will auto-launch: {saved_action}")
except ValueError:
print(f"[Backend] Saved deployment '{saved_deployment}' not found, ignoring")
self._remember_choice = False
self._should_auto_launch = False

def _save_preferences(self, action: str) -> None:
"""Save current preferences to QSettings."""
if self._remember_choice and self._selected_index >= 0:
deployment_name = self._deployment_names[self._selected_index]
self._settings.setValue(SETTINGS_REMEMBER_CHOICE, True)
self._settings.setValue(SETTINGS_LAST_DEPLOYMENT, deployment_name)
self._settings.setValue(SETTINGS_LAST_ACTION, action)
print(f"[Backend] Saved preferences: {deployment_name} -> {action}")
else:
self._settings.setValue(SETTINGS_REMEMBER_CHOICE, False)
self._settings.remove(SETTINGS_LAST_DEPLOYMENT)
self._settings.remove(SETTINGS_LAST_ACTION)

def _auto_select_single_deployment(self, emit_signals: bool = False) -> None:
"""Skip Step 1 when exactly one deployment is available."""
if (
self._should_auto_launch
or self._deployment_confirmed
or len(self._deployment_names) != 1
):
return

should_emit_selected = self._selected_index != 0
should_emit_confirmed = not self._deployment_confirmed

self._selected_index = 0
self._deployment_confirmed = True
print(f"[Backend] Auto-selecting the only deployment: {self._deployment_names[0]}")

if emit_signals:
if should_emit_selected:
self.selectedIndexChanged.emit()
if should_emit_confirmed:
self.deploymentConfirmedChanged.emit()

def _load_deployments(self) -> None:
"""Load available deployments from the filesystem."""
deployments = get_available_deployments(self._base_path)

self._deployment_names = []
self._deployment_paths_list = []
self._deployment_paths = {}

# Add production deployments first
for name in sorted(deployments["production"]):
self._deployment_names.append(name)
path = os.path.join(self._base_path, name)
self._deployment_paths_list.append(path)
self._deployment_paths[name] = path

# Then add test deployments
for name in sorted(deployments["test"]):
self._deployment_names.append(name)
path = os.path.join(self._base_path, name)
self._deployment_paths_list.append(path)
self._deployment_paths[name] = path

print(
f"[Backend] Found {len(self._deployment_names)} deployments: {self._deployment_names}"
)

self.deploymentNamesChanged.emit()
self.deploymentPathsChanged.emit()

# ─────────────────────────────────────────────────────────
# Properties exposed to QML
# ─────────────────────────────────────────────────────────

@Property(list, notify=deploymentNamesChanged)
def deploymentNames(self) -> List[str]:
"""List of deployment names available for selection."""
return self._deployment_names

@Property(list, notify=deploymentPathsChanged)
def deploymentPaths(self) -> List[str]:
"""List of deployment paths corresponding to deploymentNames."""
return self._deployment_paths_list

@Property(int, notify=selectedIndexChanged)
def selectedIndex(self) -> int:
"""Currently selected deployment index."""
return self._selected_index

@Property(bool, notify=deploymentConfirmedChanged)
def deploymentConfirmed(self) -> bool:
"""Whether a deployment has been confirmed."""
return self._deployment_confirmed

@Property(bool, notify=rememberChoiceChanged)
def rememberChoice(self) -> bool:
"""Whether to remember the user's last choice."""
return self._remember_choice

@Property(bool, constant=True)
def shouldAutoLaunch(self) -> bool:
"""Whether to auto-launch without showing UI."""
return self._should_auto_launch

@Property(str, constant=True)
def autoLaunchAction(self) -> str:
"""The action to auto-launch ('terminal', 'dock', or 'app')."""
return self._auto_launch_action

# ─────────────────────────────────────────────────────────
# Slots callable from QML
# ─────────────────────────────────────────────────────────

@Slot(int)
def selectDeployment(self, index: int) -> None:
"""Select a deployment by index."""
if index < 0 or index >= len(self._deployment_names):
return

if self._selected_index != index:
self._selected_index = index
self.selectedIndexChanged.emit()

@Slot()
def confirmDeployment(self) -> None:
"""Confirm the current deployment selection."""
if self._selected_index < 0:
return

self._deployment_confirmed = True
self.deploymentConfirmedChanged.emit()

if self._remember_choice:
self._save_preferences("terminal")

@Slot()
def changeDeployment(self) -> None:
"""Go back to deployment selection."""
self._deployment_confirmed = False
self.deploymentConfirmedChanged.emit()

@Slot(bool)
def setRememberChoice(self, remember: bool) -> None:
"""Set whether to remember the user's choice."""
if self._remember_choice != remember:
self._remember_choice = remember
self.rememberChoiceChanged.emit()

if not remember:
# Clear saved preferences when unchecked
self._settings.setValue(SETTINGS_REMEMBER_CHOICE, False)
self._settings.remove(SETTINGS_LAST_DEPLOYMENT)
self._settings.remove(SETTINGS_LAST_ACTION)

@Slot()
def resetPreferences(self) -> None:
"""Reset all saved preferences."""
self._remember_choice = False
self._settings.setValue(SETTINGS_REMEMBER_CHOICE, False)
self._settings.remove(SETTINGS_LAST_DEPLOYMENT)
self._settings.remove(SETTINGS_LAST_ACTION)
self.rememberChoiceChanged.emit()
print("[Backend] Preferences reset")

@Slot()
def launchTerminal(self) -> None:
"""Launch a terminal with the selected deployment's environment."""
self._launch_action("terminal", "bec --nogui ", "terminal")

@Slot()
def launchDock(self) -> None:
"""Launch BEC with the dock companion workflow."""
self._launch_action("dock", "bec ", "dock companion")

@Slot()
def launchApp(self) -> None:
"""Launch the standalone BEC App."""
self._launch_action("app", "bec-app", "BEC App")

@Slot()
def launchGui(self) -> None:
"""Backward-compatible alias for the dock companion action."""
self.launchDock()

def _launch_action(self, action: str, command: str, label: str) -> None:
"""Launch the selected deployment using the requested command."""
if self._selected_index < 0 or self._selected_index >= len(self._deployment_names):
print("[Backend] No deployment selected")
return

name = self._deployment_names[self._selected_index]
path = self._deployment_paths.get(name)

if not path:
print(f"[Backend] Path not found for deployment: {name}")
return

if self._remember_choice:
self._save_preferences(action)

print(f"[Backend] Launching {label} for deployment: {name} at {path}")

try:
launch_deployment(path, command, activate_env=True)
self.quitApplication.emit()
except Exception as e:
print(f"[Backend] Error launching {label}: {e}")

@Slot()
def refresh(self) -> None:
"""Refresh the list of deployments."""
self._load_deployments()
self._selected_index = -1
self._deployment_confirmed = False
self._should_auto_launch = False
self._auto_launch_action = ""
self._auto_select_single_deployment()
self.selectedIndexChanged.emit()
self.deploymentConfirmedChanged.emit()
Loading
Loading