1# This module declares the options to define a *display manager*, the
2# program responsible for handling X logins (such as xdm, gdb, or
3# SLiM). 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.session.script
31 xsession = wm: dm: pkgs.writeScript "xsession"
32 ''
33 #! ${pkgs.bash}/bin/bash
34
35 # Expected parameters:
36 # $1 = <desktop-manager>+<window-manager>
37
38 # Actual parameters (FIXME):
39 # SDDM is calling this script like the following:
40 # $1 = /nix/store/xxx-xsession (= $0)
41 # $2 = <desktop-manager>+<window-manager>
42 # SLiM is using the following parameter:
43 # $1 = /nix/store/xxx-xsession <desktop-manager>+<window-manager>
44 # LightDM keeps the double quotes:
45 # $1 = /nix/store/xxx-xsession "<desktop-manager>+<window-manager>"
46 # The fake/auto display manager doesn't use any parameters and GDM is
47 # broken.
48 # If you want to "debug" this script don't print the parameters to stdout
49 # or stderr because this script will be executed multiple times and the
50 # output won't be visible in the log when the script is executed for the
51 # first time (e.g. append them to a file instead)!
52
53 # All of the above cases are handled by the following hack (FIXME).
54 # Since this line is *very important* for *all display managers* it is
55 # very important to test changes to the following line with all display
56 # managers:
57 if [ "''${1:0:1}" = "/" ]; then eval exec "$1" "$2" ; fi
58
59 # Now it should be safe to assume that the script was called with the
60 # expected parameters.
61
62 ${optionalString cfg.displayManager.logToJournal ''
63 if [ -z "$_DID_SYSTEMD_CAT" ]; then
64 _DID_SYSTEMD_CAT=1 exec ${config.systemd.package}/bin/systemd-cat -t xsession -- "$0" "$@"
65 fi
66 ''}
67
68 . /etc/profile
69 cd "$HOME"
70
71 # The first argument of this script is the session type.
72 sessionType="$1"
73 if [ "$sessionType" = default ]; then sessionType=""; fi
74
75 ${optionalString (!cfg.displayManager.job.logsXsession && !cfg.displayManager.logToJournal) ''
76 exec > ~/.xsession-errors 2>&1
77 ''}
78
79 ${optionalString cfg.startDbusSession ''
80 if test -z "$DBUS_SESSION_BUS_ADDRESS"; then
81 exec ${pkgs.dbus.dbus-launch} --exit-with-session "$0" "$sessionType"
82 fi
83 ''}
84
85 # Start PulseAudio if enabled.
86 ${optionalString (config.hardware.pulseaudio.enable) ''
87 ${optionalString (!config.hardware.pulseaudio.systemWide)
88 "${config.hardware.pulseaudio.package.out}/bin/pulseaudio --start"
89 }
90
91 # Publish access credentials in the root window.
92 ${config.hardware.pulseaudio.package.out}/bin/pactl load-module module-x11-publish "display=$DISPLAY"
93 ''}
94
95 # Tell systemd about our $DISPLAY. This is needed by the
96 # ssh-agent unit.
97 ${config.systemd.package}/bin/systemctl --user import-environment DISPLAY
98
99 # Load X defaults.
100 ${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
101 if test -e ~/.Xresources; then
102 ${xorg.xrdb}/bin/xrdb -merge ~/.Xresources
103 elif test -e ~/.Xdefaults; then
104 ${xorg.xrdb}/bin/xrdb -merge ~/.Xdefaults
105 fi
106
107 # Speed up application start by 50-150ms according to
108 # http://kdemonkey.blogspot.nl/2008/04/magic-trick.html
109 rm -rf "$HOME/.compose-cache"
110 mkdir "$HOME/.compose-cache"
111
112 # Work around KDE errors when a user first logs in and
113 # .local/share doesn't exist yet.
114 mkdir -p "$HOME/.local/share"
115
116 unset _DID_SYSTEMD_CAT
117
118 ${cfg.displayManager.sessionCommands}
119
120 # Allow the user to execute commands at the beginning of the X session.
121 if test -f ~/.xprofile; then
122 source ~/.xprofile
123 fi
124
125 # Start systemd user services for graphical sessions
126 ${config.systemd.package}/bin/systemctl --user start graphical-session.target
127
128 # Allow the user to setup a custom session type.
129 if test -x ~/.xsession; then
130 exec ~/.xsession
131 else
132 if test "$sessionType" = "custom"; then
133 sessionType="" # fall-thru if there is no ~/.xsession
134 fi
135 fi
136
137 # The session type is "<desktop-manager>+<window-manager>", so
138 # extract those (see:
139 # http://wiki.bash-hackers.org/syntax/pe#substring_removal).
140 windowManager="''${sessionType##*+}"
141 : ''${windowManager:=${cfg.windowManager.default}}
142 desktopManager="''${sessionType%%+*}"
143 : ''${desktopManager:=${cfg.desktopManager.default}}
144
145 # Start the window manager.
146 case "$windowManager" in
147 ${concatMapStrings (s: ''
148 (${s.name})
149 ${s.start}
150 ;;
151 '') wm}
152 (*) echo "$0: Window manager '$windowManager' not found.";;
153 esac
154
155 # Start the desktop manager.
156 case "$desktopManager" in
157 ${concatMapStrings (s: ''
158 (${s.name})
159 ${s.start}
160 ;;
161 '') dm}
162 (*) echo "$0: Desktop manager '$desktopManager' not found.";;
163 esac
164
165 ${optionalString cfg.updateDbusEnvironment ''
166 ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
167 ''}
168
169 test -n "$waitPID" && wait "$waitPID"
170
171 ${config.systemd.package}/bin/systemctl --user stop graphical-session.target
172
173 exit 0
174 '';
175
176 # Desktop Entry Specification:
177 # - https://standards.freedesktop.org/desktop-entry-spec/latest/
178 # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
179 mkDesktops = names: pkgs.runCommand "desktops"
180 { # trivial derivation
181 preferLocalBuild = true;
182 allowSubstitutes = false;
183 }
184 ''
185 mkdir -p "$out"
186 ${concatMapStrings (n: ''
187 cat - > "$out/${n}.desktop" << EODESKTOP
188 [Desktop Entry]
189 Version=1.0
190 Type=XSession
191 TryExec=${cfg.displayManager.session.script}
192 Exec=${cfg.displayManager.session.script} "${n}"
193 X-GDM-BypassXsession=true
194 Name=${n}
195 Comment=
196 EODESKTOP
197 '') names}
198 '';
199
200in
201
202{
203
204 options = {
205
206 services.xserver.displayManager = {
207
208 xauthBin = mkOption {
209 internal = true;
210 default = "${xorg.xauth}/bin/xauth";
211 description = "Path to the <command>xauth</command> program used by display managers.";
212 };
213
214 xserverBin = mkOption {
215 type = types.path;
216 description = "Path to the X server used by display managers.";
217 };
218
219 xserverArgs = mkOption {
220 type = types.listOf types.str;
221 default = [];
222 example = [ "-ac" "-logverbose" "-verbose" "-nolisten tcp" ];
223 description = "List of arguments for the X server.";
224 };
225
226 sessionCommands = mkOption {
227 type = types.lines;
228 default = "";
229 example =
230 ''
231 xmessage "Hello World!" &
232 '';
233 description = "Shell commands executed just before the window or desktop manager is started.";
234 };
235
236 hiddenUsers = mkOption {
237 type = types.listOf types.str;
238 default = [ "nobody" ];
239 description = ''
240 A list of users which will not be shown in the display manager.
241 '';
242 };
243
244 session = mkOption {
245 default = [];
246 example = literalExample
247 ''
248 [ { manage = "desktop";
249 name = "xterm";
250 start = '''
251 ''${pkgs.xterm}/bin/xterm -ls &
252 waitPID=$!
253 ''';
254 }
255 ]
256 '';
257 description = ''
258 List of sessions supported with the command used to start each
259 session. Each session script can set the
260 <varname>waitPID</varname> shell variable to make this script
261 wait until the end of the user session. Each script is used
262 to define either a windows manager or a desktop manager. These
263 can be differentiated by setting the attribute
264 <varname>manage</varname> either to <literal>"window"</literal>
265 or <literal>"desktop"</literal>.
266
267 The list of desktop manager and window manager should appear
268 inside the display manager with the desktop manager name
269 followed by the window manager name.
270 '';
271 apply = list: rec {
272 wm = filter (s: s.manage == "window") list;
273 dm = filter (s: s.manage == "desktop") list;
274 names = flip concatMap dm
275 (d: map (w: d.name + optionalString (w.name != "none") ("+" + w.name))
276 (filter (w: d.name != "none" || w.name != "none") wm));
277 desktops = mkDesktops names;
278 script = xsession wm dm;
279 };
280 };
281
282 job = {
283
284 preStart = mkOption {
285 type = types.lines;
286 default = "";
287 example = "rm -f /var/log/my-display-manager.log";
288 description = "Script executed before the display manager is started.";
289 };
290
291 execCmd = mkOption {
292 type = types.str;
293 example = literalExample ''
294 "''${pkgs.slim}/bin/slim"
295 '';
296 description = "Command to start the display manager.";
297 };
298
299 environment = mkOption {
300 type = types.attrsOf types.unspecified;
301 default = {};
302 example = { SLIM_CFGFILE = "/etc/slim.conf"; };
303 description = "Additional environment variables needed by the display manager.";
304 };
305
306 logsXsession = mkOption {
307 type = types.bool;
308 default = false;
309 description = ''
310 Whether the display manager redirects the
311 output of the session script to
312 <filename>~/.xsession-errors</filename>.
313 '';
314 };
315
316 };
317
318 logToJournal = mkOption {
319 type = types.bool;
320 default = true;
321 description = ''
322 By default, the stdout/stderr of sessions is written
323 to <filename>~/.xsession-errors</filename>. When this option
324 is enabled, it will instead be written to the journal.
325 '';
326 };
327
328 };
329
330 };
331
332 config = {
333 services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
334
335 systemd.user.targets.graphical-session = {
336 unitConfig = {
337 RefuseManualStart = false;
338 StopWhenUnneeded = false;
339 };
340 };
341 };
342
343 imports = [
344 (mkRemovedOptionModule [ "services" "xserver" "displayManager" "desktopManagerHandlesLidAndPower" ]
345 "The option is no longer necessary because all display managers have already delegated lid management to systemd.")
346 ];
347
348}