nixos/programs/sessiond: init

This commit is contained in:
Gabriel Arazas 2024-01-24 10:40:22 +08:00
parent d1dc2953c7
commit 1d3bc3c013
No known key found for this signature in database
GPG Key ID: ADE0C41DAB221FCC
4 changed files with 588 additions and 0 deletions

View File

@ -5,6 +5,7 @@
./programs/distrobox.nix
./programs/gnome-session
./programs/pop-launcher.nix
./programs/sessiond
./programs/wezterm.nix
./services/archivebox.nix
./services/gallery-dl.nix

View File

@ -0,0 +1,124 @@
{ config, lib, pkgs, utils, ... }:
let
cfg = config.programs.sessiond;
sessionPackages = lib.mapAttrsToList
(name: session:
let
displaySession = ''
[Desktop Entry]
Name=${session.fullName}
Comment=${session.description}
Exec="@out@/libexec/${name}-session"
Type=Application
DesktopNames=${lib.concatStringsSep ";" session.desktopNames};
'';
sessionScript = ''
#!${pkgs.runtimeShell}
${lib.getExe' cfg.package "sessionctl"} run "${name}.target"
'';
in
pkgs.runCommandLocal "${name}-desktop-session-files"
{
inherit displaySession sessionScript;
passAsFile = [ "displaySession" "sessionScript" ];
passthru.providedSessions = [ name ];
}
''
SESSION_SCRIPT="$out/libexec/${name}-session"
install -Dm0755 "$sessionScriptPath" "$SESSION_SCRIPT"
substituteAllInPlace "$SESSION_SCRIPT"
DISPLAY_SESSION_FILE="$out/share/xsessions/${name}.desktop"
install -Dm0644 "$displaySessionPath" "$DISPLAY_SESSION_FILE"
substituteAllInPlace "$DISPLAY_SESSION_FILE"
''
)
cfg.sessions;
sessionSystemdUnits = lib.mapAttrsToList
(name: session:
let
inherit (utils.systemdUtils.lib)
pathToUnit serviceToUnit targetToUnit timerToUnit socketToUnit;
sessionComponents =
lib.foldlAttrs
(acc: name: component:
acc // {
"${component.id}.service" = serviceToUnit component.id component.serviceUnit;
"${component.id}.target" = targetToUnit component.id component.targetUnit;
} // lib.optionalAttrs (component.socketUnit != null) {
"${component.id}.socket" = socketToUnit component.id component.socketUnit;
} // lib.optionalAttrs (component.timerUnit != null) {
"${component.id}.timer" = timerToUnit component.id component.timerUnit;
} // lib.optionalAttrs (component.pathUnit != null) {
"${component.id}.path" = pathToUnit component.id component.pathUnit;
})
{ }
session.components;
in
sessionComponents // {
"${name}.service" = serviceToUnit name session.serviceUnit;
"${name}.target" = targetToUnit name session.targetUnit;
}
)
cfg.sessions;
in
{
options.programs.sessiond = {
package = lib.mkOption {
type = lib.types.package;
default = pkgs.sessiond;
defaultText = "pkgs.sessiond";
description = ''
The package containing sessiond executable and systemd units. This
module will use the `sessiond` executable for the generated session
script.
'';
};
sessions = lib.mkOption {
type = with lib.types; attrsOf (submoduleWith {
specialArgs = {
inherit utils pkgs;
sessiondPkg = cfg.package;
};
modules = [ ./submodules/session-type.nix ];
});
description = ''
A set of desktop sessions to be configured with sessiond. Each of the
attribute name will be used as the identifier of the desktop
environment.
::: {.tip}
While you can make identifiers in any way, it is
encouraged to stick to a naming scheme. Here's two common ways to name
a desktop environment.
* Reverse DNS-like scheme (e.g., `com.example.MoseyBranch`).
* Kebab-case (e.g., `mosey-branch`).
:::
'';
default = { };
};
};
config = lib.mkIf (cfg.sessions != { }) {
environment.systemPackages = [ cfg.package ];
# Install all of the desktop session files.
services.xserver.displayManager.sessionPackages = sessionPackages;
# Import those systemd units from sessiond as well.
systemd.packages = [ cfg.package ];
systemd.user.units = lib.mkMerge sessionSystemdUnits;
# We're disabling the upstream sessiond service since there can be multiple
# sessiond sessions here.
systemd.user.services.sessiond.enable = false;
};
}

View File

@ -0,0 +1,228 @@
{ name, config, lib, session, utils, ... }: {
options = {
description = lib.mkOption {
type = lib.types.nonEmptyStr;
description = "One-sentence description of the component.";
example = "Desktop widgets";
};
# Most of the systemd config types are trying to eliminate as much of the
# NixOS systemd extensions as much as possible. For more details, see
# `config` attribute of the `sessionType`.
serviceUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.lib) unitConfig serviceConfig;
inherit (utils.systemdUtils.unitOptions) commonUnitOptions serviceOptions;
in
lib.types.submodule [
commonUnitOptions
serviceOptions
serviceConfig
unitConfig
];
description = ''
systemd service configuration to be generated. This should be
configured if the session is managed by systemd.
:::{.note}
This has the same options as {option}`systemd.user.services.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
By default, this module sets the service unit as part of the respective
target unit (i.e., `PartOf=$COMPONENTID.target`).
On a typical case, you shouldn't mess with much of the dependency
ordering with the service unit. You should configure `targetUnit` for
that instead.
:::
'';
default = { };
};
targetUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.lib) unitConfig;
inherit (utils.systemdUtils.unitOptions) commonUnitOptions;
in
lib.types.submodule [
commonUnitOptions
unitConfig
];
description = ''
systemd target configuration to be generated. This should be
configured if the session is managed by systemd.
:::{.note}
This is generated by default alongside the service where it is
configured to be a part of the target unit.
This has the same options as {option}`systemd.user.targets.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
:::
'';
default = { };
};
timerUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.unitOptions) timerOptions commonUnitOptions;
inherit (utils.systemdUtils.lib) unitConfig;
in
with lib.types; nullOr (submodule [
commonUnitOptions
timerOptions
unitConfig
]);
description = ''
An optional systemd timer configuration to be generated. This should
be configured if the session is managed by systemd.
:::{.note}
This has the same options as {option}`systemd.user.timers.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
:::
'';
default = null;
};
socketUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.unitOptions) socketOptions commonUnitOptions;
inherit (utils.systemdUtils.lib) unitConfig;
in
with lib.types; nullOr (submodule [
commonUnitOptions
socketOptions
unitConfig
]);
description = ''
An optional systemd socket configuration to be generated. This should
be configured if the session is managed by systemd.
:::{.note}
This has the same options as {option}`systemd.user.sockets.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
:::
'';
default = null;
};
pathUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.unitOptions) pathOptions commonUnitOptions;
inherit (utils.systemdUtils.lib) unitConfig;
in
with lib.types; nullOr (submodule [
commonUnitOptions
pathOptions
unitConfig
]);
description = ''
An optional systemd path configuration to be generated. This should
be configured if the session is managed by systemd.
:::{.note}
This has the same options as {option}`systemd.user.paths.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
:::
'';
default = null;
};
id = lib.mkOption {
type = lib.types.str;
description = ''
The identifier of the component used in generating filenames for its
`.desktop` files and as part of systemd unit names.
'';
default = "${session.name}.${name}";
defaultText = "\${session-name}.\${name}";
readOnly = true;
};
};
config = {
/*
Setting some recommendation and requirements for sessiond components.
Note there are the missing directives that COULD include some sane
defaults here.
* The `Unit.OnFailure=` and `Unit.OnFailureJobMode=` directives. Since
different components don't have the same priority and don't handle
failures the same way, we didn't set it here. This is on the user to
know how different desktop components interact with each other
especially if one of them failed.
TODO: Is `Type=notify` a good default?
* `Service.Type=` is obviously not included since not all desktop
components are the same either. Some of them could a D-Bus service,
some of them are oneshots, etc. Not to mention, this is already implied
to be `Type=simple` by systemd anyways.
* `Service.OOMScoreAdjust=` have different values for different
components so it isn't included.
* Most sandboxing options. Aside from the fact we're dealing with a
systemd user unit, much of them are unnecessary and rarely needed (if
ever like `Service.PrivateTmp=`?) so we didn't set such defaults here.
As you can tell, this module does not provide a framework for the user
to easily compose their own desktop environment. THIS MODULE ALREADY
DOES A LOT, ALRIGHT! CUT ME SOME SLACK!
Take note that the default service configuration is leaning on the
desktop component being a simple type of service like how most NixOS
service modules are deployed.
*/
serviceUnit = {
description = lib.mkDefault config.description;
# The typical workflow for service units to have them set as part of
# the respective target unit.
requisite = [ "${config.id}.target" ];
before = [ "${config.id}.target" ];
partOf = [ "${config.id}.target" ];
# Some sane service configuration for a desktop component.
serviceConfig = {
Slice = lib.mkDefault "session.slice";
Restart = lib.mkDefault "on-failure";
TimeoutStopSec = lib.mkDefault 5;
};
startLimitBurst = lib.mkDefault 3;
startLimitIntervalSec = lib.mkDefault 15;
unitConfig = {
# We leave those up to the target units to start the services.
RefuseManualStart = lib.mkDefault true;
RefuseManualStop = lib.mkDefault true;
};
};
/*
Similarly, there are things that COULD make it here but didn't for a
variety of reasons.
* `Unit.PartOf=`, `Unit.Requisite=`, and the like since some components
require starting up earlier than the others. We could include it here
if we make it clear in the documentation or if it proves to be a
painful experience to configure this by a first-timer. For now, this is
on the user to know.
*/
targetUnit = {
wants = [ "${config.id}.service" ];
description = lib.mkDefault config.description;
};
};
}

View File

@ -0,0 +1,235 @@
{ name, config, pkgs, lib, utils, sessiondPkg, ... }:
let
# For an updated list, see `menu/menu-spec.xml` from
# https://gitlab.freedesktop.org/xdg/xdg-specs.
validDesktopNames = [
"GNOME"
"GNOME-Classic"
"GNOME-Flashback"
"KDE"
"LXDE"
"LXQt"
"MATE"
"Razor"
"ROX"
"TDE"
"Unity"
"XFCE"
"EDE"
"Cinnamon"
"Pantheon"
"Budgie"
"Enlightenment"
"DDE"
"Endless"
"Old"
];
# This is used both as the configuration format for sessiond.conf and its
# hooks.
settingsFormat = pkgs.formats.toml { };
sessionSettingsFile = settingsFormat.generate "sessiond-conf-${name}" config.settings;
in
{
options = {
fullName = lib.mkOption {
type = lib.types.nonEmptyStr;
description = "The display name of the desktop environment.";
default = name;
example = "Mosey Branch";
};
desktopNames = lib.mkOption {
type = with lib.types; listOf nonEmptyStr;
description = ''
Names to be used as `DesktopNames=` entry of the session `.desktop`
file. Useful if you're creating a customized version of an already
existing desktop session.
::: {.note}
This module sanitizes the values by prepending the given names with
`X-` if they aren't part of the registered values from XDG spec.
:::
'';
default = [ config.fullName ];
defaultText = "[ <session>.fullName ]";
apply = names:
builtins.map
(name:
if (lib.elem name validDesktopNames) || (lib.hasPrefix "X-" name) then
name
else
"X-${name}")
names;
example = [ "GNOME" "Garden" ];
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
example = {
Idle = {
Inputs = [ "motion" "button-press" ];
IdleSec = 60;
};
Lock = {
OnIdle = true;
OnSleep = true;
};
DPMS.Enable = true;
};
description = ''
The settings associated with the sessiond session. For more
details, please see {manpage}`sessiond.conf(5)`. If not given, it
will use the default configuration from the compiled package.
'';
};
description = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
A one-sentence description of the desktop environment.
'';
default = "${config.fullName} desktop environment";
defaultText = lib.literalExpression "\${<name>.fullName} desktop environment";
example = "A desktop environment featuring a scrolling compositor.";
};
components = lib.mkOption {
type = with lib.types; attrsOf (submoduleWith {
specialArgs = {
inherit utils;
session = {
inherit (config) fullName desktopNames description;
inherit name;
};
};
modules = [ ./component-type.nix ];
});
description = ''
The individual components to be launched with the desktop session.
'';
default = { };
example = lib.literalExpression ''
{
}
'';
};
targetUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.lib) unitConfig;
inherit (utils.systemdUtils.unitOptions) commonUnitOptions;
in
lib.types.submodule [
commonUnitOptions
unitConfig
];
description = ''
systemd target configuration to be generated for
`<name>.target`.
By default, the session target will have all of its components from
{option}`<session>.requiredComponents` under `Wants=` directive. It
also assumes all of them have a target unit at
`''${requiredComponent}.target`.
:::{.note}
This has the same options as {option}`systemd.user.targets.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
:::
'';
defaultText = ''
{
wants = ... # All of the required components as a target unit.
}
'';
};
serviceUnit = lib.mkOption {
type =
let
inherit (utils.systemdUtils.lib) unitConfig serviceConfig;
inherit (utils.systemdUtils.unitOptions) commonUnitOptions serviceOptions;
in
lib.types.submodule [
commonUnitOptions
serviceOptions
serviceConfig
unitConfig
];
description = ''
systemd service configuration to be generated for the sessiond session
itself.
:::{.note}
This has the same options as {option}`systemd.user.services.<name>`
but without certain options from stage 2 counterparts such as
`reloadTriggers` and `restartTriggers`.
By default, this module sets the service unit as part of the respective
target unit (i.e., `PartOf=$COMPONENTID.target`).
On a typical case, you shouldn't mess with much of the dependency
ordering with the service unit. You should configure `targetUnit` for
that instead.
:::
'';
};
extraArgs = lib.mkOption {
type = with lib.types; listOf str;
description = ''
A list of arguments from {program}`sessiond` to be added for the session
script.
'';
example = lib.literalExpression ''
[
"--hooksd=''${./config/sessiond/hooks.d}"
]
'';
};
};
# Append the session argument.
config = {
extraArgs = lib.optional (config.settings != { }) "--config=${sessionSettingsFile}";
targetUnit = {
description = config.description;
requires = [ "${name}.service" ];
wants =
let
componentTargetUnits =
lib.mapAttrsToList (_: component: "${component.id}.target") config.components;
in
componentTargetUnits;
};
serviceUnit = {
description = config.description;
partOf = [ "${name}.target" ];
before = [ "${name}.target" ];
requisite = [ "${name}.target" ];
requires = [ "dbus.socket" ];
after = [ "dbus.socket" ];
serviceConfig = {
Type = lib.mkForce "dbus";
BusName = lib.mkForce "org.sessiond.session1";
ExecStart = lib.mkForce "${lib.getExe' sessiondPkg "sessiond"} ${lib.concatStringsSep " " config.extraArgs}";
Restart = "always";
};
unitConfig = {
RefuseManualStart = true;
RefuseManualStop = true;
};
};
};
}