MacOS: app wrapper and launch agent scripts #1244 (#3008)

* Add scripts to create macOS app bundle and LaunchAgent #1244

This change introduces two new helper scripts for macOS users to improve integration with the operating system.

The `create-macos-app.sh` script builds a minimal `.app` wrapper for Solaar. This allows Solaar to be treated as a standard macOS application, enabling it to request necessary permissions, such as input monitoring, and to be discoverable by the OS. The script generates the required directory structure, a wrapper executable to launch Solaar, a standard `Info.plist` file, and an application icon from the source PNG.

The `create-macos-launchagent.sh` script sets up a LaunchAgent to automatically start Solaar at login and keep it running in the background. This ensures that the Solaar process is always available for device management without requiring manual intervention from the user. The script also configures log file locations for standard output and error streams.

Together, these scripts provide a more native and user-friendly experience for Solaar on macOS.

https://github.com/pwr-Solaar/Solaar/issues/1244

* Fix icon in script to create macOS app bundle #1244

https://github.com/pwr-Solaar/Solaar/issues/1244

* build: Remove hardcoded Homebrew path for solaar in macOS script

The `create-macos-app.sh` script previously defaulted to a hardcoded Homebrew path for the `solaar` executable. This change removes the specific path, defaulting instead to `solaar`.

This modification makes the script more flexible and robust. It allows the system's `PATH` to resolve the location of the `solaar` binary, accommodating various installation methods beyond a fixed Homebrew directory. The explicit check for the executable's existence is also removed, as the script will now rely on the command being available in the shell's environment, which is a more standard approach.

* refactor(macos): Improve solaar executable lookup in launchagent script

This change improves how the `create-macos-launchagent.sh` script locates the `solaar` executable.

Previously, the script defaulted to a hardcoded path (`/opt/homebrew/bin/solaar`) and would exit with an error if that specific file was not found and executable. This was too restrictive and failed in environments where `solaar` is installed in a different location, such as through `pipx` or in standard system paths like `/usr/local/bin`.

Now, the script defaults the `SOLAAR_PATH` to just `solaar` and uses the `command -v` utility to find the executable in the user's `PATH`. This allows the system to resolve the correct location of the `solaar` binary automatically. If the executable cannot be found in the `PATH`, the script now issues a warning instead of exiting, and proceeds to use the provided `SOLAAR_PATH` value in the generated LaunchAgent plist. This makes the script more flexible and robust for different installation methods.

* refactor(macos): Improve solaar executable lookup in launchagent script

This change improves how the `create-macos-launchagent.sh` script locates the `solaar` executable.

Previously, the script defaulted to a hardcoded path (`/opt/homebrew/bin/solaar`) and would exit with an error if that specific file was not found and executable. This was too restrictive and failed in environments where `solaar` is installed in a different location, such as through `pipx` or in standard system paths like `/usr/local/bin`.

Now, the script defaults the `SOLAAR_PATH` to just `solaar` and uses the `command -v` utility to find the executable in the user's `PATH`. This allows the system to resolve the correct location of the `solaar` binary automatically. If the executable cannot be found in the `PATH`, the script now issues a warning instead of exiting, and proceeds to use the provided `SOLAAR_PATH` value in the generated LaunchAgent plist. This makes the script more flexible and robust for different installation methods.

* refactor: Improve Solaar path resolution in macOS helper scripts

This change improves the robustness of the macOS helper scripts by ensuring the Solaar executable is found before proceeding.

Previously, `create-macos-launchagent.sh` would only issue a warning and continue with a potentially invalid path if the `solaar` command was not found. `create-macos-app.sh` had a typo (`SOLAR_PATH`) and lacked a check altogether.

Now, both scripts (`create-macos-app.sh` and `create-macos-launchagent.sh`) will:
- Correctly check if the `solaar` executable exists in the system's `PATH`.
- Use the resolved, absolute path to the executable to avoid ambiguity.
- Exit with an error if the executable cannot be found, preventing the creation of a broken app bundle or launch agent.

* feat(macos): Relaunch app to fix tray icon and add Dock icon

This change modifies the macOS app bundle creation script to improve the application's behavior and user experience on macOS.

Previously, when Solaar was launched as a standard `.app` bundle, macOS restrictions sometimes prevented the GTK tray icon from appearing correctly. This change introduces a workaround in the wrapper script. The application now relaunches itself as a detached background process on the first launch. This new, detached process is no longer subject to the same `.app` bundle restrictions, allowing the tray icon to be created reliably.

Additionally, the `LSUIElement` key in the `Info.plist` is set to `false`. This makes Solaar a regular application with an icon in the Dock, which is the standard behavior expected by most macOS users for a graphical application.

* docs: Explain macOS Python.app Dock icon limitation

This change adds a comment to the `create-macos-app.sh` script to explain why the Solaar application may still show a Dock icon on macOS, even when it is not desired for a background utility.

On macOS, Python often runs as `Python.app`, which has its own `Info.plist` file. This built-in `Info.plist` takes precedence over the one generated for the Solaar app bundle. As a result, settings like `LSUIElement=true`, which would normally hide the Dock icon, are overridden. This comment clarifies that this behavior is a known limitation of the Python distribution on macOS and not a bug in the Solaar packaging script.

* docs: Add instructions for creating macOS launcher options

This update enhances the installation guide by including steps to set up macOS launchers for Solaar. Two options are provided:
- LaunchAgent for automatic background execution and crash recovery.
- App launcher for manual addition to Login Items.
This commit is contained in:
Din Tort 2025-10-30 12:20:55 +01:00 committed by GitHub
parent 2cb5fa4b97
commit e906b83103
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 194 additions and 0 deletions

View File

@ -27,6 +27,18 @@ brew update
brew install hidapi gtk+3 pygobject3
```
### Optional: Set up macOS launcher
* Option A (recommended): Configure a LaunchAgent to automatically start Solaar and keep it running in the background.
It will also automatically restart Solaar if it crashed or closed.
```
bash <(curl -fsSL https://raw.githubusercontent.com/pwr-Solaar/Solaar/refs/heads/master/tools/create-macos-app.sh)
```
* Option B: Create Solaar.app launcher in /Applications.
It can be added to Login Items to start on login, but it will not automatically recover on crashes.
```
bash <(curl -fsSL https://raw.githubusercontent.com/pwr-Solaar/Solaar/refs/heads/master/tools/create-macos-launchagent.sh)
```
# Installating from GitHub
## Downloading

116
tools/create-macos-app.sh Executable file
View File

@ -0,0 +1,116 @@
#!/usr/bin/env bash
# Helper to build a minimal macOS .app wrapper for Solaar.
set -euo pipefail
APP_ROOT=${1:-/Applications/Solaar.app}
SOLAAR_PATH=${SOLAAR_PATH:-solaar}
SOLAAR_RESOLVED_PATH=$(command -v "${SOLAAR_PATH}" 2>/dev/null || echo "")
if [ -z "${SOLAAR_RESOLVED_PATH}" ]; then
echo "Error: '${SOLAAR_PATH}' not found" >&2
exit 1
fi
ICON_SOURCE=${ICON_SOURCE:-share/solaar/icons/solaar.svg}
case "${APP_ROOT}" in
""|"/"|".")
echo "Error: Refusing to create app bundle at unsafe location: \"${APP_ROOT}\"" >&2
exit 1
;;
esac
echo "Creating Solaar app bundle at ${APP_ROOT}"
rm -rf "${APP_ROOT}"
APP_CONTENTS="${APP_ROOT}/Contents"
MACOS_DIR="${APP_CONTENTS}/MacOS"
RESOURCES_DIR="${APP_CONTENTS}/Resources"
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
WRAPPER="${MACOS_DIR}/solaar-wrapper"
cat > "${WRAPPER}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
# When launched via 'Solaar.app', macOS applies .app bundle restrictions
# that may prevent GTK from creating a tray icon properly.
# Workaround: Launch solaar in a detached background process
# NOTE: macOS Python always uses Python.app which shows a Dock icon
# LSUIElement=true in Info.plist would have hidden it, but Python.app has its own Info.plist
# that overrides ours. This is a limitation of Python on macOS.
if [[ "\${SOLAAR_RELAUNCHED:-}" != "1" ]]; then
# First invocation from .app bundle - relaunch detached
export SOLAAR_RELAUNCHED=1
nohup "\$0" "\$@" >/dev/null 2>&1 &
exit 0
fi
# Second invocation - now detached, exec solaar normally
exec "${SOLAAR_RESOLVED_PATH}" "\$@"
EOF
chmod +x "${WRAPPER}"
HAVE_ICON=0
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1 && [[ -f "${ICON_SOURCE}" ]]; then
TMP_DIR=$(mktemp -d /tmp/solaar-icon.XXXXXX)
TMP_ICONSET="${TMP_DIR}/solaar.iconset"
mkdir -p "${TMP_ICONSET}"
trap 'rm -rf "${TMP_DIR}"' EXIT
for SIZE in 16 32 64 128 256 512; do
sips -s format png -z "${SIZE}" "${SIZE}" "${ICON_SOURCE}" --out "${TMP_ICONSET}/icon_${SIZE}x${SIZE}.png" >/dev/null
DOUBLE=$((SIZE * 2))
sips -s format png -z "${DOUBLE}" "${DOUBLE}" "${ICON_SOURCE}" --out "${TMP_ICONSET}/icon_${SIZE}x${SIZE}@2x.png" >/dev/null
done
if iconutil -c icns "${TMP_ICONSET}" -o "${RESOURCES_DIR}/solaar.icns" >/dev/null 2>&1; then
HAVE_ICON=1
echo "Added icon from ${ICON_SOURCE}"
else
echo "Warning: Failed to create solaar.icns continuing without custom icon" >&2
fi
rm -rf "${TMP_DIR}"
trap - EXIT
else
echo "Skipping icon generation (requires sips, iconutil, and ${ICON_SOURCE})"
fi
{
cat <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>solaar-wrapper</string>
<key>CFBundleIdentifier</key>
<string>io.github.pwr-solaar.solaar</string>
<key>CFBundleName</key>
<string>Solaar</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSInputMonitoringUsageDescription</key>
<string>Solaar needs to access input devices to configure and monitor your Logitech keyboards, mice, and other peripherals.</string>
<key>LSUIElement</key>
<false/>
EOF
if [[ ${HAVE_ICON} -eq 1 ]]; then
cat <<'EOF'
<key>CFBundleIconFile</key>
<string>solaar.icns</string>
EOF
fi
cat <<'EOF'
</dict>
</plist>
EOF
} > "${APP_CONTENTS}/Info.plist"
echo "Solaar app bundle created at ${APP_ROOT}"
echo ""
echo "To install the LaunchAgent for automatic startup and keep-alive execute:"
echo " bash tools/create-macos-launchagent.sh"

View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Helper to install a LaunchAgent for Solaar to keep it running in the background.
set -euo pipefail
SOLAAR_PATH=${SOLAAR_PATH:-solaar}
SOLAAR_RESOLVED_PATH=$(command -v "${SOLAAR_PATH}" 2>/dev/null || echo "")
if [ -z "${SOLAAR_RESOLVED_PATH}" ]; then
echo "Error: '${SOLAAR_PATH}' not found" >&2
exit 1
fi
LAUNCH_AGENT_DIR="${HOME}/Library/LaunchAgents"
LAUNCH_AGENT_PLIST="${LAUNCH_AGENT_DIR}/io.github.pwr-solaar.solaar.plist"
mkdir -p "${LAUNCH_AGENT_DIR}"
echo "Creating LaunchAgent to keep Solaar running..."
# Unload if already loaded (suppress errors)
launchctl unload "${LAUNCH_AGENT_PLIST}" 2>/dev/null || true
cat > "${LAUNCH_AGENT_PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>io.github.pwr-solaar.solaar</string>
<key>ProgramArguments</key>
<array>
<string>${SOLAAR_RESOLVED_PATH}</string>
<string>--window=hide</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${HOME}/Library/Logs/solaar.log</string>
<key>StandardErrorPath</key>
<string>${HOME}/Library/Logs/solaar.error.log</string>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
EOF
launchctl load "${LAUNCH_AGENT_PLIST}"
echo "LaunchAgent created at ${LAUNCH_AGENT_PLIST}"
echo ""
echo "To disable automatic startup:"
echo " launchctl unload \"${LAUNCH_AGENT_PLIST}\""
echo ""
echo "To re-enable automatic startup:"
echo " launchctl load \"${LAUNCH_AGENT_PLIST}\""
echo ""
echo "To start Solaar:"
echo " launchctl start io.github.pwr-solaar.solaar"
echo ""
echo "To stop Solaar:"
echo " launchctl stop io.github.pwr-solaar.solaar"
echo ""
echo "Logs will be written to:"
echo " ${HOME}/Library/Logs/solaar.log"
echo " ${HOME}/Library/Logs/solaar.error.log"