av-output-switcher 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- av_output_switcher-1.0.0/.gitignore +10 -0
- av_output_switcher-1.0.0/LICENSE +24 -0
- av_output_switcher-1.0.0/PKG-INFO +272 -0
- av_output_switcher-1.0.0/README.md +244 -0
- av_output_switcher-1.0.0/pyproject.toml +98 -0
- av_output_switcher-1.0.0/src/av_output_switcher/__init__.py +31 -0
- av_output_switcher-1.0.0/src/av_output_switcher/__main__.py +6 -0
- av_output_switcher-1.0.0/src/av_output_switcher/application.py +158 -0
- av_output_switcher-1.0.0/src/av_output_switcher/cli_tool.py +58 -0
- av_output_switcher-1.0.0/src/av_output_switcher/pactl.py +123 -0
- av_output_switcher-1.0.0/src/av_output_switcher/profiles.py +104 -0
- av_output_switcher-1.0.0/src/av_output_switcher/py.typed +0 -0
- av_output_switcher-1.0.0/src/av_output_switcher/selector.py +255 -0
- av_output_switcher-1.0.0/src/av_output_switcher/xrandr.py +515 -0
- av_output_switcher-1.0.0/tests/__init__.py +0 -0
- av_output_switcher-1.0.0/tests/test_package_version.py +5 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: av-output-switcher
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cycles audio/video output profiles (xrandr + pactl) on Xorg.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sevaht/av-output-switcher
|
|
6
|
+
Project-URL: Source, https://github.com/sevaht/av-output-switcher
|
|
7
|
+
Project-URL: Issues, https://github.com/sevaht/av-output-switcher/issues
|
|
8
|
+
Author-email: Jacob McIntosh <nacitar.sevaht@gmail.com>
|
|
9
|
+
License-Expression: Unlicense
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: audio,cli,display,fluxbox,monitor,output,pactl,pulseaudio,video,xorg,xrandr
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video :: Display
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Requires-Dist: platformdirs>=4.10.0
|
|
26
|
+
Requires-Dist: sevaht-utility>=1.0.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# av-output-switcher
|
|
30
|
+
|
|
31
|
+
Cycles audio/video **output profiles** for lightweight Xorg window managers
|
|
32
|
+
(it's built for a Fluxbox-style setup driven by keybinds).
|
|
33
|
+
|
|
34
|
+
A *profile* couples two things under one name:
|
|
35
|
+
|
|
36
|
+
- an **xrandr display layout** — which connected outputs are on, their
|
|
37
|
+
resolution/refresh-rate/position, and which may act as primary; and
|
|
38
|
+
- a preferred **PulseAudio sink** (matched by regex via `pactl`).
|
|
39
|
+
|
|
40
|
+
Applying a profile reconfigures the monitors *and* repoints default audio in
|
|
41
|
+
one shot. The `--cycle-*` commands are meant to be bound to keys: step through
|
|
42
|
+
valid profiles, flip the primary monitor, or rotate the default audio sink.
|
|
43
|
+
|
|
44
|
+
> **Platform:** Xorg only. It shells out to `xrandr` and `xprop` (X11) and
|
|
45
|
+
> `pactl` (PulseAudio/PipeWire's pulse shim). It does not work on Wayland.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ uv tool install av-output-switcher
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
At runtime it shells out to these external commands, which must be on `PATH`:
|
|
54
|
+
|
|
55
|
+
- `xrandr` — query and apply the display layout (X11)
|
|
56
|
+
- `xprop` — detect when the WM has settled after a layout change (X11)
|
|
57
|
+
- `pactl` — list sinks and switch the default audio output
|
|
58
|
+
(PulseAudio, or PipeWire's pulse shim)
|
|
59
|
+
|
|
60
|
+
On ArchLinux these are the `xorg-xrandr`, `xorg-xprop`, and `libpulse`
|
|
61
|
+
packages.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Exactly one action per invocation:
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
$ av-output-switcher --state # show outputs, modes, positions
|
|
69
|
+
$ av-output-switcher --list-sinks # show audio sink names
|
|
70
|
+
$ av-output-switcher --list # list configured profile names
|
|
71
|
+
$ av-output-switcher --get-current-profile
|
|
72
|
+
$ av-output-switcher --primary-resolution # e.g. 2560x1440
|
|
73
|
+
|
|
74
|
+
$ av-output-switcher --profile NAME # apply a specific profile
|
|
75
|
+
$ av-output-switcher --default-profile # apply the best profile for what's
|
|
76
|
+
# currently connected
|
|
77
|
+
$ av-output-switcher --cycle-profile # advance to the next valid profile
|
|
78
|
+
$ av-output-switcher --cycle-primary # rotate the primary output
|
|
79
|
+
$ av-output-switcher --cycle-pactl-sink # rotate the default audio sink
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Logging options (from `sevaht-utility`): `-v/--verbose`, `-q/--quiet`,
|
|
83
|
+
`--debug`, and `--log-file FILE`.
|
|
84
|
+
|
|
85
|
+
### Example keybinds (Fluxbox `~/.fluxbox/keys`)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Mod4 p :Exec av-output-switcher --cycle-profile
|
|
89
|
+
Mod4 o :Exec av-output-switcher --cycle-primary
|
|
90
|
+
Mod4 a :Exec av-output-switcher --cycle-pactl-sink
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Configuration lives in the platform user-config directory under
|
|
96
|
+
`sevaht/av-output-switcher/`:
|
|
97
|
+
|
|
98
|
+
- Linux: `~/.config/sevaht/av-output-switcher/`
|
|
99
|
+
|
|
100
|
+
Create that directory and a `profiles.json` inside it. The rest of this
|
|
101
|
+
section walks through writing one from scratch.
|
|
102
|
+
|
|
103
|
+
### Finding the values to put in a profile
|
|
104
|
+
|
|
105
|
+
You should never need to run `xrandr` or `pactl` yourself — the tool reports
|
|
106
|
+
every value a profile needs. These two commands work before any config exists.
|
|
107
|
+
|
|
108
|
+
**Outputs, resolutions, refresh rates, positions, and the current primary**
|
|
109
|
+
come from `--state`:
|
|
110
|
+
|
|
111
|
+
```console
|
|
112
|
+
$ av-output-switcher --state
|
|
113
|
+
Screen 0: current 4480x1440
|
|
114
|
+
DP-1 connected primary 2560x1440+0+0 = [DEL] DELL S2721DGF (ABC123)
|
|
115
|
+
2560x1440 144.00*+ 120.00 60.00
|
|
116
|
+
1920x1080 144.00 60.00
|
|
117
|
+
HDMI-1 connected 1920x1080+2560+0 = [SAM] Samsung
|
|
118
|
+
1920x1080 60.00*+ 50.00
|
|
119
|
+
1280x720 60.00
|
|
120
|
+
eDP-1 disconnected
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
How to read it:
|
|
124
|
+
|
|
125
|
+
- The first token on each non-indented line is the **output name** (`DP-1`,
|
|
126
|
+
`HDMI-1`, `eDP-1`) — these are the keys under `outputs` and the values for
|
|
127
|
+
`connected_output_names`. Only `connected` outputs can be enabled.
|
|
128
|
+
- `primary` marks the output currently acting as primary.
|
|
129
|
+
- `2560x1440+0+0` is the current `WIDTHxHEIGHT+X+Y`: it gives both the
|
|
130
|
+
**resolution** (`width`/`height`) and the **position** (`x`/`y`).
|
|
131
|
+
- The indented lines list each supported **resolution** followed by its
|
|
132
|
+
available **refresh rates**. `*` marks the rate currently in use and `+`
|
|
133
|
+
marks the monitor's preferred mode, so `2560x1440 144.00*+` means
|
|
134
|
+
2560×1440 @ 144.00 Hz is both active and preferred. Pick a width/height and
|
|
135
|
+
a refresh rate that appear together here.
|
|
136
|
+
|
|
137
|
+
**Audio sink names** come from `--list-sinks` (the `*` is the current
|
|
138
|
+
default):
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
$ av-output-switcher --list-sinks
|
|
142
|
+
* alsa_output.pci-0000_00_1f.3.analog-stereo
|
|
143
|
+
alsa_output.pci-0000_01_00.1.hdmi-stereo
|
|
144
|
+
alsa_output.usb-Generic_USB_Audio-00.analog-stereo
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A profile selects audio with regexes matched against these names, so you can
|
|
148
|
+
either paste a full name or match a stable fragment (e.g. `.*hdmi.*`). Only
|
|
149
|
+
ALSA sinks are listed, since those are the only ones the tool can switch to.
|
|
150
|
+
|
|
151
|
+
### Building a profile, step by step
|
|
152
|
+
|
|
153
|
+
Say `--state` shows `DP-1` (2560×1440 @144) on the left and `HDMI-1`
|
|
154
|
+
(1920×1080 @60) to its right, and you want sound to come out over HDMI:
|
|
155
|
+
|
|
156
|
+
1. **Name the profile** — add a key under `profiles` (e.g. `"desk"`).
|
|
157
|
+
2. **Add each output you want on** under `outputs`, keyed by its name from
|
|
158
|
+
`--state`. Any connected output you *omit* is turned off when the profile
|
|
159
|
+
is applied.
|
|
160
|
+
3. **Fill `resolution` and `refresh_rate`** from a `*`/`+` line in `--state`.
|
|
161
|
+
`refresh_rate` is a string to preserve the exact decimal (e.g. `"144.00"`).
|
|
162
|
+
4. **Set `position`** — the top-left pixel coordinate of that output in the
|
|
163
|
+
combined desktop. Put the leftmost output at `{"x": 0, "y": 0}`, then place
|
|
164
|
+
the next one to its right at `x` = the sum of the widths to its left
|
|
165
|
+
(`2560` here), `y` = 0 for a single top-aligned row. Stack vertically with
|
|
166
|
+
`y` instead.
|
|
167
|
+
5. **Mark `primary_candidate: true`** on outputs allowed to be primary (the
|
|
168
|
+
first such output becomes primary when the profile is applied, and these
|
|
169
|
+
are the outputs `--cycle-primary` rotates through).
|
|
170
|
+
6. **Set `pactl_sink_option_regexes`** to an ordered list of patterns from
|
|
171
|
+
`--list-sinks`; the first one that matches a present sink becomes the
|
|
172
|
+
default. Use `[]` to leave audio untouched.
|
|
173
|
+
|
|
174
|
+
That produces the `"desk"` profile in the example below.
|
|
175
|
+
|
|
176
|
+
### `profiles.json`
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"default_profiles": [
|
|
181
|
+
{
|
|
182
|
+
"connected_output_names": ["eDP-1"],
|
|
183
|
+
"profile_name": "laptop"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"connected_output_names": ["DP-1", "HDMI-1"],
|
|
187
|
+
"profile_name": "desk"
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"profiles": {
|
|
191
|
+
"laptop": {
|
|
192
|
+
"pactl_sink_option_regexes": ["alsa_output\\..*pci.*analog.*"],
|
|
193
|
+
"outputs": {
|
|
194
|
+
"eDP-1": {
|
|
195
|
+
"configuration": {
|
|
196
|
+
"mode": {
|
|
197
|
+
"resolution": {"width": 1920, "height": 1080},
|
|
198
|
+
"refresh_rate": "60.00"
|
|
199
|
+
},
|
|
200
|
+
"position": {"x": 0, "y": 0}
|
|
201
|
+
},
|
|
202
|
+
"primary_candidate": true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"desk": {
|
|
207
|
+
"pactl_sink_option_regexes": ["alsa_output\\..*hdmi.*"],
|
|
208
|
+
"outputs": {
|
|
209
|
+
"DP-1": {
|
|
210
|
+
"configuration": {
|
|
211
|
+
"mode": {
|
|
212
|
+
"resolution": {"width": 2560, "height": 1440},
|
|
213
|
+
"refresh_rate": "144.00"
|
|
214
|
+
},
|
|
215
|
+
"position": {"x": 0, "y": 0}
|
|
216
|
+
},
|
|
217
|
+
"primary_candidate": true
|
|
218
|
+
},
|
|
219
|
+
"HDMI-1": {
|
|
220
|
+
"configuration": {
|
|
221
|
+
"mode": {
|
|
222
|
+
"resolution": {"width": 1920, "height": 1080},
|
|
223
|
+
"refresh_rate": "60.00"
|
|
224
|
+
},
|
|
225
|
+
"position": {"x": 2560, "y": 0}
|
|
226
|
+
},
|
|
227
|
+
"primary_candidate": false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Field reference
|
|
236
|
+
|
|
237
|
+
- **`profiles`** — a map of profile name → profile. Applying a profile (via
|
|
238
|
+
`--profile`, `--default-profile`, or `--cycle-profile`) enables exactly the
|
|
239
|
+
listed `outputs`; any connected output *not* listed is turned off.
|
|
240
|
+
- **`outputs`** — map of output name → output state. Required.
|
|
241
|
+
- **`configuration.mode.resolution`** — `{"width", "height"}` integers.
|
|
242
|
+
- **`configuration.mode.refresh_rate`** — refresh rate as a *string*
|
|
243
|
+
(e.g. `"144.00"`). Required; use a value shown by `--state`.
|
|
244
|
+
- **`configuration.position`** — `{"x", "y"}` integers, the output's
|
|
245
|
+
top-left corner in the combined desktop.
|
|
246
|
+
- **`primary_candidate`** — optional, defaults to `false`. Outputs set to
|
|
247
|
+
`true` are eligible to be primary and are the set `--cycle-primary`
|
|
248
|
+
rotates through; the first one listed becomes primary on apply.
|
|
249
|
+
- **`pactl_sink_option_regexes`** — ordered list of regexes matched against
|
|
250
|
+
sink names from `--list-sinks`. On apply, the first pattern that matches a
|
|
251
|
+
present sink becomes the default. Required key; use `[]` to leave audio
|
|
252
|
+
alone.
|
|
253
|
+
- **`default_profiles`** — optional list of rules for `--default-profile`. A
|
|
254
|
+
rule (`connected_output_names` + `profile_name`) matches when all of its
|
|
255
|
+
outputs are currently connected; the matching rule listing the most outputs
|
|
256
|
+
wins. This is what picks the right profile automatically (e.g. at login, or
|
|
257
|
+
when you dock/undock).
|
|
258
|
+
|
|
259
|
+
### Hooks (optional)
|
|
260
|
+
|
|
261
|
+
If present and executable in the config directory, these scripts are run on
|
|
262
|
+
changes:
|
|
263
|
+
|
|
264
|
+
- `on-profile-change` — run after a profile is applied, with the profile name
|
|
265
|
+
as `$1` (after waiting briefly for the WM to settle, detected via the root
|
|
266
|
+
window pixmap).
|
|
267
|
+
- `on-primary-output-change` — run when the primary output changes, with the
|
|
268
|
+
new output name as `$1`.
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
[Unlicense](LICENSE) — public domain.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# av-output-switcher
|
|
2
|
+
|
|
3
|
+
Cycles audio/video **output profiles** for lightweight Xorg window managers
|
|
4
|
+
(it's built for a Fluxbox-style setup driven by keybinds).
|
|
5
|
+
|
|
6
|
+
A *profile* couples two things under one name:
|
|
7
|
+
|
|
8
|
+
- an **xrandr display layout** — which connected outputs are on, their
|
|
9
|
+
resolution/refresh-rate/position, and which may act as primary; and
|
|
10
|
+
- a preferred **PulseAudio sink** (matched by regex via `pactl`).
|
|
11
|
+
|
|
12
|
+
Applying a profile reconfigures the monitors *and* repoints default audio in
|
|
13
|
+
one shot. The `--cycle-*` commands are meant to be bound to keys: step through
|
|
14
|
+
valid profiles, flip the primary monitor, or rotate the default audio sink.
|
|
15
|
+
|
|
16
|
+
> **Platform:** Xorg only. It shells out to `xrandr` and `xprop` (X11) and
|
|
17
|
+
> `pactl` (PulseAudio/PipeWire's pulse shim). It does not work on Wayland.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```console
|
|
22
|
+
$ uv tool install av-output-switcher
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
At runtime it shells out to these external commands, which must be on `PATH`:
|
|
26
|
+
|
|
27
|
+
- `xrandr` — query and apply the display layout (X11)
|
|
28
|
+
- `xprop` — detect when the WM has settled after a layout change (X11)
|
|
29
|
+
- `pactl` — list sinks and switch the default audio output
|
|
30
|
+
(PulseAudio, or PipeWire's pulse shim)
|
|
31
|
+
|
|
32
|
+
On ArchLinux these are the `xorg-xrandr`, `xorg-xprop`, and `libpulse`
|
|
33
|
+
packages.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Exactly one action per invocation:
|
|
38
|
+
|
|
39
|
+
```console
|
|
40
|
+
$ av-output-switcher --state # show outputs, modes, positions
|
|
41
|
+
$ av-output-switcher --list-sinks # show audio sink names
|
|
42
|
+
$ av-output-switcher --list # list configured profile names
|
|
43
|
+
$ av-output-switcher --get-current-profile
|
|
44
|
+
$ av-output-switcher --primary-resolution # e.g. 2560x1440
|
|
45
|
+
|
|
46
|
+
$ av-output-switcher --profile NAME # apply a specific profile
|
|
47
|
+
$ av-output-switcher --default-profile # apply the best profile for what's
|
|
48
|
+
# currently connected
|
|
49
|
+
$ av-output-switcher --cycle-profile # advance to the next valid profile
|
|
50
|
+
$ av-output-switcher --cycle-primary # rotate the primary output
|
|
51
|
+
$ av-output-switcher --cycle-pactl-sink # rotate the default audio sink
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Logging options (from `sevaht-utility`): `-v/--verbose`, `-q/--quiet`,
|
|
55
|
+
`--debug`, and `--log-file FILE`.
|
|
56
|
+
|
|
57
|
+
### Example keybinds (Fluxbox `~/.fluxbox/keys`)
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Mod4 p :Exec av-output-switcher --cycle-profile
|
|
61
|
+
Mod4 o :Exec av-output-switcher --cycle-primary
|
|
62
|
+
Mod4 a :Exec av-output-switcher --cycle-pactl-sink
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Configuration lives in the platform user-config directory under
|
|
68
|
+
`sevaht/av-output-switcher/`:
|
|
69
|
+
|
|
70
|
+
- Linux: `~/.config/sevaht/av-output-switcher/`
|
|
71
|
+
|
|
72
|
+
Create that directory and a `profiles.json` inside it. The rest of this
|
|
73
|
+
section walks through writing one from scratch.
|
|
74
|
+
|
|
75
|
+
### Finding the values to put in a profile
|
|
76
|
+
|
|
77
|
+
You should never need to run `xrandr` or `pactl` yourself — the tool reports
|
|
78
|
+
every value a profile needs. These two commands work before any config exists.
|
|
79
|
+
|
|
80
|
+
**Outputs, resolutions, refresh rates, positions, and the current primary**
|
|
81
|
+
come from `--state`:
|
|
82
|
+
|
|
83
|
+
```console
|
|
84
|
+
$ av-output-switcher --state
|
|
85
|
+
Screen 0: current 4480x1440
|
|
86
|
+
DP-1 connected primary 2560x1440+0+0 = [DEL] DELL S2721DGF (ABC123)
|
|
87
|
+
2560x1440 144.00*+ 120.00 60.00
|
|
88
|
+
1920x1080 144.00 60.00
|
|
89
|
+
HDMI-1 connected 1920x1080+2560+0 = [SAM] Samsung
|
|
90
|
+
1920x1080 60.00*+ 50.00
|
|
91
|
+
1280x720 60.00
|
|
92
|
+
eDP-1 disconnected
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
How to read it:
|
|
96
|
+
|
|
97
|
+
- The first token on each non-indented line is the **output name** (`DP-1`,
|
|
98
|
+
`HDMI-1`, `eDP-1`) — these are the keys under `outputs` and the values for
|
|
99
|
+
`connected_output_names`. Only `connected` outputs can be enabled.
|
|
100
|
+
- `primary` marks the output currently acting as primary.
|
|
101
|
+
- `2560x1440+0+0` is the current `WIDTHxHEIGHT+X+Y`: it gives both the
|
|
102
|
+
**resolution** (`width`/`height`) and the **position** (`x`/`y`).
|
|
103
|
+
- The indented lines list each supported **resolution** followed by its
|
|
104
|
+
available **refresh rates**. `*` marks the rate currently in use and `+`
|
|
105
|
+
marks the monitor's preferred mode, so `2560x1440 144.00*+` means
|
|
106
|
+
2560×1440 @ 144.00 Hz is both active and preferred. Pick a width/height and
|
|
107
|
+
a refresh rate that appear together here.
|
|
108
|
+
|
|
109
|
+
**Audio sink names** come from `--list-sinks` (the `*` is the current
|
|
110
|
+
default):
|
|
111
|
+
|
|
112
|
+
```console
|
|
113
|
+
$ av-output-switcher --list-sinks
|
|
114
|
+
* alsa_output.pci-0000_00_1f.3.analog-stereo
|
|
115
|
+
alsa_output.pci-0000_01_00.1.hdmi-stereo
|
|
116
|
+
alsa_output.usb-Generic_USB_Audio-00.analog-stereo
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A profile selects audio with regexes matched against these names, so you can
|
|
120
|
+
either paste a full name or match a stable fragment (e.g. `.*hdmi.*`). Only
|
|
121
|
+
ALSA sinks are listed, since those are the only ones the tool can switch to.
|
|
122
|
+
|
|
123
|
+
### Building a profile, step by step
|
|
124
|
+
|
|
125
|
+
Say `--state` shows `DP-1` (2560×1440 @144) on the left and `HDMI-1`
|
|
126
|
+
(1920×1080 @60) to its right, and you want sound to come out over HDMI:
|
|
127
|
+
|
|
128
|
+
1. **Name the profile** — add a key under `profiles` (e.g. `"desk"`).
|
|
129
|
+
2. **Add each output you want on** under `outputs`, keyed by its name from
|
|
130
|
+
`--state`. Any connected output you *omit* is turned off when the profile
|
|
131
|
+
is applied.
|
|
132
|
+
3. **Fill `resolution` and `refresh_rate`** from a `*`/`+` line in `--state`.
|
|
133
|
+
`refresh_rate` is a string to preserve the exact decimal (e.g. `"144.00"`).
|
|
134
|
+
4. **Set `position`** — the top-left pixel coordinate of that output in the
|
|
135
|
+
combined desktop. Put the leftmost output at `{"x": 0, "y": 0}`, then place
|
|
136
|
+
the next one to its right at `x` = the sum of the widths to its left
|
|
137
|
+
(`2560` here), `y` = 0 for a single top-aligned row. Stack vertically with
|
|
138
|
+
`y` instead.
|
|
139
|
+
5. **Mark `primary_candidate: true`** on outputs allowed to be primary (the
|
|
140
|
+
first such output becomes primary when the profile is applied, and these
|
|
141
|
+
are the outputs `--cycle-primary` rotates through).
|
|
142
|
+
6. **Set `pactl_sink_option_regexes`** to an ordered list of patterns from
|
|
143
|
+
`--list-sinks`; the first one that matches a present sink becomes the
|
|
144
|
+
default. Use `[]` to leave audio untouched.
|
|
145
|
+
|
|
146
|
+
That produces the `"desk"` profile in the example below.
|
|
147
|
+
|
|
148
|
+
### `profiles.json`
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"default_profiles": [
|
|
153
|
+
{
|
|
154
|
+
"connected_output_names": ["eDP-1"],
|
|
155
|
+
"profile_name": "laptop"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"connected_output_names": ["DP-1", "HDMI-1"],
|
|
159
|
+
"profile_name": "desk"
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"profiles": {
|
|
163
|
+
"laptop": {
|
|
164
|
+
"pactl_sink_option_regexes": ["alsa_output\\..*pci.*analog.*"],
|
|
165
|
+
"outputs": {
|
|
166
|
+
"eDP-1": {
|
|
167
|
+
"configuration": {
|
|
168
|
+
"mode": {
|
|
169
|
+
"resolution": {"width": 1920, "height": 1080},
|
|
170
|
+
"refresh_rate": "60.00"
|
|
171
|
+
},
|
|
172
|
+
"position": {"x": 0, "y": 0}
|
|
173
|
+
},
|
|
174
|
+
"primary_candidate": true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"desk": {
|
|
179
|
+
"pactl_sink_option_regexes": ["alsa_output\\..*hdmi.*"],
|
|
180
|
+
"outputs": {
|
|
181
|
+
"DP-1": {
|
|
182
|
+
"configuration": {
|
|
183
|
+
"mode": {
|
|
184
|
+
"resolution": {"width": 2560, "height": 1440},
|
|
185
|
+
"refresh_rate": "144.00"
|
|
186
|
+
},
|
|
187
|
+
"position": {"x": 0, "y": 0}
|
|
188
|
+
},
|
|
189
|
+
"primary_candidate": true
|
|
190
|
+
},
|
|
191
|
+
"HDMI-1": {
|
|
192
|
+
"configuration": {
|
|
193
|
+
"mode": {
|
|
194
|
+
"resolution": {"width": 1920, "height": 1080},
|
|
195
|
+
"refresh_rate": "60.00"
|
|
196
|
+
},
|
|
197
|
+
"position": {"x": 2560, "y": 0}
|
|
198
|
+
},
|
|
199
|
+
"primary_candidate": false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Field reference
|
|
208
|
+
|
|
209
|
+
- **`profiles`** — a map of profile name → profile. Applying a profile (via
|
|
210
|
+
`--profile`, `--default-profile`, or `--cycle-profile`) enables exactly the
|
|
211
|
+
listed `outputs`; any connected output *not* listed is turned off.
|
|
212
|
+
- **`outputs`** — map of output name → output state. Required.
|
|
213
|
+
- **`configuration.mode.resolution`** — `{"width", "height"}` integers.
|
|
214
|
+
- **`configuration.mode.refresh_rate`** — refresh rate as a *string*
|
|
215
|
+
(e.g. `"144.00"`). Required; use a value shown by `--state`.
|
|
216
|
+
- **`configuration.position`** — `{"x", "y"}` integers, the output's
|
|
217
|
+
top-left corner in the combined desktop.
|
|
218
|
+
- **`primary_candidate`** — optional, defaults to `false`. Outputs set to
|
|
219
|
+
`true` are eligible to be primary and are the set `--cycle-primary`
|
|
220
|
+
rotates through; the first one listed becomes primary on apply.
|
|
221
|
+
- **`pactl_sink_option_regexes`** — ordered list of regexes matched against
|
|
222
|
+
sink names from `--list-sinks`. On apply, the first pattern that matches a
|
|
223
|
+
present sink becomes the default. Required key; use `[]` to leave audio
|
|
224
|
+
alone.
|
|
225
|
+
- **`default_profiles`** — optional list of rules for `--default-profile`. A
|
|
226
|
+
rule (`connected_output_names` + `profile_name`) matches when all of its
|
|
227
|
+
outputs are currently connected; the matching rule listing the most outputs
|
|
228
|
+
wins. This is what picks the right profile automatically (e.g. at login, or
|
|
229
|
+
when you dock/undock).
|
|
230
|
+
|
|
231
|
+
### Hooks (optional)
|
|
232
|
+
|
|
233
|
+
If present and executable in the config directory, these scripts are run on
|
|
234
|
+
changes:
|
|
235
|
+
|
|
236
|
+
- `on-profile-change` — run after a profile is applied, with the profile name
|
|
237
|
+
as `$1` (after waiting briefly for the WM to settle, detected via the root
|
|
238
|
+
window pixmap).
|
|
239
|
+
- `on-primary-output-change` — run when the primary output changes, with the
|
|
240
|
+
new output name as `$1`.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
[Unlicense](LICENSE) — public domain.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling (>=1.29.0,<2.0.0)", "hatch-vcs (>=0.5.0,<1.0.0)"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "av-output-switcher"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Cycles audio/video output profiles (xrandr + pactl) on Xorg."
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Jacob McIntosh", email = "nacitar.sevaht@gmail.com"}
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "Unlicense"
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
keywords = [
|
|
16
|
+
"xrandr", "pactl", "pulseaudio", "audio", "video", "display",
|
|
17
|
+
"monitor", "output", "xorg", "fluxbox", "cli",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: End Users/Desktop",
|
|
22
|
+
"Operating System :: POSIX :: Linux",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
29
|
+
"Topic :: Multimedia :: Video :: Display",
|
|
30
|
+
"Topic :: Utilities",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"sevaht-utility>=1.0.0",
|
|
35
|
+
"platformdirs>=4.10.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
av-output-switcher = "av_output_switcher.__main__:main"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"black (>=26.3.1,<27.0.0)",
|
|
44
|
+
"build (>=1.4.2,<2.0.0)",
|
|
45
|
+
"mypy (>=1.20.0,<2.0.0)",
|
|
46
|
+
"pytest (>=9.0.2,<10.0.0)",
|
|
47
|
+
"ruff (>=0.15.8,<0.16.0)",
|
|
48
|
+
"twine (>=6.2.0,<7.0.0)",
|
|
49
|
+
"vulture (>=2.14,<3.0.0)",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/sevaht/av-output-switcher"
|
|
54
|
+
Source = "https://github.com/sevaht/av-output-switcher"
|
|
55
|
+
Issues = "https://github.com/sevaht/av-output-switcher/issues"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.version]
|
|
58
|
+
source = "vcs"
|
|
59
|
+
|
|
60
|
+
[tool.hatch.version.raw-options]
|
|
61
|
+
fallback_version = "0.0.0"
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/av_output_switcher"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = ["/src", "/tests", "/LICENSE"]
|
|
68
|
+
|
|
69
|
+
[tool.black]
|
|
70
|
+
line-length = 79
|
|
71
|
+
skip-magic-trailing-comma = true
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
strict = true
|
|
75
|
+
exclude = ["^docs/", "^dist/"]
|
|
76
|
+
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
testpaths = ["tests"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
line-length = 79
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
extend-ignore = ["COM812", "COM819", "E203", "E501", "TD003"]
|
|
85
|
+
select = [
|
|
86
|
+
"A", "ANN", "ARG", "ASYNC", "B", "BLE", "C4", "C90", "COM",
|
|
87
|
+
"DTZ", "E", "EM", "ERA", "F", "FA", "FLY", "FURB", "I",
|
|
88
|
+
"ISC", "LOG", "N", "PERF", "PGH", "PIE", "PLR", "PLW", "PTH",
|
|
89
|
+
"Q", "RET", "RSE", "RUF", "S", "SIM", "TC", "TCH", "TD",
|
|
90
|
+
"TID", "TRY", "UP"
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint.per-file-ignores]
|
|
94
|
+
"checks" = ["S603"]
|
|
95
|
+
"tests/**/*" = ["PLR2004", "S101"]
|
|
96
|
+
|
|
97
|
+
[tool.vulture]
|
|
98
|
+
min_confidence = 70
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Cycles audio/video output profiles (xrandr + pactl) on Xorg.
|
|
2
|
+
|
|
3
|
+
Attributes:
|
|
4
|
+
__version__: The installed distribution version.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.metadata
|
|
10
|
+
from functools import cache
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from platformdirs import PlatformDirs
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
__version__ = importlib.metadata.version(__package__)
|
|
19
|
+
|
|
20
|
+
APP_NAME = "av-output-switcher"
|
|
21
|
+
APP_AUTHOR = "sevaht"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cache
|
|
25
|
+
def user_config_path() -> Path:
|
|
26
|
+
path = PlatformDirs(APP_NAME, appauthor=APP_AUTHOR).user_config_path
|
|
27
|
+
# platformdirs omits appauthor from the path on non-Windows platforms;
|
|
28
|
+
# insert it so all platforms use <author>/<appname>.
|
|
29
|
+
if path.parent.name != APP_AUTHOR:
|
|
30
|
+
path = path.parent / APP_AUTHOR / APP_NAME
|
|
31
|
+
return path
|