Add TOML config, playerctl dep, and media button defaults; bump to v1.1.0
- Switch config format from JSON to TOML (tomllib built-in on 3.11+, tomli fallback for 3.10) so the generated config can include comments - Write an annotated config.toml on first run with descriptions for every knob/button and hints for all available actions - Promote playerctl from optdepends to depends so media buttons work out of the box - Default buttons 1–3 now pre-configured for playerctl previous/play-pause/next - Rename contrib/config.example.json → config.example.toml with full TOML rewrite including per-knob LED colour override examples - Log a migration hint when a legacy config.json is detectedmain
parent
2e77bf804e
commit
916541d815
6
.SRCINFO
6
.SRCINFO
|
|
@ -1,6 +1,6 @@
|
||||||
pkgbase = turn-up-arch
|
pkgbase = turn-up-arch
|
||||||
pkgdesc = USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux
|
pkgdesc = USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux
|
||||||
pkgver = 1.0.0
|
pkgver = 1.1.0
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/sean351/turn-up-arch
|
url = https://github.com/sean351/turn-up-arch
|
||||||
arch = any
|
arch = any
|
||||||
|
|
@ -13,10 +13,10 @@ pkgbase = turn-up-arch
|
||||||
depends = python-pyserial
|
depends = python-pyserial
|
||||||
depends = python-pulsectl
|
depends = python-pulsectl
|
||||||
depends = pipewire-pulse
|
depends = pipewire-pulse
|
||||||
optdepends = playerctl: media key support via button commands
|
depends = playerctl
|
||||||
optdepends = pulseaudio: alternative to pipewire-pulse
|
optdepends = pulseaudio: alternative to pipewire-pulse
|
||||||
install = turnupd.install
|
install = turnupd.install
|
||||||
source = turn-up-arch-1.0.0.tar.gz::https://github.com/sean351/turn-up-arch/archive/refs/tags/v1.0.0.tar.gz
|
source = turn-up-arch-1.1.0.tar.gz::https://github.com/sean351/turn-up-arch/archive/refs/tags/v1.1.0.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
|
|
||||||
pkgname = turn-up-arch
|
pkgname = turn-up-arch
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
depends = python-pyserial
|
depends = python-pyserial
|
||||||
depends = python-pulsectl
|
depends = python-pulsectl
|
||||||
depends = pipewire-pulse
|
depends = pipewire-pulse
|
||||||
optdepends = playerctl: media key support via button commands
|
depends = playerctl
|
||||||
optdepends = pulseaudio: alternative to pipewire-pulse
|
optdepends = pulseaudio: alternative to pipewire-pulse
|
||||||
install = turnupd.install
|
install = turnupd.install
|
||||||
source = ${PKGNAME}-${VERSION}.tar.gz::https://github.com/${{ github.repository }}/archive/refs/tags/v${VERSION}.tar.gz
|
source = ${PKGNAME}-${VERSION}.tar.gz::https://github.com/${{ github.repository }}/archive/refs/tags/v${VERSION}.tar.gz
|
||||||
|
|
|
||||||
4
PKGBUILD
4
PKGBUILD
|
|
@ -1,7 +1,7 @@
|
||||||
# Maintainer: Sean Doran <sdoran35@gmail.com>
|
# Maintainer: Sean Doran <sdoran35@gmail.com>
|
||||||
# AUR updates are automated via GitHub Actions on version tag push
|
# AUR updates are automated via GitHub Actions on version tag push
|
||||||
pkgname=turn-up-arch
|
pkgname=turn-up-arch
|
||||||
pkgver=1.0.0
|
pkgver=1.1.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
pkgdesc="USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|
@ -12,6 +12,7 @@ depends=(
|
||||||
'python-pyserial'
|
'python-pyserial'
|
||||||
'python-pulsectl'
|
'python-pulsectl'
|
||||||
'pipewire-pulse'
|
'pipewire-pulse'
|
||||||
|
'playerctl'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'python-build'
|
'python-build'
|
||||||
|
|
@ -20,7 +21,6 @@ makedepends=(
|
||||||
'python-hatchling'
|
'python-hatchling'
|
||||||
)
|
)
|
||||||
optdepends=(
|
optdepends=(
|
||||||
'playerctl: media key support via button commands'
|
|
||||||
'pulseaudio: alternative to pipewire-pulse'
|
'pulseaudio: alternative to pipewire-pulse'
|
||||||
)
|
)
|
||||||
install=turnupd.install
|
install=turnupd.install
|
||||||
|
|
|
||||||
68
README.md
68
README.md
|
|
@ -11,7 +11,7 @@ arbitrary shell commands.
|
||||||
- [pyserial](https://pypi.org/project/pyserial/)
|
- [pyserial](https://pypi.org/project/pyserial/)
|
||||||
- [pulsectl](https://pypi.org/project/pulsectl/)
|
- [pulsectl](https://pypi.org/project/pulsectl/)
|
||||||
- PipeWire (with `pipewire-pulse`) or PulseAudio
|
- PipeWire (with `pipewire-pulse`) or PulseAudio
|
||||||
- `playerctl` *(optional — for media key bindings)*
|
- `playerctl`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -40,30 +40,54 @@ pip install .
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
On first run `turnupd` writes a default config to
|
On first run `turnupd` writes a default config to
|
||||||
`~/.config/turnup/config.json`. Edit it to match your device layout.
|
`~/.config/turnup/config.toml`. Edit it to match your device layout.
|
||||||
See [`contrib/config.example.json`](contrib/config.example.json) for a
|
See [`contrib/config.example.toml`](contrib/config.example.toml) for a
|
||||||
fully commented example.
|
fully annotated example.
|
||||||
|
|
||||||
```jsonc
|
```toml
|
||||||
{
|
port = "/dev/ttyACM0"
|
||||||
"port": "/dev/ttyACM0",
|
baud = 115200
|
||||||
"baud": 115200,
|
|
||||||
|
|
||||||
"knobs": {
|
[leds]
|
||||||
"0": { "action": "sink_volume", "target": "default" },
|
mode = "volume"
|
||||||
"1": { "action": "group_volume", "targets": ["vlc", "spotify"] },
|
low_color = [255, 0, 0] # red at 0 %
|
||||||
"2": { "action": "app_volume", "target": "Brave" },
|
high_color = [0, 255, 0] # green at 100 %
|
||||||
"3": { "action": "source_volume","target": "default" }
|
|
||||||
},
|
|
||||||
|
|
||||||
"buttons": {
|
[knobs.0]
|
||||||
"0": { "action": "mute_sink", "target": "default" },
|
action = "sink_volume"
|
||||||
"1": { "action": "command", "target": "playerctl previous" },
|
target = "default"
|
||||||
"2": { "action": "command", "target": "playerctl play-pause" },
|
|
||||||
"3": { "action": "command", "target": "playerctl next" },
|
[knobs.1]
|
||||||
"4": { "action": "mute_source","target": "default" }
|
action = "group_volume"
|
||||||
}
|
targets = ["vlc", "spotify"]
|
||||||
}
|
|
||||||
|
[knobs.2]
|
||||||
|
action = "app_volume"
|
||||||
|
target = "Brave"
|
||||||
|
|
||||||
|
[knobs.3]
|
||||||
|
action = "source_volume"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[buttons.0]
|
||||||
|
action = "mute_sink"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[buttons.1]
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl previous"
|
||||||
|
|
||||||
|
[buttons.2]
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl play-pause"
|
||||||
|
|
||||||
|
[buttons.3]
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl next"
|
||||||
|
|
||||||
|
[buttons.4]
|
||||||
|
action = "mute_source"
|
||||||
|
target = "default"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Knob actions
|
### Knob actions
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
"port": "/dev/ttyACM0",
|
|
||||||
"baud": 115200,
|
|
||||||
|
|
||||||
"leds": {
|
|
||||||
"mode": "volume",
|
|
||||||
"low_color": [255, 0, 0],
|
|
||||||
"high_color": [0, 255, 0]
|
|
||||||
},
|
|
||||||
|
|
||||||
"knobs": {
|
|
||||||
"0": {
|
|
||||||
"action": "sink_volume",
|
|
||||||
"target": "default"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"action": "group_volume",
|
|
||||||
"targets": ["vlc", "spotify", "Cider"],
|
|
||||||
"led": {
|
|
||||||
"mode": "volume",
|
|
||||||
"low_color": [255, 0, 0],
|
|
||||||
"high_color": [0, 0, 255]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"action": "app_volume",
|
|
||||||
"target": "Brave",
|
|
||||||
"led": {
|
|
||||||
"mode": "static",
|
|
||||||
"high_color": [255, 165, 0]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"action": "group_volume",
|
|
||||||
"targets": ["electron", "discord"],
|
|
||||||
"led": { "mode": "off" }
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"action": "source_volume",
|
|
||||||
"target": "default",
|
|
||||||
"led": {
|
|
||||||
"mode": "volume",
|
|
||||||
"low_color": [255, 0, 0],
|
|
||||||
"high_color": [0, 255, 255]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"buttons": {
|
|
||||||
"0": { "action": "mute_sink", "target": "default" },
|
|
||||||
"1": { "action": "command", "target": "playerctl previous" },
|
|
||||||
"2": { "action": "command", "target": "playerctl play-pause" },
|
|
||||||
"3": { "action": "command", "target": "playerctl next" },
|
|
||||||
"4": { "action": "mute_source", "target": "default" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
# turnup configuration — fully annotated example.
|
||||||
|
# Copy this file to ~/.config/turnup/config.toml and edit to taste.
|
||||||
|
# Changes are picked up automatically (no restart needed).
|
||||||
|
|
||||||
|
# Serial port your device enumerates as (check `ls /dev/ttyACM*`).
|
||||||
|
port = "/dev/ttyACM0"
|
||||||
|
baud = 115200
|
||||||
|
|
||||||
|
# ── LED settings ──────────────────────────────────────────────────────────────
|
||||||
|
[leds]
|
||||||
|
# Global LED behaviour — can be overridden per-knob via a [knobs.N.led] block.
|
||||||
|
#
|
||||||
|
# mode: "volume" — fade from low_color → high_color based on the knob position
|
||||||
|
# "static" — always show high_color regardless of volume
|
||||||
|
# "off" — LEDs disabled
|
||||||
|
mode = "volume"
|
||||||
|
low_color = [255, 0, 0] # red at 0 %
|
||||||
|
high_color = [0, 255, 0] # green at 100 %
|
||||||
|
|
||||||
|
# ── Knobs ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Available actions:
|
||||||
|
# sink_volume — output device volume (0–150 %)
|
||||||
|
# source_volume — mic / input volume (0–100 %)
|
||||||
|
# app_volume — single application matched by name or binary; set target
|
||||||
|
# group_volume — multiple applications at once; set targets = ["app1", "app2"]
|
||||||
|
|
||||||
|
[knobs.0]
|
||||||
|
# Master output volume — uses global LED colours
|
||||||
|
action = "sink_volume"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[knobs.1]
|
||||||
|
# Media players — custom blue LED to distinguish from the others
|
||||||
|
action = "group_volume"
|
||||||
|
targets = ["vlc", "spotify", "Cider"]
|
||||||
|
|
||||||
|
[knobs.1.led]
|
||||||
|
mode = "volume"
|
||||||
|
low_color = [255, 0, 0]
|
||||||
|
high_color = [0, 0, 255] # blue at 100 %
|
||||||
|
|
||||||
|
[knobs.2]
|
||||||
|
# Browser — static orange LED (always on, no volume feedback)
|
||||||
|
action = "app_volume"
|
||||||
|
target = "Brave"
|
||||||
|
|
||||||
|
[knobs.2.led]
|
||||||
|
mode = "static"
|
||||||
|
high_color = [255, 165, 0]
|
||||||
|
|
||||||
|
[knobs.3]
|
||||||
|
# Chat apps — LED disabled for this knob
|
||||||
|
action = "group_volume"
|
||||||
|
targets = ["electron", "discord"]
|
||||||
|
|
||||||
|
[knobs.3.led]
|
||||||
|
mode = "off"
|
||||||
|
|
||||||
|
[knobs.4]
|
||||||
|
# Microphone / input — teal LED
|
||||||
|
action = "source_volume"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[knobs.4.led]
|
||||||
|
mode = "volume"
|
||||||
|
low_color = [255, 0, 0]
|
||||||
|
high_color = [0, 255, 255] # teal at 100 %
|
||||||
|
|
||||||
|
# ── Buttons ───────────────────────────────────────────────────────────────────
|
||||||
|
# Available actions:
|
||||||
|
# mute_sink — toggle output mute
|
||||||
|
# mute_source — toggle mic mute
|
||||||
|
# command — run an arbitrary shell command; set target = "command args"
|
||||||
|
|
||||||
|
[buttons.0]
|
||||||
|
# Mute output
|
||||||
|
action = "mute_sink"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[buttons.1]
|
||||||
|
# Previous track (requires playerctl)
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl previous"
|
||||||
|
|
||||||
|
[buttons.2]
|
||||||
|
# Play / pause (requires playerctl)
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl play-pause"
|
||||||
|
|
||||||
|
[buttons.3]
|
||||||
|
# Next track (requires playerctl)
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl next"
|
||||||
|
|
||||||
|
[buttons.4]
|
||||||
|
# Mute microphone
|
||||||
|
action = "mute_source"
|
||||||
|
target = "default"
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "turnup"
|
name = "turnup"
|
||||||
version = "0.3.3"
|
version = "1.1.0"
|
||||||
description = "USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
description = "USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
|
|
@ -12,6 +12,7 @@ requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyserial>=3.5",
|
"pyserial>=3.5",
|
||||||
"pulsectl>=22.0",
|
"pulsectl>=22.0",
|
||||||
|
"tomli>=1.1; python_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
"""Turn Up — USB serial mixer daemon for PipeWire/PulseAudio."""
|
"""Turn Up — USB serial mixer daemon for PipeWire/PulseAudio."""
|
||||||
|
|
||||||
__version__ = "0.3.3"
|
__version__ = "1.1.0"
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,132 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
config.py — Load and validate turnup configuration from config.json
|
config.py — Load and validate turnup configuration from config.toml
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError: # Python < 3.11
|
||||||
|
import tomli as tomllib # type: ignore[no-reuse-ignore]
|
||||||
|
|
||||||
log = logging.getLogger("turnupd")
|
log = logging.getLogger("turnupd")
|
||||||
|
|
||||||
|
# In-memory defaults used as fallback when the config file is missing or a
|
||||||
|
# field is omitted. The on-disk template (DEFAULT_CONFIG_TOML) is the
|
||||||
|
# authoritative human-readable version of these values.
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"port": "/dev/ttyACM0",
|
"port": "/dev/ttyACM0",
|
||||||
"baud": 115200,
|
"baud": 115200,
|
||||||
"leds": {
|
"leds": {
|
||||||
# Global default LED behaviour — overridable per-knob via a "led": {}
|
|
||||||
# block inside each knob entry.
|
|
||||||
#
|
|
||||||
# mode:
|
|
||||||
# "volume" — interpolate low_color→high_color based on knob position
|
|
||||||
# "static" — always show high_color regardless of volume
|
|
||||||
# "off" — LEDs disabled
|
|
||||||
"mode": "volume",
|
"mode": "volume",
|
||||||
"low_color": [255, 0, 0], # red at volume 0.0
|
"low_color": [255, 0, 0],
|
||||||
"high_color": [0, 255, 0], # green at volume 1.0
|
"high_color": [0, 255, 0],
|
||||||
},
|
},
|
||||||
"knobs": {
|
"knobs": {
|
||||||
"0": {"action": "sink_volume", "target": "default"},
|
"0": {"action": "sink_volume", "target": "default"},
|
||||||
"1": {"action": "sink_volume", "target": "default"},
|
"1": {"action": "group_volume", "targets": ["spotify", "vlc", "Cider"]},
|
||||||
"2": {"action": "source_volume", "target": "default"},
|
"2": {"action": "app_volume", "target": "brave"},
|
||||||
"3": {"action": "sink_volume", "target": "default"},
|
"3": {"action": "source_volume", "target": "default"},
|
||||||
"4": {"action": "sink_volume", "target": "default"},
|
"4": {"action": "sink_volume", "target": "default"},
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"0": {"action": "mute_sink", "target": "default"},
|
"0": {"action": "mute_sink", "target": "default"},
|
||||||
"1": {"action": "mute_source", "target": "default"},
|
"1": {"action": "command", "target": "playerctl previous"},
|
||||||
"2": {"action": "command", "target": ""},
|
"2": {"action": "command", "target": "playerctl play-pause"},
|
||||||
"3": {"action": "command", "target": ""},
|
"3": {"action": "command", "target": "playerctl next"},
|
||||||
"4": {"action": "command", "target": ""},
|
"4": {"action": "mute_source", "target": "default"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Annotated TOML template written to ~/.config/turnup/config.toml on first run.
|
||||||
|
DEFAULT_CONFIG_TOML: str = """\
|
||||||
|
# turnup configuration — edit this file to match your device layout.
|
||||||
|
# Changes are picked up automatically (no restart needed).
|
||||||
|
# See https://github.com/sean351/turn-up-arch for full documentation.
|
||||||
|
|
||||||
|
# Serial port your device enumerates as (check `ls /dev/ttyACM*`).
|
||||||
|
port = "/dev/ttyACM0"
|
||||||
|
baud = 115200
|
||||||
|
|
||||||
|
# ── LED settings ──────────────────────────────────────────────────────────────
|
||||||
|
[leds]
|
||||||
|
# mode: "volume" — fade from low_color → high_color based on the knob position
|
||||||
|
# "static" — always show high_color regardless of volume
|
||||||
|
# "off" — LEDs disabled
|
||||||
|
mode = "volume"
|
||||||
|
low_color = [255, 0, 0] # red at 0 %
|
||||||
|
high_color = [0, 255, 0] # green at 100 %
|
||||||
|
|
||||||
|
# ── Knobs ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Available actions:
|
||||||
|
# sink_volume — output device volume (0–150 %)
|
||||||
|
# source_volume — mic / input volume (0–100 %)
|
||||||
|
# app_volume — single application; set target = "AppName"
|
||||||
|
# group_volume — multiple applications at once; set targets = ["app1", "app2"]
|
||||||
|
#
|
||||||
|
# Each knob can optionally include a [knobs.N.led] block to override the
|
||||||
|
# global LED colours for that specific knob.
|
||||||
|
|
||||||
|
[knobs.0]
|
||||||
|
# Knob 0 — master output volume
|
||||||
|
action = "sink_volume"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[knobs.1]
|
||||||
|
# Knob 1 — media players (group volume example)
|
||||||
|
action = "group_volume"
|
||||||
|
targets = ["spotify", "vlc", "Cider"]
|
||||||
|
|
||||||
|
[knobs.2]
|
||||||
|
# Knob 2 — browser (app volume example)
|
||||||
|
action = "app_volume"
|
||||||
|
target = "brave"
|
||||||
|
|
||||||
|
[knobs.3]
|
||||||
|
# Knob 3 — microphone / input
|
||||||
|
action = "source_volume"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[knobs.4]
|
||||||
|
# Knob 4 — unused; change to taste
|
||||||
|
action = "sink_volume"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
# ── Buttons ───────────────────────────────────────────────────────────────────
|
||||||
|
# Available actions:
|
||||||
|
# mute_sink — toggle output mute
|
||||||
|
# mute_source — toggle mic mute
|
||||||
|
# command — run an arbitrary shell command; set target = "command args"
|
||||||
|
|
||||||
|
[buttons.0]
|
||||||
|
# Button 0 — mute output
|
||||||
|
action = "mute_sink"
|
||||||
|
target = "default"
|
||||||
|
|
||||||
|
[buttons.1]
|
||||||
|
# Button 1 — previous track (requires playerctl)
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl previous"
|
||||||
|
|
||||||
|
[buttons.2]
|
||||||
|
# Button 2 — play / pause (requires playerctl)
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl play-pause"
|
||||||
|
|
||||||
|
[buttons.3]
|
||||||
|
# Button 3 — next track (requires playerctl)
|
||||||
|
action = "command"
|
||||||
|
target = "playerctl next"
|
||||||
|
|
||||||
|
[buttons.4]
|
||||||
|
# Button 4 — mute microphone
|
||||||
|
action = "mute_source"
|
||||||
|
target = "default"
|
||||||
|
"""
|
||||||
|
|
||||||
VALID_KNOB_ACTIONS: frozenset = frozenset(
|
VALID_KNOB_ACTIONS: frozenset = frozenset(
|
||||||
{"sink_volume", "source_volume", "app_volume", "group_volume"}
|
{"sink_volume", "source_volume", "app_volume", "group_volume"}
|
||||||
)
|
)
|
||||||
|
|
@ -53,7 +139,7 @@ _XDG_CONFIG_DIR = os.path.join(
|
||||||
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
||||||
"turnup",
|
"turnup",
|
||||||
)
|
)
|
||||||
DEFAULT_CONFIG_PATH = os.path.join(_XDG_CONFIG_DIR, "config.json")
|
DEFAULT_CONFIG_PATH = os.path.join(_XDG_CONFIG_DIR, "config.toml")
|
||||||
|
|
||||||
|
|
||||||
# ── LED helpers ────────────────────────────────────────────────────────────────
|
# ── LED helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -147,11 +233,15 @@ def get_led_color(led_cfg: dict, norm: float) -> tuple[int, int, int]:
|
||||||
high = led_cfg.get("high_color", DEFAULT_CONFIG["leds"]["high_color"])
|
high = led_cfg.get("high_color", DEFAULT_CONFIG["leds"]["high_color"])
|
||||||
|
|
||||||
if mode == "static":
|
if mode == "static":
|
||||||
return tuple(high)
|
return (high[0], high[1], high[2])
|
||||||
|
|
||||||
low = led_cfg.get("low_color", DEFAULT_CONFIG["leds"]["low_color"])
|
low = led_cfg.get("low_color", DEFAULT_CONFIG["leds"]["low_color"])
|
||||||
t = max(0.0, min(1.0, norm))
|
t = max(0.0, min(1.0, norm))
|
||||||
return tuple(int(low[i] + (high[i] - low[i]) * t) for i in range(3))
|
return (
|
||||||
|
int(low[0] + (high[0] - low[0]) * t),
|
||||||
|
int(low[1] + (high[1] - low[1]) * t),
|
||||||
|
int(low[2] + (high[2] - low[2]) * t),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Config I/O ─────────────────────────────────────────────────────────────────
|
# ── Config I/O ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -160,37 +250,44 @@ def load_config(path: str | None = None) -> dict:
|
||||||
"""Load configuration from *path*.
|
"""Load configuration from *path*.
|
||||||
|
|
||||||
If *path* is ``None`` the XDG-compliant location
|
If *path* is ``None`` the XDG-compliant location
|
||||||
``~/.config/turnup/config.json`` is used.
|
``~/.config/turnup/config.toml`` is used.
|
||||||
|
|
||||||
Returns the parsed and validated configuration dictionary.
|
Returns the parsed and validated configuration dictionary.
|
||||||
Exits with status 1 on malformed JSON.
|
Exits with status 1 on malformed TOML.
|
||||||
"""
|
"""
|
||||||
if path is None:
|
if path is None:
|
||||||
path = DEFAULT_CONFIG_PATH
|
path = DEFAULT_CONFIG_PATH
|
||||||
|
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
log.warning("No config.json found at %s — writing defaults", path)
|
# Hint for users upgrading from the old JSON format.
|
||||||
|
legacy_path = path.replace(".toml", ".json")
|
||||||
|
if os.path.exists(legacy_path):
|
||||||
|
log.warning(
|
||||||
|
"Found legacy JSON config at %s — "
|
||||||
|
"please migrate to TOML format at %s",
|
||||||
|
legacy_path, path,
|
||||||
|
)
|
||||||
|
log.warning("No config.toml found at %s — writing defaults", path)
|
||||||
_write_default(path)
|
_write_default(path)
|
||||||
return dict(DEFAULT_CONFIG)
|
return dict(DEFAULT_CONFIG)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path, "rb") as f:
|
||||||
cfg: dict = json.load(f)
|
cfg: dict = tomllib.load(f)
|
||||||
cfg["leds"] = _validate_leds(cfg.get("leds", {}))
|
cfg["leds"] = _validate_leds(cfg.get("leds", {}))
|
||||||
log.info("Loaded config from %s", path)
|
log.info("Loaded config from %s", path)
|
||||||
return cfg
|
return cfg
|
||||||
except json.JSONDecodeError as exc:
|
except tomllib.TOMLDecodeError as exc:
|
||||||
log.error("Invalid JSON in %s: %s", path, exc)
|
log.error("Invalid TOML in %s: %s", path, exc)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def _write_default(path: str) -> None:
|
def _write_default(path: str) -> None:
|
||||||
"""Write the default configuration to *path*, creating directories as needed."""
|
"""Write the annotated default configuration to *path*, creating directories as needed."""
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
json.dump(DEFAULT_CONFIG, f, indent=2)
|
f.write(DEFAULT_CONFIG_TOML)
|
||||||
f.write("\n")
|
|
||||||
log.info("Created default config at %s", path)
|
log.info("Created default config at %s", path)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
log.warning("Could not write default config: %s", exc)
|
log.warning("Could not write default config: %s", exc)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue