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, pkgs, ... }:
11
12with lib;
13
14let
15
16 cfg = config.services.xserver;
17 xorg = pkgs.xorg;
18
19 fontconfig = config.fonts.fontconfig;
20 xresourcesXft = pkgs.writeText "Xresources-Xft" ''
21 ${optionalString (fontconfig.dpi != 0) ''Xft.dpi: ${toString fontconfig.dpi}''}
22 Xft.antialias: ${if fontconfig.antialias then "1" else "0"}
23 Xft.rgba: ${fontconfig.subpixel.rgba}
24 Xft.lcdfilter: lcd${fontconfig.subpixel.lcdfilter}
25 Xft.hinting: ${if fontconfig.hinting.enable then "1" else "0"}
26 Xft.autohint: ${if fontconfig.hinting.autohint then "1" else "0"}
27 Xft.hintstyle: hintslight
28 '';
29
30 # file provided by services.xserver.displayManager.sessionData.wrapper
31 xsessionWrapper = pkgs.writeScript "xsession-wrapper"
32 ''
33 #! ${pkgs.bash}/bin/bash
34
35 # Shared environment setup for graphical sessions.
36
37 . /etc/profile
38 cd "$HOME"
39
40 # Allow the user to execute commands at the beginning of the X session.
41 if test -f ~/.xprofile; then
42 source ~/.xprofile
43 fi
44
45 ${optionalString cfg.displayManager.job.logToJournal ''
46 if [ -z "$_DID_SYSTEMD_CAT" ]; then
47 export _DID_SYSTEMD_CAT=1
48 exec ${config.systemd.package}/bin/systemd-cat -t xsession "$0" "$@"
49 fi
50 ''}
51
52 ${optionalString cfg.displayManager.job.logToFile ''
53 exec &> >(tee ~/.xsession-errors)
54 ''}
55
56 # Load X defaults. This should probably be safe on wayland too.
57 ${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
58 if test -e ~/.Xresources; then
59 ${xorg.xrdb}/bin/xrdb -merge ~/.Xresources
60 elif test -e ~/.Xdefaults; then
61 ${xorg.xrdb}/bin/xrdb -merge ~/.Xdefaults
62 fi
63
64 # Import environment variables into the systemd user environment.
65 ${optionalString (cfg.displayManager.importedVariables != []) (
66 "/run/current-system/systemd/bin/systemctl --user import-environment "
67 + toString (unique cfg.displayManager.importedVariables)
68 )}
69
70 # Speed up application start by 50-150ms according to
71 # http://kdemonkey.blogspot.nl/2008/04/magic-trick.html
72 compose_cache="''${XCOMPOSECACHE:-$HOME/.compose-cache}"
73 mkdir -p "$compose_cache"
74 # To avoid accidentally deleting a wrongly set up XCOMPOSECACHE directory,
75 # defensively try to delete cache *files* only, following the file format specified in
76 # https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/modules/im/ximcp/imLcIm.c#L353-358
77 # sprintf (*res, "%s/%c%d_%03x_%08x_%08x", dir, _XimGetMyEndian(), XIM_CACHE_VERSION, (unsigned int)sizeof (DefTree), hash, hash2);
78 ${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
79 unset compose_cache
80
81 # Work around KDE errors when a user first logs in and
82 # .local/share doesn't exist yet.
83 mkdir -p "''${XDG_DATA_HOME:-$HOME/.local/share}"
84
85 unset _DID_SYSTEMD_CAT
86
87 ${cfg.displayManager.sessionCommands}
88
89 # Start systemd user services for graphical sessions
90 /run/current-system/systemd/bin/systemctl --user start graphical-session.target
91
92 # Allow the user to setup a custom session type.
93 if test -x ~/.xsession; then
94 eval exec ~/.xsession "$@"
95 fi
96
97 if test "$1"; then
98 # Run the supplied session command. Remove any double quotes with eval.
99 eval exec "$@"
100 else
101 # TODO: Do we need this? Should not the session always exist?
102 echo "error: unknown session $1" 1>&2
103 exit 1
104 fi
105 '';
106
107 installedSessions = pkgs.runCommand "desktops"
108 { # trivial derivation
109 preferLocalBuild = true;
110 allowSubstitutes = false;
111 }
112 ''
113 mkdir -p "$out/share/"{xsessions,wayland-sessions}
114
115 ${concatMapStrings (pkg: ''
116 for n in ${concatStringsSep " " pkg.providedSessions}; do
117 if ! test -f ${pkg}/share/wayland-sessions/$n.desktop -o \
118 -f ${pkg}/share/xsessions/$n.desktop; then
119 echo "Couldn't find provided session name, $n.desktop, in session package ${pkg.name}:"
120 echo " ${pkg}"
121 return 1
122 fi
123 done
124
125 if test -d ${pkg}/share/xsessions; then
126 ${xorg.lndir}/bin/lndir ${pkg}/share/xsessions $out/share/xsessions
127 fi
128 if test -d ${pkg}/share/wayland-sessions; then
129 ${xorg.lndir}/bin/lndir ${pkg}/share/wayland-sessions $out/share/wayland-sessions
130 fi
131 '') cfg.displayManager.sessionPackages}
132 '';
133
134 dmDefault = cfg.desktopManager.default;
135 # fallback default for cases when only default wm is set
136 dmFallbackDefault = if dmDefault != null then dmDefault else "none";
137 wmDefault = cfg.windowManager.default;
138
139 defaultSessionFromLegacyOptions = dmFallbackDefault + optionalString (wmDefault != null && wmDefault != "none") "+${wmDefault}";
140
141in
142
143{
144 options = {
145
146 services.xserver.displayManager = {
147
148 xauthBin = mkOption {
149 internal = true;
150 default = "${xorg.xauth}/bin/xauth";
151 description = "Path to the <command>xauth</command> program used by display managers.";
152 };
153
154 xserverBin = mkOption {
155 type = types.path;
156 description = "Path to the X server used by display managers.";
157 };
158
159 xserverArgs = mkOption {
160 type = types.listOf types.str;
161 default = [];
162 example = [ "-ac" "-logverbose" "-verbose" "-nolisten tcp" ];
163 description = "List of arguments for the X server.";
164 };
165
166 setupCommands = mkOption {
167 type = types.lines;
168 default = "";
169 description = ''
170 Shell commands executed just after the X server has started.
171
172 This option is only effective for display managers for which this feature
173 is supported; currently these are LightDM, GDM and SDDM.
174 '';
175 };
176
177 sessionCommands = mkOption {
178 type = types.lines;
179 default = "";
180 example =
181 ''
182 xmessage "Hello World!" &
183 '';
184 description = ''
185 Shell commands executed just before the window or desktop manager is
186 started. These commands are not currently sourced for Wayland sessions.
187 '';
188 };
189
190 hiddenUsers = mkOption {
191 type = types.listOf types.str;
192 default = [ "nobody" ];
193 description = ''
194 A list of users which will not be shown in the display manager.
195 '';
196 };
197
198 sessionPackages = mkOption {
199 type = with types; listOf (package // {
200 description = "package with provided sessions";
201 check = p: assertMsg
202 (package.check p && p ? providedSessions
203 && p.providedSessions != [] && all isString p.providedSessions)
204 ''
205 Package, '${p.name}', did not specify any session names, as strings, in
206 'passthru.providedSessions'. This is required when used as a session package.
207
208 The session names can be looked up in:
209 ${p}/share/xsessions
210 ${p}/share/wayland-sessions
211 '';
212 });
213 default = [];
214 description = ''
215 A list of packages containing x11 or wayland session files to be passed to the display manager.
216 '';
217 };
218
219 session = mkOption {
220 default = [];
221 example = literalExample
222 ''
223 [ { manage = "desktop";
224 name = "xterm";
225 start = '''
226 ''${pkgs.xterm}/bin/xterm -ls &
227 waitPID=$!
228 ''';
229 }
230 ]
231 '';
232 description = ''
233 List of sessions supported with the command used to start each
234 session. Each session script can set the
235 <varname>waitPID</varname> shell variable to make this script
236 wait until the end of the user session. Each script is used
237 to define either a window manager or a desktop manager. These
238 can be differentiated by setting the attribute
239 <varname>manage</varname> either to <literal>"window"</literal>
240 or <literal>"desktop"</literal>.
241
242 The list of desktop manager and window manager should appear
243 inside the display manager with the desktop manager name
244 followed by the window manager name.
245 '';
246 };
247
248 sessionData = mkOption {
249 description = "Data exported for display managers’ convenience";
250 internal = true;
251 default = {};
252 apply = val: {
253 wrapper = xsessionWrapper;
254 desktops = installedSessions;
255 sessionNames = concatMap (p: p.providedSessions) cfg.displayManager.sessionPackages;
256 # We do not want to force users to set defaultSession when they have only single DE.
257 autologinSession =
258 if cfg.displayManager.defaultSession != null then
259 cfg.displayManager.defaultSession
260 else if cfg.displayManager.sessionData.sessionNames != [] then
261 head cfg.displayManager.sessionData.sessionNames
262 else
263 null;
264 };
265 };
266
267 defaultSession = mkOption {
268 type = with types; nullOr str // {
269 description = "session name";
270 check = d:
271 assertMsg (d != null -> (str.check d && elem d cfg.displayManager.sessionData.sessionNames)) ''
272 Default graphical session, '${d}', not found.
273 Valid names for 'services.xserver.displayManager.defaultSession' are:
274 ${concatStringsSep "\n " cfg.displayManager.sessionData.sessionNames}
275 '';
276 };
277 default =
278 if dmDefault != null || wmDefault != null then
279 defaultSessionFromLegacyOptions
280 else
281 null;
282 example = "gnome";
283 description = ''
284 Graphical session to pre-select in the session chooser (only effective for GDM and LightDM).
285
286 On GDM, LightDM and SDDM, it will also be used as a session for auto-login.
287 '';
288 };
289
290 importedVariables = mkOption {
291 type = types.listOf (types.strMatching "[a-zA-Z_][a-zA-Z0-9_]*");
292 visible = false;
293 description = ''
294 Environment variables to import into the systemd user environment.
295 '';
296 };
297
298 job = {
299
300 preStart = mkOption {
301 type = types.lines;
302 default = "";
303 example = "rm -f /var/log/my-display-manager.log";
304 description = "Script executed before the display manager is started.";
305 };
306
307 execCmd = mkOption {
308 type = types.str;
309 example = literalExample ''
310 "''${pkgs.lightdm}/bin/lightdm"
311 '';
312 description = "Command to start the display manager.";
313 };
314
315 environment = mkOption {
316 type = types.attrsOf types.unspecified;
317 default = {};
318 description = "Additional environment variables needed by the display manager.";
319 };
320
321 logToFile = mkOption {
322 type = types.bool;
323 default = false;
324 description = ''
325 Whether the display manager redirects the output of the
326 session script to <filename>~/.xsession-errors</filename>.
327 '';
328 };
329
330 logToJournal = mkOption {
331 type = types.bool;
332 default = true;
333 description = ''
334 Whether the display manager redirects the output of the
335 session script to the systemd journal.
336 '';
337 };
338
339 };
340
341 # Configuration for automatic login. Common for all DM.
342 autoLogin = mkOption {
343 type = types.submodule {
344 options = {
345 enable = mkOption {
346 type = types.bool;
347 default = cfg.displayManager.autoLogin.user != null;
348 description = ''
349 Automatically log in as <option>autoLogin.user</option>.
350 '';
351 };
352
353 user = mkOption {
354 type = types.nullOr types.str;
355 default = null;
356 description = ''
357 User to be used for the automatic login.
358 '';
359 };
360 };
361 };
362
363 default = {};
364 description = ''
365 Auto login configuration attrset.
366 '';
367 };
368
369 };
370
371 };
372
373 config = {
374 assertions = [
375 { assertion = cfg.displayManager.autoLogin.enable -> cfg.displayManager.autoLogin.user != null;
376 message = ''
377 services.xserver.displayManager.autoLogin.enable requires services.xserver.displayManager.autoLogin.user to be set
378 '';
379 }
380 {
381 assertion = cfg.desktopManager.default != null || cfg.windowManager.default != null -> cfg.displayManager.defaultSession == defaultSessionFromLegacyOptions;
382 message = "You cannot use both services.xserver.displayManager.defaultSession option and legacy options (services.xserver.desktopManager.default and services.xserver.windowManager.default).";
383 }
384 ];
385
386 warnings =
387 mkIf (dmDefault != null || wmDefault != null) [
388 ''
389 The following options are deprecated:
390 ${concatStringsSep "\n " (map ({c, t}: t) (filter ({c, t}: c != null) [
391 { c = dmDefault; t = "- services.xserver.desktopManager.default"; }
392 { c = wmDefault; t = "- services.xserver.windowManager.default"; }
393 ]))}
394 Please use
395 services.xserver.displayManager.defaultSession = "${defaultSessionFromLegacyOptions}";
396 instead.
397 ''
398 ];
399
400 services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
401
402 services.xserver.displayManager.importedVariables = [
403 # This is required by user units using the session bus.
404 "DBUS_SESSION_BUS_ADDRESS"
405 # These are needed by the ssh-agent unit.
406 "DISPLAY"
407 "XAUTHORITY"
408 # This is required to specify session within user units (e.g. loginctl lock-session).
409 "XDG_SESSION_ID"
410 ];
411
412 systemd.user.targets.graphical-session = {
413 unitConfig = {
414 RefuseManualStart = false;
415 StopWhenUnneeded = false;
416 };
417 };
418
419 # Create desktop files and scripts for starting sessions for WMs/DMs
420 # that do not have upstream session files (those defined using services.{display,desktop,window}Manager.session options).
421 services.xserver.displayManager.sessionPackages =
422 let
423 dms = filter (s: s.manage == "desktop") cfg.displayManager.session;
424 wms = filter (s: s.manage == "window") cfg.displayManager.session;
425
426 # Script responsible for starting the window manager and the desktop manager.
427 xsession = dm: wm: pkgs.writeScript "xsession" ''
428 #! ${pkgs.bash}/bin/bash
429
430 # Legacy session script used to construct .desktop files from
431 # `services.xserver.displayManager.session` entries. Called from
432 # `sessionWrapper`.
433
434 # Start the window manager.
435 ${wm.start}
436
437 # Start the desktop manager.
438 ${dm.start}
439
440 ${optionalString cfg.updateDbusEnvironment ''
441 ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
442 ''}
443
444 test -n "$waitPID" && wait "$waitPID"
445
446 /run/current-system/systemd/bin/systemctl --user stop graphical-session.target
447
448 exit 0
449 '';
450 in
451 # We will generate every possible pair of WM and DM.
452 concatLists (
453 builtins.map
454 ({dm, wm}: let
455 sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
456 script = xsession dm wm;
457 desktopNames = if dm ? desktopNames
458 then concatStringsSep ";" dm.desktopNames
459 else sessionName;
460 in
461 optional (dm.name != "none" || wm.name != "none")
462 (pkgs.writeTextFile {
463 name = "${sessionName}-xsession";
464 destination = "/share/xsessions/${sessionName}.desktop";
465 # Desktop Entry Specification:
466 # - https://standards.freedesktop.org/desktop-entry-spec/latest/
467 # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
468 text = ''
469 [Desktop Entry]
470 Version=1.0
471 Type=XSession
472 TryExec=${script}
473 Exec=${script}
474 Name=${sessionName}
475 DesktopNames=${desktopNames}
476 '';
477 } // {
478 providedSessions = [ sessionName ];
479 })
480 )
481 (cartesianProductOfSets { dm = dms; wm = wms; })
482 );
483
484 # Make xsessions and wayland sessions available in XDG_DATA_DIRS
485 # as some programs have behavior that depends on them being present
486 environment.sessionVariables.XDG_DATA_DIRS = [
487 "${cfg.displayManager.sessionData.desktops}/share"
488 ];
489 };
490
491 imports = [
492 (mkRemovedOptionModule [ "services" "xserver" "displayManager" "desktopManagerHandlesLidAndPower" ]
493 "The option is no longer necessary because all display managers have already delegated lid management to systemd.")
494 (mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logsXsession" ] [ "services" "xserver" "displayManager" "job" "logToFile" ])
495 (mkRenamedOptionModule [ "services" "xserver" "displayManager" "logToJournal" ] [ "services" "xserver" "displayManager" "job" "logToJournal" ])
496 (mkRenamedOptionModule [ "services" "xserver" "displayManager" "extraSessionFilesPackages" ] [ "services" "xserver" "displayManager" "sessionPackages" ])
497 ];
498
499}