7 Commits

Author SHA1 Message Date
3633793972 fix: extract tkinter from Python installer via 7z, fix NSIS icon path
Some checks failed
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 15s
Build Release / build-linux (push) Successful in 1m20s
Build Release / build-docker (push) Failing after 9s
Build Release / build-windows (push) Successful in 3m19s
- Replace nuget approach (doesn't contain tkinter) with extracting
  from the official Python amd64.exe installer using 7z
- Remove MUI_ICON/MUI_UNICON (no .ico file available, use NSIS default)
- Add p7zip-full to CI dependencies
2026-03-22 03:40:06 +03:00
7f799a914d feat: add NSIS Windows installer to release workflow
Some checks failed
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 15s
Build Release / build-linux (push) Successful in 1m21s
Build Release / build-docker (push) Failing after 9s
Build Release / build-windows (push) Failing after 1m37s
- installer.nsi: per-user install to AppData, Start Menu shortcuts,
  optional desktop shortcut and autostart, clean uninstall (preserves
  data/), Add/Remove Programs registration
- build-dist-windows.sh: runs makensis after ZIP if available
- release.yml: install nsis in CI, upload both ZIP and setup.exe
- Fix Docker registry login (sed -E for https:// stripping)
2026-03-22 03:35:34 +03:00
d5b5c255e8 feat: bundle tkinter into Windows portable build
Some checks failed
Build Release / create-release (push) Successful in 5s
Lint & Test / test (push) Failing after 21s
Build Release / build-linux (push) Successful in 1m7s
Build Release / build-docker (push) Failing after 4s
Build Release / build-windows (push) Successful in 1m50s
Download _tkinter.pyd, tkinter package, and Tcl/Tk DLLs from the
official Python nuget package and copy them into the embedded Python
directory. This enables the screen overlay visualization during
calibration in the portable build.
2026-03-22 03:32:02 +03:00
564e4c9c9c fix: accurate port banner and tkinter graceful fallback
Some checks failed
Build Release / build-linux (push) Successful in 1m24s
Build Release / create-release (push) Successful in 1s
Lint & Test / test (push) Failing after 13s
Build Release / build-windows (push) Successful in 1m35s
Build Release / build-docker (push) Failing after 9s
- Move startup banner into main.py so it shows the actual configured
  port instead of a hardcoded 8080 in the launcher scripts
- Wrap tkinter import in try/except so embedded Python (which lacks
  tkinter) logs a warning instead of crashing the overlay thread
2026-03-22 03:30:19 +03:00
7c80500d48 feat: add autostart scripts and fix port configuration in launchers
Some checks failed
Build Release / create-release (push) Successful in 1s
Build Release / build-windows (push) Successful in 1m16s
Build Release / build-docker (push) Failing after 8s
Build Release / build-linux (push) Successful in 59s
Lint & Test / test (push) Successful in 1m52s
Windows: install-autostart.bat (Startup folder shortcut),
uninstall-autostart.bat. Linux: install-service.sh (systemd unit),
uninstall-service.sh.

Both launchers now use python -m wled_controller.main so port is
read from config/env instead of being hardcoded to 8080.
2026-03-22 03:25:05 +03:00
39e3d64654 fix: replace Docker Buildx with plain docker build/push
All checks were successful
Lint & Test / test (push) Successful in 1m51s
Buildx requires container networking that fails on TrueNAS runners.
Plain docker build + docker push works without Buildx setup.
2026-03-22 03:20:37 +03:00
47a62b1aed fix: add app/src to embedded Python ._pth for module discovery
Some checks failed
Build Release / create-release (push) Successful in 1s
Build Release / build-windows (push) Successful in 1m14s
Lint & Test / test (push) Successful in 1m50s
Build Release / build-docker (push) Failing after 46s
Build Release / build-linux (push) Successful in 1m37s
Windows embedded Python ignores PYTHONPATH when a ._pth file exists.
Add ../app/src to the ._pth so wled_controller is importable.
Fixes ModuleNotFoundError on portable builds.
2026-03-22 03:14:12 +03:00
7 changed files with 483 additions and 63 deletions

View File

@@ -63,37 +63,50 @@ jobs:
- name: Install system dependencies - name: Install system dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y --no-install-recommends zip libportaudio2 sudo apt-get install -y --no-install-recommends zip libportaudio2 nsis p7zip-full
- name: Cross-build Windows distribution - name: Cross-build Windows distribution
run: | run: |
chmod +x build-dist-windows.sh chmod +x build-dist-windows.sh
./build-dist-windows.sh "${{ gitea.ref_name }}" ./build-dist-windows.sh "${{ gitea.ref_name }}"
- name: Upload build artifact - name: Upload build artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: LedGrab-${{ gitea.ref_name }}-win-x64 name: LedGrab-${{ gitea.ref_name }}-win-x64
path: build/LedGrab-*.zip path: |
build/LedGrab-*.zip
build/LedGrab-*-setup.exe
retention-days: 90 retention-days: 90
- name: Attach ZIP to release - name: Attach assets to release
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: | run: |
TAG="${{ gitea.ref_name }}"
RELEASE_ID="${{ needs.create-release.outputs.release_id }}" RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
ZIP_NAME=$(basename "$ZIP_FILE")
# Upload ZIP
ZIP_FILE=$(ls build/LedGrab-*.zip | head -1)
if [ -f "$ZIP_FILE" ]; then
curl -s -X POST \ curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$ZIP_NAME" \ "$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$ZIP_FILE")" \
-H "Authorization: token $GITEA_TOKEN" \ -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary "@$ZIP_FILE" --data-binary "@$ZIP_FILE"
echo "Uploaded: $(basename "$ZIP_FILE")"
fi
echo "Uploaded: $ZIP_NAME" # Upload installer
SETUP_FILE=$(ls build/LedGrab-*-setup.exe 2>/dev/null | head -1)
if [ -f "$SETUP_FILE" ]; then
curl -s -X POST \
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$SETUP_FILE")" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$SETUP_FILE"
echo "Uploaded: $(basename "$SETUP_FILE")"
fi
# ── Linux tarball ────────────────────────────────────────── # ── Linux tarball ──────────────────────────────────────────
build-linux: build-linux:
@@ -160,43 +173,51 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ gitea.server_url }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract version metadata - name: Extract version metadata
id: meta id: meta
run: | run: |
TAG="${{ gitea.ref_name }}" TAG="${{ gitea.ref_name }}"
VERSION="${TAG#v}" VERSION="${TAG#v}"
REGISTRY="${{ gitea.server_url }}/${{ gitea.repository }}" # Strip protocol and lowercase for Docker registry path
# Lowercase the registry path (Docker requires it) SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed -E 's|https?://||')
REGISTRY=$(echo "$REGISTRY" | tr '[:upper:]' '[:lower:]' | sed 's|https\?://||') REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
REGISTRY="${SERVER_HOST}/${REPO}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"
echo "server_host=$SERVER_HOST" >> "$GITHUB_OUTPUT"
# Build tag list: version + latest (only for stable releases) - name: Login to Gitea Container Registry
TAGS="$REGISTRY:$TAG,$REGISTRY:$VERSION" run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login \
"${{ steps.meta.outputs.server_host }}" \
-u "${{ gitea.actor }}" --password-stdin
- name: Build Docker image
run: |
TAG="${{ gitea.ref_name }}"
REGISTRY="${{ steps.meta.outputs.registry }}"
docker build \
--label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
--label "org.opencontainers.image.revision=${{ gitea.sha }}" \
-t "$REGISTRY:$TAG" \
-t "$REGISTRY:${{ steps.meta.outputs.version }}" \
./server
# Tag as latest only for stable releases
if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
TAGS="$TAGS,$REGISTRY:latest" docker tag "$REGISTRY:$TAG" "$REGISTRY:latest"
fi fi
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
- name: Build and push Docker image - name: Push Docker image
uses: docker/build-push-action@v5 run: |
with: TAG="${{ gitea.ref_name }}"
context: ./server REGISTRY="${{ steps.meta.outputs.registry }}"
push: true
tags: ${{ steps.meta.outputs.tags }} docker push "$REGISTRY:$TAG"
labels: | docker push "$REGISTRY:${{ steps.meta.outputs.version }}"
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
org.opencontainers.image.revision=${{ gitea.sha }} if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
cache-from: type=gha docker push "$REGISTRY:latest"
cache-to: type=gha,mode=max fi

View File

@@ -73,13 +73,101 @@ if [ -z "$PTH_FILE" ]; then
exit 1 exit 1
fi fi
# Uncomment 'import site' and add Lib\site-packages # Uncomment 'import site', add Lib\site-packages and app source path
sed -i 's/^#\s*import site/import site/' "$PTH_FILE" sed -i 's/^#\s*import site/import site/' "$PTH_FILE"
if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then if ! grep -q 'Lib\\site-packages' "$PTH_FILE"; then
echo 'Lib\site-packages' >> "$PTH_FILE" echo 'Lib\site-packages' >> "$PTH_FILE"
fi fi
# Embedded Python ._pth overrides PYTHONPATH, so we must add the app
# source directory here for wled_controller to be importable
if ! grep -q '\.\./app/src' "$PTH_FILE"; then
echo '../app/src' >> "$PTH_FILE"
fi
echo " Patched $(basename "$PTH_FILE")" echo " Patched $(basename "$PTH_FILE")"
# ── Bundle tkinter into embedded Python ───────────────────────
# Embedded Python doesn't include tkinter. We extract it from the
# official Windows installer (amd64.exe) which contains all components
# as MSI cab files.
echo "[3b/8] Bundling tkinter for screen overlay support..."
# Download the Windows installer (not the embed zip — the full one)
INSTALLER_URL="https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-amd64.exe"
INSTALLER_PATH="$BUILD_DIR/python-installer.exe"
if [ ! -f "$INSTALLER_PATH" ]; then
curl -sL "$INSTALLER_URL" -o "$INSTALLER_PATH"
fi
# The installer is a bundle of MSI/CAB files. We can extract with 7z or
# msiextract. The tkinter components are in the 'tcltk' feature.
TK_EXTRACT="$BUILD_DIR/tk-extract"
rm -rf "$TK_EXTRACT"
mkdir -p "$TK_EXTRACT"
if command -v 7z &>/dev/null; then
# Extract all cab files from the installer
7z x -o"$TK_EXTRACT/installer" "$INSTALLER_PATH" -y >/dev/null 2>&1 || true
# Find and extract the tcltk cab
for cab in "$TK_EXTRACT/installer"/tcltk*.msi "$TK_EXTRACT/installer"/tcltk*; do
[ -f "$cab" ] || continue
7z x -o"$TK_EXTRACT/tcltk" "$cab" -y >/dev/null 2>&1 || true
done
# Find and extract the lib cab (contains tkinter Python package)
for cab in "$TK_EXTRACT/installer"/lib*.msi "$TK_EXTRACT/installer"/lib*; do
[ -f "$cab" ] || continue
7z x -o"$TK_EXTRACT/lib" "$cab" -y >/dev/null 2>&1 || true
done
# Copy _tkinter.pyd
TKINTER_PYD=$(find "$TK_EXTRACT" -name "_tkinter.pyd" 2>/dev/null | head -1)
if [ -n "$TKINTER_PYD" ]; then
cp "$TKINTER_PYD" "$PYTHON_DIR/DLLs/" 2>/dev/null || cp "$TKINTER_PYD" "$PYTHON_DIR/"
echo " Copied _tkinter.pyd"
else
echo " WARNING: _tkinter.pyd not found"
fi
# Copy Tcl/Tk DLLs
for dll in tcl86t.dll tk86t.dll zlib1.dll; do
DLL_PATH=$(find "$TK_EXTRACT" -name "$dll" 2>/dev/null | head -1)
if [ -n "$DLL_PATH" ]; then
cp "$DLL_PATH" "$PYTHON_DIR/"
echo " Copied $dll"
fi
done
# Copy tkinter Python package
TKINTER_PKG=$(find "$TK_EXTRACT" -type d -name "tkinter" 2>/dev/null | head -1)
if [ -n "$TKINTER_PKG" ]; then
mkdir -p "$PYTHON_DIR/Lib"
cp -r "$TKINTER_PKG" "$PYTHON_DIR/Lib/tkinter"
echo " Copied tkinter/ package"
fi
# Copy tcl/tk data directories
for tcldir in tcl8.6 tk8.6; do
TCL_PATH=$(find "$TK_EXTRACT" -type d -name "$tcldir" 2>/dev/null | head -1)
if [ -n "$TCL_PATH" ]; then
cp -r "$TCL_PATH" "$PYTHON_DIR/$tcldir"
echo " Copied $tcldir/"
fi
done
echo " tkinter bundled successfully"
else
echo " WARNING: 7z not found — skipping tkinter bundling (install p7zip-full)"
fi
# Add Lib to ._pth so tkinter package is importable
if ! grep -q '^Lib$' "$PTH_FILE"; then
echo 'Lib' >> "$PTH_FILE"
fi
rm -rf "$TK_EXTRACT"
# ── Download pip and install into embedded Python ──────────── # ── Download pip and install into embedded Python ────────────
echo "[4/8] Installing pip into embedded Python..." echo "[4/8] Installing pip into embedded Python..."
@@ -242,16 +330,8 @@ set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml
if not exist "%~dp0data" mkdir "%~dp0data" if not exist "%~dp0data" mkdir "%~dp0data"
if not exist "%~dp0logs" mkdir "%~dp0logs" if not exist "%~dp0logs" mkdir "%~dp0logs"
echo. :: Start the server — reads port from config, prints its own banner
echo ============================================= "%~dp0python\python.exe" -m wled_controller.main
echo LedGrab v${VERSION_CLEAN}
echo Open http://localhost:8080 in your browser
echo =============================================
echo.
:: Start the server (open browser after short delay)
start "" /b cmd /c "timeout /t 2 /nobreak >nul && start http://localhost:8080"
"%~dp0python\python.exe" -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080
pause pause
LAUNCHER LAUNCHER
@@ -259,6 +339,64 @@ LAUNCHER
# Convert launcher to Windows line endings # Convert launcher to Windows line endings
sed -i 's/$/\r/' "$DIST_DIR/LedGrab.bat" sed -i 's/$/\r/' "$DIST_DIR/LedGrab.bat"
# ── Create autostart scripts ─────────────────────────────────
cat > "$DIST_DIR/install-autostart.bat" << 'AUTOSTART'
@echo off
:: Install LedGrab to start automatically on Windows login
:: Creates a shortcut in the Startup folder
set SHORTCUT_NAME=LedGrab
set STARTUP_DIR=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
set TARGET=%~dp0LedGrab.bat
set SHORTCUT=%STARTUP_DIR%\%SHORTCUT_NAME%.lnk
echo Installing LedGrab autostart...
:: Use PowerShell to create a proper shortcut
powershell -NoProfile -Command ^
"$ws = New-Object -ComObject WScript.Shell; ^
$sc = $ws.CreateShortcut('%SHORTCUT%'); ^
$sc.TargetPath = '%TARGET%'; ^
$sc.WorkingDirectory = '%~dp0'; ^
$sc.WindowStyle = 7; ^
$sc.Description = 'LedGrab ambient lighting server'; ^
$sc.Save()"
if exist "%SHORTCUT%" (
echo.
echo [OK] LedGrab will start automatically on login.
echo Shortcut: %SHORTCUT%
echo.
echo To remove: run uninstall-autostart.bat
) else (
echo.
echo [ERROR] Failed to create shortcut.
)
pause
AUTOSTART
sed -i 's/$/\r/' "$DIST_DIR/install-autostart.bat"
cat > "$DIST_DIR/uninstall-autostart.bat" << 'UNAUTOSTART'
@echo off
:: Remove LedGrab from Windows startup
set SHORTCUT=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\LedGrab.lnk
if exist "%SHORTCUT%" (
del "%SHORTCUT%"
echo.
echo [OK] LedGrab autostart removed.
) else (
echo.
echo LedGrab autostart was not installed.
)
pause
UNAUTOSTART
sed -i 's/$/\r/' "$DIST_DIR/uninstall-autostart.bat"
# ── Create ZIP ─────────────────────────────────────────────── # ── Create ZIP ───────────────────────────────────────────────
ZIP_PATH="$BUILD_DIR/$ZIP_NAME" ZIP_PATH="$BUILD_DIR/$ZIP_NAME"
@@ -267,8 +405,29 @@ rm -f "$ZIP_PATH"
(cd "$BUILD_DIR" && zip -rq "$ZIP_NAME" "$DIST_NAME") (cd "$BUILD_DIR" && zip -rq "$ZIP_NAME" "$DIST_NAME")
ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1) ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1)
# ── Build NSIS installer (if makensis is available) ──────────
SETUP_NAME="LedGrab-v${VERSION_CLEAN}-win-x64-setup.exe"
SETUP_PATH="$BUILD_DIR/$SETUP_NAME"
if command -v makensis &>/dev/null; then
echo "[9/8] Building NSIS installer..."
makensis -DVERSION="${VERSION_CLEAN}" "$SCRIPT_DIR/installer.nsi" >/dev/null 2>&1
if [ -f "$SETUP_PATH" ]; then
SETUP_SIZE=$(du -h "$SETUP_PATH" | cut -f1)
echo " Installer: $SETUP_PATH ($SETUP_SIZE)"
else
echo " WARNING: makensis ran but installer not found at $SETUP_PATH"
fi
else
echo "[9/8] Skipping installer (makensis not found — install nsis to enable)"
fi
echo "" echo ""
echo "=== Build complete ===" echo "=== Build complete ==="
echo " Archive: $ZIP_PATH" echo " ZIP: $ZIP_PATH ($ZIP_SIZE)"
echo " Size: $ZIP_SIZE" if [ -f "$SETUP_PATH" ]; then
echo " Installer: $SETUP_PATH ($SETUP_SIZE)"
fi
echo "" echo ""

View File

@@ -106,6 +106,11 @@ $pthContent = $pthContent -replace '#\s*import site', 'import site'
if ($pthContent -notmatch 'Lib\\site-packages') { if ($pthContent -notmatch 'Lib\\site-packages') {
$pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n" $pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n"
} }
# Embedded Python ._pth overrides PYTHONPATH, so add the app source path
# directly for wled_controller to be importable
if ($pthContent -notmatch '\.\.[/\\]app[/\\]src') {
$pthContent = $pthContent.TrimEnd() + "`n..\app\src`n"
}
Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline
Write-Host " Patched $($pthFile.Name)" Write-Host " Patched $($pthFile.Name)"

View File

@@ -103,20 +103,100 @@ export WLED_CONFIG_PATH="$SCRIPT_DIR/app/config/default_config.yaml"
mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs" mkdir -p "$SCRIPT_DIR/data" "$SCRIPT_DIR/logs"
echo ""
echo " ============================================="
echo " LedGrab vVERSION_PLACEHOLDER"
echo " Open http://localhost:8080 in your browser"
echo " ============================================="
echo ""
source "$SCRIPT_DIR/venv/bin/activate" source "$SCRIPT_DIR/venv/bin/activate"
exec python -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080 exec python -m wled_controller.main
LAUNCHER LAUNCHER
sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh" sed -i "s/VERSION_PLACEHOLDER/${VERSION_CLEAN}/" "$DIST_DIR/run.sh"
chmod +x "$DIST_DIR/run.sh" chmod +x "$DIST_DIR/run.sh"
# ── Create autostart scripts ─────────────────────────────────
cat > "$DIST_DIR/install-service.sh" << 'SERVICE_INSTALL'
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVICE_NAME="ledgrab"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
RUN_SCRIPT="$SCRIPT_DIR/run.sh"
CURRENT_USER="$(whoami)"
if [ "$EUID" -ne 0 ] && [ "$CURRENT_USER" != "root" ]; then
echo "This script requires root privileges. Re-running with sudo..."
exec sudo "$0" "$@"
fi
# Resolve the actual user (not root) when run via sudo
ACTUAL_USER="${SUDO_USER:-$CURRENT_USER}"
ACTUAL_HOME=$(eval echo "~$ACTUAL_USER")
echo "Installing LedGrab systemd service..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=LedGrab ambient lighting server
After=network.target
[Service]
Type=simple
User=$ACTUAL_USER
WorkingDirectory=$SCRIPT_DIR
ExecStart=$RUN_SCRIPT
Restart=on-failure
RestartSec=5
Environment=HOME=$ACTUAL_HOME
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl start "$SERVICE_NAME"
echo ""
echo " [OK] LedGrab service installed and started."
echo ""
echo " Commands:"
echo " sudo systemctl status $SERVICE_NAME # Check status"
echo " sudo systemctl stop $SERVICE_NAME # Stop"
echo " sudo systemctl restart $SERVICE_NAME # Restart"
echo " sudo journalctl -u $SERVICE_NAME -f # View logs"
echo ""
echo " To remove: run ./uninstall-service.sh"
SERVICE_INSTALL
chmod +x "$DIST_DIR/install-service.sh"
cat > "$DIST_DIR/uninstall-service.sh" << 'SERVICE_UNINSTALL'
#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="ledgrab"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [ "$EUID" -ne 0 ] && [ "$(whoami)" != "root" ]; then
echo "This script requires root privileges. Re-running with sudo..."
exec sudo "$0" "$@"
fi
if [ ! -f "$SERVICE_FILE" ]; then
echo "LedGrab service is not installed."
exit 0
fi
echo "Removing LedGrab systemd service..."
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo ""
echo " [OK] LedGrab service removed."
SERVICE_UNINSTALL
chmod +x "$DIST_DIR/uninstall-service.sh"
# ── Create tarball ─────────────────────────────────────────── # ── Create tarball ───────────────────────────────────────────
echo "[7/7] Creating $TAR_NAME..." echo "[7/7] Creating $TAR_NAME..."

146
installer.nsi Normal file
View File

@@ -0,0 +1,146 @@
; LedGrab NSIS Installer Script
; Cross-compilable on Linux: apt install nsis && makensis installer.nsi
;
; Expects the portable build to already exist at build/LedGrab/
; (run build-dist-windows.sh first)
!include "MUI2.nsh"
!include "FileFunc.nsh"
; ── Metadata ────────────────────────────────────────────────
!define APPNAME "LedGrab"
!define DESCRIPTION "Ambient lighting system — captures screen content and drives LED strips in real time"
!define VERSIONMAJOR 0
!define VERSIONMINOR 1
!define VERSIONBUILD 0
; Set from command line: makensis -DVERSION=0.1.0 installer.nsi
!ifndef VERSION
!define VERSION "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
!endif
Name "${APPNAME} v${VERSION}"
OutFile "build\${APPNAME}-v${VERSION}-win-x64-setup.exe"
InstallDir "$LOCALAPPDATA\${APPNAME}"
InstallDirRegKey HKCU "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel user
SetCompressor /SOLID lzma
; ── Modern UI Configuration ─────────────────────────────────
!define MUI_ABORTWARNING
; ── Pages ───────────────────────────────────────────────────
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; ── Installer Sections ──────────────────────────────────────
Section "!${APPNAME} (required)" SecCore
SectionIn RO
SetOutPath "$INSTDIR"
; Copy the entire portable build
File /r "build\LedGrab\python"
File /r "build\LedGrab\app"
File "build\LedGrab\LedGrab.bat"
; Create data and logs directories
CreateDirectory "$INSTDIR\data"
CreateDirectory "$INSTDIR\logs"
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
; Start Menu shortcuts
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
; Registry: install location + Add/Remove Programs entry
WriteRegStr HKCU "Software\${APPNAME}" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "Software\${APPNAME}" "Version" "${VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayName" "${APPNAME}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"DisplayVersion" "${VERSION}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"InstallLocation" "$INSTDIR"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"Publisher" "Alexei Dolgolyov"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"URLInfoAbout" "https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed"
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoModify" 1
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"NoRepair" 1
; Calculate installed size for Add/Remove Programs
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
"EstimatedSize" "$0"
SectionEnd
Section "Desktop shortcut" SecDesktop
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
SectionEnd
Section "Start with Windows" SecAutostart
CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" "$INSTDIR\LedGrab.bat" \
"" "" "" SW_SHOWMINIMIZED "" "${DESCRIPTION}"
SectionEnd
; ── Section Descriptions ────────────────────────────────────
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} \
"Install ${APPNAME} server and all required files."
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} \
"Create a shortcut on your desktop."
!insertmacro MUI_DESCRIPTION_TEXT ${SecAutostart} \
"Start ${APPNAME} automatically when you log in."
!insertmacro MUI_FUNCTION_DESCRIPTION_END
; ── Uninstaller ─────────────────────────────────────────────
Section "Uninstall"
; Remove shortcuts
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk"
Delete "$SMPROGRAMS\${APPNAME}\Uninstall.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
Delete "$DESKTOP\${APPNAME}.lnk"
Delete "$SMSTARTUP\${APPNAME}.lnk"
; Remove application files (but NOT data/ — preserve user config)
RMDir /r "$INSTDIR\python"
RMDir /r "$INSTDIR\app"
Delete "$INSTDIR\LedGrab.bat"
Delete "$INSTDIR\uninstall.exe"
; Remove logs (but keep data/)
RMDir /r "$INSTDIR\logs"
; Try to remove install dir (only succeeds if empty — data/ may remain)
RMDir "$INSTDIR"
; Remove registry keys
DeleteRegKey HKCU "Software\${APPNAME}"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
SectionEnd

View File

@@ -278,7 +278,12 @@ class OverlayManager:
def _start_tk_thread(self) -> None: def _start_tk_thread(self) -> None:
def _run(): def _run():
import tkinter as tk # lazy import — tkinter unavailable in headless CI try:
import tkinter as tk # lazy import — tkinter unavailable in embedded Python / headless CI
except ImportError:
logger.warning("tkinter not available — screen overlay disabled")
self._tk_ready.set()
return
try: try:
self._tk_root = tk.Tk() self._tk_root = tk.Tk()

View File

@@ -98,6 +98,10 @@ async def lifespan(app: FastAPI):
logger.info(f"Starting LED Grab v{__version__}") logger.info(f"Starting LED Grab v{__version__}")
logger.info(f"Python version: {sys.version}") logger.info(f"Python version: {sys.version}")
logger.info(f"Server listening on {config.server.host}:{config.server.port}") logger.info(f"Server listening on {config.server.host}:{config.server.port}")
print(f"\n =============================================")
print(f" LED Grab v{__version__}")
print(f" Open http://localhost:{config.server.port} in your browser")
print(f" =============================================\n")
# Validate authentication configuration # Validate authentication configuration
if not config.auth.api_keys: if not config.auth.api_keys: