Compare commits

...

10 Commits

12 changed files with 348 additions and 95 deletions

View File

@ -25,6 +25,7 @@
./deadd-notification-center
./dialect.nix
./dino.nix
./dissent.nix
./element-desktop.nix
./epiphany.nix
./evince.nix
@ -50,7 +51,6 @@
./gpodder.nix
./grimshot.nix
./gthumb.nix
./gtkcord4.nix
./handbrake.nix
./helix.nix
./imagemagick.nix

View File

@ -3,10 +3,10 @@
# - notification sounds can be handled by swaync
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.gtkcord4;
cfg = config.sane.programs.dissent;
in
{
sane.programs.gtkcord4 = {
sane.programs.dissent = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
@ -17,15 +17,15 @@ in
};
};
packageUnwrapped = pkgs.gtkcord4.overrideAttrs (upstream: {
packageUnwrapped = pkgs.dissent.overrideAttrs (upstream: {
postConfigure = (upstream.postConfigure or "") + ''
# gtkcord4 uses go-keyring to interface with the org.freedesktop.secrets provider (i.e. gnome-keyring).
# dissent uses go-keyring to interface with the org.freedesktop.secrets provider (i.e. gnome-keyring).
# go-keyring hardcodes `login.keyring` as the keyring to store secrets in, instead of reading `~/.local/share/keyring/default`.
# `login.keyring` seems to be a special keyring preconfigured (by gnome-keyring) to encrypt everything to the user's password.
# that's redundant with my fs-level encryption and makes the keyring less inspectable,
# so patch gtkcord4 to use Default_keyring instead.
# so patch dissent to use Default_keyring instead.
# see:
# - <https://github.com/diamondburned/gtkcord4/issues/139>
# - <https://github.com/diamondburned/dissent/issues/139>
# - <https://github.com/zalando/go-keyring/issues/46>
substituteInPlace vendor/github.com/zalando/go-keyring/secret_service/secret_service.go \
--replace '"login"' '"Default_keyring"'
@ -51,18 +51,17 @@ in
];
persist.byStore.private = [
".cache/gtkcord4"
".config/gtkcord4" # empty?
".cache/dissent"
".config/dissent" # empty?
];
services.gtkcord4 = {
description = "gtkcord4 Discord client";
services.dissent = {
description = "dissent Discord client";
after = [ "graphical-session.target" ];
# partOf = [ "graphical-session.target" ];
wantedBy = lib.mkIf cfg.config.autostart [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/gtkcord4";
ExecStart = "${cfg.package}/bin/dissent";
Type = "simple";
Restart = "always";
RestartSec = "20s";

View File

@ -59,9 +59,9 @@ in
persist.byStore.private = [
# XXX by default fractal stores its state in ~/.local/share/<build-profile>/<UUID>.
".local/share/hack" # for debug-like builds
".local/share/stable" # for normal releases
".local/share/fractal" # for version 5+, i think?
# ".local/share/hack" # for debug-like builds
# ".local/share/stable" # for normal releases
".local/share/fractal" # for version 5+
];
suggestedPrograms = [ "gnome-keyring" ];

View File

@ -8,7 +8,8 @@ in
sandbox.method = "bwrap";
sandbox.whitelistDbus = [ "user" ];
sandbox.extraRuntimePaths = [
"keyring/control"
"keyring" #< only needs keyring/control, but has to *create* that.
# "keyring/control"
];
sandbox.capabilities = [
# ipc_lock: used to `mlock` the secrets so they don't get swapped out.
@ -54,6 +55,7 @@ in
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/gnome-keyring-daemon --start --foreground --components=secrets";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -m 0700 -p %t/keyring";
Type = "simple";
Restart = "always";
RestartSec = "20s";

View File

@ -164,7 +164,7 @@ assign [app_id="im.dino.Dino"] workspace number 1
assign [app_id="org.gnome.Fractal"] workspace number 1
assign [app_id="geary"] workspace number 1
assign [app_id="signal"] workspace number 1
assign [app_id="so.libdb.gtkcord4"] workspace number 1
assign [app_id="so.libdb.dissent"] workspace number 1
assign [app_id="abaddon"] workspace number 1
# window display settings

View File

@ -250,7 +250,7 @@ in
incoming-im-known-app-name = {
# trigger notification sound on behalf of these IM clients.
app-name = "(Chats|Dino|discord|Element|Fractal|gtkcord4)";
app-name = "(Chats|Dino|discord|dissent|Element|Fractal)";
body = "^(?!Incoming call).*$"; #< don't match Dino Incoming calls
exec = "${fbcli} --event proxied-message-new-instant";
};
@ -403,7 +403,7 @@ in
active = true;
}
# ] ++ lib.optionals config.sane.programs.abaddon.enabled [
# # XXX: disabled in favor of gtkcord4: abaddon has troubles auto-connecting at start
# # XXX: disabled in favor of dissent: abaddon has troubles auto-connecting at start
# {
# type = "toggle";
# label = "󰊴"; # Discord chat client; icons: 󰊴, 🎮
@ -411,12 +411,12 @@ in
# update-command = "${printIsActive}/bin/print-is-active --user abaddon";
# active = true;
# }
] ++ lib.optionals config.sane.programs.gtkcord4.enabled [
] ++ lib.optionals config.sane.programs.dissent.enabled [
{
type = "toggle";
label = "󰊴"; # Discord chat client; icons: 󰊴, 🎮
command = "${systemctl-toggle}/bin/systemctl-toggle --user gtkcord4";
update-command = "${printIsActive}/bin/print-is-active --user gtkcord4";
command = "${systemctl-toggle}/bin/systemctl-toggle --user dissent";
update-command = "${printIsActive}/bin/print-is-active --user dissent";
active = true;
}
] ++ lib.optionals config.sane.programs.signal-desktop.enabled [

View File

@ -60,6 +60,7 @@ in
"delfin" # Jellyfin client
"dialect" # language translation
"dino" # XMPP client
"dissent" # Discord client (formerly known as: gtkcord4)
# "emote"
# "evince" # PDF viewer
# "flare-signal" # gtk4 signal client
@ -82,7 +83,6 @@ in
"gnome-frog" # OCR/QR decoder
"gpodder"
# "gthumb"
"gtkcord4" # Discord client. 2023/11/21: disabled because v0.0.12 leaks memory
# "lemoa" # lemmy app
"libnotify" # for notify-send; debugging
# "lollypop"

View File

@ -14,7 +14,7 @@
./services
./sops.nix
./ssh.nix
./users.nix
./users
./vpn.nix
./warnings.nix
./wowlan.nix

View File

@ -255,18 +255,12 @@ let
'';
};
services = mkOption {
# see: <repo:nixos/nixpkgs:nixos/lib/utils.nix>
# type = utils.systemdUtils.types.services;
# map to listOf attrs so that we can allow multiple assigners to the same service
# w/o worrying about merging at this layer, and defer merging to modules/users instead.
type = types.attrsOf (types.coercedTo types.attrs (a: [ a ]) (types.listOf types.attrs));
type = types.attrsOf types.anything; # options.sane.users.value.type;
default = {};
description = ''
systemd services to define if this package is enabled.
currently only defines USER services -- acts as noop for root-enabled packages.
conventions are similar to `systemd.services` or `sane.users.<user>.services`.
the type at this level is obscured only to as to allow passthrough to `sane.users` w/ proper option merging
user services to define if this package is enabled.
acts as noop for root-enabled packages.
see `sane.users.<user>.services` for options;
'';
};
slowToBuild = mkOption {
@ -542,8 +536,7 @@ let
# conditionally persist relevant user dirs and create files
sane.users = lib.mapAttrs (user: en: lib.optionalAttrs (en && p.enabled) {
inherit (p) persist;
services = lib.mapAttrs (_: lib.mkMerge) p.services;
inherit (p) persist services;
environment = p.env;
fs = lib.mkMerge [
p.fs

View File

@ -1,13 +1,103 @@
{ config, lib, options, sane-lib, utils, ... }:
{ config, lib, options, sane-lib, ... }:
let
inherit (builtins) attrValues;
inherit (lib) count mapAttrs' mapAttrsToList mkIf mkMerge mkOption types;
sane-user-cfg = config.sane.user;
cfg = config.sane.users;
path-lib = sane-lib.path;
userOptions = {
serviceType = with lib; types.submodule {
options = {
# these aoptions are mostly copied from systemd. could be improved.
description = mkOption {
type = types.str;
};
documentation = mkOption {
type = types.listOf types.str;
default = [];
description = ''
references and links for where to find documentation about this service.
'';
};
after = mkOption {
type = types.listOf types.str;
default = [];
};
bindsTo = mkOption {
type = types.listOf types.str;
default = [];
};
before = mkOption {
type = types.listOf types.str;
default = [];
};
wantedBy = mkOption {
type = types.listOf types.str;
default = [];
};
wants = mkOption {
type = types.listOf types.str;
default = [];
};
script = mkOption {
type = types.nullOr types.lines;
default = null;
};
environment = mkOption {
type = types.attrsOf types.str;
default = {};
description = ''
environment variables to set within the service.
'';
};
serviceConfig.Type = mkOption {
type = types.enum [ "dbus" "oneshot" "simple" ];
};
serviceConfig.ExecStart = mkOption {
type = types.nullOr (types.coercedTo types.package toString types.str);
default = null;
};
serviceConfig.ExecStartPre = mkOption {
type = types.nullOr (types.coercedTo types.package toString types.str);
default = null;
};
serviceConfig.ExecStartPost = mkOption {
type = types.nullOr (types.coercedTo types.package toString types.str);
default = null;
};
serviceConfig.ExecStopPost = mkOption {
type = types.nullOr types.str;
default = null;
};
serviceConfig.BusName = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
name of the dbus name this service is expected to register.
only once the name is registered will the service be considered "active".
'';
};
serviceConfig.RemainAfterExit = mkOption {
type = types.bool;
default = false;
};
serviceConfig.Restart = mkOption {
type = types.nullOr (types.enum [ "always" "on-failure" ]);
default = null;
# N.B.: systemd doesn't allow always/on-failure for Type="oneshot" services
};
serviceConfig.RestartSec = mkOption {
type = types.str;
default = "20s";
};
unitConfig.ConditionEnvironment = mkOption {
type = types.nullOr types.str;
default = null;
};
};
};
userOptions = {
options = with lib; {
fs = mkOption {
# map to listOf attrs so that we can allow multiple assigners to the same path
# w/o worrying about merging at this layer, and defer merging to modules/fs instead.
@ -50,16 +140,16 @@ let
# type = utils.systemdUtils.types.services;
# `utils.systemdUtils.types.services` is nearly what we want, but remove `stage2ServiceConfig`,
# as we don't want to force a PATH for every service.
type = types.attrsOf (types.submodule [ utils.systemdUtils.unitOptions.stage2ServiceOptions utils.systemdUtils.lib.unitConfig ]);
type = types.attrsOf serviceType;
default = {};
description = ''
systemd user-services to define for this user.
services to define for this user.
populates files in ~/.config/systemd.
'';
};
};
};
defaultUserOptions = {
defaultUserOptions = with lib; {
options = userOptions.options // {
services = mkOption {
# type = utils.systemdUtils.types.services;
@ -71,7 +161,9 @@ let
};
};
};
userModule = let nixConfig = config; in types.submodule ({ name, config, ... }: {
userModule = let
nixConfig = config;
in with lib; types.submodule ({ name, config, ... }: {
options = userOptions.options // {
default = mkOption {
type = types.bool;
@ -92,7 +184,7 @@ let
config = lib.mkMerge [
# if we're the default user, inherit whatever settings were routed to the default user
(mkIf config.default {
(lib.mkIf config.default {
inherit (sane-user-cfg) fs persist environment;
services = lib.mapAttrs (_: lib.mkMerge) sane-user-cfg.services;
})
@ -126,55 +218,11 @@ let
done
'';
}
{
fs = lib.mkMerge (mapAttrsToList (serviceName: value:
let
# see: <repo:nixos/nixpkgs:nixos/lib/utils.nix>
# see: <repo:nix-community/home-manager:modules/systemd.nix>
cleanName = utils.systemdUtils.lib.mkPathSafeName serviceName;
generatedUnit = utils.systemdUtils.lib.serviceToUnit serviceName (value // {
environment = lib.throwIf (value.path != []) "user service ${serviceName} specifies unsupported 'path' attribute (${builtins.toString value.path})" {
# clear PATH to allow inheriting it from environment.
# otherwise, nixos would force it to `systemd.globalEnvironment.PATH`, which is mostly tools like sed/find/etc.
# clearing PATH here allows user services to inherit whatever PATH the graphical session sets
# (see `dbus-update-activation-environment` call in ~/.config/sway/config),
# which is critical to making it so user services can see user *programs*/packages.
#
# note that systemd provides no way to *append* to the PATH, only to override it (or not).
# nor do they intend to ever support that:
# - <https://github.com/systemd/systemd/issues/1082>
PATH = null;
} // (value.environment or {});
});
#^ generatedUnit contains keys:
# - text
# - aliases (IGNORED)
# - wantedBy
# - requiredBy
# - enabled (IGNORED)
# - overrideStrategy (IGNORED)
# TODO: error if one of the above ignored fields are set
symlinkData = {
text = generatedUnit.text;
targetName = "${cleanName}.service"; # systemd derives unit name from symlink target
};
serviceEntry = {
".config/systemd/user/${serviceName}.service".symlink = symlinkData;
};
wants = builtins.map (wantedBy: {
".config/systemd/user/${wantedBy}.wants/${serviceName}.service".symlink = symlinkData;
}) generatedUnit.wantedBy;
requires = builtins.map (requiredBy: {
".config/systemd/user/${requiredBy}.requires/${serviceName}.service".symlink = symlinkData;
}) generatedUnit.requiredBy;
in lib.mkMerge ([ serviceEntry ] ++ wants ++ requires)
) config.services);
}
];
});
processUser = user: defn:
let
prefixWithHome = mapAttrs' (path: value: {
prefixWithHome = lib.mapAttrs' (path: value: {
name = path-lib.concat [ defn.home path ];
inherit value;
});
@ -192,7 +240,12 @@ let
};
in
{
options = {
imports = [
./s6-rc.nix
./systemd.nix
];
options = with lib; {
sane.users = mkOption {
type = types.attrsOf userModule;
default = {};
@ -224,14 +277,14 @@ in
};
config =
let
configs = mapAttrsToList processUser cfg;
num-default-users = count (u: u.default) (attrValues cfg);
configs = lib.mapAttrsToList processUser cfg;
num-default-users = lib.count (u: u.default) (lib.attrValues cfg);
take = f: {
sane.fs = f.sane.fs;
sane.persist.sys.byPath = f.sane.persist.sys.byPath;
sane.defaultUser = f.sane.defaultUser;
};
in mkMerge [
in lib.mkMerge [
(take (sane-lib.mkTypedMerge take configs))
{
assertions = [

137
modules/users/s6-rc.nix Normal file
View File

@ -0,0 +1,137 @@
{ lib, pkgs, ... }:
let
normalizeName = name: lib.removeSuffix ".service" (lib.removeSuffix ".target" name);
# infers the service type from the arguments and dispatches appropriately
genService = { name, run, depends }: if run != null then
genService' (normalizeName name) "longrun" depends [
(pkgs.writeTextFile {
name = "s6-${name}-run";
destination = "/${normalizeName name}/run";
executable = true;
# text = run;
text = ''
#!/bin/sh
${run}
'';
})
]
else
# TODO: a bundle can totally have dependencies. i can't just map them *all* to contents.
# genService' (normalizeName name) "bundle" [] (
# (builtins.map
# (d: pkgs.writeTextFile {
# name = "s6-${name}-contains-${d}";
# destination = "/${normalizeName name}/contents.d/${normalizeName d}";
# text = "";
# })
# depends
# ) ++ [
# # in case the bundle has no contents, ensure `contents.d` still gets made
# (pkgs.runCommandLocal "s6-${name}-contains.d" {} ''
# mkdir -p $out/"${normalizeName name}"/contents.d
# '')
# ]
# )
genService' (normalizeName name) "bundle" [] [
(pkgs.writeTextFile {
name = "s6-${name}-contents";
destination = "/${normalizeName name}/contents";
text = lib.concatStringsSep "\n" (builtins.map normalizeName depends);
})
]
;
genService' = name: type: depends: others: pkgs.symlinkJoin {
name = "s6-${name}";
paths = others ++ [
(pkgs.writeTextFile {
name = "s6-${name}-type";
destination = "/${name}/type";
text = type;
})
] ++ builtins.map
(d: pkgs.writeTextFile {
name = "s6-${name}-depends-${d}";
destination = "/${name}/dependencies.d/${normalizeName d}";
text = "";
})
depends;
};
# create a directory, containing N subdirectories:
# - svc-a/
# - type
# - run
# - svc-b/
# - type
# - run
# - ...
genServices = svcs: pkgs.symlinkJoin {
name = "s6-user-services";
paths = builtins.map genService svcs;
};
# output is a directory containing:
# - db
# - lock
# - n
# - resolve.cdb
# - servicedirs/
# - svc-a/
# - svc-b/
# - ...
#
# this can then be used by s6-rc-init, like:
# s6-svscan scandir &
# s6-rc-init -c $compiled -l $PWD/live -d $PWD/scandir
# s6-rc -l $PWD/live start svc-a
#
# N.B.: it seems the $compiled dir needs to be rw, for s6 to write lock files within it.
# so `cp` and `chmod -R 600` it, first.
compileServices = sources: with pkgs; stdenv.mkDerivation {
name = "s6-user-services";
src = sources;
nativeBuildInputs = [ s6-rc ];
buildPhase = ''
s6-rc-compile $out $src
'';
};
# transform the `user.services` attrset into a s6 services list.
s6SvcsFromConfigServices = services: lib.mapAttrsToList
(name: service: {
inherit name;
run = service.serviceConfig.ExecStart;
depends = service.wants ++ builtins.attrNames (
lib.filterAttrs (_: cfg: lib.elem name cfg.wantedBy) services
);
})
services
;
# in the systemd service management, these targets are implicitly defined and used
# to accomplish something like run-levels, or service groups.
# map them onto s6 "bundles". their contents are determined via reverse dependency mapping (`wantedBy` of every other service).
implicitServices = {
"default.target" = {
serviceConfig.ExecStart = null;
wants = [];
wantedBy = [];
};
"graphical-session.target" = {
serviceConfig.ExecStart = null;
wants = [];
wantedBy = [];
};
};
in
{
options.sane.users = with lib; mkOption {
type = types.attrsOf (types.submodule ({ config, ...}: let
sources = genServices (s6SvcsFromConfigServices (implicitServices // config.services));
in {
fs.".config/s6/sources".symlink.target = sources;
fs.".config/s6/compiled".symlink.target = compileServices sources;
}));
};
}

69
modules/users/systemd.nix Normal file
View File

@ -0,0 +1,69 @@
{ lib, utils, ... }:
let
# see: <repo:nixos/nixpkgs:nixos/lib/utils.nix>
# see: <repo:nix-community/home-manager:modules/systemd.nix>
mkUnit = serviceName: value: utils.systemdUtils.lib.serviceToUnit serviceName {
inherit (value)
script
wantedBy
;
serviceConfig = lib.filterAttrs (_: v: v != null) value.serviceConfig;
unitConfig = {
inherit (value.unitConfig)
ConditionEnvironment
;
After = value.after;
Before = value.before;
BindsTo = value.bindsTo;
Description = value.description;
Documentation = value.documentation;
Wants = value.wants;
};
environment = {
# clear PATH to allow inheriting it from environment.
# otherwise, nixos would force it to `systemd.globalEnvironment.PATH`, which is mostly tools like sed/find/etc.
# clearing PATH here allows user services to inherit whatever PATH the graphical session sets
# (see `dbus-update-activation-environment` call in ~/.config/sway/config),
# which is critical to making it so user services can see user *programs*/packages.
#
# note that systemd provides no way to *append* to the PATH, only to override it (or not).
# nor do they intend to ever support that:
# - <https://github.com/systemd/systemd/issues/1082>
PATH = null;
} // (value.environment or {});
};
in
{
# create fs entries for every service, in the systemd user dir.
options.sane.users = with lib; mkOption {
type = types.attrsOf (types.submodule ({ config, ...}: {
fs = lib.concatMapAttrs
(serviceName: value: let
cleanName = utils.systemdUtils.lib.mkPathSafeName serviceName;
generatedUnit = mkUnit serviceName value;
#^ generatedUnit contains keys:
# - text
# - aliases (IGNORED)
# - wantedBy
# - requiredBy
# - enabled (IGNORED)
# - overrideStrategy (IGNORED)
# TODO: error if one of the above ignored fields are set
symlinkData = {
text = generatedUnit.text;
targetName = "${cleanName}.service"; # systemd derives unit name from symlink target
};
serviceEntry = {
".config/systemd/user/${serviceName}.service".symlink = symlinkData;
};
wants = builtins.map (wantedBy: {
".config/systemd/user/${wantedBy}.wants/${serviceName}.service".symlink = symlinkData;
}) generatedUnit.wantedBy;
in
lib.mergeAttrsList ([ serviceEntry ] ++ wants)
)
config.services
;
}));
};
}