|
9 | 9 | """ |
10 | 10 |
|
11 | 11 | # from sys import exit as sys_exit |
12 | | -import contextlib |
13 | | -import glob |
| 12 | +import subprocess |
| 13 | +from contextlib import suppress as contextlib_suppress |
| 14 | +from glob import glob as glob_glob |
14 | 15 | from importlib.resources import files as importlib_files |
15 | 16 | from json import dump as json_dump |
16 | 17 | from json import load as json_load |
17 | 18 | from logging import debug as logging_debug |
18 | 19 | from logging import error as logging_error |
| 20 | +from os import chmod as os_chmod |
| 21 | +from os import environ as os_environ |
19 | 22 | from os import makedirs as os_makedirs |
| 23 | +from os import name as os_name |
20 | 24 | from os import path as os_path |
21 | 25 | from os import sep as os_sep |
22 | 26 | from pathlib import Path |
23 | 27 | from platform import system as platform_system |
24 | 28 | from re import escape as re_escape |
25 | 29 | from re import match as re_match |
26 | 30 | from re import sub as re_sub |
| 31 | +from shutil import which as shutil_which |
| 32 | +from sys import platform as sys_platform |
27 | 33 | from typing import Any, Optional, Union |
28 | 34 |
|
29 | 35 | from platformdirs import site_config_dir, user_config_dir |
@@ -109,8 +115,18 @@ def _get_settings_as_dict() -> dict[str, Any]: |
109 | 115 |
|
110 | 116 | @staticmethod |
111 | 117 | def application_icon_filepath() -> str: |
112 | | - package_path = importlib_files("ardupilot_methodic_configurator") |
113 | | - return str(package_path / "images" / "ArduPilot_icon.png") |
| 118 | + """Get the application icon path, with fallback options.""" |
| 119 | + try: |
| 120 | + package_path = importlib_files("ardupilot_methodic_configurator") |
| 121 | + except (ImportError, FileNotFoundError): |
| 122 | + # Fallback: try to find icon relative to the script |
| 123 | + package_path = Path(os_path.dirname(os_path.abspath(__file__))) |
| 124 | + |
| 125 | + icon_path = str(package_path / "images" / "ArduPilot_icon.png") |
| 126 | + if os_path.exists(icon_path): |
| 127 | + return icon_path |
| 128 | + # If no icon found, return empty string (GUI will handle the error) |
| 129 | + return "" |
114 | 130 |
|
115 | 131 | @staticmethod |
116 | 132 | def application_logo_filepath() -> str: |
@@ -175,7 +191,7 @@ def _site_config_dir() -> str: |
175 | 191 | ) |
176 | 192 |
|
177 | 193 | if not os_path.exists(site_config_directory): |
178 | | - with contextlib.suppress(OSError): |
| 194 | + with contextlib_suppress(OSError): |
179 | 195 | os_makedirs(site_config_directory, exist_ok=True) |
180 | 196 |
|
181 | 197 | if not os_path.exists(site_config_directory): |
@@ -373,7 +389,7 @@ def motor_diagram_filepath(frame_class: int, frame_type: int) -> tuple[str, str] |
373 | 389 | filename = f"m_{frame_class:02d}_{frame_type:02d}_*.png" |
374 | 390 |
|
375 | 391 | # Search for matching PNG file (since exact naming varies) |
376 | | - matching_files = glob.glob(str(images_dir / filename)) |
| 392 | + matching_files = glob_glob(str(images_dir / filename)) |
377 | 393 |
|
378 | 394 | err_msg = ( |
379 | 395 | "" |
@@ -406,3 +422,116 @@ def motor_diagram_exists(frame_class: int, frame_type: int) -> bool: |
406 | 422 | """ |
407 | 423 | filepath, _error_msg = ProgramSettings.motor_diagram_filepath(frame_class, frame_type) |
408 | 424 | return filepath != "" and os_path.exists(filepath) |
| 425 | + |
| 426 | + @staticmethod |
| 427 | + def _is_linux_system() -> bool: |
| 428 | + """Check if running on a Linux system.""" |
| 429 | + return os_name == "posix" and sys_platform.startswith("linux") |
| 430 | + |
| 431 | + @staticmethod |
| 432 | + def _get_desktop_file_path() -> str: |
| 433 | + """Get the path where the desktop file should be created.""" |
| 434 | + return os_path.expanduser("~/.local/share/applications/ardupilot_methodic_configurator.desktop") |
| 435 | + |
| 436 | + @staticmethod |
| 437 | + def _desktop_icon_exists(desktop_file_path: str) -> bool: |
| 438 | + """Check if the desktop icon already exists.""" |
| 439 | + return os_path.exists(desktop_file_path) |
| 440 | + |
| 441 | + @staticmethod |
| 442 | + def _get_virtual_env_path() -> Optional[str]: |
| 443 | + """Get the virtual environment path from environment variables.""" |
| 444 | + return os_environ.get("VIRTUAL_ENV") |
| 445 | + |
| 446 | + @staticmethod |
| 447 | + def _create_desktop_entry_content(venv_path: str, icon_path: str) -> str: |
| 448 | + """Create the desktop entry file content.""" |
| 449 | + # Try to use python executable directly for better compatibility |
| 450 | + python_exe = os_path.join(venv_path, "bin", "python") |
| 451 | + if os_path.exists(python_exe): |
| 452 | + # Use python executable directly |
| 453 | + exec_cmd = f"{python_exe} -m ardupilot_methodic_configurator" |
| 454 | + else: |
| 455 | + # Fallback to bash -c method |
| 456 | + bash_path = shutil_which("bash") or "/bin/bash" |
| 457 | + activate_cmd = f"source {venv_path}/bin/activate && ardupilot_methodic_configurator" |
| 458 | + exec_cmd = f'{bash_path} -c "{activate_cmd}"' |
| 459 | + |
| 460 | + return f"""[Desktop Entry] |
| 461 | +Version=1.0 |
| 462 | +Name=ArduPilot Methodic Configurator |
| 463 | +Comment=A clear ArduPilot configuration sequence |
| 464 | +Exec={exec_cmd} |
| 465 | +Icon={icon_path} |
| 466 | +Terminal=true |
| 467 | +Type=Application |
| 468 | +Categories=Development; |
| 469 | +Keywords=ardupilot;arducopter;drone;parameters;configuration;scm |
| 470 | +""" |
| 471 | + |
| 472 | + @staticmethod |
| 473 | + def _ensure_applications_dir_exists(desktop_file_path: str) -> str: |
| 474 | + """Ensure the applications directory exists and return it.""" |
| 475 | + apps_dir = os_path.dirname(desktop_file_path) |
| 476 | + os_makedirs(apps_dir, exist_ok=True) |
| 477 | + return apps_dir |
| 478 | + |
| 479 | + @staticmethod |
| 480 | + def _write_desktop_file(desktop_file_path: str, content: str) -> None: |
| 481 | + """Write the desktop file content to disk.""" |
| 482 | + with open(desktop_file_path, "w", encoding="utf-8") as f: |
| 483 | + f.write(content) |
| 484 | + |
| 485 | + @staticmethod |
| 486 | + def _set_desktop_file_permissions(desktop_file_path: str) -> None: |
| 487 | + """Set appropriate permissions on the desktop file.""" |
| 488 | + os_chmod(desktop_file_path, 0o644) |
| 489 | + |
| 490 | + @staticmethod |
| 491 | + def _update_desktop_database(apps_dir: str) -> None: |
| 492 | + """Update the desktop database if the command is available.""" |
| 493 | + update_desktop_db_cmd = shutil_which("update-desktop-database") |
| 494 | + if update_desktop_db_cmd: |
| 495 | + subprocess.run([update_desktop_db_cmd, apps_dir], check=False, capture_output=True) # noqa: S603 |
| 496 | + |
| 497 | + @staticmethod |
| 498 | + def create_desktop_icon_if_needed() -> None: |
| 499 | + """ |
| 500 | + Create a desktop icon for the application if running in a virtual environment and icon doesn't exist. |
| 501 | +
|
| 502 | + This function detects if we're running in a virtual environment and creates a desktop |
| 503 | + entry that activates the venv and runs the application with the correct icon. |
| 504 | + """ |
| 505 | + # Only create desktop icon on Linux systems |
| 506 | + if not ProgramSettings._is_linux_system(): |
| 507 | + return |
| 508 | + |
| 509 | + # Check if desktop icon already exists |
| 510 | + desktop_file_path = ProgramSettings._get_desktop_file_path() |
| 511 | + if ProgramSettings._desktop_icon_exists(desktop_file_path): |
| 512 | + return |
| 513 | + |
| 514 | + # Check if we're in a virtual environment |
| 515 | + venv_path = ProgramSettings._get_virtual_env_path() |
| 516 | + if not venv_path: |
| 517 | + return |
| 518 | + |
| 519 | + # Find the icon path |
| 520 | + icon_path = ProgramSettings.application_icon_filepath() |
| 521 | + if not icon_path: |
| 522 | + return |
| 523 | + |
| 524 | + # Create the desktop entry content |
| 525 | + desktop_entry = ProgramSettings._create_desktop_entry_content(venv_path, icon_path) |
| 526 | + |
| 527 | + # Ensure the applications directory exists |
| 528 | + apps_dir = ProgramSettings._ensure_applications_dir_exists(desktop_file_path) |
| 529 | + |
| 530 | + # Write the desktop file |
| 531 | + try: |
| 532 | + ProgramSettings._write_desktop_file(desktop_file_path, desktop_entry) |
| 533 | + ProgramSettings._set_desktop_file_permissions(desktop_file_path) |
| 534 | + ProgramSettings._update_desktop_database(apps_dir) |
| 535 | + |
| 536 | + except (OSError, subprocess.SubprocessError): |
| 537 | + logging_error("Failed to create application launch desktop icon") |
0 commit comments