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.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ .ipynb_checkpoints/
3
+ .mypy_cache/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .venv/
7
+ build/
8
+ dist/
9
+ docs/_build/
10
+ .claude/settings.local.json
@@ -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
@@ -0,0 +1,6 @@
1
+ from sevaht_utility.log_utility import log_exceptions
2
+
3
+ from .application import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(log_exceptions()(main)())