Compare commits

...

23 Commits
v1.0.0 ... dev

Author SHA1 Message Date
5b4a76c484 add task build preconditions 2026-03-17 00:26:27 +00:00
dfb96777bb remove emojis from release notes 2026-03-11 01:34:07 +00:00
5aa2af2acf minor bump 2026-03-11 01:11:23 +00:00
cc6e187998 bus mono now a ButtonMenu.
this allows users to select between  `mono off`, `mono on` and `stereo reverse`. This properly reflects the Voicemeter GUI.
2026-03-11 01:11:17 +00:00
1ea1c59f06 remove publish workflow 2026-03-11 00:49:24 +00:00
054ad040bb add publish+ruff workflows 2026-03-11 00:46:12 +00:00
94f0b847a7 copy only x64 and x86 dlls 2026-03-11 00:40:46 +00:00
8d251d1dea add missing compress step 2026-03-11 00:37:52 +00:00
5f7d66ceae set Task version 2026-03-11 00:29:31 +00:00
4ed17a5476 fix upload-artifact version 2026-03-11 00:26:15 +00:00
b88955a45a add release workflow 2026-03-11 00:23:37 +00:00
c3247fa5bf fix dynamic_builder 2026-03-10 23:34:18 +00:00
39c14279b2 add dynamic_builder.py script
add Taskfile.dynamic.yml for workflow builds.
2026-03-10 23:06:12 +00:00
d850581179 rename GUI entry points
define voicemeeter-api local path dependency

bump voicemeeter-api dep

add pre-commit config
2026-03-10 20:59:01 +00:00
47d38e4468 add spec_generator.py as well as generate-specs task 2026-03-10 20:39:21 +00:00
415f2e2ba3 remove controllerClient dlls, add .gitkeep to preserve directory structure 2026-03-10 16:42:51 +00:00
57b28b3507 upd pdm badge 2026-02-21 22:18:56 +00:00
cc0a1de07b call task from pdm
remove build.ps1
2025-02-13 16:47:20 +00:00
9ee0dac610 run through formatter 2025-02-07 23:00:07 +00:00
c7b8622aac Merge branch 'dev' of https://github.com/onyx-and-iris/nvda-voicemeeter into dev 2025-02-07 16:07:51 +00:00
2706338fcb add Taskfile 2025-02-07 16:07:45 +00:00
ff98aa5f89 run through ruff 2025-02-03 00:14:41 +00:00
03252fe30f rename entry points 2025-01-25 15:08:55 +00:00
22 changed files with 1180 additions and 169 deletions

237
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,237 @@
name: Build and Release
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: '3.12'
- name: Install Task
uses: go-task/setup-task@v1
with:
version: 3.x
- name: Download NVDA Controller Client
shell: pwsh
run: |
Write-Host "Downloading NVDA Controller Client..."
$url = "https://download.nvaccess.org/releases/stable/nvda_2025.3.3_controllerClient.zip"
$zipPath = "nvda_controllerClient.zip"
# Download the zip file
Invoke-WebRequest -Uri $url -OutFile $zipPath
Write-Host "Downloaded $zipPath"
# Extract to temp directory
$tempDir = "temp_controller"
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
# Find and copy DLL files to correct locations
Write-Host "Extracting DLL files..."
# Create directories if they don't exist
New-Item -ItemType Directory -Path "controllerClient/x64" -Force | Out-Null
New-Item -ItemType Directory -Path "controllerClient/x86" -Force | Out-Null
# Find and copy the DLL files
$dllFiles = Get-ChildItem -Path $tempDir -Recurse -Name "*.dll" | Where-Object { $_ -like "*controllerClient*" }
foreach ($dll in $dllFiles) {
$fullPath = Join-Path $tempDir $dll
$dirName = (Get-Item $fullPath).Directory.Name
if ($dll -match "x64" -or $dirName -match "x64") {
Copy-Item $fullPath "controllerClient/x64/nvdaControllerClient.dll"
Write-Host "Copied x64 DLL: $dll"
} elseif ($dll -match "x86" -or $dirName -match "x86") {
Copy-Item $fullPath "controllerClient/x86/nvdaControllerClient.dll"
Write-Host "Copied x86 DLL: $dll"
} elseif ($dll -match "arm64" -or $dirName -match "arm64") {
Write-Host "Skipping ARM64 DLL: $dll (not needed)"
} else {
Write-Host "Skipping unknown architecture DLL: $dll"
}
}
# Clean up
Remove-Item $zipPath -Force
Remove-Item $tempDir -Recurse -Force
# Verify files were copied
Write-Host "Verifying controller client files..."
if (Test-Path "controllerClient/x64/nvdaControllerClient.dll") {
Write-Host "[OK] x64 controller client found"
} else {
Write-Host "[ERROR] x64 controller client missing"
exit 1
}
if (Test-Path "controllerClient/x86/nvdaControllerClient.dll") {
Write-Host "[OK] x86 controller client found"
} else {
Write-Host "[ERROR] x86 controller client missing"
exit 1
}
- name: Fix dependencies for CI
shell: pwsh
run: |
echo "Fixing local dependencies for CI build..."
# Remove local path dependency for voicemeeter-api
pdm remove -dG dev voicemeeter-api || true
echo "Updated dependencies for CI build"
- name: Install dependencies
shell: pwsh
run: |
# Install project dependencies
pdm install
# Verify PyInstaller is available
Write-Host "Verifying PyInstaller installation..."
pdm list | Select-String pyinstaller
- name: Get PDM executable path
shell: pwsh
run: |
$pdmPath = Get-Command pdm | Select-Object -ExpandProperty Source
Write-Host "PDM path: $pdmPath"
echo "PDM_BIN=$pdmPath" >> $env:GITHUB_ENV
- name: Build artifacts with dynamic taskfile
shell: pwsh
env:
PDM_BIN: ${{ env.PDM_BIN }}
run: |
Write-Host "Building all executables using dynamic builder..."
task --taskfile Taskfile.dynamic.yml build-all
- name: Compress build artifacts
shell: pwsh
env:
PDM_BIN: ${{ env.PDM_BIN }}
run: |
Write-Host "Compressing build artifacts..."
task --taskfile Taskfile.dynamic.yml compress-all
- name: Verify build outputs
shell: pwsh
run: |
Write-Host "Verifying build outputs..."
$expectedFiles = @(
"dist/basic.zip",
"dist/banana.zip",
"dist/potato.zip"
)
$missingFiles = @()
$foundFiles = @()
foreach ($file in $expectedFiles) {
if (Test-Path $file) {
$size = [math]::Round((Get-Item $file).Length / 1MB, 2)
$foundFiles += "$file ($size MB)"
} else {
$missingFiles += $file
}
}
Write-Host "Found files:"
$foundFiles | ForEach-Object { Write-Host " $_" }
if ($missingFiles.Count -gt 0) {
Write-Host -ForegroundColor Red "Missing files:"
$missingFiles | ForEach-Object { Write-Host " $_" }
exit 1
}
Write-Host -ForegroundColor Green "All expected files found!"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: nvda-voicemeeter-builds
path: |
dist/basic.zip
dist/banana.zip
dist/potato.zip
- name: Build Summary
shell: pwsh
run: |
Write-Host -ForegroundColor Green "Build completed successfully!"
Write-Host ""
Write-Host "Built artifacts:"
Write-Host " - nvda-voicemeeter-basic.zip"
Write-Host " - nvda-voicemeeter-banana.zip"
Write-Host " - nvda-voicemeeter-potato.zip"
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create Release
run: |
TAG_NAME=${GITHUB_REF#refs/tags/}
gh release create $TAG_NAME \
--title "NVDA-Voicemeeter $TAG_NAME" \
--notes "## NVDA-Voicemeeter Release $TAG_NAME
### Downloads
- **nvda-voicemeeter-basic.zip** - Basic version with dependencies
- **nvda-voicemeeter-banana.zip** - Banana version with dependencies
- **nvda-voicemeeter-potato.zip** - Potato version with dependencies
### Requirements
- Windows 10/11
- Voicemeeter (Basic/Banana/Potato) installed
- NVDA screen reader
### Installation
1. Download the appropriate zip for your Voicemeeter version
2. Extract and run the executable - no installation required
3. The application will integrate with NVDA automatically
### Notes
- Built with dynamic build system using PyInstaller
- Includes NVDA Controller Client for screen reader integration"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release assets
run: |
TAG_NAME=${GITHUB_REF#refs/tags/}
find . -name "*.zip" -exec gh release upload $TAG_NAME {} \;
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

19
.github/workflows/ruff.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Ruff
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
with:
args: 'format --check --diff'

76
.gitignore vendored
View File

@ -1,6 +1,9 @@
# Generated by ignr: github.com/onyx-and-iris/ignr
## Python ##
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.py[codz]
*$py.class
# C extensions
@ -46,7 +49,7 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
@ -85,7 +88,7 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -94,23 +97,36 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
@ -123,6 +139,7 @@ celerybeat.pid
# Environments
.env
.envrc
.venv
env/
venv/
@ -161,11 +178,44 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# spec files
spec/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# persistent storage
settings.json
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# quick test
quick.py
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# End of ignr
# Controller Client
controllerClient/**/*
!controllerClient/*/
!controllerClient/**/.gitkeep
# Spec files
spec/*
!spec/.gitkeep

5
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,5 @@
repos:
- repo: https://github.com/pdm-project/pdm
rev: 2.26.6
hooks:
- id: pdm-lock-check

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.10.11

View File

@ -1,4 +1,4 @@
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev)
[![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
# NVDA Voicemeeter

76
Taskfile.dynamic.yml Normal file
View File

@ -0,0 +1,76 @@
version: '3'
# Dynamic build system - no spec files needed!
# Examples:
# - task -t Taskfile.dynamic.yml build KINDS="basic banana"
# - task -t Taskfile.dynamic.yml build-all
# KINDS can be a space-separated list of kinds to build, or "all" to build everything.
#
# Compression tasks are also dynamic, allowing you to specify which kind to compress or compress all kinds at once.
# Examples:
# - task -t Taskfile.dynamic.yml compress KIND=basic
# - task -t Taskfile.dynamic.yml compress-all
vars:
KINDS: '{{.KINDS | default "all"}}'
SHELL: pwsh
tasks:
build:
desc: Build specified kinds dynamically (no spec files needed)
preconditions:
- sh: |
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
exit 1
fi
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
cmds:
- ${PDM_BIN:-pdm} run python tools/dynamic_builder.py {{.KINDS}}
build-all:
desc: Build all kinds
preconditions:
- sh: |
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
exit 1
fi
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
cmds:
- ${PDM_BIN:-pdm} run python tools/dynamic_builder.py all
compress:
desc: Compress artifacts for specified kind
cmds:
- task: compress-{{.KIND}}
compress-all:
desc: Compress artifacts for all kinds
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
task: compress-{{.ITEM.KIND}}
compress-basic:
desc: Compress basic build artifacts
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/basic -DestinationPath dist/basic.zip -Force"'
generates:
- dist/basic.zip
compress-banana:
desc: Compress banana build artifacts
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/banana -DestinationPath dist/banana.zip -Force"'
generates:
- dist/banana.zip
compress-potato:
desc: Compress potato build artifacts
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/potato -DestinationPath dist/potato.zip -Force"'
generates:
- dist/potato.zip
clean:
desc: Clean all build artifacts
cmds:
- |
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"

49
Taskfile.yml Normal file
View File

@ -0,0 +1,49 @@
version: '3'
vars:
SHELL: pwsh
tasks:
default:
desc: Prepare artifacts for release
cmds:
- task: release
release:
desc: Build and compress all artifacts
cmds:
- task: build
- task: compress
generate-specs:
desc: Generate all spec files from templates
cmd: pdm run python tools/spec_generator.py
build:
desc: Build the project
deps: [generate-specs]
preconditions:
- sh: |
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
exit 1
fi
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: pdm run pyinstaller --noconfirm --distpath dist/{{.ITEM.KIND}} spec/{{.ITEM.KIND}}.spec
compress:
desc: Compress the build artifacts
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.KIND}}.zip -Force"'
clean:
desc: Clean the project
cmds:
- |
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"

View File

@ -2,7 +2,7 @@ import voicemeeterlib
import nvda_voicemeeter
KIND_ID = "potato"
KIND_ID = 'potato'
with voicemeeterlib.api(KIND_ID) as vm:
with nvda_voicemeeter.draw(KIND_ID, vm) as window:

View File

@ -1,23 +0,0 @@
function Compress-Builds {
$target = Join-Path -Path $PSScriptRoot -ChildPath "dist"
@("basic", "banana", "potato") | ForEach-Object {
Compress-Archive -Path $(Join-Path -Path $target -ChildPath $_) -DestinationPath $(Join-Path -Path $target -ChildPath "${_}.zip") -Force
}
}
function Get-Builds {
@("basic", "banana", "potato") | ForEach-Object {
$specName = $_
Write-Host "building $specName"
pdm run pyinstaller --noconfirm --distpath (Join-Path -Path "dist" -ChildPath $specName) (Join-Path -Path "spec" -ChildPath "${specName}.spec")
}
}
function main {
Get-Builds
Compress-Builds
}
if ($MyInvocation.InvocationName -ne '.') { main }

View File

View File

194
pdm.lock generated
View File

@ -2,37 +2,37 @@
# It is not intended for manual editing.
[metadata]
groups = ["default", "build", "format"]
groups = ["default", "build", "dev", "format"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:177dea67a7eadb6b161d3ac62fb09415d2d4f295c59914a9c5d0cbef7f0a5b65"
content_hash = "sha256:7f8d5a672b82b4ff9d793a255dcdf55415b79773d7cf7e2742f4531366daac58"
[[metadata.targets]]
requires_python = ">=3.10,<3.13"
[[package]]
name = "altgraph"
version = "0.17.4"
version = "0.17.5"
summary = "Python graph (network) package"
groups = ["build"]
files = [
{file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
{file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"},
{file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"},
]
[[package]]
name = "freesimplegui"
version = "5.1.1"
version = "5.2.0.post1"
summary = "The free-forever Python GUI framework."
groups = ["default"]
files = [
{file = "FreeSimpleGUI-5.1.1-py3-none-any.whl", hash = "sha256:d7629d5c94b55264d119bd2a89f52667d863ea7914d808e619aea29922ff842e"},
{file = "freesimplegui-5.1.1.tar.gz", hash = "sha256:2f0946c7ac221c997929181cbe7526e342fff5fc291a26d1d726287a5dd964fb"},
{file = "freesimplegui-5.2.0.post1-py3-none-any.whl", hash = "sha256:3d61eb519324503232f86b2f1bd7f5c6813ce225f6e189d0fd737ddb036af4d5"},
{file = "freesimplegui-5.2.0.post1.tar.gz", hash = "sha256:e58a0e6758e9a9e87152256911f94fcc3998356d1309973a9f4d9df2dc55f98a"},
]
[[package]]
name = "macholib"
version = "1.16.3"
version = "1.16.4"
summary = "Mach-O header analysis and editing"
groups = ["build"]
marker = "sys_platform == \"darwin\""
@ -40,37 +40,37 @@ dependencies = [
"altgraph>=0.17",
]
files = [
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
{file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"},
{file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"},
]
[[package]]
name = "packaging"
version = "24.2"
version = "26.0"
requires_python = ">=3.8"
summary = "Core utilities for Python packages"
groups = ["build"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
]
[[package]]
name = "pefile"
version = "2023.2.7"
version = "2024.8.26"
requires_python = ">=3.6.0"
summary = "Python PE parsing module"
groups = ["build"]
marker = "sys_platform == \"win32\""
files = [
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
{file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"},
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
]
[[package]]
name = "pyinstaller"
version = "6.11.1"
requires_python = "<3.14,>=3.8"
version = "6.19.0"
requires_python = "<3.15,>=3.8"
summary = "PyInstaller bundles a Python application and all its dependencies into a single package."
groups = ["build"]
dependencies = [
@ -78,29 +78,29 @@ dependencies = [
"importlib-metadata>=4.6; python_version < \"3.10\"",
"macholib>=1.8; sys_platform == \"darwin\"",
"packaging>=22.0",
"pefile!=2024.8.26,>=2022.5.30; sys_platform == \"win32\"",
"pyinstaller-hooks-contrib>=2024.9",
"pefile>=2022.5.30; sys_platform == \"win32\"",
"pyinstaller-hooks-contrib>=2026.0",
"pywin32-ctypes>=0.2.1; sys_platform == \"win32\"",
"setuptools>=42.0.0",
]
files = [
{file = "pyinstaller-6.11.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03"},
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4"},
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f"},
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda"},
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977"},
{file = "pyinstaller-6.11.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f"},
{file = "pyinstaller-6.11.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce"},
{file = "pyinstaller-6.11.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7"},
{file = "pyinstaller-6.11.1-py3-none-win32.whl", hash = "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a"},
{file = "pyinstaller-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f"},
{file = "pyinstaller-6.11.1-py3-none-win_arm64.whl", hash = "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423"},
{file = "pyinstaller-6.11.1.tar.gz", hash = "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef"},
{file = "pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134"},
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11"},
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf"},
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122"},
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63"},
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe"},
{file = "pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83"},
{file = "pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6"},
{file = "pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2"},
{file = "pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33"},
{file = "pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea"},
{file = "pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865"},
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.0"
version = "2026.3"
requires_python = ">=3.8"
summary = "Community maintained hooks for PyInstaller"
groups = ["build"]
@ -110,19 +110,19 @@ dependencies = [
"setuptools>=42.0.0",
]
files = [
{file = "pyinstaller_hooks_contrib-2025.0-py3-none-any.whl", hash = "sha256:3c0623799c3f81a37293127f485d65894c20fd718f722cb588785a3e52581ad1"},
{file = "pyinstaller_hooks_contrib-2025.0.tar.gz", hash = "sha256:6dc0b55a1acaab2ffee36ed4a05b073aa0a22e46f25fb5c66a31e217454135ed"},
{file = "pyinstaller_hooks_contrib-2026.3-py3-none-any.whl", hash = "sha256:5ecd1068ad262afecadf07556279d2be52ca93a88b049fae17f1a2eb2969254a"},
{file = "pyinstaller_hooks_contrib-2026.3.tar.gz", hash = "sha256:800d3a198a49a6cd0de2d7fb795005fdca7a0222ed9cb47c0691abd1c27b9310"},
]
[[package]]
name = "pyparsing"
version = "3.2.1"
version = "3.3.2"
requires_python = ">=3.9"
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
summary = "pyparsing - Classes and methods to define and execute parsing grammars"
groups = ["default"]
files = [
{file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"},
{file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"},
{file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"},
{file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"},
]
[[package]]
@ -139,94 +139,80 @@ files = [
[[package]]
name = "ruff"
version = "0.9.2"
version = "0.15.5"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["format"]
files = [
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
{file = "ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c"},
{file = "ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080"},
{file = "ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010"},
{file = "ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65"},
{file = "ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440"},
{file = "ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204"},
{file = "ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8"},
{file = "ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681"},
{file = "ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a"},
{file = "ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca"},
{file = "ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd"},
{file = "ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d"},
{file = "ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752"},
{file = "ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2"},
{file = "ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74"},
{file = "ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe"},
{file = "ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b"},
{file = "ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2"},
]
[[package]]
name = "setuptools"
version = "75.8.0"
version = "82.0.1"
requires_python = ">=3.9"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
summary = "Most extensible Python build backend with support for C/C++ extension modules"
groups = ["build"]
files = [
{file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"},
{file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"},
{file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
{file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
]
[[package]]
name = "tomli"
version = "2.2.1"
version = "2.4.0"
requires_python = ">=3.8"
summary = "A lil' TOML parser"
groups = ["default"]
groups = ["default", "dev"]
marker = "python_version < \"3.11\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
{file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"},
{file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"},
{file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"},
{file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"},
{file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"},
{file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"},
{file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"},
{file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"},
{file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"},
{file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"},
{file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"},
{file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"},
{file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"},
{file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"},
{file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"},
{file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"},
{file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"},
{file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"},
{file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"},
{file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"},
]
[[package]]
name = "voicemeeter-api"
version = "2.6.1"
requires_python = "<4.0,>=3.10"
version = "2.7.2"
requires_python = ">=3.10"
editable = true
path = "../voicemeeter-api-python"
summary = "A Python wrapper for the Voiceemeter API"
groups = ["default"]
groups = ["default", "dev"]
dependencies = [
"tomli<3.0,>=2.0.1; python_version < \"3.11\"",
]
files = [
{file = "voicemeeter_api-2.6.1-py3-none-any.whl", hash = "sha256:8ae3bce0f9ad6bbad78f2f69f522b6fb2e229d314918a075ad83d4009aff7020"},
{file = "voicemeeter_api-2.6.1.tar.gz", hash = "sha256:34d8672603ec66197f2d61fd8f038f46d8451759c81fbe222b00e7d3ccccd1f5"},
]

View File

@ -1,27 +1,25 @@
[project]
name = "nvda-voicemeeter"
version = "1.0.0"
version = "1.1.0"
description = "A Voicemeeter app compatible with NVDA"
authors = [
{name = "Onyx and Iris", email = "code@onyxandiris.online"},
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
dependencies = [
"pyparsing>=3.2.1",
"voicemeeter-api>=2.7.2",
"freesimplegui>=5.1.1",
]
dependencies = ["pyparsing>=3.2.1", "voicemeeter-api>=2.6.1", "freesimplegui>=5.1.1"]
requires-python = ">=3.10,<3.13"
readme = "README.md"
license = {text = "MIT"}
license = { text = "MIT" }
[dependency-groups]
format = [
"ruff>=0.9.2",
]
build = [
"pyinstaller>=6.3.0",
]
format = ["ruff>=0.9.2"]
build = ["pyinstaller>=6.3.0"]
[project.scripts]
gui-basic = "nvda_voicemeeter.gui.basic:run"
gui-banana = "nvda_voicemeeter.gui.banana:run"
gui-potato = "nvda_voicemeeter.gui.potato:run"
nvda-voicemeeter-basic = "nvda_voicemeeter.gui.basic:run"
nvda-voicemeeter-banana = "nvda_voicemeeter.gui.banana:run"
nvda-voicemeeter-potato = "nvda_voicemeeter.gui.potato:run"
[build-system]
requires = ["pdm-backend"]
@ -30,8 +28,10 @@ build-backend = "pdm.backend"
[tool.pdm]
distribution = true
[tool.pdm.scripts]
build = {shell = "pwsh build.ps1"}
[tool.pdm.dev-dependencies]
dev = [
"-e file:///${PROJECT_ROOT}/../voicemeeter-api-python#egg=voicemeeter-api",
]
[tool.ruff]
exclude = [
@ -110,7 +110,4 @@ docstring-code-line-length = "dynamic"
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
"E402",
"F401",
]
"__init__.py" = ["E402", "F401"]

0
spec/.gitkeep Normal file
View File

View File

@ -438,13 +438,20 @@ class Builder:
def make_tab3_button_row(self, i) -> psg.Frame:
"""tab3 row represents bus composite toggle"""
def add_strip_outputs(layout):
params = ['MONO', 'EQ', 'MUTE']
def add_bus_buttons(layout):
busmono = util.get_bus_mono()
params = ['EQ', 'MUTE']
if self.kind.name == 'basic':
params.remove('EQ')
busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
layout.append(
[
psg.ButtonMenu(
'Mono',
size=(6, 2),
menu_def=['', busmono],
key=f'BUS {i}||MONO',
),
*[
psg.Button(
param.capitalize(),
@ -454,7 +461,7 @@ class Builder:
for param in params
],
psg.ButtonMenu(
'BUSMODE',
'Bus Mode',
size=(12, 2),
menu_def=['', busmodes],
key=f'BUS {i}||MODE',
@ -463,7 +470,7 @@ class Builder:
)
outputs = []
[step(outputs) for step in (add_strip_outputs,)]
[step(outputs) for step in (add_bus_buttons,)]
return psg.Frame(
self.window.cache['labels'][f'BUS {i}||LABEL'],
outputs,

View File

@ -155,6 +155,10 @@ def get_bus_modes(vm) -> list:
]
def get_bus_mono() -> list:
return ['off', 'on', 'stereo reverse']
def check_bounds(val, bounds: tuple) -> int | float:
lower, upper = bounds
if val > upper:

View File

@ -58,6 +58,7 @@ class NVDAVMWindow(psg.Window):
self[f'STRIP {i}||SLIDER LIMIT'].Widget.config(**slider_opts)
for i in range(self.kind.num_bus):
self[f'BUS {i}||SLIDER GAIN'].Widget.config(**slider_opts)
self[f'BUS {i}||MONO'].Widget.config(**buttonmenu_opts)
self[f'BUS {i}||MODE'].Widget.config(**buttonmenu_opts)
self.register_events()
@ -251,13 +252,16 @@ class NVDAVMWindow(psg.Window):
self[f'STRIP {i}||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
# Bus Params
params = ['MONO', 'EQ', 'MUTE']
params = ['EQ', 'MUTE']
if self.kind.name == 'basic':
params.remove('EQ')
for i in range(self.kind.num_bus):
for param in params:
self[f'BUS {i}||{param}'].bind('<FocusIn>', '||FOCUS IN')
self[f'BUS {i}||{param}'].bind('<Return>', '||KEY ENTER')
self[f'BUS {i}||MONO'].bind('<FocusIn>', '||FOCUS IN')
self[f'BUS {i}||MONO'].bind('<space>', '||KEY SPACE', propagate=False)
self[f'BUS {i}||MONO'].bind('<Return>', '||KEY ENTER')
self[f'BUS {i}||MODE'].bind('<FocusIn>', '||FOCUS IN')
self[f'BUS {i}||MODE'].bind('<space>', '||KEY SPACE', propagate=False)
self[f'BUS {i}||MODE'].bind('<Return>', '||KEY ENTER', propagate=False)
@ -306,7 +310,7 @@ class NVDAVMWindow(psg.Window):
mode = None
continue
match parsed_cmd := self.parser.match.parseString(event):
match parsed_cmd := self.parser.match.parse_string(event):
# Slider mode
case [['ALT', 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' as direction], ['PRESS' | 'RELEASE' as e]]:
if mode:
@ -972,7 +976,7 @@ class NVDAVMWindow(psg.Window):
self.nvda.speak,
'on' if val else 'off',
)
case 'MONO' | 'MUTE':
case 'MUTE':
val = not val
setattr(self.vm.bus[int(index)], param.lower(), val)
self.cache['bus'][event] = val
@ -981,6 +985,15 @@ class NVDAVMWindow(psg.Window):
self.nvda.speak,
'on' if val else 'off',
)
case 'MONO':
chosen = values[event]
self.vm.bus[int(index)].mono = util.get_bus_mono().index(chosen)
self.cache['bus'][event] = chosen
self.TKroot.after(
200,
self.nvda.speak,
f'mono {chosen}',
)
case 'MODE':
chosen = util._bus_mode_map_reversed[values[event]]
setattr(self.vm.bus[int(index)].mode, chosen, True)
@ -996,11 +1009,19 @@ class NVDAVMWindow(psg.Window):
val = self.cache['bus'][f'BUS {index}||{param}']
if param == 'MODE':
self.nvda.speak(f'{label} bus {param} {util._bus_mode_map[val]}')
elif param == 'MONO':
busmode = util.get_bus_mono()[val]
if busmode in ('on', 'off'):
self.nvda.speak(f'{label} {param} {busmode}')
else:
self.nvda.speak(f'{label} {busmode}')
else:
self.nvda.speak(f'{label} {param} {"on" if val else "off"}')
case [['BUS', index], [param], ['KEY', 'SPACE' | 'ENTER']]:
if param == 'MODE':
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MODE')
elif param == 'MONO':
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MONO')
else:
self.find_element_with_focus().click()

302
tools/dynamic_builder.py Normal file
View File

@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
Dynamic build system for nvda-voicemeeter.
This script generates PyInstaller spec files on-the-fly and builds executables
without storing intermediate files in the repository.
Usage:
python tools/dynamic_builder.py # Build all kinds
python tools/dynamic_builder.py basic # Build basic only
python tools/dynamic_builder.py basic banana # Build specific kinds
python tools/dynamic_builder.py all # Build all kinds (explicit)
Requirements:
- PDM environment with PyInstaller installed
- controllerClient DLL files in controllerClient/{x64,x86}/
- nvda_voicemeeter package installed in environment
Environment Variables:
- PDM_BIN: Path to PDM executable (default: 'pdm')
Exit Codes:
- 0: All builds successful
- 1: One or more builds failed
"""
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict
# Build configuration
KINDS = ['basic', 'banana', 'potato']
# Templates
PYTHON_TEMPLATE = """import voicemeeterlib
import nvda_voicemeeter
KIND_ID = '{kind}'
with voicemeeterlib.api(KIND_ID) as vm:
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
window.run()
"""
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
added_files = [
( '{controller_x64_path}', 'controllerClient/x64' ),
( '{controller_x86_path}', 'controllerClient/x86' ),
]
a = Analysis(
['{script_path}'],
pathex=[],
binaries=[],
datas=added_files,
hiddenimports=[],
hookspath=[],
hooksconfig={{}},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='{kind}',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='{kind}',
)
"""
class DynamicBuilder:
def __init__(self, base_dir: Path, dist_dir: Path):
self.base_dir = base_dir
self.dist_dir = dist_dir
self.temp_dir = None
def validate_environment(self) -> bool:
"""Validate that all required files and dependencies are present."""
# Check for controller client DLLs
x64_dll = self.base_dir / 'controllerClient' / 'x64' / 'nvdaControllerClient.dll'
x86_dll = self.base_dir / 'controllerClient' / 'x86' / 'nvdaControllerClient.dll'
if not x64_dll.exists():
print(f'[ERROR] Missing x64 controller client: {x64_dll}')
return False
if not x86_dll.exists():
print(f'[ERROR] Missing x86 controller client: {x86_dll}')
return False
print('[OK] Controller client DLLs found')
# Check PyInstaller availability
try:
result = subprocess.run(['pdm', 'list'], capture_output=True, text=True)
if 'pyinstaller' not in result.stdout.lower():
print('[ERROR] PyInstaller not found in PDM environment')
return False
print('[OK] PyInstaller available')
except subprocess.CalledProcessError:
print('[ERROR] Failed to check PDM environment')
return False
return True
def __enter__(self):
# Validate environment first
if not self.validate_environment():
print('[ERROR] Environment validation failed!')
sys.exit(1)
self.temp_dir = Path(tempfile.mkdtemp(prefix='nvda_voicemeeter_build_'))
print(f'Using temp directory: {self.temp_dir}')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.temp_dir and self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
print(f'Cleaned up temp directory: {self.temp_dir}')
def create_python_file(self, kind: str) -> Path:
"""Create a temporary Python launcher file."""
content = PYTHON_TEMPLATE.format(kind=kind)
py_file = self.temp_dir / f'{kind}.py'
with open(py_file, 'w') as f:
f.write(content)
return py_file
def create_spec_file(self, kind: str, py_file: Path) -> Path:
"""Create a temporary PyInstaller spec file."""
controller_x64_path = (self.base_dir / 'controllerClient' / 'x64').as_posix()
controller_x86_path = (self.base_dir / 'controllerClient' / 'x86').as_posix()
content = SPEC_TEMPLATE.format(
script_path=py_file.as_posix(),
controller_x64_path=controller_x64_path,
controller_x86_path=controller_x86_path,
kind=kind,
)
spec_file = self.temp_dir / f'{kind}.spec'
with open(spec_file, 'w') as f:
f.write(content)
return spec_file
def build_variant(self, kind: str) -> bool:
"""Build a single kind variant."""
print(f'Building {kind}...')
# Validate kind
if kind not in KINDS:
print(f'[ERROR] Unknown kind: {kind}. Valid kinds: {", ".join(KINDS)}')
return False
# Create temporary files
py_file = self.create_python_file(kind)
spec_file = self.create_spec_file(kind, py_file)
# Build with PyInstaller
dist_path = self.dist_dir / kind
pdm_bin = os.getenv('PDM_BIN', 'pdm')
cmd = [
pdm_bin,
'run',
'pyinstaller',
'--noconfirm',
'--distpath',
str(dist_path.parent),
str(spec_file),
]
try:
result = subprocess.run(cmd, cwd=self.base_dir, capture_output=True, text=True)
if result.returncode == 0:
print(f'[OK] Built {kind}')
return True
else:
print(f'[FAIL] Failed to build {kind}')
print(f'Error: {result.stderr}')
return False
except Exception as e:
print(f'[ERROR] Exception building {kind}: {e}')
return False
def build_all_kinds(self) -> Dict[str, bool]:
"""Build all kind variants."""
results = {}
for kind in KINDS:
success = self.build_variant(kind)
results[kind] = success
return results
def main():
parser = argparse.ArgumentParser(description='Dynamic build system for nvda-voicemeeter')
parser.add_argument(
'kinds',
nargs='*',
choices=KINDS + ['all'],
help='Kinds to build (default: all)',
)
parser.add_argument(
'--dist-dir',
type=Path,
default=Path('dist'),
help='Distribution output directory',
)
args = parser.parse_args()
if not args.kinds or 'all' in args.kinds:
kinds_to_build = KINDS
else:
kinds_to_build = args.kinds
base_dir = Path.cwd()
args.dist_dir.mkdir(exist_ok=True)
print(f'Building kinds: {", ".join(kinds_to_build)}')
all_results = {}
with DynamicBuilder(base_dir, args.dist_dir) as builder:
if 'all' in kinds_to_build or len(kinds_to_build) == len(KINDS):
# Build all kinds
results = builder.build_all_kinds()
all_results.update(results)
else:
# Build specific kinds
for kind in kinds_to_build:
success = builder.build_variant(kind)
all_results[kind] = success
# Report results
print('\n' + '=' * 50)
print('BUILD SUMMARY')
print('=' * 50)
success_count = 0
total_count = 0
for build_name, success in all_results.items():
status = '[OK]' if success else '[FAIL]'
print(f'{status} {build_name}')
if success:
success_count += 1
total_count += 1
print(f'\nSuccess: {success_count}/{total_count}')
if success_count == total_count:
print('All builds completed successfully!')
sys.exit(0)
else:
print('Some builds failed!')
sys.exit(1)
if __name__ == '__main__':
main()

280
tools/spec_generator.py Normal file
View File

@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""
PyInstaller spec file generator for nvda-voicemeeter project.
This script generates .spec files for different Voicemeeter kinds (basic, banana, potato).
Based on the existing spec file patterns in the spec/ directory.
Usage:
python tools/spec_generator.py [kind]
Arguments:
kind: The Voicemeeter kind to generate spec for (basic, banana, potato).
If not provided, generates all spec files.
Examples:
python tools/spec_generator.py basic
python tools/spec_generator.py
"""
import argparse
from pathlib import Path
from typing import List
class SpecGenerator:
"""Generate PyInstaller spec files for nvda-voicemeeter project."""
# Supported Voicemeeter kinds
KINDS = ['basic', 'banana', 'potato']
def __init__(self, project_root: Path = None):
"""Initialize the spec generator.
Args:
project_root: Path to the project root. If None, uses current directory.
"""
if project_root is None:
# Assume we're running from tools/ directory
project_root = Path(__file__).parent.parent
self.project_root = Path(project_root)
self.spec_dir = self.project_root / 'spec'
# Ensure spec directory exists
self.spec_dir.mkdir(exist_ok=True)
def generate_spec_template(self, kind: str) -> str:
"""Generate a PyInstaller spec file template for the given kind.
Args:
kind: The Voicemeeter kind (basic, banana, potato).
Returns:
The generated spec file content as a string.
"""
if kind not in self.KINDS:
raise ValueError(f'Unsupported kind: {kind}. Supported kinds: {self.KINDS}')
template = f"""# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
added_files = [
( '../controllerClient/x64', 'controllerClient/x64' ),
( '../controllerClient/x86', 'controllerClient/x86' ),
]
a = Analysis(
['{kind}.py'],
pathex=[],
binaries=[],
datas=added_files,
hiddenimports=[],
hookspath=[],
hooksconfig={{}},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='{kind}',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='{kind}',
)
"""
return template
def generate_entry_point(self, kind: str) -> str:
"""Generate a Python entry point script for the given kind.
Args:
kind: The Voicemeeter kind (basic, banana, potato).
Returns:
The generated Python script content as a string.
"""
if kind not in self.KINDS:
raise ValueError(f'Unsupported kind: {kind}. Supported kinds: {self.KINDS}')
entry_point = f"""import voicemeeterlib
import nvda_voicemeeter
KIND_ID = '{kind}'
with voicemeeterlib.api(KIND_ID) as vm:
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
window.run()
"""
return entry_point
def write_spec_file(self, kind: str, overwrite: bool = False) -> Path:
"""Write a spec file for the given kind.
Args:
kind: The Voicemeeter kind (basic, banana, potato).
overwrite: Whether to overwrite existing files.
Returns:
Path to the created spec file.
Raises:
FileExistsError: If file exists and overwrite is False.
"""
spec_file = self.spec_dir / f'{kind}.spec'
entry_file = self.spec_dir / f'{kind}.py'
if spec_file.exists() and not overwrite:
raise FileExistsError(f'Spec file already exists: {spec_file}')
if entry_file.exists() and not overwrite:
raise FileExistsError(f'Entry point file already exists: {entry_file}')
# Write spec file
spec_content = self.generate_spec_template(kind)
spec_file.write_text(spec_content, encoding='utf-8')
print(f'Generated spec file: {spec_file}')
# Write entry point script
entry_content = self.generate_entry_point(kind)
entry_file.write_text(entry_content, encoding='utf-8')
print(f'Generated entry point: {entry_file}')
return spec_file
def generate_all_specs(self, overwrite: bool = False) -> List[Path]:
"""Generate spec files for all supported kinds.
Args:
overwrite: Whether to overwrite existing files.
Returns:
List of paths to the created spec files.
"""
spec_files = []
for kind in self.KINDS:
try:
spec_file = self.write_spec_file(kind, overwrite=overwrite)
spec_files.append(spec_file)
except FileExistsError as e:
print(f'Skipped {kind}: {e}')
continue
return spec_files
def list_existing_specs(self) -> List[str]:
"""List existing spec files in the spec directory.
Returns:
List of existing spec file kinds.
"""
existing = []
for kind in self.KINDS:
spec_file = self.spec_dir / f'{kind}.spec'
if spec_file.exists():
existing.append(kind)
return existing
def main():
"""Main entry point for the spec generator."""
parser = argparse.ArgumentParser(
description='Generate PyInstaller spec files for nvda-voicemeeter project',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python tools/spec_generator.py basic # Generate basic.spec
python tools/spec_generator.py # Generate all spec files
python tools/spec_generator.py --list # List existing spec files
python tools/spec_generator.py --force # Overwrite existing files
""",
)
parser.add_argument(
'kind',
nargs='?',
choices=SpecGenerator.KINDS,
help=f'Voicemeeter kind to generate spec for ({", ".join(SpecGenerator.KINDS)})',
)
parser.add_argument('--force', '-f', action='store_true', help='Overwrite existing spec files')
parser.add_argument('--list', '-l', action='store_true', help='List existing spec files')
parser.add_argument(
'--project-root', type=Path, help='Path to project root (default: detect from script location)'
)
args = parser.parse_args()
# Initialize generator
generator = SpecGenerator(project_root=args.project_root)
# List existing files if requested
if args.list:
existing = generator.list_existing_specs()
if existing:
print('Existing spec files:')
for kind in existing:
spec_file = generator.spec_dir / f'{kind}.spec'
print(f' {kind}: {spec_file}')
else:
print('No existing spec files found.')
return
try:
if args.kind:
# Generate specific kind
generator.write_spec_file(args.kind, overwrite=args.force)
print(f'Successfully generated spec for: {args.kind}')
else:
# Generate all kinds
spec_files = generator.generate_all_specs(overwrite=args.force)
if spec_files:
print(f'Successfully generated {len(spec_files)} spec files.')
else:
print('No spec files were generated (use --force to overwrite existing files).')
except ValueError as e:
print(f'Error: {e}')
return 1
except FileExistsError as e:
print(f'Error: {e}')
print('Use --force to overwrite existing files.')
return 1
return 0
if __name__ == '__main__':
exit(main())