A borderless Windows desktop slideshow, a "digital picture frame on your
desktop." It scans a folder (recursively) for images, cycles through them at a
configurable interval with your choice of 40+ transitions, and stays out of the
way in the system tray. Built in Python with PySide6, shipped as a single-file
.exe.
Status: v1.0, feature complete (Milestone 4). See Docs/milestone-log.md for the milestone history.
Download / build SlideshowWidget.exe (see Building below) and
double-click it. No install, no Python required, it's a single portable file.
On first launch, move the mouse to the top-right and open Settings (โ), then pick your image folder. Everything you change is saved immediately, so the next launch goes straight to your slideshow.
First-run note (SmartScreen): because the binary isn't code-signed, Windows SmartScreen may show an "unrecognized app" prompt the first time you run it (More info โ Run anyway). This is a reputation prompt, not a virus detection, see Antivirus / SmartScreen.
Prerequisites: Python 3.10+ and uv.
uv sync # create the venv from uv.lock
uv run python -m app # launch
A test/ folder at the repo root (gitignored, supply your own images) is used
as the first-run default if you haven't chosen a folder yet.
The window is borderless (no title bar). Move the mouse near the top-right to reveal the control cluster; it fades when the cursor leaves.
| Control | Action |
|---|---|
| ๐ Folder | Open the current image in Explorer |
| ๐ Pin | Pin/unpin the current image (stops the cycle on it) |
| โฎ / โฏ | Previous / Next |
| โฏ Pause | Pause / resume the auto-cycle (stays lit while paused) |
| ๐ Remove | Drop the current image from rotation (file is not deleted) |
| โ Settings | Open the settings dialog |
Window: drag anywhere to move, drag edges to resize, Win+Arrow to snap to Windows zones, double-click to maximize.
Keyboard (all rebindable in Settings โ Hotkeys): โ/โ next/prev,
Space pause, Del remove, P pin, S settings. Esc hides to tray (or
quits if no tray); it's intentionally fixed so a borderless window is never
un-closable.
System tray: Esc / Alt-F4 hide the window to the tray (the slideshow keeps
running there). Click the tray icon to restore; right-click for Show/Hide,
Play/Pause, Next, Previous, Settings, and Exit.
Open with the โ button. Changes apply and save instantly (no OK/Apply). Organized into four tabs:
General
- Folder: the image directory (scanned recursively). A checkbox tree below it lets you exclude individual subfolders at any depth.
- Duration: seconds each image is shown (after its transition completes).
- Scaling: Fit (letterbox) / Fill (crop) / Stretch.
- Transition + Speed: the slide-change animation (40+ styles; see Transitions) and how long it takes.
- Order: Sequential / Shuffle / Stratified (folder-aware interleave).
- Show date: overlay the photo's EXIF capture date in the corner.
- Keep pause on nav: whether manual prev/next clears a pause.
- Launch at startup (Windows): run the slideshow when you log in.
- Screensaver + idle minutes: see Screensaver mode.
- Remove all app data: wipe everything this app stored (see below).
Removed: every image you've removed from rotation, most-recent first. Double-click to preview; Restore to rotation brings one back.
Hotkeys: rebind any action; click a key, press the combo. Duplicate bindings are rejected. Reset to defaults available.
About: version, license, Show README, third-party attributions (with a viewer for each license), disclaimers, and a Cached image folders list (forget a folder's cached state without touching its images).
Everything lives in %APPDATA%\SlideshowWidget\:
settings.json: all preferences (atomic write; a corrupt file is backed up tosettings.json.badand defaults are used, so it never fails to start).library.db: SQLite cache: known images, the removed/blacklist list, the pinned image, per-folder state, and the shuffle position (so a restart resumes mid-cycle).
The Remove all app data button (Settings โ General) deletes this folder, the
launch-at-startup registry entry, and all cached folder state. Your image files
are never touched; the .exe is portable, so you delete it yourself.
Display
- Borderless, resizable, Windows-snap-aware window (not fullscreen).
- Random or sequential cycling at a configurable interval.
- Recursive scan of
jpg,jpeg,png,webp,gif,bmp. - EXIF orientation auto-correction (portrait photos display upright).
- Fit / Fill / Stretch scaling.
Transitions
- 40+ styles across 10 families: Fade (cut / crossfade / through-black), Slide, Push (filmstrip), Reveal, Wipe (each in 4 directions + random), Zoom (in/out), Spin & Rotate (CW/CCW), Squish, Carousel (perspective 3D card flip), and a top-level Random that picks a different one each time.
- Configurable duration, independent of the slide interval. Easing is tuned per family (smooth S-curve for motion, linear for fades).
Dynamic updates
watchdogdetects images added/removed from the folder live: no restart.- Resilient to the folder going away (drive unplugged): a banner shows and the app retries, without losing your removed-list or other state.
Startup
- A Launch at startup toggle writes/removes an
HKCU\โฆ\Runregistry entry. (Manual alternative: put a shortcut to the.exeinshell:startup.)
Tray, hotkeys, and polish
- Minimize-to-tray with a full right-click menu.
- Fully rebindable hotkeys.
- Pin an image, exclude subfolders, open-in-Explorer, restore removed images.
The assignment asks for at least two; this ships five:
- Smooth crossfade between images, and 40+ other transition styles built on the same engine.
- Configurable hotkeys, a dedicated Hotkeys tab; every action rebindable,
stored in
settings.json. - Persistent blacklist for removed images, removals survive restarts (SQLite), with a Removed tab to view/restore them.
- Multi-monitor awareness, the screensaver fills every connected monitor, each showing a different image; the windowed slideshow targets the screen it's on and restores its exact placement after the screensaver.
- Memory-conscious image loading, a small bounded preload cache decodes at
display size (not native), uses Pillow's
Image.draft()for fractional JPEG decode, and never holds the whole library in RAM, so memory stays bounded no matter how large the folder is.
The headline bonus feature. After a configurable idle period (Settings โ General โ Screensaver), the app goes borderless-fullscreen on every monitor, hides the cursor and controls, and cycles images with your configured transition, one distinct photo per screen. Any keyboard/mouse input restores the windowed slideshow exactly where it left off.
It behaves like a real screensaver:
- Idle detection is system-wide (
GetLastInputInfo), not app-local. - Audio-aware: it won't kick in while an application is playing sound (so a movie or music isn't interrupted); notification dings are ignored.
- Good citizen: it only suppresses the Windows screensaver / display-sleep while it's showing; at all other times your power settings are untouched.
Requires the dev dependencies (uv sync installs Nuitka). From the repo root:
.\build.ps1 # onefile build -> dist\SlideshowWidget.exe
.\build.ps1 -Compiler msvc # use the MSVC backend (recommended; fewer AV flags)
.\build.ps1 -Standalone # folder build instead of single-file
The script invokes Nuitka with (in essence):
python -m nuitka --onefile --enable-plugin=pyside6
--include-package=PIL --include-package-data=PIL
--include-package=comtypes --include-package=pycaw
--include-data-dir=assets=assets
--include-data-dir=LICENSES=LICENSES
--include-data-files=LICENSE=LICENSE
--include-data-files=app.ico=app.ico
--windows-console-mode=disable
--windows-icon-from-ico=app.ico
--company-name="TNTGuerrilla" --product-name="Slideshow Widget"
--file-version=1.0.0.0 --product-version=1.0.0.0
--onefile-tempdir-spec="{CACHE_DIR}\SlideshowWidget\1.0.0"
--lto=auto --output-dir=dist --output-filename=SlideshowWidget.exe
run.py
See build.ps1 for the exact, commented command. The app icon is
generated from the tray glyph by uv run python tools/make_icon.py.
Test the build on a clean Windows VM (no Python, no build tools) before relying on it, missing-DLL problems only surface there.
Nuitka is used precisely because it produces a real compiled binary, which Windows Defender flags far less than PyInstaller's unpack-and-reexec bootloader. The build also carries full version/company metadata, a real icon, never requests admin elevation, and unpacks to a stable cached directory, all of which reduce heuristic false positives.
The one thing that can't be fixed without a code-signing certificate is the
SmartScreen "unrecognized app" prompt on a brand-new binary. That's a reputation
prompt, not a virus detection; it clears once the binary is signed or has been
downloaded enough. To sign it yourself: signtool sign /fd SHA256 /a dist\SlideshowWidget.exe.
The GUI framework is Qt 6 via PySide6 (the official Qt for Python binding),
chosen over PyQt6 for license freedom: PySide6 is LGPL v3, so this
application's own code can be licensed however the author chooses (here, MIT),
whereas PyQt6 is GPL v3 and would force the whole app to be GPL. Functionally the
two are interchangeable; the slideshow uses only standard Qt features. The
lighter pyside6-essentials distribution is used (no 3D/Charts/WebEngine).
Other choices: Pillow for image decode (C-backed codecs, Image.draft() for
fast JPEG), watchdog for live file detection, SQLite (WAL) for the cache,
pycaw for the screensaver's audio detection, and Nuitka for packaging.
Full rationale in Docs/tech-stack.md.
This project's own code is released under the MIT License (see LICENSE).
It is distributed with third-party components under their own licenses, with full texts bundled in LICENSES/ and viewable in-app (About tab):
| Component | License |
|---|---|
| PySide6 / Qt | LGPL v3 (see LICENSES/PySide6-NOTICE.txt for the source-offer + re-linking notice) |
| Pillow | HPND |
| watchdog | Apache 2.0 |
| pycaw, comtypes | MIT |
LGPL note: the Qt/PySide6 binaries ship as dynamically-loadable modules; a user may substitute their own compatible build of PySide6/Qt. Qt source is at https://code.qt.io/cgit/pyside/pyside-setup.git/.
app/ Application source
__main__.py Entry point (settings โ library โ cache โ window wiring)
window.py Borderless main window, nav cluster, tray, date overlay
stage.py Shared two-label transition surface (window + screensaver)
transitions.py 40+ transition styles + animated label
screensaver.py Idle/audio detection + multi-monitor screensaver
cache.py Decoded-image preload cache (memory-conscious)
decoder.py Background decode (Pillow / Qt) + EXIF
library.py DB + queue + scanner + watcher coordination
shuffle.py Sequential / shuffle / stratified ordering
db.py SQLite cache (images, queue, roots)
config.py JSON settings (atomic, tolerant, migrating)
settings_dialog.py Tabbed settings (General / Removed / Hotkeys / About)
tray.py System-tray icon + menu
startup.py Launch-at-startup registry helper
shell.py Explorer / default-app open helpers
watcher.py watchdog observer โ debounced GUI events
win32.py Borderless chrome (resize / snap / drag)
nav_widget.py Control cluster toggle_switch.py Material toggle
build.ps1 Nuitka build script run.py build entry point
tools/make_icon.py Generates app.ico from the tray glyph
Docs/ Spec, phase plans, tech stack, milestone log, known issues
LICENSES/ Bundled third-party license texts
- Assignment spec, requirements + scoring
- Tech stack & architecture
- Milestone log, completion dates
- Phase plans, what was planned vs. shipped
- Known issues