Compare commits

...

4 Commits

Author SHA1 Message Date
Mel
2aea670370 update gitignore 2025-12-06 04:30:53 +01:00
Mel
d5292f7e45 add uv.lock 2025-12-06 04:30:21 +01:00
Mel
193e99d955 add main logic for everything I need 2025-12-06 04:30:11 +01:00
Mel
5dd435dcbf set build param 2025-12-06 04:29:27 +01:00
6 changed files with 348 additions and 255 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
.envrc .envrc
.python-version .python-version
.idea/ .idea/
dist

220
main.py
View File

@@ -1,220 +0,0 @@
import json
import mido
import subprocess
nanoKontrol2_mapping: dict[int, str] = {
0: "Slider_1",
1: "Slider_2",
2: "Slider_3",
3: "Slider_4",
4: "Slider_5",
5: "Slider_6",
6: "Slider_7",
7: "Slider_8",
16: "Knob_1",
17: "Knob_2",
18: "Knob_3",
19: "Knob_4",
20: "Knob_5",
21: "Knob_6",
22: "Knob_7",
23: "Knob_8",
32: "Solo_1",
33: "Solo_2",
34: "Solo_3",
35: "Solo_4",
36: "Solo_5",
37: "Solo_6",
38: "Solo_7",
39: "Solo_8",
41: "Play",
42: "Stop",
43: "Prev",
44: "Next",
45: "Record",
46: "Cycle",
48: "Mute_1",
49: "Mute_2",
50: "Mute_3",
51: "Mute_4",
52: "Mute_5",
53: "Mute_6",
54: "Mute_7",
55: "Mute_8",
58: "Track_prev",
59: "Track_next",
60: "Marker_set",
61: "Marker_prev",
62: "Marker_next",
64: "Record_1",
65: "Record_2",
66: "Record_3",
67: "Record_4",
68: "Record_5",
69: "Record_6",
70: "Record_7",
71: "Record_8",
}
audiomixer_mapping = {
"Slider_1": "mic1.output",
"Solo_1": "mic1.output",
"Mute_1": "mic1.output",
"Record_1": "mic1.output",
"Slider_2": "mic2.output",
"Solo_2": "mic2.output",
"Mute_2": "mic2.output",
"Record_2": "mic2.output",
"Slider_3": "system.output",
"Solo_3": "system.output",
"Mute_3": "system.output",
"Record_3": "system.output",
"Slider_4": "discord.output",
"Solo_4": "discord.output",
"Mute_4": "discord.output",
"Record_4": "discord.output",
"Slider_5": "music.output",
"Solo_5": "music.output",
"Mute_5": "music.output",
"Record_5": "music.output",
"Slider_6": "music.output",
"Solo_6": "music.output",
"Mute_6": "music.output",
"Record_6": "music.output",
"Slider_7": "music.output",
"Solo_7": "music.output",
"Mute_7": "music.output",
"Record_7": "music.output",
"Slider_8": "browser.output",
"Solo_8": "browser.output",
"Mute_8": "browser.output",
"Record_8": "browser.output",
"Knob_1": "alsa_output.usb-Focusrite_Scarlett_2i2_USB_Y8T4J6E095585D-00.Direct__Direct__sink",
"Knob_2": "alsa_output.pci-0000_10_00.4.analog-stereo",
"Knob_3": "obs_system.output",
"Knob_4": "obs_discord.output",
"Knob_5": "obs_music.output",
"Knob_8": "obs_browser.output",
"Prev": "XF86Previous",
"Next": "XF86Next",
}
sliders = [
"Slider_1",
"Slider_2",
"Slider_3",
"Slider_4",
"Slider_5",
"Slider_6",
"Slider_7",
"Slider_8",
"Knob_1",
"Knob_2",
"Knob_3",
"Knob_4",
"Knob_5",
"Knob_6",
"Knob_7",
"Knob_8",
]
buttons = [
"Solo_1",
"Mute_1",
"Record_1",
"Solo_2",
"Mute_2",
"Record_2",
"Solo_3",
"Mute_3",
"Record_3",
"Solo_4",
"Mute_4",
"Record_4",
"Solo_5",
"Mute_5",
"Record_5",
"Solo_6",
"Mute_6",
"Record_6",
"Solo_7",
"Mute_7",
"Record_7",
"Solo_8",
"Mute_8",
"Record_8",
]
pw_nodes = []
pw_json = subprocess.run("pw-dump", capture_output=True, text=True)
pw_data = json.loads(pw_json.stdout)
for node in pw_data:
if node["type"] == "PipeWire:Interface:Node":
pw_nodes.append(node)
def find_id(device: str) -> str:
device_id: str = ""
for pw_node in pw_nodes:
if pw_node["info"]["props"]["node.name"] == device:
device_id = str(pw_node["id"])
# else:
# raise NotImplementedError
return device_id
def set_mute(toggle: bool, device: str) -> None:
device_id = find_id(device)
if toggle:
subprocess.Popen(["/usr/bin/wpctl", "set-mute", device_id, "1"])
else:
subprocess.Popen(["/usr/bin/wpctl", "set-mute", device_id, "0"])
def set_volume(volume: int, device: str) -> None:
device_id = find_id(device)
if volume > 0:
volume = round((volume / 127), 2)
subprocess.Popen(["/usr/bin/wpctl", "set-volume", device_id, str(volume)])
def main():
# TODO threading for MIDI values;
with mido.open_input("nanoKONTROL2:nanoKONTROL2 _ CTRL 28:0") as mixer:
for update in mixer:
midi_input = nanoKontrol2_mapping[update.control]
if midi_input == "Record":
if update.value == 127:
set_mute(toggle=True, device=audiomixer_mapping["Knob_2"])
else:
set_mute(toggle=False, device=audiomixer_mapping["Knob_2"])
if midi_input in sliders:
set_volume(
volume=update.value,
device=audiomixer_mapping[nanoKontrol2_mapping[update.control]],
)
if midi_input in buttons:
if midi_input.startswith("Mute") and update.value == 127:
set_mute(toggle=True, device=audiomixer_mapping[midi_input])
elif midi_input.startswith("Mute") and update.value == 0:
set_mute(toggle=False, device=audiomixer_mapping[midi_input])
# TODO parsing return to dict to split inputs
# TODO define mixers in config files
# TODO pipewire integration
# TODO user config for audio streams
# TODO running as a daemon
# TODO systemctl unit file
# TODO evaluating update time of script. 1ms? 0.1ms?
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,10 @@
[project] [project]
name = "linux-audiomixer" name = "midimixer"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Software to control pipewire via MIDI"
authors = [
{ name="Melody LaFae", email="melafae@basicchaos.de" }
]
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
@@ -10,3 +13,11 @@ dependencies = [
"python-rtmidi>=1.5.8", "python-rtmidi>=1.5.8",
"ruff>=0.14.0", "ruff>=0.14.0",
] ]
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: Linux",
]
[build-system]
requires = ["uv_build >= 0.9.5, <0.10.0"]
build-backend = "uv_build"

View File

301
src/midimixer/main.py Executable file
View File

@@ -0,0 +1,301 @@
#! /usr/bin/env python3
import json
import mido
import subprocess
nanoKontrol2_mapping: dict[int, str] = {
0: "Slider_1",
1: "Slider_2",
2: "Slider_3",
3: "Slider_4",
4: "Slider_5",
5: "Slider_6",
6: "Slider_7",
7: "Slider_8",
16: "Knob_1",
17: "Knob_2",
18: "Knob_3",
19: "Knob_4",
20: "Knob_5",
21: "Knob_6",
22: "Knob_7",
23: "Knob_8",
32: "Solo_1",
33: "Solo_2",
34: "Solo_3",
35: "Solo_4",
36: "Solo_5",
37: "Solo_6",
38: "Solo_7",
39: "Solo_8",
41: "Play",
42: "Stop",
43: "Prev",
44: "Next",
45: "Record",
46: "Cycle",
48: "Mute_1",
49: "Mute_2",
50: "Mute_3",
51: "Mute_4",
52: "Mute_5",
53: "Mute_6",
54: "Mute_7",
55: "Mute_8",
58: "Track_prev",
59: "Track_next",
60: "Marker_set",
61: "Marker_prev",
62: "Marker_next",
64: "Record_1",
65: "Record_2",
66: "Record_3",
67: "Record_4",
68: "Record_5",
69: "Record_6",
70: "Record_7",
71: "Record_8",
}
audiomixer_mapping = {
"Slider_1": "mic1.input",
"Solo_1": "mic1.input",
"Mute_1": "mic1.input",
"Record_1": "obs_mic1.input",
"Slider_2": "mic2.input",
"Solo_2": "mic2.input",
"Mute_2": "mic2.input",
# "Record_2": "mic2.input",
"Slider_3": "system.input",
"Solo_3": "system.input",
"Mute_3": "system.input",
"Record_3": "obs_game.input",
"Slider_4": "discord.input",
"Solo_4": "discord.input",
"Mute_4": "discord.input",
"Record_4": "obs_discord.input",
"Slider_5": "music.input",
"Solo_5": "music.input",
"Mute_5": "music.input",
"Record_5": "obs_music.input",
"Slider_6": "obs.input",
"Solo_6": "obs.input",
"Mute_6": "obs.input",
# "Record_6": "obs.input",
"Slider_7": "music.input",
"Solo_7": "music.input",
"Mute_7": "music.input",
# "Record_7": "music.input",
"Slider_8": "browser.input",
"Solo_8": "browser.input",
"Mute_8": "browser.input",
"Record_8": "obs_browser.input",
"Knob_1": "alsa_output.usb-Focusrite_Scarlett_2i2_USB_Y8T4J6E095585D-00.Direct__Direct__sink",
"Knob_2": "alsa_output.pci-0000_7d_00.6.analog-stereo",
"Knob_3": "obs_game.input",
"Knob_4": "obs_discord.input",
"Knob_5": "obs_music.input",
"Knob_8": "obs_browser.input",
"Prev": "XF86AudioPrev",
"Next": "XF86AudioNext",
"Play": "XF86AudioPlay",
"Stop": "XF86AudioStop",
"Record": "alsa_output.pci-0000_7d_00.6.analog-stereo",
"Marker_set": "vts ctrl+v+2",
"Marker_prev": "vts ctrl+v+3",
"Cycle": "vts ctrl+v+0",
}
sliders = [
"Slider_1",
"Slider_2",
"Slider_3",
"Slider_4",
"Slider_5",
"Slider_6",
"Slider_7",
"Slider_8",
"Knob_1",
"Knob_2",
"Knob_3",
"Knob_4",
"Knob_5",
"Knob_6",
"Knob_7",
"Knob_8",
]
buttons = [
"Solo_1",
"Mute_1",
"Record_1",
"Solo_2",
"Mute_2",
"Record_2",
"Solo_3",
"Mute_3",
"Record_3",
"Solo_4",
"Mute_4",
"Record_4",
"Solo_5",
"Mute_5",
"Record_5",
"Solo_6",
"Mute_6",
"Record_6",
"Solo_7",
"Mute_7",
"Record_7",
"Solo_8",
"Mute_8",
"Record_8",
]
pw_nodes = []
pw_json = subprocess.run("pw-dump", capture_output=True, text=True)
pw_data = json.loads(pw_json.stdout)
for node in pw_data:
if node["type"] == "PipeWire:Interface:Node":
pw_nodes.append(node)
def find_id(device: str) -> str:
device_id: str = ""
for pw_node in pw_nodes:
if pw_node["info"]["props"]["node.name"] == device:
device_id = str(pw_node["id"])
# else:
# raise NotImplementedError
return device_id
def set_mute(toggle: bool, device: str) -> None:
device_id = find_id(device)
if toggle:
subprocess.Popen(["/usr/bin/wpctl", "set-mute", device_id, "1"])
else:
subprocess.Popen(["/usr/bin/wpctl", "set-mute", device_id, "0"])
def set_volume(volume: int, device: str) -> None:
device_id = find_id(device)
if volume > 0:
volume = round((volume / 127), 2)
subprocess.Popen(["/usr/bin/wpctl", "set-volume", device_id, str(volume)])
def press_hotkey(hotkey: str) -> None:
subprocess.Popen(["/usr/bin/xdotool", "key", hotkey])
def reset() -> None:
for channel in audiomixer_mapping:
if channel.startswith("Mute_") or channel == "Record":
set_mute(False, audiomixer_mapping[channel])
elif channel.startswith("Record_"):
set_mute(True, audiomixer_mapping[channel])
def vts_hotkey(hotkey: str) -> None:
vts = subprocess.run(
["/usr/bin/xdotool", "search", "--name", "Vtube Studio"],
capture_output=True,
text=True,
).stdout
subprocess.Popen(
[
"/usr/bin/xdotool",
"key",
"--window",
vts,
f"{hotkey}",
]
)
def main():
midi_device = [
device for device in mido.get_input_names() if "nanoKONTROL2" in device
]
# Unmute all channels for me and mute all OBS channels when program starts (usually at boot)
reset()
with mido.open_input(midi_device[0]) as mixer:
for update in mixer:
midi_input = nanoKontrol2_mapping[update.control]
# Mute speakers
if midi_input == "Record":
if update.value == 127:
set_mute(toggle=True, device=audiomixer_mapping["Knob_2"])
else:
set_mute(toggle=False, device=audiomixer_mapping["Knob_2"])
# Set volume per channel
if midi_input in sliders:
set_volume(
volume=update.value,
device=audiomixer_mapping[nanoKontrol2_mapping[update.control]],
)
# Muting channels for user
if (
midi_input in buttons
and midi_input.startswith("Mute_")
and update.value == 127
):
set_mute(toggle=True, device=audiomixer_mapping[midi_input])
elif (
midi_input in buttons
and midi_input.startswith("Mute_")
and update.value == 0
):
set_mute(toggle=False, device=audiomixer_mapping[midi_input])
# Enabling channels for OBS
if (
midi_input in buttons
and midi_input.startswith("Record_")
and update.value == 0
):
set_mute(toggle=True, device=audiomixer_mapping[midi_input])
elif (
midi_input in buttons
and midi_input.startswith("Record_")
and update.value == 127
):
set_mute(toggle=False, device=audiomixer_mapping[midi_input])
# Media controls
if midi_input in ["Prev", "Next"] and update.value == 127:
press_hotkey(hotkey=audiomixer_mapping[midi_input])
if midi_input in ["Play", "Stop"] and (
update.value == 127 or update.value == 0
):
press_hotkey(hotkey=audiomixer_mapping[midi_input])
try:
if (
midi_input in ["Marker_prev", "Marker_next"] and update.value == 127
) or midi_input in ["Cycle", "Marker_set"]:
if audiomixer_mapping[midi_input].startswith("vts"):
vts_hotkey(hotkey=audiomixer_mapping[midi_input].split(" ")[1])
except KeyError:
pass
# TODO define mixers in config files
# TODO user config for audio streams
# TODO running as a daemon
# TODO systemctl unit file
if __name__ == "__main__":
main()

64
uv.lock generated
View File

@@ -9,9 +9,9 @@ source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/fe/a17c106a1f4061ce83f04d14bcedcfb2c38c7793ea56bfb906a6fadae8cb/evdev-1.9.2.tar.gz", hash = "sha256:5d3278892ce1f92a74d6bf888cc8525d9f68af85dbe336c95d1c87fb8f423069", size = 33301, upload-time = "2025-05-01T19:53:47.69Z" } sdist = { url = "https://files.pythonhosted.org/packages/63/fe/a17c106a1f4061ce83f04d14bcedcfb2c38c7793ea56bfb906a6fadae8cb/evdev-1.9.2.tar.gz", hash = "sha256:5d3278892ce1f92a74d6bf888cc8525d9f68af85dbe336c95d1c87fb8f423069", size = 33301, upload-time = "2025-05-01T19:53:47.69Z" }
[[package]] [[package]]
name = "linux-audiomixer" name = "midimixer"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "mido" }, { name = "mido" },
{ name = "pynput" }, { name = "pynput" },
@@ -66,19 +66,19 @@ wheels = [
[[package]] [[package]]
name = "pyobjc-core" name = "pyobjc-core"
version = "12.0" version = "12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/dc/6d63019133e39e2b299dfbab786e64997fff0f145c45a417e1dd51faaf3f/pyobjc_core-12.0.tar.gz", hash = "sha256:7e05c805a776149a937b61b892a0459895d32d9002bedc95ce2be31ef1e37a29", size = 991669, upload-time = "2025-10-21T08:26:07.496Z" } sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/17/6c247bf9d8de2813f6015671f242333534797e81bdac9e85516fb57dfb00/pyobjc_core-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c44b76d8306a130c9eb0cb79d86fd6675c8ba3e5b458e78095d271a10cd38b6a", size = 679700, upload-time = "2025-10-21T07:51:09.518Z" }, { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
{ url = "https://files.pythonhosted.org/packages/08/a3/1b26c438c78821e5a82b9c02f7b19a86097aeb2c51132d06e159acc22dc2/pyobjc_core-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5c617551e0ab860c49229fcec0135a5cde702485f22254ddc17205eb24b7fc55", size = 721370, upload-time = "2025-10-21T07:51:55.981Z" }, { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
{ url = "https://files.pythonhosted.org/packages/35/b1/6df7d4b0d9f0088855a59f6af59230d1191f78fa84ca68851723272f1916/pyobjc_core-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2709ff43ac5c2e9e2c574ae515d3aa0e470345847a4d96c5d4a04b1b86e966d", size = 672302, upload-time = "2025-10-21T07:52:39.445Z" }, { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
{ url = "https://files.pythonhosted.org/packages/f8/10/3a029797c0a22c730ee0d0149ac34ab27afdf51667f96aa23a8ebe7dc3c9/pyobjc_core-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:eb6b987e53291e7cafd8f71a80a2dd44d7afec4202a143a3e47b75cb9cdb5716", size = 713255, upload-time = "2025-10-21T07:53:25.478Z" }, { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
] ]
[[package]] [[package]]
name = "pyobjc-framework-applicationservices" name = "pyobjc-framework-applicationservices"
version = "12.0" version = "12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyobjc-core" }, { name = "pyobjc-core" },
@@ -86,60 +86,60 @@ dependencies = [
{ name = "pyobjc-framework-coretext" }, { name = "pyobjc-framework-coretext" },
{ name = "pyobjc-framework-quartz" }, { name = "pyobjc-framework-quartz" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8c/79/0b7a00bcc7561c816281382c933a46aa7a90acca48b942054b7d32d0caf7/pyobjc_framework_applicationservices-12.0.tar.gz", hash = "sha256:eabbf6c57573158714aa656e5d0112330a87692db336aae7e94e216db89e93be", size = 103595, upload-time = "2025-10-21T08:26:32.651Z" } sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/cf/ae603c46217c04ec7598c62a2d46fa9b6ab66e127148bff1f352b850fc72/pyobjc_framework_applicationservices-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9ff39c0301f2430253fbfea114afb00594426e0b66a1bec1c28cd60f75d02005", size = 32871, upload-time = "2025-10-21T07:54:15.33Z" }, { url = "https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd1f4dbb38234a24ae6819f5e22485cf7dd3dd4074ff3bf9a9fdb4c01a3b4a38", size = 32859, upload-time = "2025-11-14T09:36:15.208Z" },
{ url = "https://files.pythonhosted.org/packages/c6/79/a578c8b1aa8634c2c9f8bbd66a3cdc385013a4cd9558741a4da26c040e51/pyobjc_framework_applicationservices-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ecd7651ab330790722f3590465392dbab3d76be0370ff7e015584053d571e218", size = 33132, upload-time = "2025-10-21T07:54:18.388Z" }, { url = "https://files.pythonhosted.org/packages/66/3a/0f1d4dcf2345e875e5ea9761d5a70969e241d24089133d21f008dde596f5/pyobjc_framework_applicationservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8a5d2845249b6a85ba9e320a9848468c3f8cd6f59605a9a43f406a7810eaa830", size = 33115, upload-time = "2025-11-14T09:36:18.384Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8b/336788981a3c1aa00e75d021a5ed00e453587da1eee0d55bb8b674f2b623/pyobjc_framework_applicationservices-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b153a0b8e915751ca50651be6f9fe002ef7536677f5c37a4dff0f3fd98e5b16a", size = 33007, upload-time = "2025-10-21T07:54:21.971Z" }, { url = "https://files.pythonhosted.org/packages/40/44/3196b40fec68b4413c92875311f17ccf4c3ff7d2e53676f8fc18ad29bd18/pyobjc_framework_applicationservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f43c9a24ad97a9121276d4d571aa04a924282c80d7291cfb3b29839c3e2013a8", size = 32997, upload-time = "2025-11-14T09:36:21.58Z" },
{ url = "https://files.pythonhosted.org/packages/a8/50/0e300544e8204d02b4a0477fa157e904921c98b15f67e19b4a49a80f02c9/pyobjc_framework_applicationservices-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:56f8fabdc3972fc9a97630b24c31d8b852502c3273071ab3d3b467cc5e7c6431", size = 33250, upload-time = "2025-10-21T07:54:25.395Z" }, { url = "https://files.pythonhosted.org/packages/fd/bb/dab21d2210d3ef7dd0616df7e8ea89b5d8d62444133a25f76e649a947168/pyobjc_framework_applicationservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1f72e20009a4ebfd5ed5b23dc11c1528ad6b55cc63ee71952ddb2a5e5f1cb7da", size = 33238, upload-time = "2025-11-14T09:36:24.751Z" },
] ]
[[package]] [[package]]
name = "pyobjc-framework-cocoa" name = "pyobjc-framework-cocoa"
version = "12.0" version = "12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyobjc-core" }, { name = "pyobjc-core" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/37/6f/89837da349fe7de6476c426f118096b147de923139556d98af1832c64b97/pyobjc_framework_cocoa-12.0.tar.gz", hash = "sha256:02d69305b698015a20fcc8e1296e1528e413d8cf9fdcd590478d359386d76e8a", size = 2771906, upload-time = "2025-10-21T08:30:51.765Z" } sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/29/cfef5f021576976698c6ae195fa304238b9f6716e1b3eb11258d2572afe9/pyobjc_framework_cocoa-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:13e573f5093f4158f305b1bac5e1f783881ce2f5f4a69f3c80cb000f76731259", size = 384659, upload-time = "2025-10-21T07:59:34.859Z" }, { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
{ url = "https://files.pythonhosted.org/packages/f1/37/d2d9a143ab5387815a00f478916a52425c4792678366ef6cedf20b8cc9cd/pyobjc_framework_cocoa-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3b167793cd1b509eaf693140ace9be1f827a2c8686fceb8c599907661f608bc2", size = 388787, upload-time = "2025-10-21T08:00:00.006Z" }, { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
{ url = "https://files.pythonhosted.org/packages/0f/15/0a6122e430d0e2ba27ad0e345b89f85346805f39d6f97eea6430a74350d9/pyobjc_framework_cocoa-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a2b6fb9ab3e5ab6db04dfa17828a97894e7da85dd8600885c72a0c2c2214d618", size = 384890, upload-time = "2025-10-21T08:00:25.286Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
{ url = "https://files.pythonhosted.org/packages/79/d7/1a3ad814d427c08b99405e571e47a0219598930ad73850ac02d164d88cd0/pyobjc_framework_cocoa-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:32ff10250a57f72a0b6eca85b790dcc87548ff71d33d0436ffb69680d5e2f308", size = 388925, upload-time = "2025-10-21T08:00:47.309Z" }, { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
] ]
[[package]] [[package]]
name = "pyobjc-framework-coretext" name = "pyobjc-framework-coretext"
version = "12.0" version = "12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyobjc-core" }, { name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-cocoa" },
{ name = "pyobjc-framework-quartz" }, { name = "pyobjc-framework-quartz" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/b1/36/32ec183e555b73152d7813f6f7c277fd018440f70a1f142bd75b04946089/pyobjc_framework_coretext-12.0.tar.gz", hash = "sha256:8cc0c7dd2b7e68ad1c760784e422722550c77cbdbd60eb455170ec444ca1cfd2", size = 90546, upload-time = "2025-10-21T08:32:31.291Z" } sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/64/31da2b1236c710b963510fc03008ebe607d03e2c0288467db9bf9f297873/pyobjc_framework_coretext-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecd424cf6da1a69cad40ef4007bc5af842ccb7456c5fcc4c9aded40e3e0c22ba", size = 30119, upload-time = "2025-10-21T08:04:57.402Z" }, { url = "https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cbb2c28580e6704ce10b9a991ccd9563a22b3a75f67c36cf612544bd8b21b5f", size = 30110, upload-time = "2025-11-14T09:47:07.518Z" },
{ url = "https://files.pythonhosted.org/packages/21/1d/d23fa47ffb6ad32e26a58e357619b5564b4f6e421a839d12961cce521c8f/pyobjc_framework_coretext-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:60e84e46e0aeb12101a4354c39ce84066107773b0c96fdc4ff15fd1662dc88d8", size = 30702, upload-time = "2025-10-21T08:05:00.387Z" }, { url = "https://files.pythonhosted.org/packages/0f/5d/85e059349e9cfbd57269a1f11f56747b3ff5799a3bcbd95485f363c623d8/pyobjc_framework_coretext-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:14100d1e39efb30f57869671fb6fce8d668f80c82e25e7930fb364866e5c0dab", size = 30697, upload-time = "2025-11-14T09:47:10.932Z" },
{ url = "https://files.pythonhosted.org/packages/07/e4/96caefd91817d0f82aaae089e4421cbbef2a216933b5c98435ee2927fbef/pyobjc_framework_coretext-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6ecf89af6de87072f1615fb89d7ed51b345000850a9b827774f262bf6be5acac", size = 30104, upload-time = "2025-10-21T08:05:03.331Z" }, { url = "https://files.pythonhosted.org/packages/ef/c3/adf9d306e9ead108167ab7a974ab7d171dbacf31c72fad63e12585f58023/pyobjc_framework_coretext-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:782a1a9617ea267c05226e9cd81a8dec529969a607fe1e037541ee1feb9524e9", size = 30095, upload-time = "2025-11-14T09:47:13.893Z" },
{ url = "https://files.pythonhosted.org/packages/e5/b5/9152c1a2d8a6fb06d48a36d95b5bb919e820a2f623ca8313ab5eba263be0/pyobjc_framework_coretext-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ddc34c91d16a653db81963141d29f8fc82550fc7a39ed39ff0332764d844ffe1", size = 30714, upload-time = "2025-10-21T08:05:07.092Z" }, { url = "https://files.pythonhosted.org/packages/bd/ca/6321295f47a47b0fca7de7e751ddc0ddc360413f4e506335fe9b0f0fb085/pyobjc_framework_coretext-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7afe379c5a870fa3e66e6f65231c3c1732d9ccd2cd2a4904b2cd5178c9e3c562", size = 30702, upload-time = "2025-11-14T09:47:17.292Z" },
] ]
[[package]] [[package]]
name = "pyobjc-framework-quartz" name = "pyobjc-framework-quartz"
version = "12.0" version = "12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyobjc-core" }, { name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-cocoa" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/91/0b/3c34fc9de790daff5ca49d1f36cb8dcc353ac10e4e29b4759e397a3831f4/pyobjc_framework_quartz-12.0.tar.gz", hash = "sha256:5bcb9e78d671447e04d89e2e3c39f3135157892243facc5f8468aa333e40d67f", size = 3159509, upload-time = "2025-10-21T08:40:01.918Z" } sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/60/d8/05f8fb5f27af69c0b5a9802f220a7c00bbe595c790e13edefa042603b957/pyobjc_framework_quartz-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ece7a05aa2bfc3aa215f1a7c8580e873f3867ba40d0006469618cc2ceb796578", size = 219201, upload-time = "2025-10-21T08:17:59.277Z" }, { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" },
{ url = "https://files.pythonhosted.org/packages/7e/3f/1228f86de266874e20c04f04736a5f11c5a29a1839efde594ba4097d0255/pyobjc_framework_quartz-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f1b2e34f6f0dd023f80a0e875af4dab0ad27fccac239da9ad3d311a2d2578e27", size = 224330, upload-time = "2025-10-21T08:18:14.776Z" }, { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" },
{ url = "https://files.pythonhosted.org/packages/8a/23/ec1804bd10c409fe98ba086329569914fd10b6814208ca6168e81ca0ec1a/pyobjc_framework_quartz-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a2cde43ddc5d2a9ace13af38b4a9ee70dbd47d1707ec6b7185a1a3a1d48e54f9", size = 219581, upload-time = "2025-10-21T08:18:30.219Z" }, { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/86/c2/cf89fda2e477c0c4e2a8aae86202c2891a83bead24e8a7fc733ff490dffc/pyobjc_framework_quartz-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9b928d551ec779141558d986684c19f8f5742251721f440d7087257e4e35b22b", size = 224613, upload-time = "2025-10-21T08:18:45.39Z" }, { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" },
] ]
[[package]] [[package]]