wren-linux / bin / display-scale
#!/bin/sh
# display-scale: text too small on a sharp screen? Sets the scale of each
# screen and remembers it for that particular screen, on whichever port it
# is plugged in. Written by Wren Linux install-base.sh. Run it inside sway.
#
# display-scale asks which screen and which scale
# display-scale list the screens, their sharpness and scale
# display-scale set SCREEN N e.g. display-scale set eDP-1 1.5 (SCREEN
# can be "all"; N from 0.5 to 4)
# display-scale reset back to 1 for every screen, forgets the settings
# display-scale watch sway starts this: it says so once, in a
# notification, when a sharp screen is at scale 1
#
# Settings are in ~/.config/sway/config.d/display.conf, one line per screen.
# sway doesn't say how big a screen is, so its size comes from the screen's
# own data (EDID) in /sys/class/drm; without it the sharpness is unknown. A
# virtual machine's screen (Virtual-1) has a size the VM made up, so it is
# left unknown too: no "sharp screen" notice from a VM.
DRM=${WREN_DRM_DIR:-/sys/class/drm}
CONF=$HOME/.config/sway/config.d/display.conf
SEEN=${XDG_STATE_HOME:-$HOME/.local/state}/wren/display-seen
SHARP=150 # pixels per inch from which text at scale 1 looks small
die() { echo "display-scale: $*" >&2; exit 1; }
need_sway() {
swaymsg -t get_version >/dev/null 2>&1 ||
die "sway isn't running here. Run this inside sway (Super+Enter opens a terminal)."
}
# edid_mm NAME: "WIDTH HEIGHT" of that screen in millimetres, or nothing
edid_mm() {
for d in "$DRM"/card*-"$1"; do
[ -r "$d/edid" ] || continue
od -An -v -tu1 -N128 "$d/edid" 2>/dev/null | awk '
{ for (i = 1; i <= NF; i++) b[n++] = $i }
END {
if (n < 128 || b[0] != 0 || b[1] != 255 || b[7] != 0) exit
# the first detailed timing has the image size in mm: the low 8
# bits of each in bytes 66 and 67, the high 4 in byte 68
w = b[66] + int(b[68] / 16) * 256; h = b[67] + (b[68] % 16) * 256
if (b[54] + b[55] == 0 || w == 0 || h == 0) { w = b[21] * 10; h = b[22] * 10 }
if (w > 0 && h > 0) print w, h
}'
return
done
}
# screens: one line per active screen, tab separated:
# NAME ID WIDTHxHEIGHT SCALE MM-WIDTH MM-HEIGHT PPI SUGGESTED-SCALE
# (a "-" where something isn't known)
screens() {
swaymsg -p -t get_outputs | awk -v q="'" '
function emit() { if (name != "" && mode != "") print name "\t" id "\t" mode "\t" scale }
/^Output / {
emit(); name = $2; mode = ""; scale = "1"
i = index($0, q); j = length($0)
while (j > i && substr($0, j, 1) != q) j--
id = substr($0, i + 1, j - i - 1)
}
/Current mode:/ { mode = $3 }
/Scale factor:/ { scale = $3 + 0 }
END { emit() }' |
while IFS=' ' read -r name id mode scale; do
# a VM's screen (QEMU names it Virtual-1): the size in its EDID is made up
# shellcheck disable=SC2046 # the two numbers are meant to split
case $name in
Virtual-*) set -- ;;
*) set -- $(edid_mm "$name") ;;
esac
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$id" "$mode" "$scale" "${1:--}" "${2:--}"
done |
awk -F'\t' -v sharp="$SHARP" '{
ppi = "-"; sug = "-"
split($3, wh, "x")
if ($5 + 0 > 0) {
p = wh[1] / ($5 / 25.4); ppi = int(p + 0.5)
s = (p < sharp) ? 1 : int(p / 110 * 4 + 0.5) / 4
if (s < 1) s = 1; if (s > 3) s = 3
sug = s
}
print $0 "\t" ppi "\t" sug
}'
}
# key NAME ID: what the config line uses to name that screen. A screen that
# gives no make or model shares its id with every other one, so it is named
# by its port instead
key() {
case $2 in
*Unknown*) echo "$1" ;;
*) echo "$2" ;;
esac
}
list() {
n=0
screens | while IFS=' ' read -r name id mode scale mmw mmh ppi sug; do
n=$((n + 1))
if [ "$ppi" = - ]; then
case $name in Virtual-*) sharp="a VM's screen, no real size" ;; *) sharp="size unknown" ;; esac
else
sharp="${mmw}x${mmh} mm, $ppi pixels per inch"
fi
printf ' %s) %-9s %-10s %s, scale %s' "$n" "$name" "$mode" "$sharp" "$scale"
[ "$sug" = - ] || printf ', suggested %s' "$sug"
printf '\n %s\n' "$id"
done
}
# write_conf KEY SCALE: that screen's line in display.conf, checked with sway
write_conf() {
mkdir -p "${CONF%/*}"
old=$(mktemp)
new=$(mktemp)
if [ -f "$CONF" ]; then cp "$CONF" "$old"; else : >"$old"; fi
{
[ -s "$old" ] || echo "# Screen scales, written by Wren Linux display-scale (display-scale list shows them)"
awk -v k="output \"$1\" scale " 'index($0, k) != 1' "$old"
echo "output \"$1\" scale $2"
} >"$new"
cp "$new" "$CONF"
out=$(WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_LIBINPUT_NO_DEVICES=1 \
sway -C -c "$HOME/.config/sway/config" 2>&1 || true)
if printf '%s\n' "$out" | grep -q '\[ERROR\]'; then
cp "$old" "$CONF"
rm -f "$old" "$new"
die "sway rejected the setting, so it was not saved: $(printf '%s\n' "$out" | grep -m1 '\[ERROR\]')"
fi
rm -f "$old" "$new"
}
valid_scale() {
awk -v s="$1" 'BEGIN { exit !(s ~ /^[0-9]+(\.[0-9]+)?$/ && s + 0 >= 0.5 && s + 0 <= 4) }'
}
# set_screens SCREEN SCALE
set_screens() {
valid_scale "$2" || die "'$2' isn't a scale between 0.5 and 4 (1 is normal size, 2 is double)."
tmp=$(mktemp)
screens >"$tmp"
found=0
while IFS=' ' read -r name id mode scale mmw mmh ppi sug; do
[ "$1" = all ] || [ "$1" = "$name" ] || continue
found=1
write_conf "$(key "$name" "$id")" "$2"
swaymsg output "$name" scale "$2" >/dev/null
echo "$name: scale $2"
done <"$tmp"
rm -f "$tmp"
[ "$found" = 1 ] || die "No screen called '$1'. display-scale list shows the names."
}
reset() {
rm -f "$CONF"
for line in $(screens | cut -f1); do swaymsg output "$line" scale 1 >/dev/null; done
echo "All screens are back at scale 1."
}
ask() {
need_sway
echo "Screens:"
list
count=$(screens | wc -l)
[ "$count" -gt 0 ] || die "No screens found."
if [ "$count" -eq 1 ]; then
pick=1
else
printf 'Which one (number, or all)? '
read -r pick || exit 1
fi
if [ "$pick" = all ]; then
target=all
sug=
else
row=$(screens | sed -n "${pick}p")
[ -n "$row" ] || die "No screen number '$pick'."
target=$(printf '%s' "$row" | cut -f1)
sug=$(printf '%s' "$row" | cut -f8)
[ "$sug" != - ] || sug=
fi
printf 'Scale (1 normal, 1.5, 2 ... bigger text)%s: ' "${sug:+, Enter for $sug}"
read -r scale || exit 1
scale=${scale:-$sug}
[ -n "$scale" ] || die "No scale given."
set_screens "$target" "$scale"
echo "Saved in $CONF: it applies whenever that screen is plugged in."
}
# Says once, per screen, that a sharp screen is at scale 1
check() {
mkdir -p "${SEEN%/*}"
touch "$SEEN"
screens | while IFS=' ' read -r name id mode scale mmw mmh ppi sug; do
[ "$ppi" != - ] || continue
awk -v p="$ppi" -v s="$scale" -v sharp="$SHARP" 'BEGIN { exit !(p >= sharp && s <= 1.01) }' || continue
k=$(key "$name" "$id")
grep -qxF "$k" "$SEEN" && continue
echo "$k" >>"$SEEN"
notify-send -a wren -t 20000 "Sharp screen: $name" \
"$ppi pixels per inch: text may look small. Type display-scale to make it bigger (suggested $sug)."
done
}
watch() {
command -v notify-send >/dev/null || exit 0
sleep 2
check
# one event whenever a screen is plugged in, unplugged or changed
swaymsg -t subscribe -m '["output"]' | while read -r _; do
sleep 1
check
done
}
case ${1:-} in
"") ask ;;
list) need_sway; list ;;
set) need_sway; [ $# -eq 3 ] || die "usage: display-scale set SCREEN SCALE"; set_screens "$2" "$3" ;;
reset) need_sway; reset ;;
watch) need_sway; watch ;;
*) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
esac