Contributing Guide¶
Thanks for your interest in contributing! This guide covers everything you need to set up a development environment and submit changes.
Development Setup¶
Prerequisites¶
- Windows 10 (build 1903+) or Windows 11
- Python 3.13 — Download (check "Add to PATH")
- Git — Download
- VS Code (recommended) — Download
ARM64 Windows
Install the x64 edition of Python, not ARM64. Many dependencies (OpenCV, windows-capture) don't have ARM64 wheels. x64 Python runs fine via emulation.
Clone & Install¶
git clone https://github.com/sabbour/followcursor.git
cd followcursor/followcursor
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Or run .\scripts\Start-Dev.ps1 to do all of this and launch the app.
VS Code Integration¶
The repo includes VS Code configuration in .vscode/:
- F5 — Launch with debugger (debugpy attached)
- Ctrl+Shift+B — Build standalone
.exevia PyInstaller - Automation terminals use
cmd.exe(not WSL)
Repository Layout¶
followcursor/ <-- repo root (.git, .github, .vscode)
|-- .github/workflows/ <-- CI pipeline
|-- .vscode/ <-- VS Code tasks, launch, settings
|-- docs/ <-- Documentation (mkdocs-material site)
+-- followcursor/ <-- Python project root
|-- main.py <-- Entry point
|-- requirements.txt
|-- pytest.ini
|-- scripts/ <-- Build & infra PowerShell scripts
|-- tests/ <-- Unit tests (pytest)
+-- app/ <-- Application package
|-- version.py <-- Single source of truth for version
|-- models.py <-- Data classes
|-- tokens.py <-- Fluent 2 design tokens
|-- theme.py <-- QSS dark theme
+-- widgets/ <-- UI widgets
Note
.github/ and .vscode/ live at repo root, not inside followcursor/. Python project files live inside the followcursor/ subfolder.
Coding Conventions¶
Python¶
- Type hints on all function signatures
- Docstrings on classes and complex methods
- Python 3.10+ union syntax (
X | None) used directly - Logging via
loggingmodule — no bareprint(). Each module:logger = logging.getLogger(__name__)
Qt / PySide6¶
- All UI built with PySide6 widgets — no QML, no Qt Designer
.uifiles - Dark theme via QSS in
theme.py— not palette manipulation - Signals/slots for all inter-component communication
- Use
setPixelSize()for fonts — neversetPointSize()(avoids DPI issues)
Design Tokens¶
All colors, spacing, radii, and typography values come from app/tokens.py. Import as from . import tokens as T and reference token constants instead of hardcoding hex values or pixel sizes.
Naming¶
snake_casefor functions and variablesPascalCasefor classes- Private methods prefixed with
_ - Constants in
UPPER_SNAKE_CASE
Threading¶
- Background threads for: recording, export, input hooks, thumbnail generation
- Never access Qt widgets from background threads — use signals
- Win32 hooks use
WINFUNCTYPE(notCFUNCTYPE) for 64-bit compatibility
Testing¶
Test Suite¶
- Framework: pytest (configured via
followcursor/pytest.ini) - Location:
followcursor/tests/— onetest_<module>.pyper source module - Current count: 386 tests
- Scope: Pure-logic layer (no Qt dependency in tests)
Modules Tested¶
models, zoom_engine, activity_analyzer, utils, frames, backgrounds, project_file, ai_service, fluent_effects
Running Tests¶
Execute the Run Tests VS Code task (Ctrl+Shift+P > Tasks: Run Task > Run Tests).
Warning
Do not run pytest manually in a terminal — use the VS Code task to ensure the correct environment and working directory.
Writing Tests¶
- Place tests in
followcursor/tests/test_<module>.py - Use shared fixtures from
conftest.py - Test data models, serialization roundtrips, algorithms, and edge cases
- Tests must not depend on Qt (no widget instantiation)
Security Conventions¶
Credential Storage¶
- Use Windows DPAPI — encrypt credentials before storing in Windows Registry
- Decrypt on-demand — decrypt only when the credential is needed
- Never log credentials — sanitize debug output
- See
app/credentials.pyfor the encrypt/decrypt implementation
Temporary File Cleanup¶
- Delete temp files immediately when no longer needed
- Use unique filenames (
tempfile.NamedTemporaryFile()or randomized names) - Track extraction directories and clean up in
closeEvent() - Wrap deletion in try/except for file-in-use errors
Common Pitfalls¶
- Never use
sourceorbashcommands for Windows Python — use.venv\Scripts\python.exe - Never add
SetProcessDpiAwareness— PySide6 already setsPER_MONITOR_DPI_AWARE_V2 - Never use
CFUNCTYPEfor Win32 hook callbacks — useWINFUNCTYPE - Never use trademarked device names in frame presets
- Never run the compositor during recording — use blur overlay instead
- Never import heavy modules at top of widget files — use deferred imports
- Catch both
BrokenPipeErrorandOSErroron ffmpeg pipe writes (Windows raisesOSError(22)) closeEventusesos._exit(0)for clean shutdown
Making Changes¶
Workflow¶
- Create a feature branch from
main - Make your changes
- Run the Run Tests VS Code task — all tests must pass
- Test manually — launch the app and verify
- Commit with a descriptive message
- Push and open a Pull Request
Branching Strategy¶
All features, bug fixes, and significant changes must be developed on a dedicated branch (e.g. fix/encoder-fallback, feat/gif-palette). Trivial documentation-only edits may go directly on main.
Adding a New Widget¶
- Create
app/widgets/your_widget.py - Use
QWidgetwith anobjectNamefor QSS styling - Define signals for outbound communication
- Wire it up in
MainWindow.__init__ - Add QSS rules in
theme.pyusing tokens fromtokens.py
Adding a Background Preset¶
Edit app/backgrounds.py — add a BackgroundPreset to the PRESETS list. The editor panel grid auto-generates buttons.
Adding a Frame Preset¶
Edit app/frames.py — add a FramePreset to FRAME_PRESETS. Use generic names only (no trademarks).
Versioning¶
FollowCursor uses Semantic Versioning (MAJOR.MINOR.PATCH):
| Bump | When |
|---|---|
| MAJOR | Breaking changes to project file format, CLI, or public API |
| MINOR | New features, new export formats, new UI panels |
| PATCH | Bug fixes, performance improvements, documentation fixes |
The version lives in followcursor/app/version.py:
__version__ = "X.Y.Z"
Note:
0.13.0placeholders in this documentation are replaced at build time by MkDocs — they always reflect the current release when viewed on the docs site.
Releasing¶
- Update
__version__inapp/version.py - Add a new section to
CHANGELOG.mdwith today's date - Run tests — all must pass
- Commit:
release: vX.Y.Z - Merge to
main - Tag:
git tag vX.Y.Z - Push:
git push origin main --tags - CI builds
.exe+ signed MSIX and creates a GitHub Release - The tag push automatically triggers the docs site to rebuild and deploy — the version number shown in all pages updates to the new release automatically (no manual docs edits needed)
Dependencies¶
| Package | Purpose |
|---|---|
| PySide6 | Qt 6 GUI framework |
| mss | GDI fallback screen capture |
| opencv-python | Video decode/encode, image processing |
| numpy | Array operations for frame manipulation |
| imageio-ffmpeg | Bundled ffmpeg binary |
| windows-capture | Windows Graphics Capture API bindings |
To add a dependency:
pip install package-name- Add to
requirements.txtwith a minimum version - If heavy and not needed at import time, use a deferred import
Build & CI¶
Local Build¶
.\scripts\Build-App.ps1
Produces dist\FollowCursor\FollowCursor.exe — a single-folder PyInstaller distribution.
MSIX Package¶
# Unsigned (local testing)
.\scripts\Build-Msix.ps1 -Version "X.Y.Z" -SkipSign
# Signed with local PFX
.\scripts\Build-Msix.ps1 -Version "X.Y.Z" -LocalPfx ".\cert.pfx" -Publisher "CN=MyName"
CI Pipeline¶
GitHub Actions runs on every push/PR to main:
- Extracts version from
app/version.py - Installs dependencies (Python 3.13, Windows runner)
- Runs the pytest suite
- Builds with PyInstaller
- Uploads versioned artifact
- On
v*tags: builds MSIX, signs with Azure Trusted Signing, creates GitHub Release
Questions?¶
Open an issue on GitHub or check the Quickstart Guide for usage help.