Linux 07 Wrzesień 2026

Instant monitor switching in KDE (`kscreen-doctor` + aliases)

Also in:

The situation at my desk looks like this: a laptop plugged into a docking station, two external monitors attached to it, plus the laptop's own screen — three in total. The very same monitors are also connected to a second, desktop computer. Sometimes I need to work on both at once, so I hand one monitor over to the PC (with the input-select button) and the laptop has to adapt. The keyboard and mouse are paired with both machines, so there a single button is enough.

One bottleneck remained: clicking through Plasma's Display Settings. Open settings, disable a monitor, drag the rectangles around, apply, confirm. A dozen seconds and a bit of irritation, several times a day. Now I open a terminal, type d_major, and that is it.

What it gives you

Four aliases covering every layout I use:

Alias Effect
d_set Full layout — all three screens for the laptop.
d_major The ultrawide stays with the laptop, the side monitor goes to the PC.
d_minor The other way round — the ultrawide goes to the PC, the laptop gets the side monitor.
d_disable Both externals off, only the laptop screen remains.

The crucial part is that once a monitor is handed over to the other computer, the laptop has to stop seeing it. Otherwise windows keep opening on a screen I cannot see, the cursor wanders off into the void, and the Plasma panel stays on a monitor that is no longer there. Switching the monitor's input does not take care of this — the laptop still keeps an active desktop over there.

Why kscreen-doctor, not xrandr

The reflex is to reach for xrandr, but that is a dead end: xrandr only works on X11, and Plasma 6 starts on Wayland by default. Under Wayland xrandr sees the XWayland layer at best and will not move physical screens around.

kscreen-doctor, on the other hand, is the official CLI tool from the KScreen library — it talks to KWin directly and works the same on X11 and on Wayland. It ships in the libkscreen-bin package, so in practice you already have it installed together with Plasma.

Its second advantage matters more than it seems: it applies the configuration atomically. All the settings passed in a single invocation take effect together, in one recalculation. Doing them one by one would produce intermediate states with overlapping or misaligned screens, and Plasma would shuffle the windows at every step.

Get to know your outputs

The starting point is always the same:

kscreen-doctor -o

You get a list of outputs with their name, status, mode list, and current geometry. Mine, abridged:

Output: 1 eDP-1     enabled connected priority 1  Panel        Geometry: 1520,1440 1920x1200
Output: 2 DP-5      enabled connected priority 2  DisplayPort  Geometry: 3440,0    1920x1080
Output: 3 DP-6      enabled connected priority 3  DisplayPort  Geometry: 0,0       3440x1440

Three things worth picking out of that:

  • the output name (eDP-1, DP-5, DP-6) — this is how you address a screen in commands,
  • Geometry — the position of the top-left corner plus the resolution,
  • the Modes list — the exact resolutions and refresh rates a given screen will accept.

There is also kscreen-doctor -j, which returns the same thing as JSON — that will come in handy at the end.

The aliases

The whole setup fits into a dozen-odd lines in ~/.bash_aliases:

# DP-6 = ultrawide 3440x1440@100, DP-5 = side 1920x1080@60, eDP-1 = laptop panel 1920x1200@60
alias d_set='kscreen-doctor \
output.DP-6.enable output.DP-6.position.0,0 output.DP-6.mode.3440x1440@100 \
output.DP-5.enable output.DP-5.position.3440,0 output.DP-5.mode.1920x1080@60 \
output.eDP-1.enable output.eDP-1.position.1520,1440 output.eDP-1.mode.1920x1200@60'

alias d_major='kscreen-doctor \
output.DP-6.enable output.DP-6.position.0,0 output.DP-6.mode.3440x1440@100 \
output.DP-5.disable \
output.eDP-1.enable output.eDP-1.position.1520,1440 output.eDP-1.mode.1920x1200@60'

alias d_minor='kscreen-doctor \
output.DP-6.disable \
output.DP-5.enable output.DP-5.position.0,0 output.DP-5.mode.1920x1080@60 \
output.eDP-1.enable output.eDP-1.position.0,1080 output.eDP-1.mode.1920x1200@60'

alias d_disable='kscreen-doctor output.DP-6.disable output.DP-5.disable'

alias d_off="echo 'Blanking the screen in 2 s.'; sleep 2; kscreen-doctor --dpms off"

After editing, a source ~/.bash_aliases or a fresh terminal is all you need.

How it works

The syntax is uniform: output.<name>.<setting>[.<value>], and you pass as many settings as needed in a single invocation.

Positions live in one shared space

position.X,Y are the coordinates of the screen's top-left corner in the shared coordinate space of all monitors. The 0,0 origin is arbitrary — it is simply the corner of whichever screen you place first. My default layout looks like this:

x=0                               x=3440         x=5360
+---------------------------------+--------------+  y=0
|                                 | DP-5         |
|  DP-6   3440x1440 @100          | 1920x1080    |
|  (ultrawide)                    | @60          |
|                                 +--------------+  y=1080
|                                 |
+----------------+----------------+                 y=1440
                 | eDP-1          |
                 | 1920x1200 @60  |
                 | (laptop)       |
                 +----------------+                 y=2640
                x=1520        x=3440

The laptop physically sits below the ultrawide, hence y=1440 — exactly the height of the screen above it. x=1520 aligns it with the ultrawide's right edge (1520 + 1920 = 3440), because that is how it stands on my desk; if you wanted it centred, that would be (3440 − 1920) / 2 = 760. It is worth computing these values deliberately: if you leave a gap between screens, the cursor will stop at its edge.

That was, incidentally, a genuine bug in my older aliases. In d_minor I disabled the ultrawide, but the side monitor stayed at position 3440,0 — that is, in the void left by a screen that is no longer there. Hence in the version above the side monitor lands at 0,0, with the laptop below it.

Modes accept rounded refresh rates

mode.3440x1440@100 works even though -o reports that mode as 3440x1440@99.98. kscreen-doctor matches modes by the rounded frequency, so there is no need to copy fractions. If a mode is not found, the command simply reports an error and changes nothing — atomicity works in your favour on typos too.

enable has to be stated explicitly

Setting the position and mode alone will not turn a disabled screen back on. That is why every variant carries an explicit output.<name>.enable — including for the laptop panel. Without it, after d_disable (or after manually turning the panel off while docked) the aliases would be unable to bring it back.

Blanking

kscreen-doctor --dpms off blanks the screens until the first mouse movement. Note: --dpms only works on Wayland. On X11 you have to do it the old way, with xset dpms force off.

The trap: output names can change

This one caught me out. My aliases stopped working overnight because the monitors had changed names: DP-3-2 became DP-6, and DP-3-1-6 became DP-5.

An output name is the name of the connector as assigned by the graphics driver, not an identifier of the monitor. It can change after moving to a different docking station or a different port, after a kernel or driver update, and with monitors chained over DisplayPort MST (typical in docks) there is also the branch numbering of the chain — hence those multi-segment DP-3-1-6 names. The fix is trivial, but you have to know where to look: kscreen-doctor -o and rewrite the names.

If you would rather be immune to that, you can derive the name from the monitor's native resolution, which is considerably more stable than the connector name:

# Returns the name of the output whose native mode has the given resolution.
d_out() {
  kscreen-doctor -j | jq -r --arg res "$1" '
    .outputs[] | select(.connected) | . as $o
    | ($o.preferredModes[0] // $o.currentModeId) as $pm
    | select([$o.modes[] | select(.id == $pm) | "\(.size.width)x\(.size.height)"] | index($res))
    | $o.name' | head -1
}

The alias then turns into a function that resolves the names only at call time — and therefore survives a connector rename:

d_major() {
  local uw lcd side
  uw=$(d_out 3440x1440); lcd=$(d_out 1920x1200); side=$(d_out 1920x1080)
  [ -n "$uw" ] && [ -n "$lcd" ] || { echo "display: a screen is missing" >&2; return 1; }
  kscreen-doctor \
    output."$uw".enable output."$uw".position.0,0 output."$uw".mode.3440x1440@100 \
    ${side:+output."$side".disable} \
    output."$lcd".enable output."$lcd".position.1520,1440 output."$lcd".mode.1920x1200@60
}

The empty-name check is not decoration. Had d_out found nothing, without it the call would carry an output..enable argument and kscreen-doctor would reject the whole invocation with an unhelpful parse error. Better to say outright which screen is missing. The side monitor stays optional thanks to ${side:+...} — if it is not plugged in, there is simply nothing to disable.

A function cannot share its name with an alias

I walked into this trap while migrating from aliases to functions. If alias d_major=... stays in the file and you add a function of the same name below it, sourcing blows up somewhere that explains very little:

bash: .bashrc: line 136: syntax error near unexpected token `('
bash: .bashrc: line 136: `d_major() {'

The reason is that bash expands aliases before parsing. The d_major token gets replaced by the alias body, so the parser sees something like kscreen-doctor output... () { — and rightly objects at the parenthesis. The message points at the function, while the culprit is an alias dozens of lines higher up.

And here is the second layer of that trap, which I hit right afterwards. Deleting the aliases from the file is not enough, because aliases loaded by an earlier version of the file live on in the already-open shell. The file was clean by then, yet source ~/.bashrc in the same terminal still blew up — this time on d_set() {. A fresh terminal worked flawlessly, which is a misleading trait, because it suggests a problem with the file rather than with the session state.

That is why the cleanup line belongs in the file permanently, above the definitions, not just for the duration of the migration:

unalias d_set d_major d_minor d_disable d_off d_show 2>/dev/null

It works because bash reads and executes commands one by one while sourcing a file — the unalias gets to run before the parser reaches the function definitions below. A pleasant side effect is idempotence: the file can be sourced any number of times in the same session.

It is worth prefixing helper names with an underscore (_d_out) so they collide with nothing in the future.

One small curiosity to close with: the line number in the message can be off. The old aliases were multi-line (with trailing \), and expanding them injects those lines into the stream bash counts — in my case the error was reported three lines below where the function definition actually sat. Search by the name in the message, not by the number.

I deliberately compare only the native mode (preferredModes) rather than the whole mode list. Almost every monitor supports 1920x1080, so searching across any mode would hit whichever screen came first. The native resolution is, in practice, unique for each of my three screens. The price is that two identical models side by side still have to be told apart some other way — for example by sizeMM, or simply by the connector name.

Limitations (deliberate trade-offs)

  • Output names are not stable. The plain alias version is readable but needs a one-off fix after a dock or driver change. The d_out version is robust but less obvious to read.
  • Outside a graphical session you have to supply the environment. Run from cron, from a systemd unit, or from an agent tool, kscreen-doctor dies on a missing wl_display. It needs XDG_RUNTIME_DIR, WAYLAND_DISPLAY, and DBUS_SESSION_BUS_ADDRESS from a live session. In an ordinary terminal all of these are already set.
  • Some options are Wayland-only — --dpms and fractional scaling. On X11 you have to look for substitutes.
  • This does not replace KScreen profiles. Plasma remembers the layout per monitor set on its own and restores it when you dock. The aliases are for deliberate, manual switching within the same set — which is exactly where the automation has no way of guessing what I want.

Customization

  • Choosing the primary screen — output.DP-6.priority.1 moves the panel and default windows there; the remaining outputs get the subsequent numbers.
  • Scaling and rotation — output.DP-5.scale.1.5, output.DP-5.rotation.left. Handy with a monitor stood on its side.
  • HDR and brightness — output.DP-6.hdr.enable, output.DP-6.brightness.60.
  • Without a terminal — the same commands can be bound to keyboard shortcuts in System Settings → Shortcuts → Add Command. The screen layout then changes with a single keystroke.

kscreen-doctor --help gives the full list of settings with examples.

Summary

A dozen-odd lines in ~/.bash_aliases turn "open settings and drag rectangles around" into a single word in the terminal. When working on two computers that share the same monitors, that is the difference between a small ritual several times a day and something you stop thinking about. And kscreen-doctor is already on the disk of anyone running Plasma — kscreen-doctor -o is all you need to start.