Switching between High DPI and Low DPI in XMonad
Posted on Wed 05 March 2025 in xmonad
Context¶
I enjoying using a tiling window manager on Linux. However it is hard to get them to work with my multiple monitor requirements.
My situation can be described as follows: 1. I have multiple monitors, not all of them have the same DPI. 2. Not all of them are connected at the same time as I use a laptop. Sometimes I no monitors, sometimes three. And I like to plug them while the session is open. 3. Whenever possible I would like to have the highest DPI setting possible.
DPI means dots per inch and loosely speaking refers to resolution. High DPI monitors have such a high resolution they require bigger fonts and upscaling.
In general on Linux, Wayland based window managers and desktop environmenti, like Gnome or Hyprland handle the best high DPI settings. They support fractional scaling (for when monitors are halfway between low and high DPI). They work nicely with mixed setups like described above.
However I am not yet satisfied with either Gnome (which isn’t a dynamic tiling window manager), or Hyprland (which seems to have a static approach to multiple monitors). And I haven’t tried Sway yet. In the future I planned to revisit Sway and Hyprland but in the meantime I wanted to get XMonad to work well because I like how XMonad handle multiple monitors.
Finding out current DPI¶
I wrote a simple script using xdpyinfo
to fetch information
about the current DPI:
TODO: This script is broken (fix this)
#!/usr/bin/env bash
xdpyinfo | grep dots | tr -s ' ' | cut -d ' ' -f 3 | cut -d 'x' -f 1
This will return a number giving the current DPI setting. The purpose of this script to allow XMonad to tell the DPI setting and adapt accordingly. In my case I use it to tell which XMobar config to load (either a low DPI setting or a high DPI setting).
Creating configs for each DPI setting¶
The main trick I use for multiple DPI setting is having multiple
config files for each DPI config. The files I differentiate are:
* .xsession
files for loading Xft settings
* xmobarrc
files for the XMobar status bar (with different dpi settings)
So for instance here is the two x session files
File | Low DPI | High DPI |
---|---|---|
X Session file |
|
|
XMobar config |
|
|
The main notable thing is that the font dpi changes in both case. The rest is not important in this context.
Toggling between DPI settings¶
To switch between the two DPI a second script is created :
#!/usr/bin/env bash
DPI=$(~/.local/bin/what-dpi.sh)
if [ "$DPI" -eq "192" ]; then
echo "Switching to low DPI (96)"
xrdb -merge ~/.config/xsession/xsession.lowdpi
xrandr -s 1920x1080
else
echo "Switching to high DPI (192)"
xrdb -merge ~/.config/xsession/xsession.hidpi
xrandr -s 3840x2160
fi
This script picks up the current DPI setting and applies the new setting matching the opposite DPI setting. It does three things : 1. Change the Xft settings for fonts 2. Change the screen resolution
Incorporating in XMonad¶
Now we incorporate the logic in XMonad. First I define a data type (to follow good programming practices)
data DPI = HiDPI | LowDPI
I create a status bar hook that takes a DPI as an input (and monitor number) and creates the appropriate instances of xmobar:
import XMonad.Hooks.StatusBar
import XMonad.Hooks.StatusBar.PP
mySB num dpi = statusBarProp ( "xmobar -x " ++ show num ++ dpiConf dpi) (pure myXmobarPP)
where dpiConf LowDPI = " ~/.config/xmobar/xmobarrc.lowdpi"
dpiConf HiDPI = " ~/.config/xmobar/xmobarrc.hidpi"
myXmobarPP :: PP
myXmobarPP = def
{ ppSep = " | "
, ppCurrent = xmobarColor "#7FBBB3" "" . wrap "[" "]"
, ppVisible = xmobarColor "#A7C080" "" . wrap "[" "]"
, ppHidden = xmobarColor "#E67E80" "" . wrap "(" ")"
, ppHiddenNoWindows = xmobarColor "#D3C6AA" "" . wrap " " " "
, ppTitle = xmobarColor "#D3C6AA" "" . shorten 60
, ppTitleSanitize = xmobarStrip
}
I then hook this into the main script
fetchDPI :: IO DPI
fetchDPI = do
(_, maybeOutH, _, _) <- createProcess shellCmd { std_out = CreatePipe }
maybe (return LowDPI) processOut maybeOutH
where shellCmd = shell "~/.local/bin/what-dpi.sh"
processOut :: Handle -> IO DPI
processOut h = do
output <- hGetLine h
let currDpi = readMaybe output :: Maybe Int
return $ maybe HiDPI intToDPIEnum currDpi
where intToDPIEnum x = if x == 192 then HiDPI else LowDPI
main :: IO ()
main = do
dpi <- fetchDPI
xmonadMain dpi
where xmonadMain dpi = xmonad
. ewmhFullscreen
. ewmh
. withEasySB (mySB 0 dpi) toggleStrutsKey
. withEasySB (mySB 1 dpi) toggleStrutsKey
$ myConfig dpi
where toggleStrutsKey :: XConfig Layout -> (KeyMask, KeySym)
toggleStrutsKey XConfig { modMask = myModMask } = (myModMask, xK_b)
So let’s explain what happens :
1. When XMonad launches it fetches the current DPI from the what-dpi.sh
script.
This part is done in fetchDPI
and it is basically a lot of boilerplate code
to start a process and return the DPI setting with lots of error off ramps to
default to a low DPI setting (in exotic cases it is safer to assume low DPI)
2. Launch the status bar using withEasySB multiple times as we may have multiple
monitors with the appropriate DPI setting.
Now this allows XMonad upon start to adapt but not when monitors are plugged in or plugged out. For this I create a keyboard shortcut as follows
myKeys =
[ ("M-v", toggleSmartSpacing)
, ("<XF86MonBrightnessUp>", spawn "brightnessctl s +10%")
, ("<XF86MonBrightnessDown>", spawn "brightnessctl s 10-%")
, ("<XF86AudioRaiseVolume>", raiseVolume 3 >> return ())
, ("<XF86AudioLowerVolume>", lowerVolume 3 >> return ())
, ("<XF86AudioMute>", toggleMute >> return ())
, ("C-<Print>", spawn "scrot -s")
, ("C-'", spawn "~/.local/bin/switch-dpi.sh; killall xmobar; xmonad --restart")
]
Here I add a keyboard shortcut Control-’ to request a DPI switch, kill xmobar and restart xmonad.
For more articles about Nix and NixOS the following RSS feed is available over here.