1# This module declares the options to define a *display manager*, the
2# program responsible for handling X logins (such as LightDM, GDM, or SDDM).
3# The display manager allows the user to select a *session
4# type*. When the user logs in, the display manager starts the
5# *session script* ("xsession" below) to launch the selected session
6# type. The session type defines two things: the *desktop manager*
7# (e.g., KDE, Gnome or a plain xterm), and optionally the *window
8# manager* (e.g. kwin or twm).
9
10{ config, lib, options, pkgs, ... }:
11
12let
13 inherit (lib) mkOption types literalExpression optionalString;
14
15 cfg = config.services.xserver;
16 xorg = pkgs.xorg;
17
18 fontconfig = config.fonts.fontconfig;
19 xresourcesXft = pkgs.writeText "Xresources-Xft" ''
20 Xft.antialias: ${if fontconfig.antialias then "1" else "0"}
21 Xft.rgba: ${fontconfig.subpixel.rgba}
22 Xft.lcdfilter: lcd${fontconfig.subpixel.lcdfilter}
23 Xft.hinting: ${if fontconfig.hinting.enable then "1" else "0"}
24 Xft.autohint: ${if fontconfig.hinting.autohint then "1" else "0"}
25 Xft.hintstyle: ${fontconfig.hinting.style}
26 '';
27
28 # FIXME: this is an ugly hack.
29 # Some sessions (read: most WMs) don't activate systemd's `graphical-session.target`.
30 # Other sessions (read: most non-WMs) expect `graphical-session.target` to be reached
31 # when the entire session is actually ready. We used to just unconditionally force
32 # `graphical-session.target` to be activated in the session wrapper so things like
33 # xdg-autostart-generator work on sessions that are wrong, but this broke sessions
34 # that do things right. So, preserve this behavior (with some extra steps) by matching
35 # on XDG_CURRENT_DESKTOP and deliberately ignoring sessions we know can do the right thing.
36 fakeSession = action: ''
37 session_is_systemd_aware=$(
38 IFS=:
39 for i in $XDG_CURRENT_DESKTOP; do
40 case $i in
41 KDE|GNOME|Pantheon|X-NIXOS-SYSTEMD-AWARE) echo "1"; exit; ;;
42 *) ;;
43 esac
44 done
45 )
46
47 if [ -z "$session_is_systemd_aware" ]; then
48 /run/current-system/systemd/bin/systemctl --user ${action} nixos-fake-graphical-session.target
49 fi
50 '';
51
52 # file provided by services.xserver.displayManager.sessionData.wrapper
53 xsessionWrapper = pkgs.writeScript "xsession-wrapper"
54 ''
55 #! ${pkgs.bash}/bin/bash
56
57 # Shared environment setup for graphical sessions.
58
59 . /etc/profile
60 if test -f ~/.profile; then
61 source ~/.profile
62 fi
63
64 cd "$HOME"
65
66 # Allow the user to execute commands at the beginning of the X session.
67 if test -f ~/.xprofile; then
68 source ~/.xprofile
69 fi
70
71 ${optionalString config.services.displayManager.logToJournal ''
72 if [ -z "$_DID_SYSTEMD_CAT" ]; then
73 export _DID_SYSTEMD_CAT=1
74 exec ${config.systemd.package}/bin/systemd-cat -t xsession "$0" "$@"
75 fi
76 ''}
77
78 ${optionalString config.services.displayManager.logToFile ''
79 exec &> >(tee ~/.xsession-errors)
80 ''}
81
82 # Load X defaults. This should probably be safe on wayland too.
83 ${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
84 if test -e ~/.Xresources; then
85 ${xorg.xrdb}/bin/xrdb -merge ~/.Xresources
86 elif test -e ~/.Xdefaults; then
87 ${xorg.xrdb}/bin/xrdb -merge ~/.Xdefaults
88 fi
89
90 # Import environment variables into the systemd user environment.
91 ${optionalString (cfg.displayManager.importedVariables != []) (
92 "/run/current-system/systemd/bin/systemctl --user import-environment "
93 + toString (lib.unique cfg.displayManager.importedVariables)
94 )}
95
96 # Speed up application start by 50-150ms according to
97 # https://kdemonkey.blogspot.com/2008/04/magic-trick.html
98 compose_cache="''${XCOMPOSECACHE:-$HOME/.compose-cache}"
99 mkdir -p "$compose_cache"
100 # To avoid accidentally deleting a wrongly set up XCOMPOSECACHE directory,
101 # defensively try to delete cache *files* only, following the file format specified in
102 # https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/modules/im/ximcp/imLcIm.c#L353-358
103 # sprintf (*res, "%s/%c%d_%03x_%08x_%08x", dir, _XimGetMyEndian(), XIM_CACHE_VERSION, (unsigned int)sizeof (DefTree), hash, hash2);
104 ${pkgs.findutils}/bin/find "$compose_cache" -maxdepth 1 -regextype posix-extended -regex '.*/[Bl][0-9]+_[0-9a-f]{3}_[0-9a-f]{8}_[0-9a-f]{8}' -delete
105 unset compose_cache
106
107 # Work around KDE errors when a user first logs in and
108 # .local/share doesn't exist yet.
109 mkdir -p "''${XDG_DATA_HOME:-$HOME/.local/share}"
110
111 unset _DID_SYSTEMD_CAT
112
113 ${cfg.displayManager.sessionCommands}
114
115 ${fakeSession "start"}
116
117 # Allow the user to setup a custom session type.
118 if test -x ~/.xsession; then
119 eval exec ~/.xsession "$@"
120 fi
121
122 if test "$1"; then
123 # Run the supplied session command. Remove any double quotes with eval.
124 eval exec "$@"
125 else
126 # TODO: Do we need this? Should not the session always exist?
127 echo "error: unknown session $1" 1>&2
128 exit 1
129 fi
130 '';
131in
132
133{
134 options = {
135
136 services.xserver.displayManager = {
137
138 xauthBin = mkOption {
139 internal = true;
140 default = "${xorg.xauth}/bin/xauth";
141 defaultText = literalExpression ''"''${pkgs.xorg.xauth}/bin/xauth"'';
142 description = "Path to the {command}`xauth` program used by display managers.";
143 };
144
145 xserverBin = mkOption {
146 type = types.path;
147 description = "Path to the X server used by display managers.";
148 };
149
150 xserverArgs = mkOption {
151 type = types.listOf types.str;
152 default = [];
153 example = [ "-ac" "-logverbose" "-verbose" "-nolisten tcp" ];
154 description = "List of arguments for the X server.";
155 };
156
157 setupCommands = mkOption {
158 type = types.lines;
159 default = "";
160 description = ''
161 Shell commands executed just after the X server has started.
162
163 This option is only effective for display managers for which this feature
164 is supported; currently these are LightDM, GDM and SDDM.
165 '';
166 };
167
168 sessionCommands = mkOption {
169 type = types.lines;
170 default = "";
171 example =
172 ''
173 xmessage "Hello World!" &
174 '';
175 description = ''
176 Shell commands executed just before the window or desktop manager is
177 started. These commands are not currently sourced for Wayland sessions.
178 '';
179 };
180
181 session = mkOption {
182 default = [];
183 type = types.listOf types.attrs;
184 example = literalExpression
185 ''
186 [ { manage = "desktop";
187 name = "xterm";
188 start = '''
189 ''${pkgs.xterm}/bin/xterm -ls &
190 waitPID=$!
191 ''';
192 }
193 ]
194 '';
195 description = ''
196 List of sessions supported with the command used to start each
197 session. Each session script can set the
198 {var}`waitPID` shell variable to make this script
199 wait until the end of the user session. Each script is used
200 to define either a window manager or a desktop manager. These
201 can be differentiated by setting the attribute
202 {var}`manage` either to `"window"`
203 or `"desktop"`.
204
205 The list of desktop manager and window manager should appear
206 inside the display manager with the desktop manager name
207 followed by the window manager name.
208 '';
209 };
210
211 importedVariables = mkOption {
212 type = types.listOf (types.strMatching "[a-zA-Z_][a-zA-Z0-9_]*");
213 visible = false;
214 description = ''
215 Environment variables to import into the systemd user environment.
216 '';
217 };
218
219 };
220
221 };
222
223 config = {
224 services.displayManager.sessionData.wrapper = xsessionWrapper;
225
226 services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
227
228 services.xserver.displayManager.importedVariables = [
229 # This is required by user units using the session bus.
230 "DBUS_SESSION_BUS_ADDRESS"
231 # These are needed by the ssh-agent unit.
232 "DISPLAY"
233 "XAUTHORITY"
234 # This is required to specify session within user units (e.g. loginctl lock-session).
235 "XDG_SESSION_ID"
236 ];
237
238 systemd.user.targets.nixos-fake-graphical-session = {
239 unitConfig = {
240 Description = "Fake graphical-session target for non-systemd-aware sessions";
241 BindsTo = "graphical-session.target";
242 };
243 };
244
245 # Create desktop files and scripts for starting sessions for WMs/DMs
246 # that do not have upstream session files (those defined using services.{display,desktop,window}Manager.session options).
247 services.displayManager.sessionPackages =
248 let
249 dms = lib.filter (s: s.manage == "desktop") cfg.displayManager.session;
250 wms = lib.filter (s: s.manage == "window") cfg.displayManager.session;
251
252 # Script responsible for starting the window manager and the desktop manager.
253 xsession = dm: wm: pkgs.writeScript "xsession" ''
254 #! ${pkgs.bash}/bin/bash
255
256 # Legacy session script used to construct .desktop files from
257 # `services.xserver.displayManager.session` entries. Called from
258 # `sessionWrapper`.
259
260 # Start the window manager.
261 ${wm.start}
262
263 # Start the desktop manager.
264 ${dm.start}
265
266 ${optionalString cfg.updateDbusEnvironment ''
267 ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
268 ''}
269
270 test -n "$waitPID" && wait "$waitPID"
271
272 ${fakeSession "stop"}
273
274 exit 0
275 '';
276 in
277 # We will generate every possible pair of WM and DM.
278 lib.concatLists (
279 lib.mapCartesianProduct
280 ({dm, wm}: let
281 sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
282 script = xsession dm wm;
283 desktopNames = if dm ? desktopNames
284 then lib.concatStringsSep ";" dm.desktopNames
285 else sessionName;
286 in
287 lib.optional (dm.name != "none" || wm.name != "none")
288 (pkgs.writeTextFile {
289 name = "${sessionName}-xsession";
290 destination = "/share/xsessions/${sessionName}.desktop";
291 # Desktop Entry Specification:
292 # - https://standards.freedesktop.org/desktop-entry-spec/latest/
293 # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
294 text = ''
295 [Desktop Entry]
296 Version=1.0
297 Type=XSession
298 TryExec=${script}
299 Exec=${script}
300 Name=${sessionName}
301 DesktopNames=${desktopNames}
302 '';
303 } // {
304 providedSessions = [ sessionName ];
305 })
306 )
307 { dm = dms; wm = wms; }
308 );
309 };
310
311 imports = [
312 (lib.mkRemovedOptionModule [ "services" "xserver" "displayManager" "desktopManagerHandlesLidAndPower" ]
313 "The option is no longer necessary because all display managers have already delegated lid management to systemd.")
314 (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logsXsession" ] [ "services" "displayManager" "logToFile" ])
315 (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "logToJournal" ] [ "services" "displayManager" "logToJournal" ])
316 (lib.mkRenamedOptionModule [ "services" "xserver" "displayManager" "extraSessionFilesPackages" ] [ "services" "displayManager" "sessionPackages" ])
317 ];
318
319}