mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-03-19 01:09:12 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b4a76c484 | |||
| dfb96777bb | |||
| 5aa2af2acf | |||
| cc6e187998 | |||
| 1ea1c59f06 | |||
| 054ad040bb | |||
| 94f0b847a7 | |||
| 8d251d1dea | |||
| 5f7d66ceae | |||
| 4ed17a5476 | |||
| b88955a45a | |||
| c3247fa5bf | |||
| 39c14279b2 | |||
| d850581179 | |||
| 47d38e4468 | |||
| 415f2e2ba3 | |||
| 57b28b3507 | |||
| cc0a1de07b | |||
| 9ee0dac610 | |||
| c7b8622aac | |||
| 2706338fcb | |||
| ff98aa5f89 | |||
| 03252fe30f |
237
.github/workflows/release.yml
vendored
Normal file
237
.github/workflows/release.yml
vendored
Normal 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
19
.github/workflows/ruff.yml
vendored
Normal 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
76
.gitignore
vendored
@ -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
5
.pre-commit-config.yaml
Normal 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
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.10.11
|
||||
@ -1,4 +1,4 @@
|
||||
[](https://pdm.fming.dev)
|
||||
[](https://pdm-project.org)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
|
||||
# NVDA Voicemeeter
|
||||
|
||||
76
Taskfile.dynamic.yml
Normal file
76
Taskfile.dynamic.yml
Normal 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
49
Taskfile.yml
Normal 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"
|
||||
@ -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:
|
||||
|
||||
23
build.ps1
23
build.ps1
@ -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 }
|
||||
0
controllerClient/x64/.gitkeep
Normal file
0
controllerClient/x64/.gitkeep
Normal file
Binary file not shown.
0
controllerClient/x86/.gitkeep
Normal file
0
controllerClient/x86/.gitkeep
Normal file
Binary file not shown.
194
pdm.lock
generated
194
pdm.lock
generated
@ -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"},
|
||||
]
|
||||
|
||||
@ -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
0
spec/.gitkeep
Normal 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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
302
tools/dynamic_builder.py
Normal 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
280
tools/spec_generator.py
Normal 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())
|
||||
Loading…
x
Reference in New Issue
Block a user