12 Commits

Author SHA1 Message Date
c9781ff92a bump to 0.5 2023-09-27 18:39:09 +01:00
bfccc323f1 slider mode commands split up
config.py added. loads custom user settings

_make_gestures moved into util.py
2023-09-27 18:38:13 +01:00
86dbe0b335 add Install section to README 2023-09-27 16:53:02 +01:00
82f32643d6 fix typo 2023-09-27 16:47:11 +01:00
51ccd76c2a adds version number to
announce_voicemeeter_version
2023-09-27 15:58:08 +01:00
149ed73605 swap Copy-Item for Robocopy 2023-09-27 14:46:37 +01:00
ecca4c65c8 add pyproject.toml 2023-09-27 14:16:52 +01:00
770a7742a2 add headers 2023-09-24 19:22:50 +01:00
2ca201af3a bump to 2023-09-24 19:20:01 +01:00
15a1747921 add Keybinds section to README 2023-09-24 19:16:32 +01:00
bac1fb09ec fix __name__ 2023-09-24 18:31:06 +01:00
f24ef8442e move vm_path and dll loading into cdll.py
define binds explicitly in Binds.

bump to version 0.3
2023-09-24 17:25:43 +01:00
14 changed files with 334 additions and 153 deletions

View File

@@ -1,8 +1,5 @@
{ {
"python.analysis.diagnosticSeverityOverrides": { "python.analysis.diagnosticSeverityOverrides": {
"reportMissingImports": "none" "reportMissingImports": "none"
}, }
"black-formatter.args": [
"--line-length=120"
]
} }

View File

@@ -1,3 +1,53 @@
# NVDA Addon Voicemeeter # NVDA Addon Voicemeeter
Control Voicemeeter GUI with customisable hotkeys. Control Voicemeeter with global hotkeys.
## Install
This addon can be installed through the Add-on store, `Install from external source`. Simply download the latest Release and load it with NVDA.
## Keybinds
### Controllers
- `NVDA+alt+s`: Enable strip mode
- `NVDA+alt+b`: Enable bus mode.
- `NVDA+alt+1`: Enable controller for channel 1 (strip|bus)
- `NVDA+alt+2`: Enable controller for channel 2 (strip|bus)
- `NVDA+alt+3`: Enable controller for channel 3 (strip|bus)
- `NVDA+alt+4`: Enable controller for channel 4 (strip|bus)
- `NVDA+alt+5`: Enable controller for channel 5 (strip|bus)
- `NVDA+alt+6`: Enable controller for channel 6 (strip|bus)
- `NVDA+alt+7`: Enable controller for channel 7 (strip|bus)
- `NVDA+alt+8`: Enable controller for channel 8 (strip|bus)
### Slider Modes
- `NVDA+alt+g`: Enable gain slider mode.
- `NVDA+alt+c`: Enable comp slider mode.
- `NVDA+alt+t`: Enable gate slider mode.
- `NVDA+alt+d`: Enable denoiser slider mode.
- `NVDA+alt+a`: Enable audibility slider mode.
### Sliders
- `NVDA+shift+upArrow`: Move slider up by 1 step
- `NVDA+shift+downArrow`: Move slider down by 1 step
- `NVDA+shift+alt+upArrow`: Move slider up by 0.1 step
- `NVDA+shift+alt+downArrow`: Move slider down by 0.1 step
- `NVDA+shift+control+upArrow`: Move slider up by 3 steps
- `NVDA+shift+control+downArrow`: Move slider down by 3 steps
### Channel Parameters
- `NVDA+shift+o`: Mono
- `NVDA+shift+s`: Solo
- `NVDA+shift+m`: Mute
- `NVDA+shift+c`: MC
- `NVDA+shift+k`: Karaoke
### Announcements
- `NVDA+shift+q`: Announce current controller.
- `NVDA+shift+a`: Announce Voicemeeter kind.

View File

@@ -1,63 +1,16 @@
import json
import time import time
from pathlib import Path
import globalPluginHandler import globalPluginHandler
from logHandler import log
from . import config, util
from .commands import CommandsMixin from .commands import CommandsMixin
from .controller import Controller from .controller import Controller
from .kinds import KindId, request_kind_map from .kinds import KindId, request_kind_map
def _make_gestures():
defaults = {
"kb:NVDA+alt+s": "strip_mode",
"kb:NVDA+alt+b": "bus_mode",
"kb:NVDA+alt+g": "slider_mode",
"kb:NVDA+alt+c": "slider_mode",
"kb:NVDA+alt+t": "slider_mode",
"kb:NVDA+alt+d": "slider_mode",
"kb:NVDA+alt+a": "slider_mode",
"kb:NVDA+shift+q": "announce_controller",
"kb:NVDA+shift+a": "announce_voicemeeter_version",
"kb:NVDA+shift+o": "toggle_mono",
"kb:NVDA+shift+s": "toggle_solo",
"kb:NVDA+shift+m": "toggle_mute",
"kb:NVDA+shift+c": "toggle_mc",
"kb:NVDA+shift+k": "karaoke",
"kb:NVDA+shift+upArrow": "slider_increase",
"kb:NVDA+shift+downArrow": "slider_decrease",
"kb:NVDA+shift+alt+upArrow": "slider_increase",
"kb:NVDA+shift+alt+downArrow": "slider_decrease",
"kb:NVDA+shift+control+upArrow": "slider_increase",
"kb:NVDA+shift+control+downArrow": "slider_decrease",
}
overrides = None
pn = Path.home() / "Documents" / "Voicemeeter" / "keybinds.json"
if pn.exists():
with open(pn, "r") as f:
data = json.load(f)
overrides = {f"kb:{v}": k for k, v in data.items()}
log.info("INFO - loading settings from keybinds.json")
if overrides:
return {**defaults, **overrides}
return defaults
def _get_kind_id():
pn = Path.home() / "Documents" / "Voicemeeter" / "settings.json"
if pn.exists():
with open(pn, "r") as f:
data = json.load(f)
return data["voicemeeter"]
return "potato"
class GlobalPlugin(globalPluginHandler.GlobalPlugin, CommandsMixin): class GlobalPlugin(globalPluginHandler.GlobalPlugin, CommandsMixin):
__gestures = _make_gestures() __kind_id = config.get("voicemeeter", "potato")
__kind_id = _get_kind_id() __gestures = util._make_gestures(__kind_id)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -65,12 +18,7 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin, CommandsMixin):
if self.controller.login() == 1: if self.controller.login() == 1:
self.controller.run_voicemeeter(KindId[self.__kind_id.upper()]) self.controller.run_voicemeeter(KindId[self.__kind_id.upper()])
time.sleep(1) time.sleep(1)
self.kind = request_kind_map(self.controller.kind_id) self.kind = request_kind_map(self.__kind_id)
for i in range(1, self.kind.num_strip + 1):
self.bindGesture(f"kb:NVDA+alt+{i}", "index")
for i in range(1, self.kind.phys_out + self.kind.virt_out + 1):
self.bindGesture(f"kb:NVDA+shift+{i}", "bus_assignment")
def terminate(self, *args, **kwargs): def terminate(self, *args, **kwargs):
super().terminate(*args, **kwargs) super().terminate(*args, **kwargs)

View File

@@ -1,51 +1,45 @@
import ctypes as ct import ctypes as ct
import winreg from ctypes.wintypes import CHAR, FLOAT, LONG
from pathlib import Path
from .error import VMError from .cdll import libc
from .error import VMCAPIError
class Binds: class Binds:
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}" bind_login = libc.VBVMR_Login
BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32 bind_login.restype = LONG
bind_login.argtypes = None
def __init__(self): bind_logout = libc.VBVMR_Logout
dll_path = Path(self.__vmpath()).parent.joinpath( bind_logout.restype = LONG
f'VoicemeeterRemote{"64" if self.BITS == 64 else ""}.dll' bind_logout.argtypes = None
)
if self.BITS == 64:
self.libc = ct.CDLL(str(dll_path))
else:
self.libc = ct.WinDLL(str(dll_path))
def __vmpath(self): bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter
with winreg.OpenKey( bind_run_voicemeeter.restype = LONG
winreg.HKEY_LOCAL_MACHINE, bind_run_voicemeeter.argtypes = [LONG]
r"{}".format(
"\\".join( bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType
( bind_get_voicemeeter_type.restype = LONG
"\\".join( bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)]
filter(
None, bind_get_voicemeeter_version = libc.VBVMR_GetVoicemeeterVersion
( bind_get_voicemeeter_version.restype = LONG
"SOFTWARE", bind_get_voicemeeter_version.argtypes = [ct.POINTER(LONG)]
"WOW6432Node" if self.BITS == 64 else "",
"Microsoft", bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty
"Windows", bind_is_parameters_dirty.restype = LONG
"CurrentVersion", bind_is_parameters_dirty.argtypes = None
"Uninstall",
), bind_get_parameter_float = libc.VBVMR_GetParameterFloat
) bind_get_parameter_float.restype = LONG
), bind_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
self.VM_KEY,
) bind_set_parameter_float = libc.VBVMR_SetParameterFloat
) bind_set_parameter_float.restype = LONG
), bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
def call(self, fn, *args, ok=(0,)): def call(self, fn, *args, ok=(0,)):
retval = getattr(self.libc, fn)(*args) retval = fn(*args)
if retval not in ok: if retval not in ok:
raise VMError(f"{fn} returned {retval}") raise VMCAPIError(fn.__name__, retval)
return retval return retval

View File

@@ -0,0 +1,49 @@
import ctypes as ct
import platform
import winreg
from pathlib import Path
from .error import VMError
BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
if platform.system() != "Windows":
raise VMError("Only Windows OS supported")
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
REG_KEY = "\\".join(
filter(
None,
(
"SOFTWARE",
"WOW6432Node" if BITS == 64 else "",
"Microsoft",
"Windows",
"CurrentVersion",
"Uninstall",
),
)
)
def get_vmpath():
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"{}".format("\\".join((REG_KEY, VM_KEY)))) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
try:
vm_parent = Path(get_vmpath()).parent
except FileNotFoundError as e:
raise VMError("Unable to fetch DLL path from the registry") from e
DLL_NAME = f'VoicemeeterRemote{"64" if BITS == 64 else ""}.dll'
dll_path = vm_parent.joinpath(DLL_NAME)
if not dll_path.is_file():
raise VMError(f"Could not find {dll_path}")
if BITS == 64:
libc = ct.CDLL(str(dll_path))
else:
libc = ct.WinDLL(str(dll_path))

View File

@@ -1,14 +1,14 @@
import ui import ui
from logHandler import log from logHandler import log
from . import context, util from . import context
class CommandsMixin: class CommandsMixin:
### ANNOUNCEMENTS ### ### ANNOUNCEMENTS ###
def script_announce_voicemeeter_version(self, _): def script_announce_voicemeeter_version(self, _):
ui.message(f"Running Voicemeeter {self.kind}") ui.message(f"Running Voicemeeter {self.kind} {self.controller.version}")
def script_announce_controller(self, _): def script_announce_controller(self, _):
ui.message(f"Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}") ui.message(f"Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}")
@@ -37,18 +37,24 @@ class CommandsMixin:
ui.message(f"Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}") ui.message(f"Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}")
log.info(f"INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode") log.info(f"INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode")
def script_slider_mode(self, gesture): def __set_slider_mode(self, mode):
if gesture.displayName.endswith("g"): self.controller.ctx.slider_mode = mode
self.controller.ctx.slider_mode = "gain" ui.message(f"{mode} mode enabled")
elif gesture.displayName.endswith("c"):
self.controller.ctx.slider_mode = "comp" def script_gain_mode(self, _):
elif gesture.displayName.endswith("t"): self.__set_slider_mode("gain")
self.controller.ctx.slider_mode = "gate"
elif gesture.displayName.endswith("d"): def script_comp_mode(self, _):
self.controller.ctx.slider_mode = "denoiser" self.__set_slider_mode("comp")
elif gesture.displayName.endswith("a"):
self.controller.ctx.slider_mode = "audibility" def script_gate_mode(self, _):
ui.message(f"{self.controller.ctx.slider_mode} mode enabled") self.__set_slider_mode("gate")
def script_denoiser_mode(self, _):
self.__set_slider_mode("denoiser")
def script_audibility_mode(self, _):
self.__set_slider_mode("audibility")
### BOOLEAN PARAMETERS ### ### BOOLEAN PARAMETERS ###
@@ -89,28 +95,34 @@ class CommandsMixin:
self.controller.ctx.set_bool(output, val) self.controller.ctx.set_bool(output, val)
ui.message("on" if val else "off") ui.message("on" if val else "off")
### SLIDER MODES ### ### CONTROL SLIDERS ###
def script_slider_increase(self, gesture): def script_slider_increase_by_point_one(self, gesture):
op = util.remove_prefix(gesture.displayName, "kb:NVDA+shift+") val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 0.1
if op.startswith("alt"):
offset = 0.1
elif op.startswith("ctrl"):
offset = 3
else:
offset = 1
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + offset
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))
def script_slider_decrease(self, gesture): def script_slider_decrease_by_point_one(self, gesture):
op = util.remove_prefix(gesture.displayName, "kb:NVDA+shift+") val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 0.1
if op.startswith("alt"): self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
offset = 0.1 ui.message(str(round(val, 1)))
elif op.startswith("ctrl"):
offset = 3 def script_slider_increase_by_one(self, gesture):
else: val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 1
offset = 1 self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - offset ui.message(str(round(val, 1)))
def script_slider_decrease_by_one(self, gesture):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_increase_by_three(self, gesture):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 3
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_decrease_by_three(self, gesture):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 3
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))

View File

@@ -0,0 +1,20 @@
import json
from pathlib import Path
def config_from_json():
pn = Path.home() / "Documents" / "Voicemeeter" / "nvda_settings.json"
data = None
if pn.exists():
with open(pn, "r") as f:
data = json.load(f)
return data or {}
__config = config_from_json()
def get(name, default=None):
if name in __config:
return __config[name]
return default

View File

@@ -8,12 +8,8 @@ class Strategy(ABC):
self._slider_mode = "gain" self._slider_mode = "gain"
@abstractmethod @abstractmethod
def __str__(self):
pass
@property
def identifier(self): def identifier(self):
return f"{self}[{self._index}]" pass
@property @property
def index(self): def index(self):
@@ -54,11 +50,19 @@ class StripStrategy(Strategy):
def __str__(self): def __str__(self):
return "Strip" return "Strip"
@property
def identifier(self):
return f"{self}[{self._index}]"
class BusStrategy(Strategy): class BusStrategy(Strategy):
def __str__(self): def __str__(self):
return "Bus" return "Bus"
@property
def identifier(self):
return f"{self}[{self._index}]"
class Context: class Context:
def __init__(self, strategy: Strategy) -> None: def __init__(self, strategy: Strategy) -> None:

View File

@@ -3,45 +3,56 @@ import ctypes as ct
from logHandler import log from logHandler import log
from .binds import Binds from .binds import Binds
from .cdll import BITS
from .context import Context, StripStrategy from .context import Context, StripStrategy
from .kinds import KindId from .kinds import KindId
class Controller: class Controller(Binds):
def __init__(self): def __init__(self):
self.binds = Binds()
self.ctx = Context(StripStrategy(self, 0)) self.ctx = Context(StripStrategy(self, 0))
def login(self): def login(self):
retval = self.binds.call("VBVMR_Login", ok=(0, 1)) retval = self.call(self.bind_login, ok=(0, 1))
log.info("INFO - logged into Voicemeeter Remote API") log.info("INFO - logged into Voicemeeter Remote API")
return retval return retval
def logout(self): def logout(self):
self.binds.call("VBVMR_Logout") self.call(self.bind_logout)
log.info("NFO - logged out of Voicemeeter Remote API") log.info("NFO - logged out of Voicemeeter Remote API")
@property @property
def kind_id(self): def kind_id(self):
c_type = ct.c_long() c_type = ct.c_long()
self.binds.call("VBVMR_GetVoicemeeterType", ct.byref(c_type)) self.call(self.bind_get_voicemeeter_type, ct.byref(c_type))
return KindId(c_type.value).name.lower() return KindId(c_type.value).name.lower()
@property
def version(self):
ver = ct.c_long()
self.call(self.bind_get_voicemeeter_version, ct.byref(ver))
return "{}.{}.{}.{}".format(
(ver.value & 0xFF000000) >> 24,
(ver.value & 0x00FF0000) >> 16,
(ver.value & 0x0000FF00) >> 8,
ver.value & 0x000000FF,
)
def run_voicemeeter(self, kind_id): def run_voicemeeter(self, kind_id):
val = kind_id.value val = kind_id.value
if val == 3 and Binds.BITS == 64: if val == 3 and BITS == 64:
val = 6 val = 6
self.binds.call("VBVMR_RunVoicemeeter", val) self.call(self.bind_run_voicemeeter, val)
def __clear(self): def __clear(self):
while self.binds.call("VBVMR_IsParametersDirty", ok=(0, 1)) == 1: while self.call(self.bind_is_parameters_dirty, ok=(0, 1)) == 1:
pass pass
def _get(self, param): def _get(self, param):
self.__clear() self.__clear()
buf = ct.c_float() buf = ct.c_float()
self.binds.call("VBVMR_GetParameterFloat", param.encode(), ct.byref(buf)) self.call(self.bind_get_parameter_float, param.encode(), ct.byref(buf))
return buf.value return buf.value
def _set(self, param, val): def _set(self, param, val):
self.binds.call("VBVMR_SetParameterFloat", param.encode(), ct.c_float(float(val))) self.call(self.bind_set_parameter_float, param.encode(), ct.c_float(float(val)))

View File

@@ -1,2 +1,11 @@
class VMError(Exception): class VMError(Exception):
"""Base VMError class""" """Base voicemeeterlib exception class"""
class VMCAPIError(VMError):
"""Exception raised when the C-API returns an error code"""
def __init__(self, fn_name, code):
self.fn_name = fn_name
self.code = code
super().__init__(f"{self.fn_name} returned {self.code}")

View File

@@ -1,3 +1,7 @@
from . import config, util
from .kinds import request_kind_map
def remove_prefix(input_string, prefix): def remove_prefix(input_string, prefix):
if prefix and input_string.startswith(prefix): if prefix and input_string.startswith(prefix):
return input_string[len(prefix) :] return input_string[len(prefix) :]
@@ -8,3 +12,40 @@ def remove_suffix(input_string, suffix):
if suffix and input_string.endswith(suffix): if suffix and input_string.endswith(suffix):
return input_string[: -len(suffix)] return input_string[: -len(suffix)]
return input_string return input_string
def _make_gestures(kind_id):
kind = request_kind_map(kind_id)
defaults = {
"kb:NVDA+alt+s": "strip_mode",
"kb:NVDA+alt+b": "bus_mode",
"kb:NVDA+alt+g": "gain_mode",
"kb:NVDA+alt+c": "comp_mode",
"kb:NVDA+alt+t": "gate_mode",
"kb:NVDA+alt+d": "denoiser_mode",
"kb:NVDA+alt+a": "audibility_mode",
"kb:NVDA+shift+q": "announce_controller",
"kb:NVDA+shift+v": "announce_voicemeeter_version",
"kb:NVDA+shift+o": "toggle_mono",
"kb:NVDA+shift+s": "toggle_solo",
"kb:NVDA+shift+m": "toggle_mute",
"kb:NVDA+shift+c": "toggle_mc",
"kb:NVDA+shift+k": "karaoke",
"kb:NVDA+shift+upArrow": "slider_increase_by_point_one",
"kb:NVDA+shift+downArrow": "slider_decrease_by_point_one",
"kb:NVDA+shift+alt+upArrow": "slider_increase_by_one",
"kb:NVDA+shift+alt+downArrow": "slider_decrease_by_one",
"kb:NVDA+shift+control+upArrow": "slider_increase_by_three",
"kb:NVDA+shift+control+downArrow": "slider_decrease_by_three",
}
for i in range(1, kind.num_strip + 1):
defaults[f"kb:NVDA+alt+{i}"] = "index"
for i in range(1, kind.phys_out + kind.virt_out + 1):
defaults[f"kb:NVDA+alt+{i}"] = "bus_assignment"
abc = config.get("keybinds")
if abc:
overrides = {f"kb:{util.remove_prefix(k, 'kb:')}": v for k, v in abc.items()}
matching_values = set(defaults.values()).intersection(set(overrides.values()))
defaults = {k: v for k, v in defaults.items() if v not in matching_values}
return {**defaults, **overrides}
return defaults

View File

@@ -4,8 +4,8 @@ param(
function Copy-FilestoScratchpad { function Copy-FilestoScratchpad {
$source = Join-Path $PSScriptRoot "addon" "globalPlugins" "voicemeeter" $source = Join-Path $PSScriptRoot "addon" "globalPlugins" "voicemeeter"
$target = Join-Path $env:appdata "nvda" "scratchpad" "globalPlugins" $target = Join-Path $env:appdata "nvda" "scratchpad" "globalPlugins" "voicemeeter"
Copy-Item -Path $source -Destination $target -Recurse -Force Robocopy $source $target | Out-Null
} }
function main { function main {

View File

@@ -28,7 +28,7 @@ addon_info = {
The add-on requires Voicemeeter to be installed.""" The add-on requires Voicemeeter to be installed."""
), ),
# version # version
"addon_version": "0.2", "addon_version": "0.5",
# Author(s) # Author(s)
"addon_author": "onyx-and-iris <code@onyxandiris.online>", "addon_author": "onyx-and-iris <code@onyxandiris.online>",
# URL for the add-on documentation support # URL for the add-on documentation support

46
pyproject.toml Normal file
View File

@@ -0,0 +1,46 @@
[tool.black]
line-length = 119
[tool.ruff]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F"]
# Avoid enforcing line-length violations (`E501`). Let Black deal with this.
ignore = ["E501"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
unfixable = []
# Exclude a variety of commonly ignored directories.
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 = 119
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.7
target-version = "py37"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10
[tool.ruff.per-file-ignores]
"__init__.py" = ["E402", "F401"] # Ignore unused import and variable not accessed violations