mirror of
https://github.com/onyx-and-iris/voicemeeter-compact.git
synced 2026-03-21 17:29:11 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5258671a79 | |||
| 2185748435 | |||
| d82afc1973 | |||
| 6be32dcd32 | |||
| abb55d5090 | |||
| 3271a52f15 | |||
| eb2ce5360f | |||
| 45dbcae804 | |||
| 4fab6d9ad9 | |||
| fee3fa199b | |||
| cf3205a86f | |||
| c0416d5b7c | |||
| a952f64bab | |||
| 3f6172c4bf | |||
| 96f3fbbee1 | |||
| 0bc566fa00 | |||
| c9b7f89453 | |||
| bdba07694b | |||
| 462301cd4e | |||
| 768fed217b | |||
| 34299ad84e | |||
| 7a78d7233e | |||
| 971b4a4601 | |||
| b219511ef8 | |||
| 270bda2dc1 | |||
| 6d5bd673a4 | |||
| 16ac188eb4 | |||
| 737dc75cba | |||
| 03d8415f68 | |||
| bd868d4613 | |||
| a65f851403 | |||
| 9e5b5a31a8 | |||
| 7aa8091de6 | |||
| d903faecd9 | |||
| b0d7d734fb | |||
| 262dcd572b | |||
| d612d38933 | |||
| 5a693b8aaf | |||
| 9210a26de6 | |||
| b0f634f1e8 | |||
| 5e5ae33e6a | |||
| 0d04a2f33e | |||
| 81a5497a32 | |||
| edc76db88e | |||
| 3b701a074d | |||
| 66cabb68cf | |||
|
|
37d7e58704 | ||
| 293bccc5ba | |||
| 1d8ffdc756 | |||
| e4d87334cb | |||
| ad3020809e | |||
| 76c6630892 | |||
| cc46fc31f8 | |||
| 8657e8846a | |||
| 43aad156a0 | |||
| 5101ff01f2 | |||
| c437ae5843 | |||
| ae59ba30f9 | |||
| a3fa227ac1 | |||
| b1b6c66828 | |||
| cb00de36f0 | |||
| ae200068d0 | |||
| b720494c68 | |||
| 6e6308a17f | |||
| 848248d02b | |||
| e4fc68c1ad | |||
| 752d1d7dd9 | |||
| 70c225bda3 | |||
| f459cdee44 | |||
| 198c08003e | |||
| 6fa9bf7131 |
53
.github/workflows/publish.yml
vendored
Normal file
53
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pip install poetry==2.3.1
|
||||
poetry --version
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
poetry install --only-root
|
||||
poetry build
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
pypi-publish:
|
||||
needs: build
|
||||
name: Upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/voicemeeter-compact/
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./dist
|
||||
341
.github/workflows/release.yml
vendored
Normal file
341
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,341 @@
|
||||
name: Release Voicemeeter Compact
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags: ['v*.*.*']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: latest
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
with:
|
||||
version: 3.x
|
||||
|
||||
- name: Download Forest TTK Theme
|
||||
run: |
|
||||
# Clone the Forest theme repository
|
||||
git clone https://github.com/rdbende/Forest-ttk-theme.git temp-forest-theme
|
||||
|
||||
# Copy the required theme files to theme/forest
|
||||
Copy-Item "temp-forest-theme\forest-dark.tcl" "theme\forest\"
|
||||
Copy-Item "temp-forest-theme\forest-light.tcl" "theme\forest\"
|
||||
Copy-Item "temp-forest-theme\forest-dark" "theme\forest\" -Recurse
|
||||
Copy-Item "temp-forest-theme\forest-light" "theme\forest\" -Recurse
|
||||
|
||||
# Clean up
|
||||
Remove-Item temp-forest-theme -Recurse -Force
|
||||
shell: pwsh
|
||||
|
||||
- name: Download Azure TTK Theme
|
||||
run: |
|
||||
# Clone the Azure theme repository
|
||||
git clone https://github.com/rdbende/Azure-ttk-theme.git temp-azure-theme
|
||||
|
||||
# Copy the required theme files to theme/azure
|
||||
Copy-Item "temp-azure-theme\azure.tcl" "theme\azure\"
|
||||
Copy-Item "temp-azure-theme\theme" "theme\azure\" -Recurse
|
||||
|
||||
# Clean up
|
||||
Remove-Item temp-azure-theme -Recurse -Force
|
||||
shell: pwsh
|
||||
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- name: Install Poetry plugins
|
||||
run: poetry self add poethepoet
|
||||
shell: bash
|
||||
|
||||
- name: Replace path dependencies with PyPI versions
|
||||
run: |
|
||||
poetry remove voicemeeter-api vban-cmd || true
|
||||
poetry add voicemeeter-api vban-cmd
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
# Install project dependencies
|
||||
poetry install --with build
|
||||
|
||||
# Verify PyInstaller is available
|
||||
echo "Verifying PyInstaller installation..."
|
||||
poetry show pyinstaller
|
||||
|
||||
- name: Get Poetry executable path
|
||||
shell: bash
|
||||
run: |
|
||||
poetryPath=$(which poetry)
|
||||
echo "Poetry path: $poetryPath"
|
||||
echo "POETRY_BIN=$poetryPath" >> $GITHUB_ENV
|
||||
|
||||
- name: Build artifacts with dynamic taskfile
|
||||
run: task --taskfile Taskfile.dynamic.yml build-all
|
||||
shell: bash
|
||||
env:
|
||||
POETRY_BIN: ${{ env.POETRY_BIN }}
|
||||
|
||||
- name: Create release archives
|
||||
run: task --taskfile Taskfile.dynamic.yml compress-all
|
||||
shell: bash
|
||||
env:
|
||||
POETRY_BIN: ${{ env.POETRY_BIN }}
|
||||
|
||||
- name: Verify build outputs
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Verifying build outputs..."
|
||||
|
||||
$expectedFiles = @(
|
||||
"dist/sunvalley-basic.zip",
|
||||
"dist/sunvalley-banana.zip",
|
||||
"dist/sunvalley-potato.zip",
|
||||
"dist/forest-dark-basic.zip",
|
||||
"dist/forest-dark-banana.zip",
|
||||
"dist/forest-dark-potato.zip",
|
||||
"dist/forest-light-basic.zip",
|
||||
"dist/forest-light-banana.zip",
|
||||
"dist/forest-light-potato.zip",
|
||||
"dist/azure-dark-basic.zip",
|
||||
"dist/azure-dark-banana.zip",
|
||||
"dist/azure-dark-potato.zip",
|
||||
"dist/azure-light-basic.zip",
|
||||
"dist/azure-light-banana.zip",
|
||||
"dist/azure-light-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!"
|
||||
|
||||
# Sunvalley theme variants
|
||||
- name: Upload build artifacts - Sunvalley Basic
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sunvalley-basic
|
||||
path: dist/sunvalley-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Sunvalley Banana
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sunvalley-banana
|
||||
path: dist/sunvalley-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Sunvalley Potato
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sunvalley-potato
|
||||
path: dist/sunvalley-potato.zip
|
||||
|
||||
# Forest theme variants (dark)
|
||||
- name: Upload build artifacts - Forest Basic Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-dark-basic
|
||||
path: dist/forest-dark-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Banana Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-dark-banana
|
||||
path: dist/forest-dark-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Potato Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-dark-potato
|
||||
path: dist/forest-dark-potato.zip
|
||||
|
||||
# Forest theme variants (light)
|
||||
- name: Upload build artifacts - Forest Basic Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-light-basic
|
||||
path: dist/forest-light-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Banana Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-light-banana
|
||||
path: dist/forest-light-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Forest Potato Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forest-light-potato
|
||||
path: dist/forest-light-potato.zip
|
||||
|
||||
# Azure theme variants (dark)
|
||||
- name: Upload build artifacts - Azure Basic Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-dark-basic
|
||||
path: dist/azure-dark-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Banana Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-dark-banana
|
||||
path: dist/azure-dark-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Potato Dark
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-dark-potato
|
||||
path: dist/azure-dark-potato.zip
|
||||
|
||||
# Azure theme variants (light)
|
||||
- name: Upload build artifacts - Azure Basic Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-light-basic
|
||||
path: dist/azure-light-basic.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Banana Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-light-banana
|
||||
path: dist/azure-light-banana.zip
|
||||
|
||||
- name: Upload build artifacts - Azure Potato Light
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: azure-light-potato
|
||||
path: dist/azure-light-potato.zip
|
||||
|
||||
- name: Build Summary
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host -ForegroundColor Green "Build completed successfully!"
|
||||
Write-Host ""
|
||||
Write-Host "Built artifacts (15 theme variants):"
|
||||
Write-Host " Sunvalley Theme:"
|
||||
Write-Host " - voicemeeter-compact-sunvalley-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-sunvalley-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-sunvalley-potato.zip"
|
||||
Write-Host " Forest Theme:"
|
||||
Write-Host " - voicemeeter-compact-forest-dark-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-dark-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-dark-potato.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-light-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-light-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-forest-light-potato.zip"
|
||||
Write-Host " Azure Theme:"
|
||||
Write-Host " - voicemeeter-compact-azure-dark-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-dark-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-dark-potato.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-light-basic.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-light-banana.zip"
|
||||
Write-Host " - voicemeeter-compact-azure-light-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 "Voicemeeter Compact $TAG_NAME" \
|
||||
--notes "## Voicemeeter Compact Release $TAG_NAME
|
||||
|
||||
### Theme Variants
|
||||
Choose your preferred theme and Voicemeeter version:
|
||||
|
||||
**Sunvalley Theme**
|
||||
- **sunvalley-basic.zip** - For Voicemeeter Basic
|
||||
- **sunvalley-banana.zip** - For Voicemeeter Banana
|
||||
- **sunvalley-potato.zip** - For Voicemeeter Potato
|
||||
|
||||
**Forest Theme**
|
||||
- **forest-dark-basic.zip** - Dark theme for Voicemeeter Basic
|
||||
- **forest-dark-banana.zip** - Dark theme for Voicemeeter Banana
|
||||
- **forest-dark-potato.zip** - Dark theme for Voicemeeter Potato
|
||||
- **forest-light-basic.zip** - Light theme for Voicemeeter Basic
|
||||
- **forest-light-banana.zip** - Light theme for Voicemeeter Banana
|
||||
- **forest-light-potato.zip** - Light theme for Voicemeeter Potato
|
||||
|
||||
**Azure Theme**
|
||||
- **azure-dark-basic.zip** - Dark theme for Voicemeeter Basic
|
||||
- **azure-dark-banana.zip** - Dark theme for Voicemeeter Banana
|
||||
- **azure-dark-potato.zip** - Dark theme for Voicemeeter Potato
|
||||
- **azure-light-basic.zip** - Light theme for Voicemeeter Basic
|
||||
- **azure-light-banana.zip** - Light theme for Voicemeeter Banana
|
||||
- **azure-light-potato.zip** - Light theme for Voicemeeter Potato
|
||||
|
||||
### Requirements
|
||||
- Windows 10/11
|
||||
- Voicemeeter (Basic/Banana/Potato) installed
|
||||
- Python 3.10+ (if running from source)
|
||||
|
||||
### Installation
|
||||
1. Download the zip file matching your Voicemeeter version and preferred theme
|
||||
2. Extract and run the executable - no installation required
|
||||
3. The application will automatically detect your Voicemeeter installation
|
||||
|
||||
### Notes
|
||||
- Built with PyInstaller for standalone execution
|
||||
- Each variant is scaled for its specific Voicemeeter version
|
||||
- Themes provide different visual styles while maintaining full functionality"
|
||||
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@v6
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
args: 'format --check --diff'
|
||||
115
.gitignore
vendored
115
.gitignore
vendored
@ -1,9 +1,9 @@
|
||||
# quick test
|
||||
quick.py
|
||||
# Generated by ignr: github.com/onyx-and-iris/ignr
|
||||
|
||||
## Python ##
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
@ -23,7 +23,6 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
@ -50,9 +49,10 @@ htmlcov/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@ -75,6 +75,7 @@ instance/
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
@ -85,7 +86,9 @@ profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# 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
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
@ -94,7 +97,37 @@ ipython_config.py
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
# 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.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__/
|
||||
|
||||
# Celery stuff
|
||||
@ -106,13 +139,13 @@ celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
venv_vmcompact/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@ -132,11 +165,65 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# build
|
||||
sv_ttk/
|
||||
theme/
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
sv_*.py
|
||||
fst_*.py
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
.vscode/
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# 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/
|
||||
|
||||
# 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/
|
||||
|
||||
# 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
|
||||
|
||||
# Test files
|
||||
test-*.py
|
||||
|
||||
# Forest/Azure theme files
|
||||
theme/**/*
|
||||
!theme/*/
|
||||
!theme/**/.gitkeep
|
||||
|
||||
# Spec files
|
||||
spec/**/*
|
||||
!spec/*/
|
||||
!spec/**/.gitkeep
|
||||
|
||||
# Taskfile build files
|
||||
Taskfile.unified.yml
|
||||
SPEC_CONSOLIDATION.md
|
||||
|
||||
13
.pre-commit-config.yaml
Normal file
13
.pre-commit-config.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: '2.3.2'
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
- id: poetry-lock
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@ -7,7 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- [ ] Add support for forest theme (if rbende adds it to pypi)
|
||||
- [ ]
|
||||
|
||||
## [1.10.0] - 2026-03-26
|
||||
|
||||
### Added
|
||||
|
||||
- Automated builds for Releases. This is much preferred over manual releases because users can be sure the files are built directly from the source code.
|
||||
- Azure theme added to Releases.
|
||||
- vban.toml files can now use key `host` intead of `ip`.
|
||||
- `ip` is still usable for backwards compatibility.
|
||||
|
||||
### Changed
|
||||
|
||||
- Attempting a VBAN connection now uses a PING/PONG handshake to verify connection, this makes connections more reliable.
|
||||
- Navigation frame is disabled by default. You can easily enable it from the menu or with an app.toml config.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Comp, Gate sliders now receive feedback when changes are made on the Voicemeeter GUI.
|
||||
- Bus CONFIG mode button rotates through the correct modes for Basic Kind.
|
||||
- Bus CONFIG mono now rotates through *off, on, stereo reverse*.
|
||||
- Bus CONFIG mode/mono buttons are now a fixed width.
|
||||
|
||||
## [1.9.8] - 2025-01-22
|
||||
|
||||
### Changed
|
||||
|
||||
- vm-compact config dirs now override _internal/configs (if using build from releases). See [TOML Files](https://github.com/onyx-and-iris/voicemeeter-compact?tab=readme-ov-file#toml-files) section in README.
|
||||
- after disconnecting from a vban connection, vban menus are re-enabled after 500ms.
|
||||
|
||||
## [1.9.5] - 2024-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Launching the Voicemeeter Compact app will now launch the x64 bit Voicemeeter GUI (on 64 bit systems) for all kinds.
|
||||
|
||||
## [1.9.0] - 2023-07-10
|
||||
|
||||
|
||||
31
README.md
31
README.md
@ -1,7 +1,7 @@
|
||||
[](https://badge.fury.io/py/voicemeeter-compact)
|
||||
[](https://github.com/onyx-and-iris/voicemeeter-compact/blob/main/LICENSE)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||
|
||||

|
||||
@ -31,12 +31,13 @@ Example `__main__.py` file:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
# choose the kind of Voicemeeter (Local connection)
|
||||
KIND_ID = "banana"
|
||||
KIND_ID = 'banana'
|
||||
|
||||
# pass the KIND_ID and the vm object to the app
|
||||
with voicemeeterlib.api(KIND_ID) as vm:
|
||||
@ -44,7 +45,7 @@ def main():
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
@ -64,15 +65,18 @@ Set the kind of Voicemeeter, KIND_ID may be:
|
||||
|
||||
## TOML Files
|
||||
|
||||
This is how your files should be organised. Wherever your `__main__.py` file is located (after install this can be any location), `configs` should be in the same location.
|
||||
Directly inside of configs directory you may place an app.toml, vban.toml and a directory for each kind.
|
||||
Inside each kind directory you may place as many custom toml configurations as you wish.
|
||||
If you've downloaded the binary from [Releases][releases] you can find configs included in the `_internal/configs` directory.
|
||||
|
||||
You may override these configs by placing a directory `vm-compact` in one of the following locations:
|
||||
|
||||
- `user home directory / .config`
|
||||
- `user home directory / Documents / Voicemeeter`
|
||||
|
||||
The contents should match the following directory structure:
|
||||
|
||||
.
|
||||
|
||||
├── `__main__.py`
|
||||
|
||||
├── configs
|
||||
├── vm-compact
|
||||
|
||||
├── app.toml
|
||||
|
||||
@ -110,7 +114,7 @@ Configure certain startup states for the app.
|
||||
Configure a user config to load on app startup. Don't include the .toml extension in the config name.
|
||||
|
||||
- `theme`
|
||||
By default the app loads up the [Sun Valley light or dark theme][def] by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
|
||||
By default the app loads up the [Sun Valley light or dark theme][releases] by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
|
||||
|
||||
- `extends`
|
||||
Extending the app will show both strips and buses. In reduced mode only one or the other. This app will extend both horizontally and vertically, simply set `extends_horizontal` true or false accordingly.
|
||||
@ -135,13 +139,13 @@ A valid `vban.toml` might look like this:
|
||||
```toml
|
||||
[connection-1]
|
||||
kind = 'banana'
|
||||
ip = '192.168.1.2'
|
||||
host = '192.168.1.2'
|
||||
streamname = 'worklaptop'
|
||||
port = 6980
|
||||
|
||||
[connection-2]
|
||||
kind = 'potato'
|
||||
ip = '192.168.1.3'
|
||||
host = '192.168.1.3'
|
||||
streamname = 'streampc'
|
||||
port = 6990
|
||||
```
|
||||
@ -163,4 +167,5 @@ User configs may be loaded at any time via the menu.
|
||||
[Rdbende](https://github.com/rdbende) for creating the beautiful [Sun Valley theme][sv-theme].
|
||||
|
||||
|
||||
[sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme
|
||||
[sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme
|
||||
[releases]: https://github.com/onyx-and-iris/voicemeeter-compact/releases
|
||||
38
Taskfile.azure.yml
Normal file
38
Taskfile.azure.yml
Normal file
@ -0,0 +1,38 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build Azure artifacts
|
||||
deps: [rewrite]
|
||||
cmds:
|
||||
- defer: { task: restore }
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [azure-light, azure-dark]
|
||||
cmd: poetry run pyinstaller --noconfirm --distpath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} spec/azure/{{.ITEM.THEME}}-{{.ITEM.KIND}}.spec
|
||||
|
||||
rewrite:
|
||||
desc: Run the source code rewriter
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --rewrite --theme {{.THEME}}
|
||||
|
||||
restore:
|
||||
desc: Restore the backup files
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --restore
|
||||
|
||||
compress:
|
||||
desc: Compress Azure artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [azure-light, azure-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean build and dist directories
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/azure-*,dist/azure-* -Recurse -Force"
|
||||
84
Taskfile.dynamic.yml
Normal file
84
Taskfile.dynamic.yml
Normal file
@ -0,0 +1,84 @@
|
||||
version: '3'
|
||||
|
||||
# Dynamic build system - no spec files needed!
|
||||
# Examples:
|
||||
# - task -t Taskfile.dynamic.yml build THEMES="azure forest"
|
||||
# - task -t Taskfile.dynamic.yml build-all
|
||||
# THEMES can be specified as a space-separated list or "all" to build everything.
|
||||
#
|
||||
# Compression tasks are also dynamic and can be used like:
|
||||
# Usage examples:
|
||||
# - task -t Taskfile.dynamic.yml compress THEME=azure
|
||||
# - task -t Taskfile.dynamic.yml compress-all
|
||||
|
||||
vars:
|
||||
THEMES: '{{.THEMES | default "all"}}'
|
||||
SHELL: pwsh
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build specified themes dynamically (no spec files needed)
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py {{.THEMES}}
|
||||
|
||||
build-all:
|
||||
desc: Build all themes
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py all
|
||||
|
||||
build-azure:
|
||||
desc: Build only azure theme
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py azure
|
||||
|
||||
build-forest:
|
||||
desc: Build only forest theme
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py forest
|
||||
|
||||
build-sunvalley:
|
||||
desc: Build only sunvalley theme
|
||||
cmds:
|
||||
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py sunvalley
|
||||
|
||||
compress:
|
||||
desc: Compress artifacts for specified theme
|
||||
cmds:
|
||||
- task: compress-{{.THEME}}
|
||||
|
||||
compress-all:
|
||||
desc: Compress artifacts for all themes
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [azure, forest, sunvalley]
|
||||
task: compress-{{.ITEM.THEME}}
|
||||
|
||||
compress-azure:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
VARIANT: [azure-light, azure-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
compress-forest:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
VARIANT: [forest-light, forest-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
compress-sunvalley:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/sunvalley-{{.ITEM.KIND}} -DestinationPath dist/sunvalley-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean all build artifacts
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"
|
||||
38
Taskfile.forest.yml
Normal file
38
Taskfile.forest.yml
Normal file
@ -0,0 +1,38 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build Forest artifacts
|
||||
deps: [rewrite]
|
||||
cmds:
|
||||
- defer: { task: restore }
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [forest-light, forest-dark]
|
||||
cmd: poetry run pyinstaller --noconfirm --distpath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} spec/forest/{{.ITEM.THEME}}-{{.ITEM.KIND}}.spec
|
||||
|
||||
rewrite:
|
||||
desc: Run the source code rewriter
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --rewrite --theme {{.THEME}}
|
||||
|
||||
restore:
|
||||
desc: Restore the backup files
|
||||
cmds:
|
||||
- poetry run python tools/rewriter.py --restore
|
||||
|
||||
compress:
|
||||
desc: Compress Forest artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
THEME: [forest-light, forest-dark]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean build and dist directories
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/forest-*,dist/forest-* -Recurse -Force"
|
||||
24
Taskfile.sunvalley.yml
Normal file
24
Taskfile.sunvalley.yml
Normal file
@ -0,0 +1,24 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build Sunvalley artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
cmd: poetry run pyinstaller --noconfirm --distpath dist/sunvalley-{{.ITEM.KIND}} spec/sunvalley/sunvalley-{{.ITEM.KIND}}.spec
|
||||
|
||||
compress:
|
||||
desc: Compress Sunvalley artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/sunvalley-{{.ITEM.KIND}} -DestinationPath dist/sunvalley-{{.ITEM.KIND}}.zip -Force"'
|
||||
|
||||
clean:
|
||||
desc: Clean build and dist directories
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/sunvalley-*,dist/sunvalley-* -Recurse -Force"
|
||||
61
Taskfile.yml
Normal file
61
Taskfile.yml
Normal file
@ -0,0 +1,61 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
sunvalley:
|
||||
taskfile: ./Taskfile.sunvalley.yml
|
||||
vars:
|
||||
THEME: sunvalley
|
||||
forest:
|
||||
taskfile: ./Taskfile.forest.yml
|
||||
vars:
|
||||
THEME: forest
|
||||
azure:
|
||||
taskfile: ./Taskfile.azure.yml
|
||||
vars:
|
||||
THEME: azure
|
||||
|
||||
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
|
||||
- echo "Release complete"
|
||||
|
||||
generate-specs:
|
||||
desc: Generate all spec files from templates
|
||||
cmds:
|
||||
- poetry run python tools/spec_generator.py --clean
|
||||
|
||||
build:
|
||||
desc: Build all artifacts
|
||||
deps: [generate-specs]
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [sunvalley, forest, azure]
|
||||
task: '{{.ITEM.THEME}}:build'
|
||||
|
||||
compress:
|
||||
desc: Compress all artifacts
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [sunvalley, forest, azure]
|
||||
task: '{{.ITEM.THEME}}:compress'
|
||||
|
||||
clean:
|
||||
desc: Clean up build and dist directories
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
THEME: [sunvalley, forest, azure]
|
||||
task: '{{.ITEM.THEME}}:clean'
|
||||
@ -4,12 +4,12 @@ import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = "banana"
|
||||
KIND_ID = 'banana'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
40
build.ps1
40
build.ps1
@ -1,40 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$prefix,
|
||||
[string]$theme
|
||||
)
|
||||
|
||||
function Format-Path {
|
||||
param($Kind)
|
||||
return @(
|
||||
$prefix,
|
||||
(& { if ($theme) { $theme } else { "" } }),
|
||||
"${Kind}"
|
||||
).Where({ $_ -ne "" }) -Join "_"
|
||||
}
|
||||
|
||||
function Compress-Builds {
|
||||
$target = Join-Path -Path $PSScriptRoot -ChildPath "dist"
|
||||
@("basic", "banana", "potato") | ForEach-Object {
|
||||
$compress_path = Format-Path -Kind $_
|
||||
Compress-Archive -Path $(Join-Path -Path $target -ChildPath $compress_path) -DestinationPath $(Join-Path -Path $target -ChildPath "${compress_path}.zip") -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Builds {
|
||||
@("basic", "banana", "potato") | ForEach-Object {
|
||||
$spec_path = Format-Path -Kind $_
|
||||
|
||||
"building $spec_path" | Write-Host
|
||||
|
||||
poetry run pyinstaller "$spec_path.spec" --noconfirm
|
||||
}
|
||||
}
|
||||
|
||||
function main {
|
||||
Get-Builds
|
||||
|
||||
Compress-Builds
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -ne '.') { main }
|
||||
@ -1,10 +1,9 @@
|
||||
# load a specific profile on start (file name without .toml ext)
|
||||
# [configs]
|
||||
# config="example"
|
||||
# load with themes enabled? set the default mode
|
||||
# load with themes enabled?
|
||||
[theme]
|
||||
enabled = true
|
||||
mode = "light"
|
||||
# load in extended mode? if so which orientation
|
||||
[extends]
|
||||
extended = true
|
||||
@ -22,4 +21,4 @@ size = 3
|
||||
default = 0
|
||||
# show the navigation frame?
|
||||
[navigation]
|
||||
show = true
|
||||
show = false
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
### set the ip then uncomment
|
||||
# [connection-1]
|
||||
# kind = 'banana'
|
||||
# ip = '<ip address 1>'
|
||||
# ip = 'localhost'
|
||||
# streamname = 'Command1'
|
||||
# port = 6980
|
||||
|
||||
# [connection-2]
|
||||
# kind = 'potato'
|
||||
# ip = '<ip address 2>'
|
||||
# ip = 'gamepc.local'
|
||||
# streamname = 'Command1'
|
||||
# port = 6980
|
||||
|
||||
323
poetry.lock
generated
323
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
@ -6,102 +6,20 @@ version = "0.17.4"
|
||||
description = "Python graph (network) package"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
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"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "24.4.2"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
|
||||
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
|
||||
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
|
||||
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
|
||||
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
|
||||
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
|
||||
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
|
||||
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
|
||||
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
|
||||
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
|
||||
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
|
||||
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
|
||||
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
|
||||
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
|
||||
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
|
||||
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
|
||||
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
|
||||
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
|
||||
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
|
||||
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
|
||||
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
|
||||
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
description = "Mach-O header analysis and editing"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["build"]
|
||||
markers = "sys_platform == \"darwin\""
|
||||
files = [
|
||||
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
|
||||
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
|
||||
@ -110,37 +28,16 @@ files = [
|
||||
[package.dependencies]
|
||||
altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -149,54 +46,41 @@ version = "2023.2.7"
|
||||
description = "Python PE parsing module"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
groups = ["build"]
|
||||
markers = "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"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.2"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
|
||||
type = ["mypy (>=1.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.8.0"
|
||||
version = "6.11.1"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.8"
|
||||
python-versions = "<3.14,>=3.8"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
|
||||
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2024.6"
|
||||
pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2024.9"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
@ -206,13 +90,14 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.7"
|
||||
version = "2024.11"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.11-py3-none-any.whl", hash = "sha256:2781d121a1ee961152ba7287a262c65a1078da30c9ef7621cb8c819326884fd5"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.11.tar.gz", hash = "sha256:84399af6b4b902030958063df25f657abbff249d0f329c5344928355c9833ab4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -221,29 +106,65 @@ setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["build"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
|
||||
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
|
||||
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
|
||||
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.1"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"},
|
||||
{file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"},
|
||||
{file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"},
|
||||
{file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"},
|
||||
{file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "70.2.0"
|
||||
version = "78.1.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["build"]
|
||||
files = [
|
||||
{file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"},
|
||||
{file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"},
|
||||
{file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"},
|
||||
{file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
|
||||
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "sv-ttk"
|
||||
@ -251,6 +172,7 @@ version = "2.6.0"
|
||||
description = "A gorgeous theme for Tkinter, based on Windows 11's UI"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sv_ttk-2.6.0-py3-none-any.whl", hash = "sha256:4319c52edf2e14732fe84bdc9788e26f9e9a1ad79451ec0f89f0120ffc8105d9"},
|
||||
{file = "sv_ttk-2.6.0.tar.gz", hash = "sha256:3fd440396c95e30e88f686fcf28be425480f7320d6bf346f9cea5d6f56702cc2"},
|
||||
@ -258,55 +180,82 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
version = "2.2.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vban-cmd"
|
||||
version = "2.4.11"
|
||||
version = "2.10.3"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
optional = false
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = [
|
||||
{file = "vban_cmd-2.4.11-py3-none-any.whl", hash = "sha256:a74b7631222f340488f5d45bc9aa9d4e7a0f919687c9715619e8809c684875b7"},
|
||||
{file = "vban_cmd-2.4.11.tar.gz", hash = "sha256:250ca8043f075eee11d2a811142d0205900d14d7e2f0cd98eb597901ead738f5"},
|
||||
]
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../vban-cmd-python"
|
||||
|
||||
[[package]]
|
||||
name = "voicemeeter-api"
|
||||
version = "2.6.0"
|
||||
version = "2.7.2"
|
||||
description = "A Python wrapper for the Voiceemeter API"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.10"
|
||||
files = [
|
||||
{file = "voicemeeter_api-2.6.0-py3-none-any.whl", hash = "sha256:c2ef8eb063ce3aeac4827ad7883150c407a0effb0fde3778782cd3024a295255"},
|
||||
{file = "voicemeeter_api-2.6.0.tar.gz", hash = "sha256:db93f27b58ce927c7d56084b224e1d0cdd6406ab7f26e4f99a9a873495a3d2e7"},
|
||||
]
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../voicemeeter-api-python"
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "343eb64c79bba2d92eeb9af614c782dd175bd8a7b1a236a330b1dd9ae4d18a57"
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "f1e1782280c5e165fef043ca2695ea5f5c93fd00a66ace809266e0196fef6b71"
|
||||
|
||||
133
pyproject.toml
133
pyproject.toml
@ -1,35 +1,128 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "voicemeeter-compact"
|
||||
version = "1.9.5"
|
||||
version = "1.10.0"
|
||||
description = "A Compact Voicemeeter Remote App"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"voicemeeter-api (>=2.7.2,<3.0.0)",
|
||||
"vban-cmd (>=2.10.2,<3.0.0)",
|
||||
"sv-ttk (>=2.6.0,<3.0.0)",
|
||||
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
voicemeeter-compact-basic = "vmcompact.gui.basic:run"
|
||||
voicemeeter-compact-banana = "vmcompact.gui.banana:run"
|
||||
voicemeeter-compact-potato = "vmcompact.gui.potato:run"
|
||||
|
||||
[tool.poetry]
|
||||
packages = [{ include = "vmcompact" }]
|
||||
include = ["vmcompact/img/cat.ico"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.13"
|
||||
sv-ttk = "^2.6.0"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
voicemeeter-api = "^2.6.0"
|
||||
vban-cmd = "^2.4.11"
|
||||
[tool.poetry.requires-plugins]
|
||||
poethepoet = ">=0.42.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = { version = ">=22.6,<25.0", allow-prereleases = true }
|
||||
isort = "^5.12.0"
|
||||
|
||||
ruff = "^0.9.1"
|
||||
voicemeeter-api = { path = "../voicemeeter-api-python/", develop = true }
|
||||
vban-cmd = { path = "../vban-cmd-python/", develop = true }
|
||||
|
||||
[tool.poetry.group.build.dependencies]
|
||||
pyinstaller = "^6.3.0"
|
||||
pyinstaller = "^6.11.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
build_sunvalley = "scripts:build_sunvalley"
|
||||
build_forest = "scripts:build_forest"
|
||||
build_all = "scripts:build_all"
|
||||
[tool.poe.tasks]
|
||||
build-sunvalley = "task build-sunvalley"
|
||||
build-forest = "task build-forest"
|
||||
release = [
|
||||
{ ref = "build-sunvalley" },
|
||||
{ ref = "build-forest" },
|
||||
{ cmd = "task compress-sunvalley" },
|
||||
{ cmd = "task compress-forest" },
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.10
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
|
||||
[tool.ruff.format]
|
||||
# Unlike Black, use single quotes for strings.
|
||||
quote-style = "single"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
# Enable auto-formatting of code examples in docstrings. Markdown,
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
#
|
||||
# This is currently disabled by default, but it is planned for this
|
||||
# to be opt-out in the future.
|
||||
docstring-code-format = false
|
||||
|
||||
# Set the line length limit used when formatting code snippets in
|
||||
# docstrings.
|
||||
#
|
||||
# This only has an effect when the `docstring-code-format` setting is
|
||||
# enabled.
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["E402", "F401"]
|
||||
|
||||
24
scripts.py
24
scripts.py
@ -1,24 +0,0 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def build_sunvalley():
|
||||
buildscript = Path.cwd() / "build.ps1"
|
||||
subprocess.run(["powershell", str(buildscript), "sv"])
|
||||
|
||||
|
||||
def build_forest():
|
||||
rewriter = Path.cwd() / "tools" / "rewriter.py"
|
||||
subprocess.run([sys.executable, str(rewriter), "-r"])
|
||||
|
||||
buildscript = Path.cwd() / "build.ps1"
|
||||
for theme in ("light", "dark"):
|
||||
subprocess.run(["powershell", str(buildscript), "fst", theme])
|
||||
|
||||
subprocess.run([sys.executable, str(rewriter), "-c"])
|
||||
|
||||
|
||||
def build_all():
|
||||
steps = (build_sunvalley, build_forest)
|
||||
[step() for step in steps]
|
||||
0
spec/azure/.gitkeep
Normal file
0
spec/azure/.gitkeep
Normal file
0
spec/forest/.gitkeep
Normal file
0
spec/forest/.gitkeep
Normal file
0
spec/sunvalley/.gitkeep
Normal file
0
spec/sunvalley/.gitkeep
Normal file
0
theme/azure/.gitkeep
Normal file
0
theme/azure/.gitkeep
Normal file
0
theme/forest/.gitkeep
Normal file
0
theme/forest/.gitkeep
Normal file
322
tools/dynamic_builder.py
Normal file
322
tools/dynamic_builder.py
Normal file
@ -0,0 +1,322 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dynamic build system for voicemeeter-compact.
|
||||
Generates spec files on-the-fly and builds executables without storing intermediate files.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
# Build configuration
|
||||
THEMES = {
|
||||
'azure': ['azure-light', 'azure-dark'],
|
||||
'forest': ['forest-light', 'forest-dark'],
|
||||
'sunvalley': ['sunvalley'],
|
||||
}
|
||||
|
||||
KINDS = ['basic', 'banana', 'potato']
|
||||
|
||||
# Templates (same as spec_generator.py)
|
||||
PYTHON_TEMPLATE = """import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = '{kind}'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg}
|
||||
app = vmcompact.connect(KIND_ID, vmr{theme_param})
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
"""
|
||||
|
||||
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( '{img_path}', 'img' ),{theme_files}
|
||||
( '{config_path}', 'configs' ),
|
||||
]
|
||||
|
||||
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='{theme_variant}-{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='{theme_variant}-{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 __enter__(self):
|
||||
self.temp_dir = Path(tempfile.mkdtemp(prefix='vmcompact_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, theme_variant: str, kind: str) -> Path:
|
||||
"""Create a temporary Python launcher file."""
|
||||
if theme_variant == 'sunvalley':
|
||||
theme_arg = ''
|
||||
theme_param = ''
|
||||
else:
|
||||
theme_arg = f"\n theme = '{theme_variant}'"
|
||||
theme_param = ', theme=theme'
|
||||
|
||||
content = PYTHON_TEMPLATE.format(
|
||||
kind=kind, theme_arg=theme_arg, theme_param=theme_param
|
||||
)
|
||||
|
||||
py_file = self.temp_dir / f'{theme_variant}-{kind}.py'
|
||||
with open(py_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return py_file
|
||||
|
||||
def create_spec_file(self, theme_variant: str, kind: str, py_file: Path) -> Path:
|
||||
"""Create a temporary PyInstaller spec file."""
|
||||
if theme_variant == 'sunvalley':
|
||||
theme_files = ''
|
||||
else:
|
||||
theme_base = theme_variant.split('-')[0]
|
||||
theme_path = (self.base_dir / 'theme' / theme_base).as_posix()
|
||||
theme_files = f"\n ( '{theme_path}', 'theme' ),"
|
||||
|
||||
content = SPEC_TEMPLATE.format(
|
||||
script_path=py_file.as_posix(),
|
||||
img_path=(self.base_dir / 'vmcompact' / 'img').as_posix(),
|
||||
config_path=(self.base_dir / 'configs').as_posix(),
|
||||
theme_files=theme_files,
|
||||
kind=kind,
|
||||
theme_variant=theme_variant,
|
||||
)
|
||||
|
||||
spec_file = self.temp_dir / f'{theme_variant}-{kind}.spec'
|
||||
with open(spec_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return spec_file
|
||||
|
||||
def build_variant(self, theme_variant: str, kind: str) -> bool:
|
||||
"""Build a single theme/kind variant."""
|
||||
print(f'Building {theme_variant}-{kind}...')
|
||||
|
||||
# Create temporary files
|
||||
py_file = self.create_python_file(theme_variant, kind)
|
||||
spec_file = self.create_spec_file(theme_variant, kind, py_file)
|
||||
|
||||
# Build with PyInstaller
|
||||
dist_path = self.dist_dir / f'{theme_variant}-{kind}'
|
||||
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
|
||||
cmd = [
|
||||
poetry_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 {theme_variant}-{kind}')
|
||||
return True
|
||||
else:
|
||||
print(f'[FAIL] Failed to build {theme_variant}-{kind}')
|
||||
print(f'Error: {result.stderr}')
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Exception building {theme_variant}-{kind}: {e}')
|
||||
return False
|
||||
|
||||
def build_theme(self, theme_family: str) -> Dict[str, bool]:
|
||||
"""Build all variants for a theme family."""
|
||||
results = {}
|
||||
|
||||
if theme_family not in THEMES:
|
||||
print(f'Unknown theme: {theme_family}')
|
||||
return results
|
||||
|
||||
variants = THEMES[theme_family]
|
||||
|
||||
for variant in variants:
|
||||
for kind in KINDS:
|
||||
success = self.build_variant(variant, kind)
|
||||
results[f'{variant}-{kind}'] = success
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def run_rewriter(theme_family: str, base_dir: Path) -> bool:
|
||||
"""Run the theme rewriter if needed."""
|
||||
if theme_family in ['azure', 'forest']:
|
||||
print(f'Running rewriter for {theme_family} theme...')
|
||||
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
|
||||
cmd = [
|
||||
poetry_bin,
|
||||
'run',
|
||||
'python',
|
||||
'tools/rewriter.py',
|
||||
'--rewrite',
|
||||
'--theme',
|
||||
theme_family,
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, cwd=base_dir)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f'Rewriter failed: {e}')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def restore_rewriter(base_dir: Path) -> bool:
|
||||
"""Restore files after rewriter."""
|
||||
print('Restoring rewriter changes...')
|
||||
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
|
||||
cmd = [poetry_bin, 'run', 'python', 'tools/rewriter.py', '--restore']
|
||||
try:
|
||||
result = subprocess.run(cmd, cwd=base_dir)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f'Restore failed: {e}')
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Dynamic build system for voicemeeter-compact'
|
||||
)
|
||||
parser.add_argument(
|
||||
'themes',
|
||||
nargs='*',
|
||||
choices=list(THEMES.keys()) + ['all'],
|
||||
help='Themes 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.themes or 'all' in args.themes:
|
||||
themes_to_build = list(THEMES.keys())
|
||||
else:
|
||||
themes_to_build = args.themes
|
||||
|
||||
base_dir = Path.cwd()
|
||||
args.dist_dir.mkdir(exist_ok=True)
|
||||
|
||||
print(f'Building themes: {", ".join(themes_to_build)}')
|
||||
|
||||
all_results = {}
|
||||
|
||||
with DynamicBuilder(base_dir, args.dist_dir) as builder:
|
||||
for theme_family in themes_to_build:
|
||||
# Run rewriter if needed
|
||||
if not run_rewriter(theme_family, base_dir):
|
||||
print(f'Skipping {theme_family} due to rewriter failure')
|
||||
continue
|
||||
|
||||
try:
|
||||
# Build theme
|
||||
results = builder.build_theme(theme_family)
|
||||
all_results.update(results)
|
||||
|
||||
finally:
|
||||
# Always restore rewriter changes
|
||||
if theme_family in ['azure', 'forest']:
|
||||
restore_rewriter(base_dir)
|
||||
|
||||
# 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()
|
||||
@ -1,14 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rewrites app.py, builders.py, menu.py, and navigation.py to remove sv_ttk dependencies and apply theme changes for the build process.
|
||||
Also provides a cleanup function to restore the original files after building.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger("vm-compact-rewriter")
|
||||
logger = logging.getLogger('vm-compact-rewriter')
|
||||
|
||||
PACKAGE_DIR = Path(__file__).parent.parent / "vmcompact"
|
||||
PACKAGE_DIR = Path(__file__).parent.parent / 'vmcompact'
|
||||
|
||||
SRC_DIR = Path(__file__).parent / "src"
|
||||
SRC_DIR = Path(__file__).parent / 'src'
|
||||
|
||||
|
||||
def write_outs(output, outs: tuple):
|
||||
@ -16,235 +21,265 @@ def write_outs(output, outs: tuple):
|
||||
output.write(out)
|
||||
|
||||
|
||||
def rewrite_app():
|
||||
app_logger = logger.getChild("app")
|
||||
app_logger.info("rewriting app.py")
|
||||
infile = Path(SRC_DIR) / "app.bk"
|
||||
outfile = Path(PACKAGE_DIR) / "app.py"
|
||||
with open(infile, "r") as input:
|
||||
with open(outfile, "w") as output:
|
||||
def rewrite_app(theme):
|
||||
app_logger = logger.getChild('app')
|
||||
app_logger.info('rewriting app.py')
|
||||
infile = Path(SRC_DIR) / 'app.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'app.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
for line in input:
|
||||
match line:
|
||||
# App init()
|
||||
case " def __init__(self, vmr):\n":
|
||||
output.write(" def __init__(self, vmr, theme):\n")
|
||||
case " self._vmr = vmr\n":
|
||||
case ' self._vmr = vmr\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" self._vmr = vmr\n",
|
||||
" self._theme = theme\n",
|
||||
' tcldir = Path.cwd() / "theme"\n',
|
||||
" if not tcldir.is_dir():\n",
|
||||
' self._vmr = vmr\n',
|
||||
' self._theme = theme\n',
|
||||
' self._theme_name = theme.split("-")[0]\n',
|
||||
' self._theme_type = theme.split("-")[-1]\n',
|
||||
' tcldir = Path.cwd() / "theme" / self._theme_name\n',
|
||||
' if not tcldir.is_dir():\n',
|
||||
' tcldir = Path.cwd() / "_internal" / "theme"\n',
|
||||
' self.tk.call("source", tcldir.resolve() / f"forest-{self._theme}.tcl")\n',
|
||||
' match self._theme_name:\n',
|
||||
' case "forest":\n',
|
||||
' self.tk.call("source", tcldir.resolve() / f"{self._theme}.tcl")\n',
|
||||
' case "azure":\n',
|
||||
' self.tk.call("source", tcldir.resolve() / f"{self._theme_name}.tcl")\n',
|
||||
),
|
||||
)
|
||||
# def connect()
|
||||
case "def connect(kind_id: str, vmr) -> App:\n":
|
||||
output.write(
|
||||
'def connect(kind_id: str, vmr, theme="light") -> App:\n'
|
||||
)
|
||||
case " return VMMIN_cls(vmr)\n":
|
||||
output.write(" return VMMIN_cls(vmr, theme)\n")
|
||||
case _:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def rewrite_builders():
|
||||
builders_logger = logger.getChild("builders")
|
||||
builders_logger.info("rewriting builders.py")
|
||||
infile = Path(SRC_DIR) / "builders.bk"
|
||||
outfile = Path(PACKAGE_DIR) / "builders.py"
|
||||
with open(infile, "r") as input:
|
||||
with open(outfile, "w") as output:
|
||||
def rewrite_builders(theme):
|
||||
builders_logger = logger.getChild('builders')
|
||||
builders_logger.info('rewriting builders.py')
|
||||
infile = Path(SRC_DIR) / 'builders.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'builders.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
ignore_next_lines = 0
|
||||
|
||||
for line in input:
|
||||
if ignore_next_lines > 0:
|
||||
builders_logger.info(f"ignoring: {line}")
|
||||
builders_logger.info(f'ignoring: {line}')
|
||||
ignore_next_lines -= 1
|
||||
continue
|
||||
|
||||
match line:
|
||||
# loading themes
|
||||
case "import sv_ttk\n":
|
||||
output.write("#import sv_ttk\n")
|
||||
case " self.app.resizable(False, False)\n":
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" self.app.resizable(False, False)\n"
|
||||
" if _configuration.themes_enabled:\n",
|
||||
' ttk.Style().theme_use(f"forest-{self.app._theme}")\n',
|
||||
' self.logger.info(f"Forest Theme applied")\n',
|
||||
),
|
||||
)
|
||||
case 'import sv_ttk\n':
|
||||
output.write('#import sv_ttk\n')
|
||||
case ' self.app.resizable(False, False)\n':
|
||||
if theme.startswith('forest'):
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self.app.resizable(False, False)\n'
|
||||
' if _configuration.themes_enabled:\n',
|
||||
' ttk.Style().theme_use(self.app._theme)\n',
|
||||
' self.logger.info(f"{self.app._theme} Theme applied")\n',
|
||||
),
|
||||
)
|
||||
elif theme.startswith('azure'):
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
' self.app.resizable(False, False)\n'
|
||||
' if _configuration.themes_enabled:\n',
|
||||
' self.app.tk.call("set_theme", self.app._theme_type)\n',
|
||||
' self.logger.info(f"Azure {self.app._theme_type} Theme applied")\n',
|
||||
),
|
||||
)
|
||||
ignore_next_lines = 6
|
||||
# setting navframe button widths
|
||||
case " variable=self.navframe.submix,\n":
|
||||
case ' variable=self.navframe.submix,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.navframe.submix,\n"
|
||||
" width=8,\n",
|
||||
' variable=self.navframe.submix,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
case " variable=self.navframe.channel,\n":
|
||||
case ' variable=self.navframe.channel,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.navframe.channel,\n"
|
||||
" width=8,\n",
|
||||
' variable=self.navframe.channel,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
case " variable=self.navframe.extend,\n":
|
||||
case ' variable=self.navframe.extend,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.navframe.extend,\n"
|
||||
" width=8,\n",
|
||||
' variable=self.navframe.extend,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
case " variable=self.navframe.info,\n":
|
||||
case ' variable=self.navframe.info,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.navframe.info,\n"
|
||||
" width=8,\n",
|
||||
' variable=self.navframe.info,\n'
|
||||
' width=8,\n',
|
||||
),
|
||||
)
|
||||
# set channelframe button widths
|
||||
case " variable=self.labelframe.mute,\n":
|
||||
case ' variable=self.labelframe.mute,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.labelframe.mute,\n"
|
||||
" width=7,\n",
|
||||
' variable=self.labelframe.mute,\n'
|
||||
' width=7,\n',
|
||||
),
|
||||
)
|
||||
case " variable=self.labelframe.conf,\n":
|
||||
case ' variable=self.labelframe.conf,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.labelframe.conf,\n"
|
||||
" width=7,\n",
|
||||
' variable=self.labelframe.conf,\n'
|
||||
' width=7,\n',
|
||||
),
|
||||
)
|
||||
case " variable=self.labelframe.on,\n":
|
||||
case ' variable=self.labelframe.on,\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.labelframe.on,\n"
|
||||
" width=7,\n",
|
||||
' variable=self.labelframe.on,\n'
|
||||
' width=7,\n',
|
||||
),
|
||||
)
|
||||
# set stripconfigframe button widths
|
||||
case " self.configframe.phys_out_params.index(param)\n":
|
||||
case ' self.configframe.phys_out_params.index(param)\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" self.configframe.phys_out_params.index(param)\n",
|
||||
" ],\n",
|
||||
" width=6,\n",
|
||||
' self.configframe.phys_out_params.index(param)\n',
|
||||
' ],\n',
|
||||
' width=6,\n',
|
||||
),
|
||||
)
|
||||
ignore_next_lines = 1
|
||||
case " self.configframe.virt_out_params.index(param)\n":
|
||||
case ' self.configframe.virt_out_params.index(param)\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" self.configframe.virt_out_params.index(param)\n",
|
||||
" ],\n",
|
||||
" width=6,\n",
|
||||
' self.configframe.virt_out_params.index(param)\n',
|
||||
' ],\n',
|
||||
' width=6,\n',
|
||||
),
|
||||
)
|
||||
ignore_next_lines = 1
|
||||
# This does both strip and bus param vars buttons
|
||||
case " variable=self.configframe.param_vars[i],\n":
|
||||
case ' variable=self.configframe.param_vars[i],\n':
|
||||
write_outs(
|
||||
output,
|
||||
(
|
||||
" variable=self.configframe.param_vars[i],\n",
|
||||
" width=6,\n",
|
||||
' variable=self.configframe.param_vars[i],\n',
|
||||
' width=6,\n',
|
||||
),
|
||||
)
|
||||
case _:
|
||||
if "Toggle.TButton" in line:
|
||||
output.write(line.replace("Toggle.TButton", "ToggleButton"))
|
||||
if 'Toggle.TButton' in line:
|
||||
if theme.startswith('forest'):
|
||||
output.write(
|
||||
line.replace('Toggle.TButton', 'ToggleButton')
|
||||
)
|
||||
elif theme.startswith('azure'):
|
||||
output.write(
|
||||
line.replace(
|
||||
'Toggle.TButton', 'Switch.TCheckbutton'
|
||||
)
|
||||
)
|
||||
else:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def rewrite_menu():
|
||||
menu_logger = logger.getChild("menu")
|
||||
menu_logger.info("rewriting menu.py")
|
||||
infile = Path(SRC_DIR) / "menu.bk"
|
||||
outfile = Path(PACKAGE_DIR) / "menu.py"
|
||||
with open(infile, "r") as input:
|
||||
with open(outfile, "w") as output:
|
||||
def rewrite_menu(theme):
|
||||
menu_logger = logger.getChild('menu')
|
||||
menu_logger.info('rewriting menu.py')
|
||||
infile = Path(SRC_DIR) / 'menu.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'menu.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
ignore_next_lines = 0
|
||||
|
||||
for line in input:
|
||||
if ignore_next_lines > 0:
|
||||
menu_logger.info(f"ignoring: {line}")
|
||||
menu_logger.info(f'ignoring: {line}')
|
||||
ignore_next_lines -= 1
|
||||
continue
|
||||
match line:
|
||||
case "import sv_ttk\n":
|
||||
output.write("#import sv_ttk\n")
|
||||
case " # layout/themes\n":
|
||||
case 'import sv_ttk\n':
|
||||
output.write('#import sv_ttk\n')
|
||||
case ' # layout/themes\n':
|
||||
ignore_next_lines = 14
|
||||
case _:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def prepare_for_build():
|
||||
def rewrite_navigation(theme):
|
||||
navigation_logger = logger.getChild('navigation')
|
||||
navigation_logger.info('rewriting navigation.py')
|
||||
infile = Path(SRC_DIR) / 'navigation.bk'
|
||||
outfile = Path(PACKAGE_DIR) / 'navigation.py'
|
||||
with open(infile, 'r') as input:
|
||||
with open(outfile, 'w') as output:
|
||||
for line in input:
|
||||
match line:
|
||||
case ' self.builder.create_info_button()\n':
|
||||
if theme.startswith('azure'):
|
||||
output.write(
|
||||
' # self.builder.create_info_button()\n'
|
||||
)
|
||||
else:
|
||||
output.write(line)
|
||||
case _:
|
||||
output.write(line)
|
||||
|
||||
|
||||
def prepare_for_build(theme):
|
||||
################# MOVE FILES FROM PACKAGE DIR INTO SRC DIR #########################
|
||||
for file in (
|
||||
PACKAGE_DIR / "app.py",
|
||||
PACKAGE_DIR / "builders.py",
|
||||
PACKAGE_DIR / "menu.py",
|
||||
PACKAGE_DIR / 'app.py',
|
||||
PACKAGE_DIR / 'builders.py',
|
||||
PACKAGE_DIR / 'menu.py',
|
||||
PACKAGE_DIR / 'navigation.py',
|
||||
):
|
||||
if file.exists():
|
||||
logger.debug(f"moving {str(file)}")
|
||||
file.rename(SRC_DIR / f"{file.stem}.bk")
|
||||
logger.debug(f'moving {str(file)}')
|
||||
file.rename(SRC_DIR / f'{file.stem}.bk')
|
||||
|
||||
###################### RUN THE FILE REWRITER FOR EACH *.BK #########################
|
||||
steps = (
|
||||
rewrite_app,
|
||||
rewrite_builders,
|
||||
rewrite_menu,
|
||||
)
|
||||
[step() for step in steps]
|
||||
for step in (rewrite_app, rewrite_builders, rewrite_menu, rewrite_navigation):
|
||||
step(theme)
|
||||
|
||||
|
||||
def cleanup():
|
||||
########################## RESTORE *.BK FILES #####################################
|
||||
for file in (
|
||||
PACKAGE_DIR / "app.py",
|
||||
PACKAGE_DIR / "builders.py",
|
||||
PACKAGE_DIR / "menu.py",
|
||||
SRC_DIR / 'app.bk',
|
||||
SRC_DIR / 'builders.bk',
|
||||
SRC_DIR / 'menu.bk',
|
||||
SRC_DIR / 'navigation.bk',
|
||||
):
|
||||
file.unlink()
|
||||
|
||||
for file in (
|
||||
SRC_DIR / "app.bk",
|
||||
SRC_DIR / "builders.bk",
|
||||
SRC_DIR / "menu.bk",
|
||||
):
|
||||
file.rename(PACKAGE_DIR / f"{file.stem}.py")
|
||||
if file.exists():
|
||||
logger.debug(f'moving {str(file)}')
|
||||
file.replace(PACKAGE_DIR / f'{file.stem}.py')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-r", "--rewrite", action="store_true")
|
||||
parser.add_argument("-c", "--cleanup", action="store_true")
|
||||
parser.add_argument('--rewrite', action='store_true')
|
||||
parser.add_argument('--theme', type=str, default='forest')
|
||||
parser.add_argument('--restore', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.rewrite:
|
||||
logger.info("preparing files for build")
|
||||
prepare_for_build()
|
||||
elif args.cleanup:
|
||||
logger.info("cleaning up files")
|
||||
logger.info('preparing files for build')
|
||||
prepare_for_build(args.theme)
|
||||
elif args.restore:
|
||||
logger.info('cleaning up files')
|
||||
cleanup()
|
||||
|
||||
192
tools/spec_generator.py
Normal file
192
tools/spec_generator.py
Normal file
@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Spec file generator for voicemeeter-compact builds.
|
||||
Generates Python launcher files and PyInstaller spec files from templates.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Build configuration
|
||||
THEMES = {
|
||||
'azure': ['azure-light', 'azure-dark'],
|
||||
'forest': ['forest-light', 'forest-dark'],
|
||||
'sunvalley': ['sunvalley'], # Single variant, no light/dark
|
||||
}
|
||||
|
||||
KINDS = ['basic', 'banana', 'potato']
|
||||
|
||||
# Templates
|
||||
PYTHON_TEMPLATE = """import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = '{kind}'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg}
|
||||
app = vmcompact.connect(KIND_ID, vmr{theme_param})
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
"""
|
||||
|
||||
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( '../../vmcompact/img', 'img' ),{theme_files}
|
||||
( '../../configs', 'configs' ),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['{script_name}'],
|
||||
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}',
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def generate_python_file(theme_variant: str, kind: str, output_dir: Path) -> None:
|
||||
"""Generate a Python launcher file."""
|
||||
if theme_variant == 'sunvalley':
|
||||
# Sunvalley doesn't use theme parameter
|
||||
theme_arg = ''
|
||||
theme_param = ''
|
||||
else:
|
||||
theme_arg = f"\n theme = '{theme_variant}'"
|
||||
theme_param = ', theme=theme'
|
||||
|
||||
content = PYTHON_TEMPLATE.format(
|
||||
kind=kind, theme_arg=theme_arg, theme_param=theme_param
|
||||
)
|
||||
|
||||
filename = f'{theme_variant}-{kind}.py'
|
||||
output_path = output_dir / filename
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'Generated: {output_path}')
|
||||
|
||||
|
||||
def generate_spec_file(theme_variant: str, kind: str, output_dir: Path) -> None:
|
||||
"""Generate a PyInstaller spec file."""
|
||||
script_name = f'{theme_variant}-{kind}.py'
|
||||
|
||||
if theme_variant == 'sunvalley':
|
||||
# Sunvalley doesn't include theme files
|
||||
theme_files = ''
|
||||
else:
|
||||
theme_base = theme_variant.split('-')[0] # 'azure' from 'azure-dark'
|
||||
theme_files = f"\n ( '../../theme/{theme_base}', 'theme' ),"
|
||||
|
||||
content = SPEC_TEMPLATE.format(
|
||||
script_name=script_name, theme_files=theme_files, kind=kind
|
||||
)
|
||||
|
||||
filename = f'{theme_variant}-{kind}.spec'
|
||||
output_path = output_dir / filename
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f'Generated: {output_path}')
|
||||
|
||||
|
||||
def generate_all_files(output_base_dir: Path) -> None:
|
||||
"""Generate all Python and spec files for all theme/kind combinations."""
|
||||
for theme_family, theme_variants in THEMES.items():
|
||||
theme_dir = output_base_dir / theme_family
|
||||
theme_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for theme_variant in theme_variants:
|
||||
for kind in KINDS:
|
||||
generate_python_file(theme_variant, kind, theme_dir)
|
||||
generate_spec_file(theme_variant, kind, theme_dir)
|
||||
|
||||
|
||||
def clean_existing_files(output_base_dir: Path) -> None:
|
||||
"""Remove all existing generated files."""
|
||||
for theme_family in THEMES.keys():
|
||||
theme_dir = output_base_dir / theme_family
|
||||
if theme_dir.exists():
|
||||
for file in theme_dir.glob('*.py'):
|
||||
file.unlink()
|
||||
print(f'Removed: {file}')
|
||||
for file in theme_dir.glob('*.spec'):
|
||||
file.unlink()
|
||||
print(f'Removed: {file}')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate spec files for voicemeeter-compact'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--clean', action='store_true', help='Clean existing files before generating'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output-dir',
|
||||
type=Path,
|
||||
default=Path('spec'),
|
||||
help='Output directory for spec files (default: spec)',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.clean:
|
||||
print('Cleaning existing files...')
|
||||
clean_existing_files(args.output_dir)
|
||||
|
||||
print('Generating spec files...')
|
||||
generate_all_files(args.output_dir)
|
||||
print('Done!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,3 +1,3 @@
|
||||
from .app import connect
|
||||
|
||||
__ALL__ = ["connect"]
|
||||
__ALL__ = ['connect']
|
||||
|
||||
@ -6,10 +6,11 @@ from tkinter import messagebox, ttk
|
||||
from typing import NamedTuple
|
||||
|
||||
import voicemeeterlib
|
||||
from voicemeeterlib import kinds
|
||||
|
||||
from .builders import MainFrameBuilder
|
||||
from .configurations import loader
|
||||
from .data import _base_values, _configuration, _kinds_all, get_configuration
|
||||
from .data import _base_values, _configuration, get_configuration
|
||||
from .errors import VMCompactError
|
||||
from .menu import Menus
|
||||
from .subject import Subject
|
||||
@ -29,42 +30,46 @@ class App(tk.Tk):
|
||||
"""
|
||||
|
||||
APP_cls = type(
|
||||
f"Voicemeeter{kind}.Compact",
|
||||
f'Voicemeeter{kind}.Compact',
|
||||
(cls,),
|
||||
{
|
||||
"kind": kind,
|
||||
'kind': kind,
|
||||
},
|
||||
)
|
||||
return APP_cls
|
||||
|
||||
def __init__(self, vmr):
|
||||
def __init__(self, vmr, theme):
|
||||
super().__init__()
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._vmr = vmr
|
||||
self._vmr.event.add(["pdirty", "ldirty"])
|
||||
self._vmr.event.add(['pdirty', 'ldirty'])
|
||||
self.subject = Subject()
|
||||
self.start_updates()
|
||||
self._vmr.init_thread()
|
||||
icon_path = Path(__file__).parent.resolve() / "img" / "cat.ico"
|
||||
if icon_path.is_file():
|
||||
self.iconbitmap(str(icon_path))
|
||||
for pn in (
|
||||
Path(__file__).parent.resolve() / 'img' / 'cat.ico',
|
||||
Path.cwd() / '_internal' / 'img' / 'cat.ico',
|
||||
):
|
||||
if pn.is_file():
|
||||
self.iconbitmap(str(pn))
|
||||
break
|
||||
self.minsize(275, False)
|
||||
self._configs = None
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_close_window)
|
||||
self.menu = self["menu"] = Menus(self, vmr)
|
||||
self.protocol('WM_DELETE_WINDOW', self.on_close_window)
|
||||
self.menu = self['menu'] = Menus(self, vmr)
|
||||
self.styletable = ttk.Style()
|
||||
if _configuration.config:
|
||||
vmr.apply_config(_configuration.config)
|
||||
|
||||
self.build_app()
|
||||
|
||||
self.drag_id = ""
|
||||
self.bind("<Configure>", self.dragging)
|
||||
self.drag_id = ''
|
||||
self.bind('<Configure>', self.dragging)
|
||||
|
||||
self.after(1, self.healthcheck_step)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}App"
|
||||
return f'{type(self).__name__}App'
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
@ -80,8 +85,8 @@ class App(tk.Tk):
|
||||
frame
|
||||
for frame in self.winfo_children()
|
||||
if isinstance(frame, ttk.Frame)
|
||||
and "!stripconfig" in str(frame)
|
||||
or "!busconfig" in str(frame)
|
||||
and '!stripconfig' in str(frame)
|
||||
or '!busconfig' in str(frame)
|
||||
)
|
||||
|
||||
def build_app(self, kind=None, vban=None):
|
||||
@ -97,22 +102,22 @@ class App(tk.Tk):
|
||||
self.submix_frame = None
|
||||
self.builder = MainFrameBuilder(self)
|
||||
self.builder.setup()
|
||||
self.builder.create_channelframe("strip")
|
||||
self.builder.create_channelframe('strip')
|
||||
self.builder.create_separator()
|
||||
self.builder.create_navframe()
|
||||
if _configuration.extended:
|
||||
self.nav_frame.extend.set(True)
|
||||
self.nav_frame.extend_frame()
|
||||
if self.kind.name == "potato":
|
||||
if self.kind.name == 'potato':
|
||||
self.builder.create_banner()
|
||||
|
||||
def on_pdirty(self):
|
||||
if _base_values.run_update:
|
||||
self.after(1, self.subject.notify, "pdirty")
|
||||
self.after(1, self.subject.notify, 'pdirty')
|
||||
|
||||
def on_ldirty(self):
|
||||
if not _base_values.dragging:
|
||||
self.after(1, self.subject.notify, "ldirty")
|
||||
self.after(1, self.subject.notify, 'ldirty')
|
||||
|
||||
def _destroy_top_level_frames(self):
|
||||
"""
|
||||
@ -132,14 +137,14 @@ class App(tk.Tk):
|
||||
|
||||
def dragging(self, event, *args):
|
||||
if event.widget is self:
|
||||
if self.drag_id == "":
|
||||
if self.drag_id == '':
|
||||
_base_values.dragging = True
|
||||
else:
|
||||
self.after_cancel(self.drag_id)
|
||||
self.drag_id = self.after(100, self.stop_drag)
|
||||
|
||||
def stop_drag(self):
|
||||
self.drag_id = ""
|
||||
self.drag_id = ''
|
||||
_base_values.dragging = False
|
||||
|
||||
@cached_property
|
||||
@ -149,11 +154,11 @@ class App(tk.Tk):
|
||||
|
||||
def start_updates(self):
|
||||
def init():
|
||||
self.logger.debug("updates started")
|
||||
self.logger.debug('updates started')
|
||||
_base_values.run_update = True
|
||||
|
||||
if self._vmr.gui.launched_by_api:
|
||||
self.subject.notify("pdirty")
|
||||
self.subject.notify('pdirty')
|
||||
self.after(12000, init)
|
||||
else:
|
||||
init()
|
||||
@ -163,10 +168,10 @@ class App(tk.Tk):
|
||||
try:
|
||||
self._vmr.version
|
||||
except voicemeeterlib.error.CAPIError:
|
||||
resp = messagebox.askyesno(message="Restart Voicemeeter GUI?")
|
||||
resp = messagebox.askyesno(message='Restart Voicemeeter GUI?')
|
||||
if resp:
|
||||
self.logger.debug(
|
||||
"healthcheck failed, rebuilding the app after GUI restart."
|
||||
'healthcheck failed, rebuilding the app after GUI restart.'
|
||||
)
|
||||
self._vmr.end_thread()
|
||||
self._vmr.run_voicemeeter(self._vmr.kind.name)
|
||||
@ -175,13 +180,13 @@ class App(tk.Tk):
|
||||
self.after(8000, self.start_updates)
|
||||
self._destroy_top_level_frames()
|
||||
self.build_app(self._vmr.kind)
|
||||
vban_config = get_configuration("vban")
|
||||
vban_config = get_configuration('vban')
|
||||
for i, _ in enumerate(vban_config):
|
||||
target = getattr(self.menu, f"menu_vban_{i+1}")
|
||||
target.entryconfig(0, state="normal")
|
||||
target.entryconfig(1, state="disabled")
|
||||
target = getattr(self.menu, f'menu_vban_{i + 1}')
|
||||
target.entryconfig(0, state='normal')
|
||||
target.entryconfig(1, state='disabled')
|
||||
[
|
||||
self.menu.menu_vban.entryconfig(j, state="normal")
|
||||
self.menu.menu_vban.entryconfig(j, state='normal')
|
||||
for j, _ in enumerate(self.menu.menu_vban.winfo_children())
|
||||
]
|
||||
else:
|
||||
@ -194,14 +199,14 @@ class App(tk.Tk):
|
||||
self.destroy()
|
||||
|
||||
|
||||
_apps = {kind.name: App.make(kind) for kind in _kinds_all}
|
||||
_apps = {kind.name: App.make(kind) for kind in kinds.all}
|
||||
|
||||
|
||||
def connect(kind_id: str, vmr) -> App:
|
||||
def connect(kind_id: str, vmr, theme=None) -> App:
|
||||
"""return App of the kind requested"""
|
||||
|
||||
try:
|
||||
VMMIN_cls = _apps[kind_id]
|
||||
except KeyError:
|
||||
raise VMCompactError(f"Invalid kind: {kind_id}")
|
||||
return VMMIN_cls(vmr)
|
||||
raise VMCompactError(f'Invalid kind: {kind_id}')
|
||||
return VMMIN_cls(vmr, theme)
|
||||
|
||||
@ -17,7 +17,7 @@ class Banner(ttk.Frame):
|
||||
|
||||
self.label = ttk.Label(
|
||||
self,
|
||||
text=f"SUBMIX: {self.submix.get().upper()}",
|
||||
text=f'SUBMIX: {self.submix.get().upper()}',
|
||||
)
|
||||
self.label.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.W, tk.E))
|
||||
|
||||
@ -28,8 +28,8 @@ class Banner(ttk.Frame):
|
||||
return self.parent.target
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "submix":
|
||||
if subject == 'submix':
|
||||
if not _base_values.dragging:
|
||||
self.logger.debug("checking submix for banner")
|
||||
self.logger.debug('checking submix for banner')
|
||||
self.submix.set(self.target.bus[_configuration.submixes].label)
|
||||
self.label["text"] = f"SUBMIX: {self.submix.get().upper()}"
|
||||
self.label['text'] = f'SUBMIX: {self.submix.get().upper()}'
|
||||
|
||||
@ -6,6 +6,7 @@ from tkinter import ttk
|
||||
|
||||
import sv_ttk
|
||||
|
||||
from . import util
|
||||
from .banner import Banner
|
||||
from .channels import _make_channelframe
|
||||
from .config import BusConfig, StripConfig
|
||||
@ -41,31 +42,31 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
)
|
||||
self.app.resizable(False, False)
|
||||
if _configuration.themes_enabled:
|
||||
if sv_ttk.get_theme() not in ("light", "dark"):
|
||||
if sv_ttk.get_theme() not in ('light', 'dark'):
|
||||
sv_ttk.set_theme(_configuration.theme_mode)
|
||||
self.logger.info(
|
||||
f"Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied"
|
||||
f'Sunvalley {sv_ttk.get_theme().capitalize()} Theme applied'
|
||||
)
|
||||
|
||||
def create_channelframe(self, type_):
|
||||
if type_ == "strip":
|
||||
if type_ == 'strip':
|
||||
self.app.strip_frame = _make_channelframe(self.app, type_)
|
||||
else:
|
||||
self.app.bus_frame = _make_channelframe(self.app, type_)
|
||||
self.logger.info(f"Finished building channelframe type {type_}")
|
||||
self.logger.info(f'Finished building channelframe type {type_}')
|
||||
|
||||
def create_separator(self):
|
||||
self.app.sep = ttk.Separator(self.app, orient="vertical")
|
||||
self.app.sep = ttk.Separator(self.app, orient='vertical')
|
||||
self.app.sep.grid(row=0, column=1, sticky=(tk.N, tk.S))
|
||||
self.app.columnconfigure(1, minsize=15)
|
||||
self.logger.info(f"Finished building separator")
|
||||
self.logger.info('Finished building separator')
|
||||
|
||||
def create_navframe(self):
|
||||
self.app.nav_frame = Navigation(self.app)
|
||||
self.logger.info(f"Finished building navframe")
|
||||
self.logger.info('Finished building navframe')
|
||||
|
||||
def create_configframe(self, type_, index, id):
|
||||
if type_ == "strip":
|
||||
if type_ == 'strip':
|
||||
self.app.config_frame = StripConfig(self.app, index, id)
|
||||
if self.app.strip_frame:
|
||||
[
|
||||
@ -95,20 +96,20 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
if self.app.strip_frame:
|
||||
[
|
||||
frame.styletable.configure(
|
||||
f"{frame.identifier}Conf{frame.index}.TButton",
|
||||
background=f"{'white' if not frame.conf.get() else 'yellow'}",
|
||||
f'{frame.identifier}Conf{frame.index}.TButton',
|
||||
background=f'{"white" if not frame.conf.get() else "yellow"}',
|
||||
)
|
||||
for _, frame in enumerate(self.app.strip_frame.labelframes)
|
||||
]
|
||||
if self.app.bus_frame:
|
||||
[
|
||||
frame.styletable.configure(
|
||||
f"{frame.identifier}Conf{frame.index}.TButton",
|
||||
background=f"{'white' if not frame.conf.get() else 'yellow'}",
|
||||
f'{frame.identifier}Conf{frame.index}.TButton',
|
||||
background=f'{"white" if not frame.conf.get() else "yellow"}',
|
||||
)
|
||||
for _, frame in enumerate(self.app.bus_frame.labelframes)
|
||||
]
|
||||
self.logger.info(f"Finished building configframe for {type_}[{index}]")
|
||||
self.logger.info(f'Finished building configframe for {type_}[{index}]')
|
||||
self.app.after(5, self.reset_config_frames)
|
||||
|
||||
def reset_config_frames(self):
|
||||
@ -121,7 +122,7 @@ class MainFrameBuilder(AbstractBuilder):
|
||||
def create_banner(self):
|
||||
self.app.banner = Banner(self.app)
|
||||
self.app.banner.grid(row=4, column=0, columnspan=3)
|
||||
self.logger.info(f"Finished building banner")
|
||||
self.logger.info('Finished building banner')
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
@ -140,31 +141,31 @@ class NavigationFrameBuilder(AbstractBuilder):
|
||||
self.navframe.info = tk.BooleanVar()
|
||||
|
||||
self.navframe.channel_text = tk.StringVar(
|
||||
value=f"{self.navframe.parent.strip_frame.identifier.upper()}"
|
||||
value=f'{self.navframe.parent.strip_frame.identifier.upper()}'
|
||||
)
|
||||
self.navframe.extend_text = tk.StringVar(
|
||||
value=f"{'REDUCE' if self.navframe.extend.get() else 'EXTEND'}"
|
||||
value=f'{"REDUCE" if self.navframe.extend.get() else "EXTEND"}'
|
||||
)
|
||||
self.navframe.info_text = tk.StringVar()
|
||||
|
||||
def create_submix_button(self):
|
||||
self.navframe.submix_button = ttk.Checkbutton(
|
||||
self.navframe,
|
||||
text="SUBMIX",
|
||||
text='SUBMIX',
|
||||
command=self.navframe.show_submix,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Submix.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Submix.TButton"}',
|
||||
variable=self.navframe.submix,
|
||||
)
|
||||
self.navframe.submix_button.grid(column=0, row=0)
|
||||
if self.navframe.parent.kind.name != "potato":
|
||||
self.navframe.submix_button["state"] = "disabled"
|
||||
if self.navframe.parent.kind.name != 'potato':
|
||||
self.navframe.submix_button['state'] = 'disabled'
|
||||
|
||||
def create_channel_button(self):
|
||||
self.navframe.channel_button = ttk.Checkbutton(
|
||||
self.navframe,
|
||||
textvariable=self.navframe.channel_text,
|
||||
command=self.navframe.switch_channel,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Channel.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Channel.TButton"}',
|
||||
variable=self.navframe.channel,
|
||||
)
|
||||
self.navframe.channel_button.grid(column=0, row=1, rowspan=1)
|
||||
@ -174,7 +175,7 @@ class NavigationFrameBuilder(AbstractBuilder):
|
||||
self.navframe,
|
||||
textvariable=self.navframe.extend_text,
|
||||
command=self.navframe.extend_frame,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Extend.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Extend.TButton"}',
|
||||
variable=self.navframe.extend,
|
||||
)
|
||||
self.navframe.extend_button.grid(column=0, row=2)
|
||||
@ -183,7 +184,7 @@ class NavigationFrameBuilder(AbstractBuilder):
|
||||
self.navframe.info_button = ttk.Checkbutton(
|
||||
self.navframe,
|
||||
textvariable=self.navframe.info_text,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'Rec.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else "Rec.TButton"}',
|
||||
variable=self.navframe.info,
|
||||
)
|
||||
self.navframe.info_button.grid(column=0, row=3)
|
||||
@ -227,9 +228,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
"""Adds a progress bar widget to a single label frame"""
|
||||
self.labelframe.pb = ttk.Progressbar(
|
||||
self.labelframe,
|
||||
maximum=72,
|
||||
orient="vertical",
|
||||
mode="determinate",
|
||||
maximum=72, # Range: 0 = -60dB, 72 = +12dB (72dB total range)
|
||||
orient='vertical',
|
||||
mode='determinate',
|
||||
variable=self.labelframe.level,
|
||||
)
|
||||
self.labelframe.pb.grid(column=0, row=0)
|
||||
@ -240,17 +241,17 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
self.labelframe,
|
||||
from_=12.0,
|
||||
to=-60.0,
|
||||
orient="vertical",
|
||||
orient='vertical',
|
||||
variable=self.labelframe.gain,
|
||||
command=self.labelframe.scale_callback,
|
||||
length=_configuration.channel_height,
|
||||
)
|
||||
self.scale.grid(column=1, row=0)
|
||||
self.scale.bind("<Double-Button-1>", self.labelframe.reset_gain)
|
||||
self.scale.bind("<Button-1>", self.labelframe.scale_press)
|
||||
self.scale.bind("<ButtonRelease-1>", self.labelframe.scale_release)
|
||||
self.scale.bind('<Double-Button-1>', self.labelframe.reset_gain)
|
||||
self.scale.bind('<Button-1>', self.labelframe.scale_press)
|
||||
self.scale.bind('<ButtonRelease-1>', self.labelframe.scale_release)
|
||||
self.scale.bind(
|
||||
"<MouseWheel>",
|
||||
'<MouseWheel>',
|
||||
partial(
|
||||
self.labelframe.pause_updates,
|
||||
self.labelframe._on_mousewheel,
|
||||
@ -268,9 +269,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
"""Adds a mute button widget to a single label frame"""
|
||||
self.button_mute = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="MUTE",
|
||||
text='MUTE',
|
||||
command=partial(self.labelframe.pause_updates, self.labelframe.toggle_mute),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Mute{self.index}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}Mute{self.index}.TButton"}',
|
||||
variable=self.labelframe.mute,
|
||||
)
|
||||
self.button_mute.grid(column=0, row=2, columnspan=2)
|
||||
@ -278,9 +279,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
def add_conf_button(self):
|
||||
self.button_conf = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="CONFIG",
|
||||
text='CONFIG',
|
||||
command=self.labelframe.open_config,
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}Conf{self.index}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}Conf{self.index}.TButton"}',
|
||||
variable=self.labelframe.conf,
|
||||
)
|
||||
self.button_conf.grid(column=0, row=3, columnspan=2)
|
||||
@ -288,9 +289,9 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
|
||||
def add_on_button(self):
|
||||
self.button_on = ttk.Checkbutton(
|
||||
self.labelframe,
|
||||
text="ON",
|
||||
text='ON',
|
||||
command=partial(self.labelframe.pause_updates, self.labelframe.set_on),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{self.identifier}On{self.index}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{self.identifier}On{self.index}.TButton"}',
|
||||
variable=self.labelframe.on,
|
||||
)
|
||||
self.button_on.grid(column=0, row=2, columnspan=2)
|
||||
@ -339,40 +340,40 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
"""Responsible for building channel configframe widgets"""
|
||||
|
||||
def setup(self):
|
||||
if self.configframe.parent.kind.name == "basic":
|
||||
self.configframe.slider_params = ("audibility",)
|
||||
if self.configframe.parent.kind.name == 'basic':
|
||||
self.configframe.slider_params = ('audibility',)
|
||||
self.configframe.slider_vars = (tk.DoubleVar(),)
|
||||
else:
|
||||
self.configframe.slider_params = ("comp.knob", "gate.knob", "limit")
|
||||
self.configframe.slider_params = ('comp.knob', 'gate.knob', 'limit')
|
||||
self.configframe.slider_vars = [
|
||||
tk.DoubleVar() for _ in self.configframe.slider_params
|
||||
]
|
||||
|
||||
self.configframe.phys_out_params = [
|
||||
f"A{i+1}" for i in range(self.configframe.phys_out)
|
||||
f'A{i + 1}' for i in range(self.configframe.phys_out)
|
||||
]
|
||||
self.configframe.phys_out_params_vars = [
|
||||
tk.BooleanVar() for _ in self.configframe.phys_out_params
|
||||
]
|
||||
|
||||
self.configframe.virt_out_params = [
|
||||
f"B{i+1}" for i in range(self.configframe.virt_out)
|
||||
f'B{i + 1}' for i in range(self.configframe.virt_out)
|
||||
]
|
||||
self.configframe.virt_out_params_vars = [
|
||||
tk.BooleanVar() for _ in self.configframe.virt_out_params
|
||||
]
|
||||
|
||||
self.configframe.params = ("mono", "solo")
|
||||
self.configframe.param_vars = list(
|
||||
tk.BooleanVar() for _ in self.configframe.params
|
||||
self.configframe.bool_params = ('mono', 'solo')
|
||||
self.configframe.bool_param_vars = list(
|
||||
tk.BooleanVar() for _ in self.configframe.bool_params
|
||||
)
|
||||
|
||||
if self.configframe.parent.kind.name in ("banana", "potato"):
|
||||
if self.configframe.parent.kind.name in ('banana', 'potato'):
|
||||
if self.configframe.index == self.configframe.phys_in:
|
||||
self.configframe.params = list(
|
||||
map(lambda x: x.replace("mono", "mc"), self.configframe.params)
|
||||
map(lambda x: x.replace('mono', 'mc'), self.configframe.bool_params)
|
||||
)
|
||||
if self.configframe.parent.kind.name == "banana":
|
||||
if self.configframe.parent.kind.name == 'banana':
|
||||
pass
|
||||
# karaoke modes not in RT Packet yet. May implement in future
|
||||
"""
|
||||
@ -388,101 +389,104 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
== self.configframe.phys_in + self.configframe.virt_in - 1
|
||||
):
|
||||
self.configframe.params = list(
|
||||
map(lambda x: x.replace("mono", "mc"), self.configframe.params)
|
||||
map(
|
||||
lambda x: x.replace('mono', 'mc'),
|
||||
self.configframe.bool_params,
|
||||
)
|
||||
)
|
||||
|
||||
def create_comp_slider(self):
|
||||
comp_label = ttk.Label(self.configframe, text="Comp")
|
||||
comp_label = ttk.Label(self.configframe, text='Comp')
|
||||
comp_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("comp.knob")
|
||||
self.configframe.slider_params.index('comp.knob')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "comp.knob"),
|
||||
command=partial(self.configframe.scale_callback, 'comp.knob'),
|
||||
)
|
||||
comp_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "comp.knob", 0)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'comp.knob', 0)
|
||||
)
|
||||
comp_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
comp_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
comp_scale.bind("<Enter>", partial(self.configframe.scale_enter, "comp.knob"))
|
||||
comp_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
comp_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
comp_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
comp_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'comp.knob'))
|
||||
comp_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
comp_label.grid(column=0, row=0)
|
||||
comp_scale.grid(column=1, row=0)
|
||||
|
||||
def create_gate_slider(self):
|
||||
gate_label = ttk.Label(self.configframe, text="Gate")
|
||||
gate_label = ttk.Label(self.configframe, text='Gate')
|
||||
gate_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("gate.knob")
|
||||
self.configframe.slider_params.index('gate.knob')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "gate.knob"),
|
||||
command=partial(self.configframe.scale_callback, 'gate.knob'),
|
||||
)
|
||||
gate_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "gate.knob", 0)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'gate.knob', 0)
|
||||
)
|
||||
gate_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
gate_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
gate_scale.bind("<Enter>", partial(self.configframe.scale_enter, "gate.knob"))
|
||||
gate_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
gate_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
gate_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
gate_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'gate.knob'))
|
||||
gate_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
gate_label.grid(column=2, row=0)
|
||||
gate_scale.grid(column=3, row=0)
|
||||
|
||||
def create_limit_slider(self):
|
||||
limit_label = ttk.Label(self.configframe, text="Limit")
|
||||
limit_label = ttk.Label(self.configframe, text='Limit')
|
||||
limit_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=-40,
|
||||
to=12,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("limit")
|
||||
self.configframe.slider_params.index('limit')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "limit"),
|
||||
command=partial(self.configframe.scale_callback, 'limit'),
|
||||
)
|
||||
limit_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "limit", 12)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'limit', 12)
|
||||
)
|
||||
limit_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
limit_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
limit_scale.bind("<Enter>", partial(self.configframe.scale_enter, "limit"))
|
||||
limit_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
limit_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
limit_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
limit_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'limit'))
|
||||
limit_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
limit_label.grid(column=4, row=0)
|
||||
limit_scale.grid(column=5, row=0)
|
||||
|
||||
def create_audibility_slider(self):
|
||||
aud_label = ttk.Label(self.configframe, text="Audibility")
|
||||
aud_label = ttk.Label(self.configframe, text='Audibility')
|
||||
aud_scale = ttk.Scale(
|
||||
self.configframe,
|
||||
from_=0.0,
|
||||
to=10.0,
|
||||
orient="horizontal",
|
||||
orient='horizontal',
|
||||
length=_configuration.channel_width,
|
||||
variable=self.configframe.slider_vars[
|
||||
self.configframe.slider_params.index("audibility")
|
||||
self.configframe.slider_params.index('audibility')
|
||||
],
|
||||
command=partial(self.configframe.scale_callback, "audibility"),
|
||||
command=partial(self.configframe.scale_callback, 'audibility'),
|
||||
)
|
||||
aud_scale.bind(
|
||||
"<Double-Button-1>", partial(self.configframe.reset_scale, "audibility", 0)
|
||||
'<Double-Button-1>', partial(self.configframe.reset_scale, 'audibility', 0)
|
||||
)
|
||||
aud_scale.bind("<Button-1>", self.configframe.scale_press)
|
||||
aud_scale.bind("<ButtonRelease-1>", self.configframe.scale_release)
|
||||
aud_scale.bind("<Enter>", partial(self.configframe.scale_enter, "audibility"))
|
||||
aud_scale.bind("<Leave>", self.configframe.scale_leave)
|
||||
aud_scale.bind('<Button-1>', self.configframe.scale_press)
|
||||
aud_scale.bind('<ButtonRelease-1>', self.configframe.scale_release)
|
||||
aud_scale.bind('<Enter>', partial(self.configframe.scale_enter, 'audibility'))
|
||||
aud_scale.bind('<Leave>', self.configframe.scale_leave)
|
||||
|
||||
aud_label.grid(column=0, row=0)
|
||||
aud_scale.grid(column=1, row=0)
|
||||
@ -495,7 +499,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_a, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.phys_out_params_vars[
|
||||
self.configframe.phys_out_params.index(param)
|
||||
],
|
||||
@ -518,7 +522,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_b, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.virt_out_params_vars[
|
||||
self.configframe.virt_out_params.index(param)
|
||||
],
|
||||
@ -541,10 +545,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_p, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
variable=self.configframe.param_vars[i],
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.bool_param_vars[i],
|
||||
)
|
||||
for i, param in enumerate(self.configframe.params)
|
||||
for i, param in enumerate(self.configframe.bool_params)
|
||||
]
|
||||
[
|
||||
button.grid(
|
||||
@ -558,50 +562,73 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
"""Responsible for building channel configframe widgets"""
|
||||
|
||||
def __init__(self, configframe, app):
|
||||
super().__init__(configframe)
|
||||
self.app = app
|
||||
|
||||
def setup(self):
|
||||
# fmt: off
|
||||
self.configframe.bus_mode_map = {
|
||||
"normal": "Normal",
|
||||
"amix": "Mix Down A",
|
||||
"bmix": "Mix Down B",
|
||||
"repeat": "Stereo Repeat",
|
||||
"composite": "Composite",
|
||||
"tvmix": "Up Mix TV",
|
||||
"upmix21": "Up Mix 2.1",
|
||||
"upmix41": "Up Mix 4.1",
|
||||
"upmix61": "Up Mix 6.1",
|
||||
"centeronly": "Center Only",
|
||||
"lfeonly": "LFE Only",
|
||||
"rearonly": "Rear Only",
|
||||
}
|
||||
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
|
||||
# fmt: on
|
||||
self.configframe.params = ("mono", "eq.on", "eq.ab")
|
||||
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
|
||||
self.configframe.bus_mode_map = util.get_busmode_fullnames(self.app.kind)
|
||||
self.configframe.bus_mode_map_reverse = util.get_busmode_fullnames_reversed(
|
||||
self.app.kind
|
||||
)
|
||||
self.configframe.bus_modes = util.get_busmode_shortnames(self.app.kind)
|
||||
self.configframe.int_params = ('mono',)
|
||||
self.configframe.int_param_vars = [
|
||||
tk.IntVar(value=getattr(self.configframe.target, param))
|
||||
for param in self.configframe.int_params
|
||||
]
|
||||
self.configframe.mono_modes = util.get_busmono_modes()
|
||||
self.configframe.bus_mono_label_text = tk.StringVar(
|
||||
value=self.configframe.mono_modes[self.configframe.target.mono]
|
||||
)
|
||||
self.configframe.bool_params = ('eq.on', 'eq.ab')
|
||||
self.configframe.bool_param_vars = [
|
||||
tk.BooleanVar() for _ in self.configframe.bool_params
|
||||
]
|
||||
self.configframe.bus_mode_label_text = tk.StringVar(
|
||||
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
|
||||
)
|
||||
|
||||
def create_bus_mode_button(self):
|
||||
self.configframe.busmode_button = ttk.Button(
|
||||
self.configframe, textvariable=self.configframe.bus_mode_label_text
|
||||
self.configframe,
|
||||
textvariable=self.configframe.bus_mode_label_text,
|
||||
width=15,
|
||||
)
|
||||
self.configframe.busmode_button.grid(
|
||||
column=0, row=0, columnspan=2, sticky=(tk.W)
|
||||
column=0, row=0, columnspan=2, sticky=(tk.W), padx=1, pady=1
|
||||
)
|
||||
self.configframe.busmode_button.bind(
|
||||
"<Button-1>",
|
||||
'<Button-1>',
|
||||
partial(
|
||||
self.configframe.pause_updates, self.configframe.rotate_bus_modes_right
|
||||
),
|
||||
)
|
||||
self.configframe.busmode_button.bind(
|
||||
"<Button-3>",
|
||||
'<Button-3>',
|
||||
partial(
|
||||
self.configframe.pause_updates, self.configframe.rotate_bus_modes_left
|
||||
),
|
||||
)
|
||||
|
||||
def create_bus_mono_button(self):
|
||||
self.configframe.mono_button = ttk.Button(
|
||||
self.configframe,
|
||||
textvariable=self.configframe.bus_mono_label_text,
|
||||
width=15,
|
||||
)
|
||||
self.configframe.mono_button.bind(
|
||||
'<Button-1>',
|
||||
partial(self.configframe.pause_updates, self.configframe.rotate_mono_right),
|
||||
)
|
||||
self.configframe.mono_button.bind(
|
||||
'<Button-3>',
|
||||
partial(self.configframe.pause_updates, self.configframe.rotate_mono_left),
|
||||
)
|
||||
self.configframe.mono_button.grid(
|
||||
column=0, row=1, sticky=(tk.W), padx=1, pady=1
|
||||
)
|
||||
|
||||
def create_param_buttons(self):
|
||||
param_buttons = [
|
||||
ttk.Checkbutton(
|
||||
@ -610,14 +637,14 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
|
||||
command=partial(
|
||||
self.configframe.pause_updates, self.configframe.toggle_p, param
|
||||
),
|
||||
style=f"{'Toggle.TButton' if _configuration.themes_enabled else f'{param}.TButton'}",
|
||||
variable=self.configframe.param_vars[i],
|
||||
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
|
||||
variable=self.configframe.bool_param_vars[i],
|
||||
)
|
||||
for i, param in enumerate(self.configframe.params)
|
||||
for i, param in enumerate(self.configframe.bool_params)
|
||||
]
|
||||
[
|
||||
button.grid(
|
||||
column=i,
|
||||
column=i + 1,
|
||||
row=1,
|
||||
)
|
||||
for i, button in enumerate(param_buttons)
|
||||
|
||||
@ -46,7 +46,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
try:
|
||||
return getattr(self.target, param)
|
||||
except AttributeError as e:
|
||||
self.logger(f"{type(e).__name__}: {e}")
|
||||
self.logger(f'{type(e).__name__}: {e}')
|
||||
|
||||
def setter(self, param, value):
|
||||
if param in dir(self.target): # avoid calling getattr (with hasattr)
|
||||
@ -56,19 +56,19 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
"""callback function for scale widget"""
|
||||
|
||||
val = round(self.gain.get(), 1)
|
||||
self.setter("gain", val)
|
||||
self.setter('gain', val)
|
||||
self.gainlabel.set(val)
|
||||
|
||||
def toggle_mute(self, *args):
|
||||
self.target.mute = self.mute.get()
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}Mute{self.index}.TButton",
|
||||
f'{self.identifier}Mute{self.index}.TButton',
|
||||
background=f'{"red" if self.mute.get() else "white"}',
|
||||
)
|
||||
|
||||
def reset_gain(self, *args):
|
||||
self.setter("gain", 0)
|
||||
self.setter('gain', 0)
|
||||
self.gain.set(0)
|
||||
self.gainlabel.set(self.gain.get())
|
||||
|
||||
@ -76,16 +76,16 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
self.after(1, self.remove_events)
|
||||
|
||||
def remove_events(self):
|
||||
self.parent.target.event.remove("pdirty")
|
||||
self.parent.target.event.remove("ldirty")
|
||||
self.parent.target.event.remove('pdirty')
|
||||
self.parent.target.event.remove('ldirty')
|
||||
|
||||
def scale_release(self, *args):
|
||||
_base_values.run_update = False
|
||||
self.after(1, self.add_events)
|
||||
|
||||
def add_events(self):
|
||||
self.parent.target.event.add("pdirty")
|
||||
self.parent.target.event.add("ldirty")
|
||||
self.parent.target.event.add('pdirty')
|
||||
self.parent.target.event.add('ldirty')
|
||||
self.after(500, self.resume_updates)
|
||||
|
||||
def pause_updates(self, func, *args):
|
||||
@ -115,7 +115,7 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
self.gain.set(12)
|
||||
elif self.gain.get() < -60:
|
||||
self.gain.set(-60)
|
||||
self.setter("gain", self.gain.get())
|
||||
self.setter('gain', self.gain.get())
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
|
||||
def open_config(self):
|
||||
@ -125,36 +125,36 @@ class ChannelLabelFrame(ttk.LabelFrame):
|
||||
self.parent.parent.config_frame.teardown()
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}Conf{self.index}.TButton",
|
||||
f'{self.identifier}Conf{self.index}.TButton',
|
||||
background=f'{"yellow" if self.conf.get() else "white"}',
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "ldirty":
|
||||
if subject == 'ldirty':
|
||||
self.upd_levels()
|
||||
elif subject == "pdirty":
|
||||
elif subject == 'pdirty':
|
||||
self.sync_params()
|
||||
elif subject == "labelframe":
|
||||
elif subject == 'labelframe':
|
||||
self.after(5, self.sync_labels)
|
||||
|
||||
def sync_params(self):
|
||||
"""sync parameter states, update button colours"""
|
||||
self.gain.set(self.getter("gain"))
|
||||
self.gain.set(self.getter('gain'))
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
self.mute.set(self.getter("mute"))
|
||||
self.mute.set(self.getter('mute'))
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}Mute{self.index}.TButton",
|
||||
background=f'{"red" if self.mute.get() else "white"}',
|
||||
f'{self.identifier}Mute{self.index}.TButton',
|
||||
background=f'{"red" if self.mute.get() else "white"}',
|
||||
)
|
||||
|
||||
def sync_labels(self):
|
||||
"""sync labelframes according to label text"""
|
||||
retval = self.getter("label")
|
||||
retval = self.getter('label')
|
||||
if self.parent.label_cache[self.id][self.index] != retval:
|
||||
self.parent.label_cache[self.id][self.index] = retval
|
||||
if len(retval) > 10:
|
||||
retval = f"{retval[:8]}.."
|
||||
retval = f'{retval[:8]}..'
|
||||
if not retval:
|
||||
self.parent.columnconfigure(self.index, minsize=0)
|
||||
self.parent.parent.subject.remove(self)
|
||||
@ -197,14 +197,22 @@ class Strip(ChannelLabelFrame):
|
||||
|
||||
def upd_levels(self):
|
||||
"""
|
||||
Updates level values.
|
||||
Updates level values using direct dB values.
|
||||
"""
|
||||
if self.index < self.parent.parent.kind.num_strip:
|
||||
if self.target.levels.is_updated:
|
||||
val = max(self.target.levels.prefader)
|
||||
self.level.set(
|
||||
(0 if self.mute.get() else 72 + val - 12 + self.gain.get())
|
||||
)
|
||||
if val < -72:
|
||||
if self.level.get() != 0:
|
||||
self.level.set(0)
|
||||
return
|
||||
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
|
||||
if self.mute.get():
|
||||
level_display = 0
|
||||
else:
|
||||
level_db = val + self.gain.get()
|
||||
level_display = max(0, min(72, level_db + 60))
|
||||
self.level.set(level_display)
|
||||
|
||||
|
||||
class Bus(ChannelLabelFrame):
|
||||
@ -223,9 +231,18 @@ class Bus(ChannelLabelFrame):
|
||||
|
||||
def upd_levels(self):
|
||||
if self.index < self.parent.parent.kind.num_bus:
|
||||
if self.target.levels.is_updated or self.level.get() != -118:
|
||||
if self.target.levels.is_updated:
|
||||
val = max(self.target.levels.all)
|
||||
self.level.set((0 if self.mute.get() else 72 + val - 12))
|
||||
if val < -72:
|
||||
if self.level.get() != 0:
|
||||
self.level.set(0)
|
||||
return
|
||||
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
|
||||
if self.mute.get():
|
||||
level_display = 0
|
||||
else:
|
||||
level_display = max(0, min(72, val + 60))
|
||||
self.level.set(level_display)
|
||||
|
||||
|
||||
class ChannelFrame(ttk.Frame):
|
||||
@ -236,8 +253,8 @@ class ChannelFrame(ttk.Frame):
|
||||
self.phys_in, self.virt_in = parent.kind.ins
|
||||
self.phys_out, self.virt_out = parent.kind.outs
|
||||
self.label_cache = {
|
||||
"strip": [""] * (self.phys_in + self.virt_in),
|
||||
"bus": [""] * (self.phys_out + self.virt_out),
|
||||
'strip': [''] * (self.phys_in + self.virt_in),
|
||||
'bus': [''] * (self.phys_out + self.virt_out),
|
||||
}
|
||||
self.parent.subject.add(self)
|
||||
self.update_labels()
|
||||
@ -264,10 +281,10 @@ class ChannelFrame(ttk.Frame):
|
||||
|
||||
def update_labels(self):
|
||||
for labelframe in self.labelframes:
|
||||
labelframe.on_update("labelframe")
|
||||
labelframe.on_update('labelframe')
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
if subject == 'pdirty':
|
||||
self.update_labels()
|
||||
|
||||
def grid_configure(self):
|
||||
@ -281,7 +298,7 @@ class ChannelFrame(ttk.Frame):
|
||||
[self.parent.subject.remove(frame) for frame in self.labelframes]
|
||||
self.parent.subject.remove(self)
|
||||
self.destroy()
|
||||
setattr(self.parent, f"{self.identifier}_frame", None)
|
||||
setattr(self.parent, f'{self.identifier}_frame', None)
|
||||
|
||||
|
||||
def _make_channelframe(parent, identifier):
|
||||
@ -298,7 +315,7 @@ def _make_channelframe(parent, identifier):
|
||||
"""
|
||||
|
||||
for i, labelframe in enumerate(
|
||||
getattr(self, "strips" if identifier == "strip" else "buses")
|
||||
getattr(self, 'strips' if identifier == 'strip' else 'buses')
|
||||
):
|
||||
labelframe.grid(row=0, column=i)
|
||||
label = labelframe.target.label
|
||||
@ -329,20 +346,20 @@ def _make_channelframe(parent, identifier):
|
||||
self.grid_configure()
|
||||
init_labels(self)
|
||||
|
||||
if identifier == "strip":
|
||||
if identifier == 'strip':
|
||||
CHANNELFRAME_cls = type(
|
||||
f"ChannelFrame{identifier.capitalize()}",
|
||||
f'ChannelFrame{identifier.capitalize()}',
|
||||
(ChannelFrame,),
|
||||
{
|
||||
"__init__": init_strip,
|
||||
'__init__': init_strip,
|
||||
},
|
||||
)
|
||||
else:
|
||||
CHANNELFRAME_cls = type(
|
||||
f"ChannelFrame{identifier.capitalize()}",
|
||||
f'ChannelFrame{identifier.capitalize()}',
|
||||
(ChannelFrame,),
|
||||
{
|
||||
"__init__": init_bus,
|
||||
'__init__': init_bus,
|
||||
},
|
||||
)
|
||||
return CHANNELFRAME_cls(parent)
|
||||
|
||||
@ -31,7 +31,7 @@ class Config(ttk.Frame):
|
||||
return self.parent.target
|
||||
|
||||
def getter(self, param):
|
||||
param = param.split(".")
|
||||
param = param.split('.')
|
||||
try:
|
||||
if len(param) == 2:
|
||||
target = getattr(self.target, param[0])
|
||||
@ -39,10 +39,10 @@ class Config(ttk.Frame):
|
||||
else:
|
||||
return getattr(self.target, param[0])
|
||||
except AttributeError as e:
|
||||
self.logger.error(f"{type(e).__name__}: {e}")
|
||||
self.logger.error(f'{type(e).__name__}: {e}')
|
||||
|
||||
def setter(self, param, value):
|
||||
param = param.split(".")
|
||||
param = param.split('.')
|
||||
try:
|
||||
if len(param) == 2:
|
||||
target = getattr(self.target, param[0])
|
||||
@ -50,22 +50,22 @@ class Config(ttk.Frame):
|
||||
else:
|
||||
setattr(self.target, param[0], value)
|
||||
except AttributeError as e:
|
||||
self.logger(f"{type(e).__name__}: {e}")
|
||||
self.logger(f'{type(e).__name__}: {e}')
|
||||
|
||||
def scale_press(self, *args):
|
||||
self.after(1, self.remove_events)
|
||||
|
||||
def remove_events(self):
|
||||
self.parent.target.event.remove("pdirty")
|
||||
self.parent.target.event.remove("ldirty")
|
||||
self.parent.target.event.remove('pdirty')
|
||||
self.parent.target.event.remove('ldirty')
|
||||
|
||||
def scale_release(self, *args):
|
||||
_base_values.run_update = False
|
||||
self.after(1, self.add_events)
|
||||
|
||||
def add_events(self):
|
||||
self.parent.target.event.add("pdirty")
|
||||
self.parent.target.event.add("ldirty")
|
||||
self.parent.target.event.add('pdirty')
|
||||
self.parent.target.event.add('ldirty')
|
||||
self.after(350, self.resume_updates)
|
||||
|
||||
def pause_updates(self, func, *args):
|
||||
@ -84,7 +84,7 @@ class Config(ttk.Frame):
|
||||
self.parent.nav_frame.info_text.set(round(val, 1))
|
||||
|
||||
def scale_leave(self, *args):
|
||||
self.parent.nav_frame.info_text.set("")
|
||||
self.parent.nav_frame.info_text.set('')
|
||||
|
||||
def scale_callback(self, param, *args):
|
||||
"""callback function for scale widget"""
|
||||
@ -98,16 +98,16 @@ class Config(ttk.Frame):
|
||||
self.slider_vars[self.slider_params.index(param)].set(val)
|
||||
|
||||
def toggle_p(self, param):
|
||||
val = self.param_vars[self.params.index(param)].get()
|
||||
val = self.bool_param_vars[self.bool_params.index(param)].get()
|
||||
self.setter(param, val)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton", background=f'{"green" if val else "white"}'
|
||||
f'{param}.TButton', background=f'{"green" if val else "white"}'
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
"""update parameters"""
|
||||
if subject == "pdirty":
|
||||
if subject == 'pdirty':
|
||||
self.sync()
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ class StripConfig(Config):
|
||||
|
||||
def make_row_0(self):
|
||||
if self.index < self.phys_in:
|
||||
if self.parent.kind.name == "basic":
|
||||
if self.parent.kind.name == 'basic':
|
||||
self.builder.create_audibility_slider()
|
||||
else:
|
||||
self.builder.create_comp_slider()
|
||||
@ -153,7 +153,7 @@ class StripConfig(Config):
|
||||
self.setter(param, val)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton", background=f'{"green" if val else "white"}'
|
||||
f'{param}.TButton', background=f'{"green" if val else "white"}'
|
||||
)
|
||||
|
||||
def toggle_b(self, param):
|
||||
@ -161,7 +161,7 @@ class StripConfig(Config):
|
||||
self.setter(param, val)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton", background=f'{"green" if val else "white"}'
|
||||
f'{param}.TButton', background=f'{"green" if val else "white"}'
|
||||
)
|
||||
|
||||
def teardown(self):
|
||||
@ -177,37 +177,36 @@ class StripConfig(Config):
|
||||
for i, param in enumerate(self.virt_out_params)
|
||||
]
|
||||
[
|
||||
self.param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.params)
|
||||
self.bool_param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
[
|
||||
self.slider_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.slider_params)
|
||||
if self.index < self.phys_in
|
||||
]
|
||||
if not _base_values.vban_connected: # slider vars not defined in RT Packet
|
||||
[
|
||||
self.slider_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.slider_params)
|
||||
if self.index < self.phys_in
|
||||
]
|
||||
|
||||
if not _configuration.themes_enabled:
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.phys_out_params_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.phys_out_params)
|
||||
]
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.virt_out_params_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.virt_out_params)
|
||||
]
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.param_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.params)
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
|
||||
|
||||
@ -218,7 +217,7 @@ class BusConfig(Config):
|
||||
self.grid(column=0, row=1, columnspan=4, padx=(2,))
|
||||
else:
|
||||
self.grid(column=0, row=3, columnspan=4, padx=(2,))
|
||||
self.builder = builders.BusConfigFrameBuilder(self)
|
||||
self.builder = builders.BusConfigFrameBuilder(self, parent)
|
||||
self.builder.setup()
|
||||
self.make_row_0()
|
||||
self.make_row_1()
|
||||
@ -238,55 +237,56 @@ class BusConfig(Config):
|
||||
self.builder.create_bus_mode_button()
|
||||
|
||||
def make_row_1(self):
|
||||
self.builder.create_bus_mono_button()
|
||||
self.builder.create_param_buttons()
|
||||
|
||||
def current_bus_mode(self):
|
||||
for mode in self.bus_modes:
|
||||
if getattr(self.target.mode, mode):
|
||||
return mode
|
||||
return self.target.mode.get()
|
||||
|
||||
def rotate_bus_modes_right(self, *args):
|
||||
current_mode = self.current_bus_mode()
|
||||
next = self.bus_modes.index(current_mode) + 1
|
||||
if next < len(self.bus_modes):
|
||||
setattr(
|
||||
self.target.mode,
|
||||
self.bus_modes[next],
|
||||
True,
|
||||
)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[next]])
|
||||
else:
|
||||
self.target.mode.normal = True
|
||||
self.bus_mode_label_text.set("Normal")
|
||||
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
|
||||
current_index = self.bus_modes.index(current_mode)
|
||||
next_index = (current_index + 1) % len(self.bus_modes)
|
||||
next_mode = self.bus_modes[next_index]
|
||||
|
||||
setattr(self.target.mode, next_mode, True)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[next_mode])
|
||||
|
||||
def rotate_bus_modes_left(self, *args):
|
||||
current_mode = self.current_bus_mode()
|
||||
prev = self.bus_modes.index(current_mode) - 1
|
||||
if prev < 0:
|
||||
self.target.mode.rearonly = True
|
||||
self.bus_mode_label_text.set("Rear Only")
|
||||
else:
|
||||
setattr(
|
||||
self.target.mode,
|
||||
self.bus_modes[prev],
|
||||
True,
|
||||
)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]])
|
||||
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
|
||||
current_index = self.bus_modes.index(current_mode)
|
||||
prev_index = (current_index - 1) % len(self.bus_modes)
|
||||
prev_mode = self.bus_modes[prev_index]
|
||||
|
||||
setattr(self.target.mode, prev_mode, True)
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[prev_mode])
|
||||
|
||||
def rotate_mono_right(self, *args):
|
||||
current_val = self.mono_modes.index(self.bus_mono_label_text.get())
|
||||
next_val = (current_val + 1) % 3
|
||||
self.bus_mono_label_text.set(self.mono_modes[next_val])
|
||||
self.setter('mono', next_val)
|
||||
|
||||
def rotate_mono_left(self, *args):
|
||||
current_val = self.mono_modes.index(self.bus_mono_label_text.get())
|
||||
next_val = (current_val - 1) % 3
|
||||
self.bus_mono_label_text.set(self.mono_modes[next_val])
|
||||
self.setter('mono', next_val)
|
||||
|
||||
def teardown(self):
|
||||
self.builder.teardown()
|
||||
|
||||
def sync(self):
|
||||
[
|
||||
self.param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.params)
|
||||
self.bool_param_vars[i].set(self.getter(param))
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()])
|
||||
if not _configuration.themes_enabled:
|
||||
[
|
||||
self.styletable.configure(
|
||||
f"{param}.TButton",
|
||||
background=f'{"green" if self.param_vars[i].get() else "white"}',
|
||||
f'{param}.TButton',
|
||||
background=f'{"green" if self.bool_param_vars[i].get() else "white"}',
|
||||
)
|
||||
for i, param in enumerate(self.params)
|
||||
for i, param in enumerate(self.bool_params)
|
||||
]
|
||||
|
||||
@ -12,66 +12,66 @@ configuration = {}
|
||||
|
||||
|
||||
def get_configpath():
|
||||
configpaths = [
|
||||
Path.cwd() / "configs",
|
||||
Path.home() / ".config" / "vm-compact" / "configs",
|
||||
Path.home() / "Documents" / "Voicemeeter" / "configs",
|
||||
]
|
||||
for configpath in configpaths:
|
||||
if configpath.exists():
|
||||
return configpath
|
||||
for pn in (
|
||||
Path.home() / '.config' / 'vm-compact',
|
||||
Path.home() / 'Documents' / 'Voicemeeter' / 'vm-compact',
|
||||
Path.cwd() / '_internal' / 'configs',
|
||||
Path.cwd() / 'configs',
|
||||
):
|
||||
if pn.exists():
|
||||
return pn
|
||||
|
||||
|
||||
if configpath := get_configpath():
|
||||
filepaths = list(configpath.glob("*.toml"))
|
||||
if any(f.stem in ("app", "vban") for f in filepaths):
|
||||
filepaths = list(configpath.glob('*.toml'))
|
||||
if any(f.stem in ('app', 'vban') for f in filepaths):
|
||||
configs = {}
|
||||
for filepath in filepaths:
|
||||
filename = filepath.with_suffix("").stem
|
||||
if filename in ("app", "vban"):
|
||||
filename = filepath.with_suffix('').stem
|
||||
if filename in ('app', 'vban'):
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
with open(filepath, 'rb') as f:
|
||||
configs[filename] = tomllib.load(f)
|
||||
logger.info(f"configuration: {filename} loaded into memory")
|
||||
logger.info(f'configuration: {filename} loaded into memory')
|
||||
except tomllib.TOMLDecodeError:
|
||||
logger.error(f"Invalid TOML config: configs/{filename.stem}")
|
||||
logger.error(f'Invalid TOML config: configs/{filename.stem}')
|
||||
configuration |= configs
|
||||
|
||||
_defaults = {
|
||||
"configs": {
|
||||
"config": None,
|
||||
'configs': {
|
||||
'config': None,
|
||||
},
|
||||
"theme": {
|
||||
"enabled": True,
|
||||
"mode": "light",
|
||||
'theme': {
|
||||
'enabled': True,
|
||||
'mode': 'light',
|
||||
},
|
||||
"extends": {
|
||||
"extended": True,
|
||||
"extends_horizontal": True,
|
||||
'extends': {
|
||||
'extended': True,
|
||||
'extends_horizontal': True,
|
||||
},
|
||||
"channel": {
|
||||
"width": 80,
|
||||
"height": 130,
|
||||
"xpadding": 3,
|
||||
'channel': {
|
||||
'width': 80,
|
||||
'height': 130,
|
||||
'xpadding': 3,
|
||||
},
|
||||
"mwscroll_step": {
|
||||
"size": 3,
|
||||
'mwscroll_step': {
|
||||
'size': 3,
|
||||
},
|
||||
"submixes": {
|
||||
"default": 0,
|
||||
'submixes': {
|
||||
'default': 0,
|
||||
},
|
||||
"navigation": {"show": True},
|
||||
'navigation': {'show': False},
|
||||
}
|
||||
|
||||
|
||||
if "app" in configuration:
|
||||
if 'app' in configuration:
|
||||
for key in _defaults:
|
||||
if key in configuration["app"]:
|
||||
configuration["app"][key] = _defaults[key] | configuration["app"][key]
|
||||
if key in configuration['app']:
|
||||
configuration['app'][key] = _defaults[key] | configuration['app'][key]
|
||||
else:
|
||||
configuration["app"][key] = _defaults[key]
|
||||
configuration['app'][key] = _defaults[key]
|
||||
else:
|
||||
configuration["app"] = _defaults
|
||||
configuration['app'] = _defaults
|
||||
|
||||
|
||||
def get_configuration(key):
|
||||
@ -80,19 +80,19 @@ def get_configuration(key):
|
||||
|
||||
|
||||
def loader(kind_id, target):
|
||||
configs = {"reset": target.configs["reset"]}
|
||||
configs = {'reset': target.configs['reset']}
|
||||
if configpath := get_configpath():
|
||||
userconfigpath = configpath / kind_id
|
||||
if userconfigpath.exists():
|
||||
filepaths = list(userconfigpath.glob("*.toml"))
|
||||
filepaths = list(userconfigpath.glob('*.toml'))
|
||||
for filepath in filepaths:
|
||||
identifier = filepath.with_suffix("").stem
|
||||
identifier = filepath.with_suffix('').stem
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
with open(filepath, 'rb') as f:
|
||||
configs[identifier] = tomllib.load(f)
|
||||
logger.info(f"loader: {identifier} loaded into memory")
|
||||
logger.info(f'loader: {identifier} loaded into memory')
|
||||
except tomllib.TOMLDecodeError:
|
||||
logger.error(f"Invalid TOML config: configs/{filename.stem}")
|
||||
logger.error(f'Invalid TOML config: configs/{filename.stem}')
|
||||
|
||||
target.configs = configs
|
||||
return target.configs
|
||||
|
||||
@ -4,7 +4,7 @@ from voicemeeterlib import kinds
|
||||
|
||||
from .configurations import get_configuration
|
||||
|
||||
configuration = get_configuration("app")
|
||||
configuration = get_configuration('app')
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
@ -20,32 +20,32 @@ class SingletonMeta(type):
|
||||
@dataclass
|
||||
class Configurations(metaclass=SingletonMeta):
|
||||
# is the gui extended
|
||||
extended: bool = configuration["extends"]["extended"]
|
||||
extended: bool = configuration['extends']['extended']
|
||||
# direction the gui extends
|
||||
extends_horizontal: bool = configuration["extends"]["extends_horizontal"]
|
||||
extends_horizontal: bool = configuration['extends']['extends_horizontal']
|
||||
# are themes enabled
|
||||
themes_enabled: bool = configuration["theme"]["enabled"]
|
||||
themes_enabled: bool = configuration['theme']['enabled']
|
||||
# light or dark
|
||||
theme_mode: str = configuration["theme"]["mode"]
|
||||
theme_mode: str = configuration['theme']['mode']
|
||||
# size of mousewheel scroll step
|
||||
mwscroll_step: int = configuration["mwscroll_step"]["size"]
|
||||
mwscroll_step: int = configuration['mwscroll_step']['size']
|
||||
# bus assigned as current submix
|
||||
submixes: int = configuration["submixes"]["default"]
|
||||
submixes: int = configuration['submixes']['default']
|
||||
|
||||
# width of a single channel labelframe
|
||||
channel_width: int = configuration["channel"]["width"]
|
||||
channel_width: int = configuration['channel']['width']
|
||||
# height of a single channel labelframe
|
||||
channel_height: int = configuration["channel"]["height"]
|
||||
channel_height: int = configuration['channel']['height']
|
||||
# xpadding for a single channel labelframe
|
||||
channel_xpadding: int = configuration["channel"]["xpadding"]
|
||||
channel_xpadding: int = configuration['channel']['xpadding']
|
||||
|
||||
# do we grid the navigation frame?
|
||||
navigation_show: bool = configuration["navigation"]["show"]
|
||||
navigation_show: bool = configuration['navigation']['show']
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
if "configs" in configuration:
|
||||
return configuration["configs"]["config"]
|
||||
if 'configs' in configuration:
|
||||
return configuration['configs']['config']
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -61,10 +61,6 @@ class BaseValues(metaclass=SingletonMeta):
|
||||
_base_values = BaseValues()
|
||||
_configuration = Configurations()
|
||||
|
||||
_kinds = {kind.name: kind for kind in kinds.kinds_all}
|
||||
|
||||
_kinds_all = _kinds.values()
|
||||
|
||||
|
||||
def kind_get(kind_id):
|
||||
return _kinds[kind_id]
|
||||
return kinds.request_kind_map(kind_id)
|
||||
|
||||
@ -19,7 +19,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
else:
|
||||
self.level_offset = parent.phys_in * 2 + (index - parent.phys_in) * 8
|
||||
|
||||
self.builder = builders.ChannelLabelFrameBuilder(self, index, id="gainlayer")
|
||||
self.builder = builders.ChannelLabelFrameBuilder(self, index, id='gainlayer')
|
||||
self.builder.setup()
|
||||
self.builder.add_progressbar()
|
||||
self.builder.add_scale()
|
||||
@ -38,20 +38,20 @@ class GainLayer(ttk.LabelFrame):
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return "gainlayer"
|
||||
return 'gainlayer'
|
||||
|
||||
def getter(self, param):
|
||||
try:
|
||||
return getattr(self.target, param)
|
||||
except AttributeError as e:
|
||||
self.logger(f"{type(e).__name__}: {e}")
|
||||
self.logger(f'{type(e).__name__}: {e}')
|
||||
|
||||
def setter(self, param, value):
|
||||
if param in dir(self.target): # avoid calling getattr (with hasattr)
|
||||
setattr(self.target, param, value)
|
||||
|
||||
def reset_gain(self, *args):
|
||||
self.setter("gain", 0)
|
||||
self.setter('gain', 0)
|
||||
self.gain.set(0)
|
||||
self.gainlabel.set(self.gain.get())
|
||||
|
||||
@ -59,23 +59,23 @@ class GainLayer(ttk.LabelFrame):
|
||||
"""callback function for scale widget"""
|
||||
|
||||
val = round(self.gain.get(), 1)
|
||||
self.setter("gain", val)
|
||||
self.setter('gain', val)
|
||||
self.gainlabel.set(val)
|
||||
|
||||
def scale_press(self, *args):
|
||||
self.after(1, self.remove_events)
|
||||
|
||||
def remove_events(self):
|
||||
self.parent.target.event.remove("pdirty")
|
||||
self.parent.target.event.remove("ldirty")
|
||||
self.parent.target.event.remove('pdirty')
|
||||
self.parent.target.event.remove('ldirty')
|
||||
|
||||
def scale_release(self, *args):
|
||||
_base_values.run_update = False
|
||||
self.after(1, self.add_events)
|
||||
|
||||
def add_events(self):
|
||||
self.parent.target.event.add("pdirty")
|
||||
self.parent.target.event.add("ldirty")
|
||||
self.parent.target.event.add('pdirty')
|
||||
self.parent.target.event.add('ldirty')
|
||||
self.after(500, self.resume_updates)
|
||||
|
||||
def pause_updates(self, func, *args):
|
||||
@ -103,7 +103,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
self.gain.set(12)
|
||||
elif self.gain.get() < -60:
|
||||
self.gain.set(-60)
|
||||
self.setter("gain", self.gain.get())
|
||||
self.setter('gain', self.gain.get())
|
||||
self.after(1, self.resume_updates)
|
||||
|
||||
def set_on(self):
|
||||
@ -116,20 +116,20 @@ class GainLayer(ttk.LabelFrame):
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}On{self.index}.TButton",
|
||||
f'{self.identifier}On{self.index}.TButton',
|
||||
background=f'{"green" if self.on.get() else "white"}',
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "ldirty":
|
||||
if subject == 'ldirty':
|
||||
self.upd_levels()
|
||||
elif subject == "pdirty":
|
||||
elif subject == 'pdirty':
|
||||
self.sync_params()
|
||||
elif subject == "labelframe":
|
||||
elif subject == 'labelframe':
|
||||
self.after(5, self.sync_labels)
|
||||
|
||||
def sync_params(self):
|
||||
self.gain.set(self.getter("gain"))
|
||||
self.gain.set(self.getter('gain'))
|
||||
self.gainlabel.set(round(self.gain.get(), 1))
|
||||
self.on.set(
|
||||
getattr(
|
||||
@ -139,7 +139,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"{self.identifier}On{self.index}.TButton",
|
||||
f'{self.identifier}On{self.index}.TButton',
|
||||
background=f'{"green" if self.on.get() else "white"}',
|
||||
)
|
||||
|
||||
@ -147,7 +147,7 @@ class GainLayer(ttk.LabelFrame):
|
||||
"""sync params with voicemeeter"""
|
||||
retval = self.parent.target.strip[self.index].label
|
||||
if len(retval) > 10:
|
||||
retval = f"{retval[:8]}.."
|
||||
retval = f'{retval[:8]}..'
|
||||
if not retval:
|
||||
self.parent.columnconfigure(self.index, minsize=0)
|
||||
self.parent.parent.subject.remove(self)
|
||||
@ -161,17 +161,18 @@ class GainLayer(ttk.LabelFrame):
|
||||
"""
|
||||
Updates level values.
|
||||
"""
|
||||
|
||||
if self.parent.target.strip[self.index].levels.is_updated:
|
||||
val = max(self.parent.target.strip[self.index].levels.prefader)
|
||||
self.level.set(
|
||||
(
|
||||
0
|
||||
if self.parent.parent.strip_frame.strips[self.index].mute.get()
|
||||
or not self.on.get()
|
||||
else 72 + val - 12 + self.gain.get()
|
||||
)
|
||||
)
|
||||
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
|
||||
if (
|
||||
self.parent.parent.strip_frame.strips[self.index].mute.get()
|
||||
or not self.on.get()
|
||||
):
|
||||
level_display = 0
|
||||
else:
|
||||
level_db = val + self.gain.get()
|
||||
level_display = max(0, min(72, level_db + 60))
|
||||
self.level.set(level_display)
|
||||
|
||||
def grid_configure(self):
|
||||
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))
|
||||
@ -201,8 +202,8 @@ class SubMixFrame(ttk.Frame):
|
||||
self.parent = parent
|
||||
self.phys_in, self.virt_in = parent.kind.ins
|
||||
self.phys_out, self.virt_out = parent.kind.outs
|
||||
self.buses = tuple(f"A{i+1}" for i in range(self.phys_out)) + tuple(
|
||||
f"B{i+1}" for i in range(self.virt_out)
|
||||
self.buses = tuple(f'A{i + 1}' for i in range(self.phys_out)) + tuple(
|
||||
f'B{i + 1}' for i in range(self.virt_out)
|
||||
)
|
||||
|
||||
self.gainlayers = [
|
||||
@ -221,7 +222,7 @@ class SubMixFrame(ttk.Frame):
|
||||
else:
|
||||
if parent.bus_frame and parent.bus_frame.grid_info():
|
||||
self.grid(
|
||||
row=parent.bus_frame.grid_info()["row"], column=0, sticky=(tk.W)
|
||||
row=parent.bus_frame.grid_info()['row'], column=0, sticky=(tk.W)
|
||||
)
|
||||
parent.bus_frame.grid_remove()
|
||||
else:
|
||||
@ -256,9 +257,9 @@ class SubMixFrame(ttk.Frame):
|
||||
)
|
||||
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
if subject == 'pdirty':
|
||||
for labelframe in self.labelframes:
|
||||
labelframe.on_update("labelframe")
|
||||
labelframe.on_update('labelframe')
|
||||
|
||||
def grid_configure(self):
|
||||
[
|
||||
|
||||
1
vmcompact/gui/banana/__init__.py
Normal file
1
vmcompact/gui/banana/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import run
|
||||
11
vmcompact/gui/banana/main.py
Normal file
11
vmcompact/gui/banana/main.py
Normal file
@ -0,0 +1,11 @@
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def run():
|
||||
KIND_ID = 'banana'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
1
vmcompact/gui/basic/__init__.py
Normal file
1
vmcompact/gui/basic/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import run
|
||||
11
vmcompact/gui/basic/main.py
Normal file
11
vmcompact/gui/basic/main.py
Normal file
@ -0,0 +1,11 @@
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def run():
|
||||
KIND_ID = 'basic'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
1
vmcompact/gui/potato/__init__.py
Normal file
1
vmcompact/gui/potato/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .main import run
|
||||
11
vmcompact/gui/potato/main.py
Normal file
11
vmcompact/gui/potato/main.py
Normal file
@ -0,0 +1,11 @@
|
||||
import voicemeeterlib
|
||||
|
||||
import vmcompact
|
||||
|
||||
|
||||
def run():
|
||||
KIND_ID = 'potato'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vmr:
|
||||
app = vmcompact.connect(KIND_ID, vmr)
|
||||
app.mainloop()
|
||||
@ -4,11 +4,10 @@ import webbrowser
|
||||
from functools import partial
|
||||
from tkinter import messagebox
|
||||
|
||||
import sv_ttk
|
||||
import vban_cmd
|
||||
from vban_cmd.error import VBANCMDConnectionError
|
||||
|
||||
import sv_ttk
|
||||
|
||||
from .data import _base_values, _configuration, get_configuration, kind_get
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -20,8 +19,8 @@ class Menus(tk.Menu):
|
||||
self.parent = parent
|
||||
self.vmr = vmr
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.vban_config = get_configuration("vban")
|
||||
self.app_config = get_configuration("app")
|
||||
self.vban_config = get_configuration('vban')
|
||||
self.app_config = get_configuration('app')
|
||||
self._is_topmost = tk.BooleanVar()
|
||||
self._lock = tk.BooleanVar()
|
||||
self._unlock = tk.BooleanVar()
|
||||
@ -31,9 +30,9 @@ class Menus(tk.Menu):
|
||||
|
||||
# voicemeeter menu
|
||||
self.menu_voicemeeter = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_voicemeeter, label="Voicemeeter")
|
||||
self.add_cascade(menu=self.menu_voicemeeter, label='Voicemeeter')
|
||||
self.menu_voicemeeter.add_checkbutton(
|
||||
label="Always On Top",
|
||||
label='Always On Top',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._is_topmost,
|
||||
@ -41,51 +40,51 @@ class Menus(tk.Menu):
|
||||
)
|
||||
self.menu_voicemeeter.add_separator()
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Show",
|
||||
label='Show',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "show"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'show'),
|
||||
)
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Hide",
|
||||
label='Hide',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "hide"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'hide'),
|
||||
)
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Restart",
|
||||
label='Restart',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "restart"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'restart'),
|
||||
)
|
||||
self.menu_voicemeeter.add_command(
|
||||
label="Shutdown",
|
||||
label='Shutdown',
|
||||
underline=0,
|
||||
command=partial(self.action_invoke_voicemeeter, "shutdown"),
|
||||
command=partial(self.action_invoke_voicemeeter, 'shutdown'),
|
||||
)
|
||||
self.menu_voicemeeter.add_separator()
|
||||
self.menu_lock = tk.Menu(self.menu_voicemeeter, tearoff=0)
|
||||
self.menu_voicemeeter.add_cascade(
|
||||
menu=self.menu_lock, label="GUI Lock", underline=0
|
||||
menu=self.menu_lock, label='GUI Lock', underline=0
|
||||
)
|
||||
self.menu_lock.add_checkbutton(
|
||||
label="Lock",
|
||||
label='Lock',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._lock,
|
||||
command=partial(self.action_set_voicemeeter, "lock"),
|
||||
command=partial(self.action_set_voicemeeter, 'lock'),
|
||||
)
|
||||
self.menu_lock.add_checkbutton(
|
||||
label="Unlock",
|
||||
label='Unlock',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._unlock,
|
||||
command=partial(self.action_set_voicemeeter, "lock", False),
|
||||
command=partial(self.action_set_voicemeeter, 'lock', False),
|
||||
)
|
||||
|
||||
# configs menu
|
||||
self.menu_configs = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_configs, label="Configs")
|
||||
self.add_cascade(menu=self.menu_configs, label='Configs')
|
||||
self.menu_configs_load = tk.Menu(self.menu_configs, tearoff=0)
|
||||
self.menu_configs.add_cascade(menu=self.menu_configs_load, label="Load config")
|
||||
self.config_defaults = {"reset"}
|
||||
self.menu_configs.add_cascade(menu=self.menu_configs_load, label='Load config')
|
||||
self.config_defaults = {'reset'}
|
||||
if len(self.parent.userconfigs) > len(self.config_defaults) and all(
|
||||
key in self.parent.userconfigs for key in self.config_defaults
|
||||
):
|
||||
@ -97,22 +96,24 @@ class Menus(tk.Menu):
|
||||
if profile not in self.config_defaults
|
||||
]
|
||||
else:
|
||||
self.menu_configs.entryconfig(0, state="disabled")
|
||||
self.menu_configs.entryconfig(0, state='disabled')
|
||||
self.menu_configs.add_command(
|
||||
label="Reset to defaults", command=self.load_defaults
|
||||
label='Reset to defaults', command=self.load_defaults
|
||||
)
|
||||
|
||||
# layout menu
|
||||
self.menu_layout = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_layout, label="Layout")
|
||||
self.add_cascade(menu=self.menu_layout, label='Layout')
|
||||
# layout/submixes
|
||||
# here we build menu regardless of kind but disable if not potato
|
||||
buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3))
|
||||
buses = tuple(f'A{i + 1}' for i in range(5)) + tuple(
|
||||
f'B{i + 1}' for i in range(3)
|
||||
)
|
||||
self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(menu=self.menu_submixes, label="Submixes")
|
||||
self.menu_layout.add_cascade(menu=self.menu_submixes, label='Submixes')
|
||||
[
|
||||
self.menu_submixes.add_checkbutton(
|
||||
label=f"Bus {buses[i]}",
|
||||
label=f'Bus {buses[i]}',
|
||||
underline=0,
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
@ -122,94 +123,94 @@ class Menus(tk.Menu):
|
||||
for i in range(8)
|
||||
]
|
||||
self._selected_bus[_configuration.submixes].set(True)
|
||||
if self.parent.kind.name != "potato":
|
||||
self.menu_layout.entryconfig(0, state="disabled")
|
||||
if self.parent.kind.name != 'potato':
|
||||
self.menu_layout.entryconfig(0, state='disabled')
|
||||
# layout/extends
|
||||
self.menu_extends = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(
|
||||
menu=self.menu_extends, label="Extends", underline=0
|
||||
menu=self.menu_extends, label='Extends', underline=0
|
||||
)
|
||||
self.menu_extends.add_command(
|
||||
label="horizontal",
|
||||
label='horizontal',
|
||||
underline=0,
|
||||
command=partial(self.switch_orientation, extends_horizontal=True),
|
||||
)
|
||||
self.menu_extends.add_command(
|
||||
label="vertical",
|
||||
label='vertical',
|
||||
underline=0,
|
||||
command=partial(self.switch_orientation, extends_horizontal=False),
|
||||
)
|
||||
self.menu_extends.entryconfig(
|
||||
0 if _configuration.extends_horizontal else 1, state="disabled"
|
||||
0 if _configuration.extends_horizontal else 1, state='disabled'
|
||||
)
|
||||
# layout/themes
|
||||
self.menu_themes = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(menu=self.menu_themes, label="Themes")
|
||||
self.menu_layout.add_cascade(menu=self.menu_themes, label='Themes')
|
||||
self.menu_themes.add_command(
|
||||
label="light", command=partial(self.load_theme, "light")
|
||||
label='light', command=partial(self.load_theme, 'light')
|
||||
)
|
||||
self.menu_themes.add_command(
|
||||
label="dark", command=partial(self.load_theme, "dark")
|
||||
label='dark', command=partial(self.load_theme, 'dark')
|
||||
)
|
||||
self.menu_themes.entryconfig(
|
||||
0 if self.app_config["theme"]["mode"] == "light" else 1,
|
||||
state="disabled",
|
||||
0 if self.app_config['theme']['mode'] == 'light' else 1,
|
||||
state='disabled',
|
||||
)
|
||||
if not _configuration.themes_enabled:
|
||||
self.menu_layout.entryconfig(2, state="disabled")
|
||||
self.menu_layout.entryconfig(2, state='disabled')
|
||||
# layout/navigation
|
||||
self.menu_navigation = tk.Menu(self.menu_layout, tearoff=0)
|
||||
self.menu_layout.add_cascade(menu=self.menu_navigation, label="Navigation")
|
||||
self.menu_layout.add_cascade(menu=self.menu_navigation, label='Navigation')
|
||||
self.menu_navigation.add_checkbutton(
|
||||
label="show",
|
||||
label='show',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._navigation_show,
|
||||
command=partial(self.toggle_navigation, "show"),
|
||||
command=partial(self.toggle_navigation, 'show'),
|
||||
)
|
||||
self.menu_navigation.add_checkbutton(
|
||||
label="hide",
|
||||
label='hide',
|
||||
onvalue=1,
|
||||
offvalue=0,
|
||||
variable=self._navigation_hide,
|
||||
command=partial(self.toggle_navigation, "hide"),
|
||||
command=partial(self.toggle_navigation, 'hide'),
|
||||
)
|
||||
|
||||
# vban connect menu
|
||||
self.menu_vban = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_vban, label="VBAN")
|
||||
self.add_cascade(menu=self.menu_vban, label='VBAN')
|
||||
if self.vban_config:
|
||||
for i, _ in enumerate(self.vban_config):
|
||||
setattr(self, f"menu_vban_{i+1}", tk.Menu(self.menu_vban, tearoff=0))
|
||||
target_menu = getattr(self, f"menu_vban_{i+1}")
|
||||
setattr(self, f'menu_vban_{i + 1}', tk.Menu(self.menu_vban, tearoff=0))
|
||||
target_menu = getattr(self, f'menu_vban_{i + 1}')
|
||||
self.menu_vban.add_cascade(
|
||||
menu=target_menu,
|
||||
label=f"{self.vban_config[f'connection-{i+1}']['streamname']}",
|
||||
label=f'{self.vban_config[f"connection-{i + 1}"]["streamname"]}',
|
||||
underline=0,
|
||||
)
|
||||
target_menu.add_command(
|
||||
label="Connect", command=partial(self.vban_connect, i)
|
||||
label='Connect', command=partial(self.vban_connect, i)
|
||||
)
|
||||
target_menu.add_command(
|
||||
label="Disconnect", command=partial(self.vban_disconnect, i)
|
||||
label='Disconnect', command=partial(self.vban_disconnect, i)
|
||||
)
|
||||
target_menu.entryconfig(1, state="disabled")
|
||||
target_menu.entryconfig(1, state='disabled')
|
||||
else:
|
||||
self.entryconfig(4, state="disabled")
|
||||
self.entryconfig(4, state='disabled')
|
||||
|
||||
# Help menu
|
||||
self.menu_help = tk.Menu(self, tearoff=0)
|
||||
self.add_cascade(menu=self.menu_help, label="Help")
|
||||
self.add_cascade(menu=self.menu_help, label='Help')
|
||||
self.menu_help.add_command(
|
||||
label="Voicemeeter Site",
|
||||
label='Voicemeeter Site',
|
||||
command=self.documentation,
|
||||
)
|
||||
self.menu_help.add_command(
|
||||
label="Source Code",
|
||||
label='Source Code',
|
||||
command=self.github,
|
||||
)
|
||||
self.menu_help.add_command(
|
||||
label="App Creator",
|
||||
label='App Creator',
|
||||
command=self.onyxandiris,
|
||||
)
|
||||
|
||||
@ -220,56 +221,56 @@ class Menus(tk.Menu):
|
||||
|
||||
def enable_vban_menus(self):
|
||||
[
|
||||
self.menu_vban.entryconfig(j, state="normal")
|
||||
self.menu_vban.entryconfig(j, state='normal')
|
||||
for j, _ in enumerate(self.menu_vban.winfo_children())
|
||||
]
|
||||
|
||||
def action_invoke_voicemeeter(self, cmd):
|
||||
if fn := getattr(self.target.command, cmd):
|
||||
fn()
|
||||
if cmd == "shutdown":
|
||||
if cmd == 'shutdown':
|
||||
self.parent.on_close_window()
|
||||
|
||||
def action_set_voicemeeter(self, cmd, val=True):
|
||||
if cmd == "lock":
|
||||
if cmd == 'lock':
|
||||
self._lock.set(val)
|
||||
self._unlock.set(not self._lock.get())
|
||||
setattr(self.target.command, cmd, val)
|
||||
|
||||
def load_custom_profile(self, profile):
|
||||
self.logger.info(f"loading user profile {profile}")
|
||||
self.logger.info(f'loading user profile {profile}')
|
||||
self.target.apply(profile)
|
||||
if not _base_values.run_update:
|
||||
self.parent.subject.notify("pdirty")
|
||||
self.parent.subject.notify('pdirty')
|
||||
|
||||
def load_profile(self, profile):
|
||||
self.logger.info(f"loading user profile {profile}")
|
||||
self.logger.info(f'loading user profile {profile}')
|
||||
self.target.apply_config(profile)
|
||||
if not _base_values.run_update:
|
||||
self.parent.subject.notify("pdirty")
|
||||
self.parent.subject.notify('pdirty')
|
||||
|
||||
def load_defaults(self):
|
||||
msg = (
|
||||
"Are you sure you want to Reset values to defaults?",
|
||||
"Physical strips B1, Virtual strips A1",
|
||||
"Mono, Solo, Mute, EQ all OFF",
|
||||
"Gain sliders for Strip/Bus at 0.0",
|
||||
'Are you sure you want to Reset values to defaults?',
|
||||
'Physical strips B1, Virtual strips A1',
|
||||
'Mono, Solo, Mute, EQ all OFF',
|
||||
'Gain sliders for Strip/Bus at 0.0',
|
||||
)
|
||||
resp = messagebox.askyesno(message="\n".join(msg))
|
||||
resp = messagebox.askyesno(message='\n'.join(msg))
|
||||
if resp:
|
||||
self.load_profile("reset")
|
||||
self.load_profile('reset')
|
||||
|
||||
def always_on_top(self):
|
||||
self.parent.attributes("-topmost", self._is_topmost.get())
|
||||
self.parent.attributes('-topmost', self._is_topmost.get())
|
||||
|
||||
def switch_orientation(self, extends_horizontal: bool = True, *args):
|
||||
_configuration.extends_horizontal = extends_horizontal
|
||||
if extends_horizontal:
|
||||
self.menu_extends.entryconfig(0, state="disabled")
|
||||
self.menu_extends.entryconfig(1, state="normal")
|
||||
self.menu_extends.entryconfig(0, state='disabled')
|
||||
self.menu_extends.entryconfig(1, state='normal')
|
||||
else:
|
||||
self.menu_extends.entryconfig(1, state="disabled")
|
||||
self.menu_extends.entryconfig(0, state="normal")
|
||||
self.menu_extends.entryconfig(1, state='disabled')
|
||||
self.menu_extends.entryconfig(0, state='normal')
|
||||
|
||||
def set_submix(self, i):
|
||||
if _configuration.submixes != i:
|
||||
@ -279,38 +280,38 @@ class Menus(tk.Menu):
|
||||
self.parent.nav_frame.show_submix()
|
||||
for j, var in enumerate(self._selected_bus):
|
||||
var.set(i == j)
|
||||
self.parent.subject.notify("submix")
|
||||
self.parent.subject.notify('submix')
|
||||
|
||||
def load_theme(self, theme):
|
||||
sv_ttk.set_theme(theme)
|
||||
_configuration.theme_mode = theme
|
||||
self.menu_themes.entryconfig(
|
||||
0,
|
||||
state=f"{'disabled' if theme == 'light' else 'normal'}",
|
||||
state=f'{"disabled" if theme == "light" else "normal"}',
|
||||
)
|
||||
self.menu_themes.entryconfig(
|
||||
1,
|
||||
state=f"{'disabled' if theme == 'dark' else 'normal'}",
|
||||
state=f'{"disabled" if theme == "dark" else "normal"}',
|
||||
)
|
||||
[
|
||||
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
for menu in self.winfo_children()
|
||||
if isinstance(menu, tk.Menu)
|
||||
]
|
||||
self.menu_lock.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
self.menu_configs_load.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
self.menu_lock.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
self.menu_configs_load.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
[
|
||||
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
for menu in self.menu_vban.winfo_children()
|
||||
if isinstance(menu, tk.Menu)
|
||||
]
|
||||
[
|
||||
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
|
||||
menu.config(bg=f'{"black" if theme == "dark" else "white"}')
|
||||
for menu in self.menu_layout.winfo_children()
|
||||
if isinstance(menu, tk.Menu)
|
||||
]
|
||||
self.logger.info(
|
||||
f"Finished loading theme Sunvalley {sv_ttk.get_theme().capitalize()} theme"
|
||||
f'Finished loading theme Sunvalley {sv_ttk.get_theme().capitalize()} theme'
|
||||
)
|
||||
|
||||
def menu_teardown(self, i):
|
||||
@ -321,10 +322,10 @@ class Menus(tk.Menu):
|
||||
try:
|
||||
self.menu_configs_load.delete(profile)
|
||||
except tk._tkinter.tclError as e:
|
||||
self.logger.warning(f"{type(e).__name__}: {e}")
|
||||
self.logger.warning(f'{type(e).__name__}: {e}')
|
||||
|
||||
[
|
||||
self.menu_vban.entryconfig(j, state="disabled")
|
||||
self.menu_vban.entryconfig(j, state='disabled')
|
||||
for j, _ in enumerate(self.menu_vban.winfo_children())
|
||||
if j != i
|
||||
]
|
||||
@ -336,44 +337,46 @@ class Menus(tk.Menu):
|
||||
self.menu_configs_load.add_command(
|
||||
label=profile, command=partial(self.load_profile, profile)
|
||||
)
|
||||
self.menu_configs.entryconfig(0, state="normal")
|
||||
self.menu_configs.entryconfig(0, state='normal')
|
||||
else:
|
||||
self.menu_configs.entryconfig(0, state="disabled")
|
||||
self.menu_configs.entryconfig(0, state='disabled')
|
||||
|
||||
def toggle_navigation(self, cmd=None):
|
||||
if cmd == "show":
|
||||
self.logger.debug("show navframe")
|
||||
if cmd == 'show':
|
||||
self.logger.debug('show navframe')
|
||||
self.parent.nav_frame.grid()
|
||||
self._navigation_show.set(True)
|
||||
self._navigation_hide.set(not self._navigation_show.get())
|
||||
else:
|
||||
self.logger.debug("hide navframe")
|
||||
self.logger.debug('hide navframe')
|
||||
self.parent.nav_frame.grid_remove()
|
||||
self._navigation_hide.set(True)
|
||||
self._navigation_show.set(not self._navigation_hide.get())
|
||||
|
||||
def vban_connect(self, i):
|
||||
opts = {}
|
||||
opts |= self.vban_config[f"connection-{i+1}"]
|
||||
kind_id = opts.pop("kind")
|
||||
opts |= self.vban_config[f'connection-{i + 1}']
|
||||
kind_id = opts.pop('kind')
|
||||
if 'ip' in opts:
|
||||
opts['host'] = opts.pop('ip')
|
||||
self.vban = vban_cmd.api(kind_id, **opts)
|
||||
# login to vban interface
|
||||
try:
|
||||
self.logger.info(f"Attempting vban connection to {opts.get('ip')}")
|
||||
self.logger.info(f'Attempting vban connection to {opts.get("host")}')
|
||||
self.vban.login()
|
||||
except VBANCMDConnectionError as e:
|
||||
self.vban.logout()
|
||||
msg = (
|
||||
f"Timeout attempting to establish connection to {opts.get('ip')}",
|
||||
f"Please check your connection settings",
|
||||
f'Timeout attempting to establish connection to {opts.get("host")}',
|
||||
'Please check your connection settings',
|
||||
)
|
||||
messagebox.showerror("Connection Error", "\n".join(msg))
|
||||
msg = (str(e), f"resuming local connection")
|
||||
self.logger.error(", ".join(msg))
|
||||
messagebox.showerror('Connection Error', '\n'.join(msg))
|
||||
msg = (str(e), 'resuming local connection')
|
||||
self.logger.error(', '.join(msg))
|
||||
self.after(1, self.enable_vban_menus)
|
||||
return
|
||||
self.menu_teardown(i)
|
||||
self.vban.event.add(["pdirty", "ldirty"])
|
||||
self.vban.event.add(['pdirty', 'ldirty'])
|
||||
# destroy the current App frames
|
||||
self.parent._destroy_top_level_frames()
|
||||
_base_values.vban_connected = True
|
||||
@ -382,17 +385,17 @@ class Menus(tk.Menu):
|
||||
# build new app frames according to a kind
|
||||
kind = kind_get(kind_id)
|
||||
self.parent.build_app(kind, self.vban)
|
||||
target_menu = getattr(self, f"menu_vban_{i+1}")
|
||||
target_menu.entryconfig(0, state="disabled")
|
||||
target_menu.entryconfig(1, state="normal")
|
||||
target_menu = getattr(self, f'menu_vban_{i + 1}')
|
||||
target_menu.entryconfig(0, state='disabled')
|
||||
target_menu.entryconfig(1, state='normal')
|
||||
self.menu_layout.entryconfig(
|
||||
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
|
||||
0, state=f'{"normal" if kind.name == "potato" else "disabled"}'
|
||||
)
|
||||
# ensure the configs are reloaded into memory
|
||||
if "config" in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__["config"]
|
||||
if "userconfigs" in self.parent.__dict__:
|
||||
del self.parent.__dict__["userconfigs"]
|
||||
if 'config' in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__['config']
|
||||
if 'userconfigs' in self.parent.__dict__:
|
||||
del self.parent.__dict__['userconfigs']
|
||||
self.menu_setup()
|
||||
|
||||
def vban_disconnect(self, i):
|
||||
@ -407,26 +410,26 @@ class Menus(tk.Menu):
|
||||
# build new app frames according to a kind
|
||||
kind = kind_get(self.vmr.type)
|
||||
self.parent.build_app(kind)
|
||||
target_menu = getattr(self, f"menu_vban_{i+1}")
|
||||
target_menu.entryconfig(0, state="normal")
|
||||
target_menu.entryconfig(1, state="disabled")
|
||||
target_menu = getattr(self, f'menu_vban_{i + 1}')
|
||||
target_menu.entryconfig(0, state='normal')
|
||||
target_menu.entryconfig(1, state='disabled')
|
||||
self.menu_layout.entryconfig(
|
||||
0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
|
||||
0, state=f'{"normal" if kind.name == "potato" else "disabled"}'
|
||||
)
|
||||
# ensure the configs are reloaded into memory
|
||||
if "config" in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__["config"]
|
||||
if "userconfigs" in self.parent.__dict__:
|
||||
del self.parent.__dict__["userconfigs"]
|
||||
if 'config' in self.parent.target.__dict__:
|
||||
del self.parent.target.__dict__['config']
|
||||
if 'userconfigs' in self.parent.__dict__:
|
||||
del self.parent.__dict__['userconfigs']
|
||||
self.menu_setup()
|
||||
|
||||
self.after(15000, self.enable_vban_menus)
|
||||
self.after(50, self.enable_vban_menus)
|
||||
|
||||
def documentation(self):
|
||||
webbrowser.open_new(r"https://voicemeeter.com/")
|
||||
webbrowser.open_new(r'https://voicemeeter.com/')
|
||||
|
||||
def github(self):
|
||||
webbrowser.open_new(r"https://github.com/onyx-and-iris/voicemeeter-compact")
|
||||
webbrowser.open_new(r'https://github.com/onyx-and-iris/voicemeeter-compact')
|
||||
|
||||
def onyxandiris(self):
|
||||
webbrowser.open_new(r"https://onyxandiris.online")
|
||||
webbrowser.open_new(r'https://onyxandiris.online')
|
||||
|
||||
@ -33,7 +33,7 @@ class Navigation(ttk.Frame):
|
||||
if self.submix.get():
|
||||
self.parent.submix_frame = SubMixFrame(self.parent)
|
||||
self.logger.info(
|
||||
f"Finished building submixframe for submix {_configuration.submixes}"
|
||||
f'Finished building submixframe for submix {_configuration.submixes}'
|
||||
)
|
||||
else:
|
||||
if _configuration.extends_horizontal:
|
||||
@ -49,51 +49,51 @@ class Navigation(ttk.Frame):
|
||||
else:
|
||||
self.parent.rowconfigure(2, weight=0, minsize=0)
|
||||
self.logger.info(
|
||||
f"Finished tearing down submixframe for submix {_configuration.submixes}"
|
||||
f'Finished tearing down submixframe for submix {_configuration.submixes}'
|
||||
)
|
||||
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"Submix.TButton",
|
||||
'Submix.TButton',
|
||||
background=f'{"purple" if self.submix.get() else "white"}',
|
||||
)
|
||||
|
||||
def switch_channel(self):
|
||||
if self.channel_text.get() == "STRIP":
|
||||
self.mainframebuilder.create_channelframe("bus")
|
||||
if self.channel_text.get() == 'STRIP':
|
||||
self.mainframebuilder.create_channelframe('bus')
|
||||
self.parent.strip_frame.teardown()
|
||||
else:
|
||||
self.mainframebuilder.create_channelframe("strip")
|
||||
self.mainframebuilder.create_channelframe('strip')
|
||||
self.parent.bus_frame.teardown()
|
||||
|
||||
self.extend_button["state"] = (
|
||||
"disabled" if self.channel_text.get() == "STRIP" else "normal"
|
||||
self.extend_button['state'] = (
|
||||
'disabled' if self.channel_text.get() == 'STRIP' else 'normal'
|
||||
)
|
||||
[frame.teardown() for frame in self.parent.configframes]
|
||||
self.channel_text.set("BUS" if self.channel_text.get() == "STRIP" else "STRIP")
|
||||
self.channel_text.set('BUS' if self.channel_text.get() == 'STRIP' else 'STRIP')
|
||||
|
||||
def extend_frame(self):
|
||||
_configuration.extended = self.extend.get()
|
||||
if self.extend.get():
|
||||
self.channel_button["state"] = "disabled"
|
||||
self.mainframebuilder.create_channelframe("bus")
|
||||
self.channel_button['state'] = 'disabled'
|
||||
self.mainframebuilder.create_channelframe('bus')
|
||||
else:
|
||||
[
|
||||
frame.teardown()
|
||||
for frame in self.parent.configframes
|
||||
if "!busconfig" in str(frame)
|
||||
if '!busconfig' in str(frame)
|
||||
]
|
||||
self.parent.bus_frame.teardown()
|
||||
self.parent.bus_frame = None
|
||||
self.channel_button["state"] = "normal"
|
||||
self.channel_button['state'] = 'normal'
|
||||
|
||||
if self.parent.submix_frame:
|
||||
self.parent.submix_frame.teardown()
|
||||
self.submix.set(False)
|
||||
if not _configuration.themes_enabled:
|
||||
self.styletable.configure(
|
||||
f"Submix.TButton",
|
||||
'Submix.TButton',
|
||||
background=f'{"purple" if self.submix.get() else "white"}',
|
||||
)
|
||||
|
||||
self.extend_text.set("REDUCE" if self.extend.get() else "EXTEND")
|
||||
self.extend_text.set('REDUCE' if self.extend.get() else 'EXTEND')
|
||||
|
||||
34
vmcompact/util.py
Normal file
34
vmcompact/util.py
Normal file
@ -0,0 +1,34 @@
|
||||
def get_busmode_fullnames(kind) -> dict:
|
||||
if kind.name == 'basic':
|
||||
return {
|
||||
'normal': 'Normal',
|
||||
'amix': 'Mix Down A',
|
||||
'repeat': 'Stereo Repeat',
|
||||
'composite': 'Composite',
|
||||
}
|
||||
return {
|
||||
'normal': 'Normal',
|
||||
'amix': 'Mix Down A',
|
||||
'bmix': 'Mix Down B',
|
||||
'repeat': 'Stereo Repeat',
|
||||
'composite': 'Composite',
|
||||
'tvmix': 'Up Mix TV',
|
||||
'upmix21': 'Up Mix 2.1',
|
||||
'upmix41': 'Up Mix 4.1',
|
||||
'upmix61': 'Up Mix 6.1',
|
||||
'centeronly': 'Center Only',
|
||||
'lfeonly': 'LFE Only',
|
||||
'rearonly': 'Rear Only',
|
||||
}
|
||||
|
||||
|
||||
def get_busmode_fullnames_reversed(kind) -> dict:
|
||||
return {v: k for k, v in get_busmode_fullnames(kind).items()}
|
||||
|
||||
|
||||
def get_busmode_shortnames(kind) -> list:
|
||||
return list(get_busmode_fullnames(kind).keys())
|
||||
|
||||
|
||||
def get_busmono_modes() -> list:
|
||||
return ['Mono: off', 'Mono: on', 'Stereo Reverse']
|
||||
Loading…
x
Reference in New Issue
Block a user