Metadata-Version: 2.4
Name: av-output-switcher
Version: 1.0.0
Summary: Cycles audio/video output profiles (xrandr + pactl) on Xorg.
Project-URL: Homepage, https://github.com/sevaht/av-output-switcher
Project-URL: Source, https://github.com/sevaht/av-output-switcher
Project-URL: Issues, https://github.com/sevaht/av-output-switcher/issues
Author-email: Jacob McIntosh <nacitar.sevaht@gmail.com>
License-Expression: Unlicense
License-File: LICENSE
Keywords: audio,cli,display,fluxbox,monitor,output,pactl,pulseaudio,video,xorg,xrandr
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Video :: Display
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: platformdirs>=4.10.0
Requires-Dist: sevaht-utility>=1.0.0
Description-Content-Type: text/markdown

# av-output-switcher

Cycles audio/video **output profiles** for lightweight Xorg window managers
(it's built for a Fluxbox-style setup driven by keybinds).

A *profile* couples two things under one name:

- an **xrandr display layout** — which connected outputs are on, their
  resolution/refresh-rate/position, and which may act as primary; and
- a preferred **PulseAudio sink** (matched by regex via `pactl`).

Applying a profile reconfigures the monitors *and* repoints default audio in
one shot. The `--cycle-*` commands are meant to be bound to keys: step through
valid profiles, flip the primary monitor, or rotate the default audio sink.

> **Platform:** Xorg only. It shells out to `xrandr` and `xprop` (X11) and
> `pactl` (PulseAudio/PipeWire's pulse shim). It does not work on Wayland.

## Installation

```console
$ uv tool install av-output-switcher
```

At runtime it shells out to these external commands, which must be on `PATH`:

- `xrandr` — query and apply the display layout (X11)
- `xprop` — detect when the WM has settled after a layout change (X11)
- `pactl` — list sinks and switch the default audio output
  (PulseAudio, or PipeWire's pulse shim)

On ArchLinux these are the `xorg-xrandr`, `xorg-xprop`, and `libpulse`
packages.

## Usage

Exactly one action per invocation:

```console
$ av-output-switcher --state              # show outputs, modes, positions
$ av-output-switcher --list-sinks         # show audio sink names
$ av-output-switcher --list               # list configured profile names
$ av-output-switcher --get-current-profile
$ av-output-switcher --primary-resolution # e.g. 2560x1440

$ av-output-switcher --profile NAME       # apply a specific profile
$ av-output-switcher --default-profile    # apply the best profile for what's
                                          #   currently connected
$ av-output-switcher --cycle-profile      # advance to the next valid profile
$ av-output-switcher --cycle-primary      # rotate the primary output
$ av-output-switcher --cycle-pactl-sink   # rotate the default audio sink
```

Logging options (from `sevaht-utility`): `-v/--verbose`, `-q/--quiet`,
`--debug`, and `--log-file FILE`.

### Example keybinds (Fluxbox `~/.fluxbox/keys`)

```
Mod4 p :Exec av-output-switcher --cycle-profile
Mod4 o :Exec av-output-switcher --cycle-primary
Mod4 a :Exec av-output-switcher --cycle-pactl-sink
```

## Configuration

Configuration lives in the platform user-config directory under
`sevaht/av-output-switcher/`:

- Linux: `~/.config/sevaht/av-output-switcher/`

Create that directory and a `profiles.json` inside it. The rest of this
section walks through writing one from scratch.

### Finding the values to put in a profile

You should never need to run `xrandr` or `pactl` yourself — the tool reports
every value a profile needs. These two commands work before any config exists.

**Outputs, resolutions, refresh rates, positions, and the current primary**
come from `--state`:

```console
$ av-output-switcher --state
Screen 0: current 4480x1440
DP-1 connected primary 2560x1440+0+0 = [DEL] DELL S2721DGF (ABC123)
   2560x1440	144.00*+	120.00 	60.00
   1920x1080	144.00 	60.00
HDMI-1 connected 1920x1080+2560+0 = [SAM] Samsung
   1920x1080	60.00*+	50.00
   1280x720	60.00
eDP-1 disconnected
```

How to read it:

- The first token on each non-indented line is the **output name** (`DP-1`,
  `HDMI-1`, `eDP-1`) — these are the keys under `outputs` and the values for
  `connected_output_names`. Only `connected` outputs can be enabled.
- `primary` marks the output currently acting as primary.
- `2560x1440+0+0` is the current `WIDTHxHEIGHT+X+Y`: it gives both the
  **resolution** (`width`/`height`) and the **position** (`x`/`y`).
- The indented lines list each supported **resolution** followed by its
  available **refresh rates**. `*` marks the rate currently in use and `+`
  marks the monitor's preferred mode, so `2560x1440  144.00*+` means
  2560×1440 @ 144.00 Hz is both active and preferred. Pick a width/height and
  a refresh rate that appear together here.

**Audio sink names** come from `--list-sinks` (the `*` is the current
default):

```console
$ av-output-switcher --list-sinks
* alsa_output.pci-0000_00_1f.3.analog-stereo
  alsa_output.pci-0000_01_00.1.hdmi-stereo
  alsa_output.usb-Generic_USB_Audio-00.analog-stereo
```

A profile selects audio with regexes matched against these names, so you can
either paste a full name or match a stable fragment (e.g. `.*hdmi.*`). Only
ALSA sinks are listed, since those are the only ones the tool can switch to.

### Building a profile, step by step

Say `--state` shows `DP-1` (2560×1440 @144) on the left and `HDMI-1`
(1920×1080 @60) to its right, and you want sound to come out over HDMI:

1. **Name the profile** — add a key under `profiles` (e.g. `"desk"`).
2. **Add each output you want on** under `outputs`, keyed by its name from
   `--state`. Any connected output you *omit* is turned off when the profile
   is applied.
3. **Fill `resolution` and `refresh_rate`** from a `*`/`+` line in `--state`.
   `refresh_rate` is a string to preserve the exact decimal (e.g. `"144.00"`).
4. **Set `position`** — the top-left pixel coordinate of that output in the
   combined desktop. Put the leftmost output at `{"x": 0, "y": 0}`, then place
   the next one to its right at `x` = the sum of the widths to its left
   (`2560` here), `y` = 0 for a single top-aligned row. Stack vertically with
   `y` instead.
5. **Mark `primary_candidate: true`** on outputs allowed to be primary (the
   first such output becomes primary when the profile is applied, and these
   are the outputs `--cycle-primary` rotates through).
6. **Set `pactl_sink_option_regexes`** to an ordered list of patterns from
   `--list-sinks`; the first one that matches a present sink becomes the
   default. Use `[]` to leave audio untouched.

That produces the `"desk"` profile in the example below.

### `profiles.json`

```json
{
    "default_profiles": [
        {
            "connected_output_names": ["eDP-1"],
            "profile_name": "laptop"
        },
        {
            "connected_output_names": ["DP-1", "HDMI-1"],
            "profile_name": "desk"
        }
    ],
    "profiles": {
        "laptop": {
            "pactl_sink_option_regexes": ["alsa_output\\..*pci.*analog.*"],
            "outputs": {
                "eDP-1": {
                    "configuration": {
                        "mode": {
                            "resolution": {"width": 1920, "height": 1080},
                            "refresh_rate": "60.00"
                        },
                        "position": {"x": 0, "y": 0}
                    },
                    "primary_candidate": true
                }
            }
        },
        "desk": {
            "pactl_sink_option_regexes": ["alsa_output\\..*hdmi.*"],
            "outputs": {
                "DP-1": {
                    "configuration": {
                        "mode": {
                            "resolution": {"width": 2560, "height": 1440},
                            "refresh_rate": "144.00"
                        },
                        "position": {"x": 0, "y": 0}
                    },
                    "primary_candidate": true
                },
                "HDMI-1": {
                    "configuration": {
                        "mode": {
                            "resolution": {"width": 1920, "height": 1080},
                            "refresh_rate": "60.00"
                        },
                        "position": {"x": 2560, "y": 0}
                    },
                    "primary_candidate": false
                }
            }
        }
    }
}
```

### Field reference

- **`profiles`** — a map of profile name → profile. Applying a profile (via
  `--profile`, `--default-profile`, or `--cycle-profile`) enables exactly the
  listed `outputs`; any connected output *not* listed is turned off.
  - **`outputs`** — map of output name → output state. Required.
    - **`configuration.mode.resolution`** — `{"width", "height"}` integers.
    - **`configuration.mode.refresh_rate`** — refresh rate as a *string*
      (e.g. `"144.00"`). Required; use a value shown by `--state`.
    - **`configuration.position`** — `{"x", "y"}` integers, the output's
      top-left corner in the combined desktop.
    - **`primary_candidate`** — optional, defaults to `false`. Outputs set to
      `true` are eligible to be primary and are the set `--cycle-primary`
      rotates through; the first one listed becomes primary on apply.
  - **`pactl_sink_option_regexes`** — ordered list of regexes matched against
    sink names from `--list-sinks`. On apply, the first pattern that matches a
    present sink becomes the default. Required key; use `[]` to leave audio
    alone.
- **`default_profiles`** — optional list of rules for `--default-profile`. A
  rule (`connected_output_names` + `profile_name`) matches when all of its
  outputs are currently connected; the matching rule listing the most outputs
  wins. This is what picks the right profile automatically (e.g. at login, or
  when you dock/undock).

### Hooks (optional)

If present and executable in the config directory, these scripts are run on
changes:

- `on-profile-change` — run after a profile is applied, with the profile name
  as `$1` (after waiting briefly for the WM to settle, detected via the root
  window pixmap).
- `on-primary-output-change` — run when the primary output changes, with the
  new output name as `$1`.

## License

[Unlicense](LICENSE) — public domain.
