initial commit

This commit is contained in:
onyx-and-iris 2023-08-22 02:04:00 +01:00
parent 7c0f3327f8
commit e200462b85
13 changed files with 222 additions and 0 deletions

4
.gitignore vendored
View File

@ -108,6 +108,8 @@ ipython_config.py
# in version control. # in version control.
# https://pdm.fming.dev/#use-with-ide # https://pdm.fming.dev/#use-with-ide
.pdm.toml .pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
@ -158,3 +160,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
controllerClient/

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"black-formatter.args": [
"--line-length=120"
]
}

1
README.md Normal file
View File

@ -0,0 +1 @@
# example-package

9
__main__.py Normal file
View File

@ -0,0 +1,9 @@
import voicemeeterlib
import nvda_voicemeeter
kind_id = "potato"
with voicemeeterlib.api("potato") as vm:
with nvda_voicemeeter.build(f"Voicemeeter {kind_id.capitalize()} NVDA", vm) as window:
window.run()

51
pdm.lock Normal file
View File

@ -0,0 +1,51 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default"]
cross_platform = true
static_urls = false
lock_version = "4.3"
content_hash = "sha256:0dfd1ea07c294dd2b837a34ff9d286134e5119c6249e4f9cd6c1ed121de97851"
[[package]]
name = "pyparsing"
version = "3.1.1"
requires_python = ">=3.6.8"
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
files = [
{file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
{file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
]
[[package]]
name = "pysimplegui"
version = "4.60.5"
summary = "Python GUIs for Humans. Launched in 2018. It's 2022 & PySimpleGUI is an ACTIVE & supported project. Super-simple to create custom GUI's. 325+ Demo programs & Cookbook for rapid start. Extensive documentation. Main docs at www.PySimpleGUI.org. Fun & your success are the focus. Examples using Machine Learning (GUI, OpenCV Integration), Rainmeter Style Desktop Widgets, Matplotlib + Pyplot, PIL support, add GUI to command line scripts, PDF & Image Viewers. Great for beginners & advanced GUI programmers."
files = [
{file = "PySimpleGUI-4.60.5-py3-none-any.whl", hash = "sha256:004f33311ee685a5287fad54f500f7290b40d7c806044e478b1384f85dedce64"},
{file = "PySimpleGUI-4.60.5.tar.gz", hash = "sha256:31014d1cc5eef1373d7e93564ff2604662645cc774a939b1f01aa253e7f9d78b"},
]
[[package]]
name = "tomli"
version = "2.0.1"
requires_python = ">=3.7"
summary = "A lil' TOML parser"
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 = "voicemeeter-api"
version = "2.4.8"
requires_python = ">=3.10,<4.0"
summary = "A Python wrapper for the Voiceemeter API"
dependencies = [
"tomli<3.0.0,>=2.0.1; python_version < \"3.11\"",
]
files = [
{file = "voicemeeter_api-2.4.8-py3-none-any.whl", hash = "sha256:a3ff9e6f7516d2adedde93f6976526b385d8ae6d86598bfe541a44f498f42ea6"},
{file = "voicemeeter_api-2.4.8.tar.gz", hash = "sha256:0d37a9f2af0f68087aa9c76a8cfb2ba44c6b75bb344e8dfa9fd18ad2c862730c"},
]

16
pyproject.toml Normal file
View File

@ -0,0 +1,16 @@
[project]
name = "nvda_voicemeeter"
version = "0.1.0"
description = "A Voicemeeter app compatible with NVDA"
authors = [
{name = "onyx-and-iris", email = "code@onyxandiris.online"},
]
dependencies = [
"pysimplegui>=4.60.5",
"pyparsing>=3.1.1",
"voicemeeter-api>=2.4.8",
]
requires-python = ">=3.10"
readme = "README.md"
license = {text = "MIT"}

View File

@ -0,0 +1 @@
from .window import request_window_object as build

View File

@ -0,0 +1,10 @@
import ctypes as ct
from pathlib import Path
bits = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
controller_path = Path(__file__).parents[2].resolve() / "controllerClient"
DLL_PATH = controller_path / f"x{64 if bits == 64 else 86}" / f"nvdaControllerClient{bits}.dll"
libc = ct.CDLL(str(DLL_PATH))

View File

@ -0,0 +1,26 @@
def _make_cache(vm) -> dict:
match vm.kind.name:
case "basic":
return {
**{f"BUTTON||strip {i} A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
}
case "banana":
return {
**{f"BUTTON||strip {i} A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} A2": vm.strip[i].A2 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} A3": vm.strip[i].A3 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} B2": vm.strip[i].B2 for i in range(vm.kind.num_strip)},
}
case "potato":
return {
**{f"BUTTON||strip {i} A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} A2": vm.strip[i].A2 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} A3": vm.strip[i].A3 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} A4": vm.strip[i].A4 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} A5": vm.strip[i].A5 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} B2": vm.strip[i].B2 for i in range(vm.kind.num_strip)},
**{f"BUTTON||strip {i} B3": vm.strip[i].B3 for i in range(vm.kind.num_strip)},
}

View File

@ -0,0 +1,29 @@
from .cdll import libc
class CBindings:
bind_test_if_running = libc.nvdaController_testIfRunning
bind_speak_text = libc.nvdaController_speakText
bind_cancel_speech = libc.nvdaController_cancelSpeech
bind_braille_message = libc.nvdaController_brailleMessage
def call(self, fn, *args, ok=(0,)):
retval = fn(*args)
if retval not in ok:
raise RuntimeError(f"{fn.__name__} returned {retval}")
return retval
class Nvda(CBindings):
@property
def is_running(self):
return self.call(self.bind_test_if_running, ok=(0, 1)) == 0
def speak(self, text):
self.call(self.bind_speak_text, text)
def cancel_speech(self):
self.call(self.bind_cancel_speech)
def braille_message(self, text):
self.call(self.bind_braille_message, text)

View File

@ -0,0 +1,10 @@
from pyparsing import Group, OneOrMore, Optional, Suppress, Word, alphanums
class Parser:
def __init__(self):
self.widget = Group(OneOrMore(Word(alphanums)))
self.token = Suppress("||")
self.identifier = Group(OneOrMore(Word(alphanums)))
self.event = OneOrMore(Word(alphanums))
self.match = self.widget + self.token + self.identifier + Optional(self.token) + Optional(self.event)

View File

@ -0,0 +1,60 @@
import PySimpleGUI as psg
from .models import _make_cache
from .nvda import Nvda
from .parser import Parser
class Window(psg.Window):
def __init__(self, title, vm):
self.vm = vm
self.kind = self.vm.kind
super().__init__(title, self.make_layout(), finalize=True)
self.cache = _make_cache(self.vm)
self.nvda = Nvda()
self.parser = Parser()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def make_layout(self) -> list:
"""Builds the window layout step by step"""
def add_physical_device_opts(layout):
devices = ["{type}: {name}".format(**self.vm.device.output(i)) for i in range(self.vm.device.outs)]
layout.append(
[
psg.Combo(
devices,
size=(22, 4),
expand_x=True,
enable_events=True,
key=f"DEVICE LIST||PHYSOUT {i}",
)
for i in range(self.kind.phys_out)
]
)
upper_layout = list()
[step(upper_layout) for step in (add_physical_device_opts,)]
row0 = psg.Frame("Hardware Out", upper_layout)
return [[row0]]
def run(self):
"""Runs the main window until an Close/Exit event"""
while True:
event, values = self.read()
if event in (psg.WIN_CLOSED, "Exit"):
break
match self.parser.match.parseString(event):
case _:
pass
def request_window_object(title, vm):
WINDOW_cls = Window
return WINDOW_cls(title, vm)

0
tests/__init__.py Normal file
View File