Skip to content

Commit d36b8d7

Browse files
committed
feat(desktop icon): Automatically install the desktop icon on linux systems
Closes #717
1 parent cd3fd9a commit d36b8d7

4 files changed

Lines changed: 598 additions & 55 deletions

File tree

SetupDeveloperPC.sh

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -41,36 +41,6 @@ InstallDependencies() {
4141
python3 -m pip install -e .[dev]
4242
}
4343

44-
CreateDesktopEntry() {
45-
# Get the directory of the script
46-
prog_dir=$(realpath "$(dirname "$0")")/ardupilot_methodic_configurator
47-
48-
# Check if the system is Debian-based
49-
if [ -f /etc/debian_version ] || [ -f /etc/os-release ] && grep -q 'ID_LIKE=.*debian.*' /etc/os-release; then
50-
echo "Creating ardupilot_methodic_configurator.desktop for Debian-based systems..."
51-
# Define the desktop entry content
52-
desktop_entry="[Desktop Entry]\nName=ArduPilot Methodic Configurator\nComment=A clear ArduPilot configuration sequence\nExec=bash -c 'cd $prog_dir && python3 -m ardupilot_methodic_configurator'\nIcon=$prog_dir/images/ArduPilot_icon.png\nTerminal=true\nType=Application\nCategories=Development;\nKeywords=ardupilot;arducopter;drone;copter;scm"
53-
# Create the .desktop file in the appropriate directory
54-
echo -e "$desktop_entry" > "/home/$USER/.local/share/applications/ardupilot_methodic_configurator.desktop"
55-
echo "ardupilot_methodic_configurator.desktop created successfully."
56-
57-
# Check if the ~/Desktop directory exists
58-
if [ -d "$HOME/Desktop" ]; then
59-
# Copy the .desktop file to the ~/Desktop directory
60-
cp "/home/$USER/.local/share/applications/ardupilot_methodic_configurator.desktop" "$HOME/Desktop/"
61-
# Mark it as trusted
62-
chmod 755 "$HOME/Desktop/ardupilot_methodic_configurator.desktop"
63-
echo "ardupilot_methodic_configurator.desktop copied to ~/Desktop."
64-
else
65-
echo "$HOME/Desktop directory does not exist. Skipping copy to Desktop."
66-
fi
67-
68-
update-desktop-database ~/.local/share/applications/
69-
else
70-
echo "This system is not Debian-based. Skipping .desktop file creation."
71-
fi
72-
}
73-
7444
ConfigureGit() {
7545
echo "Configuring Git settings..."
7646
git config --local commit.gpgsign false
@@ -144,7 +114,6 @@ ConfigureVSCode() {
144114

145115
# Call configuration functions
146116
InstallDependencies
147-
CreateDesktopEntry
148117
ConfigureGit
149118
ConfigureVSCode
150119

ardupilot_methodic_configurator/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ def main() -> None:
497497
"""
498498
args = create_argument_parser().parse_args()
499499

500+
# Create desktop icon if needed (only on first run in venv)
501+
ProgramSettings.create_desktop_icon_if_needed()
502+
500503
state = ApplicationState(args)
501504

502505
setup_logging(state)

ardupilot_methodic_configurator/backend_filesystem_program_settings.py

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@
99
"""
1010

1111
# 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
1415
from importlib.resources import files as importlib_files
1516
from json import dump as json_dump
1617
from json import load as json_load
1718
from logging import debug as logging_debug
1819
from logging import error as logging_error
20+
from os import chmod as os_chmod
21+
from os import environ as os_environ
1922
from os import makedirs as os_makedirs
23+
from os import name as os_name
2024
from os import path as os_path
2125
from os import sep as os_sep
2226
from pathlib import Path
2327
from platform import system as platform_system
2428
from re import escape as re_escape
2529
from re import match as re_match
2630
from re import sub as re_sub
31+
from shutil import which as shutil_which
32+
from sys import platform as sys_platform
2733
from typing import Any, Optional, Union
2834

2935
from platformdirs import site_config_dir, user_config_dir
@@ -109,8 +115,18 @@ def _get_settings_as_dict() -> dict[str, Any]:
109115

110116
@staticmethod
111117
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 ""
114130

115131
@staticmethod
116132
def application_logo_filepath() -> str:
@@ -175,7 +191,7 @@ def _site_config_dir() -> str:
175191
)
176192

177193
if not os_path.exists(site_config_directory):
178-
with contextlib.suppress(OSError):
194+
with contextlib_suppress(OSError):
179195
os_makedirs(site_config_directory, exist_ok=True)
180196

181197
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]
373389
filename = f"m_{frame_class:02d}_{frame_type:02d}_*.png"
374390

375391
# 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))
377393

378394
err_msg = (
379395
""
@@ -406,3 +422,116 @@ def motor_diagram_exists(frame_class: int, frame_type: int) -> bool:
406422
"""
407423
filepath, _error_msg = ProgramSettings.motor_diagram_filepath(frame_class, frame_type)
408424
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

Comments
 (0)