detached 13ff6ac

fix(xwm): announce scale 1 to X11 clients while Xwayland runs at logical size

commit
13ff6ac624533c1ed5fb1f224ac1abcb7efa104a
author
brice · · 2026-10-22 17:01 UTC
committer
brice · · 2026-10-22 17:55 UTC
parent
f2f4074
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +69 -63
M docs/config.md +2 -2
@@ -28,7 +28,7 @@ See [examples/config](../examples/config) for every key with its default.
2828 | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) |
2929 | `startup` | none | Commands run at session start, separated by `;` |
3030 | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path |
31-| `xwayland.xsettings` | `on` | With `xwayland = builtin`, publish `Gtk/CursorThemeName`, `Gtk/CursorThemeSize` and `Xft/DPI` to X11 apps through XSETTINGS (restart required); `off` leaves them to the apps |
31+| `xwayland.xsettings` | `on` | With `xwayland = builtin`, publish `Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor` and `Gdk/UnscaledDPI` to X11 apps through XSETTINGS (restart required); the scale is always announced as 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows; `off` leaves them to the apps |
3232 | `background` | `#111111` | Background color |
3333 | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps |
3434 | `border.width` | `2` | Border width in pixels; `0` hides borders |
@@ -104,7 +104,7 @@ destination output, even if that output has no windows. Same-screen workspace
104104 switches and mouse focus do not trigger a flash.
105105 It runs when `animations = on`.
106106
107-`xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. X11 apps get the cursor theme and size from `XCURSOR_THEME` and `XCURSOR_SIZE`, the DPI of the first output (96 times its scale) through XSETTINGS (`xwayland.xsettings = off` turns it off), and the themed arrow on the X background. The clipboard and the primary selection are shared with X11 apps in both directions (text only; nothing crosses while the session is locked or otherwise protected).
107+`xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. X11 apps get the cursor theme and size from `XCURSOR_THEME` and `XCURSOR_SIZE`, a scale-1 DPI (96) through XSETTINGS (X11 apps see logical sizes; neferwl upscales their windows) (`xwayland.xsettings = off` turns it off), and the themed arrow on the X background. The clipboard and the primary selection are shared with X11 apps in both directions (text only; nothing crosses while the session is locked or otherwise protected).
108108
109109 ## Automatic terminal
110110
@@ -28,7 +28,7 @@ See [examples/config](../examples/config) for every key with its default.
28 | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) |28 | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) |
29 | `startup` | none | Commands run at session start, separated by `;` |29 | `startup` | none | Commands run at session start, separated by `;` |
30 | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path |30 | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path |
31-| `xwayland.xsettings` | `on` | With `xwayland = builtin`, publish `Gtk/CursorThemeName`, `Gtk/CursorThemeSize` and `Xft/DPI` to X11 apps through XSETTINGS (restart required); `off` leaves them to the apps |31+| `xwayland.xsettings` | `on` | With `xwayland = builtin`, publish `Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor` and `Gdk/UnscaledDPI` to X11 apps through XSETTINGS (restart required); the scale is always announced as 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows; `off` leaves them to the apps |
32 | `background` | `#111111` | Background color |32 | `background` | `#111111` | Background color |
33 | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps |33 | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps |
34 | `border.width` | `2` | Border width in pixels; `0` hides borders |34 | `border.width` | `2` | Border width in pixels; `0` hides borders |
@@ -104,7 +104,7 @@ destination output, even if that output has no windows. Same-screen workspace
104 switches and mouse focus do not trigger a flash.104 switches and mouse focus do not trigger a flash.
105 It runs when `animations = on`.105 It runs when `animations = on`.
106 106
107-`xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. X11 apps get the cursor theme and size from `XCURSOR_THEME` and `XCURSOR_SIZE`, the DPI of the first output (96 times its scale) through XSETTINGS (`xwayland.xsettings = off` turns it off), and the themed arrow on the X background. The clipboard and the primary selection are shared with X11 apps in both directions (text only; nothing crosses while the session is locked or otherwise protected).107+`xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. X11 apps get the cursor theme and size from `XCURSOR_THEME` and `XCURSOR_SIZE`, a scale-1 DPI (96) through XSETTINGS (X11 apps see logical sizes; neferwl upscales their windows) (`xwayland.xsettings = off` turns it off), and the themed arrow on the X background. The clipboard and the primary selection are shared with X11 apps in both directions (text only; nothing crosses while the session is locked or otherwise protected).
108 108
109 ## Automatic terminal109 ## Automatic terminal
110 110
M docs/desktop.md +1 -1
@@ -58,7 +58,7 @@ By default, X11 apps such as Steam run through [xwayland-satellite](https://gith
5858 xwayland = off # no X11 support
5959 ```
6060
61-With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. X11 apps get the cursor theme (`XCURSOR_THEME`), cursor size (`XCURSOR_SIZE`) and DPI (96 times the scale of the first output) through XSETTINGS (`Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`; `xwayland.xsettings = off` to disable), and the X background shows the themed arrow. The clipboard and the primary selection work between X11 and Wayland apps in both directions, for text; large copies are transferred in chunks (INCR). While the session is locked, no selection data crosses.
61+With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. X11 apps get the cursor theme (`XCURSOR_THEME`), cursor size (`XCURSOR_SIZE`) and DPI through XSETTINGS, always announced as scale 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows (`Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor`, `Gdk/UnscaledDPI`; `xwayland.xsettings = off` to disable), and the X background shows the themed arrow. The clipboard and the primary selection work between X11 and Wayland apps in both directions, for text; large copies are transferred in chunks (INCR). While the session is locked, no selection data crosses.
6262
6363 Games with `builtin`:
6464
@@ -58,7 +58,7 @@ By default, X11 apps such as Steam run through [xwayland-satellite](https://gith
58 xwayland = off # no X11 support58 xwayland = off # no X11 support
59 ```59 ```
60 60
61-With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. X11 apps get the cursor theme (`XCURSOR_THEME`), cursor size (`XCURSOR_SIZE`) and DPI (96 times the scale of the first output) through XSETTINGS (`Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`; `xwayland.xsettings = off` to disable), and the X background shows the themed arrow. The clipboard and the primary selection work between X11 and Wayland apps in both directions, for text; large copies are transferred in chunks (INCR). While the session is locked, no selection data crosses.61+With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. X11 apps get the cursor theme (`XCURSOR_THEME`), cursor size (`XCURSOR_SIZE`) and DPI through XSETTINGS, always announced as scale 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows (`Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor`, `Gdk/UnscaledDPI`; `xwayland.xsettings = off` to disable), and the X background shows the themed arrow. The clipboard and the primary selection work between X11 and Wayland apps in both directions, for text; large copies are transferred in chunks (INCR). While the session is locked, no selection data crosses.
62 62
63 Games with `builtin`:63 Games with `builtin`:
64 64
M docs/dev/headless.md +2 -2
@@ -66,7 +66,7 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本
6666
6767 `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. The clipboard test also needs `wl-copy` and `wl-paste` (wl-clipboard) and skips without them; they run with the headless `WAYLAND_DISPLAY` and need no extra focus step, since the focused probe window is enough for `wl-copy --foreground` to set the selection.
6868
69-The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`, `-at X,Y`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. `-fullscreen` (with `-fullscreen-delay`, `-fullscreen-off`) requests `_NET_WM_STATE_FULLSCREEN` by client message and prints `fullscreen WxH` / `fullscreen off WxH` when the window manager publishes the state; `-keyboard-grab` calls `XGrabKeyboard` and prints `grab`. Windowless modes: `-copy TEXT|@file` (with `-copy-sel CLIPBOARD|PRIMARY`, `-copy-for`) owns a selection and serves `TARGETS`, `UTF8_STRING`, `STRING` and `TEXT`, with INCR above 64 KiB, and prints `copy owned`; `-paste CLIPBOARD|PRIMARY` prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB, or `paste none`; `-xsettings` prints `xsettings serial=N Xft/DPI=… Gtk/CursorThemeName=… Gtk/CursorThemeSize=…` or `xsettings none`; `-root-cursor` prints `cursor WxH` from XFixes `GetCursorImage`. `-at` also sends a ConfigureRequest with the window rect before the map, like games do. It checks:
69+The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`, `-at X,Y`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. `-fullscreen` (with `-fullscreen-delay`, `-fullscreen-off`) requests `_NET_WM_STATE_FULLSCREEN` by client message and prints `fullscreen WxH` / `fullscreen off WxH` when the window manager publishes the state; `-keyboard-grab` calls `XGrabKeyboard` and prints `grab`. Windowless modes: `-copy TEXT|@file` (with `-copy-sel CLIPBOARD|PRIMARY`, `-copy-for`) owns a selection and serves `TARGETS`, `UTF8_STRING`, `STRING` and `TEXT`, with INCR above 64 KiB, and prints `copy owned`; `-paste CLIPBOARD|PRIMARY` prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB, or `paste none`; `-xsettings` prints `xsettings serial=N Xft/DPI=… Gdk/WindowScalingFactor=… Gdk/UnscaledDPI=… Gtk/CursorThemeName=… Gtk/CursorThemeSize=…` or `xsettings none`; `-root-cursor` prints `cursor WxH` from XFixes `GetCursorImage`. `-at` also sends a ConfigureRequest with the window rect before the map, like games do. It checks:
7070
7171 - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile;
7272 - the probe receives focus;
@@ -79,7 +79,7 @@ The test starts a headless NeferWL with a real `Xwayland` on the first free X di
7979 - with the keyboard grab, `Super+q` does not close a fullscreen focused client and works again once it leaves fullscreen; on a windowed client it closes it;
8080 - on two outputs, a window created at the second output's origin opens on `HEADLESS-2` and fullscreens there;
8181 - clipboard: `wl-copy` text of 4 KiB and 1 MiB (INCR) is read by `x11probe -paste`, and an `x11probe -copy` of the same sizes is read byte-exact by `wl-paste`, for CLIPBOARD and PRIMARY; `wl-copy --clear` gives `paste none`;
82-- XSETTINGS: serial, `Xft/DPI=98304` (scale 1) or `196608` (`output.HEADLESS-1.scale = 2`), and the cursor theme and size from `XCURSOR_THEME`/`XCURSOR_SIZE`; `xwayland.xsettings = off` leaves no owner;
82+- XSETTINGS: serial, `Xft/DPI=98304` and `Gdk/WindowScalingFactor=1` at any output scale (also with `output.HEADLESS-1.scale = 2`), and the cursor theme and size from `XCURSOR_THEME`/`XCURSOR_SIZE`; `xwayland.xsettings = off` leaves no owner;
8383 - root cursor: `GetCursorImage` over the root is the theme arrow at the theme size (a throwaway theme in `XCURSOR_PATH`);
8484 - a window rule `rule.games.app-id = steam_app_\d+` floats an X11 window of class `steam_app_413150`, and not another class.
8585
@@ -66,7 +66,7 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本
66 66
67 `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. The clipboard test also needs `wl-copy` and `wl-paste` (wl-clipboard) and skips without them; they run with the headless `WAYLAND_DISPLAY` and need no extra focus step, since the focused probe window is enough for `wl-copy --foreground` to set the selection.67 `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. The clipboard test also needs `wl-copy` and `wl-paste` (wl-clipboard) and skips without them; they run with the headless `WAYLAND_DISPLAY` and need no extra focus step, since the focused probe window is enough for `wl-copy --foreground` to set the selection.
68 68
69-The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`, `-at X,Y`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. `-fullscreen` (with `-fullscreen-delay`, `-fullscreen-off`) requests `_NET_WM_STATE_FULLSCREEN` by client message and prints `fullscreen WxH` / `fullscreen off WxH` when the window manager publishes the state; `-keyboard-grab` calls `XGrabKeyboard` and prints `grab`. Windowless modes: `-copy TEXT|@file` (with `-copy-sel CLIPBOARD|PRIMARY`, `-copy-for`) owns a selection and serves `TARGETS`, `UTF8_STRING`, `STRING` and `TEXT`, with INCR above 64 KiB, and prints `copy owned`; `-paste CLIPBOARD|PRIMARY` prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB, or `paste none`; `-xsettings` prints `xsettings serial=N Xft/DPI=… Gtk/CursorThemeName=… Gtk/CursorThemeSize=…` or `xsettings none`; `-root-cursor` prints `cursor WxH` from XFixes `GetCursorImage`. `-at` also sends a ConfigureRequest with the window rect before the map, like games do. It checks:69+The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`, `-at X,Y`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. `-fullscreen` (with `-fullscreen-delay`, `-fullscreen-off`) requests `_NET_WM_STATE_FULLSCREEN` by client message and prints `fullscreen WxH` / `fullscreen off WxH` when the window manager publishes the state; `-keyboard-grab` calls `XGrabKeyboard` and prints `grab`. Windowless modes: `-copy TEXT|@file` (with `-copy-sel CLIPBOARD|PRIMARY`, `-copy-for`) owns a selection and serves `TARGETS`, `UTF8_STRING`, `STRING` and `TEXT`, with INCR above 64 KiB, and prints `copy owned`; `-paste CLIPBOARD|PRIMARY` prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB, or `paste none`; `-xsettings` prints `xsettings serial=N Xft/DPI=… Gdk/WindowScalingFactor=… Gdk/UnscaledDPI=… Gtk/CursorThemeName=… Gtk/CursorThemeSize=…` or `xsettings none`; `-root-cursor` prints `cursor WxH` from XFixes `GetCursorImage`. `-at` also sends a ConfigureRequest with the window rect before the map, like games do. It checks:
70 70
71 - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile;71 - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile;
72 - the probe receives focus;72 - the probe receives focus;
@@ -79,7 +79,7 @@ The test starts a headless NeferWL with a real `Xwayland` on the first free X di
79 - with the keyboard grab, `Super+q` does not close a fullscreen focused client and works again once it leaves fullscreen; on a windowed client it closes it;79 - with the keyboard grab, `Super+q` does not close a fullscreen focused client and works again once it leaves fullscreen; on a windowed client it closes it;
80 - on two outputs, a window created at the second output's origin opens on `HEADLESS-2` and fullscreens there;80 - on two outputs, a window created at the second output's origin opens on `HEADLESS-2` and fullscreens there;
81 - clipboard: `wl-copy` text of 4 KiB and 1 MiB (INCR) is read by `x11probe -paste`, and an `x11probe -copy` of the same sizes is read byte-exact by `wl-paste`, for CLIPBOARD and PRIMARY; `wl-copy --clear` gives `paste none`;81 - clipboard: `wl-copy` text of 4 KiB and 1 MiB (INCR) is read by `x11probe -paste`, and an `x11probe -copy` of the same sizes is read byte-exact by `wl-paste`, for CLIPBOARD and PRIMARY; `wl-copy --clear` gives `paste none`;
82-- XSETTINGS: serial, `Xft/DPI=98304` (scale 1) or `196608` (`output.HEADLESS-1.scale = 2`), and the cursor theme and size from `XCURSOR_THEME`/`XCURSOR_SIZE`; `xwayland.xsettings = off` leaves no owner;82+- XSETTINGS: serial, `Xft/DPI=98304` and `Gdk/WindowScalingFactor=1` at any output scale (also with `output.HEADLESS-1.scale = 2`), and the cursor theme and size from `XCURSOR_THEME`/`XCURSOR_SIZE`; `xwayland.xsettings = off` leaves no owner;
83 - root cursor: `GetCursorImage` over the root is the theme arrow at the theme size (a throwaway theme in `XCURSOR_PATH`);83 - root cursor: `GetCursorImage` over the root is the theme arrow at the theme size (a throwaway theme in `XCURSOR_PATH`);
84 - a window rule `rule.games.app-id = steam_app_\d+` floats an X11 window of class `steam_app_413150`, and not another class.84 - a window rule `rule.games.app-id = steam_app_\d+` floats an X11 window of class `steam_app_413150`, and not another class.
85 85
M internal/adapters/wayland/xwm/configure.go +0 -1
@@ -123,7 +123,6 @@ func (x *XWM) LayoutChanged(changed bool) {
123123 x.warnedSpace = true
124124 x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")
125125 }
126- x.refreshXSettings()
127126 if !changed {
128127 return
129128 }
@@ -123,7 +123,6 @@ func (x *XWM) LayoutChanged(changed bool) {
123 x.warnedSpace = true123 x.warnedSpace = true
124 x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")124 x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")
125 }125 }
126- x.refreshXSettings()
127 if !changed {126 if !changed {
128 return127 return
129 }128 }
M internal/adapters/wayland/xwm/selection_xsettings.go +17 -29
@@ -2,7 +2,6 @@ package xwm
22
33 import (
44 "encoding/binary"
5- "math"
65 "unicode/utf8"
76
87 "bnema.dev/xturbo"
@@ -29,24 +28,18 @@ type xsettings struct {
2928
3029 serial uint32
3130 // Last published values and the serial of their last change.
32- dpi int32
3331 theme string
3432 size int32
35- dpiSerial uint32
33+ fixedSerial uint32 // serial of the constant settings (scale 1, 96 dpi)
3634 themeSerial uint32
3735 sizeSerial uint32
3836
3937 buf [xsBufSize]byte
4038 }
4139
42-// xsettingsDPI is Xft/DPI*1024 for a scale: 96 dpi at scale 1, rounded to an
43-// integer dpi.
44-func xsettingsDPI(scale float64) int32 {
45- if scale <= 0 {
46- scale = 1
47- }
48- return int32(math.Round(96*scale)) * 1024
49-}
40+// X11 clients see outputs at their logical size and the compositor upscales
41+// their windows, so the scale announced is always 1 at 96 dpi.
42+const xsDPI = 96 * 1024
5043
5144 func (x *XWM) cursorThemeName() string {
5245 t := x.cfg.CursorTheme
@@ -136,29 +129,18 @@ func (x *XWM) onXSettingsClear(ev *xturbo.Event) {
136129 }
137130 }
138131
139-// refreshXSettings re-evaluates the DPI after a layout change.
140-func (x *XWM) refreshXSettings() {
141- if !x.xs.active {
142- return
143- }
144- if x.publishXSettings(false) {
145- x.wake()
146- }
147-}
148-
149132 // publishXSettings encodes and writes the property when a value changed
150133 // (force: the first time). It reports whether it wrote. Unchanged values cost
151134 // no allocation and no request.
152135 func (x *XWM) publishXSettings(force bool) bool {
153136 s := &x.xs
154- dpi := xsettingsDPI(x.space.Scale())
155137 theme, size := x.cursorThemeName(), x.cursorThemeSize()
156- if !force && s.serial != 0 && dpi == s.dpi && theme == s.theme && size == s.size {
138+ if !force && s.serial != 0 && theme == s.theme && size == s.size {
157139 return false
158140 }
159141 next := s.serial + 1 // wraps
160- if force || s.serial == 0 || dpi != s.dpi {
161- s.dpiSerial = next
142+ if force || s.serial == 0 {
143+ s.fixedSerial = next
162144 }
163145 if force || s.serial == 0 || theme != s.theme {
164146 s.themeSerial = next
@@ -166,7 +148,7 @@ func (x *XWM) publishXSettings(force bool) bool {
166148 if force || s.serial == 0 || size != s.size {
167149 s.sizeSerial = next
168150 }
169- s.serial, s.dpi, s.theme, s.size = next, dpi, theme, size
151+ s.serial, s.theme, s.size = next, theme, size
170152 n := s.encode()
171153 xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(s.win),
172154 xproto.Atom(x.atoms[atomXSettingsSettings]), xproto.Atom(x.atoms[atomXSettingsSettings]), 8, uint32(n), s.buf[:n])
@@ -182,7 +164,7 @@ func (s *xsettings) encode() int {
182164 clear(b[:16])
183165 b[0] = 0 // LSBFirst
184166 le.PutUint32(b[4:], s.serial)
185- le.PutUint32(b[8:], 3)
167+ le.PutUint32(b[8:], 5)
186168 n := 12
187169 putName := func(typ byte, name string, serial uint32) {
188170 b[n], b[n+1] = typ, 0
@@ -194,6 +176,12 @@ func (s *xsettings) encode() int {
194176 le.PutUint32(b[n:], serial)
195177 n += 4
196178 }
179+ putName(xsInt, "Gdk/UnscaledDPI", s.fixedSerial)
180+ le.PutUint32(b[n:], xsDPI)
181+ n += 4
182+ putName(xsInt, "Gdk/WindowScalingFactor", s.fixedSerial)
183+ le.PutUint32(b[n:], 1)
184+ n += 4
197185 putName(xsString, "Gtk/CursorThemeName", s.themeSerial)
198186 le.PutUint32(b[n:], uint32(len(s.theme)))
199187 n += 4
@@ -203,8 +191,8 @@ func (s *xsettings) encode() int {
203191 putName(xsInt, "Gtk/CursorThemeSize", s.sizeSerial)
204192 le.PutUint32(b[n:], uint32(s.size))
205193 n += 4
206- putName(xsInt, "Xft/DPI", s.dpiSerial)
207- le.PutUint32(b[n:], uint32(s.dpi))
194+ putName(xsInt, "Xft/DPI", s.fixedSerial)
195+ le.PutUint32(b[n:], xsDPI)
208196 n += 4
209197 return n
210198 }
@@ -2,7 +2,6 @@ package xwm
2 2
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5- "math"
6 "unicode/utf8"5 "unicode/utf8"
7 6
8 "bnema.dev/xturbo"7 "bnema.dev/xturbo"
@@ -29,24 +28,18 @@ type xsettings struct {
29 28
30 serial uint3229 serial uint32
31 // Last published values and the serial of their last change.30 // Last published values and the serial of their last change.
32- dpi int32
33 theme string31 theme string
34 size int3232 size int32
35- dpiSerial uint3233+ fixedSerial uint32 // serial of the constant settings (scale 1, 96 dpi)
36 themeSerial uint3234 themeSerial uint32
37 sizeSerial uint3235 sizeSerial uint32
38 36
39 buf [xsBufSize]byte37 buf [xsBufSize]byte
40 }38 }
41 39
42-// xsettingsDPI is Xft/DPI*1024 for a scale: 96 dpi at scale 1, rounded to an40+// X11 clients see outputs at their logical size and the compositor upscales
43-// integer dpi.41+// their windows, so the scale announced is always 1 at 96 dpi.
44-func xsettingsDPI(scale float64) int32 {42+const xsDPI = 96 * 1024
45- if scale <= 0 {
46- scale = 1
47- }
48- return int32(math.Round(96*scale)) * 1024
49-}
50 43
51 func (x *XWM) cursorThemeName() string {44 func (x *XWM) cursorThemeName() string {
52 t := x.cfg.CursorTheme45 t := x.cfg.CursorTheme
@@ -136,29 +129,18 @@ func (x *XWM) onXSettingsClear(ev *xturbo.Event) {
136 }129 }
137 }130 }
138 131
139-// refreshXSettings re-evaluates the DPI after a layout change.
140-func (x *XWM) refreshXSettings() {
141- if !x.xs.active {
142- return
143- }
144- if x.publishXSettings(false) {
145- x.wake()
146- }
147-}
148-
149 // publishXSettings encodes and writes the property when a value changed132 // publishXSettings encodes and writes the property when a value changed
150 // (force: the first time). It reports whether it wrote. Unchanged values cost133 // (force: the first time). It reports whether it wrote. Unchanged values cost
151 // no allocation and no request.134 // no allocation and no request.
152 func (x *XWM) publishXSettings(force bool) bool {135 func (x *XWM) publishXSettings(force bool) bool {
153 s := &x.xs136 s := &x.xs
154- dpi := xsettingsDPI(x.space.Scale())
155 theme, size := x.cursorThemeName(), x.cursorThemeSize()137 theme, size := x.cursorThemeName(), x.cursorThemeSize()
156- if !force && s.serial != 0 && dpi == s.dpi && theme == s.theme && size == s.size {138+ if !force && s.serial != 0 && theme == s.theme && size == s.size {
157 return false139 return false
158 }140 }
159 next := s.serial + 1 // wraps141 next := s.serial + 1 // wraps
160- if force || s.serial == 0 || dpi != s.dpi {142+ if force || s.serial == 0 {
161- s.dpiSerial = next143+ s.fixedSerial = next
162 }144 }
163 if force || s.serial == 0 || theme != s.theme {145 if force || s.serial == 0 || theme != s.theme {
164 s.themeSerial = next146 s.themeSerial = next
@@ -166,7 +148,7 @@ func (x *XWM) publishXSettings(force bool) bool {
166 if force || s.serial == 0 || size != s.size {148 if force || s.serial == 0 || size != s.size {
167 s.sizeSerial = next149 s.sizeSerial = next
168 }150 }
169- s.serial, s.dpi, s.theme, s.size = next, dpi, theme, size151+ s.serial, s.theme, s.size = next, theme, size
170 n := s.encode()152 n := s.encode()
171 xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(s.win),153 xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(s.win),
172 xproto.Atom(x.atoms[atomXSettingsSettings]), xproto.Atom(x.atoms[atomXSettingsSettings]), 8, uint32(n), s.buf[:n])154 xproto.Atom(x.atoms[atomXSettingsSettings]), xproto.Atom(x.atoms[atomXSettingsSettings]), 8, uint32(n), s.buf[:n])
@@ -182,7 +164,7 @@ func (s *xsettings) encode() int {
182 clear(b[:16])164 clear(b[:16])
183 b[0] = 0 // LSBFirst165 b[0] = 0 // LSBFirst
184 le.PutUint32(b[4:], s.serial)166 le.PutUint32(b[4:], s.serial)
185- le.PutUint32(b[8:], 3)167+ le.PutUint32(b[8:], 5)
186 n := 12168 n := 12
187 putName := func(typ byte, name string, serial uint32) {169 putName := func(typ byte, name string, serial uint32) {
188 b[n], b[n+1] = typ, 0170 b[n], b[n+1] = typ, 0
@@ -194,6 +176,12 @@ func (s *xsettings) encode() int {
194 le.PutUint32(b[n:], serial)176 le.PutUint32(b[n:], serial)
195 n += 4177 n += 4
196 }178 }
179+ putName(xsInt, "Gdk/UnscaledDPI", s.fixedSerial)
180+ le.PutUint32(b[n:], xsDPI)
181+ n += 4
182+ putName(xsInt, "Gdk/WindowScalingFactor", s.fixedSerial)
183+ le.PutUint32(b[n:], 1)
184+ n += 4
197 putName(xsString, "Gtk/CursorThemeName", s.themeSerial)185 putName(xsString, "Gtk/CursorThemeName", s.themeSerial)
198 le.PutUint32(b[n:], uint32(len(s.theme)))186 le.PutUint32(b[n:], uint32(len(s.theme)))
199 n += 4187 n += 4
@@ -203,8 +191,8 @@ func (s *xsettings) encode() int {
203 putName(xsInt, "Gtk/CursorThemeSize", s.sizeSerial)191 putName(xsInt, "Gtk/CursorThemeSize", s.sizeSerial)
204 le.PutUint32(b[n:], uint32(s.size))192 le.PutUint32(b[n:], uint32(s.size))
205 n += 4193 n += 4
206- putName(xsInt, "Xft/DPI", s.dpiSerial)194+ putName(xsInt, "Xft/DPI", s.fixedSerial)
207- le.PutUint32(b[n:], uint32(s.dpi))195+ le.PutUint32(b[n:], xsDPI)
208 n += 4196 n += 4
209 return n197 return n
210 }198 }
M internal/adapters/wayland/xwm/xsettings_test.go +35 -20
@@ -33,13 +33,25 @@ func (p *xpeer) count(major uint8, match func(xpeerReq) bool) (n int) {
3333 }
3434
3535 func TestXSettingsEncode(t *testing.T) {
36- s := &xsettings{serial: 7, theme: "ab", size: 24, dpi: 96 * 1024, themeSerial: 7, sizeSerial: 5, dpiSerial: 3}
36+ s := &xsettings{serial: 7, theme: "ab", size: 24, themeSerial: 7, sizeSerial: 5, fixedSerial: 3}
3737 n := s.encode()
3838 u32 := binary.LittleEndian.AppendUint32
3939 var want []byte
4040 want = append(want, 0, 0, 0, 0)
4141 want = u32(want, 7)
42+ want = u32(want, 5)
43+ // Gdk/UnscaledDPI INT (name length 15 -> pad 16)
44+ want = append(want, 0, 0, 15, 0)
45+ want = append(want, "Gdk/UnscaledDPI"...)
46+ want = append(want, 0)
4247 want = u32(want, 3)
48+ want = u32(want, 96*1024)
49+ // Gdk/WindowScalingFactor INT 1 (length 23 -> pad 24)
50+ want = append(want, 0, 0, 23, 0)
51+ want = append(want, "Gdk/WindowScalingFactor"...)
52+ want = append(want, 0)
53+ want = u32(want, 3)
54+ want = u32(want, 1)
4355 // Gtk/CursorThemeName STRING "ab"
4456 want = append(want, 1, 0, 19, 0)
4557 want = append(want, "Gtk/CursorThemeName"...)
@@ -103,14 +115,6 @@ func TestXSettingsOwnership(t *testing.T) {
103115 })
104116 require.NoError(t, x.conn.Sync(&x.h))
105117 require.False(t, x.xs.active)
106- before := p.count(opChangeProperty, nil)
107- var sp Space
108- sp.Set(ports.Layout{{Width: 10, Height: 10, Scale: 2}})
109- x.space = &sp
110- x.refreshXSettings()
111- require.NoError(t, x.conn.Flush())
112- require.NoError(t, x.conn.Sync(&x.h))
113- require.Equal(t, before, p.count(opChangeProperty, nil))
114118 }
115119
116120 func TestXSettingsExistingOwner(t *testing.T) {
@@ -166,28 +170,38 @@ func TestXSettingsSerial(t *testing.T) {
166170 return p.count(opChangeProperty, func(r xpeerReq) bool { return r.u32(4) == win })
167171 }
168172 require.Equal(t, 1, props())
169- layout := func(scale float64) {
173+ // Layout changes (any scale) and identical settings write nothing.
174+ for _, scale := range []float64{1, 2} {
170175 var sp Space
171176 sp.Set(ports.Layout{{Width: 100, Height: 100, Scale: scale}})
172177 x.space = &sp
173178 x.LayoutChanged(true)
174179 require.NoError(t, x.conn.Flush())
175180 require.NoError(t, x.conn.Sync(&x.h))
181+ require.Equal(t, 1, props(), "scale %v", scale)
176182 }
177- layout(1) // same settings as boot
178- require.Equal(t, 1, props())
179- layout(2)
180- require.Equal(t, 2, props())
181183 prop, _ := p.prop(win, "_XSETTINGS_SETTINGS")
182184 serial, last := decodeSettings(t, prop.data)
185+ require.EqualValues(t, 1, serial)
186+ require.EqualValues(t, 1, last["Xft/DPI"])
187+ require.EqualValues(t, 1, last["Gdk/WindowScalingFactor"])
188+ require.EqualValues(t, 1, last["Gdk/UnscaledDPI"])
189+ require.EqualValues(t, 96*1024, int32(binary.LittleEndian.Uint32(prop.data[len(prop.data)-4:])), "Xft/DPI is last")
190+
191+ // A changed cursor size republishes: only that setting's serial moves.
192+ x.cfg.CursorSize = 32
193+ require.True(t, x.publishXSettings(false))
194+ require.NoError(t, x.conn.Flush())
195+ require.NoError(t, x.conn.Sync(&x.h))
196+ prop, _ = p.prop(win, "_XSETTINGS_SETTINGS")
197+ serial, last = decodeSettings(t, prop.data)
183198 require.EqualValues(t, 2, serial)
184- require.EqualValues(t, 2, last["Xft/DPI"])
185- require.EqualValues(t, 1, last["Gtk/CursorThemeSize"])
186- require.EqualValues(t, 1, last["Gtk/CursorThemeName"])
187- require.EqualValues(t, 192*1024, int32(binary.LittleEndian.Uint32(prop.data[len(prop.data)-4:])))
199+ require.EqualValues(t, 2, last["Gtk/CursorThemeSize"])
200+ require.EqualValues(t, 1, last["Xft/DPI"])
201+ require.False(t, x.publishXSettings(false), "identical settings write nothing")
188202
189203 x.xs.serial = 0xffffffff
190- x.xs.dpi = 0
204+ x.xs.size = 0
191205 x.publishXSettings(false)
192206 require.EqualValues(t, 0, x.xs.serial, "wraps")
193207 }
@@ -198,10 +212,11 @@ func TestXSettingsNoChangeAllocations(t *testing.T) {
198212 sp.Set(ports.Layout{{Width: 100, Height: 100, Scale: 1}})
199213 x.space = &sp
200214 x.LayoutChanged(true)
201- l := ports.Layout{{Width: 100, Height: 100, Scale: 1}}
215+ l := ports.Layout{{Width: 100, Height: 100, Scale: 2}}
202216 n := testing.AllocsPerRun(100, func() {
203217 sp.Set(l)
204218 x.LayoutChanged(false)
219+ x.publishXSettings(false)
205220 })
206221 require.Zero(t, n)
207222 }
@@ -33,13 +33,25 @@ func (p *xpeer) count(major uint8, match func(xpeerReq) bool) (n int) {
33 }33 }
34 34
35 func TestXSettingsEncode(t *testing.T) {35 func TestXSettingsEncode(t *testing.T) {
36- s := &xsettings{serial: 7, theme: "ab", size: 24, dpi: 96 * 1024, themeSerial: 7, sizeSerial: 5, dpiSerial: 3}36+ s := &xsettings{serial: 7, theme: "ab", size: 24, themeSerial: 7, sizeSerial: 5, fixedSerial: 3}
37 n := s.encode()37 n := s.encode()
38 u32 := binary.LittleEndian.AppendUint3238 u32 := binary.LittleEndian.AppendUint32
39 var want []byte39 var want []byte
40 want = append(want, 0, 0, 0, 0)40 want = append(want, 0, 0, 0, 0)
41 want = u32(want, 7)41 want = u32(want, 7)
42+ want = u32(want, 5)
43+ // Gdk/UnscaledDPI INT (name length 15 -> pad 16)
44+ want = append(want, 0, 0, 15, 0)
45+ want = append(want, "Gdk/UnscaledDPI"...)
46+ want = append(want, 0)
42 want = u32(want, 3)47 want = u32(want, 3)
48+ want = u32(want, 96*1024)
49+ // Gdk/WindowScalingFactor INT 1 (length 23 -> pad 24)
50+ want = append(want, 0, 0, 23, 0)
51+ want = append(want, "Gdk/WindowScalingFactor"...)
52+ want = append(want, 0)
53+ want = u32(want, 3)
54+ want = u32(want, 1)
43 // Gtk/CursorThemeName STRING "ab"55 // Gtk/CursorThemeName STRING "ab"
44 want = append(want, 1, 0, 19, 0)56 want = append(want, 1, 0, 19, 0)
45 want = append(want, "Gtk/CursorThemeName"...)57 want = append(want, "Gtk/CursorThemeName"...)
@@ -103,14 +115,6 @@ func TestXSettingsOwnership(t *testing.T) {
103 })115 })
104 require.NoError(t, x.conn.Sync(&x.h))116 require.NoError(t, x.conn.Sync(&x.h))
105 require.False(t, x.xs.active)117 require.False(t, x.xs.active)
106- before := p.count(opChangeProperty, nil)
107- var sp Space
108- sp.Set(ports.Layout{{Width: 10, Height: 10, Scale: 2}})
109- x.space = &sp
110- x.refreshXSettings()
111- require.NoError(t, x.conn.Flush())
112- require.NoError(t, x.conn.Sync(&x.h))
113- require.Equal(t, before, p.count(opChangeProperty, nil))
114 }118 }
115 119
116 func TestXSettingsExistingOwner(t *testing.T) {120 func TestXSettingsExistingOwner(t *testing.T) {
@@ -166,28 +170,38 @@ func TestXSettingsSerial(t *testing.T) {
166 return p.count(opChangeProperty, func(r xpeerReq) bool { return r.u32(4) == win })170 return p.count(opChangeProperty, func(r xpeerReq) bool { return r.u32(4) == win })
167 }171 }
168 require.Equal(t, 1, props())172 require.Equal(t, 1, props())
169- layout := func(scale float64) {173+ // Layout changes (any scale) and identical settings write nothing.
174+ for _, scale := range []float64{1, 2} {
170 var sp Space175 var sp Space
171 sp.Set(ports.Layout{{Width: 100, Height: 100, Scale: scale}})176 sp.Set(ports.Layout{{Width: 100, Height: 100, Scale: scale}})
172 x.space = &sp177 x.space = &sp
173 x.LayoutChanged(true)178 x.LayoutChanged(true)
174 require.NoError(t, x.conn.Flush())179 require.NoError(t, x.conn.Flush())
175 require.NoError(t, x.conn.Sync(&x.h))180 require.NoError(t, x.conn.Sync(&x.h))
181+ require.Equal(t, 1, props(), "scale %v", scale)
176 }182 }
177- layout(1) // same settings as boot
178- require.Equal(t, 1, props())
179- layout(2)
180- require.Equal(t, 2, props())
181 prop, _ := p.prop(win, "_XSETTINGS_SETTINGS")183 prop, _ := p.prop(win, "_XSETTINGS_SETTINGS")
182 serial, last := decodeSettings(t, prop.data)184 serial, last := decodeSettings(t, prop.data)
185+ require.EqualValues(t, 1, serial)
186+ require.EqualValues(t, 1, last["Xft/DPI"])
187+ require.EqualValues(t, 1, last["Gdk/WindowScalingFactor"])
188+ require.EqualValues(t, 1, last["Gdk/UnscaledDPI"])
189+ require.EqualValues(t, 96*1024, int32(binary.LittleEndian.Uint32(prop.data[len(prop.data)-4:])), "Xft/DPI is last")
190+
191+ // A changed cursor size republishes: only that setting's serial moves.
192+ x.cfg.CursorSize = 32
193+ require.True(t, x.publishXSettings(false))
194+ require.NoError(t, x.conn.Flush())
195+ require.NoError(t, x.conn.Sync(&x.h))
196+ prop, _ = p.prop(win, "_XSETTINGS_SETTINGS")
197+ serial, last = decodeSettings(t, prop.data)
183 require.EqualValues(t, 2, serial)198 require.EqualValues(t, 2, serial)
184- require.EqualValues(t, 2, last["Xft/DPI"])199+ require.EqualValues(t, 2, last["Gtk/CursorThemeSize"])
185- require.EqualValues(t, 1, last["Gtk/CursorThemeSize"])200+ require.EqualValues(t, 1, last["Xft/DPI"])
186- require.EqualValues(t, 1, last["Gtk/CursorThemeName"])201+ require.False(t, x.publishXSettings(false), "identical settings write nothing")
187- require.EqualValues(t, 192*1024, int32(binary.LittleEndian.Uint32(prop.data[len(prop.data)-4:])))
188 202
189 x.xs.serial = 0xffffffff203 x.xs.serial = 0xffffffff
190- x.xs.dpi = 0204+ x.xs.size = 0
191 x.publishXSettings(false)205 x.publishXSettings(false)
192 require.EqualValues(t, 0, x.xs.serial, "wraps")206 require.EqualValues(t, 0, x.xs.serial, "wraps")
193 }207 }
@@ -198,10 +212,11 @@ func TestXSettingsNoChangeAllocations(t *testing.T) {
198 sp.Set(ports.Layout{{Width: 100, Height: 100, Scale: 1}})212 sp.Set(ports.Layout{{Width: 100, Height: 100, Scale: 1}})
199 x.space = &sp213 x.space = &sp
200 x.LayoutChanged(true)214 x.LayoutChanged(true)
201- l := ports.Layout{{Width: 100, Height: 100, Scale: 1}}215+ l := ports.Layout{{Width: 100, Height: 100, Scale: 2}}
202 n := testing.AllocsPerRun(100, func() {216 n := testing.AllocsPerRun(100, func() {
203 sp.Set(l)217 sp.Set(l)
204 x.LayoutChanged(false)218 x.LayoutChanged(false)
219+ x.publishXSettings(false)
205 })220 })
206 require.Zero(t, n)221 require.Zero(t, n)
207 }222 }
M internal/app/testdata/x11probe/main.go +3 -3
@@ -21,7 +21,7 @@
2121 // -copy-for, serving TARGETS and UTF8_STRING/STRING/TEXT, with INCR above
2222 // 64 KiB, and prints "copy owned". -paste SEL requests UTF8_STRING (INCR
2323 // aware) and prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB or
24-// `paste none`. -xsettings prints the XSETTINGS owner's serial, Xft/DPI,
24+// `paste none`. -xsettings prints the XSETTINGS owner's serial, Xft/DPI, Gdk/WindowScalingFactor, Gdk/UnscaledDPI,
2525 // Gtk/CursorThemeName and Gtk/CursorThemeSize, or "xsettings none".
2626 // -root-cursor prints "cursor WxH" of the XFixes cursor image.
2727 package main
@@ -654,8 +654,8 @@ func (t *tool) xsettingsMode() error {
654654 return fmt.Errorf("xsettings: type %d", typ)
655655 }
656656 }
657- fmt.Printf("xsettings serial=%d Xft/DPI=%s Gtk/CursorThemeName=%s Gtk/CursorThemeSize=%s\n",
658- serial, vals["Xft/DPI"], vals["Gtk/CursorThemeName"], vals["Gtk/CursorThemeSize"])
657+ fmt.Printf("xsettings serial=%d Xft/DPI=%s Gdk/WindowScalingFactor=%s Gdk/UnscaledDPI=%s Gtk/CursorThemeName=%s Gtk/CursorThemeSize=%s\n",
658+ serial, vals["Xft/DPI"], vals["Gdk/WindowScalingFactor"], vals["Gdk/UnscaledDPI"], vals["Gtk/CursorThemeName"], vals["Gtk/CursorThemeSize"])
659659 return nil
660660 }
661661
@@ -21,7 +21,7 @@
21 // -copy-for, serving TARGETS and UTF8_STRING/STRING/TEXT, with INCR above21 // -copy-for, serving TARGETS and UTF8_STRING/STRING/TEXT, with INCR above
22 // 64 KiB, and prints "copy owned". -paste SEL requests UTF8_STRING (INCR22 // 64 KiB, and prints "copy owned". -paste SEL requests UTF8_STRING (INCR
23 // aware) and prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB or23 // aware) and prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB or
24-// `paste none`. -xsettings prints the XSETTINGS owner's serial, Xft/DPI,24+// `paste none`. -xsettings prints the XSETTINGS owner's serial, Xft/DPI, Gdk/WindowScalingFactor, Gdk/UnscaledDPI,
25 // Gtk/CursorThemeName and Gtk/CursorThemeSize, or "xsettings none".25 // Gtk/CursorThemeName and Gtk/CursorThemeSize, or "xsettings none".
26 // -root-cursor prints "cursor WxH" of the XFixes cursor image.26 // -root-cursor prints "cursor WxH" of the XFixes cursor image.
27 package main27 package main
@@ -654,8 +654,8 @@ func (t *tool) xsettingsMode() error {
654 return fmt.Errorf("xsettings: type %d", typ)654 return fmt.Errorf("xsettings: type %d", typ)
655 }655 }
656 }656 }
657- fmt.Printf("xsettings serial=%d Xft/DPI=%s Gtk/CursorThemeName=%s Gtk/CursorThemeSize=%s\n",657+ fmt.Printf("xsettings serial=%d Xft/DPI=%s Gdk/WindowScalingFactor=%s Gdk/UnscaledDPI=%s Gtk/CursorThemeName=%s Gtk/CursorThemeSize=%s\n",
658- serial, vals["Xft/DPI"], vals["Gtk/CursorThemeName"], vals["Gtk/CursorThemeSize"])658+ serial, vals["Xft/DPI"], vals["Gdk/WindowScalingFactor"], vals["Gdk/UnscaledDPI"], vals["Gtk/CursorThemeName"], vals["Gtk/CursorThemeSize"])
659 return nil659 return nil
660 }660 }
661 661
M internal/app/xwayland_test.go +9 -5
@@ -730,11 +730,11 @@ func cursorTheme(t *testing.T, size int) {
730730 }
731731
732732 // TestXwaylandXSettings: the XSETTINGS manager publishes the cursor theme,
733-// the cursor size and the DPI of the first output.
733+// the cursor size and a fixed scale 1 / 96 dpi.
734734 func TestXwaylandXSettings(t *testing.T) {
735735 cursorTheme(t, 32)
736736 e := startXwayland(t)
737- want := "xsettings serial=1 Xft/DPI=98304 Gtk/CursorThemeName=theme Gtk/CursorThemeSize=32"
737+ want := "xsettings serial=1 Xft/DPI=98304 Gdk/WindowScalingFactor=1 Gdk/UnscaledDPI=98304 Gtk/CursorThemeName=theme Gtk/CursorThemeSize=32"
738738 var got string
739739 until(t, "XSETTINGS owner", func() bool { got = runProbe(t, e, "-xsettings"); return got != "xsettings none" })
740740 if got != want {
@@ -742,14 +742,18 @@ func TestXwaylandXSettings(t *testing.T) {
742742 }
743743 }
744744
745-// TestXwaylandXSettingsScale2: the DPI follows the output scale.
746-func TestXwaylandXSettingsScale2(t *testing.T) {
745+// TestXwaylandXSettingsScale2AnnouncesScale1: X11 sees logical sizes, so the
746+// DPI stays 96 even on a scale-2 output.
747+func TestXwaylandXSettingsScale2AnnouncesScale1(t *testing.T) {
747748 cursorTheme(t, 24)
748749 e := startXwaylandWith(t, [][2]int{{xwaylandW, xwaylandH}}, func(c *ports.Config) {
749750 c.Outputs = append(c.Outputs, ports.OutputConfig{Name: "HEADLESS-1", Scale: 2, ScaleOnly: true})
750751 })
751752 var got string
752- until(t, "Xft/DPI=196608", func() bool { got = runProbe(t, e, "-xsettings"); return strings.Contains(got, "Xft/DPI=196608") })
753+ until(t, "XSETTINGS owner", func() bool { got = runProbe(t, e, "-xsettings"); return got != "xsettings none" })
754+ if !strings.Contains(got, "Xft/DPI=98304") || !strings.Contains(got, "Gdk/WindowScalingFactor=1") {
755+ t.Errorf("-xsettings = %q, want Xft/DPI=98304 and Gdk/WindowScalingFactor=1", got)
756+ }
753757 if !strings.Contains(got, "Gtk/CursorThemeSize=24") {
754758 t.Errorf("-xsettings = %q, want Gtk/CursorThemeSize=24", got)
755759 }
@@ -730,11 +730,11 @@ func cursorTheme(t *testing.T, size int) {
730 }730 }
731 731
732 // TestXwaylandXSettings: the XSETTINGS manager publishes the cursor theme,732 // TestXwaylandXSettings: the XSETTINGS manager publishes the cursor theme,
733-// the cursor size and the DPI of the first output.733+// the cursor size and a fixed scale 1 / 96 dpi.
734 func TestXwaylandXSettings(t *testing.T) {734 func TestXwaylandXSettings(t *testing.T) {
735 cursorTheme(t, 32)735 cursorTheme(t, 32)
736 e := startXwayland(t)736 e := startXwayland(t)
737- want := "xsettings serial=1 Xft/DPI=98304 Gtk/CursorThemeName=theme Gtk/CursorThemeSize=32"737+ want := "xsettings serial=1 Xft/DPI=98304 Gdk/WindowScalingFactor=1 Gdk/UnscaledDPI=98304 Gtk/CursorThemeName=theme Gtk/CursorThemeSize=32"
738 var got string738 var got string
739 until(t, "XSETTINGS owner", func() bool { got = runProbe(t, e, "-xsettings"); return got != "xsettings none" })739 until(t, "XSETTINGS owner", func() bool { got = runProbe(t, e, "-xsettings"); return got != "xsettings none" })
740 if got != want {740 if got != want {
@@ -742,14 +742,18 @@ func TestXwaylandXSettings(t *testing.T) {
742 }742 }
743 }743 }
744 744
745-// TestXwaylandXSettingsScale2: the DPI follows the output scale.745+// TestXwaylandXSettingsScale2AnnouncesScale1: X11 sees logical sizes, so the
746-func TestXwaylandXSettingsScale2(t *testing.T) {746+// DPI stays 96 even on a scale-2 output.
747+func TestXwaylandXSettingsScale2AnnouncesScale1(t *testing.T) {
747 cursorTheme(t, 24)748 cursorTheme(t, 24)
748 e := startXwaylandWith(t, [][2]int{{xwaylandW, xwaylandH}}, func(c *ports.Config) {749 e := startXwaylandWith(t, [][2]int{{xwaylandW, xwaylandH}}, func(c *ports.Config) {
749 c.Outputs = append(c.Outputs, ports.OutputConfig{Name: "HEADLESS-1", Scale: 2, ScaleOnly: true})750 c.Outputs = append(c.Outputs, ports.OutputConfig{Name: "HEADLESS-1", Scale: 2, ScaleOnly: true})
750 })751 })
751 var got string752 var got string
752- until(t, "Xft/DPI=196608", func() bool { got = runProbe(t, e, "-xsettings"); return strings.Contains(got, "Xft/DPI=196608") })753+ until(t, "XSETTINGS owner", func() bool { got = runProbe(t, e, "-xsettings"); return got != "xsettings none" })
754+ if !strings.Contains(got, "Xft/DPI=98304") || !strings.Contains(got, "Gdk/WindowScalingFactor=1") {
755+ t.Errorf("-xsettings = %q, want Xft/DPI=98304 and Gdk/WindowScalingFactor=1", got)
756+ }
753 if !strings.Contains(got, "Gtk/CursorThemeSize=24") {757 if !strings.Contains(got, "Gtk/CursorThemeSize=24") {
754 t.Errorf("-xsettings = %q, want Gtk/CursorThemeSize=24", got)758 t.Errorf("-xsettings = %q, want Gtk/CursorThemeSize=24", got)
755 }759 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close