diff --git a/.gitignore b/.gitignore index c46af62..79658fd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,13 @@ Windows_and_Linux/config.json **/__pycache__ **/*.mo **/pot_files +Windows_and_Linux/dist/ +Windows_and_Linux/packaging/stage/ +Windows_and_Linux/packaging/dist/ +Windows_and_Linux/*.deb +Windows_and_Linux/*.venv +Windows_and_Linux/packaging/nfpm.yaml # Xcode user-specific data **/xcuserdata/ -**/project.xcworkspace/xcuserdata/ \ No newline at end of file +**/project.xcworkspace/xcuserdata/ diff --git a/README's Linked Content/To Compile the Application Yourself.md b/README's Linked Content/To Compile the Application Yourself.md index 829c9e9..fcedfe2 100644 --- a/README's Linked Content/To Compile the Application Yourself.md +++ b/README's Linked Content/To Compile the Application Yourself.md @@ -5,23 +5,23 @@ Here's how to compile it with PyInstaller and a virtual environment: 1. First, create and activate a virtual environment: ```bash -# Install virtualenv if you haven't already -pip install virtualenv +# Debian/Ubuntu only (one-time): install venv support +sudo apt install -y python3-venv # Create a new virtual environment -virtualenv myvenv +python3 -m venv .venv # Activate it # On Windows: -myvenv\Scripts\activate +.venv\Scripts\activate # On Linux: -source myvenv/bin/activate +source .venv/bin/activate ``` 2. Once activated, install the required packages: ```bash -pip install -r requirements.txt +python -m pip install -r requirements.txt ``` 3. Build Writing Tools: @@ -29,6 +29,24 @@ pip install -r requirements.txt python pyinstaller-build-script.py ``` +4. (Linux optional) Build a Debian package: + +Install nFPM first (one-time) using the official installation instructions: +https://nfpm.goreleaser.com/docs/install/ + +```bash +# Build .deb (outputs to packaging/dist/) +./build-deb.sh + +# Install it +sudo apt install ./packaging/dist/*.deb +``` + +5. (Linux optional) Manual source install without `.deb`: +```bash +./install-local-linux.sh +``` + ### macOS Version (by [Aryamirsepasi](https://github.com/Aryamirsepasi)) build instructions: 1. **Install Xcode** diff --git a/README's Linked Content/To Run Writing Tools Directly from the Source Code.md b/README's Linked Content/To Run Writing Tools Directly from the Source Code.md index 329df10..5d033db 100644 --- a/README's Linked Content/To Run Writing Tools Directly from the Source Code.md +++ b/README's Linked Content/To Run Writing Tools Directly from the Source Code.md @@ -11,13 +11,19 @@ After extracting the folder, open your **Terminal** (or **Command Prompt**) in t - Windows: ```bash cd path\to\Windows_and_Linux - pip install -r requirements.txt + py -m venv .venv + .venv\Scripts\activate + python -m pip install -r requirements.txt ``` - Linux: ```bash cd /path/to/Windows_and_Linux - pip3 install -r requirements.txt + # Debian/Ubuntu only (one-time): install venv support + sudo apt install -y python3-venv + python3 -m venv .venv + source .venv/bin/activate + python -m pip install -r requirements.txt ``` Of course, you'll need to have [Python installed](https://www.python.org/downloads/)! @@ -34,5 +40,11 @@ Of course, you'll need to have [Python installed](https://www.python.org/downloa python3 main.py ``` +**Optional Linux desktop integration (manual/source install):** +```bash +cd /path/to/Windows_and_Linux +./install-local-linux.sh +``` + ### [**◀️ Back to main page**](https://github.com/theJayTea/WritingTools) diff --git a/README.md b/README.md index c1a1fcd..1c36841 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,18 @@ Aside from being the only Windows/Linux program like Apple's Writing Tools, and ### **🐧 Linux (work-in-progress)**: [Run it from the source code](https://github.com/theJayTea/WritingTools/blob/main/README's%20Linked%20Content/To%20Run%20Writing%20Tools%20Directly%20from%20the%20Source%20Code.md) +You can also build an installable `.deb` package yourself: +1. `cd Windows_and_Linux` +2. On Debian/Ubuntu, install venv support (one-time): `sudo apt install -y python3-venv` +3. Create and activate a virtual environment: `python3 -m venv .venv && source .venv/bin/activate` +4. Install requirements: `python -m pip install -r requirements.txt` +5. Install nFPM (packager): https://nfpm.goreleaser.com/docs/install/ +6. Build package: `./build-deb.sh` +7. Install package: `sudo apt install ./packaging/dist/*.deb` + +Note: on first launch, `writing-tools` auto-initializes your user profile under `~/.local/share/writingtools`. +`install-local-linux.sh` remains available for manual/source installs. + Writing Tools works well on x11. On Wayland, there are a few caveats: - [it works on XWayland apps](https://github.com/theJayTea/WritingTools/issues/34#issuecomment-2461633556) - [and it works if you disable Wayland for individual Flatpaks with Flatseal.](https://github.com/theJayTea/WritingTools/issues/93#issuecomment-2576511041) diff --git a/Windows_and_Linux/Writing Tools.spec b/Windows_and_Linux/Writing Tools.spec new file mode 100644 index 0000000..7d898aa --- /dev/null +++ b/Windows_and_Linux/Writing Tools.spec @@ -0,0 +1,39 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[('locales', 'locales')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['tkinter', 'unittest', 'IPython', 'jedi', 'email_validator', 'psutil', 'pyzmq', 'tornado', 'PySide6.QtNetwork', 'PySide6.QtXml', 'PySide6.QtQml', 'PySide6.QtQuick', 'PySide6.QtQuickWidgets', 'PySide6.QtPrintSupport', 'PySide6.QtSql', 'PySide6.QtTest', 'PySide6.QtSvg', 'PySide6.QtSvgWidgets', 'PySide6.QtHelp', 'PySide6.QtMultimedia', 'PySide6.QtMultimediaWidgets', 'PySide6.QtOpenGL', 'PySide6.QtOpenGLWidgets', 'PySide6.QtPositioning', 'PySide6.QtLocation', 'PySide6.QtSerialPort', 'PySide6.QtWebChannel', 'PySide6.QtWebSockets', 'PySide6.QtWinExtras', 'PySide6.QtNetworkAuth', 'PySide6.QtRemoteObjects', 'PySide6.QtTextToSpeech', 'PySide6.QtWebEngineCore', 'PySide6.QtWebEngineWidgets', 'PySide6.QtWebEngine', 'PySide6.QtBluetooth', 'PySide6.QtNfc', 'PySide6.QtWebView', 'PySide6.QtCharts', 'PySide6.QtDataVisualization', 'PySide6.QtPdf', 'PySide6.QtPdfWidgets', 'PySide6.QtQuick3D', 'PySide6.QtQuickControls2', 'PySide6.QtQuickParticles', 'PySide6.QtQuickTest', 'PySide6.QtQuickWidgets', 'PySide6.QtSensors', 'PySide6.QtStateMachine', 'PySide6.Qt3DCore', 'PySide6.Qt3DRender', 'PySide6.Qt3DInput', 'PySide6.Qt3DLogic', 'PySide6.Qt3DAnimation', 'PySide6.Qt3DExtras'], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='Writing Tools', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['icons/app_icon.png'], +) diff --git a/Windows_and_Linux/build-deb.sh b/Windows_and_Linux/build-deb.sh new file mode 100755 index 0000000..2ed725b --- /dev/null +++ b/Windows_and_Linux/build-deb.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PKG_DIR="${SCRIPT_DIR}/packaging" +STAGE_DIR="${PKG_DIR}/stage" +OUTPUT_DIR="${PKG_DIR}/dist" + +DEFAULT_VERSION="$(tr -d '[:space:]' < "${SCRIPT_DIR}/Latest_Version_for_Update_Check.txt")" +VERSION="${1:-${DEFAULT_VERSION}}" +ARCH_OVERRIDE="${2:-}" + +if [[ -z "${VERSION}" ]]; then + echo "ERROR: Package version is empty." + echo "Provide a version as the first argument, e.g. ./build-deb.sh 8" + exit 1 +fi + +if ! command -v nfpm >/dev/null 2>&1; then + echo "ERROR: nfpm is required but was not found in PATH." + echo "Install: https://nfpm.goreleaser.com/docs/install/" + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 is required to build the app binary." + exit 1 +fi + +map_arch() { + local machine + machine="${1}" + + case "${machine}" in + x86_64) + echo "amd64" + ;; + aarch64|arm64) + echo "arm64" + ;; + armv7l) + echo "arm7" + ;; + i686|i386) + echo "386" + ;; + *) + return 1 + ;; + esac +} + +if [[ -n "${ARCH_OVERRIDE}" ]]; then + NFPM_ARCH="${ARCH_OVERRIDE}" +else + if ! NFPM_ARCH="$(map_arch "$(uname -m)")"; then + echo "ERROR: Unsupported architecture: $(uname -m)" + echo "Pass nfpm arch manually as second argument (e.g. amd64, arm64)." + exit 1 + fi +fi + +export NFPM_VERSION="${VERSION}" +export NFPM_RELEASE="${NFPM_RELEASE:-1}" +export NFPM_ARCH + +echo "[1/4] Building PyInstaller binary" +( + cd "${SCRIPT_DIR}" + python3 pyinstaller-build-script.py +) + +DIST_EXE="${SCRIPT_DIR}/dist/Writing Tools" +if [[ ! -x "${DIST_EXE}" ]]; then + echo "ERROR: Expected binary not found: ${DIST_EXE}" + exit 1 +fi + +echo "[2/4] Staging package files" +rm -rf "${STAGE_DIR}" "${OUTPUT_DIR}" +mkdir -p \ + "${STAGE_DIR}/usr/bin" \ + "${STAGE_DIR}/usr/lib/writing-tools/payload" \ + "${STAGE_DIR}/usr/share/applications" \ + "${STAGE_DIR}/usr/share/icons/hicolor/256x256/apps" \ + "${OUTPUT_DIR}" + +install -m 0755 "${DIST_EXE}" "${STAGE_DIR}/usr/lib/writing-tools/payload/Writing Tools" +install -m 0755 "${SCRIPT_DIR}/install-local-linux.sh" "${STAGE_DIR}/usr/lib/writing-tools/install-local-linux.sh" + +cp -a "${SCRIPT_DIR}/icons" "${STAGE_DIR}/usr/lib/writing-tools/payload/icons" +cp -a "${SCRIPT_DIR}/locales" "${STAGE_DIR}/usr/lib/writing-tools/payload/locales" + +for file_name in background.png background_dark.png background_popup.png background_popup_dark.png options.json Latest_Version_for_Update_Check.txt; do + install -m 0644 "${SCRIPT_DIR}/${file_name}" "${STAGE_DIR}/usr/lib/writing-tools/payload/${file_name}" +done + +install -m 0644 "${SCRIPT_DIR}/icons/app_icon.png" "${STAGE_DIR}/usr/share/icons/hicolor/256x256/apps/writing-tools.png" + +cat > "${STAGE_DIR}/usr/share/applications/writing-tools.desktop" <<'EOF' +[Desktop Entry] +Type=Application +Name=Writing Tools +Comment=AI-powered writing helper with global hotkey popup +Exec=/usr/bin/writing-tools +Icon=writing-tools +Terminal=false +Categories=Office;Utility; +StartupNotify=false +EOF +chmod 0644 "${STAGE_DIR}/usr/share/applications/writing-tools.desktop" + +cat > "${STAGE_DIR}/usr/bin/writing-tools" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +INSTALLER="/usr/lib/writing-tools/install-local-linux.sh" +PAYLOAD="/usr/lib/writing-tools/payload" +USER_LAUNCHER="${HOME}/.local/bin/writing-tools" + +if [[ "${EUID}" -eq 0 ]]; then + echo "Please run writing-tools as a regular desktop user, not as root." + echo "Example: sudo -u writing-tools" + exit 1 +fi + +if [[ ! -x "${USER_LAUNCHER}" ]]; then + if [[ ! -x "${INSTALLER}" ]]; then + echo "ERROR: Missing installer helper at ${INSTALLER}" + exit 1 + fi + + if [[ ! -d "${PAYLOAD}" ]]; then + echo "ERROR: Missing payload directory at ${PAYLOAD}" + exit 1 + fi + + "${INSTALLER}" --package-mode --app-source "${PAYLOAD}" || true +fi + +if [[ -x "${USER_LAUNCHER}" ]]; then + exec "${USER_LAUNCHER}" "$@" +fi + +echo "Writing Tools could not initialize your local profile automatically." +echo "Run this once as your user and try again:" +echo " /usr/lib/writing-tools/install-local-linux.sh --package-mode --app-source /usr/lib/writing-tools/payload" +exit 1 +EOF +chmod 0755 "${STAGE_DIR}/usr/bin/writing-tools" + +echo "[3/4] Building .deb package with nfpm" +( + cd "${SCRIPT_DIR}" + nfpm pkg --packager deb --config "${PKG_DIR}/nfpm.yaml" --target "${OUTPUT_DIR}/" +) + +echo "[4/4] Build completed" +ls -1 "${OUTPUT_DIR}"/*.deb diff --git a/Windows_and_Linux/install-local-linux.sh b/Windows_and_Linux/install-local-linux.sh new file mode 100755 index 0000000..d245893 --- /dev/null +++ b/Windows_and_Linux/install-local-linux.sh @@ -0,0 +1,395 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +ENABLE_AUTOSTART="" +PACKAGE_MODE="no" +TARGET_USER="${SUDO_USER:-${USER:-}}" +APP_SOURCE="" + +TARGET_HOME="" +TARGET_UID="" +TARGET_GID="" +DIST_EXE="" + +DATA_HOME="" +CONFIG_HOME="" +INSTALL_ROOT="" +APP_DIR="" +BIN_DIR="" +APPS_DIR="" +AUTOSTART_DIR="" + +LAUNCHER_PATH="" +DESKTOP_PATH="" +AUTOSTART_PATH="" + +print_usage() { + cat <<'USAGE' +Usage: + ./install-local-linux.sh [--enable-autostart|--disable-autostart] + ./install-local-linux.sh --package-mode --target-user --app-source [--enable-autostart|--disable-autostart] + +Installs the compiled Writing Tools app for one Linux user: +- App files: ~/.local/share/writingtools/app +- Launcher command: ~/.local/bin/writing-tools +- App menu entry: ~/.local/share/applications/writing-tools.desktop + +Options: + --enable-autostart Create ~/.config/autostart/writing-tools.desktop + --disable-autostart Remove ~/.config/autostart/writing-tools.desktop + --package-mode Enable package-managed behavior (skip local vendoring) + --target-user Target Linux username for provisioning (required when run as root in package mode) + --app-source Directory containing app payload files. If omitted, defaults to this script directory + -h, --help Show this help message +USAGE +} + +download_deb() { + local package_name="$1" + local download_dir="$2" + + if ! command -v apt >/dev/null 2>&1; then + return 1 + fi + + ( + cd "${download_dir}" + apt download "${package_name}" >/dev/null 2>&1 + ) +} + +ensure_local_libxcb_cursor() { + if ldconfig -p 2>/dev/null | grep -q 'libxcb-cursor.so.0'; then + return 0 + fi + + if [[ -e "${APP_DIR}/lib/libxcb-cursor.so.0" ]]; then + return 0 + fi + + if [[ -f "${APP_DIR}/lib/libxcb-cursor.so.0.0.0" ]]; then + ln -s "libxcb-cursor.so.0.0.0" "${APP_DIR}/lib/libxcb-cursor.so.0" + return 0 + fi + + if ! command -v dpkg-deb >/dev/null 2>&1; then + echo "WARNING: dpkg-deb not found; cannot vendor libxcb-cursor0 locally." + return 1 + fi + + local temp_dir + temp_dir="$(mktemp -d)" + + if ! download_deb "libxcb-cursor0" "${temp_dir}"; then + echo "WARNING: Could not download libxcb-cursor0 package." + rm -rf "${temp_dir}" + return 1 + fi + + local deb_file + deb_file="$(ls "${temp_dir}"/libxcb-cursor0_*.deb 2>/dev/null | head -n 1 || true)" + if [[ -z "${deb_file}" ]]; then + echo "WARNING: libxcb-cursor0 package download did not produce a .deb file." + rm -rf "${temp_dir}" + return 1 + fi + + dpkg-deb -x "${deb_file}" "${temp_dir}/pkg" + mkdir -p "${APP_DIR}/lib" + find "${temp_dir}/pkg" -type f -name 'libxcb-cursor.so.0*' -exec cp -a {} "${APP_DIR}/lib/" \; + + if [[ -f "${APP_DIR}/lib/libxcb-cursor.so.0.0.0" && ! -e "${APP_DIR}/lib/libxcb-cursor.so.0" ]]; then + ln -s "libxcb-cursor.so.0.0.0" "${APP_DIR}/lib/libxcb-cursor.so.0" + fi + + rm -rf "${temp_dir}" + return 0 +} + +ensure_local_xclip() { + if command -v xclip >/dev/null 2>&1; then + return 0 + fi + + if [[ -x "${APP_DIR}/bin/xclip" ]]; then + return 0 + fi + + if ! command -v dpkg-deb >/dev/null 2>&1; then + echo "WARNING: dpkg-deb not found; cannot vendor xclip locally." + return 1 + fi + + local temp_dir + temp_dir="$(mktemp -d)" + + if ! download_deb "xclip" "${temp_dir}"; then + echo "WARNING: Could not download xclip package." + rm -rf "${temp_dir}" + return 1 + fi + + local deb_file + deb_file="$(ls "${temp_dir}"/xclip_*.deb 2>/dev/null | head -n 1 || true)" + if [[ -z "${deb_file}" ]]; then + echo "WARNING: xclip package download did not produce a .deb file." + rm -rf "${temp_dir}" + return 1 + fi + + dpkg-deb -x "${deb_file}" "${temp_dir}/pkg" + mkdir -p "${APP_DIR}/bin" + + if [[ -x "${temp_dir}/pkg/usr/bin/xclip" ]]; then + install -m 0755 "${temp_dir}/pkg/usr/bin/xclip" "${APP_DIR}/bin/xclip" + fi + + rm -rf "${temp_dir}" + return 0 +} + +resolve_target_user() { + local current_uid + current_uid="$(id -u)" + + if [[ -z "${TARGET_USER}" || "${TARGET_USER}" == "root" ]]; then + local login_user + login_user="$(logname 2>/dev/null || true)" + if [[ -n "${login_user}" && "${login_user}" != "root" ]]; then + TARGET_USER="${login_user}" + fi + fi + + if [[ "${current_uid}" -eq 0 ]]; then + if [[ -z "${TARGET_USER}" || "${TARGET_USER}" == "root" ]]; then + echo "ERROR: Could not determine a non-root target user." + echo " Re-run with: --target-user " + exit 1 + fi + + TARGET_HOME="$(getent passwd "${TARGET_USER}" | cut -d: -f6)" + if [[ -z "${TARGET_HOME}" ]]; then + echo "ERROR: User '${TARGET_USER}' was not found on this system." + exit 1 + fi + + TARGET_UID="$(id -u "${TARGET_USER}")" + TARGET_GID="$(id -g "${TARGET_USER}")" + + DATA_HOME="${TARGET_HOME}/.local/share" + CONFIG_HOME="${TARGET_HOME}/.config" + else + if [[ -n "${TARGET_USER}" && "${TARGET_USER}" != "${USER}" ]]; then + echo "ERROR: --target-user only supports the current user when not running as root." + exit 1 + fi + + TARGET_USER="${USER}" + TARGET_HOME="${HOME}" + TARGET_UID="$(id -u)" + TARGET_GID="$(id -g)" + + DATA_HOME="${XDG_DATA_HOME:-${TARGET_HOME}/.local/share}" + CONFIG_HOME="${XDG_CONFIG_HOME:-${TARGET_HOME}/.config}" + fi + + INSTALL_ROOT="${DATA_HOME}/writingtools" + APP_DIR="${INSTALL_ROOT}/app" + BIN_DIR="${TARGET_HOME}/.local/bin" + APPS_DIR="${DATA_HOME}/applications" + AUTOSTART_DIR="${CONFIG_HOME}/autostart" + + LAUNCHER_PATH="${BIN_DIR}/writing-tools" + DESKTOP_PATH="${APPS_DIR}/writing-tools.desktop" + AUTOSTART_PATH="${AUTOSTART_DIR}/writing-tools.desktop" +} + +resolve_app_source() { + APP_SOURCE="${APP_SOURCE:-${SCRIPT_DIR}}" + + if [[ ! -d "${APP_SOURCE}" ]]; then + echo "ERROR: App source directory not found: ${APP_SOURCE}" + exit 1 + fi + + if [[ -x "${APP_SOURCE}/Writing Tools" ]]; then + DIST_EXE="${APP_SOURCE}/Writing Tools" + elif [[ -x "${APP_SOURCE}/dist/Writing Tools" ]]; then + DIST_EXE="${APP_SOURCE}/dist/Writing Tools" + else + echo "ERROR: Compiled binary not found under app source: ${APP_SOURCE}" + if [[ "${APP_SOURCE}" == "${SCRIPT_DIR}" ]]; then + echo "Build first with: python3 pyinstaller-build-script.py" + fi + exit 1 + fi + + for required_dir in icons locales; do + if [[ ! -d "${APP_SOURCE}/${required_dir}" ]]; then + echo "ERROR: Missing required directory in app source: ${APP_SOURCE}/${required_dir}" + exit 1 + fi + done + + for required_file in background.png background_dark.png background_popup.png background_popup_dark.png Latest_Version_for_Update_Check.txt options.json; do + if [[ ! -f "${APP_SOURCE}/${required_file}" ]]; then + echo "ERROR: Missing required file in app source: ${APP_SOURCE}/${required_file}" + exit 1 + fi + done +} + +chown_paths_if_needed() { + if [[ "$(id -u)" -ne 0 ]]; then + return 0 + fi + + for path in "${INSTALL_ROOT}" "${BIN_DIR}" "${APPS_DIR}" "${AUTOSTART_DIR}"; do + if [[ -e "${path}" ]]; then + chown -R "${TARGET_UID}:${TARGET_GID}" "${path}" + fi + done + + for file_path in "${LAUNCHER_PATH}" "${DESKTOP_PATH}" "${AUTOSTART_PATH}"; do + if [[ -e "${file_path}" ]]; then + chown "${TARGET_UID}:${TARGET_GID}" "${file_path}" + fi + done +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --enable-autostart) + ENABLE_AUTOSTART="yes" + ;; + --disable-autostart) + ENABLE_AUTOSTART="no" + ;; + --package-mode) + PACKAGE_MODE="yes" + ;; + --target-user) + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: --target-user requires a value" + exit 1 + fi + TARGET_USER="$1" + ;; + --app-source) + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: --app-source requires a value" + exit 1 + fi + APP_SOURCE="$1" + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo "ERROR: Unknown option: $1" + print_usage + exit 1 + ;; + esac + shift +done + +resolve_target_user +resolve_app_source + +mkdir -p "${APP_DIR}" "${BIN_DIR}" "${APPS_DIR}" "${AUTOSTART_DIR}" + +install -m 0755 "${DIST_EXE}" "${APP_DIR}/Writing Tools" + +# Runtime assets expected by current app code next to sys.argv[0]. +rm -rf "${APP_DIR}/icons" "${APP_DIR}/locales" +cp -a "${APP_SOURCE}/icons" "${APP_DIR}/icons" +cp -a "${APP_SOURCE}/locales" "${APP_DIR}/locales" + +for file_name in background.png background_dark.png background_popup.png background_popup_dark.png Latest_Version_for_Update_Check.txt; do + install -m 0644 "${APP_SOURCE}/${file_name}" "${APP_DIR}/${file_name}" +done + +# Preserve user's customized options if already present. +if [[ ! -f "${APP_DIR}/options.json" ]]; then + install -m 0644 "${APP_SOURCE}/options.json" "${APP_DIR}/options.json" +fi + +if [[ "${PACKAGE_MODE}" != "yes" ]]; then + ensure_local_libxcb_cursor || true + ensure_local_xclip || true +fi + +cat > "${LAUNCHER_PATH}" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +APP_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/writingtools/app" +if [[ -d "${APP_DIR}/lib" ]]; then + export LD_LIBRARY_PATH="${APP_DIR}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +fi +if [[ -d "${APP_DIR}/bin" ]]; then + export PATH="${APP_DIR}/bin:${PATH}" +fi +cd "${APP_DIR}" +exec "${APP_DIR}/Writing Tools" "$@" +EOF +chmod 0755 "${LAUNCHER_PATH}" + +cat > "${DESKTOP_PATH}" < "${AUTOSTART_PATH}" </dev/null 2>&1 && ! command -v xsel >/dev/null 2>&1 && ! command -v wl-copy >/dev/null 2>&1; then + if [[ "${PACKAGE_MODE}" == "yes" ]]; then + echo "WARNING: No clipboard backend detected (xclip/xsel/wl-copy)." + echo " Install one with your package manager so copy/replace works." + elif [[ -x "${APP_DIR}/bin/xclip" ]]; then + echo "Clipboard backend provided via local xclip binary." + else + echo "WARNING: No clipboard backend detected for pyperclip (xclip/xsel/wl-copy)." + echo " Install one to ensure copy/replace flow works." + fi +fi + +if [[ "${XDG_SESSION_TYPE:-}" == "wayland" ]]; then + echo "NOTICE: Running in Wayland session; global hotkey/focus behavior may be limited." +fi diff --git a/Windows_and_Linux/pyinstaller-build-script.py b/Windows_and_Linux/pyinstaller-build-script.py index 5681988..d472782 100644 --- a/Windows_and_Linux/pyinstaller-build-script.py +++ b/Windows_and_Linux/pyinstaller-build-script.py @@ -1,105 +1,110 @@ -import os -import subprocess -import sys - - -def run_pyinstaller_build(): - pyinstaller_command = [ - "pyinstaller", - "--onefile", - "--windowed", - "--icon=icons/app_icon.ico", - "--name=Writing Tools", - "--clean", - "--noconfirm", - # Exclude unnecessary modules - "--exclude-module", "tkinter", - "--exclude-module", "unittest", - "--exclude-module", "IPython", - "--exclude-module", "jedi", - "--exclude-module", "email_validator", - # NOTE: do NOT exclude `cryptography` — google-genai's auth chain pulls - # it in heavily during `genai.Client()` construction. Excluding it - # makes the compiled exe crash on startup. - "--exclude-module", "psutil", - "--exclude-module", "pyzmq", - "--exclude-module", "tornado", - # Exclude modules related to PySide6 that are not used - "--exclude-module", "PySide6.QtNetwork", - "--exclude-module", "PySide6.QtXml", - "--exclude-module", "PySide6.QtQml", - "--exclude-module", "PySide6.QtQuick", - "--exclude-module", "PySide6.QtQuickWidgets", - "--exclude-module", "PySide6.QtPrintSupport", - "--exclude-module", "PySide6.QtSql", - "--exclude-module", "PySide6.QtTest", - "--exclude-module", "PySide6.QtSvg", - "--exclude-module", "PySide6.QtSvgWidgets", - "--exclude-module", "PySide6.QtHelp", - "--exclude-module", "PySide6.QtMultimedia", - "--exclude-module", "PySide6.QtMultimediaWidgets", - "--exclude-module", "PySide6.QtOpenGL", - "--exclude-module", "PySide6.QtOpenGLWidgets", - "--exclude-module", "PySide6.QtPositioning", - "--exclude-module", "PySide6.QtLocation", - "--exclude-module", "PySide6.QtSerialPort", - "--exclude-module", "PySide6.QtWebChannel", - "--exclude-module", "PySide6.QtWebSockets", - "--exclude-module", "PySide6.QtWinExtras", - "--exclude-module", "PySide6.QtNetworkAuth", - "--exclude-module", "PySide6.QtRemoteObjects", - "--exclude-module", "PySide6.QtTextToSpeech", - "--exclude-module", "PySide6.QtWebEngineCore", - "--exclude-module", "PySide6.QtWebEngineWidgets", - "--exclude-module", "PySide6.QtWebEngine", - "--exclude-module", "PySide6.QtBluetooth", - "--exclude-module", "PySide6.QtNfc", - "--exclude-module", "PySide6.QtWebView", - "--exclude-module", "PySide6.QtCharts", - "--exclude-module", "PySide6.QtDataVisualization", - "--exclude-module", "PySide6.QtPdf", - "--exclude-module", "PySide6.QtPdfWidgets", - "--exclude-module", "PySide6.QtQuick3D", - "--exclude-module", "PySide6.QtQuickControls2", - "--exclude-module", "PySide6.QtQuickParticles", - "--exclude-module", "PySide6.QtQuickTest", - "--exclude-module", "PySide6.QtQuickWidgets", - "--exclude-module", "PySide6.QtSensors", - "--exclude-module", "PySide6.QtStateMachine", - "--exclude-module", "PySide6.Qt3DCore", - "--exclude-module", "PySide6.Qt3DRender", - "--exclude-module", "PySide6.Qt3DInput", - "--exclude-module", "PySide6.Qt3DLogic", - "--exclude-module", "PySide6.Qt3DAnimation", - "--exclude-module", "PySide6.Qt3DExtras", - "main.py" - ] - - try: - # Remove previous build directories - if os.path.exists('dist'): - os.system("rmdir /s /q dist") - if os.path.exists('build'): - os.system("rmdir /s /q build") - if os.path.exists('__pycache__'): - os.system("rmdir /s /q __pycache__") - - # Run PyInstaller - subprocess.run(pyinstaller_command, check=True) - print("Build completed successfully!") - - # Clean up unnecessary files - if os.path.exists('build'): - os.system("rmdir /s /q build") - if os.path.exists('__pycache__'): - os.system("rmdir /s /q __pycache__") - - # No need to copy data files manually since they are included - # in the executable using --add-data - - except subprocess.CalledProcessError as e: - print(f"Build failed with error: {e}") - sys.exit(1) - -if __name__ == "__main__": +import os +import shutil +import subprocess +import sys + + +def run_pyinstaller_build(): + base_dir = os.path.dirname(os.path.abspath(__file__)) + + icon_name = "app_icon.ico" if sys.platform.startswith("win") else "app_icon.png" + icon_path = os.path.join("icons", icon_name) + data_separator = ";" if sys.platform.startswith("win") else ":" + + # Ensure compiled locale files are available in one-file builds. + add_data_locales = f"locales{data_separator}locales" + + pyinstaller_command = [ + "pyinstaller", + "--onefile", + "--windowed", + f"--icon={icon_path}", + "--name=Writing Tools", + "--clean", + "--noconfirm", + "--add-data", add_data_locales, + # Exclude unnecessary modules + "--exclude-module", "tkinter", + "--exclude-module", "unittest", + "--exclude-module", "IPython", + "--exclude-module", "jedi", + "--exclude-module", "email_validator", + # NOTE: do NOT exclude `cryptography` - google-genai auth paths rely + # on it during `genai.Client()` construction. + "--exclude-module", "psutil", + "--exclude-module", "pyzmq", + "--exclude-module", "tornado", + # Exclude modules related to PySide6 that are not used + "--exclude-module", "PySide6.QtNetwork", + "--exclude-module", "PySide6.QtXml", + "--exclude-module", "PySide6.QtQml", + "--exclude-module", "PySide6.QtQuick", + "--exclude-module", "PySide6.QtQuickWidgets", + "--exclude-module", "PySide6.QtPrintSupport", + "--exclude-module", "PySide6.QtSql", + "--exclude-module", "PySide6.QtTest", + "--exclude-module", "PySide6.QtSvg", + "--exclude-module", "PySide6.QtSvgWidgets", + "--exclude-module", "PySide6.QtHelp", + "--exclude-module", "PySide6.QtMultimedia", + "--exclude-module", "PySide6.QtMultimediaWidgets", + "--exclude-module", "PySide6.QtOpenGL", + "--exclude-module", "PySide6.QtOpenGLWidgets", + "--exclude-module", "PySide6.QtPositioning", + "--exclude-module", "PySide6.QtLocation", + "--exclude-module", "PySide6.QtSerialPort", + "--exclude-module", "PySide6.QtWebChannel", + "--exclude-module", "PySide6.QtWebSockets", + "--exclude-module", "PySide6.QtWinExtras", + "--exclude-module", "PySide6.QtNetworkAuth", + "--exclude-module", "PySide6.QtRemoteObjects", + "--exclude-module", "PySide6.QtTextToSpeech", + "--exclude-module", "PySide6.QtWebEngineCore", + "--exclude-module", "PySide6.QtWebEngineWidgets", + "--exclude-module", "PySide6.QtWebEngine", + "--exclude-module", "PySide6.QtBluetooth", + "--exclude-module", "PySide6.QtNfc", + "--exclude-module", "PySide6.QtWebView", + "--exclude-module", "PySide6.QtCharts", + "--exclude-module", "PySide6.QtDataVisualization", + "--exclude-module", "PySide6.QtPdf", + "--exclude-module", "PySide6.QtPdfWidgets", + "--exclude-module", "PySide6.QtQuick3D", + "--exclude-module", "PySide6.QtQuickControls2", + "--exclude-module", "PySide6.QtQuickParticles", + "--exclude-module", "PySide6.QtQuickTest", + "--exclude-module", "PySide6.QtQuickWidgets", + "--exclude-module", "PySide6.QtSensors", + "--exclude-module", "PySide6.QtStateMachine", + "--exclude-module", "PySide6.Qt3DCore", + "--exclude-module", "PySide6.Qt3DRender", + "--exclude-module", "PySide6.Qt3DInput", + "--exclude-module", "PySide6.Qt3DLogic", + "--exclude-module", "PySide6.Qt3DAnimation", + "--exclude-module", "PySide6.Qt3DExtras", + "main.py" + ] + + try: + # Remove previous build directories in a cross-platform way. + for folder in ("dist", "build", "__pycache__"): + target = os.path.join(base_dir, folder) + if os.path.exists(target): + shutil.rmtree(target) + + # Run PyInstaller + subprocess.run(pyinstaller_command, check=True, cwd=base_dir) + print("Build completed successfully!") + + # Clean up intermediate build folders. + for folder in ("build", "__pycache__"): + target = os.path.join(base_dir, folder) + if os.path.exists(target): + shutil.rmtree(target) + + except subprocess.CalledProcessError as e: + print(f"Build failed with error: {e}") + sys.exit(1) + +if __name__ == "__main__": run_pyinstaller_build() \ No newline at end of file