diff options
author | Kaiden Fey <kookie@spacekookie.de> | 2020-09-21 14:12:32 +0200 |
---|---|---|
committer | Katharina Fey <kookie@spacekookie.de> | 2020-09-21 14:12:32 +0200 |
commit | f80843dd45d7acd563d0a5b014cec3a2ea686fc2 (patch) | |
tree | 87189d873d6f932d85f9c1a480462b37d96cd6a5 /home-manager/modules | |
parent | e0800985dab8f8ebb4cebdfd7e361fd1fafdb2a7 (diff) | |
parent | 9b1b55ba0264a55add4b7b4e022bdc2832b531f6 (diff) |
Merge commit '9b1b55ba0264a55add4b7b4e022bdc2832b531f6'
Diffstat (limited to 'home-manager/modules')
112 files changed, 7908 insertions, 2039 deletions
diff --git a/home-manager/modules/accounts/email.nix b/home-manager/modules/accounts/email.nix index b3a9db947a9..1e7aff94611 100644 --- a/home-manager/modules/accounts/email.nix +++ b/home-manager/modules/accounts/email.nix @@ -106,7 +106,7 @@ let tls = mkOption { type = tlsModule; - default = {}; + default = { }; description = '' Configuration for secure connections. ''; @@ -136,7 +136,7 @@ let tls = mkOption { type = tlsModule; - default = {}; + default = { }; description = '' Configuration for secure connections. ''; @@ -209,7 +209,7 @@ let aliases = mkOption { type = types.listOf (types.strMatching ".*@.*"); - default = []; + default = [ ]; example = [ "webmaster@example.org" "admin@example.org" ]; description = "Alternative email addresses of this account."; }; @@ -276,7 +276,7 @@ let }; }; }; - default = {}; + default = { }; description = '' Standard email folders. ''; @@ -292,7 +292,7 @@ let signature = mkOption { type = signatureModule; - default = {}; + default = { }; description = '' Signature configuration. ''; @@ -332,9 +332,7 @@ let (mkIf (config.flavor == "gmail.com") { userName = mkDefault config.address; - imap = { - host = "imap.gmail.com"; - }; + imap = { host = "imap.gmail.com"; }; smtp = { host = "smtp.gmail.com"; @@ -343,20 +341,14 @@ let }) (mkIf (config.flavor == "runbox.com") { - imap = { - host = "mail.runbox.com"; - }; + imap = { host = "mail.runbox.com"; }; - smtp = { - host = "mail.runbox.com"; - }; + smtp = { host = "mail.runbox.com"; }; }) ]; }; -in - -{ +in { options.accounts.email = { certificatesFile = mkOption { type = types.path; @@ -373,9 +365,7 @@ in default = "${config.home.homeDirectory}/Maildir"; defaultText = "$HOME/Maildir"; apply = p: - if hasPrefix "/" p - then p - else "${config.home.homeDirectory}/${p}"; + if hasPrefix "/" p then p else "${config.home.homeDirectory}/${p}"; description = '' The base directory for account maildir directories. May be a relative path, in which case it is relative the home @@ -384,40 +374,23 @@ in }; accounts = mkOption { - type = types.attrsOf (types.submodule [ - mailAccountOpts - (import ../programs/alot-accounts.nix pkgs) - (import ../programs/astroid-accounts.nix) - (import ../programs/getmail-accounts.nix) - (import ../programs/mbsync-accounts.nix) - (import ../programs/msmtp-accounts.nix) - (import ../programs/neomutt-accounts.nix) - (import ../programs/notmuch-accounts.nix) - (import ../programs/offlineimap-accounts.nix) - ]); - default = {}; + type = types.attrsOf (types.submodule mailAccountOpts); + default = { }; description = "List of email accounts."; }; }; - config = mkIf (cfg.accounts != {}) { + config = mkIf (cfg.accounts != { }) { assertions = [ - ( - let - primaries = - catAttrs "name" - (filter (a: a.primary) - (attrValues cfg.accounts)); - in - { - assertion = length primaries == 1; - message = - "Must have exactly one primary mail account but found " - + toString (length primaries) - + optionalString (length primaries > 1) - (", namely " + concatStringsSep ", " primaries); - } - ) + (let + primaries = + catAttrs "name" (filter (a: a.primary) (attrValues cfg.accounts)); + in { + assertion = length primaries == 1; + message = "Must have exactly one primary mail account but found " + + toString (length primaries) + optionalString (length primaries > 1) + (", namely " + concatStringsSep ", " primaries); + }) ]; }; } diff --git a/home-manager/modules/files.nix b/home-manager/modules/files.nix index 94b3aa565a8..09ecf715497 100644 --- a/home-manager/modules/files.nix +++ b/home-manager/modules/files.nix @@ -39,10 +39,24 @@ in }; config = { + lib.file.mkOutOfStoreSymlink = path: + let + pathStr = toString path; + name = hm.strings.storeFileName (baseNameOf pathStr); + in + pkgs.runCommandLocal name {} ''ln -s ${escapeShellArg pathStr} $out''; + # This verifies that the links we are about to create will not # overwrite an existing file. home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] ( let + # Paths that should be forcibly overwritten by Home Manager. + # Caveat emptor! + forcedPaths = + concatMapStringsSep " " (p: ''"$HOME/${p}"'') + (mapAttrsToList (n: v: v.target) + (filterAttrs (n: v: v.force) cfg)); + check = pkgs.writeText "check" '' . ${./lib-bash/color-echo.sh} @@ -50,12 +64,25 @@ in # considered part of a Home Manager generation. homeFilePattern="$(readlink -e "${builtins.storeDir}")/*-home-manager-files/*" + forcedPaths=(${forcedPaths}) + newGenFiles="$1" shift for sourcePath in "$@" ; do relativePath="''${sourcePath#$newGenFiles/}" targetPath="$HOME/$relativePath" - if [[ -e "$targetPath" \ + + forced="" + for forcedPath in "''${forcedPaths[@]}"; do + if [[ $targetPath == $forcedPath* ]]; then + forced="yeah" + break + fi + done + + if [[ -n $forced ]]; then + $VERBOSE_ECHO "Skipping collision check for $targetPath" + elif [[ -e "$targetPath" \ && ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then if [[ ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then backup="$targetPath.$HOME_MANAGER_BACKUP_EXT" @@ -63,10 +90,10 @@ in errorEcho "Existing file '$backup' would be clobbered by backing up '$targetPath'" collision=1 else - warnEcho "Existing file '$targetPath' is in the way, will be moved to '$backup'" + warnEcho "Existing file '$targetPath' is in the way of '$sourcePath', will be moved to '$backup'" fi else - errorEcho "Existing file '$targetPath' is in the way" + errorEcho "Existing file '$targetPath' is in the way of '$sourcePath'" collision=1 fi fi @@ -142,7 +169,7 @@ in if [[ -e "$newGenFiles/$relativePath" ]] ; then $VERBOSE_ECHO "Checking $targetPath: exists" elif [[ ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then - warnEcho "Path '$targetPath' not link into Home Manager generation. Skipping delete." + warnEcho "Path '$targetPath' does not link into a Home Manager generation. Skipping delete." else $VERBOSE_ECHO "Checking $targetPath: gone (deleting)" $DRY_RUN_CMD rm $VERBOSE_ARG "$targetPath" @@ -197,8 +224,7 @@ in if [[ ! -v oldGenPath || "$oldGenPath" != "$newGenPath" ]] ; then echo "Creating profile generation $newGenNum" - $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG "$newGenPath" "$newGenProfilePath" - $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG $(basename "$newGenProfilePath") "$genProfilePath" + $DRY_RUN_CMD nix-env $VERBOSE_ARG --profile "$genProfilePath" --set "$newGenPath" $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG "$newGenPath" "$newGenGcPath" else echo "No change so reusing latest profile generation $oldGenNum" @@ -231,7 +257,7 @@ in home-files = pkgs.runCommand "home-manager-files" { - nativeBuildInputs = [ pkgs.xlibs.lndir ]; + nativeBuildInputs = [ pkgs.xorg.lndir ]; preferLocalBuild = true; allowSubstitutes = false; } @@ -290,12 +316,15 @@ in } '' + concatStrings ( mapAttrsToList (n: v: '' - insertFile "${sourceStorePath v}" \ - "${v.target}" \ - "${if v.executable == null - then "inherit" - else builtins.toString v.executable}" \ - "${builtins.toString v.recursive}" + insertFile ${ + escapeShellArgs [ + (sourceStorePath v) + v.target + (if v.executable == null + then "inherit" + else toString v.executable) + (toString v.recursive) + ]} '') cfg )); }; diff --git a/home-manager/modules/home-environment.nix b/home-manager/modules/home-environment.nix index 0b2fb8f4ef9..2947433993e 100644 --- a/home-manager/modules/home-environment.nix +++ b/home-manager/modules/home-environment.nix @@ -16,11 +16,35 @@ let ''; }; - address = mkOption { + ctype = mkOption { default = null; type = types.nullOr types.str; description = '' - The language to use for addresses. + Character classification category. + ''; + }; + + numeric = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for numerical values. + ''; + }; + + time = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for formatting times. + ''; + }; + + collate = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for collation (alphabetical ordering). ''; }; @@ -32,6 +56,14 @@ let ''; }; + messages = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for messages, application UI languages, etc. + ''; + }; + paper = mkOption { default = null; type = types.nullOr types.str; @@ -40,13 +72,38 @@ let ''; }; - time = mkOption { + name = mkOption { default = null; type = types.nullOr types.str; description = '' - The language to use for formatting times. + The language to use for personal names. + ''; + }; + + address = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for addresses. + ''; + }; + + telephone = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for telephone numbers. ''; }; + + measurement = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for measurement values. + ''; + }; + }; }; @@ -125,14 +182,23 @@ in options = { home.username = mkOption { type = types.str; - defaultText = "$USER"; + defaultText = literalExample '' + "$USER" for state version < 20.09, + undefined for state version β₯ 20.09 + ''; + example = "jane.doe"; description = "The user's username."; }; home.homeDirectory = mkOption { type = types.path; - defaultText = "$HOME"; - description = "The user's home directory."; + defaultText = literalExample '' + "$HOME" for state version < 20.09, + undefined for state version β₯ 20.09 + ''; + apply = toString; + example = "/home/jane.doe"; + description = "The user's home directory. Must be an absolute path."; }; home.profileDirectory = mkOption { @@ -198,6 +264,16 @@ in ''; }; + home.sessionVariablesExtra = mkOption { + type = types.lines; + default = ""; + internal = true; + description = '' + Extra configuration to add to the + <filename>hm-session-vars.sh</filename> file. + ''; + }; + home.packages = mkOption { type = types.listOf types.package; default = []; @@ -226,8 +302,8 @@ in type = types.bool; description = '' Whether the activation script should start with an empty - <envvar>PATH</envvar> variable. When <literal>false</literal> - then the user's <envvar>PATH</envvar> will be used. + <envar>PATH</envar> variable. When <literal>false</literal> + then the user's <envar>PATH</envar> will be used. ''; }; @@ -317,13 +393,17 @@ in } ]; - home.username = mkDefault (builtins.getEnv "USER"); - home.homeDirectory = mkDefault (builtins.getEnv "HOME"); + home.username = + mkIf (versionOlder config.home.stateVersion "20.09") + (mkDefault (builtins.getEnv "USER")); + home.homeDirectory = + mkIf (versionOlder config.home.stateVersion "20.09") + (mkDefault (builtins.getEnv "HOME")); home.profileDirectory = if config.submoduleSupport.enable && config.submoduleSupport.externalPackageInstall - then config.home.path + then "/etc/profiles/per-user/${cfg.username}" else cfg.homeDirectory + "/.nix-profile"; home.sessionVariables = @@ -332,13 +412,27 @@ in in (maybeSet "LANG" cfg.language.base) // - (maybeSet "LC_ADDRESS" cfg.language.address) + (maybeSet "LC_CTYPE" cfg.language.ctype) + // + (maybeSet "LC_NUMERIC" cfg.language.numeric) + // + (maybeSet "LC_TIME" cfg.language.time) + // + (maybeSet "LC_COLLATE" cfg.language.collate) // (maybeSet "LC_MONETARY" cfg.language.monetary) // + (maybeSet "LC_MESSAGES" cfg.language.messages) + // (maybeSet "LC_PAPER" cfg.language.paper) // - (maybeSet "LC_TIME" cfg.language.time); + (maybeSet "LC_NAME" cfg.language.name) + // + (maybeSet "LC_ADDRESS" cfg.language.address) + // + (maybeSet "LC_TELEPHONE" cfg.language.telephone) + // + (maybeSet "LC_MEASUREMENT" cfg.language.measurement); home.packages = [ # Provide a file holding all session variables. @@ -352,7 +446,7 @@ in export __HM_SESS_VARS_SOURCED=1 ${config.lib.shell.exportAll cfg.sessionVariables} - ''; + '' + cfg.sessionVariablesExtra; } ) ]; diff --git a/home-manager/modules/lib-bash/activation-init.sh b/home-manager/modules/lib-bash/activation-init.sh index 5cdb66e5920..f95008ee75b 100755 --- a/home-manager/modules/lib-bash/activation-init.sh +++ b/home-manager/modules/lib-bash/activation-init.sh @@ -1,15 +1,19 @@ #!/usr/bin/env bash function setupVars() { - local profilesPath="/nix/var/nix/profiles/per-user/$USER" - local gcPath="/nix/var/nix/gcroots/per-user/$USER" - local greatestGenNum + local nixStateDir="${NIX_STATE_DIR:-/nix/var/nix}" + local profilesPath="$nixStateDir/profiles/per-user/$USER" + local gcPath="$nixStateDir/gcroots/per-user/$USER" + + genProfilePath="$profilesPath/home-manager" + newGenPath="@GENERATION_DIR@"; + newGenGcPath="$gcPath/current-home" + local greatestGenNum greatestGenNum=$( \ - find "$profilesPath" -name 'home-manager-*-link' \ - | sed 's/^.*-\([0-9]*\)-link$/\1/' \ - | sort -rn \ - | head -1) + nix-env --list-generations --profile "$genProfilePath" \ + | tail -1 \ + | sed -E 's/ *([[:digit:]]+) .*/\1/') if [[ -n $greatestGenNum ]] ; then oldGenNum=$greatestGenNum @@ -18,15 +22,15 @@ function setupVars() { newGenNum=1 fi - if [[ -e $gcPath/current-home ]] ; then - oldGenPath="$(readlink -e "$gcPath/current-home")" + if [[ -e $profilesPath/home-manager ]] ; then + oldGenPath="$(readlink -e "$profilesPath/home-manager")" fi $VERBOSE_ECHO "Sanity checking oldGenNum and oldGenPath" if [[ -v oldGenNum && ! -v oldGenPath || ! -v oldGenNum && -v oldGenPath ]]; then - errorEcho "Invalid profile number and GC root values! These must be" - errorEcho "either both empty or both set but are now set to" + errorEcho "Invalid profile number and current profile values! These" + errorEcho "must be either both empty or both set but are now set to" errorEcho " '${oldGenNum:-}' and '${oldGenPath:-}'" errorEcho "If you don't mind losing previous profile generations then" errorEcho "the easiest solution is probably to run" @@ -35,12 +39,6 @@ function setupVars() { errorEcho "and trying home-manager switch again. Good luck!" exit 1 fi - - - genProfilePath="$profilesPath/home-manager" - newGenPath="@GENERATION_DIR@"; - newGenProfilePath="$profilesPath/home-manager-$newGenNum-link" - newGenGcPath="$gcPath/current-home" } if [[ -v VERBOSE ]]; then @@ -53,6 +51,11 @@ fi echo "Starting home manager activation" +# Verify that we can connect to the Nix store and/or daemon. This will +# also create the necessary directories in profiles and gcroots. +$VERBOSE_ECHO "Sanity checking Nix" +nix-build --expr '{}' --no-out-link + setupVars if [[ -v DRY_RUN ]] ; then @@ -78,6 +81,5 @@ else fi $VERBOSE_ECHO " newGenPath=$newGenPath" $VERBOSE_ECHO " newGenNum=$newGenNum" -$VERBOSE_ECHO " newGenProfilePath=$newGenProfilePath" $VERBOSE_ECHO " newGenGcPath=$newGenGcPath" $VERBOSE_ECHO " genProfilePath=$genProfilePath" diff --git a/home-manager/modules/lib/default.nix b/home-manager/modules/lib/default.nix index 6b2bbacc249..7c2c72f709c 100644 --- a/home-manager/modules/lib/default.nix +++ b/home-manager/modules/lib/default.nix @@ -16,8 +16,10 @@ rec { entryBefore = d.dagEntryBefore; }; + gvariant = import ./gvariant.nix { inherit lib; }; + maintainers = import ./maintainers.nix; strings = import ./strings.nix { inherit lib; }; - types = import ./types.nix { inherit dag lib; }; + types = import ./types.nix { inherit dag gvariant lib; }; shell = import ./shell.nix { inherit lib; }; zsh = import ./zsh.nix { inherit lib; }; diff --git a/home-manager/modules/lib/file-type.nix b/home-manager/modules/lib/file-type.nix index 3096a6d37bb..56a3a1286a0 100644 --- a/home-manager/modules/lib/file-type.nix +++ b/home-manager/modules/lib/file-type.nix @@ -11,7 +11,7 @@ with lib; # Arguments: # - basePathDesc docbook compatible description of the base path # - basePath the file base path - fileType = basePathDesc: basePath: types.loaOf (types.submodule ( + fileType = basePathDesc: basePath: types.attrsOf (types.submodule ( { name, config, ... }: { options = { target = mkOption { @@ -21,6 +21,7 @@ with lib; absPath = if hasPrefix "/" p then p else "${basePath}/${p}"; in removePrefix (homeDirectory + "/") absPath; + defaultText = literalExample "<name>"; description = '' Path to target file relative to ${basePathDesc}. ''; @@ -29,17 +30,20 @@ with lib; text = mkOption { default = null; type = types.nullOr types.lines; - description = "Text of the file."; + description = '' + Text of the file. If this option is null then + <link linkend="opt-home.file._name_.source">home.file.<name?>.source</link> + must be set. + ''; }; source = mkOption { type = types.path; description = '' - Path of the source file. The file name must not start - with a period since Nix will not allow such names in - the Nix store. - </para><para> - This may refer to a directory. + Path of the source file or directory. If + <link linkend="opt-home.file._name_.text">home.file.<name?>.text</link> + is non-null then this option will automatically point to a file + containing that text. ''; }; @@ -80,6 +84,18 @@ with lib; into place. ''; }; + + force = mkOption { + type = types.bool; + default = false; + visible = false; + description = '' + Whether the target path should be unconditionally replaced + by the managed file source. Warning, this will silently + delete the target regardless of whether it is a file or + link. + ''; + }; }; config = { diff --git a/home-manager/modules/lib/gvariant.nix b/home-manager/modules/lib/gvariant.nix new file mode 100644 index 00000000000..92aa7d98371 --- /dev/null +++ b/home-manager/modules/lib/gvariant.nix @@ -0,0 +1,156 @@ +# A partial and basic implementation of GVariant formatted strings. +# +# Note, this API is not considered fully stable and it might therefore +# change in backwards incompatible ways without prior notice. + +{ lib }: + +with lib; + +let + + mkPrimitive = t: v: { + _type = "gvariant"; + type = t; + value = v; + __toString = self: "@${self.type} ${toString self.value}"; + }; + + type = { + arrayOf = t: "a${t}"; + maybeOf = t: "m${t}"; + tupleOf = ts: "(${concatStrings ts})"; + string = "s"; + boolean = "b"; + uchar = "y"; + int16 = "n"; + uint16 = "q"; + int32 = "i"; + uint32 = "u"; + int64 = "x"; + uint64 = "t"; + double = "d"; + }; + + # Returns the GVariant type of a given Nix value. If no type can be + # found for the value then the empty string is returned. + typeOf = v: + with type; + if builtins.isBool v then + boolean + else if builtins.isInt v then + int32 + else if builtins.isFloat v then + double + else if builtins.isString v then + string + else if builtins.isList v then + let elemType = elemTypeOf v; + in if elemType == "" then "" else arrayOf elemType + else if builtins.isAttrs v && v ? type then + v.type + else + ""; + + elemTypeOf = vs: + if builtins.isList vs then + if vs == [ ] then "" else typeOf (head vs) + else + ""; + + mkMaybe = elemType: elem: + mkPrimitive (type.maybeOf elemType) elem // { + __toString = self: + if self.value == null then + "@${self.type} nothing" + else + "just ${toString self.value}"; + }; + +in rec { + + inherit type typeOf; + + isArray = hasPrefix "a"; + isMaybe = hasPrefix "m"; + isTuple = hasPrefix "("; + + # Returns the GVariant value that most closely matches the given Nix + # value. If no GVariant value can be found then `null` is returned. + # + # No support for dictionaries, maybe types, or variants. + mkValue = v: + if builtins.isBool v then + mkBoolean v + else if builtins.isInt v then + mkInt32 v + else if builtins.isFloat v then + mkDouble v + else if builtins.isString v then + mkString v + else if builtins.isList v then + if v == [ ] then mkArray type.string [ ] else mkArray (elemTypeOf v) v + else if builtins.isAttrs v && (v._type or "") == "gvariant" then + v + else + null; + + mkArray = elemType: elems: + mkPrimitive (type.arrayOf elemType) (map mkValue elems) // { + __toString = self: + "@${self.type} [${concatMapStringsSep "," toString self.value}]"; + }; + + mkEmptyArray = elemType: mkArray elemType [ ]; + + mkNothing = elemType: mkMaybe elemType null; + + mkJust = elem: let gvarElem = mkValue elem; in mkMaybe gvarElem.type gvarElem; + + mkTuple = elems: + let + gvarElems = map mkValue elems; + tupleType = type.tupleOf (map (e: e.type) gvarElems); + in mkPrimitive tupleType gvarElems // { + __toString = self: + "@${self.type} (${concatMapStringsSep "," toString self.value})"; + }; + + mkBoolean = v: + mkPrimitive type.boolean v // { + __toString = self: if self.value then "true" else "false"; + }; + + mkString = v: + mkPrimitive type.string v // { + __toString = self: "'${escape [ "'" "\\" ] self.value}'"; + }; + + mkObjectpath = v: + mkPrimitive type.string v // { + __toString = self: "objectpath '${escape [ "'" ] self.value}'"; + }; + + mkUchar = mkPrimitive type.uchar; + + mkInt16 = mkPrimitive type.int16; + + mkUint16 = mkPrimitive type.uint16; + + mkInt32 = v: + mkPrimitive type.int32 v // { + __toString = self: toString self.value; + }; + + mkUint32 = mkPrimitive type.uint32; + + mkInt64 = mkPrimitive type.int64; + + mkUint64 = mkPrimitive type.uint64; + + mkDouble = v: + mkPrimitive type.double v // { + __toString = self: toString self.value; + }; + +} diff --git a/home-manager/modules/lib/maintainers.nix b/home-manager/modules/lib/maintainers.nix new file mode 100644 index 00000000000..8cb8781228b --- /dev/null +++ b/home-manager/modules/lib/maintainers.nix @@ -0,0 +1,44 @@ +# Home Manager maintainers. +# +# This attribute set contains Home Manager module maintainers that do +# not have an entry in the Nixpkgs maintainer list [1]. Entries here +# are expected to be follow the same format as described in [1]. +# +# [1] https://github.com/NixOS/nixpkgs/blob/fca0d6e093c82b31103dc0dacc48da2a9b06e24b/maintainers/maintainer-list.nix#LC1 + +{ + justinlovinger = { + name = "Justin Lovinger"; + email = "git@justinlovinger.com"; + github = "JustinLovinger"; + githubId = 7183441; + }; + owm111 = { + email = "7798336+owm111@users.noreply.github.com"; + name = "Owen McGrath"; + github = "owm111"; + githubId = 7798336; + }; + cwyc = { + email = "cwyc@users.noreply.github.com"; + name = "cwyc"; + github = "cwyc"; + githubId = 16950437; + }; + berbiche = { + name = "Nicolas Berbiche"; + email = "berbiche@users.noreply.github.com"; + github = "berbiche"; + githubId = 20448408; + keys = [{ + longkeyid = "rsa4096/0xB461292445C6E696"; + fingerprint = "D446 E58D 87A0 31C7 EC15 88D7 B461 2924 45C6 E696"; + }]; + }; + olmokramer = { + name = "Olmo Kramer"; + email = "olmokramer@users.noreply.github.com"; + github = "olmokramer"; + githubId = 3612514; + }; +} diff --git a/home-manager/modules/lib/types-dag.nix b/home-manager/modules/lib/types-dag.nix index 4dbdb907b0e..2efb12645d4 100644 --- a/home-manager/modules/lib/types-dag.nix +++ b/home-manager/modules/lib/types-dag.nix @@ -7,16 +7,25 @@ let isDagEntry = e: isAttrs e && (e ? data) && (e ? after) && (e ? before); dagContentType = elemType: - types.submodule { + types.submodule ({ name, ... }: { options = { data = mkOption { type = elemType; }; after = mkOption { type = with types; uniq (listOf str); }; before = mkOption { type = with types; uniq (listOf str); }; }; - }; + config = mkIf (elemType.name == "submodule") { + data._module.args.dagName = name; + }; + }); -in { +in rec { # A directed acyclic graph of some inner type. + # + # Note, if the element type is a submodule then the `name` argument + # will always be set to the string "data" since it picks up the + # internal structure of the DAG values. To give access to the + # "actual" attribute name a new submodule argument is provided with + # the name `dagName`. dagOf = elemType: let convertAllToDags = let @@ -51,34 +60,40 @@ in { let padWidth = stringLength (toString (length list)); in fixedWidthNumber padWidth i; - convertAllToDags = defs: + convertAll = loc: defs: let - convertAttrValue = n: v: - if isDagEntry v then v else dag.entryAnywhere v; - - convertListValue = namePrefix: vs: + convertListValue = namePrefix: def: let + vs = def.value; pad = paddedIndexStr vs; - makeEntry = i: v: - nameValuePair "${namePrefix}.${pad i}" (dag.entryAnywhere v); - in listToAttrs (imap1 makeEntry vs); + makeEntry = i: v: nameValuePair "${namePrefix}.${pad i}" v; + warning = '' + In file ${def.file} + a list is being assigned to the option '${ + concatStringsSep "." loc + }'. + This will soon be an error due to the list form being deprecated. + Please use the attribute set form instead with DAG functions to + express the desired order of entries. + ''; + in warn warning (listToAttrs (imap1 makeEntry vs)); - convertValue = i: value: - if isList value then - convertListValue "unnamed-${paddedIndexStr defs i}" value + convertValue = i: def: + if isList def.value then + convertListValue "unnamed-${paddedIndexStr defs i}" def else - mapAttrs convertAttrValue value; - in imap1 (i: def: def // { value = convertValue i def.value; }) defs; + def.value; + in imap1 (i: def: def // { value = convertValue i def; }) defs; - attrEquivalent = types.attrsOf (dagContentType elemType); + dagType = dagOf elemType; in mkOptionType rec { - name = "dagOf"; - description = "DAG of ${elemType.description}s"; - check = x: isAttrs x || isList x; - merge = loc: defs: attrEquivalent.merge loc (convertAllToDags defs); - getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<name>" ]); - getSubModules = elemType.getSubModules; - substSubModules = m: dagOf (elemType.substSubModules m); + name = "listOrDagOf"; + description = "list or DAG of ${elemType.description}s"; + check = x: isList x || dagType.check x; + merge = loc: defs: dagType.merge loc (convertAll loc defs); + getSubOptions = dagType.getSubOptions; + getSubModules = dagType.getSubModules; + substSubModules = m: listOrDagOf (elemType.substSubModules m); functor = (defaultFunctor name) // { wrapped = elemType; }; }; } diff --git a/home-manager/modules/lib/types.nix b/home-manager/modules/lib/types.nix index 78a875f519e..64a6b4a34fa 100644 --- a/home-manager/modules/lib/types.nix +++ b/home-manager/modules/lib/types.nix @@ -1,4 +1,5 @@ -{ lib, dag ? import ./dag.nix { inherit lib; } }: +{ lib, dag ? import ./dag.nix { inherit lib; } +, gvariant ? import ./gvariant.nix { inherit lib; } }: with lib; @@ -6,31 +7,84 @@ let typesDag = import ./types-dag.nix { inherit dag lib; }; -in + # Needed since the type is called gvariant and its merge attribute + # must refer back to the type. + gvar = gvariant; -{ +in rec { inherit (typesDag) dagOf listOrDagOf; selectorFunction = mkOptionType { name = "selectorFunction"; - description = - "Function that takes an attribute set and returns a list" + description = "Function that takes an attribute set and returns a list" + " containing a selection of the values of the input set"; check = isFunction; - merge = _loc: defs: - as: concatMap (select: select as) (getValues defs); + merge = _loc: defs: as: concatMap (select: select as) (getValues defs); }; overlayFunction = mkOptionType { name = "overlayFunction"; - description = - "An overlay function, takes self and super and returns" + description = "An overlay function, takes self and super and returns" + " an attribute set overriding the desired attributes."; check = isFunction; - merge = _loc: defs: - self: super: - foldl' (res: def: mergeAttrs res (def.value self super)) {} defs; + merge = _loc: defs: self: super: + foldl' (res: def: mergeAttrs res (def.value self super)) { } defs; + }; + + fontType = types.submodule { + options = { + package = mkOption { + type = types.nullOr types.package; + default = null; + example = literalExample "pkgs.dejavu_fonts"; + description = '' + Package providing the font. This package will be installed + to your profile. If <literal>null</literal> then the font + is assumed to already be available in your profile. + ''; + }; + + name = mkOption { + type = types.str; + example = "DejaVu Sans 8"; + description = '' + The family name and size of the font within the package. + ''; + }; + }; + }; + + gvariant = mkOptionType rec { + name = "gvariant"; + description = "GVariant value"; + check = v: gvar.mkValue v != null; + merge = loc: defs: + let + vdefs = map (d: d // { value = gvar.mkValue d.value; }) defs; + vals = map (d: d.value) vdefs; + defTypes = map (x: x.type) vals; + sameOrNull = x: y: if x == y then y else null; + # A bit naive to just check the first entryβ¦ + sharedDefType = foldl' sameOrNull (head defTypes) defTypes; + allChecked = all (x: check x) vals; + in if sharedDefType == null then + throw ("Cannot merge definitions of `${showOption loc}' with" + + " mismatched GVariant types given in" + + " ${showFiles (getFiles defs)}.") + else if gvar.isArray sharedDefType && allChecked then + (types.listOf gvariant).merge loc + (map (d: d // { value = d.value.value; }) vdefs) + else if gvar.isTuple sharedDefType && allChecked then + mergeOneOption loc defs + else if gvar.isMaybe sharedDefType && allChecked then + mergeOneOption loc defs + else if gvar.type.string == sharedDefType && allChecked then + types.str.merge loc defs + else if gvar.type.double == sharedDefType && allChecked then + types.float.merge loc defs + else + mergeDefaultOption loc defs; }; } diff --git a/home-manager/modules/misc/dconf.nix b/home-manager/modules/misc/dconf.nix index f5c9bf71456..5fc7748a76b 100644 --- a/home-manager/modules/misc/dconf.nix +++ b/home-manager/modules/misc/dconf.nix @@ -9,22 +9,7 @@ let toDconfIni = generators.toINI { mkKeyValue = mkIniKeyValue; }; mkIniKeyValue = key: value: - let - tweakVal = v: - if isString v then "'${v}'" - else if isList v then tweakList v - else if isBool v then (if v then "true" else "false") - else toString v; - - # Assume empty list is a list of strings, see #769 - tweakList = v: - if v == [] then "@as []" - else "[" + concatMapStringsSep "," tweakVal v + "]"; - - in - "${key}=${tweakVal value}"; - - primitive = with types; either bool (either int (either float str)); + "${key}=${toString (hm.gvariant.mkValue value)}"; in @@ -43,8 +28,7 @@ in }; settings = mkOption { - type = with types; - attrsOf (attrsOf (either primitive (listOf primitive))); + type = with types; attrsOf (attrsOf hm.types.gvariant); default = {}; example = literalExample '' { @@ -53,6 +37,7 @@ in show-thousands = true; base = 10; word-size = 64; + window-position = lib.hm.gvariant.mkTuple [100 100]; }; } ''; @@ -76,9 +61,9 @@ in fi if [[ -v DRY_RUN ]]; then - echo $DCONF_DBUS_RUN_SESSION ${pkgs.gnome3.dconf}/bin/dconf load / "<" ${iniFile} + echo $DCONF_DBUS_RUN_SESSION ${pkgs.dconf}/bin/dconf load / "<" ${iniFile} else - $DCONF_DBUS_RUN_SESSION ${pkgs.gnome3.dconf}/bin/dconf load / < ${iniFile} + $DCONF_DBUS_RUN_SESSION ${pkgs.dconf}/bin/dconf load / < ${iniFile} fi unset DCONF_DBUS_RUN_SESSION diff --git a/home-manager/modules/misc/debug.nix b/home-manager/modules/misc/debug.nix new file mode 100644 index 00000000000..d27d496b423 --- /dev/null +++ b/home-manager/modules/misc/debug.nix @@ -0,0 +1,26 @@ +{ config, pkgs, lib, ... }: + +with lib; + +{ + options.home = { + enableDebugInfo = mkEnableOption "" // { + description = '' + Some Nix-packages provide debug symbols for + <command>gdb</command> in the <literal>debug</literal>-output. + This option ensures that those are automatically fetched from + the binary cache if available and <command>gdb</command> is + configured to find those symbols. + ''; + }; + }; + + config = mkIf config.home.enableDebugInfo { + home.extraOutputsToInstall = [ "debug" ]; + + home.sessionVariables = { + NIX_DEBUG_INFO_DIRS = + "$NIX_DEBUG_INFO_DIRS\${NIX_DEBUG_INFO_DIRS:+:}${config.home.profileDirectory}/lib/debug"; + }; + }; +} diff --git a/home-manager/modules/misc/gtk.nix b/home-manager/modules/misc/gtk.nix index 1222db4ecab..bf25aaaf664 100644 --- a/home-manager/modules/misc/gtk.nix +++ b/home-manager/modules/misc/gtk.nix @@ -11,44 +11,22 @@ let toGtk3Ini = generators.toINI { mkKeyValue = key: value: let - value' = - if isBool value then (if value then "true" else "false") - else toString value; - in - "${key}=${value'}"; + value' = if isBool value then + (if value then "true" else "false") + else + toString value; + in "${key}=${value'}"; }; formatGtk2Option = n: v: let - v' = - if isBool v then (if v then "true" else "false") - else if isString v then "\"${v}\"" - else toString v; - in - "${n} = ${v'}"; - - fontType = types.submodule { - options = { - package = mkOption { - type = types.nullOr types.package; - default = null; - example = literalExample "pkgs.dejavu_fonts"; - description = '' - Package providing the font. This package will be installed - to your profile. If <literal>null</literal> then the font - is assumed to already be available in your profile. - ''; - }; - - name = mkOption { - type = types.str; - example = "DejaVu Sans 8"; - description = '' - The family name and size of the font within the package. - ''; - }; - }; - }; + v' = if isBool v then + (if v then "true" else "false") + else if isString v then + ''"${v}"'' + else + toString v; + in "${n} = ${v'}"; themeType = types.submodule { options = { @@ -71,13 +49,11 @@ let }; }; -in - -{ +in { meta.maintainers = [ maintainers.rycee ]; imports = [ - (mkRemovedOptionModule ["gtk" "gtk3" "waylandSupport"] '' + (mkRemovedOptionModule [ "gtk" "gtk3" "waylandSupport" ] '' This options is not longer needed and can be removed. '') ]; @@ -87,7 +63,7 @@ in enable = mkEnableOption "GTK 2/3 configuration"; font = mkOption { - type = types.nullOr fontType; + type = types.nullOr hm.types.fontType; default = null; description = '' The font to use in GTK+ 2/3 applications. @@ -119,10 +95,20 @@ in }; gtk3 = { + bookmarks = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "file:///home/jane/Documents" ]; + description = "Bookmarks in the sidebar of the GTK file browser"; + }; + extraConfig = mkOption { type = with types; attrsOf (either bool (either int str)); - default = {}; - example = { gtk-cursor-blink = false; gtk-recent-files-limit = 20; }; + default = { }; + example = { + gtk-cursor-blink = false; + gtk-recent-files-limit = 20; + }; description = '' Extra configuration options to add to <filename>~/.config/gtk-3.0/settings.ini</filename>. @@ -141,48 +127,38 @@ in }; }; - config = mkIf cfg.enable ( - let - ini = - optionalAttrs (cfg.font != null) - { gtk-font-name = cfg.font.name; } - // - optionalAttrs (cfg.theme != null) - { gtk-theme-name = cfg.theme.name; } - // - optionalAttrs (cfg.iconTheme != null) - { gtk-icon-theme-name = cfg.iconTheme.name; }; - - dconfIni = - optionalAttrs (cfg.font != null) - { font-name = cfg.font.name; } - // - optionalAttrs (cfg.theme != null) - { gtk-theme = cfg.theme.name; } - // - optionalAttrs (cfg.iconTheme != null) - { icon-theme = cfg.iconTheme.name; }; - - optionalPackage = opt: - optional (opt != null && opt.package != null) opt.package; - in - { - home.packages = - optionalPackage cfg.font - ++ optionalPackage cfg.theme - ++ optionalPackage cfg.iconTheme; - - home.file.".gtkrc-2.0".text = - concatStringsSep "\n" ( - mapAttrsToList formatGtk2Option ini - ) + "\n" + cfg2.extraConfig; - - xdg.configFile."gtk-3.0/settings.ini".text = - toGtk3Ini { Settings = ini // cfg3.extraConfig; }; - - xdg.configFile."gtk-3.0/gtk.css".text = cfg3.extraCss; - - dconf.settings."org/gnome/desktop/interface" = dconfIni; - } - ); + config = mkIf cfg.enable (let + ini = optionalAttrs (cfg.font != null) { gtk-font-name = cfg.font.name; } + // optionalAttrs (cfg.theme != null) { gtk-theme-name = cfg.theme.name; } + // optionalAttrs (cfg.iconTheme != null) { + gtk-icon-theme-name = cfg.iconTheme.name; + }; + + dconfIni = optionalAttrs (cfg.font != null) { font-name = cfg.font.name; } + // optionalAttrs (cfg.theme != null) { gtk-theme = cfg.theme.name; } + // optionalAttrs (cfg.iconTheme != null) { + icon-theme = cfg.iconTheme.name; + }; + + optionalPackage = opt: + optional (opt != null && opt.package != null) opt.package; + in { + home.packages = optionalPackage cfg.font ++ optionalPackage cfg.theme + ++ optionalPackage cfg.iconTheme; + + home.file.".gtkrc-2.0".text = + concatStringsSep "\n" (mapAttrsToList formatGtk2Option ini) + "\n" + + cfg2.extraConfig; + + xdg.configFile."gtk-3.0/settings.ini".text = + toGtk3Ini { Settings = ini // cfg3.extraConfig; }; + + xdg.configFile."gtk-3.0/gtk.css".text = cfg3.extraCss; + + xdg.configFile."gtk-3.0/bookmarks" = mkIf (cfg3.bookmarks != [ ]) { + text = concatStringsSep "\n" cfg3.bookmarks; + }; + + dconf.settings."org/gnome/desktop/interface" = dconfIni; + }); } diff --git a/home-manager/modules/misc/news.nix b/home-manager/modules/misc/news.nix index 6b01617fc55..b031af99685 100644 --- a/home-manager/modules/misc/news.nix +++ b/home-manager/modules/misc/news.nix @@ -452,7 +452,7 @@ in { time = "2017-12-11T17:23:12+00:00"; - condition = config.home.activation ? reloadSystemD; + condition = config.home.activation ? reloadSystemd; message = '' The Boolean option 'systemd.user.startServices' is now available. When enabled the current naive systemd unit @@ -1318,6 +1318,361 @@ in A new module is available: 'programs.neomutt'. ''; } + + { + time = "2020-02-23T10:19:48+00:00"; + message = '' + A new module is available: 'programs.kitty'. + ''; + } + + { + time = "2020-02-26T21:20:55+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'wayland.windowManager.sway' + ''; + } + + { + time = "2020-03-04T18:55:03+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'programs.abook' + ''; + } + + { + time = "2020-03-07T11:43:26+00:00"; + condition = config.programs.fish.enable; + message = '' + The option 'programs.fish.functions' has been reworked in + order to support all available flags, such as + '--description', '--on-event', and more. + ''; + } + + { + time = "2020-03-07T13:11:43+00:00"; + condition = hostPlatform.isLinux; + message = '' + The NixOS module has a new option: 'home-manager.useGlobalPkgs'. + + This enables using the system configuration's 'pkgs' + argument in Home Manager. + + To learn more, see the installation section of the manual + + https://rycee.gitlab.io/home-manager/#sec-install-nixos-module + ''; + } + + { + time = "2020-03-07T14:12:50+00:00"; + message = '' + A new module is available: 'programs.lieer'. + ''; + } + + { + time = "2020-03-07T14:12:50+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.lieer'. + ''; + } + + { + time = "2020-03-15T16:55:28+00:00"; + condition = config.programs.firefox.enable; + message = '' + In anticipation of Firefox dropping support for extension + sideloading[1], we now install extensions directly to + Firefox profiles managed through Home Manager's + + 'programs.firefox.profiles' + + option. + + Unfortunately this will most likely trigger an "Existing + file is in the way" error when activating your configuration + since Firefox keeps a copy of the add-on in the location + Home Manager wants to overwrite. If this is the case, remove + the listed '.xpi' files and try again. + + This change also means that extensions installed through + Home Manager may disappear from unmanaged profiles in future + Firefox releases. + + [1] https://blog.mozilla.org/addons/2019/10/31/firefox-to-discontinue-sideloaded-extensions/ + ''; + } + + { + time = "2020-03-17T21:56:26+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.keynav'. + ''; + } + + { + time = "2020-03-24T22:17:20+00:00"; + condition = config.services.compton.enable; + message = '' + The 'services.compton' module has been deprecated and + instead the new module 'services.picom' should be used. This + is because Nixpkgs no longer packages compton, and instead + packages the (mostly) compatible fork called picom. + + The 'services.compton' and 'services.picom' modules have a + few differences: + + - 'services.picom' has a new 'experimentalBackends' + option. + + - 'vSync' is now a boolean value on 'services.picom', as + opposed to the string in 'services.compton'. + + Migrating to the new picom service is simple - just change + all references to 'services.compton' to 'services.picom', + and adhere to the above changes. + + The deprecated 'services.compton' will eventually be removed + in the future. Please update your configurations to use + 'services.picom' as soon as possible. + ''; + } + + { + time = "2020-04-08T09:33:05+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'targets.genericLinux'. + + When enabled, this module will configure various settings + and environment variables to make Home Manager and programs + installed through Nix work better on GNU/Linux distributions + other than NixOS. + + It should not be enabled if your Home Manager configuration + is deployed on a NixOS host. + ''; + } + + { + time = "2020-04-08T11:51:15+00:00"; + message = '' + A new module is available: 'programs.qutebrowser' + ''; + } + + { + time = "2020-04-09T09:19:38+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.mako' + ''; + } + + { + time = "2020-04-23T19:45:26+00:00"; + message = '' + A new module is available: 'programs.lf' + ''; + } + + { + time = "2020-04-26T13:46:28+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.pulseeffects' + ''; + } + + { + time = "2020-05-03T11:13:07+00:00"; + message = '' + A new module is available: 'programs.i3status' + ''; + } + + { + time = "2020-05-03T11:21:42+00:00"; + message = '' + A new module is available: 'programs.aria2' + ''; + } + + { + time = "2020-05-04T21:19:43+00:00"; + condition = config.programs.git.enable; + message = '' + The Git module now supports the 'delta' syntax highlighter. + + It can be enabled through the option 'programs.git.delta.enable'. + ''; + } + + { + time = "2020-05-12T20:09:54+00:00"; + message = '' + A new module is available: 'programs.dircolors' + ''; + } + + { + time = "2020-05-26T17:13:58+00:00"; + message = '' + A new module is available: 'programs.zoxide' + ''; + } + + { + time = "2020-06-03T17:46:11+00:00"; + condition = config.programs.ssh.enable; + message = '' + The ssh module now supports the 'ServerAliveCountMax' option + both globally through + + programs.ssh.serverAliveCountMax + + and per match blocks + + programs.ssh.matchBlocks.<name>.serverAliveCountMax + ''; + } + + { + time = "2020-06-11T18:06:37+00:00"; + condition = hostPlatform.isLinux && config.services.emacs.enable; + message = '' + The Emacs service now supports systemd socket activation. + + It can be enabled through the option 'services.emacs.socketActivation.enable'. + ''; + } + + { + time = "2020-06-12T17:48:01+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.clipmenu' + ''; + } + + { + time = "2020-06-12T07:08:09+00:00"; + condition = config.programs.bash.enable; + message = '' + A new module is available: 'programs.powerline-go' + ''; + } + + { + time = "2020-06-14T13:30:19+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'service.fluidsynth' + ''; + } + + { + time = "2020-06-17T22:17:52+00:00"; + condition = config.programs.git.enable; + message = '' + Since May 1, 2020 string values in Git configurations are + automatically escaped. If you have any manually escaped characters, + then you may need to restore them to their unescaped form to avoid + double escaping. + + In other words, if you now have something along the lines of + + programs.git.aliases.hello = '''"!echo $'Hello\\nWorld'"'''; + + you must replace it by the unescaped form + + programs.git.aliases.hello = "!echo $'Hello\nWorld'"; + + Apologies for the belated notification! + ''; + } + + { + time = "2020-06-23T20:06:39+00:00"; + message = '' + A new module is available: 'programs.ne' + ''; + } + + { + time = "2020-07-24T15:03:11+00:00"; + message = '' + A new module is available: 'programs.nushell'. + ''; + } + + { + time = "2020-07-25T21:04:59+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.dropbox'. + ''; + } + + { + time = "2020-08-13T22:15:27+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'programs.waybar' + ''; + } + + { + time = "2020-08-14T22:44:20+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.kanshi' + ''; + } + + { + time = "2020-08-25T22:14:01+00:00"; + message = '' + A new module is available: 'programs.mcfly' + ''; + } + + { + time = "2020-09-01T18:38:18+00:00"; + message = '' + A new module is available: 'programs.ncmpcpp' + ''; + } + + { + time = "2020-09-11T10:06:47+00:00"; + condition = hostPlatform.isLinux && config.targets.genericLinux.enable; + message = '' + A new option 'targets.genericLinux.extraXdgDataDirs' is available + to setup the user environment with the OS's data files. + + This is useful for example to get Bash completion for + 'systemctl' which shouldn't be installed through Home Manager. + + This is also useful to have non Home Manager applications + available in menus. + ''; + } + + { + time = "2020-09-09T06:54:59+00:00"; + condition = config.programs.man.enable; + message = '' + A new option 'programs.man.generateCaches' was added to + support the apropos command. + ''; + } ]; }; } diff --git a/home-manager/modules/misc/nixpkgs.nix b/home-manager/modules/misc/nixpkgs.nix index 7b0904a5f20..511dbec10b2 100644 --- a/home-manager/modules/misc/nixpkgs.nix +++ b/home-manager/modules/misc/nixpkgs.nix @@ -1,6 +1,6 @@ # Adapted from Nixpkgs. -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, pkgsPath, ... }: with lib; @@ -49,7 +49,7 @@ let merge = lib.mergeOneOption; }; - _pkgs = import <nixpkgs> ( + _pkgs = import pkgsPath ( filterAttrs (n: v: v != null) config.nixpkgs ); diff --git a/home-manager/modules/misc/numlock.nix b/home-manager/modules/misc/numlock.nix index 199dd317daa..c823f6dbdd2 100644 --- a/home-manager/modules/misc/numlock.nix +++ b/home-manager/modules/misc/numlock.nix @@ -7,6 +7,8 @@ let cfg = config.xsession.numlock; in { + meta.maintainers = [ maintainers.evanjs ]; + options = { xsession.numlock.enable = mkEnableOption "Num Lock"; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/misc/tmpfiles.nix b/home-manager/modules/misc/tmpfiles.nix new file mode 100644 index 00000000000..c46fe2c553a --- /dev/null +++ b/home-manager/modules/misc/tmpfiles.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.systemd.user.tmpfiles; + +in { + meta.maintainers = [ maintainers.dawidsowa ]; + + options.systemd.user.tmpfiles.rules = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "L /home/user/Documents - - - - /mnt/data/Documents" ]; + description = '' + Rules for creating and cleaning up temporary files + automatically. See + <citerefentry> + <refentrytitle>tmpfiles.d</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry> + for the exact format. + ''; + }; + + config = mkIf (cfg.rules != [ ]) { + xdg = { + dataFile."user-tmpfiles.d/home-manager.conf" = { + text = '' + # This file is created automatically and should not be modified. + # Please change the option βsystemd.user.tmpfiles.rulesβ instead. + ${concatStringsSep "\n" cfg.rules} + ''; + onChange = "${pkgs.systemd}/bin/systemd-tmpfiles --user --create"; + }; + configFile = { + "systemd/user/basic.target.wants/systemd-tmpfiles-setup.service".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; + "systemd/user/systemd-tmpfiles-setup.service".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-setup.service"; + "systemd/user/timers.target.wants/systemd-tmpfiles-clean.timer".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.timer"; + "systemd/user/systemd-tmpfiles-clean.service".source = + "${pkgs.systemd}/example/systemd/user/systemd-tmpfiles-clean.service"; + }; + }; + }; +} diff --git a/home-manager/modules/misc/version.nix b/home-manager/modules/misc/version.nix index 1352aadc614..fbeb3ec539a 100644 --- a/home-manager/modules/misc/version.nix +++ b/home-manager/modules/misc/version.nix @@ -5,7 +5,7 @@ with lib; { options = { home.stateVersion = mkOption { - type = types.enum [ "18.09" "19.03" "19.09" "20.03" ]; + type = types.enum [ "18.09" "19.03" "19.09" "20.03" "20.09" ]; default = "18.09"; description = '' It is occasionally necessary for Home Manager to change diff --git a/home-manager/modules/misc/vte.nix b/home-manager/modules/misc/vte.nix new file mode 100644 index 00000000000..fbe38c0163e --- /dev/null +++ b/home-manager/modules/misc/vte.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.rycee ]; + + options.programs = let + description = '' + Whether to enable integration with terminals using the VTE + library. This will let the terminal track the current working + directory. + ''; + in { + bash.enableVteIntegration = mkEnableOption "" // { inherit description; }; + + zsh.enableVteIntegration = mkEnableOption "" // { inherit description; }; + }; + + config = mkMerge [ + (mkIf config.programs.bash.enableVteIntegration { + # Unfortunately we have to do a little dance here to fix two + # problems with the upstream vte.sh file: + # + # - It does `PROMPT_COMMAND="__vte_prompt_command"` which + # clobbers any previously assigned prompt command. + # + # - Its `__vte_prompt_command` function runs commands that will + # overwrite the exit status of the command the user ran. + programs.bash.initExtra = '' + __HM_PROMPT_COMMAND="''${PROMPT_COMMAND:+''${PROMPT_COMMAND%;};}__hm_vte_prompt_command" + . ${pkgs.vte}/etc/profile.d/vte.sh + if [[ $(type -t __vte_prompt_command) = function ]]; then + __hm_vte_prompt_command() { + local old_exit_status=$? + __vte_prompt_command + return $old_exit_status + } + PROMPT_COMMAND="$__HM_PROMPT_COMMAND" + fi + unset __HM_PROMPT_COMMAND + ''; + }) + + (mkIf config.programs.zsh.enableVteIntegration { + programs.zsh.initExtra = '' + . ${pkgs.vte}/etc/profile.d/vte.sh + ''; + }) + ]; +} diff --git a/home-manager/modules/misc/xdg-mime-apps.nix b/home-manager/modules/misc/xdg-mime-apps.nix index 7ba4083b3c0..81d2ba0fcbe 100644 --- a/home-manager/modules/misc/xdg-mime-apps.nix +++ b/home-manager/modules/misc/xdg-mime-apps.nix @@ -74,7 +74,7 @@ in { config = mkIf cfg.enable { # Deprecated but still used by some applications. - home.file.".local/share/applications/mimeapps.list".source = + xdg.dataFile."applications/mimeapps.list".source = config.xdg.configFile."mimeapps.list".source; xdg.configFile."mimeapps.list".text = diff --git a/home-manager/modules/misc/xdg-mime.nix b/home-manager/modules/misc/xdg-mime.nix index 32006e025ff..5999e1299c9 100644 --- a/home-manager/modules/misc/xdg-mime.nix +++ b/home-manager/modules/misc/xdg-mime.nix @@ -27,10 +27,18 @@ in { home.packages = [ # Explicitly install package to provide basic mime types. pkgs.shared-mime-info + + # Make sure the target directories will be real directories. + (pkgs.runCommandLocal "dummy-xdg-mime-dirs1" { } '' + mkdir -p $out/share/{applications,mime/packages} + '') + (pkgs.runCommandLocal "dummy-xdg-mime-dirs2" { } '' + mkdir -p $out/share/{applications,mime/packages} + '') ]; home.extraProfileCommands = '' - if [[ -w $out/share/mime && -d $out/share/mime/packages ]]; then + if [[ -w $out/share/mime && -w $out/share/mime/packages && -d $out/share/mime/packages ]]; then XDG_DATA_DIRS=$out/share \ PKGSYSTEM_ENABLE_FSYNC=0 \ ${pkgs.buildPackages.shared-mime-info}/bin/update-mime-database \ diff --git a/home-manager/modules/misc/xdg-user-dirs.nix b/home-manager/modules/misc/xdg-user-dirs.nix index da9d3c43ad9..a1db6b115a1 100644 --- a/home-manager/modules/misc/xdg-user-dirs.nix +++ b/home-manager/modules/misc/xdg-user-dirs.nix @@ -89,15 +89,22 @@ in { }; config = mkIf cfg.enable { - xdg.configFile."user-dirs.dirs".text = generators.toKeyValue { } ({ - XDG_DESKTOP_DIR = cfg.desktop; - XDG_DOCUMENTS_DIR = cfg.documents; - XDG_DOWNLOAD_DIR = cfg.download; - XDG_MUSIC_DIR = cfg.music; - XDG_PICTURES_DIR = cfg.pictures; - XDG_PUBLICSHARE_DIR = cfg.publicShare; - XDG_TEMPLATES_DIR = cfg.templates; - XDG_VIDEOS_DIR = cfg.videos; - } // cfg.extraConfig); + xdg.configFile."user-dirs.dirs".text = let + options = { + XDG_DESKTOP_DIR = cfg.desktop; + XDG_DOCUMENTS_DIR = cfg.documents; + XDG_DOWNLOAD_DIR = cfg.download; + XDG_MUSIC_DIR = cfg.music; + XDG_PICTURES_DIR = cfg.pictures; + XDG_PUBLICSHARE_DIR = cfg.publicShare; + XDG_TEMPLATES_DIR = cfg.templates; + XDG_VIDEOS_DIR = cfg.videos; + } // cfg.extraConfig; + + # For some reason, these need to be wrapped with quotes to be valid. + wrapped = mapAttrs (_: value: ''"${value}"'') options; + in generators.toKeyValue { } wrapped; + + xdg.configFile."user-dirs.conf".text = "enabled=False"; }; } diff --git a/home-manager/modules/misc/xdg.nix b/home-manager/modules/misc/xdg.nix index 84ab4ada59a..7420e8e92b3 100644 --- a/home-manager/modules/misc/xdg.nix +++ b/home-manager/modules/misc/xdg.nix @@ -85,12 +85,20 @@ in }; }) - (mkIf (!cfg.enable) { + # Legacy non-deterministic setup. + (mkIf (!cfg.enable && versionOlder config.home.stateVersion "20.09") { xdg.cacheHome = getXdgDir "XDG_CACHE_HOME" defaultCacheHome; xdg.configHome = getXdgDir "XDG_CONFIG_HOME" defaultConfigHome; xdg.dataHome = getXdgDir "XDG_DATA_HOME" defaultDataHome; }) + # "Modern" deterministic setup. + (mkIf (!cfg.enable && versionAtLeast config.home.stateVersion "20.09") { + xdg.cacheHome = mkDefault defaultCacheHome; + xdg.configHome = mkDefault defaultConfigHome; + xdg.dataHome = mkDefault defaultDataHome; + }) + { home.file = mkMerge [ cfg.configFile diff --git a/home-manager/modules/modules.nix b/home-manager/modules/modules.nix index 64418dbae11..08c978b177d 100644 --- a/home-manager/modules/modules.nix +++ b/home-manager/modules/modules.nix @@ -5,6 +5,9 @@ # Whether to enable module type checking. , check ? true + +# If disabled, the pkgs attribute passed to this function is used instead. +, useNixpkgsModule ? true }: with lib; @@ -13,8 +16,6 @@ let hostPlatform = pkgs.stdenv.hostPlatform; - checkPlatform = any (meta.platformMatch pkgs.stdenv.hostPlatform); - loadModule = file: { condition ? true }: { inherit file condition; }; @@ -25,23 +26,28 @@ let (loadModule ./home-environment.nix { }) (loadModule ./manual.nix { }) (loadModule ./misc/dconf.nix { }) + (loadModule ./misc/debug.nix { }) (loadModule ./misc/fontconfig.nix { }) (loadModule ./misc/gtk.nix { }) (loadModule ./misc/lib.nix { }) (loadModule ./misc/news.nix { }) - (loadModule ./misc/nixpkgs.nix { }) + (loadModule ./misc/nixpkgs.nix { condition = useNixpkgsModule; }) (loadModule ./misc/numlock.nix { condition = hostPlatform.isLinux; }) (loadModule ./misc/pam.nix { }) (loadModule ./misc/qt.nix { }) (loadModule ./misc/submodule-support.nix { }) + (loadModule ./misc/tmpfiles.nix { condition = hostPlatform.isLinux; }) (loadModule ./misc/version.nix { }) + (loadModule ./misc/vte.nix { }) (loadModule ./misc/xdg-mime.nix { condition = hostPlatform.isLinux; }) (loadModule ./misc/xdg-mime-apps.nix { condition = hostPlatform.isLinux; }) (loadModule ./misc/xdg-user-dirs.nix { condition = hostPlatform.isLinux; }) (loadModule ./misc/xdg.nix { }) + (loadModule ./programs/abook.nix { condition = hostPlatform.isLinux; }) (loadModule ./programs/afew.nix { }) (loadModule ./programs/alacritty.nix { }) (loadModule ./programs/alot.nix { }) + (loadModule ./programs/aria2.nix { }) (loadModule ./programs/astroid.nix { }) (loadModule ./programs/autorandr.nix { }) (loadModule ./programs/bash.nix { }) @@ -51,6 +57,7 @@ let (loadModule ./programs/browserpass.nix { }) (loadModule ./programs/chromium.nix { condition = hostPlatform.isLinux; }) (loadModule ./programs/command-not-found/command-not-found.nix { }) + (loadModule ./programs/dircolors.nix { }) (loadModule ./programs/direnv.nix { }) (loadModule ./programs/eclipse.nix { }) (loadModule ./programs/emacs.nix { }) @@ -65,30 +72,40 @@ let (loadModule ./programs/gpg.nix { }) (loadModule ./programs/home-manager.nix { }) (loadModule ./programs/htop.nix { }) + (loadModule ./programs/i3status.nix { }) (loadModule ./programs/info.nix { }) (loadModule ./programs/irssi.nix { }) + (loadModule ./programs/lieer.nix { }) (loadModule ./programs/jq.nix { }) (loadModule ./programs/kakoune.nix { }) (loadModule ./programs/keychain.nix { }) + (loadModule ./programs/kitty.nix { }) (loadModule ./programs/lesspipe.nix { }) + (loadModule ./programs/lf.nix { }) (loadModule ./programs/lsd.nix { }) (loadModule ./programs/man.nix { }) (loadModule ./programs/matplotlib.nix { }) (loadModule ./programs/mbsync.nix { }) + (loadModule ./programs/mcfly.nix { }) (loadModule ./programs/mercurial.nix { }) (loadModule ./programs/mpv.nix { }) (loadModule ./programs/msmtp.nix { }) + (loadModule ./programs/ncmpcpp.nix { }) + (loadModule ./programs/ne.nix { }) (loadModule ./programs/neomutt.nix { }) (loadModule ./programs/neovim.nix { }) (loadModule ./programs/newsboat.nix { }) (loadModule ./programs/noti.nix { }) (loadModule ./programs/notmuch.nix { }) + (loadModule ./programs/nushell.nix { }) (loadModule ./programs/obs-studio.nix { }) (loadModule ./programs/offlineimap.nix { }) (loadModule ./programs/opam.nix { }) (loadModule ./programs/password-store.nix { }) (loadModule ./programs/pazi.nix { }) (loadModule ./programs/pidgin.nix { }) + (loadModule ./programs/powerline-go.nix { }) + (loadModule ./programs/qutebrowser.nix { }) (loadModule ./programs/readline.nix { }) (loadModule ./programs/rofi.nix { }) (loadModule ./programs/rtorrent.nix { }) @@ -103,27 +120,37 @@ let (loadModule ./programs/vim.nix { }) (loadModule ./programs/vscode.nix { }) (loadModule ./programs/vscode/haskell.nix { }) + (loadModule ./programs/waybar.nix { condition = hostPlatform.isLinux; }) (loadModule ./programs/z-lua.nix { }) (loadModule ./programs/zathura.nix { }) + (loadModule ./programs/zoxide.nix { }) + (loadModule ./programs/zplug.nix { }) (loadModule ./programs/zsh.nix { }) (loadModule ./services/blueman-applet.nix { }) (loadModule ./services/cbatticon.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/clipmenu.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/compton.nix { }) + (loadModule ./services/dropbox.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/dunst.nix { }) (loadModule ./services/dwm-status.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/emacs.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/flameshot.nix { }) + (loadModule ./services/fluidsynth.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/getmail.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/gnome-keyring.nix { }) (loadModule ./services/gpg-agent.nix { }) (loadModule ./services/grobi.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/hound.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/imapnotify.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/kanshi.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/kbfs.nix { }) (loadModule ./services/kdeconnect.nix { }) (loadModule ./services/keepassx.nix { }) (loadModule ./services/keybase.nix { }) + (loadModule ./services/keynav.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/lieer.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/lorri.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/mako.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/mbsync.nix { }) (loadModule ./services/mpd.nix { }) (loadModule ./services/mpdris2.nix { condition = hostPlatform.isLinux; }) @@ -134,7 +161,9 @@ let (loadModule ./services/parcellite.nix { }) (loadModule ./services/password-store-sync.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/pasystray.nix { }) + (loadModule ./services/picom.nix { }) (loadModule ./services/polybar.nix { }) + (loadModule ./services/pulseeffects.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/random-background.nix { }) (loadModule ./services/redshift.nix { }) (loadModule ./services/rsibreak.nix { condition = hostPlatform.isLinux; }) @@ -152,13 +181,16 @@ let (loadModule ./services/unison.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/window-managers/awesome.nix { }) (loadModule ./services/window-managers/bspwm/default.nix { condition = hostPlatform.isLinux; }) - (loadModule ./services/window-managers/i3.nix { }) + (loadModule ./services/window-managers/i3-sway/i3.nix { }) + (loadModule ./services/window-managers/i3-sway/sway.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/window-managers/xmonad.nix { }) (loadModule ./services/xcape.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/xembed-sni-proxy.nix { condition = hostPlatform.isLinux; }) (loadModule ./services/xscreensaver.nix { }) (loadModule ./services/xsuspender.nix { condition = hostPlatform.isLinux; }) (loadModule ./systemd.nix { }) + (loadModule ./targets/darwin.nix { condition = hostPlatform.isDarwin; }) + (loadModule ./targets/generic-linux.nix { condition = hostPlatform.isLinux; }) (loadModule ./xcursor.nix { }) (loadModule ./xresources.nix { }) (loadModule ./xsession.nix { }) @@ -168,12 +200,20 @@ let modules = map (getAttr "file") (filter (getAttr "condition") allModules); - pkgsModule = { - config._module.args.baseModules = modules; - config._module.args.pkgs = lib.mkDefault pkgs; - config._module.check = check; - config.lib = lib.hm; - config.nixpkgs.system = mkDefault pkgs.system; + pkgsModule = { config, ... }: { + config = { + _module.args.baseModules = modules; + _module.args.pkgsPath = lib.mkDefault ( + if versionAtLeast config.home.stateVersion "20.09" then + pkgs.path + else + <nixpkgs>); + _module.args.pkgs = lib.mkDefault pkgs; + _module.check = check; + lib = lib.hm; + } // optionalAttrs useNixpkgsModule { + nixpkgs.system = mkDefault pkgs.system; + }; }; in diff --git a/home-manager/modules/programs/abook.nix b/home-manager/modules/programs/abook.nix new file mode 100644 index 00000000000..4ddc080ad51 --- /dev/null +++ b/home-manager/modules/programs/abook.nix @@ -0,0 +1,40 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.abook; + +in { + options.programs.abook = { + enable = mkEnableOption "Abook"; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + field pager = Pager + view CONTACT = name, email + set autosave=true + ''; + description = '' + Extra lines added to <filename>$HOME/.config/abook/abookrc</filename>. + Available configuration options are described in the abook repository: + <link xlink:href="https://sourceforge.net/p/abook/git/ci/master/tree/sample.abookrc" />. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.abook ]; + xdg.configFile."abook/abookrc" = mkIf (cfg.extraConfig != "") { + text = '' + # Generated by Home Manager. + # See http://abook.sourceforge.net/ + + ${cfg.extraConfig} + ''; + }; + }; +} diff --git a/home-manager/modules/programs/alacritty.nix b/home-manager/modules/programs/alacritty.nix index 69b9ea9673d..ea908f2b056 100644 --- a/home-manager/modules/programs/alacritty.nix +++ b/home-manager/modules/programs/alacritty.nix @@ -11,6 +11,13 @@ in { programs.alacritty = { enable = mkEnableOption "Alacritty"; + package = mkOption { + type = types.package; + default = pkgs.alacritty; + defaultText = literalExample "pkgs.alacritty"; + description = "The Alacritty package to install."; + }; + settings = mkOption { type = types.attrs; default = { }; @@ -41,7 +48,7 @@ in { config = mkMerge [ (mkIf cfg.enable { - home.packages = [ pkgs.alacritty ]; + home.packages = [ cfg.package ]; xdg.configFile."alacritty/alacritty.yml" = mkIf (cfg.settings != { }) { text = diff --git a/home-manager/modules/programs/alot.nix b/home-manager/modules/programs/alot.nix index 2b28f34caa3..e907cd3e0ac 100644 --- a/home-manager/modules/programs/alot.nix +++ b/home-manager/modules/programs/alot.nix @@ -7,167 +7,231 @@ let cfg = config.programs.alot; - alotAccounts = filter (a: a.notmuch.enable) - (attrValues config.accounts.email.accounts); + alotAccounts = + filter (a: a.notmuch.enable) (attrValues config.accounts.email.accounts); boolStr = v: if v then "True" else "False"; - accountStr = account: with account; - concatStringsSep "\n" ( - [ "[[${name}]]" ] - ++ mapAttrsToList (n: v: n + "=" + v) ( - { - address = address; - realname = realName; - sendmail_command = - optionalString (alot.sendMailCommand != null) alot.sendMailCommand; - sent_box = "maildir" + "://" + maildir.absPath + "/" + folders.sent; - draft_box = "maildir" + "://"+ maildir.absPath + "/" + folders.drafts; - } - // optionalAttrs (aliases != []) { - aliases = concatStringsSep "," aliases; - } - // optionalAttrs (gpg != null) { - gpg_key = gpg.key; - encrypt_by_default = if gpg.encryptByDefault then "all" else "none"; - sign_by_default = boolStr gpg.signByDefault; - } - // optionalAttrs (signature.showSignature != "none") { - signature = pkgs.writeText "signature.txt" signature.text; - signature_as_attachment = - boolStr (signature.showSignature == "attach"); - } - ) - ++ [ alot.extraConfig ] - ++ [ "[[[abook]]]" ] - ++ mapAttrsToList (n: v: n + "=" + v) alot.contactCompletion - ); - - configFile = - let - bindingsToStr = attrSet: - concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${v}") attrSet); - in - '' - # Generated by Home Manager. - # See http://alot.readthedocs.io/en/latest/configuration/config_options.html - - ${cfg.extraConfig} - - [bindings] - ${bindingsToStr cfg.bindings.global} - - [[bufferlist]] - ${bindingsToStr cfg.bindings.bufferlist} - [[search]] - ${bindingsToStr cfg.bindings.search} - [[envelope]] - ${bindingsToStr cfg.bindings.envelope} - [[taglist]] - ${bindingsToStr cfg.bindings.taglist} - [[thread]] - ${bindingsToStr cfg.bindings.thread} - - [accounts] - - ${concatStringsSep "\n\n" (map accountStr alotAccounts)} - ''; - -in - -{ - options.programs.alot = { - enable = mkOption { - type = types.bool; - default = false; - example = true; - description = '' - Whether to enable the Alot mail user agent. Alot uses the - Notmuch email system and will therefore be automatically - enabled for each email account that is managed by Notmuch. - ''; - }; + mkKeyValue = key: value: + let value' = if isBool value then boolStr value else toString value; + in "${key} = ${value'}"; + + mk2ndLevelSectionName = name: "[" + name + "]"; + + tagSubmodule = types.submodule { + options = { + translated = mkOption { + type = types.nullOr types.str; + description = '' + Fixed string representation for this tag. The tag can be + hidden from view, if the key translated is set to + <literal>""</literal>, the empty string. + ''; + }; - hooks = mkOption { - type = types.lines; - default = ""; - description = '' - Content of the hooks file. - ''; - }; + translation = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A pair of strings that define a regular substitution to + compute the string representation on the fly using + <literal>re.sub</literal>. + ''; + }; - bindings = mkOption { - type = types.submodule { - options = { - global = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Global keybindings."; - }; + normal = mkOption { + type = types.nullOr types.str; + default = null; + example = "'','', 'white','light red', 'white','#d66'"; + description = '' + How to display the tag when unfocused. + See <link xlink:href="https://alot.readthedocs.io/en/latest/configuration/theming.html#tagstring-formatting"/>. + ''; + }; - bufferlist = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Bufferlist mode keybindings."; - }; + focus = mkOption { + type = types.nullOr types.str; + default = null; + description = "How to display the tag when focused."; + }; + }; + }; - search = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Search mode keybindings."; - }; + accountStr = account: + with account; + concatStringsSep "\n" ([ "[[${name}]]" ] + ++ mapAttrsToList (n: v: n + "=" + v) ({ + address = address; + realname = realName; + sendmail_command = + optionalString (alot.sendMailCommand != null) alot.sendMailCommand; + sent_box = "maildir" + "://" + maildir.absPath + "/" + folders.sent; + draft_box = "maildir" + "://" + maildir.absPath + "/" + folders.drafts; + } // optionalAttrs (aliases != [ ]) { + aliases = concatStringsSep "," aliases; + } // optionalAttrs (gpg != null) { + gpg_key = gpg.key; + encrypt_by_default = if gpg.encryptByDefault then "all" else "none"; + sign_by_default = boolStr gpg.signByDefault; + } // optionalAttrs (signature.showSignature != "none") { + signature = pkgs.writeText "signature.txt" signature.text; + signature_as_attachment = boolStr (signature.showSignature == "attach"); + }) ++ [ alot.extraConfig ] ++ [ "[[[abook]]]" ] + ++ mapAttrsToList (n: v: n + "=" + v) alot.contactCompletion); + + configFile = let + bindingsToStr = attrSet: + concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${v}") attrSet); + in '' + # Generated by Home Manager. + # See http://alot.readthedocs.io/en/latest/configuration/config_options.html + + ${generators.toKeyValue { inherit mkKeyValue; } cfg.settings} + ${cfg.extraConfig} + [tags] + '' + (let + submoduleToAttrs = m: + filterAttrs (name: v: name != "_module" && v != null) m; + in generators.toINI { mkSectionName = mk2ndLevelSectionName; } + (mapAttrs (name: x: submoduleToAttrs x) cfg.tags)) + '' + [bindings] + ${bindingsToStr cfg.bindings.global} + + [[bufferlist]] + ${bindingsToStr cfg.bindings.bufferlist} + [[search]] + ${bindingsToStr cfg.bindings.search} + [[envelope]] + ${bindingsToStr cfg.bindings.envelope} + [[taglist]] + ${bindingsToStr cfg.bindings.taglist} + [[thread]] + ${bindingsToStr cfg.bindings.thread} + + [accounts] + + ${concatStringsSep "\n\n" (map accountStr alotAccounts)} + ''; + +in { + options = { + programs.alot = { + enable = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to enable the Alot mail user agent. Alot uses the + Notmuch email system and will therefore be automatically + enabled for each email account that is managed by Notmuch. + ''; + }; - envelope = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Envelope mode keybindings."; - }; + hooks = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the hooks file. + ''; + }; - taglist = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Taglist mode keybindings."; + bindings = mkOption { + type = types.submodule { + options = { + global = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Global keybindings."; + }; + + bufferlist = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Bufferlist mode keybindings."; + }; + + search = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Search mode keybindings."; + }; + + envelope = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Envelope mode keybindings."; + }; + + taglist = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Taglist mode keybindings."; + }; + + thread = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Thread mode keybindings."; + }; }; + }; + default = { }; + description = '' + Keybindings. + ''; + }; - thread = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Thread mode keybindings."; - }; + tags = mkOption { + type = types.attrsOf tagSubmodule; + default = { }; + description = "How to display the tags."; + }; + + settings = mkOption { + type = with types; + let primitive = either (either (either str int) bool) float; + in attrsOf primitive; + default = { + initial_command = "search tag:inbox AND NOT tag:killed"; + auto_remove_unread = true; + handle_mouse = true; + prefer_plaintext = true; }; + example = literalExample '' + { + auto_remove_unread = true; + ask_subject = false; + thread_indent_replies = 2; + } + ''; + description = '' + Configuration options added to alot configuration file. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines added to alot configuration file. + ''; }; - default = {}; - description = '' - Keybindings. - ''; }; - extraConfig = mkOption { - type = types.lines; - default = '' - auto_remove_unread = True - ask_subject = False - handle_mouse = True - initial_command = "search tag:inbox AND NOT tag:killed" - input_timeout = 0.3 - prefer_plaintext = True - thread_indent_replies = 4 - ''; - description = '' - Extra lines added to alot configuration file. - ''; + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./alot-accounts.nix pkgs)); }; }; config = mkIf cfg.enable { - home.packages = [ pkgs.alot ]; + home.packages = [ pkgs.alot ]; xdg.configFile."alot/config".text = configFile; - xdg.configFile."alot/hooks.py".text = - '' + xdg.configFile."alot/hooks.py" = mkIf (cfg.hooks != "") { + text = '' # Generated by Home Manager. - '' - + cfg.hooks; + '' + cfg.hooks; + }; }; } diff --git a/home-manager/modules/programs/aria2.nix b/home-manager/modules/programs/aria2.nix new file mode 100644 index 00000000000..d1317ff7616 --- /dev/null +++ b/home-manager/modules/programs/aria2.nix @@ -0,0 +1,61 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.aria2; + + formatLine = n: v: + let + formatValue = v: + if builtins.isBool v then + (if v then "true" else "false") + else + toString v; + in "${n}=${formatValue v}"; +in { + meta.maintainers = [ hm.maintainers.justinlovinger ]; + + options.programs.aria2 = { + enable = mkEnableOption "aria2"; + + settings = mkOption { + type = with types; attrsOf (oneOf [ bool float int str ]); + default = { }; + description = '' + Options to add to <filename>aria2.conf</filename> file. + See + <citerefentry> + <refentrytitle>aria2c</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for options. + ''; + example = literalExample '' + { + listen-port = 60000; + dht-listen-port = 60000; + seed-ratio = 1.0; + max-upload-limit = "50K"; + ftp-pasv = true; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines added to <filename>aria2.conf</filename> file. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.aria2 ]; + + xdg.configFile."aria2/aria2.conf".text = concatStringsSep "\n" ([ ] + ++ mapAttrsToList formatLine cfg.settings + ++ optional (cfg.extraConfig != "") cfg.extraConfig); + }; +} diff --git a/home-manager/modules/programs/astroid.nix b/home-manager/modules/programs/astroid.nix index 8b3762fac0b..af12b10edbb 100644 --- a/home-manager/modules/programs/astroid.nix +++ b/home-manager/modules/programs/astroid.nix @@ -98,6 +98,10 @@ in { ''; }; }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./astroid-accounts.nix)); + }; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/programs/autorandr.nix b/home-manager/modules/programs/autorandr.nix index 02fc77c1e58..40cad704db9 100644 --- a/home-manager/modules/programs/autorandr.nix +++ b/home-manager/modules/programs/autorandr.nix @@ -55,6 +55,13 @@ let default = true; }; + crtc = mkOption { + type = types.nullOr types.ints.unsigned; + description = "Output video display controller."; + default = null; + example = 0; + }; + primary = mkOption { type = types.bool; description = "Whether output should be marked as primary"; @@ -244,21 +251,22 @@ let ]); fingerprintToString = name: edid: "${name} ${edid}"; configToString = name: config: - if config.enable then '' - output ${name} - ${optionalString (config.position != "") "pos ${config.position}"} - ${optionalString config.primary "primary"} - ${optionalString (config.dpi != null) "dpi ${toString config.dpi}"} - ${optionalString (config.gamma != "") "gamma ${config.gamma}"} - ${optionalString (config.mode != "") "mode ${config.mode}"} - ${optionalString (config.rate != "") "rate ${config.rate}"} - ${optionalString (config.rotate != null) "rotate ${config.rotate}"} - ${optionalString (config.scale != null) - ((if config.scale.method == "factor" then "scale" else "scale-from") - + " ${toString config.scale.x}x${toString config.scale.y}")} - ${optionalString (config.transform != null) ("transform " - + concatMapStringsSep "," toString (flatten config.transform))} - '' else '' + if config.enable then + concatStringsSep "\n" ([ "output ${name}" ] + ++ optional (config.position != "") "pos ${config.position}" + ++ optional (config.crtc != null) "crtc ${toString config.crtc}" + ++ optional config.primary "primary" + ++ optional (config.dpi != null) "dpi ${toString config.dpi}" + ++ optional (config.gamma != "") "gamma ${config.gamma}" + ++ optional (config.mode != "") "mode ${config.mode}" + ++ optional (config.rate != "") "rate ${config.rate}" + ++ optional (config.rotate != null) "rotate ${config.rotate}" + ++ optional (config.transform != null) ("transform " + + concatMapStringsSep "," toString (flatten config.transform)) + ++ optional (config.scale != null) + ((if config.scale.method == "factor" then "scale" else "scale-from") + + " ${toString config.scale.x}x${toString config.scale.y}")) + else '' output ${name} off ''; @@ -315,6 +323,7 @@ in { eDP1.enable = false; DP1 = { enable = true; + crtc = 0; primary = true; position = "0x0"; mode = "3840x2160"; diff --git a/home-manager/modules/programs/bash.nix b/home-manager/modules/programs/bash.nix index 82a9fbe8f8b..45fe368bddc 100644 --- a/home-manager/modules/programs/bash.nix +++ b/home-manager/modules/programs/bash.nix @@ -82,7 +82,12 @@ in shellAliases = mkOption { default = {}; type = types.attrsOf types.str; - example = { ll = "ls -l"; ".." = "cd .."; }; + example = literalExample '' + { + ll = "ls -l"; + ".." = "cd .."; + } + ''; description = '' An attribute set that maps aliases (the top level attribute names in this option) to command strings or directly to build outputs. @@ -171,10 +176,10 @@ in ${aliasesStr} - ${cfg.initExtra} - ${optionalString cfg.enableAutojump ". ${pkgs.autojump}/share/autojump/autojump.bash"} + + ${cfg.initExtra} fi ''; diff --git a/home-manager/modules/programs/bat.nix b/home-manager/modules/programs/bat.nix index aa0df9abd45..e2b30ea9333 100644 --- a/home-manager/modules/programs/bat.nix +++ b/home-manager/modules/programs/bat.nix @@ -24,14 +24,35 @@ in { ''; }; + themes = mkOption { + type = types.attrsOf types.lines; + default = { }; + example = literalExample '' + { + dracula = builtins.readFile (pkgs.fetchFromGitHub { + owner = "dracula"; + repo = "sublime"; # Bat uses sublime syntax for its themes + rev = "26c57ec282abcaa76e57e055f38432bd827ac34e"; + sha256 = "019hfl4zbn4vm4154hh3bwk6hm7bdxbr1hdww83nabxwjn99ndhv"; + } + "/Dracula.tmTheme"); + } + ''; + description = '' + Additional themes to provide. + ''; + }; + }; config = mkIf cfg.enable { home.packages = [ pkgs.bat ]; - xdg.configFile."bat/config" = mkIf (cfg.config != { }) { - text = concatStringsSep "\n" - (mapAttrsToList (n: v: ''--${n}="${v}"'') cfg.config); - }; + xdg.configFile = mkMerge ([{ + "bat/config" = mkIf (cfg.config != { }) { + text = concatStringsSep "\n" + (mapAttrsToList (n: v: ''--${n}="${v}"'') cfg.config); + }; + }] ++ flip mapAttrsToList cfg.themes + (name: body: { "bat/themes/${name}.tmTheme" = { text = body; }; })); }; } diff --git a/home-manager/modules/programs/broot.nix b/home-manager/modules/programs/broot.nix index eac31b56801..6951e035d32 100644 --- a/home-manager/modules/programs/broot.nix +++ b/home-manager/modules/programs/broot.nix @@ -172,11 +172,11 @@ in { xdg.configFile."broot/conf.toml".source = configFile brootConf; # Dummy file to prevent broot from trying to reinstall itself - xdg.configFile."broot/launcher/installed".text = ""; + xdg.configFile."broot/launcher/installed-v1".text = ""; programs.bash.initExtra = mkIf cfg.enableBashIntegration ( - # Using mkAfter to make it more likely to appear after other - # manipulations of the prompt. + # Using mkAfter to make it more likely to appear after other + # manipulations of the prompt. mkAfter '' # This script was automatically generated by the broot function # More information can be found in https://github.com/Canop/broot diff --git a/home-manager/modules/programs/dircolors.nix b/home-manager/modules/programs/dircolors.nix new file mode 100644 index 00000000000..026de72d711 --- /dev/null +++ b/home-manager/modules/programs/dircolors.nix @@ -0,0 +1,223 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.dircolors; + + formatLine = n: v: "${n} ${toString v}"; +in { + meta.maintainers = [ hm.maintainers.justinlovinger ]; + + options.programs.dircolors = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to manage <filename>.dir_colors</filename> + and set <code>LS_COLORS</code>. + ''; + }; + + enableBashIntegration = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable Bash integration. + ''; + }; + + enableFishIntegration = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable Fish integration. + ''; + }; + + enableZshIntegration = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable Zsh integration. + ''; + }; + + settings = mkOption { + type = with types; attrsOf str; + default = { }; + description = '' + Options to add to <filename>.dir_colors</filename> file. + See <command>dircolors --print-database</command> + for options. + ''; + example = literalExample '' + { + OTHER_WRITABLE = "30;46"; + ".sh" = "01;32"; + ".csh" = "01;32"; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines added to <filename>.dir_colors</filename> file. + ''; + }; + }; + + config = mkIf cfg.enable { + # Add default settings from `dircolors --print-database`. + programs.dircolors.settings = { + RESET = mkDefault "0"; + DIR = mkDefault "01;34"; + LINK = mkDefault "01;36"; + MULTIHARDLINK = mkDefault "00"; + FIFO = mkDefault "40;33"; + SOCK = mkDefault "01;35"; + DOOR = mkDefault "01;35"; + BLK = mkDefault "40;33;01"; + CHR = mkDefault "40;33;01"; + ORPHAN = mkDefault "40;31;01"; + MISSING = mkDefault "00"; + SETUID = mkDefault "37;41"; + SETGID = mkDefault "30;43"; + CAPABILITY = mkDefault "30;41"; + STICKY_OTHER_WRITABLE = mkDefault "30;42"; + OTHER_WRITABLE = mkDefault "34;42"; + STICKY = mkDefault "37;44"; + EXEC = mkDefault "01;32"; + ".tar" = mkDefault "01;31"; + ".tgz" = mkDefault "01;31"; + ".arc" = mkDefault "01;31"; + ".arj" = mkDefault "01;31"; + ".taz" = mkDefault "01;31"; + ".lha" = mkDefault "01;31"; + ".lz4" = mkDefault "01;31"; + ".lzh" = mkDefault "01;31"; + ".lzma" = mkDefault "01;31"; + ".tlz" = mkDefault "01;31"; + ".txz" = mkDefault "01;31"; + ".tzo" = mkDefault "01;31"; + ".t7z" = mkDefault "01;31"; + ".zip" = mkDefault "01;31"; + ".z" = mkDefault "01;31"; + ".dz" = mkDefault "01;31"; + ".gz" = mkDefault "01;31"; + ".lrz" = mkDefault "01;31"; + ".lz" = mkDefault "01;31"; + ".lzo" = mkDefault "01;31"; + ".xz" = mkDefault "01;31"; + ".zst" = mkDefault "01;31"; + ".tzst" = mkDefault "01;31"; + ".bz2" = mkDefault "01;31"; + ".bz" = mkDefault "01;31"; + ".tbz" = mkDefault "01;31"; + ".tbz2" = mkDefault "01;31"; + ".tz" = mkDefault "01;31"; + ".deb" = mkDefault "01;31"; + ".rpm" = mkDefault "01;31"; + ".jar" = mkDefault "01;31"; + ".war" = mkDefault "01;31"; + ".ear" = mkDefault "01;31"; + ".sar" = mkDefault "01;31"; + ".rar" = mkDefault "01;31"; + ".alz" = mkDefault "01;31"; + ".ace" = mkDefault "01;31"; + ".zoo" = mkDefault "01;31"; + ".cpio" = mkDefault "01;31"; + ".7z" = mkDefault "01;31"; + ".rz" = mkDefault "01;31"; + ".cab" = mkDefault "01;31"; + ".wim" = mkDefault "01;31"; + ".swm" = mkDefault "01;31"; + ".dwm" = mkDefault "01;31"; + ".esd" = mkDefault "01;31"; + ".jpg" = mkDefault "01;35"; + ".jpeg" = mkDefault "01;35"; + ".mjpg" = mkDefault "01;35"; + ".mjpeg" = mkDefault "01;35"; + ".gif" = mkDefault "01;35"; + ".bmp" = mkDefault "01;35"; + ".pbm" = mkDefault "01;35"; + ".pgm" = mkDefault "01;35"; + ".ppm" = mkDefault "01;35"; + ".tga" = mkDefault "01;35"; + ".xbm" = mkDefault "01;35"; + ".xpm" = mkDefault "01;35"; + ".tif" = mkDefault "01;35"; + ".tiff" = mkDefault "01;35"; + ".png" = mkDefault "01;35"; + ".svg" = mkDefault "01;35"; + ".svgz" = mkDefault "01;35"; + ".mng" = mkDefault "01;35"; + ".pcx" = mkDefault "01;35"; + ".mov" = mkDefault "01;35"; + ".mpg" = mkDefault "01;35"; + ".mpeg" = mkDefault "01;35"; + ".m2v" = mkDefault "01;35"; + ".mkv" = mkDefault "01;35"; + ".webm" = mkDefault "01;35"; + ".ogm" = mkDefault "01;35"; + ".mp4" = mkDefault "01;35"; + ".m4v" = mkDefault "01;35"; + ".mp4v" = mkDefault "01;35"; + ".vob" = mkDefault "01;35"; + ".qt" = mkDefault "01;35"; + ".nuv" = mkDefault "01;35"; + ".wmv" = mkDefault "01;35"; + ".asf" = mkDefault "01;35"; + ".rm" = mkDefault "01;35"; + ".rmvb" = mkDefault "01;35"; + ".flc" = mkDefault "01;35"; + ".avi" = mkDefault "01;35"; + ".fli" = mkDefault "01;35"; + ".flv" = mkDefault "01;35"; + ".gl" = mkDefault "01;35"; + ".dl" = mkDefault "01;35"; + ".xcf" = mkDefault "01;35"; + ".xwd" = mkDefault "01;35"; + ".yuv" = mkDefault "01;35"; + ".cgm" = mkDefault "01;35"; + ".emf" = mkDefault "01;35"; + ".ogv" = mkDefault "01;35"; + ".ogx" = mkDefault "01;35"; + ".aac" = mkDefault "00;36"; + ".au" = mkDefault "00;36"; + ".flac" = mkDefault "00;36"; + ".m4a" = mkDefault "00;36"; + ".mid" = mkDefault "00;36"; + ".midi" = mkDefault "00;36"; + ".mka" = mkDefault "00;36"; + ".mp3" = mkDefault "00;36"; + ".mpc" = mkDefault "00;36"; + ".ogg" = mkDefault "00;36"; + ".ra" = mkDefault "00;36"; + ".wav" = mkDefault "00;36"; + ".oga" = mkDefault "00;36"; + ".opus" = mkDefault "00;36"; + ".spx" = mkDefault "00;36"; + ".xspf" = mkDefault "00;36"; + }; + + home.file.".dir_colors".text = concatStringsSep "\n" ([ ] + ++ optional (cfg.extraConfig != "") cfg.extraConfig + ++ mapAttrsToList formatLine cfg.settings) + "\n"; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + eval $(${pkgs.coreutils}/bin/dircolors -b ~/.dir_colors) + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + eval (${pkgs.coreutils}/bin/dircolors -c ~/.dir_colors) + ''; + + # Set `LS_COLORS` before Oh My Zsh and `initExtra`. + programs.zsh.initExtraBeforeCompInit = mkIf cfg.enableZshIntegration '' + eval $(${pkgs.coreutils}/bin/dircolors -b ~/.dir_colors) + ''; + }; +} diff --git a/home-manager/modules/programs/direnv.nix b/home-manager/modules/programs/direnv.nix index beb40a96261..1d1374b8e26 100644 --- a/home-manager/modules/programs/direnv.nix +++ b/home-manager/modules/programs/direnv.nix @@ -70,6 +70,11 @@ in { Whether to enable Fish integration. ''; }; + + enableNixDirenvIntegration = mkEnableOption '' + <link + xlink:href="https://github.com/nix-community/nix-direnv">nix-direnv</link>, + a fast, persistent use_nix implementation for direnv''; }; config = mkIf cfg.enable { @@ -78,12 +83,15 @@ in { xdg.configFile."direnv/config.toml" = mkIf (cfg.config != { }) { source = configFile cfg.config; }; - xdg.configFile."direnv/direnvrc" = - mkIf (cfg.stdlib != "") { text = cfg.stdlib; }; + xdg.configFile."direnv/direnvrc" = let + text = concatStringsSep "\n" (optional (cfg.stdlib != "") cfg.stdlib + ++ optional cfg.enableNixDirenvIntegration + "source ${pkgs.nix-direnv}/share/nix-direnv/direnvrc"); + in mkIf (text != "") { inherit text; }; programs.bash.initExtra = mkIf cfg.enableBashIntegration ( - # Using mkAfter to make it more likely to appear after other - # manipulations of the prompt. + # Using mkAfter to make it more likely to appear after other + # manipulations of the prompt. mkAfter '' eval "$(${pkgs.direnv}/bin/direnv hook bash)" ''); diff --git a/home-manager/modules/programs/eclipse.nix b/home-manager/modules/programs/eclipse.nix index 8ce605b106a..21973ab937e 100644 --- a/home-manager/modules/programs/eclipse.nix +++ b/home-manager/modules/programs/eclipse.nix @@ -13,6 +13,16 @@ in { programs.eclipse = { enable = mkEnableOption "Eclipse"; + package = mkOption { + type = types.package; + default = pkgs.eclipses.eclipse-platform; + defaultText = literalExample "pkgs.eclipses.eclipse-platform"; + example = literalExample "pkgs.eclipses.eclipse-java"; + description = '' + The Eclipse package to install. + ''; + }; + enableLombok = mkOption { type = types.bool; default = false; @@ -40,7 +50,7 @@ in { config = mkIf cfg.enable { home.packages = [ (pkgs.eclipses.eclipseWithPlugins { - eclipse = pkgs.eclipses.eclipse-platform; + eclipse = cfg.package; jvmArgs = cfg.jvmArgs ++ optional cfg.enableLombok "-javaagent:${pkgs.lombok}/share/java/lombok.jar"; plugins = cfg.plugins; diff --git a/home-manager/modules/programs/emacs.nix b/home-manager/modules/programs/emacs.nix index 987a9f2431e..b785f71358c 100644 --- a/home-manager/modules/programs/emacs.nix +++ b/home-manager/modules/programs/emacs.nix @@ -8,16 +8,12 @@ let # Copied from all-packages.nix, with modifications to support # overrides. - emacsPackages = - let - epkgs = pkgs.emacsPackagesGen cfg.package; - in - epkgs.overrideScope' cfg.overrides; - emacsWithPackages = emacsPackages.emacsWithPackages; + emacsPackages = let epkgs = pkgs.emacsPackagesFor cfg.package; + in epkgs.overrideScope' cfg.overrides; -in + emacsWithPackages = emacsPackages.emacsWithPackages; -{ +in { meta.maintainers = [ maintainers.rycee ]; options = { @@ -33,7 +29,7 @@ in }; extraPackages = mkOption { - default = self: []; + default = self: [ ]; type = hm.types.selectorFunction; defaultText = "epkgs: []"; example = literalExample "epkgs: [ epkgs.emms epkgs.magit ]"; @@ -45,7 +41,7 @@ in }; overrides = mkOption { - default = self: super: {}; + default = self: super: { }; type = hm.types.overlayFunction; defaultText = "self: super: {}"; example = literalExample '' diff --git a/home-manager/modules/programs/firefox.nix b/home-manager/modules/programs/firefox.nix index 17c64752d66..d5003f59edc 100644 --- a/home-manager/modules/programs/firefox.nix +++ b/home-manager/modules/programs/firefox.nix @@ -23,8 +23,15 @@ let then "${firefoxConfigPath}/Profiles" else firefoxConfigPath; + # The extensions path shared by all profiles; will not be supported + # by future Firefox versions. extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; + extensionsEnvPkg = pkgs.buildEnv { + name = "hm-firefox-extensions"; + paths = cfg.extensions; + }; + profiles = flip mapAttrs' cfg.profiles (_: profile: nameValuePair "Profile${toString profile.id}" { @@ -59,6 +66,13 @@ in { meta.maintainers = [ maintainers.rycee ]; + imports = [ + (mkRemovedOptionModule ["programs" "firefox" "enableGoogleTalk"] + "Support for this option has been removed.") + (mkRemovedOptionModule ["programs" "firefox" "enableIcedTea"] + "Support for this option has been removed.") + ]; + options = { programs.firefox = { enable = mkEnableOption "Firefox"; @@ -87,9 +101,29 @@ in ] ''; description = '' - List of Firefox add-on packages to install. Note, it is - necessary to manually enable these extensions inside Firefox - after the first installation. + List of Firefox add-on packages to install. Some + pre-packaged add-ons are accessible from NUR, + <link xlink:href="https://github.com/nix-community/NUR"/>. + Once you have NUR installed run + + <screen language="console"> + <prompt>$</prompt> <userinput>nix-env -f '<nixpkgs>' -qaP -A nur.repos.rycee.firefox-addons</userinput> + </screen> + + to list the available Firefox add-ons. + + </para><para> + + Note that it is necessary to manually enable these + extensions inside Firefox after the first installation. + + </para><para> + + Extensions listed here will only be available in Firefox + profiles managed through the + <link linkend="opt-programs.firefox.profiles">programs.firefox.profiles</link> + option. This is due to recent changes in the way Firefox + handles extension side-loading. ''; }; @@ -137,7 +171,7 @@ in userChrome = mkOption { type = types.lines; default = ""; - description = "Custom Firefox CSS."; + description = "Custom Firefox user chrome CSS."; example = '' /* Hide tab bar in FF Quantum */ @-moz-document url("chrome://browser/content/browser.xul") { @@ -153,6 +187,16 @@ in ''; }; + userContent = mkOption { + type = types.lines; + default = ""; + description = "Custom Firefox user content CSS."; + example = '' + /* Hide scrollbar in FF Quantum */ + *{scrollbar-width:none !important} + ''; + }; + path = mkOption { type = types.str; default = name; @@ -176,38 +220,6 @@ in default = false; description = "Whether to enable the unfree Adobe Flash plugin."; }; - - enableGoogleTalk = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable the unfree Google Talk plugin. This option - is <emphasis>deprecated</emphasis> and will only work if - - <programlisting language="nix"> - programs.firefox.package = pkgs.firefox-esr-52-unwrapped; - </programlisting> - - and the <option>plugin.load_flash_only</option> Firefox - option has been disabled. - ''; - }; - - enableIcedTea = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable the Java applet plugin. This option is - <emphasis>deprecated</emphasis> and will only work if - - <programlisting language="nix"> - programs.firefox.package = pkgs.firefox-esr-52-unwrapped; - </programlisting> - - and the <option>plugin.load_flash_only</option> Firefox - option has been disabled. - ''; - }; }; }; @@ -250,8 +262,6 @@ in # The configuration expected by the Firefox wrapper. fcfg = { enableAdobeFlash = cfg.enableAdobeFlash; - enableGoogleTalkPlugin = cfg.enableGoogleTalk; - icedtea = cfg.enableIcedTea; }; # A bit of hackery to force a config into the wrapper. @@ -273,17 +283,10 @@ in home.file = mkMerge ( [{ - "${mozillaConfigPath}/${extensionPath}" = mkIf (cfg.extensions != []) ( - let - extensionsEnv = pkgs.buildEnv { - name = "hm-firefox-extensions"; - paths = cfg.extensions; - }; - in { - source = "${extensionsEnv}/share/mozilla/${extensionPath}"; - recursive = true; - } - ); + "${mozillaConfigPath}/${extensionPath}" = mkIf (cfg.extensions != []) { + source = "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; + recursive = true; + }; "${firefoxConfigPath}/profiles.ini" = mkIf (cfg.profiles != {}) { text = profilesIni; @@ -295,10 +298,21 @@ in text = profile.userChrome; }; + "${profilesPath}/${profile.path}/chrome/userContent.css" = + mkIf (profile.userContent != "") { + text = profile.userContent; + }; + "${profilesPath}/${profile.path}/user.js" = mkIf (profile.settings != {} || profile.extraConfig != "") { text = mkUserJs profile.settings profile.extraConfig; }; + + "${profilesPath}/${profile.path}/extensions" = mkIf (cfg.extensions != []) { + source = "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; + recursive = true; + force = true; + }; }) ); }; diff --git a/home-manager/modules/programs/fish.nix b/home-manager/modules/programs/fish.nix index 87a17b85507..730afa79262 100644 --- a/home-manager/modules/programs/fish.nix +++ b/home-manager/modules/programs/fish.nix @@ -6,119 +6,310 @@ let cfg = config.programs.fish; - abbrsStr = concatStringsSep "\n" ( - mapAttrsToList (k: v: "abbr --add --global ${k} '${v}'") cfg.shellAbbrs - ); + pluginModule = types.submodule ({ config, ... }: { + options = { + src = mkOption { + type = types.path; + description = '' + Path to the plugin folder. + </para><para> + Relevant pieces will be added to the fish function path and + the completion path. The <filename>init.fish</filename> and + <filename>key_binding.fish</filename> files are sourced if + they exist. + ''; + }; - aliasesStr = concatStringsSep "\n" ( - mapAttrsToList (k: v: "alias ${k}='${v}'") cfg.shellAliases - ); + name = mkOption { + type = types.str; + description = '' + The name of the plugin. + ''; + }; + }; + }); -in + functionModule = types.submodule { + options = { + body = mkOption { + type = types.lines; + description = '' + The function body. + ''; + }; + + argumentNames = mkOption { + type = with types; nullOr (either str (listOf str)); + default = null; + description = '' + Assigns the value of successive command line arguments to the names + given. + ''; + }; -{ + description = mkOption { + type = with types; nullOr str; + default = null; + description = '' + A description of what the function does, suitable as a completion + description. + ''; + }; + + wraps = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Causes the function to inherit completions from the given wrapped + command. + ''; + }; + + onEvent = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Tells fish to run this function when the specified named event is + emitted. Fish internally generates named events e.g. when showing the + prompt. + ''; + }; + + onVariable = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Tells fish to run this function when the specified variable changes + value. + ''; + }; + + onJobExit = mkOption { + type = with types; nullOr (either str int); + default = null; + description = '' + Tells fish to run this function when the job with the specified group + ID exits. Instead of a PID, the stringer <literal>caller</literal> can + be specified. This is only legal when in a command substitution, and + will result in the handler being triggered by the exit of the job + which created this command substitution. + ''; + }; + + onProcessExit = mkOption { + type = with types; nullOr (either str int); + default = null; + example = "$fish_pid"; + description = '' + Tells fish to run this function when the fish child process with the + specified process ID exits. Instead of a PID, for backwards + compatibility, <literal>%self</literal> can be specified as an alias + for <literal>$fish_pid</literal>, and the function will be run when + the current fish instance exits. + ''; + }; + + onSignal = mkOption { + type = with types; nullOr (either str int); + default = null; + example = [ "SIGHUP" "HUP" 1 ]; + description = '' + Tells fish to run this function when the specified signal is + delievered. The signal can be a signal number or signal name. + ''; + }; + + noScopeShadowing = mkOption { + type = types.bool; + default = false; + description = '' + Allows the function to access the variables of calling functions. + ''; + }; + + inheritVariable = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Snapshots the value of the specified variable and defines a local + variable with that same name and value when the function is defined. + ''; + }; + }; + }; + + abbrsStr = concatStringsSep "\n" + (mapAttrsToList (k: v: "abbr --add --global -- ${k} ${escapeShellArg v}") + cfg.shellAbbrs); + + aliasesStr = concatStringsSep "\n" + (mapAttrsToList (k: v: "alias ${k} ${escapeShellArg v}") cfg.shellAliases); + +in { options = { programs.fish = { - enable = mkEnableOption "fish friendly interactive shell"; + enable = mkEnableOption "fish, the friendly interactive shell"; package = mkOption { + type = types.package; default = pkgs.fish; defaultText = literalExample "pkgs.fish"; description = '' The fish package to install. May be used to change the version. ''; - type = types.package; }; shellAliases = mkOption { - default = {}; + type = with types; attrsOf str; + default = { }; + example = literalExample '' + { + ll = "ls -l"; + ".." = "cd .."; + } + ''; description = '' - Set of aliases for fish shell. See - <option>environment.shellAliases</option> for an option - format description. + An attribute set that maps aliases (the top level attribute names + in this option) to command strings or directly to build outputs. ''; - type = types.attrs; }; shellAbbrs = mkOption { - default = {}; + type = with types; attrsOf str; + default = { }; + example = { + l = "less"; + gco = "git checkout"; + }; description = '' - Set of abbreviations for fish shell. + An attribute set that maps aliases (the top level attribute names + in this option) to abbreviations. Abbreviations are expanded with + the longer phrase after they are entered. ''; - type = types.attrs; }; shellInit = mkOption { + type = types.lines; default = ""; description = '' - Shell script code called during fish shell initialisation. + Shell script code called during fish shell + initialisation. ''; - type = types.lines; }; loginShellInit = mkOption { + type = types.lines; default = ""; description = '' - Shell script code called during fish login shell initialisation. + Shell script code called during fish login shell + initialisation. ''; - type = types.lines; }; interactiveShellInit = mkOption { + type = types.lines; default = ""; description = '' - Shell script code called during interactive fish shell initialisation. + Shell script code called during interactive fish shell + initialisation. ''; - type = types.lines; }; promptInit = mkOption { + type = types.lines; default = ""; description = '' Shell script code used to initialise fish prompt. ''; - type = types.lines; }; }; + + programs.fish.plugins = mkOption { + type = types.listOf pluginModule; + default = [ ]; + example = literalExample '' + [ + { + name = "z"; + src = pkgs.fetchFromGitHub { + owner = "jethrokuan"; + repo = "z"; + rev = "ddeb28a7b6a1f0ec6dae40c636e5ca4908ad160a"; + sha256 = "0c5i7sdrsp0q3vbziqzdyqn4fmp235ax4mn4zslrswvn8g3fvdyh"; + }; + } + + # oh-my-fish plugins are stored in their own repositories, which + # makes them simple to import into home-manager. + { + name = "fasd"; + src = pkgs.fetchFromGitHub { + owner = "oh-my-fish"; + repo = "plugin-fasd"; + rev = "38a5b6b6011106092009549e52249c6d6f501fba"; + sha256 = "06v37hqy5yrv5a6ssd1p3cjd9y3hnp19d3ab7dag56fs1qmgyhbs"; + }; + } + ] + ''; + description = '' + The plugins to source in + <filename>conf.d/99plugins.fish</filename>. + ''; + }; + + programs.fish.functions = mkOption { + type = with types; attrsOf (either lines functionModule); + default = { }; + example = literalExample '' + { + __fish_command_not_found_handler = { + body = "__fish_default_command_not_found_handler $argv[1]"; + onEvent = "fish_command_not_found"; + }; + + gitignore = "curl -sL https://www.gitignore.io/api/$argv"; + } + ''; + description = '' + Basic functions to add to fish. For more information see + <link xlink:href="https://fishshell.com/docs/current/cmds/function.html"/>. + ''; + }; + }; - config = mkIf cfg.enable { - home.packages = [ cfg.package ]; + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ cfg.package ]; - xdg.dataFile."fish/home-manager_generated_completions".source = - let + xdg.dataFile."fish/home-manager_generated_completions".source = let # paths later in the list will overwrite those already linked - destructiveSymlinkJoin = - args_@{ name - , paths - , preferLocalBuild ? true - , allowSubstitutes ? false - , postBuild ? "" - , ... - }: + destructiveSymlinkJoin = args_@{ name, paths, preferLocalBuild ? true + , allowSubstitutes ? false, postBuild ? "", ... }: let - args = removeAttrs args_ [ "name" "postBuild" ] - // { inherit preferLocalBuild allowSubstitutes; }; # pass the defaults - in pkgs.runCommand name args - '' - mkdir -p $out - for i in $paths; do - if [ -z "$(find $i -prune -empty)" ]; then - cp -srf $i/* $out - fi - done - ${postBuild} - ''; - generateCompletions = package: pkgs.runCommand - "${package.name}-fish-completions" - { + args = removeAttrs args_ [ "name" "postBuild" ] // { + # pass the defaults + inherit preferLocalBuild allowSubstitutes; + }; + in pkgs.runCommand name args '' + mkdir -p $out + for i in $paths; do + if [ -z "$(find $i -prune -empty)" ]; then + cp -srf $i/* $out + fi + done + ${postBuild} + ''; + + generateCompletions = package: + pkgs.runCommand "${package.name}-fish-completions" { src = package; nativeBuildInputs = [ pkgs.python2 ]; buildInputs = [ cfg.package ]; preferLocalBuild = true; allowSubstitutes = false; - } - '' + } '' mkdir -p $out if [ -d $src/share/man ]; then find $src/share/man -type f \ @@ -126,66 +317,144 @@ in > /dev/null fi ''; - in - destructiveSymlinkJoin { - name = "${config.home.username}-fish-completions"; - paths = - let - cmp = (a: b: (a.meta.priority or 0) > (b.meta.priority or 0)); - in - map generateCompletions (sort cmp config.home.packages); + in destructiveSymlinkJoin { + name = "${config.home.username}-fish-completions"; + paths = + let cmp = (a: b: (a.meta.priority or 0) > (b.meta.priority or 0)); + in map generateCompletions (sort cmp config.home.packages); + }; + + programs.fish.interactiveShellInit = '' + # add completions generated by Home Manager to $fish_complete_path + begin + set -l joined (string join " " $fish_complete_path) + set -l prev_joined (string replace --regex "[^\s]*generated_completions.*" "" $joined) + set -l post_joined (string replace $prev_joined "" $joined) + set -l prev (string split " " (string trim $prev_joined)) + set -l post (string split " " (string trim $post_joined)) + set fish_complete_path $prev "${config.xdg.dataHome}/fish/home-manager_generated_completions" $post + end + ''; + + xdg.configFile."fish/config.fish".text = '' + # ~/.config/fish/config.fish: DO NOT EDIT -- this file has been generated + # automatically by home-manager. + + # if we haven't sourced the general config, do it + if not set -q __fish_general_config_sourced + + set -p fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions + fenv source ${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh > /dev/null + set -e fish_function_path[1] + + ${cfg.shellInit} + # and leave a note so we don't source this config section again from + # this very shell (children will source the general config anew) + set -g __fish_general_config_sourced 1 + + end + + # if we haven't sourced the login config, do it + status --is-login; and not set -q __fish_login_config_sourced + and begin + + # Login shell initialisation + ${cfg.loginShellInit} + + # and leave a note so we don't source this config section again from + # this very shell (children will source the general config anew) + set -g __fish_login_config_sourced 1 + + end + + # if we haven't sourced the interactive config, do it + status --is-interactive; and not set -q __fish_interactive_config_sourced + and begin + + # Abbreviations + ${abbrsStr} + + # Aliases + ${aliasesStr} + + # Prompt initialisation + ${cfg.promptInit} + + # Interactive shell intialisation + ${cfg.interactiveShellInit} + + # and leave a note so we don't source this config section again from + # this very shell (children will source the general config anew, + # allowing configuration changes in, e.g, aliases, to propagate) + set -g __fish_interactive_config_sourced 1 + + end + ''; + } + { + xdg.configFile = mapAttrs' (name: def: { + name = "fish/functions/${name}.fish"; + value = { + text = let + modifierStr = n: v: optional (v != null) ''--${n}="${toString v}"''; + modifierStrs = n: v: optional (v != null) "--${n}=${toString v}"; + modifierBool = n: v: optional (v != null && v) "--${n}"; + + mods = with def; + modifierStr "description" description ++ modifierStr "wraps" wraps + ++ modifierStr "on-event" onEvent + ++ modifierStr "on-variable" onVariable + ++ modifierStr "on-job-exit" onJobExit + ++ modifierStr "on-process-exit" onProcessExit + ++ modifierStr "on-signal" onSignal + ++ modifierBool "no-scope-shadowing" noScopeShadowing + ++ modifierStr "inherit-variable" inheritVariable + ++ modifierStrs "argument-names" argumentNames; + + modifiers = if isAttrs def then " ${toString mods}" else ""; + body = if isAttrs def then def.body else def; + in '' + function ${name}${modifiers} + ${body} + end + ''; }; + }) cfg.functions; + } - programs.fish.interactiveShellInit = '' - # add completions generated by Home Manager to $fish_complete_path - begin - set -l joined (string join " " $fish_complete_path) - set -l prev_joined (string replace --regex "[^\s]*generated_completions.*" "" $joined) - set -l post_joined (string replace $prev_joined "" $joined) - set -l prev (string split " " (string trim $prev_joined)) - set -l post (string split " " (string trim $post_joined)) - set fish_complete_path $prev "${config.xdg.dataHome}/fish/home-manager_generated_completions" $post - end - ''; - - xdg.configFile."fish/config.fish".text = '' - # ~/.config/fish/config.fish: DO NOT EDIT -- this file has been generated automatically. - # if we haven't sourced the general config, do it - if not set -q __fish_general_config_sourced - set fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions $fish_function_path - fenv source ${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh > /dev/null - set -e fish_function_path[1] - - ${cfg.shellInit} - # and leave a note so we don't source this config section again from - # this very shell (children will source the general config anew) - set -g __fish_general_config_sourced 1 - end - # if we haven't sourced the login config, do it - status --is-login; and not set -q __fish_login_config_sourced - and begin - - ${cfg.loginShellInit} - # and leave a note so we don't source this config section again from - # this very shell (children will source the general config anew) - set -g __fish_login_config_sourced 1 - end - # if we haven't sourced the interactive config, do it - status --is-interactive; and not set -q __fish_interactive_config_sourced - and begin - # Abbrs - ${abbrsStr} - - # Aliases - ${aliasesStr} - - ${cfg.promptInit} - ${cfg.interactiveShellInit} - # and leave a note so we don't source this config section again from - # this very shell (children will source the general config anew, - # allowing configuration changes in, e.g, aliases, to propagate) - set -g __fish_interactive_config_sourced 1 - end - ''; - }; + # Each plugin gets a corresponding conf.d/plugin-NAME.fish file to load + # in the paths and any initialization scripts. + (mkIf (length cfg.plugins > 0) { + xdg.configFile = mkMerge ((map (plugin: { + "fish/conf.d/plugin-${plugin.name}.fish".text = '' + # Plugin ${plugin.name} + set -l plugin_dir ${plugin.src} + + # Set paths to import plugin components + if test -d $plugin_dir/functions + set fish_function_path $fish_function_path[1] $plugin_dir/functions $fish_function_path[2..-1] + end + + if test -d $plugin_dir/completions + set fish_complete_path $fish_complete_path[1] $plugin_dir/completions $fish_complete_path[2..-1] + end + + # Source initialization code if it exists. + if test -d $plugin_dir/conf.d + for f in $plugin_dir/conf.d/*.fish + source $f + end + end + + if test -f $plugin_dir/key_bindings.fish + source $plugin_dir/key_bindings.fish + end + + if test -f $plugin_dir/init.fish + source $plugin_dir/init.fish + end + ''; + }) cfg.plugins)); + }) + ]); } diff --git a/home-manager/modules/programs/fzf.nix b/home-manager/modules/programs/fzf.nix index 36eb3a1cdba..3aee57768ea 100644 --- a/home-manager/modules/programs/fzf.nix +++ b/home-manager/modules/programs/fzf.nix @@ -100,6 +100,14 @@ in { Whether to enable Zsh integration. ''; }; + + enableFishIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Fish integration. + ''; + }; }; config = mkIf cfg.enable { @@ -130,5 +138,9 @@ in { . ${pkgs.fzf}/share/fzf/key-bindings.zsh fi ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + source ${pkgs.fzf}/share/fzf/key-bindings.fish && fzf_key_bindings + ''; }; } diff --git a/home-manager/modules/programs/getmail.nix b/home-manager/modules/programs/getmail.nix index 2c3919dcf2f..f83c469ff24 100644 --- a/home-manager/modules/programs/getmail.nix +++ b/home-manager/modules/programs/getmail.nix @@ -49,6 +49,12 @@ let ".getmail/getmail${if a.primary then "rc" else a.name}"; in { + options = { + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./getmail-accounts.nix)); + }; + }; + config = mkIf getmailEnabled { home.file = foldl' (a: b: a // b) { } (map (a: { "${renderConfigFilepath a}".text = renderAccountConfig a; }) diff --git a/home-manager/modules/programs/git.nix b/home-manager/modules/programs/git.nix index a56aa10d50e..312269de316 100644 --- a/home-manager/modules/programs/git.nix +++ b/home-manager/modules/programs/git.nix @@ -19,10 +19,20 @@ let else ''${section} "${subsection}"''; + mkValueString = v: + let + escapedV = '' + "${ + replaceStrings [ "\n" " " ''"'' "\\" ] [ "\\n" "\\t" ''\"'' "\\\\" ] v + }"''; + in generators.mkValueStringDefault { } (if isString v then escapedV else v); + # generation for multiple ini values mkKeyValue = k: v: - let mkKeyValue = generators.mkKeyValueDefault { } "=" k; - in concatStringsSep "\n" (map mkKeyValue (toList v)); + let + mkKeyValue = + generators.mkKeyValueDefault { inherit mkValueString; } " = " k; + in concatStringsSep "\n" (map (kv: " " + mkKeyValue kv) (toList v)); # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI gitFlattenAttrs = let @@ -205,6 +215,36 @@ in { ''; }; }; + + delta = { + enable = mkEnableOption "" // { + description = '' + Whether to enable the <command>delta</command> syntax highlighter. + See <link xlink:href="https://github.com/dandavison/delta" />. + ''; + }; + + options = mkOption { + type = with types; + let + primitiveType = either str (either bool int); + sectionType = attrsOf primitiveType; + in attrsOf (either primitiveType sectionType); + default = { }; + example = { + features = "decorations"; + whitespace-error-style = "22 reverse"; + decorations = { + commit-decoration-style = "bold yellow box ul"; + file-style = "bold yellow ul"; + file-decoration-style = "none"; + }; + }; + description = '' + Options to configure delta. + ''; + }; + }; }; }; @@ -237,7 +277,14 @@ in { genIdentity = name: account: with account; nameValuePair "sendemail.${name}" ({ - smtpEncryption = if smtp.tls.enable then "tls" else ""; + smtpEncryption = if smtp.tls.enable then + (if smtp.tls.useStartTls + || versionOlder config.home.stateVersion "20.09" then + "tls" + else + "ssl") + else + ""; smtpServer = smtp.host; smtpUser = userName; from = address; @@ -299,5 +346,15 @@ in { ([ "git-lfs" "smudge" ] ++ skipArg ++ [ "--" "%f" ]); }; }) + + (mkIf cfg.delta.enable { + programs.git.iniContent = + let deltaCommand = "${pkgs.gitAndTools.delta}/bin/delta"; + in { + core.pager = deltaCommand; + interactive.diffFilter = "${deltaCommand} --color-only"; + delta = cfg.delta.options; + }; + }) ]); } diff --git a/home-manager/modules/programs/gnome-terminal.nix b/home-manager/modules/programs/gnome-terminal.nix index 570a1fc7df0..f1b15862130 100644 --- a/home-manager/modules/programs/gnome-terminal.nix +++ b/home-manager/modules/programs/gnome-terminal.nix @@ -6,11 +6,6 @@ let cfg = config.programs.gnome-terminal; - vteInitStr = '' - # gnome-terminal: Show current directory in the terminal window title. - . ${pkgs.gnome3.vte}/etc/profile.d/vte.sh - ''; - backForeSubModule = types.submodule ({ ... }: { options = { foreground = mkOption { @@ -81,6 +76,12 @@ let description = "The terminal colors, null to use system default."; }; + cursorBlinkMode = mkOption { + default = "system"; + type = types.enum [ "system" "on" "off" ]; + description = "The cursor blink mode."; + }; + cursorShape = mkOption { default = "block"; type = types.enum [ "block" "ibeam" "underline" ]; @@ -121,6 +122,20 @@ let The number of scrollback lines to keep, null for infinite. ''; }; + + customCommand = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The command to use to start the shell, or null for default shell. + ''; + }; + + loginShell = mkOption { + default = false; + type = types.bool; + description = "Run command as a login shell."; + }; }; }); @@ -130,7 +145,14 @@ let scrollbar-policy = if pcfg.showScrollbar then "always" else "never"; scrollback-lines = pcfg.scrollbackLines; cursor-shape = pcfg.cursorShape; - } // (if (pcfg.font == null) then { + cursor-blink-mode = pcfg.cursorBlinkMode; + login-shell = pcfg.loginShell; + } // (if (pcfg.customCommand != null) then { + use-custom-command = true; + custom-command = pcfg.customCommand; + } else { + use-custom-command = false; + }) // (if (pcfg.font == null) then { use-system-font = true; } else { use-system-font = false; @@ -179,7 +201,7 @@ in { themeVariant = mkOption { default = "default"; - type = types.enum [ "default" "light" "dark" ]; + type = types.enum [ "default" "light" "dark" "system" ]; description = "The theme variation to request"; }; @@ -192,7 +214,7 @@ in { }; config = mkIf cfg.enable { - home.packages = [ pkgs.gnome3.gnome_terminal ]; + home.packages = [ pkgs.gnome3.gnome-terminal ]; dconf.settings = let dconfPath = "org/gnome/terminal/legacy"; in { @@ -210,7 +232,7 @@ in { (n: v: nameValuePair ("${dconfPath}/profiles:/:${n}") (buildProfileSet v)) cfg.profile; - programs.bash.initExtra = mkBefore vteInitStr; - programs.zsh.initExtra = vteInitStr; + programs.bash.enableVteIntegration = true; + programs.zsh.enableVteIntegration = true; }; } diff --git a/home-manager/modules/programs/go.nix b/home-manager/modules/programs/go.nix index 983769d26af..4b85ec854ad 100644 --- a/home-manager/modules/programs/go.nix +++ b/home-manager/modules/programs/go.nix @@ -62,6 +62,18 @@ in { example = ".local/bin.go"; description = "GOBIN relative to HOME"; }; + + goPrivate = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "*.corp.example.com" "rsc.io/private" ]; + description = '' + The <envar>GOPRIVATE</envar> environment variable controls + which modules the go command considers to be private (not + available publicly) and should therefore not use the proxy + or checksum database. + ''; + }; }; }; @@ -85,5 +97,9 @@ in { home.sessionVariables.GOBIN = builtins.toPath "${config.home.homeDirectory}/${cfg.goBin}"; }) + + (mkIf (cfg.goPrivate != [ ]) { + home.sessionVariables.GOPRIVATE = concatStringsSep "," cfg.goPrivate; + }) ]); } diff --git a/home-manager/modules/programs/htop.nix b/home-manager/modules/programs/htop.nix index 84966040534..1fb397cdc38 100644 --- a/home-manager/modules/programs/htop.nix +++ b/home-manager/modules/programs/htop.nix @@ -61,6 +61,9 @@ let CGROUP = 112; OOM = 113; IO_PRIORITY = 114; + M_PSS = 118; + M_SWAP = 119; + M_PSSWP = 120; }; # Mapping from names to defaults @@ -76,16 +79,32 @@ let Hostname = 2; AllCPUs = 1; AllCPUs2 = 1; + AllCPUs4 = 1; LeftCPUs = 1; RightCPUs = 1; + Right = 1; + CPUs = 1; LeftCPUs2 = 1; RightCPUs2 = 1; + LeftCPUs4 = 1; + RightCPUs4 = 1; Blank = 2; + PressureStallCPUSome = 2; + PressureStallIOSome = 2; + PressureStallIOFull = 2; + PressureStallMemorySome = 2; + PressureStallMemoryFull = 2; + ZFSARC = 2; + ZFSCARC = 2; CPU = 1; "CPU(1)" = 1; "CPU(2)" = 1; "CPU(3)" = 1; "CPU(4)" = 1; + "CPU(5)" = 1; + "CPU(6)" = 1; + "CPU(7)" = 1; + "CPU(8)" = 1; }; singleMeterType = let @@ -268,6 +287,18 @@ in { description = "Count CPUs from 0 instead of 1."; }; + showCpuUsage = mkOption { + type = types.bool; + default = false; + description = "Show CPU usage frequency."; + }; + + showCpuFrequency = mkOption { + type = types.bool; + default = false; + description = "Show CPU frequency."; + }; + updateProcessNames = mkOption { type = types.bool; default = false; @@ -287,6 +318,12 @@ in { description = "Which color scheme to use."; }; + enableMouse = mkOption { + type = types.bool; + default = true; + description = "Enable mouse support."; + }; + delay = mkOption { type = types.int; default = 15; @@ -328,6 +365,11 @@ in { type = meterType; }; + vimMode = mkOption { + type = types.bool; + default = false; + description = "Vim key bindings."; + }; }; config = mkIf cfg.enable { @@ -357,14 +399,18 @@ in { header_margin=${bool cfg.headerMargin} detailed_cpu_time=${bool cfg.detailedCpuTime} cpu_count_from_zero=${bool cfg.cpuCountFromZero} + show_cpu_usage=${bool cfg.showCpuUsage} + show_cpu_frequency=${bool cfg.showCpuFrequency} update_process_names=${bool cfg.updateProcessNames} account_guest_in_cpu_meter=${bool cfg.accountGuestInCpuMeter} color_scheme=${toString cfg.colorScheme} + enable_mouse=${bool cfg.enableMouse} delay=${toString cfg.delay} left_meters=${list leftMeters} left_meter_modes=${list leftModes} right_meters=${list rightMeters} right_meter_modes=${list rightModes} + vim_mode=${bool cfg.vimMode} ''; }; } diff --git a/home-manager/modules/programs/i3status.nix b/home-manager/modules/programs/i3status.nix new file mode 100644 index 00000000000..c1e12fe71d7 --- /dev/null +++ b/home-manager/modules/programs/i3status.nix @@ -0,0 +1,208 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.i3status; + + enabledModules = filterAttrs (n: v: v.enable) cfg.modules; + + formatOrder = n: ''order += "${n}"''; + + formatModule = n: v: + let + formatLine = n: v: + let + formatValue = v: + if isBool v then + (if v then "true" else "false") + else if isString v then + ''"${v}"'' + else + toString v; + in "${n} = ${formatValue v}"; + in '' + ${n} { + ${concatStringsSep "\n " (mapAttrsToList formatLine v)} + } + ''; + + settingsType = with types; attrsOf (oneOf [ bool int str ]); + + sortAttrNamesByPosition = comparator: set: + let pos = n: set."${n}".position; + in sort (a: b: comparator (pos a) (pos b)) (attrNames set); +in { + meta.maintainers = [ hm.maintainers.justinlovinger ]; + + options.programs.i3status = { + enable = mkEnableOption "i3status"; + + enableDefault = mkOption { + type = types.bool; + default = true; + description = '' + Whether or not to enable + the default configuration. + ''; + }; + + general = mkOption { + type = settingsType; + default = { }; + description = '' + Configuration to add to i3status <filename>config</filename> + <code>general</code> section. + See + <citerefentry> + <refentrytitle>i3status</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for options. + ''; + example = literalExample '' + { + colors = true; + color_good = "#e0e0e0"; + color_degraded = "#d7ae00"; + color_bad = "#f69d6a"; + interval = 1; + } + ''; + }; + + modules = mkOption { + type = types.attrsOf (types.submodule { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether or not to enable this module. + ''; + }; + position = mkOption { + type = with types; either int float; + description = '' + Position of this module in i3status <code>order</code>. + ''; + }; + settings = mkOption { + type = settingsType; + default = { }; + description = '' + Configuration to add to this i3status module. + See + <citerefentry> + <refentrytitle>i3status</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for options. + ''; + example = literalExample '' + { + format = "βͺ %volume"; + format_muted = "βͺ muted (%volume)"; + device = "pulse:1"; + } + ''; + }; + }; + }); + default = { }; + description = '' + Modules to add to i3status <filename>config</filename> file. + See + <citerefentry> + <refentrytitle>i3status</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for options. + ''; + example = literalExample '' + { + "volume master" = { + position = 1; + settings = { + format = "βͺ %volume"; + format_muted = "βͺ muted (%volume)"; + device = "pulse:1"; + }; + }; + "disk /" = { + position = 2; + settings = { + format = "/ %avail"; + }; + }; + } + ''; + }; + }; + + config = mkIf cfg.enable { + programs.i3status = mkIf cfg.enableDefault { + general = { + colors = mkDefault true; + interval = mkDefault 5; + }; + + modules = { + ipv6 = { position = mkDefault 1; }; + + "wireless _first_" = { + position = mkDefault 2; + settings = { + format_up = mkDefault "W: (%quality at %essid) %ip"; + format_down = mkDefault "W: down"; + }; + }; + + "ethernet _first_" = { + position = mkDefault 3; + settings = { + format_up = mkDefault "E: %ip (%speed)"; + format_down = mkDefault "E: down"; + }; + }; + + "battery all" = { + position = mkDefault 4; + settings = { format = mkDefault "%status %percentage %remaining"; }; + }; + + "disk /" = { + position = mkDefault 5; + settings = { format = mkDefault "%avail"; }; + }; + + load = { + position = mkDefault 6; + settings = { format = mkDefault "%1min"; }; + }; + + memory = { + position = mkDefault 7; + settings = { + format = mkDefault "%used | %available"; + threshold_degraded = mkDefault "1G"; + format_degraded = mkDefault "MEMORY < %available"; + }; + }; + + "tztime local" = { + position = mkDefault 8; + settings = { format = mkDefault "%Y-%m-%d %H:%M:%S"; }; + }; + }; + }; + + home.packages = [ pkgs.i3status ]; + + xdg.configFile."i3status/config".text = concatStringsSep "\n" ([ ] + ++ optional (cfg.general != { }) (formatModule "general" cfg.general) + ++ map formatOrder (sortAttrNamesByPosition lessThan enabledModules) + ++ mapAttrsToList formatModule + (mapAttrs (n: v: v.settings) enabledModules)); + }; +} diff --git a/home-manager/modules/programs/info.nix b/home-manager/modules/programs/info.nix index 9e4a5d4aaff..a7d2692b515 100644 --- a/home-manager/modules/programs/info.nix +++ b/home-manager/modules/programs/info.nix @@ -1,22 +1,21 @@ -# info.nix -- install texinfo, set INFOPATH, create `dir` file +# info.nix -- install texinfo and create `dir` file # This is a helper for the GNU info documentation system. By default, # the `info` command (and the Info subsystem within Emacs) gives easy # access to the info files stored system-wide, but not info files in # your ~/.nix-profile. -# We set $INFOPATH to include `/run/current-system/sw/share/info` and -# `~/.nix-profile/share/info` but it's not enough. Although info can -# then find files when you explicitly ask for them, it doesn't show -# them to you in the table of contents on startup. To do that requires -# a `dir` file. NixOS keeps the system-wide `dir` file up to date, but -# ignores home-installed packages. +# Specifically, although info can then find files when you explicitly +# ask for them, it doesn't show them to you in the table of contents +# on startup. To do that requires a `dir` file. NixOS keeps the +# system-wide `dir` file up to date, but ignores files installed in +# user profiles. -# So this module contains an activation script that generates the -# `dir` for your home profile. Then when you start info (and both -# `dir` files are in your $INFOPATH), it will *merge* the contents of -# the two files, showing you a unified table of contents for all -# packages. This is really nice. +# This module contains extra profile commands that generate the `dir` +# for your home profile. Then when you start info (and both `dir` +# files are in your $INFOPATH), it will *merge* the contents of the +# two files, showing you a unified table of contents for all packages. +# This is really nice. { config, lib, pkgs, ... }: @@ -26,50 +25,39 @@ let cfg = config.programs.info; - # Indexes info files found in this location - homeInfoPath = "${config.home.profileDirectory}/share/info"; - # Installs this package -- the interactive just means that it # includes the curses `info` program. We also use `install-info` # from this package in the activation script. infoPkg = pkgs.texinfoInteractive; in { - options = { - programs.info = { - enable = mkEnableOption "GNU Info"; + imports = [ + (mkRemovedOptionModule [ "programs" "info" "homeInfoDirLocation" ] '' + The `dir` file is now generated as part of the Home Manager profile and + will no longer be placed in your home directory. + '') + ]; - homeInfoDirLocation = mkOption { - default = "\${XDG_CACHE_HOME:-$HOME/.cache}/info"; - description = '' - Directory in which to store the info <filename>dir</filename> - file within your home. - ''; - }; - }; - }; + options.programs.info.enable = mkEnableOption "GNU Info"; config = mkIf cfg.enable { - home.sessionVariables.INFOPATH = - "${cfg.homeInfoDirLocation}\${INFOPATH:+:}\${INFOPATH}"; + home.packages = [ + infoPkg - home.activation.createHomeInfoDir = - hm.dag.entryAfter [ "installPackages" ] '' - oPATH=$PATH - export PATH="${lib.makeBinPath [ pkgs.gzip ]}''${PATH:+:}$PATH" - $DRY_RUN_CMD mkdir -p "${cfg.homeInfoDirLocation}" - $DRY_RUN_CMD rm -f "${cfg.homeInfoDirLocation}/dir" - if [[ -d "${homeInfoPath}" ]]; then - find -L "${homeInfoPath}" \( -name '*.info' -o -name '*.info.gz' \) \ - -exec $DRY_RUN_CMD ${infoPkg}/bin/install-info '{}' \ - "${cfg.homeInfoDirLocation}/dir" \; - fi - export PATH="$oPATH" - unset oPATH - ''; - - home.packages = [ infoPkg ]; + # Make sure the target directory is a real directory. + (pkgs.runCommandLocal "dummy-info-dir1" { } "mkdir -p $out/share/info") + (pkgs.runCommandLocal "dummy-info-dir2" { } "mkdir -p $out/share/info") + ]; home.extraOutputsToInstall = [ "info" ]; + + home.extraProfileCommands = let infoPath = "$out/share/info"; + in '' + if [[ -w "${infoPath}" && ! -e "${infoPath}/dir" ]]; then + PATH="${lib.makeBinPath [ pkgs.gzip infoPkg ]}''${PATH:+:}$PATH" \ + find -L "${infoPath}" \( -name '*.info' -o -name '*.info.gz' \) \ + -exec install-info '{}' "${infoPath}/dir" ';' + fi + ''; }; } diff --git a/home-manager/modules/programs/kakoune.nix b/home-manager/modules/programs/kakoune.nix index faf2542dc70..6db311a1376 100644 --- a/home-manager/modules/programs/kakoune.nix +++ b/home-manager/modules/programs/kakoune.nix @@ -49,6 +49,7 @@ let "InsertCompletionShow" "InsertCompletionHide" "InsertCompletionSelect" + "ModuleLoaded" ]; example = "SetOption"; description = '' @@ -96,16 +97,7 @@ let keyMapping = types.submodule { options = { mode = mkOption { - type = types.enum [ - "insert" - "normal" - "prompt" - "menu" - "user" - "goto" - "view" - "object" - ]; + type = types.str; example = "user"; description = '' The mode in which the mapping takes effect. @@ -497,6 +489,10 @@ let }; }; + kakouneWithPlugins = pkgs.wrapKakoune pkgs.kakoune-unwrapped { + configure = { plugins = cfg.plugins; }; + }; + configFile = let wrapOptions = with cfg.config.wrapLines; concatStrings [ @@ -513,6 +509,25 @@ let "${optionalString (separator != null) " -separator ${separator}"}" ]; + showWhitespaceOptions = with cfg.config.showWhitespace; + let + quoteSep = sep: + if sep == "'" then + ''"'"'' + else if lib.strings.stringLength sep == 1 then + "'${sep}'" + else + sep; # backwards compat, in case sep == "' '", etc. + + in concatStrings [ + (optionalString (tab != null) " -tab ${quoteSep tab}") + (optionalString (tabStop != null) " -tabpad ${quoteSep tabStop}") + (optionalString (space != null) " -spc ${quoteSep space}") + (optionalString (nonBreakingSpace != null) + " -nbsp ${quoteSep nonBreakingSpace}") + (optionalString (lineFeed != null) " -lf ${quoteSep lineFeed}") + ]; + uiOptions = with cfg.config.ui; concatStringsSep " " [ "ncurses_set_title=${if setTitle then "true" else "false"}" @@ -533,6 +548,21 @@ let }" ]; + userModeString = mode: + optionalString (!builtins.elem mode [ + "insert" + "normal" + "prompt" + "menu" + "user" + "goto" + "view" + "object" + ]) "try %{declare-user-mode ${mode}}"; + + userModeStrings = map userModeString + (lists.unique (map (km: km.mode) cfg.config.keyMappings)); + keyMappingString = km: concatStringsSep " " [ "map global" @@ -566,12 +596,14 @@ let ++ optional (autoComplete != null) "set-option global autocomplete ${concatStringsSep "|" autoComplete}" ++ optional (autoReload != null) - "set-option global/ autoreload ${autoReload}" + "set-option global autoreload ${autoReload}" ++ optional (wrapLines != null && wrapLines.enable) "add-highlighter global/ wrap${wrapOptions}" ++ optional (numberLines != null && numberLines.enable) "add-highlighter global/ number-lines${numberLinesOptions}" ++ optional showMatching "add-highlighter global/ show-matching" + ++ optional (showWhitespace != null && showWhitespace.enable) + "add-highlighter global/ show-whitespaces${showWhitespaceOptions}" ++ optional (scrollOff != null) "set-option global scrolloff ${toString scrollOff.lines},${ toString scrollOff.columns @@ -580,7 +612,8 @@ let ++ [ "# UI options" ] ++ optional (ui != null) "set-option global ui_options ${uiOptions}" - ++ [ "# Key mappings" ] ++ map keyMappingString keyMappings + ++ [ "# User modes" ] ++ userModeStrings ++ [ "# Key mappings" ] + ++ map keyMappingString keyMappings ++ [ "# Hooks" ] ++ map hookString hooks); in pkgs.writeText "kakrc" @@ -605,11 +638,22 @@ in { <filename>~/.config/kak/kakrc</filename>. ''; }; + + plugins = mkOption { + type = with types; listOf package; + default = [ ]; + example = literalExample "[ pkgs.kakounePlugins.kak-fzf ]"; + description = '' + List of kakoune plugins to install. To get a list of + supported plugins run: + <command>nix-env -f '<nixpkgs>' -qaP -A kakounePlugins</command>. + ''; + }; }; }; config = mkIf cfg.enable { - home.packages = [ pkgs.kakoune ]; + home.packages = [ kakouneWithPlugins ]; xdg.configFile."kak/kakrc".source = configFile; }; } diff --git a/home-manager/modules/programs/kitty.nix b/home-manager/modules/programs/kitty.nix new file mode 100644 index 00000000000..313a0bfadd7 --- /dev/null +++ b/home-manager/modules/programs/kitty.nix @@ -0,0 +1,91 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.kitty; + + eitherStrBoolInt = with types; either str (either bool int); + + optionalPackage = opt: + optional (opt != null && opt.package != null) opt.package; + + toKittyConfig = generators.toKeyValue { + mkKeyValue = key: value: + let + value' = if isBool value then + (if value then "yes" else "no") + else + toString value; + in "${key} ${value'}"; + }; + + toKittyKeybindings = generators.toKeyValue { + mkKeyValue = key: command: "map ${key} ${command}"; + }; + +in { + options.programs.kitty = { + enable = mkEnableOption "Kitty terminal emulator"; + + settings = mkOption { + type = types.attrsOf eitherStrBoolInt; + default = { }; + example = literalExample '' + { + scrollback_lines = 10000; + enable_audio_bell = false; + update_check_interval = 0; + } + ''; + description = '' + Configuration written to + <filename>~/.config/kitty/kitty.conf</filename>. See + <link xlink:href="https://sw.kovidgoyal.net/kitty/conf.html" /> + for the documentation. + ''; + }; + + font = mkOption { + type = types.nullOr hm.types.fontType; + default = null; + description = "The font to use."; + }; + + keybindings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Mapping of keybindings to actions."; + example = literalExample '' + { + "ctrl+c" = "copy_or_interrupt"; + "ctrl+f>2" = "set_font_size 20"; + } + ''; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = "Additional configuration to add."; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.kitty ] ++ optionalPackage cfg.font; + + xdg.configFile."kitty/kitty.conf".text = '' + # Generated by Home Manager. + # See https://sw.kovidgoyal.net/kitty/conf.html + + ${optionalString (cfg.font != null) "font_family ${cfg.font.name}"} + + ${toKittyConfig cfg.settings} + + ${toKittyKeybindings cfg.keybindings} + + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/lf.nix b/home-manager/modules/programs/lf.nix new file mode 100644 index 00000000000..ee4e9b5bfce --- /dev/null +++ b/home-manager/modules/programs/lf.nix @@ -0,0 +1,219 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.lf; + + knownSettings = { + anchorfind = types.bool; + color256 = types.bool; + dircounts = types.bool; + dirfirst = types.bool; + drawbox = types.bool; + globsearch = types.bool; + icons = types.bool; + hidden = types.bool; + ignorecase = types.bool; + ignoredia = types.bool; + incsearch = types.bool; + preview = types.bool; + reverse = types.bool; + smartcase = types.bool; + smartdia = types.bool; + wrapscan = types.bool; + wrapscroll = types.bool; + number = types.bool; + relativenumber = types.bool; + findlen = types.int; + period = types.int; + scrolloff = types.int; + tabstop = types.int; + errorfmt = types.str; + filesep = types.str; + ifs = types.str; + promptfmt = types.str; + shell = types.str; + sortby = types.str; + timefmt = types.str; + ratios = types.str; + info = types.str; + shellopts = types.str; + }; + + lfSettingsType = types.submodule { + options = let + opt = name: type: + mkOption { + type = types.nullOr type; + default = null; + visible = false; + }; + in mapAttrs opt knownSettings; + }; +in { + meta.maintainers = [ hm.maintainers.owm111 ]; + + options = { + programs.lf = { + enable = mkEnableOption "lf"; + + settings = mkOption { + type = lfSettingsType; + default = { }; + example = { + tabstop = 4; + number = true; + ratios = "1:1:2"; + }; + description = '' + An attribute set of lf settings. The attribute names and corresponding + values must be among the following supported options. + + <informaltable frame="none"><tgroup cols="1"><tbody> + ${concatStringsSep "\n" (mapAttrsToList (n: v: '' + <row> + <entry><varname>${n}</varname></entry> + <entry>${v.description}</entry> + </row> + '') knownSettings)} + </tbody></tgroup></informaltable> + + See the lf documentation for detailed descriptions of these options. + Note, use <varname>previewer</varname> to set lf's + <varname>previewer</varname> option, and + <varname>extraConfig</varname> for any other option not listed above. + All string options are quoted with double quotes. + ''; + }; + + commands = mkOption { + type = with types; attrsOf (nullOr str); + default = { }; + example = { + get-mime-type = ''%xdg-mime query filetype "$f"''; + open = "$$OPENER $f"; + }; + description = '' + Commands to declare. Commands set to null or an empty string are + deleted. + ''; + }; + + keybindings = mkOption { + type = with types; attrsOf (nullOr str); + default = { }; + example = { + gh = "cd ~"; + D = "trash"; + i = "$less $f"; + U = "!du -sh"; + gg = null; + }; + description = + "Keys to bind. Keys set to null or an empty string are deleted."; + }; + + cmdKeybindings = mkOption { + type = with types; attrsOf (nullOr str); + default = { }; + example = literalExample ''{ "<c-g>" = "cmd-escape"; }''; + description = '' + Keys to bind to command line commands which can only be one of the + builtin commands. Keys set to null or an empty string are deleted. + ''; + }; + + previewer.source = mkOption { + type = with types; nullOr path; + default = null; + example = literalExample '' + pkgs.writeShellScript "pv.sh" ''' + #!/bin/sh + + case "$1" in + *.tar*) tar tf "$1";; + *.zip) unzip -l "$1";; + *.rar) unrar l "$1";; + *.7z) 7z l "$1";; + *.pdf) pdftotext "$1" -;; + *) highlight -O ansi "$1" || cat "$1";; + esac + ''' + ''; + description = '' + Script or executable to use to preview files. Sets lf's + <varname>previewer</varname> option. + ''; + }; + + previewer.keybinding = mkOption { + type = with types; nullOr str; + default = null; + example = "i"; + description = '' + Key to bind to the script at <varname>previewer.source</varname> and + pipe through less. Setting to null will not bind any key. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + $mkdir -p ~/.trash + ''; + description = "Custom lfrc lines."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.lf ]; + + xdg.configFile."lf/lfrc".text = let + fmtSetting = k: v: + optionalString (v != null) "set ${ + if isBool v then + "${optionalString (!v) "no"}${k}" + else + "${k} ${if isInt v then toString v else ''"${v}"''}" + }"; + + settingsStr = concatStringsSep "\n" (filter (x: x != "") + (mapAttrsToList fmtSetting + (builtins.intersectAttrs knownSettings cfg.settings))); + + fmtCmdMap = before: k: v: + "${before} ${k}${optionalString (v != null && v != "") " ${v}"}"; + fmtCmd = fmtCmdMap "cmd"; + fmtMap = fmtCmdMap "map"; + fmtCmap = fmtCmdMap "cmap"; + + commandsStr = concatStringsSep "\n" (mapAttrsToList fmtCmd cfg.commands); + keybindingsStr = + concatStringsSep "\n" (mapAttrsToList fmtMap cfg.keybindings); + cmdKeybindingsStr = + concatStringsSep "\n" (mapAttrsToList fmtCmap cfg.cmdKeybindings); + + previewerStr = optionalString (cfg.previewer.source != null) '' + set previewer ${cfg.previewer.source} + ${optionalString (cfg.previewer.keybinding != null) '' + map ${cfg.previewer.keybinding} ''$${cfg.previewer.source} "$f" | less -R + ''} + ''; + in '' + ${settingsStr} + + ${commandsStr} + + ${keybindingsStr} + + ${cmdKeybindingsStr} + + ${previewerStr} + + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/lieer-accounts.nix b/home-manager/modules/programs/lieer-accounts.nix new file mode 100644 index 00000000000..238049065b3 --- /dev/null +++ b/home-manager/modules/programs/lieer-accounts.nix @@ -0,0 +1,69 @@ +{ lib, ... }: + +with lib; + +{ + options.lieer = { + enable = mkEnableOption "lieer Gmail synchronization for notmuch"; + + timeout = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + HTTP timeout in seconds. 0 means forever or system timeout. + ''; + }; + + replaceSlashWithDot = mkOption { + type = types.bool; + default = false; + description = '' + Replace '/' with '.' in Gmail labels. + ''; + }; + + dropNonExistingLabels = mkOption { + type = types.bool; + default = false; + description = '' + Allow missing labels on the Gmail side to be dropped. + ''; + }; + + ignoreTagsLocal = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Set custom tags to ignore when syncing from local to + remote (after translations). + ''; + }; + + ignoreTagsRemote = mkOption { + type = types.listOf types.str; + default = [ + "CATEGORY_FORUMS" + "CATEGORY_PROMOTIONS" + "CATEGORY_UPDATES" + "CATEGORY_SOCIAL" + "CATEGORY_PERSONAL" + ]; + description = '' + Set custom tags to ignore when syncing from remote to + local (before translations). + ''; + }; + + notmuchSetupWarning = mkOption { + type = types.bool; + default = true; + description = '' + Warn if Notmuch is not also enabled for this account. + </para><para> + This can safely be disabled if <command>notmuch init</command> + has been used to configure this account outside of Home + Manager. + ''; + }; + }; +} diff --git a/home-manager/modules/programs/lieer.nix b/home-manager/modules/programs/lieer.nix new file mode 100644 index 00000000000..e34a247af46 --- /dev/null +++ b/home-manager/modules/programs/lieer.nix @@ -0,0 +1,93 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.lieer; + + lieerAccounts = + filter (a: a.lieer.enable) (attrValues config.accounts.email.accounts); + + nonGmailAccounts = + map (a: a.name) (filter (a: a.flavor != "gmail.com") lieerAccounts); + + nonGmailConfigHelp = + map (name: ''accounts.email.accounts.${name}.flavor = "gmail.com";'') + nonGmailAccounts; + + missingNotmuchAccounts = map (a: a.name) + (filter (a: !a.notmuch.enable && a.lieer.notmuchSetupWarning) + lieerAccounts); + + notmuchConfigHelp = + map (name: "accounts.email.accounts.${name}.notmuch.enable = true;") + missingNotmuchAccounts; + + configFile = account: { + name = "${account.maildir.absPath}/.gmailieer.json"; + value = { + text = builtins.toJSON { + inherit (account.lieer) timeout; + account = account.address; + replace_slash_with_dot = account.lieer.replaceSlashWithDot; + drop_non_existing_label = account.lieer.dropNonExistingLabels; + ignore_tags = account.lieer.ignoreTagsLocal; + ignore_remote_labels = account.lieer.ignoreTagsRemote; + } + "\n"; + }; + }; + +in { + meta.maintainers = [ maintainers.tadfisher ]; + + options = { + programs.lieer.enable = + mkEnableOption "lieer Gmail synchronization for notmuch"; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./lieer-accounts.nix)); + }; + }; + + config = mkIf cfg.enable (mkMerge [ + (mkIf (missingNotmuchAccounts != [ ]) { + warnings = ['' + lieer is enabled for the following email accounts, but notmuch is not: + + ${concatStringsSep "\n " missingNotmuchAccounts} + + Notmuch can be enabled with: + + ${concatStringsSep "\n " notmuchConfigHelp} + + If you have configured notmuch outside of Home Manager, you can suppress this + warning with: + + programs.lieer.notmuchSetupWarning = false; + '']; + }) + + { + assertions = [{ + assertion = nonGmailAccounts == [ ]; + message = '' + lieer is enabled for non-Gmail accounts: + + ${concatStringsSep "\n " nonGmailAccounts} + + If these accounts are actually Gmail accounts, you can + fix this error with: + + ${concatStringsSep "\n " nonGmailConfigHelp} + ''; + }]; + + home.packages = [ pkgs.gmailieer ]; + + # Notmuch should ignore non-mail files created by lieer. + programs.notmuch.new.ignore = [ "/.*[.](json|lock|bak)$/" ]; + + home.file = listToAttrs (map configFile lieerAccounts); + } + ]); +} diff --git a/home-manager/modules/programs/man.nix b/home-manager/modules/programs/man.nix index 0ed376780d4..b235b02fe2d 100644 --- a/home-manager/modules/programs/man.nix +++ b/home-manager/modules/programs/man.nix @@ -4,19 +4,69 @@ with lib; { options = { - programs.man.enable = mkOption { - type = types.bool; - default = true; - description = '' - Whether to enable manual pages and the <command>man</command> - command. This also includes "man" outputs of all - <literal>home.packages</literal>. - ''; + programs.man = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable manual pages and the <command>man</command> + command. This also includes "man" outputs of all + <literal>home.packages</literal>. + ''; + }; + + generateCaches = mkOption { + type = types.bool; + default = false; + description = '' + Whether to generate the manual page index caches using + <citerefentry> + <refentrytitle>mandb</refentrytitle> + <manvolnum>8</manvolnum> + </citerefentry>. This allows searching for a page or + keyword using utilities like <citerefentry> + <refentrytitle>apropos</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>. + </para><para> + This feature is disabled by default because it slows down + building. If you don't mind waiting a few more seconds when + Home Manager builds a new generation, you may safely enable + this option. + ''; + }; }; }; config = mkIf config.programs.man.enable { home.packages = [ pkgs.man ]; home.extraOutputsToInstall = [ "man" ]; + + # This is mostly copy/pasted/adapted from NixOS' documentation.nix. + home.file = mkIf config.programs.man.generateCaches { + ".manpath".text = let + # Generate a directory containing installed packages' manpages. + manualPages = pkgs.buildEnv { + name = "man-paths"; + paths = config.home.packages; + pathsToLink = [ "/share/man" ]; + extraOutputsToInstall = [ "man" ]; + ignoreCollisions = true; + }; + + # Generate a database of all manpages in ${manualPages}. + manualCache = pkgs.runCommandLocal "man-cache" { } '' + # Generate a temporary man.conf so mandb knows where to + # write cache files. + echo "MANDB_MAP ${manualPages}/share/man $out" > man.conf + + # Run mandb to generate cache files: + ${pkgs.man-db}/bin/mandb -C man.conf --no-straycats --create \ + ${manualPages}/share/man + ''; + in '' + MANDB_MAP ${config.home.profileDirectory}/share/man ${manualCache} + ''; + }; }; } diff --git a/home-manager/modules/programs/mbsync.nix b/home-manager/modules/programs/mbsync.nix index 6ade10986fe..f2814b393d0 100644 --- a/home-manager/modules/programs/mbsync.nix +++ b/home-manager/modules/programs/mbsync.nix @@ -122,6 +122,10 @@ in { ''; }; }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./mbsync-accounts.nix)); + }; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/programs/mcfly.nix b/home-manager/modules/programs/mcfly.nix new file mode 100644 index 00000000000..1206f9da566 --- /dev/null +++ b/home-manager/modules/programs/mcfly.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + + cfg = config.programs.mcfly; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.mcfly = { + enable = mkEnableOption "mcfly"; + + keyScheme = mkOption { + type = types.enum [ "emacs" "vim" ]; + default = "emacs"; + description = '' + Key scheme to use. + ''; + }; + + enableLightTheme = mkOption { + default = false; + type = types.bool; + description = '' + Whether to enable light mode theme. + ''; + }; + + enableBashIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Bash integration. + ''; + }; + + enableZshIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Zsh integration. + ''; + }; + + enableFishIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Fish integration. + ''; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ pkgs.mcfly ]; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + source "${pkgs.mcfly}/share/mcfly/mcfly.bash" + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + source "${pkgs.mcfly}/share/mcfly/mcfly.zsh" + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + source "${pkgs.mcfly}/share/mcfly/mcfly.fish" + if status is-interactive + mcfly_key_bindings + end + ''; + + home.sessionVariables.MCFLY_KEY_SCHEME = cfg.keyScheme; + } + + (mkIf cfg.enableLightTheme { home.sessionVariables.MCFLY_LIGHT = "TRUE"; }) + ]); +} diff --git a/home-manager/modules/programs/mpv.nix b/home-manager/modules/programs/mpv.nix index 3a4e5092f9a..a5b0517fe0a 100644 --- a/home-manager/modules/programs/mpv.nix +++ b/home-manager/modules/programs/mpv.nix @@ -125,7 +125,7 @@ in { (if cfg.scripts == [ ] then pkgs.mpv else - pkgs.mpv-with-scripts.override { scripts = cfg.scripts; }) + pkgs.wrapMpv pkgs.mpv-unwrapped { scripts = cfg.scripts; }) ]; } (mkIf (cfg.config != { } || cfg.profiles != { }) { diff --git a/home-manager/modules/programs/msmtp.nix b/home-manager/modules/programs/msmtp.nix index f34fd72f8b1..7b6704860e0 100644 --- a/home-manager/modules/programs/msmtp.nix +++ b/home-manager/modules/programs/msmtp.nix @@ -56,6 +56,10 @@ in { ''; }; }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./msmtp-accounts.nix)); + }; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/programs/ncmpcpp.nix b/home-manager/modules/programs/ncmpcpp.nix new file mode 100644 index 00000000000..a39baab6ca5 --- /dev/null +++ b/home-manager/modules/programs/ncmpcpp.nix @@ -0,0 +1,135 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.ncmpcpp; + + renderSettings = settings: + concatStringsSep "\n" (mapAttrsToList renderSetting settings); + + renderSetting = name: value: "${name}=${renderValue value}"; + + renderValue = option: + { + int = toString option; + bool = if option then "yes" else "no"; + string = option; + }.${builtins.typeOf option}; + + renderBindings = bindings: concatStringsSep "\n" (map renderBinding bindings); + + renderBinding = { key, command }: + concatStringsSep "\n " ([ ''def_key "${key}"'' ] ++ maybeWrapList command); + + maybeWrapList = xs: if isList xs then xs else [ xs ]; + + valueType = with types; oneOf [ bool int str ]; + + bindingType = types.submodule ({ name, config, ... }: { + options = { + key = mkOption { + type = types.str; + description = "Key to bind."; + example = "j"; + }; + + command = mkOption { + type = with types; either str (listOf str); + description = "Command or sequence of commands to be executed."; + example = "scroll_down"; + }; + }; + }); + +in { + meta.maintainers = with maintainers; [ olmokramer ]; + + options.programs.ncmpcpp = { + enable = + mkEnableOption "ncmpcpp - an ncurses Music Player Daemon (MPD) client"; + + package = mkOption { + type = types.package; + default = pkgs.ncmpcpp; + defaultText = literalExample "pkgs.ncmpcpp"; + description = '' + Package providing the <code>ncmpcpp</code> command. + ''; + example = + literalExample "pkgs.ncmpcpp.override { visualizerSupport = true; }"; + }; + + mpdMusicDir = mkOption { + type = types.nullOr types.path; + default = let mpdCfg = config.services.mpd; + in if pkgs.stdenv.hostPlatform.isLinux && mpdCfg.enable then + mpdCfg.musicDirectory + else + null; + defaultText = literalExample '' + if pkgs.stdenv.hostPlatform.isLinux && config.services.mpd.enable then + config.services.mpd.musicDirectory + else + null + ''; + description = '' + Value of the <code>mpd_music_dir</code> setting. On Linux platforms the + value of <varname>services.mpd.musicDirectory</varname> is used as the + default if <varname>services.mpd.enable</varname> is + <literal>true</literal>. + ''; + example = "~/music"; + }; + + settings = mkOption { + type = types.attrsOf valueType; + default = { }; + description = '' + Attribute set from name of a setting to its value. For available options + see + <citerefentry> + <refentrytitle>ncmpcpp</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>. + ''; + example = { ncmpcpp_directory = "~/.local/share/ncmpcpp"; }; + }; + + bindings = mkOption { + type = types.listOf bindingType; + default = [ ]; + description = "List of keybindings."; + example = literalExample '' + [ + { key = "j"; command = "scroll_down"; } + { key = "k"; command = "scroll_up"; } + { key = "J"; command = [ "select_item" "scroll_down" ]; } + { key = "K"; command = [ "select_item" "scroll_up" ]; } + ] + ''; + }; + }; + + config = mkIf cfg.enable { + warnings = mkIf (cfg.settings ? mpd_music_dir && cfg.mpdMusicDir != null) [ + ("programs.ncmpcpp.settings.mpd_music_dir will be overridden by" + + " programs.ncmpcpp.mpdMusicDir.") + ]; + + home.packages = [ cfg.package ]; + + xdg.configFile = { + "ncmpcpp/config" = let + settings = cfg.settings // optionalAttrs (cfg.mpdMusicDir != null) { + mpd_music_dir = toString cfg.mpdMusicDir; + }; + in mkIf (settings != { }) { text = renderSettings settings + "\n"; }; + + "ncmpcpp/bindings" = mkIf (cfg.bindings != [ ]) { + text = renderBindings cfg.bindings + "\n"; + }; + }; + }; +} diff --git a/home-manager/modules/programs/ne.nix b/home-manager/modules/programs/ne.nix new file mode 100644 index 00000000000..a88d23d9133 --- /dev/null +++ b/home-manager/modules/programs/ne.nix @@ -0,0 +1,95 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.ne; + + autoPrefFiles = let + autoprefs = cfg.automaticPreferences + // optionalAttrs (cfg.defaultPreferences != "") { + ".default" = cfg.defaultPreferences; + }; + + gen = fileExtension: configText: + nameValuePair ".ne/${fileExtension}#ap" { + text = configText; + }; # Generates [path].text format expected by home.file. + in mapAttrs' gen autoprefs; + +in { + meta.maintainers = [ hm.maintainers.cwyc ]; + + options.programs.ne = { + enable = mkEnableOption "ne"; + + keybindings = mkOption { + type = types.lines; + default = ""; + example = '' + KEY 7f BS + SEQ "\x1b[1;5D" 7f + ''; + description = '' + Keybinding file for ne. + ''; + }; + + defaultPreferences = mkOption { + type = types.lines; + default = ""; + description = '' + Default preferences for ne. + </para><para> + Equivalent to <literal>programs.ne.automaticPreferences.".default"</literal>. + ''; + }; + + automaticPreferences = mkOption { + type = types.attrsOf types.lines; + default = { }; + example = literalExample '' + { + nix = ''' + TAB 0 + TS 2 + '''; + js = ''' + TS 4 + '''; + } + ''; + description = '' + Automatic preferences files for ne. + ''; + }; + + menus = mkOption { + type = types.lines; + default = ""; + description = "Menu configuration file for ne."; + }; + + virtualExtensions = mkOption { + type = types.lines; + default = ""; + example = '' + sh 1 ^#!\s*/.*\b(bash|sh|ksh|zsh)\s* + csh 1 ^#!\s*/.*\b(csh|tcsh)\s* + ''; + description = "Virtual extensions configuration file for ne."; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.ne ]; + + home.file = { + ".ne/.keys" = mkIf (cfg.keybindings != "") { text = cfg.keybindings; }; + ".ne/.extensions" = + mkIf (cfg.virtualExtensions != "") { text = cfg.virtualExtensions; }; + ".ne/.menus" = mkIf (cfg.menus != "") { text = cfg.menus; }; + } // autoPrefFiles; + }; +} diff --git a/home-manager/modules/programs/neomutt-accounts.nix b/home-manager/modules/programs/neomutt-accounts.nix index 033db38eb0a..009cf1fa7e8 100644 --- a/home-manager/modules/programs/neomutt-accounts.nix +++ b/home-manager/modules/programs/neomutt-accounts.nix @@ -8,7 +8,16 @@ with lib; sendMailCommand = mkOption { type = types.nullOr types.str; - default = null; + default = if config.msmtp.enable then + "msmtpq --read-envelope-from --read-recipients" + else + null; + defaultText = literalExample '' + if config.msmtp.enable then + "msmtpq --read-envelope-from --read-recipients" + else + null + ''; example = "msmtpq --read-envelope-from --read-recipients"; description = '' Command to send a mail. If not set, neomutt will be in charge of sending mails. @@ -24,11 +33,4 @@ with lib; ''; }; }; - - config = mkIf config.neomutt.enable { - neomutt.sendMailCommand = mkOptionDefault (if config.msmtp.enable then - "msmtpq --read-envelope-from --read-recipients" - else - null); - }; } diff --git a/home-manager/modules/programs/neomutt.nix b/home-manager/modules/programs/neomutt.nix index 85af0353b6c..f2a6bbfff08 100644 --- a/home-manager/modules/programs/neomutt.nix +++ b/home-manager/modules/programs/neomutt.nix @@ -38,6 +38,19 @@ let }; }; + sortOptions = [ + "date" + "date-received" + "from" + "mailbox-order" + "score" + "size" + "spam" + "subject" + "threads" + "to" + ]; + bindModule = types.submodule { options = { map = mkOption { @@ -98,7 +111,9 @@ let } else let smtpProto = if smtp.tls.enable then "smtps" else "smtp"; - smtpBaseUrl = "${smtpProto}://${escape userName}@${smtp.host}"; + smtpPort = if smtp.port != null then ":${toString smtp.port}" else ""; + smtpBaseUrl = + "${smtpProto}://${escape userName}@${smtp.host}${smtpPort}"; in { smtp_url = "'${smtpBaseUrl}'"; smtp_pass = "'`${passCmd}`'"; @@ -211,18 +226,9 @@ in { }; sort = mkOption { - type = types.enum [ - "date" - "date-received" - "from" - "mailbox-order" - "score" - "size" - "spam" - "subject" - "threads" - "to" - ]; + # allow users to choose any option from sortOptions, or any option prefixed with "reverse-" + type = types.enum + (sortOptions ++ (map (option: "reverse-" + option) sortOptions)); default = "threads"; description = "Sorting method on messages."; }; @@ -258,6 +264,10 @@ in { description = "Extra configuration appended to the end."; }; }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./neomutt-accounts.nix)); + }; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/programs/neovim.nix b/home-manager/modules/programs/neovim.nix index 4101dc0f4e7..858f5576ad1 100644 --- a/home-manager/modules/programs/neovim.nix +++ b/home-manager/modules/programs/neovim.nix @@ -43,7 +43,7 @@ in type = types.bool; default = false; description = '' - Symlink `vi` to `nvim` binary. + Symlink <command>vi</command> to <command>nvim</command> binary. ''; }; @@ -51,7 +51,15 @@ in type = types.bool; default = false; description = '' - Symlink `vim` to `nvim` binary. + Symlink <command>vim</command> to <command>nvim</command> binary. + ''; + }; + + vimdiffAlias = mkOption { + type = types.bool; + default = false; + description = '' + Alias <command>vimdiff</command> to <command>nvim -d</command>. ''; }; @@ -203,5 +211,9 @@ in configure = cfg.configure // moduleConfigure; }; + + programs.bash.shellAliases = mkIf cfg.vimdiffAlias { vimdiff = "nvim -d"; }; + programs.fish.shellAliases = mkIf cfg.vimdiffAlias { vimdiff = "nvim -d"; }; + programs.zsh.shellAliases = mkIf cfg.vimdiffAlias { vimdiff = "nvim -d"; }; }; } diff --git a/home-manager/modules/programs/newsboat.nix b/home-manager/modules/programs/newsboat.nix index 6b59ed713d8..793b30680bf 100644 --- a/home-manager/modules/programs/newsboat.nix +++ b/home-manager/modules/programs/newsboat.nix @@ -102,7 +102,11 @@ in { mkQueryEntry = n: v: ''"query:${n}:${escape [ ''"'' ] v}"''; queries = mapAttrsToList mkQueryEntry cfg.queries; - in concatStringsSep "\n" (urls ++ queries) + "\n"; + in concatStringsSep "\n" + (if versionAtLeast config.home.stateVersion "20.03" then + queries ++ urls + else + urls ++ queries) + "\n"; home.file.".newsboat/config".text = '' max-items ${toString cfg.maxItems} diff --git a/home-manager/modules/programs/notmuch.nix b/home-manager/modules/programs/notmuch.nix index 7e7a140b20c..9070d755671 100644 --- a/home-manager/modules/programs/notmuch.nix +++ b/home-manager/modules/programs/notmuch.nix @@ -143,6 +143,10 @@ in { }; }; }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./notmuch-accounts.nix)); + }; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/programs/nushell.nix b/home-manager/modules/programs/nushell.nix new file mode 100644 index 00000000000..1eb42f9515c --- /dev/null +++ b/home-manager/modules/programs/nushell.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.nushell; + + configFile = config: + pkgs.runCommand "config.toml" { + buildInputs = [ pkgs.remarshal ]; + preferLocalBuild = true; + allowSubstitutes = false; + } '' + remarshal -if json -of toml \ + < ${pkgs.writeText "config.json" (builtins.toJSON config)} \ + > $out + ''; + +in { + meta.maintainers = [ maintainers.Philipp-M ]; + + options.programs.nushell = { + enable = mkEnableOption "nushell"; + + package = mkOption { + type = types.package; + default = pkgs.nushell; + defaultText = literalExample "pkgs.nushell"; + description = "The package to use for nushell."; + }; + + settings = mkOption { + type = with types; + let + prim = oneOf [ bool int str ]; + primOrPrimAttrs = either prim (attrsOf prim); + entry = either prim (listOf primOrPrimAttrs); + entryOrAttrsOf = t: either entry (attrsOf t); + entries = entryOrAttrsOf (entryOrAttrsOf entry); + in attrsOf entries // { description = "Nushell configuration"; }; + default = { }; + example = literalExample '' + { + edit_mode = "vi"; + startup = [ "alias la [] { ls -a }" "alias e [msg] { echo $msg }" ]; + key_timeout = 10; + completion_mode = "circular"; + no_auto_pivot = true; + } + ''; + description = '' + Configuration written to + <filename>~/.config/nushell/config.toml</filename>. + </para><para> + See <link xlink:href="https://www.nushell.sh/book/en/configuration.html" /> for the full list + of options. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xdg.configFile."nu/config.toml" = + mkIf (cfg.settings != { }) { source = configFile cfg.settings; }; + }; +} diff --git a/home-manager/modules/programs/offlineimap.nix b/home-manager/modules/programs/offlineimap.nix index 4ce12ec0a61..b6ba847e9b7 100644 --- a/home-manager/modules/programs/offlineimap.nix +++ b/home-manager/modules/programs/offlineimap.nix @@ -147,6 +147,11 @@ in { ''; }; }; + + accounts.email.accounts = mkOption { + type = with types; + attrsOf (submodule (import ./offlineimap-accounts.nix)); + }; }; config = mkIf cfg.enable { diff --git a/home-manager/modules/programs/powerline-go.nix b/home-manager/modules/programs/powerline-go.nix new file mode 100644 index 00000000000..a4cd233cf70 --- /dev/null +++ b/home-manager/modules/programs/powerline-go.nix @@ -0,0 +1,123 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.powerline-go; + + # Convert an option value to a string to be passed as argument to + # powerline-go: + valueToString = value: + if builtins.isList value then + builtins.concatStringsSep "," (builtins.map valueToString value) + else if builtins.isAttrs value then + valueToString + (mapAttrsToList (key: val: "${valueToString key}=${valueToString val}") + value) + else + builtins.toString value; + + modulesArgument = optionalString (cfg.modules != null) + "-modules ${valueToString cfg.modules}"; + + newlineArgument = optionalString cfg.newline "-newline"; + + pathAliasesArgument = optionalString (cfg.pathAliases != null) + "-path-aliases ${valueToString cfg.pathAliases}"; + + otherSettingPairArgument = name: value: + if value == true then "-${name}" else "-${name} ${valueToString value}"; + + otherSettingsArgument = optionalString (cfg.settings != { }) + (concatStringsSep " " + (mapAttrsToList otherSettingPairArgument cfg.settings)); + + commandLineArguments = '' + ${modulesArgument} ${newlineArgument} ${pathAliasesArgument} ${otherSettingsArgument} + ''; + +in { + meta.maintainers = [ maintainers.DamienCassou ]; + + options = { + programs.powerline-go = { + enable = mkEnableOption + "Powerline-go, a beautiful and useful low-latency prompt for your shell"; + + modules = mkOption { + default = null; + type = types.nullOr (types.listOf types.str); + description = '' + List of module names to load. The list of all available + modules as well as the choice of default ones are at + <link xlink:href="https://github.com/justjanne/powerline-go"/>. + ''; + example = [ "host" "ssh" "cwd" "gitlite" "jobs" "exit" ]; + }; + + newline = mkOption { + default = false; + type = types.bool; + description = '' + Set to true if the prompt should be on a line of its own. + ''; + example = true; + }; + + pathAliases = mkOption { + default = null; + type = types.nullOr (types.attrsOf types.str); + description = '' + Pairs of full-path and corresponding desired short name. You + may use '~' to represent your home directory but you should + protect it to avoid shell substitution. + ''; + example = literalExample '' + { "\\~/projects/home-manager" = "prj:home-manager"; } + ''; + }; + + settings = mkOption { + default = { }; + type = with types; attrsOf (oneOf [ bool int str (listOf str) ]); + description = '' + This can be any key/value pair as described in + <link xlink:href="https://github.com/justjanne/powerline-go"/>. + ''; + example = literalExample '' + { + hostname-only-if-ssh = true; + numeric-exit-codes = true; + cwd-max-depth = 7; + ignore-repos = [ "/home/me/big-project" "/home/me/huge-project" ]; + } + ''; + }; + + extraUpdatePS1 = mkOption { + default = ""; + description = "Shell code to execute after the prompt is set."; + example = '' + PS1=$PS1"NixOS> "; + ''; + type = types.str; + }; + }; + }; + + config = mkIf (cfg.enable && config.programs.bash.enable) { + programs.bash.initExtra = '' + function _update_ps1() { + local old_exit_status=$? + PS1="$(${pkgs.powerline-go}/bin/powerline-go -error $old_exit_status ${commandLineArguments})" + ${cfg.extraUpdatePS1} + return $old_exit_status + } + + if [ "$TERM" != "linux" ]; then + PROMPT_COMMAND="_update_ps1;$PROMPT_COMMAND" + fi + ''; + }; +} diff --git a/home-manager/modules/programs/qutebrowser.nix b/home-manager/modules/programs/qutebrowser.nix new file mode 100644 index 00000000000..798363fb187 --- /dev/null +++ b/home-manager/modules/programs/qutebrowser.nix @@ -0,0 +1,268 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.qutebrowser; + + formatLine = o: n: v: + let + formatValue = v: + if builtins.isNull v then + "None" + else if builtins.isBool v then + (if v then "True" else "False") + else if builtins.isString v then + ''"${v}"'' + else if builtins.isList v then + "[${concatStringsSep ", " (map formatValue v)}]" + else + builtins.toString v; + in if builtins.isAttrs v then + concatStringsSep "\n" (mapAttrsToList (formatLine "${o}${n}.") v) + else + "${o}${n} = ${formatValue v}"; + + formatDictLine = o: n: v: ''${o}['${n}'] = "${v}"''; + + formatKeyBindings = m: b: + let + formatKeyBinding = m: k: c: + ''config.bind("${k}", "${escape [ ''"'' ] c}", mode="${m}")''; + in concatStringsSep "\n" (mapAttrsToList (formatKeyBinding m) b); + +in { + options.programs.qutebrowser = { + enable = mkEnableOption "qutebrowser"; + + package = mkOption { + type = types.package; + default = pkgs.qutebrowser; + defaultText = literalExample "pkgs.qutebrowser"; + description = "Qutebrowser package to install."; + }; + + aliases = mkOption { + type = types.attrsOf types.str; + default = { }; + description = '' + Aliases for commands. + ''; + }; + + searchEngines = mkOption { + type = types.attrsOf types.str; + default = { }; + description = '' + Search engines that can be used via the address bar. Maps a search + engine name (such as <literal>DEFAULT</literal>, or + <literal>ddg</literal>) to a URL with a <literal>{}</literal> + placeholder. The placeholder will be replaced by the search term, use + <literal>{{</literal> and <literal>}}</literal> for literal + <literal>{/}</literal> signs. The search engine named + <literal>DEFAULT</literal> is used when + <literal>url.auto_search</literal> is turned on and something else than + a URL was entered to be opened. Other search engines can be used by + prepending the search engine name to the search term, for example + <literal>:open google qutebrowser</literal>. + ''; + example = literalExample '' + { + w = "https://en.wikipedia.org/wiki/Special:Search?search={}&go=Go&ns0=1"; + aw = "https://wiki.archlinux.org/?search={}"; + nw = "https://nixos.wiki/index.php?search={}"; + g = "https://www.google.com/search?hl=en&q={}"; + } + ''; + }; + + settings = mkOption { + type = types.attrs; + default = { }; + description = '' + Options to add to qutebrowser <filename>config.py</filename> file. + See <link xlink:href="https://qutebrowser.org/doc/help/settings.html"/> + for options. + ''; + example = literalExample '' + { + colors = { + hints = { + bg = "#000000"; + fg = "#ffffff"; + }; + tabs.bar.bg = "#000000"; + }; + tabs.tabs_are_windows = true; + } + ''; + }; + + keyMappings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = '' + This setting can be used to map keys to other keys. When the key used + as dictionary-key is pressed, the binding for the key used as + dictionary-value is invoked instead. This is useful for global + remappings of keys, for example to map Ctrl-[ to Escape. Note that when + a key is bound (via <literal>bindings.default</literal> or + <literal>bindings.commands</literal>), the mapping is ignored. + ''; + }; + + enableDefaultBindings = mkOption { + type = types.bool; + default = true; + description = '' + Disable to prevent loading default key bindings. + ''; + }; + + keyBindings = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { }; + description = '' + Key bindings mapping keys to commands in different modes. This setting + is a dictionary containing mode names and dictionaries mapping keys to + commands: <literal>{mode: {key: command}}</literal> If you want to map + a key to another key, check the <literal>keyMappings</literal> setting + instead. For modifiers, you can use either <literal>-</literal> or + <literal>+</literal> as delimiters, and these names: + + <itemizedlist> + <listitem><para> + Control: <literal>Control</literal>, <literal>Ctrl</literal> + </para></listitem> + <listitem><para> + Meta: <literal>Meta</literal>, <literal>Windows</literal>, + <literal>Mod4</literal> + </para></listitem> + <listitem><para> + Alt: <literal>Alt</literal>, <literal>Mod1</literal> + </para></listitem> + <listitem><para> + Shift: <literal>Shift</literal> + </para></listitem> + </itemizedlist> + + For simple keys (no <literal><></literal>-signs), a capital + letter means the key is pressed with Shift. For special keys (with + <literal><></literal>-signs), you need to explicitly add + <literal>Shift-</literal> to match a key pressed with shift. If you + want a binding to do nothing, bind it to the <literal>nop</literal> + command. If you want a default binding to be passed through to the + website, bind it to null. Note that some commands which are only useful + for bindings (but not used interactively) are hidden from the command + completion. See <literal>:</literal>help for a full list of available + commands. The following modes are available: + + <variablelist> + <varlistentry> + <term><literal>normal</literal></term> + <listitem><para> + Default mode, where most commands are invoked. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>insert</literal></term> + <listitem><para> + Entered when an input field is focused on a website, or by + pressing i in normal mode. Passes through almost all keypresses + to the website, but has some bindings like + <literal><Ctrl-e></literal> to open an external editor. + Note that single keys canβt be bound in this mode. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>hint</literal></term> + <listitem><para> + Entered when f is pressed to select links with the keyboard. Note + that single keys canβt be bound in this mode. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>passthrough</literal></term> + <listitem><para> + Similar to insert mode, but passes through all keypresses except + <literal><Escape></literal> to leave the mode. It might be + useful to bind <literal><Escape></literal> to some other + key in this mode if you want to be able to send an Escape key to + the website as well. Note that single keys canβt be bound in this + mode. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>command</literal></term> + <listitem><para> + Entered when pressing the : key in order to enter a command. Note + that single keys canβt be bound in this mode. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>prompt</literal></term> + <listitem><para> + Entered when thereβs a prompt to display, like for download + locations or when invoked from JavaScript. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>yesno</literal></term> + <listitem><para> + Entered when thereβs a yes/no prompt displayed. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>caret</literal></term> + <listitem><para> + Entered when pressing the v mode, used to select text using the + keyboard. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>register</literal></term> + <listitem><para> + Entered when qutebrowser is waiting for a register name/key for + commands like <literal>:set-mark</literal>. + </para></listitem> + </varlistentry> + </variablelist> + ''; + example = literalExample '' + { + normal = { + "<Ctrl-v>" = "spawn mpv {url}"; + ",p" = "spawn --userscript qute-pass"; + ",l" = '''config-cycle spellcheck.languages ["en-GB"] ["en-US"]'''; + }; + prompt = { + "<Ctrl-y>" = "prompt-yes"; + }; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines added to qutebrowser <filename>config.py</filename> file. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xdg.configFile."qutebrowser/config.py".text = concatStringsSep "\n" ([ ] + ++ mapAttrsToList (formatLine "c.") cfg.settings + ++ mapAttrsToList (formatDictLine "c.aliases") cfg.aliases + ++ mapAttrsToList (formatDictLine "c.url.searchengines") cfg.searchEngines + ++ mapAttrsToList (formatDictLine "c.bindings.key_mappings") + cfg.keyMappings + ++ optional (!cfg.enableDefaultBindings) "c.bindings.default = {}" + ++ mapAttrsToList formatKeyBindings cfg.keyBindings + ++ optional (cfg.extraConfig != "") cfg.extraConfig); + }; +} diff --git a/home-manager/modules/programs/rofi.nix b/home-manager/modules/programs/rofi.nix index f344e88e2ff..734bcc423e6 100644 --- a/home-manager/modules/programs/rofi.nix +++ b/home-manager/modules/programs/rofi.nix @@ -131,6 +131,17 @@ in { enable = mkEnableOption "Rofi: A window switcher, application launcher and dmenu replacement"; + package = mkOption { + default = pkgs.rofi; + type = types.package; + description = '' + Package providing the <command>rofi</command> binary. + ''; + example = literalExample '' + pkgs.rofi.override { plugins = [ pkgs.rofi-emoji ]; }; + ''; + }; + width = mkOption { default = null; type = types.nullOr types.int; @@ -295,7 +306,7 @@ in { ''; }]; - home.packages = [ pkgs.rofi ]; + home.packages = [ cfg.package ]; home.file."${cfg.configPath}".text = '' ${setOption "width" cfg.width} diff --git a/home-manager/modules/programs/ssh.nix b/home-manager/modules/programs/ssh.nix index 6b0747dd9b1..ae1f221803c 100644 --- a/home-manager/modules/programs/ssh.nix +++ b/home-manager/modules/programs/ssh.nix @@ -56,7 +56,7 @@ let }; }; - matchBlockModule = types.submodule ({ name, ... }: { + matchBlockModule = types.submodule ({ dagName, ... }: { options = { host = mkOption { type = types.str; @@ -143,6 +143,15 @@ let "Set timeout in seconds after which response will be requested."; }; + serverAliveCountMax = mkOption { + type = types.ints.positive; + default = 3; + description = '' + Sets the number of server alive messages which may be sent + without SSH receiving any messages back from the server. + ''; + }; + sendEnv = mkOption { type = types.listOf types.str; default = []; @@ -266,7 +275,7 @@ let }; }; - config.host = mkDefault name; + config.host = mkDefault dagName; }); matchBlockStr = cf: concatStringsSep "\n" ( @@ -281,7 +290,9 @@ let ++ optional (cf.addressFamily != null) " AddressFamily ${cf.addressFamily}" ++ optional (cf.sendEnv != []) " SendEnv ${unwords cf.sendEnv}" ++ optional (cf.serverAliveInterval != 0) - " ServerAliveInterval ${toString cf.serverAliveInterval}" + " ServerAliveInterval ${toString cf.serverAliveInterval}" + ++ optional (cf.serverAliveCountMax != 3) + " ServerAliveCountMax ${toString cf.serverAliveCountMax}" ++ optional (cf.compression != null) " Compression ${yn cf.compression}" ++ optional (!cf.checkHostIP) " CheckHostIP no" ++ optional (cf.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}" @@ -325,6 +336,15 @@ in ''; }; + serverAliveCountMax = mkOption { + type = types.ints.positive; + default = 3; + description = '' + Sets the default number of server alive messages which may be + sent without SSH receiving any messages back from the server. + ''; + }; + hashKnownHosts = mkOption { default = false; type = types.bool; @@ -392,7 +412,7 @@ in }; matchBlocks = mkOption { - type = types.loaOf matchBlockModule; + type = hm.types.listOrDagOf matchBlockModule; default = {}; example = literalExample '' { @@ -400,7 +420,7 @@ in hostname = "example.com"; user = "john"; }; - foo = { + foo = lib.hm.dag.entryBefore ["john.example.com"] { hostname = "example.com"; identityFile = "/home/john/.ssh/foo_rsa"; }; @@ -408,11 +428,15 @@ in ''; description = '' Specify per-host settings. Note, if the order of rules matter - then this must be a list. See + then use the DAG functions to express the dependencies as + shown in the example. + </para><para> + See <citerefentry> <refentrytitle>ssh_config</refentrytitle> <manvolnum>5</manvolnum> - </citerefentry>. + </citerefentry> + for more information. ''; }; }; @@ -432,23 +456,30 @@ in checkLocal = block: any' checkBindAndHost block.localForwards; checkRemote = block: any' checkBindAndHost block.remoteForwards; checkMatchBlock = block: all (fn: fn block) [ checkLocal checkRemote checkDynamic ]; - in any' checkMatchBlock (builtins.attrValues cfg.matchBlocks); + in any' checkMatchBlock (map (block: block.data) (builtins.attrValues cfg.matchBlocks)); message = "Forwarded paths cannot have ports."; } ]; - home.file.".ssh/config".text = '' + home.file.".ssh/config".text = + let + sortedMatchBlocks = hm.dag.topoSort cfg.matchBlocks; + sortedMatchBlocksStr = builtins.toJSON sortedMatchBlocks; + matchBlocks = + if sortedMatchBlocks ? result + then sortedMatchBlocks.result + else abort "Dependency cycle in SSH match blocks: ${sortedMatchBlocksStr}"; + in '' ${concatStringsSep "\n" ( mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)} - ${concatStringsSep "\n\n" ( - map matchBlockStr ( - builtins.attrValues cfg.matchBlocks))} + ${concatStringsSep "\n\n" (map (block: matchBlockStr block.data) matchBlocks)} Host * ForwardAgent ${yn cfg.forwardAgent} Compression ${yn cfg.compression} ServerAliveInterval ${toString cfg.serverAliveInterval} + ServerAliveCountMax ${toString cfg.serverAliveCountMax} HashKnownHosts ${yn cfg.hashKnownHosts} UserKnownHostsFile ${cfg.userKnownHostsFile} ControlMaster ${cfg.controlMaster} diff --git a/home-manager/modules/programs/starship.nix b/home-manager/modules/programs/starship.nix index 7c7819865f7..8462d331501 100644 --- a/home-manager/modules/programs/starship.nix +++ b/home-manager/modules/programs/starship.nix @@ -31,8 +31,23 @@ in { }; settings = mkOption { - type = types.attrs; + type = with types; + let + prim = either bool (either int str); + primOrPrimAttrs = either prim (attrsOf prim); + entry = either prim (listOf primOrPrimAttrs); + entryOrAttrsOf = t: either entry (attrsOf t); + entries = entryOrAttrsOf (entryOrAttrsOf entry); + in attrsOf entries // { description = "Starship configuration"; }; default = { }; + example = literalExample '' + { + add_newline = false; + prompt_order = [ "line_break" "package" "line_break" "character" ]; + scan_timeout = 10; + character.symbol = "β"; + } + ''; description = '' Configuration written to <filename>~/.config/starship.toml</filename>. @@ -74,7 +89,7 @@ in { mkIf (cfg.settings != { }) { source = configFile cfg.settings; }; programs.bash.initExtra = mkIf cfg.enableBashIntegration '' - if [[ -z $INSIDE_EMACS ]]; then + if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then eval "$(${cfg.package}/bin/starship init bash)" fi ''; @@ -85,8 +100,8 @@ in { fi ''; - programs.fish.shellInit = mkIf cfg.enableFishIntegration '' - if test -z "$INSIDE_EMACS" + programs.fish.promptInit = mkIf cfg.enableFishIntegration '' + if test "$TERM" != "dumb" -a \( -z "$INSIDE_EMACS" -o "$INSIDE_EMACS" = "vterm" \) eval (${cfg.package}/bin/starship init fish) end ''; diff --git a/home-manager/modules/programs/termite.nix b/home-manager/modules/programs/termite.nix index 8a05db03558..e3d704424e8 100644 --- a/home-manager/modules/programs/termite.nix +++ b/home-manager/modules/programs/termite.nix @@ -362,7 +362,7 @@ in { ${optionalString "cursor" cfg.cursorColor} ${optionalString "cursor_foreground" cfg.cursorForegroundColor} ${optionalString "foreground" cfg.foregroundColor} - ${optionalString "foregroundBold" cfg.foregroundBoldColor} + ${optionalString "foreground_bold" cfg.foregroundBoldColor} ${optionalString "highlight" cfg.highlightColor} ${cfg.colorsExtra} diff --git a/home-manager/modules/programs/tmux.nix b/home-manager/modules/programs/tmux.nix index 766bc6238ba..a71c302ac6f 100644 --- a/home-manager/modules/programs/tmux.nix +++ b/home-manager/modules/programs/tmux.nix @@ -6,7 +6,7 @@ let cfg = config.programs.tmux; - pluginName = p: if types.package.check p then p.name else p.plugin.name; + pluginName = p: if types.package.check p then p.pname else p.plugin.pname; pluginModule = types.submodule { options = { @@ -31,6 +31,13 @@ let boolToStr = value: if value then "on" else "off"; tmuxConf = '' + ${optionalString cfg.sensibleOnTop '' + # ============================================= # + # Start with defaults from the Sensible plugin # + # --------------------------------------------- # + run-shell ${pkgs.tmuxPlugins.sensible.rtp} + # ============================================= # + ''} set -g default-terminal "${cfg.terminal}" set -g base-index ${toString cfg.baseIndex} setw -g pane-base-index ${toString cfg.baseIndex} @@ -74,10 +81,40 @@ let setw -g clock-mode-style ${if cfg.clock24 then "24" else "12"} set -s escape-time ${toString cfg.escapeTime} set -g history-limit ${toString cfg.historyLimit} - - ${cfg.extraConfig} ''; + configPlugins = { + assertions = [( + let + hasBadPluginName = p: !(hasPrefix "tmuxplugin" (pluginName p)); + badPlugins = filter hasBadPluginName cfg.plugins; + in + { + assertion = badPlugins == []; + message = + "Invalid tmux plugin (not prefixed with \"tmuxplugins\"): " + + concatMapStringsSep ", " pluginName badPlugins; + } + )]; + + home.file.".tmux.conf".text = '' + # ============================================= # + # Load plugins with Home Manager # + # --------------------------------------------- # + + ${(concatMapStringsSep "\n\n" (p: '' + # ${pluginName p} + # --------------------- + ${p.extraConfig or ""} + run-shell ${ + if types.package.check p + then p.rtp + else p.plugin.rtp + } + '') cfg.plugins)} + # ============================================= # + ''; + }; in { @@ -214,7 +251,7 @@ in }; secureSocket = mkOption { - default = true; + default = pkgs.stdenv.isLinux; type = types.bool; description = '' Store tmux socket under <filename>/run</filename>, which is more @@ -258,63 +295,22 @@ in }; config = mkIf cfg.enable ( - mkMerge [ + mkMerge ([ { home.packages = [ cfg.package ] ++ optional cfg.tmuxinator.enable pkgs.tmuxinator ++ optional cfg.tmuxp.enable pkgs.tmuxp; - - home.file.".tmux.conf".text = tmuxConf; } - - (mkIf cfg.sensibleOnTop { - home.file.".tmux.conf".text = mkBefore '' - # ============================================= # - # Start with defaults from the Sensible plugin # - # --------------------------------------------- # - run-shell ${pkgs.tmuxPlugins.sensible.rtp} - # ============================================= # - ''; - }) - (mkIf cfg.secureSocket { home.sessionVariables = { TMUX_TMPDIR = ''''${XDG_RUNTIME_DIR:-"/run/user/\$(id -u)"}''; }; }) - (mkIf (cfg.plugins != []) { - assertions = [( - let - hasBadPluginName = p: !(hasPrefix "tmuxplugin" (pluginName p)); - badPlugins = filter hasBadPluginName cfg.plugins; - in - { - assertion = badPlugins == []; - message = - "Invalid tmux plugin (not prefixed with \"tmuxplugins\"): " - + concatMapStringsSep ", " pluginName badPlugins; - } - )]; - - home.file.".tmux.conf".text = mkAfter '' - # ============================================= # - # Load plugins with Home Manager # - # --------------------------------------------- # - - ${(concatMapStringsSep "\n\n" (p: '' - # ${pluginName p} - # --------------------- - ${p.extraConfig or ""} - run-shell ${ - if types.package.check p - then p.rtp - else p.plugin.rtp - } - '') cfg.plugins)} - # ============================================= # - ''; - }) - ] + # config file ~/.tmux.conf + { home.file.".tmux.conf".text = mkBefore tmuxConf; } + (mkIf (cfg.plugins != []) configPlugins) + { home.file.".tmux.conf".text = mkAfter cfg.extraConfig; } + ]) ); } diff --git a/home-manager/modules/programs/vim.nix b/home-manager/modules/programs/vim.nix index 39826a9a5d6..3325bf22516 100644 --- a/home-manager/modules/programs/vim.nix +++ b/home-manager/modules/programs/vim.nix @@ -5,7 +5,7 @@ with lib; let cfg = config.programs.vim; - defaultPlugins = [ pkgs.vimPlugins.sensible ]; + defaultPlugins = [ pkgs.vimPlugins.vim-sensible ]; knownSettings = { background = types.enum [ "dark" "light" ]; diff --git a/home-manager/modules/programs/vscode.nix b/home-manager/modules/programs/vscode.nix index cf7ac722210..099760c834a 100644 --- a/home-manager/modules/programs/vscode.nix +++ b/home-manager/modules/programs/vscode.nix @@ -20,11 +20,14 @@ let "vscodium" = "vscode-oss"; }.${vscodePname}; - configFilePath = + userDir = if pkgs.stdenv.hostPlatform.isDarwin then - "Library/Application Support/${configDir}/User/settings.json" + "Library/Application Support/${configDir}/User" else - "${config.xdg.configHome}/${configDir}/User/settings.json"; + "${config.xdg.configHome}/${configDir}/User"; + + configFilePath = "${userDir}/settings.json"; + keybindingsFilePath = "${userDir}/keybindings.json"; # TODO: On Darwin where are the extensions? extensionPath = ".${extensionDir}/extensions"; @@ -59,6 +62,45 @@ in ''; }; + keybindings = mkOption { + type = types.listOf (types.submodule { + options = { + key = mkOption { + type = types.str; + example = "ctrl+c"; + description = "The key or key-combination to bind."; + }; + + command = mkOption { + type = types.str; + example = "editor.action.clipboardCopyAction"; + description = "The VS Code command to execute."; + }; + + when = mkOption { + type = types.str; + default = ""; + example = "textInputFocus"; + description = "Optional context filter."; + }; + }; + }); + default = []; + example = literalExample '' + [ + { + key = "ctrl+c"; + command = "editor.action.clipboardCopyAction"; + when = "textInputFocus"; + } + ] + ''; + description = '' + Keybindings written to Visual Studio Code's + <filename>keybindings.json</filename>. + ''; + }; + extensions = mkOption { type = types.listOf types.package; default = []; @@ -77,15 +119,13 @@ in # Adapted from https://discourse.nixos.org/t/vscode-extensions-setup/1801/2 home.file = let + subDir = "share/vscode/extensions"; toPaths = path: - let - p = "${path}/share/vscode/extensions"; - in - # Links every dir in p to the extension path. - mapAttrsToList (k: v: - { - "${extensionPath}/${k}".source = "${p}/${k}"; - }) (builtins.readDir p); + # Links every dir in path to the extension path. + mapAttrsToList (k: _: + { + "${extensionPath}/${k}".source = "${path}/${subDir}/${k}"; + }) (builtins.readDir (path + "/${subDir}")); toSymlink = concatMap toPaths cfg.extensions; in foldr @@ -95,6 +135,10 @@ in mkIf (cfg.userSettings != {}) { text = builtins.toJSON cfg.userSettings; }; + "${keybindingsFilePath}" = + mkIf (cfg.keybindings != []) { + text = builtins.toJSON cfg.keybindings; + }; } toSymlink; }; diff --git a/home-manager/modules/programs/waybar.nix b/home-manager/modules/programs/waybar.nix new file mode 100644 index 00000000000..369f6e32aba --- /dev/null +++ b/home-manager/modules/programs/waybar.nix @@ -0,0 +1,363 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.programs.waybar; + + # Used when generating warnings + modulesPath = "programs.waybar.settings.[].modules"; + + # Taken from <https://github.com/Alexays/Waybar/blob/adaf84304865e143e4e83984aaea6f6a7c9d4d96/src/factory.cpp> + defaultModuleNames = [ + "sway/mode" + "sway/workspaces" + "sway/window" + "wlr/taskbar" + "idle_inhibitor" + "memory" + "cpu" + "clock" + "disk" + "tray" + "network" + "backlight" + "pulseaudio" + "mpd" + "temperature" + "bluetooth" + "battery" + ]; + + isValidCustomModuleName = x: + elem x defaultModuleNames || (hasPrefix "custom/" x && stringLength x > 7); + + margins = let + mkMargin = name: { + "margin-${name}" = mkOption { + type = types.nullOr types.int; + default = null; + example = 10; + description = "Margins value without unit."; + }; + }; + margins = map mkMargin [ "top" "left" "bottom" "right" ]; + in foldl' mergeAttrs { } margins; + + waybarBarConfig = with lib.types; + submodule { + options = { + layer = mkOption { + type = nullOr (enum [ "top" "bottom" ]); + default = null; + description = '' + Decide if the bar is displayed in front (<code>"top"</code>) + of the windows or behind (<code>"bottom"</code>). + ''; + example = "top"; + }; + + output = mkOption { + type = nullOr (either str (listOf str)); + default = null; + example = literalExample '' + [ "DP-1" "!DP-2" "!DP-3" ] + ''; + description = '' + Specifies on which screen this bar will be displayed. + Exclamation mark(!) can be used to exclude specific output. + ''; + }; + + position = mkOption { + type = nullOr (enum [ "top" "bottom" "left" "right" ]); + default = null; + example = "right"; + description = "Bar position relative to the output."; + }; + + height = mkOption { + type = nullOr ints.unsigned; + default = null; + example = 5; + description = + "Height to be used by the bar if possible. Leave blank for a dynamic value."; + }; + + width = mkOption { + type = nullOr ints.unsigned; + default = null; + example = 5; + description = + "Width to be used by the bar if possible. Leave blank for a dynamic value."; + }; + + modules-left = mkOption { + type = nullOr (listOf str); + default = null; + description = "Modules that will be displayed on the left."; + example = literalExample '' + [ "sway/workspaces" "sway/mode" "wlr/taskbar" ] + ''; + }; + + modules-center = mkOption { + type = nullOr (listOf str); + default = null; + description = "Modules that will be displayed in the center."; + example = literalExample '' + [ "sway/window" ] + ''; + }; + + modules-right = mkOption { + type = nullOr (listOf str); + default = null; + description = "Modules that will be displayed on the right."; + example = literalExample '' + [ "mpd" "custom/mymodule#with-css-id" "temperature" ] + ''; + }; + + modules = mkOption { + type = attrsOf unspecified; + default = { }; + description = "Modules configuration."; + example = literalExample '' + { + "sway/window" = { + max-length = 50; + }; + "clock" = { + format-alt = "{:%a, %d. %b %H:%M}"; + }; + } + ''; + }; + + margin = mkOption { + type = nullOr str; + default = null; + description = "Margins value using the CSS format without units."; + example = "20 5"; + }; + + inherit (margins) margin-top margin-left margin-bottom margin-right; + + name = mkOption { + type = nullOr str; + default = null; + description = + "Optional name added as a CSS class, for styling multiple waybars."; + example = "waybar-1"; + }; + + gtk-layer-shell = mkOption { + type = nullOr bool; + default = null; + example = false; + description = + "Option to disable the use of gtk-layer-shell for popups."; + }; + }; + }; +in { + meta.maintainers = [ hm.maintainers.berbiche ]; + + options.programs.waybar = with lib.types; { + enable = mkEnableOption "Waybar"; + + package = mkOption { + type = package; + default = pkgs.waybar; + defaultText = literalExample "${pkgs.waybar}"; + description = '' + Waybar package to use. Set to <code>null</code> to use the default module. + ''; + }; + + settings = mkOption { + type = listOf waybarBarConfig; + default = [ ]; + description = '' + Configuration for Waybar, see <link + xlink:href="https://github.com/Alexays/Waybar/wiki/Configuration"/> + for supported values. + ''; + example = literalExample '' + [ + { + layer = "top"; + position = "top"; + height = 30; + output = [ + "eDP-1" + "HDMI-A-1" + ]; + modules-left = [ "sway/workspaces" "sway/mode" "wlr/taskbar" ]; + modules-center = [ "sway/window" "custom/hello-from-waybar" ]; + modules-right = [ "mpd" "custom/mymodule#with-css-id" "temperature" ]; + modules = { + "sway/workspaces" = { + disable-scroll = true; + all-outputs = true; + }; + "custom/hello-from-waybar" = { + format = "hello {}"; + max-length = 40; + interval = "once"; + exec = pkgs.writeShellScript "hello-from-waybar" ''' + echo "from within waybar" + '''; + }; + }; + } + ] + ''; + }; + + systemd.enable = mkEnableOption "Waybar systemd integration"; + + style = mkOption { + type = nullOr str; + default = null; + description = '' + CSS style of the bar. + See <link xlink:href="https://github.com/Alexays/Waybar/wiki/Configuration"/> + for the documentation. + ''; + example = '' + * { + border: none; + border-radius: 0; + font-family: Source Code Pro; + } + window#waybar { + background: #16191C; + color: #AAB2BF; + } + #workspaces button { + padding: 0 5px; + } + ''; + }; + }; + + config = let + # Inspired by https://github.com/NixOS/nixpkgs/pull/89781 + writePrettyJSON = name: x: + pkgs.runCommandLocal name { } '' + ${pkgs.jq}/bin/jq . > $out <<<${escapeShellArg (builtins.toJSON x)} + ''; + + configSource = let + # Removes nulls because Waybar ignores them for most values + removeNulls = filterAttrs (_: v: v != null); + + # Makes the actual valid configuration Waybar accepts + # (strips our custom settings before converting to JSON) + makeConfiguration = configuration: + let + # The "modules" option is not valid in the JSON + # as its descendants have to live at the top-level + settingsWithoutModules = + filterAttrs (n: _: n != "modules") configuration; + settingsModules = + optionalAttrs (configuration.modules != { }) configuration.modules; + in removeNulls (settingsWithoutModules // settingsModules); + # The clean list of configurations + finalConfiguration = map makeConfiguration cfg.settings; + in writePrettyJSON "waybar-config.json" finalConfiguration; + + warnings = let + mkUnreferencedModuleWarning = name: + "The module '${name}' defined in '${modulesPath}' is not referenced " + + "in either `modules-left`, `modules-center` or `modules-right` of Waybar's options"; + mkUndefinedModuleWarning = settings: name: + let + # Locations where the module is undefined (a combination modules-{left,center,right}) + locations = flip filter [ "left" "center" "right" ] + (x: elem name settings."modules-${x}"); + mkPath = loc: "'${modulesPath}-${loc}'"; + # The modules-{left,center,right} configuration that includes + # an undefined module + path = concatMapStringsSep " and " mkPath locations; + in "The module '${name}' defined in ${path} is neither " + + "a default module or a custom module declared in '${modulesPath}'"; + mkInvalidModuleNameWarning = name: + "The custom module '${name}' defined in '${modulesPath}' is not a valid " + + "module name. A custom module's name must start with 'custom/' " + + "like 'custom/mymodule' for instance"; + + # Find all modules in `modules-{left,center,right}` and `modules` not declared/referenced. + # `cfg.settings` is a list of Waybar configurations + # and we need to preserve the index for appropriate warnings + allFaultyModules = flip map cfg.settings (settings: + let + allModules = unique + (concatMap (x: attrByPath [ "modules-${x}" ] [ ] settings) [ + "left" + "center" + "right" + ]); + declaredModules = attrNames settings.modules; + # Modules declared in `modules` but not referenced in `modules-{left,center,right}` + unreferencedModules = subtractLists allModules declaredModules; + # Modules listed in modules-{left,center,right} that are not default modules + nonDefaultModules = subtractLists defaultModuleNames allModules; + # Modules referenced in `modules-{left,center,right}` but not declared in `modules` + undefinedModules = subtractLists declaredModules nonDefaultModules; + # Check for invalid module names + invalidModuleNames = + filter (m: !isValidCustomModuleName m) (attrNames settings.modules); + in { + # The Waybar bar configuration (since config.settings is a list) + settings = settings; + undef = undefinedModules; + unref = unreferencedModules; + invalidName = invalidModuleNames; + }); + + allWarnings = flip concatMap allFaultyModules + ({ settings, undef, unref, invalidName }: + let + unreferenced = map mkUnreferencedModuleWarning unref; + undefined = map (mkUndefinedModuleWarning settings) undef; + invalid = map mkInvalidModuleNameWarning invalidName; + in undefined ++ unreferenced ++ invalid); + in allWarnings; + + in mkIf cfg.enable (mkMerge [ + { home.packages = [ cfg.package ]; } + (mkIf (cfg.settings != [ ]) { + # Generate warnings about defined but unreferenced modules + inherit warnings; + + xdg.configFile."waybar/config".source = configSource; + }) + (mkIf (cfg.style != null) { + xdg.configFile."waybar/style.css".text = cfg.style; + }) + (mkIf cfg.systemd.enable { + systemd.user.services.waybar = { + Unit = { + Description = + "Highly customizable Wayland bar for Sway and Wlroots based compositors."; + Documentation = "https://github.com/Alexays/Waybar/wiki"; + PartOf = [ "graphical-session.target" ]; + Requisite = [ "dbus.service" ]; + After = [ "dbus.service" ]; + }; + + Service = { + Type = "dbus"; + BusName = "fr.arouillard.waybar"; + ExecStart = "${cfg.package}/bin/waybar"; + Restart = "always"; + RestartSec = "1sec"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }) + ]); +} diff --git a/home-manager/modules/programs/zoxide.nix b/home-manager/modules/programs/zoxide.nix new file mode 100644 index 00000000000..842ff109294 --- /dev/null +++ b/home-manager/modules/programs/zoxide.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.zoxide; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.zoxide = { + enable = mkEnableOption "zoxide"; + + package = mkOption { + type = types.package; + default = pkgs.zoxide; + defaultText = literalExample "pkgs.zoxide"; + description = '' + Zoxide package to install. + ''; + }; + + options = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--no-aliases" ]; + description = '' + List of options to pass to zoxide. + ''; + }; + + enableBashIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Bash integration. + ''; + }; + + enableZshIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Zsh integration. + ''; + }; + + enableFishIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Fish integration. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + eval "$(${cfg.package}/bin/zoxide init bash ${ + concatStringsSep " " cfg.options + })" + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + eval "$(${cfg.package}/bin/zoxide init zsh ${ + concatStringsSep " " cfg.options + })" + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + ${cfg.package}/bin/zoxide init fish ${ + concatStringsSep " " cfg.options + } | source + ''; + }; +} diff --git a/home-manager/modules/programs/zplug.nix b/home-manager/modules/programs/zplug.nix new file mode 100644 index 00000000000..6cb5e98e313 --- /dev/null +++ b/home-manager/modules/programs/zplug.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.zsh.zplug; + + pluginModule = types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.str; + description = "The name of the plugin."; + }; + + tags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "The plugin tags."; + }; + }; + + }); + +in { + options.programs.zsh.zplug = { + enable = mkEnableOption "zplug - a zsh plugin manager"; + + plugins = mkOption { + default = [ ]; + type = types.listOf pluginModule; + description = "List of zplug plugins."; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.zplug ]; + + programs.zsh.initExtraBeforeCompInit = '' + source ${pkgs.zplug}/init.zsh + + ${optionalString (cfg.plugins != [ ]) '' + ${concatStrings (map (plugin: '' + zplug "${plugin.name}"${ + optionalString (plugin.tags != [ ]) '' + ${concatStrings (map (tag: ", ${tag}") plugin.tags)} + '' + } + '') cfg.plugins)} + ''} + + if ! zplug check; then + zplug install + fi + + zplug load + ''; + + }; +} diff --git a/home-manager/modules/programs/zsh.nix b/home-manager/modules/programs/zsh.nix index c5694e1f704..ed65d5fe487 100644 --- a/home-manager/modules/programs/zsh.nix +++ b/home-manager/modules/programs/zsh.nix @@ -18,6 +18,10 @@ let mapAttrsToList (k: v: "alias ${k}=${lib.escapeShellArg v}") cfg.shellAliases ); + globalAliasesStr = concatStringsSep "\n" ( + mapAttrsToList (k: v: "alias -g ${k}=${lib.escapeShellArg v}") cfg.shellGlobalAliases + ); + zdotdir = "$HOME/" + cfg.dotDir; bindkeyCommands = { @@ -152,6 +156,17 @@ let Name of the theme to be used by oh-my-zsh. ''; }; + + extraConfig = mkOption { + default = ""; + example = '' + zstyle :omz:plugins:ssh-agent identities id_rsa id_rsa2 id_github + ''; + type = types.lines; + description = '' + Extra settings for plugins. + ''; + }; }; }; @@ -170,6 +185,14 @@ in type = types.nullOr types.bool; }; + cdpath = mkOption { + default = []; + description = '' + List of paths to autocomplete calls to `cd`. + ''; + type = types.listOf types.str; + }; + dotDir = mkOption { default = null; example = ".config/zsh"; @@ -183,7 +206,12 @@ in shellAliases = mkOption { default = {}; - example = { ll = "ls -l"; ".." = "cd .."; }; + example = literalExample '' + { + ll = "ls -l"; + ".." = "cd .."; + } + ''; description = '' An attribute set that maps aliases (the top level attribute names in this option) to command strings or directly to build outputs. @@ -191,6 +219,21 @@ in type = types.attrsOf types.str; }; + shellGlobalAliases = mkOption { + default = {}; + example = literalExample '' + { + UUID = "$(uuidgen | tr -d \\n)"; + G = "| grep"; + } + ''; + description = '' + Similar to <varname><link linkend="opt-programs.zsh.shellAliases">opt-programs.zsh.shellAliases</link></varname>, + but are substituted anywhere on a line. + ''; + type = types.attrsOf types.str; + }; + enableCompletion = mkOption { default = true; description = '' @@ -357,6 +400,10 @@ in home.file."${relToDotDir ".zshrc"}".text = '' typeset -U path cdpath fpath manpath + ${optionalString (cfg.cdpath != []) '' + cdpath+=(${concatStringsSep " " cfg.cdpath}) + ''} + for profile in ''${(z)NIX_PROFILES}; do fpath+=($profile/share/zsh/site-functions $profile/share/zsh/$ZSH_VERSION/functions $profile/share/zsh/vendor-completions) done @@ -393,6 +440,8 @@ in ${envVarsStr} ${optionalString cfg.oh-my-zsh.enable '' + # oh-my-zsh extra settings for plugins + ${cfg.oh-my-zsh.extraConfig} # oh-my-zsh configuration generated by NixOS ${optionalString (cfg.oh-my-zsh.plugins != []) "plugins=(${concatStringsSep " " cfg.oh-my-zsh.plugins})" @@ -433,6 +482,9 @@ in # Aliases ${aliasesStr} + + # Global Aliases + ${globalAliasesStr} ''; } diff --git a/home-manager/modules/services/clipmenu.nix b/home-manager/modules/services/clipmenu.nix new file mode 100644 index 00000000000..2e1c10e43d8 --- /dev/null +++ b/home-manager/modules/services/clipmenu.nix @@ -0,0 +1,43 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.clipmenu; + +in { + meta.maintainers = [ maintainers.DamienCassou ]; + + options.services.clipmenu = { + enable = mkEnableOption "clipmenu, the clipboard management daemon"; + + package = mkOption { + type = types.package; + default = pkgs.clipmenu; + defaultText = "pkgs.clipmenu"; + description = "clipmenu derivation to use."; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + systemd.user.services.clipmenu = { + Unit = { + Description = "Clipboard management daemon"; + After = [ "graphical-session.target" ]; + }; + + Service = { + ExecStart = "${cfg.package}/bin/clipmenud"; + Environment = "PATH=${ + makeBinPath + (with pkgs; [ coreutils findutils gnugrep gnused systemd ]) + }"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/compton.nix b/home-manager/modules/services/compton.nix index c5b96af34da..0b8e7232b45 100644 --- a/home-manager/modules/services/compton.nix +++ b/home-manager/modules/services/compton.nix @@ -1,291 +1,43 @@ { config, lib, pkgs, ... }: -with lib; -with builtins; - -let - - cfg = config.services.compton; - - configFile = pkgs.writeText "compton.conf" (optionalString cfg.fade '' - # fading - fading = true; - fade-delta = ${toString cfg.fadeDelta}; - fade-in-step = ${elemAt cfg.fadeSteps 0}; - fade-out-step = ${elemAt cfg.fadeSteps 1}; - fade-exclude = ${toJSON cfg.fadeExclude}; - '' + optionalString cfg.shadow '' - - # shadows - shadow = true; - shadow-offset-x = ${toString (elemAt cfg.shadowOffsets 0)}; - shadow-offset-y = ${toString (elemAt cfg.shadowOffsets 1)}; - shadow-opacity = ${cfg.shadowOpacity}; - shadow-exclude = ${toJSON cfg.shadowExclude}; - no-dock-shadow = ${toJSON cfg.noDockShadow}; - no-dnd-shadow = ${toJSON cfg.noDNDShadow}; - '' + optionalString cfg.blur '' - - # blur - blur-background = true; - blur-background-exclude = ${toJSON cfg.blurExclude}; - '' + '' - - # opacity - active-opacity = ${cfg.activeOpacity}; - inactive-opacity = ${cfg.inactiveOpacity}; - menu-opacity = ${cfg.menuOpacity}; - opacity-rule = ${toJSON cfg.opacityRule}; - - # other options - backend = ${toJSON cfg.backend}; - vsync = ${toJSON cfg.vSync}; - refresh-rate = ${toString cfg.refreshRate}; - '' + cfg.extraOptions); - -in { - - options.services.compton = { - enable = mkEnableOption "Compton X11 compositor"; - - blur = mkOption { - type = types.bool; - default = false; - description = '' - Enable background blur on transparent windows. - ''; - }; - - blurExclude = mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ "class_g = 'slop'" "class_i = 'polybar'" ]; - description = '' - List of windows to exclude background blur. - See the - <citerefentry> - <refentrytitle>compton</refentrytitle> - <manvolnum>1</manvolnum> - </citerefentry> - man page for more examples. - ''; - }; - - fade = mkOption { - type = types.bool; - default = false; - description = '' - Fade windows in and out. - ''; - }; - - fadeDelta = mkOption { - type = types.int; - default = 10; - example = 5; - description = '' - Time between fade animation step (in ms). - ''; - }; - - fadeSteps = mkOption { - type = types.listOf types.str; - default = [ "0.028" "0.03" ]; - example = [ "0.04" "0.04" ]; - description = '' - Opacity change between fade steps (in and out). - ''; - }; - - fadeExclude = mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ "window_type *= 'menu'" "name ~= 'Firefox$'" "focused = 1" ]; - description = '' - List of conditions of windows that should not be faded. - See the - <citerefentry> - <refentrytitle>compton</refentrytitle> - <manvolnum>1</manvolnum> - </citerefentry> - man page for more examples. - ''; - }; - - shadow = mkOption { - type = types.bool; - default = false; - description = '' - Draw window shadows. - ''; - }; - - shadowOffsets = mkOption { - type = types.listOf types.int; - default = [ (-15) (-15) ]; - example = [ (-10) (-15) ]; - description = '' - Horizontal and vertical offsets for shadows (in pixels). - ''; - }; - - shadowOpacity = mkOption { - type = types.str; - default = "0.75"; - example = "0.8"; - description = '' - Window shadows opacity (number in range 0 - 1). - ''; - }; - - shadowExclude = mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ "window_type *= 'menu'" "name ~= 'Firefox$'" "focused = 1" ]; - description = '' - List of conditions of windows that should have no shadow. - See the - <citerefentry> - <refentrytitle>compton</refentrytitle> - <manvolnum>1</manvolnum> - </citerefentry> - man page for more examples. - ''; - }; - - noDockShadow = mkOption { - type = types.bool; - default = true; - description = '' - Avoid shadow on docks. - ''; - }; - - noDNDShadow = mkOption { - type = types.bool; - default = true; - description = '' - Avoid shadow on drag-and-drop windows. - ''; - }; - - activeOpacity = mkOption { - type = types.str; - default = "1.0"; - example = "0.8"; - description = '' - Opacity of active windows. - ''; - }; - - inactiveOpacity = mkOption { - type = types.str; - default = "1.0"; - example = "0.8"; - description = '' - Opacity of inactive windows. - ''; - }; - - menuOpacity = mkOption { - type = types.str; - default = "1.0"; - example = "0.8"; - description = '' - Opacity of dropdown and popup menu. - ''; - }; - - opacityRule = mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ "87:class_i ?= 'scratchpad'" "91:class_i ?= 'xterm'" ]; - description = '' - List of opacity rules. - See the - <citerefentry> - <refentrytitle>compton</refentrytitle> - <manvolnum>1</manvolnum> - </citerefentry> - man page for more examples. - ''; - }; - - backend = mkOption { - type = types.str; - default = "glx"; - description = '' - Backend to use: <literal>glx</literal> or <literal>xrender</literal>. - ''; - }; - - vSync = mkOption { - type = types.str; - default = "none"; - example = "opengl-swc"; - description = '' - Enable vertical synchronization using the specified method. - See the - <citerefentry> - <refentrytitle>compton</refentrytitle> - <manvolnum>1</manvolnum> - </citerefentry> - man page for available methods. - ''; - }; - - refreshRate = mkOption { - type = types.int; - default = 0; - example = 60; - description = '' - Screen refresh rate (0 = automatically detect). - ''; - }; - - package = mkOption { - type = types.package; - default = pkgs.compton; - defaultText = literalExample "pkgs.compton"; - example = literalExample "pkgs.compton"; - description = '' - Compton derivation to use. - ''; - }; - - extraOptions = mkOption { - type = types.str; - default = ""; - example = '' - unredir-if-possible = true; - dbe = true; - ''; - description = '' - Additional Compton configuration. - ''; - }; +with lib; { + imports = let + old = n: [ "services" "compton" n ]; + new = n: [ "services" "picom" n ]; + in [ + (mkRenamedOptionModule (old "activeOpacity") (new "activeOpacity")) + (mkRenamedOptionModule (old "backend") (new "backend")) + (mkRenamedOptionModule (old "blur") (new "blur")) + (mkRenamedOptionModule (old "blurExclude") (new "blurExclude")) + (mkRenamedOptionModule (old "extraOptions") (new "extraOptions")) + (mkRenamedOptionModule (old "fade") (new "fade")) + (mkRenamedOptionModule (old "fadeDelta") (new "fadeDelta")) + (mkRenamedOptionModule (old "fadeExclude") (new "fadeExclude")) + (mkRenamedOptionModule (old "fadeSteps") (new "fadeSteps")) + (mkRenamedOptionModule (old "inactiveDim") (new "inactiveDim")) + (mkRenamedOptionModule (old "inactiveOpacity") (new "inactiveOpacity")) + (mkRenamedOptionModule (old "menuOpacity") (new "menuOpacity")) + (mkRenamedOptionModule (old "noDNDShadow") (new "noDNDShadow")) + (mkRenamedOptionModule (old "noDockShadow") (new "noDockShadow")) + (mkRenamedOptionModule (old "opacityRule") (new "opacityRule")) + (mkRenamedOptionModule (old "package") (new "package")) + (mkRenamedOptionModule (old "refreshRate") (new "refreshRate")) + (mkRenamedOptionModule (old "shadow") (new "shadow")) + (mkRenamedOptionModule (old "shadowExclude") (new "shadowExclude")) + (mkRenamedOptionModule (old "shadowOffsets") (new "shadowOffsets")) + (mkRenamedOptionModule (old "shadowOpacity") (new "shadowOpacity")) + (mkChangedOptionModule (old "vSync") (new "vSync") (v: v != "none")) + ]; + + options.services.compton.enable = mkEnableOption "Compton X11 compositor" // { + visible = false; }; - config = mkIf cfg.enable { - home.packages = [ cfg.package ]; - - systemd.user.services.compton = { - Unit = { - Description = "Compton X11 compositor"; - After = [ "graphical-session-pre.target" ]; - PartOf = [ "graphical-session.target" ]; - }; - - Install = { WantedBy = [ "graphical-session.target" ]; }; + config = mkIf config.services.compton.enable { + warnings = [ + "Obsolete option `services.compton.enable' is used. It was renamed to `services.picom.enable'." + ]; - Service = { - ExecStart = "${cfg.package}/bin/compton --config ${configFile}"; - Restart = "always"; - RestartSec = 3; - } // optionalAttrs (cfg.backend == "glx") { - # Temporarily fixes corrupt colours with Mesa 18. - Environment = [ "allow_rgb10_configs=false" ]; - }; - }; + services.picom.enable = true; }; } diff --git a/home-manager/modules/services/dropbox.nix b/home-manager/modules/services/dropbox.nix new file mode 100644 index 00000000000..bcf3ba2b457 --- /dev/null +++ b/home-manager/modules/services/dropbox.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.dropbox; + baseDir = ".dropbox-hm"; + dropboxCmd = "${pkgs.dropbox-cli}/bin/dropbox"; + homeBaseDir = "${config.home.homeDirectory}/${baseDir}"; + +in { + meta.maintainers = [ maintainers.eyjhb ]; + + options = { + services.dropbox = { + enable = mkEnableOption "Dropbox daemon"; + + path = mkOption { + type = types.path; + default = "${config.home.homeDirectory}/Dropbox"; + defaultText = + literalExample ''"''${config.home.homeDirectory}/Dropbox"''; + apply = toString; # Prevent copies to Nix store. + description = "Where to put the Dropbox directory."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.dropbox-cli ]; + + systemd.user.services.dropbox = { + Unit = { Description = "dropbox"; }; + + Install = { WantedBy = [ "default.target" ]; }; + + Service = { + Environment = [ "HOME=${homeBaseDir}" "DISPLAY=" ]; + + Type = "forking"; + PIDFile = "${homeBaseDir}/.dropbox/dropbox.pid"; + + Restart = "on-failure"; + PrivateTmp = true; + ProtectSystem = "full"; + Nice = 10; + + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStop = "${dropboxCmd} stop"; + ExecStart = toString (pkgs.writeShellScript "dropbox-start" '' + # ensure we have the dirs we need + $DRY_RUN_CMD ${pkgs.coreutils}/bin/mkdir $VERBOSE_ARG -p \ + ${homeBaseDir}/{.dropbox,.dropbox-dist,Dropbox} + + # symlink them as needed + if [[ ! -d ${config.home.homeDirectory}/.dropbox ]]; then + $DRY_RUN_CMD ${pkgs.coreutils}/bin/ln $VERBOSE_ARG -s \ + ${homeBaseDir}/.dropbox ${config.home.homeDirectory}/.dropbox + fi + + if [[ ! -d ${escapeShellArg cfg.path} ]]; then + $DRY_RUN_CMD ${pkgs.coreutils}/bin/ln $VERBOSE_ARG -s \ + ${homeBaseDir}/Dropbox ${escapeShellArg cfg.path} + fi + + # get the dropbox bins if needed + if [[ ! -f $HOME/.dropbox-dist/VERSION ]]; then + ${pkgs.coreutils}/bin/yes | ${dropboxCmd} update + fi + + ${dropboxCmd} start + ''); + }; + }; + }; +} diff --git a/home-manager/modules/services/dunst.nix b/home-manager/modules/services/dunst.nix index d32e875137b..5fbbb884a8e 100644 --- a/home-manager/modules/services/dunst.nix +++ b/home-manager/modules/services/dunst.nix @@ -45,7 +45,7 @@ let }; hicolorTheme = { - package = pkgs.hicolor_icon_theme; + package = pkgs.hicolor-icon-theme; name = "hicolor"; size = "32x32"; }; @@ -89,6 +89,8 @@ in { config = mkIf cfg.enable (mkMerge [ { + home.packages = [ (getOutput "man" pkgs.dunst) ]; + xdg.dataFile."dbus-1/services/org.knopwob.dunst.service".source = "${pkgs.dunst}/share/dbus-1/services/org.knopwob.dunst.service"; diff --git a/home-manager/modules/services/emacs.nix b/home-manager/modules/services/emacs.nix index 5b0e88db72d..a73b750c513 100644 --- a/home-manager/modules/services/emacs.nix +++ b/home-manager/modules/services/emacs.nix @@ -7,36 +7,138 @@ let cfg = config.services.emacs; emacsCfg = config.programs.emacs; emacsBinPath = "${emacsCfg.finalPackage}/bin"; + emacsVersion = getVersion emacsCfg.finalPackage; + + # Adapted from upstream emacs.desktop + clientDesktopItem = pkgs.makeDesktopItem rec { + name = "emacsclient"; + desktopName = "Emacs Client"; + genericName = "Text Editor"; + comment = "Edit text"; + mimeType = + "text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;"; + exec = "${emacsBinPath}/emacsclient ${ + concatStringsSep " " cfg.client.arguments + } %F"; + icon = "emacs"; + type = "Application"; + terminal = "false"; + categories = "Utility;TextEditor;"; + extraEntries = '' + StartupWMClass=Emacs + ''; + }; + + # Match the default socket path for the Emacs version so emacsclient continues + # to work without wrapping it. It might be worthwhile to allow customizing the + # socket path, but we would want to wrap emacsclient in the user profile to + # connect to the alternative socket by default for Emacs 26, and set + # EMACS_SOCKET_NAME for Emacs 27. + # + # As systemd doesn't perform variable expansion for the ListenStream param, we + # would also have to solve the problem of matching the shell path to the path + # used in the socket unit, which would likely involve templating. It seems of + # little value for the most common use case of one Emacs daemon per user + # session. + socketPath = if versionAtLeast emacsVersion "27" then + "%t/emacs/server" + else + "%T/emacs%U/server"; in { - options.services.emacs = { enable = mkEnableOption "the Emacs daemon"; }; - - config = mkIf cfg.enable { - assertions = [{ - assertion = emacsCfg.enable; - message = "The Emacs service module requires" - + " 'programs.emacs.enable = true'."; - }]; - - systemd.user.services.emacs = { - Unit = { - Description = "Emacs: the extensible, self-documenting text editor"; - Documentation = - "info:emacs man:emacs(1) https://gnu.org/software/emacs/"; - - # Avoid killing the Emacs session, which may be full of - # unsaved buffers. - X-RestartIfChanged = false; - }; + meta.maintainers = [ maintainers.tadfisher ]; - Service = { - ExecStart = - "${pkgs.runtimeShell} -l -c 'exec ${emacsBinPath}/emacs --fg-daemon'"; - ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs)'"; - Restart = "on-failure"; + options.services.emacs = { + enable = mkEnableOption "the Emacs daemon"; + + client = { + enable = mkEnableOption "generation of Emacs client desktop file"; + arguments = mkOption { + type = with types; listOf str; + default = [ "-c" ]; + description = '' + Command-line arguments to pass to <command>emacsclient</command>. + ''; }; + }; - Install = { WantedBy = [ "default.target" ]; }; + # Attrset for forward-compatibility; there may be a need to customize the + # socket path, though allowing for such is not easy to do as systemd socket + # units don't perform variable expansion for 'ListenStream'. + socketActivation = { + enable = mkEnableOption "systemd socket activation for the Emacs service"; }; }; + + config = mkIf cfg.enable (mkMerge [ + { + assertions = [ + { + assertion = emacsCfg.enable; + message = "The Emacs service module requires" + + " 'programs.emacs.enable = true'."; + } + { + assertion = cfg.socketActivation.enable + -> versionAtLeast emacsVersion "26"; + message = "Socket activation requires Emacs 26 or newer."; + } + ]; + + systemd.user.services.emacs = { + Unit = { + Description = "Emacs: the extensible, self-documenting text editor"; + Documentation = + "info:emacs man:emacs(1) https://gnu.org/software/emacs/"; + + # Avoid killing the Emacs session, which may be full of + # unsaved buffers. + X-RestartIfChanged = false; + }; + + Service = { + # We wrap ExecStart in a login shell so Emacs starts with the user's + # environment, most importantly $PATH and $NIX_PROFILES. It may be + # worth investigating a more targeted approach for user services to + # import the user environment. + ExecStart = '' + ${pkgs.runtimeShell} -l -c "${emacsBinPath}/emacs --fg-daemon${ + # In case the user sets 'server-directory' or 'server-name' in + # their Emacs config, we want to specify the socket path explicitly + # so launching 'emacs.service' manually doesn't break emacsclient + # when using socket activation. + optionalString cfg.socketActivation.enable + "=${escapeShellArg socketPath}" + }"''; + # We use '(kill-emacs 0)' to avoid exiting with a failure code, which + # would restart the service immediately. + ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs 0)'"; + Restart = "on-failure"; + }; + } // optionalAttrs (!cfg.socketActivation.enable) { + Install = { WantedBy = [ "default.target" ]; }; + }; + + home.packages = optional cfg.client.enable clientDesktopItem; + } + + (mkIf cfg.socketActivation.enable { + systemd.user.sockets.emacs = { + Unit = { + Description = "Emacs: the extensible, self-documenting text editor"; + Documentation = + "info:emacs man:emacs(1) https://gnu.org/software/emacs/"; + }; + + Socket = { + ListenStream = socketPath; + FileDescriptorName = "server"; + SocketMode = "0600"; + DirectoryMode = "0700"; + }; + + Install = { WantedBy = [ "sockets.target" ]; }; + }; + }) + ]); } diff --git a/home-manager/modules/services/fluidsynth.nix b/home-manager/modules/services/fluidsynth.nix new file mode 100644 index 00000000000..18913fe5426 --- /dev/null +++ b/home-manager/modules/services/fluidsynth.nix @@ -0,0 +1,57 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.fluidsynth; + +in { + meta.maintainers = [ maintainers.valodim ]; + + options = { + services.fluidsynth = { + enable = mkEnableOption "fluidsynth midi synthesizer"; + + soundFont = mkOption { + type = types.path; + default = "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"; + description = '' + The soundfont file to use, in SoundFont 2 format. + ''; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--sample-rate 96000" ]; + description = '' + Extra arguments, added verbatim to the fluidsynth command. See + <citerefentry> + <refentrytitle>fluidsynth.conf</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.fluidsynth = { + Unit = { + Description = "FluidSynth Daemon"; + Documentation = "man:fluidsynth(1)"; + BindsTo = [ "pulseaudio.service" ]; + After = [ "pulseaudio.service" ]; + }; + + Install = { WantedBy = [ "default.target" ]; }; + + Service = { + ExecStart = "${pkgs.fluidsynth}/bin/fluidsynth -a pulseaudio -si ${ + lib.concatStringsSep " " cfg.extraOptions + } ${cfg.soundFont}"; + }; + }; + }; +} diff --git a/home-manager/modules/services/gnome-keyring.nix b/home-manager/modules/services/gnome-keyring.nix index 6d8317dcffc..ce39cea93f9 100644 --- a/home-manager/modules/services/gnome-keyring.nix +++ b/home-manager/modules/services/gnome-keyring.nix @@ -36,7 +36,7 @@ in { args = concatStringsSep " " ([ "--start" "--foreground" ] ++ optional (cfg.components != [ ]) ("--components=" + concatStringsSep "," cfg.components)); - in "${pkgs.gnome3.gnome_keyring}/bin/gnome-keyring-daemon ${args}"; + in "${pkgs.gnome3.gnome-keyring}/bin/gnome-keyring-daemon ${args}"; Restart = "on-abort"; }; diff --git a/home-manager/modules/services/kanshi.nix b/home-manager/modules/services/kanshi.nix new file mode 100644 index 00000000000..4e5e5f104e6 --- /dev/null +++ b/home-manager/modules/services/kanshi.nix @@ -0,0 +1,194 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.kanshi; + + outputModule = types.submodule { + options = { + + criteria = mkOption { + type = types.str; + description = '' + The criteria can either be an output name, an output description or "*". + The latter can be used to match any output. + + On + <citerefentry> + <refentrytitle>sway</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>, + output names and descriptions can be obtained via + <literal>swaymsg -t get_outputs</literal>. + ''; + }; + + status = mkOption { + type = types.nullOr (types.enum [ "enable" "disable" ]); + default = null; + description = '' + Enables or disables the specified output. + ''; + }; + + mode = mkOption { + type = types.nullOr types.str; + default = null; + example = "1920x1080@60Hz"; + description = '' + <width>x<height>[@<rate>[Hz]] + </para><para> + Configures the specified output to use the specified mode. + Modes are a combination of width and height (in pixels) and + a refresh rate (in Hz) that your display can be configured to use. + ''; + }; + + position = mkOption { + type = types.nullOr types.str; + default = null; + example = "1600,0"; + description = '' + <x>,<y> + </para><para> + Places the output at the specified position in the global coordinates + space. + ''; + }; + + scale = mkOption { + type = types.nullOr types.float; + default = null; + example = 2; + description = '' + Scales the output by the specified scale factor. + ''; + }; + + transform = mkOption { + type = types.nullOr (types.enum [ + "normal" + "90" + "180" + "270" + "flipped" + "flipped-90" + "flipped-180" + "flipped-270" + ]); + default = null; + description = '' + Sets the output transform. + ''; + }; + }; + }; + + outputStr = { criteria, status, mode, position, scale, transform, ... }: + ''output "${criteria}"'' + optionalString (status != null) " ${status}" + + optionalString (mode != null) " mode ${mode}" + + optionalString (position != null) " position ${position}" + + optionalString (scale != null) " scale ${toString scale}" + + optionalString (transform != null) " transform ${transform}"; + + profileModule = types.submodule { + options = { + outputs = mkOption { + type = types.listOf outputModule; + default = [ ]; + description = '' + Outputs configuration. + ''; + }; + + exec = mkOption { + type = types.nullOr types.str; + default = null; + example = + "\${pkg.sway}/bin/swaymsg workspace 1, move workspace to eDP-1"; + description = '' + Command executed after the profile is succesfully applied. + ''; + }; + }; + }; + + profileStr = name: + { outputs, exec, ... }: + '' + profile ${name} { + ${concatStringsSep "\n " (map outputStr outputs)} + '' + optionalString (exec != null) " exec ${exec}\n" + '' + } + ''; +in { + + meta.maintainers = [ maintainers.nurelin ]; + + options.services.kanshi = { + enable = mkEnableOption + "kanshi, a Wayland daemon that automatically configures outputs"; + + package = mkOption { + type = types.package; + default = pkgs.kanshi; + defaultText = literalExample "pkgs.kanshi"; + description = '' + kanshi derivation to use. + ''; + }; + + profiles = mkOption { + type = types.attrsOf profileModule; + default = { }; + description = '' + List of profiles. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to append to the kanshi + configuration file. + ''; + }; + + systemdTarget = mkOption { + type = types.str; + default = "sway-session.target"; + description = '' + Systemd target to bind to. + ''; + }; + }; + + config = mkIf cfg.enable { + + xdg.configFile."kanshi/config".text = '' + ${concatStringsSep "\n" (mapAttrsToList profileStr cfg.profiles)} + ${cfg.extraConfig} + ''; + + systemd.user.services.kanshi = { + Unit = { + Description = "Dynamic output configuration"; + Documentation = "man:kanshi(1)"; + PartOf = cfg.systemdTarget; + Requires = cfg.systemdTarget; + After = cfg.systemdTarget; + }; + + Service = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/kanshi"; + Restart = "always"; + }; + + Install = { WantedBy = [ cfg.systemdTarget ]; }; + }; + }; +} diff --git a/home-manager/modules/services/keynav.nix b/home-manager/modules/services/keynav.nix new file mode 100644 index 00000000000..c7f1df373b8 --- /dev/null +++ b/home-manager/modules/services/keynav.nix @@ -0,0 +1,29 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.keynav; + +in { + options.services.keynav = { enable = mkEnableOption "keynav"; }; + + config = mkIf cfg.enable { + systemd.user.services.keynav = { + Unit = { + Description = "keynav"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + ExecStart = "${pkgs.keynav}/bin/keynav"; + RestartSec = 3; + Restart = "always"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/lieer-accounts.nix b/home-manager/modules/services/lieer-accounts.nix new file mode 100644 index 00000000000..187f7dff980 --- /dev/null +++ b/home-manager/modules/services/lieer-accounts.nix @@ -0,0 +1,25 @@ +{ lib, ... }: + +with lib; + +{ + options.lieer.sync = { + enable = mkEnableOption "lieer synchronization service"; + + frequency = mkOption { + type = types.str; + default = "*:0/5"; + description = '' + How often to synchronize the account. + </para><para> + This value is passed to the systemd timer configuration as the + onCalendar option. See + <citerefentry> + <refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum> + </citerefentry> + for more information about the format. + ''; + }; + }; +} diff --git a/home-manager/modules/services/lieer.nix b/home-manager/modules/services/lieer.nix new file mode 100644 index 00000000000..571e2af75c8 --- /dev/null +++ b/home-manager/modules/services/lieer.nix @@ -0,0 +1,66 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.lieer; + + syncAccounts = filter (a: a.lieer.enable && a.lieer.sync.enable) + (attrValues config.accounts.email.accounts); + + escapeUnitName = name: + let + good = upperChars ++ lowerChars ++ stringToCharacters "0123456789-_"; + subst = c: if any (x: x == c) good then c else "-"; + in stringAsChars subst name; + + serviceUnit = account: { + name = escapeUnitName "lieer-${account.name}"; + value = { + Unit = { + Description = "lieer Gmail synchronization for ${account.name}"; + ConditionPathExists = "${account.maildir.absPath}/.gmailieer.json"; + }; + + Service = { + Type = "oneshot"; + ExecStart = "${pkgs.gmailieer}/bin/gmi sync"; + WorkingDirectory = account.maildir.absPath; + }; + }; + }; + + timerUnit = account: { + name = escapeUnitName "lieer-${account.name}"; + value = { + Unit = { + Description = "lieer Gmail synchronization for ${account.name}"; + }; + + Timer = { + OnCalendar = account.lieer.sync.frequency; + RandomizedDelaySec = 30; + }; + + Install = { WantedBy = [ "timers.target" ]; }; + }; + }; + +in { + meta.maintainers = [ maintainers.tadfisher ]; + + options = { + services.lieer.enable = + mkEnableOption "lieer Gmail synchronization service"; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./lieer-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + programs.lieer.enable = true; + systemd.user.services = listToAttrs (map serviceUnit syncAccounts); + systemd.user.timers = listToAttrs (map timerUnit syncAccounts); + }; +} diff --git a/home-manager/modules/services/lorri.nix b/home-manager/modules/services/lorri.nix index 3b2c244e3c0..6183699088b 100644 --- a/home-manager/modules/services/lorri.nix +++ b/home-manager/modules/services/lorri.nix @@ -9,10 +9,19 @@ let in { meta.maintainers = [ maintainers.gerschtli ]; - options = { services.lorri.enable = mkEnableOption "lorri build daemon"; }; + options.services.lorri = { + enable = mkEnableOption "lorri build daemon"; + + package = mkOption { + type = types.package; + default = pkgs.lorri; + defaultText = literalExample "pkgs.lorri"; + description = "Which lorri package to install."; + }; + }; config = mkIf cfg.enable { - home.packages = [ pkgs.lorri ]; + home.packages = [ cfg.package ]; systemd.user = { services.lorri = { @@ -24,7 +33,7 @@ in { }; Service = { - ExecStart = "${pkgs.lorri}/bin/lorri daemon"; + ExecStart = "${cfg.package}/bin/lorri daemon"; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = "read-only"; @@ -32,7 +41,7 @@ in { Environment = let path = with pkgs; makeSearchPath "bin" [ nix gitMinimal gnutar gzip ]; - in "PATH=${path}"; + in [ "PATH=${path}" ]; }; }; diff --git a/home-manager/modules/services/mako.nix b/home-manager/modules/services/mako.nix new file mode 100644 index 00000000000..77ea3011678 --- /dev/null +++ b/home-manager/modules/services/mako.nix @@ -0,0 +1,316 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.mako; + +in { + meta.maintainers = [ maintainers.onny ]; + + options = { + programs.mako = { + enable = mkEnableOption '' + Mako, lightweight notification daemon for Wayland + ''; + + maxVisible = mkOption { + default = 5; + type = types.nullOr types.int; + description = '' + Set maximum number of visible notifications. Set -1 to show all. + ''; + }; + + sort = mkOption { + default = "-time"; + type = + types.nullOr (types.enum [ "+time" "-time" "+priority" "-priority" ]); + description = '' + Sorts incoming notifications by time and/or priority in ascending(+) + or descending(-) order. + ''; + }; + + output = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + Show notifications on the specified output. If empty, notifications + will appear on the focused output. Requires the compositor to support + the Wayland protocol xdg-output-unstable-v1 version 2. + ''; + }; + + layer = mkOption { + default = "top"; + type = + types.nullOr (types.enum [ "background" "bottom" "top" "overlay" ]); + description = '' + Arrange mako at the specified layer, relative to normal windows. + Supported values are background, bottom, top, and overlay. Using + overlay will cause notifications to be displayed above fullscreen + windows, though this may also occur at top depending on your + compositor. + ''; + }; + + anchor = mkOption { + default = "top-right"; + type = types.nullOr (types.enum [ + "top-right" + "top-center" + "top-left" + "bottom-right" + "bottom-center" + "bottom-left" + "center" + ]); + description = '' + Show notifications at the specified position on the output. + Supported values are top-right, top-center, top-left, bottom-right, + bottom-center, bottom-left, and center. + ''; + }; + + font = mkOption { + default = "monospace 10"; + type = types.nullOr types.str; + description = '' + Font to use, in Pango format. + ''; + }; + + backgroundColor = mkOption { + default = "#285577FF"; + type = types.nullOr types.str; + description = '' + Set popup background color to a specific color, represented in hex + color code. + ''; + }; + + textColor = mkOption { + default = "#FFFFFFFF"; + type = types.nullOr types.str; + description = '' + Set popup text color to a specific color, represented in hex color + code. + ''; + }; + + width = mkOption { + default = 300; + type = types.nullOr types.int; + description = '' + Set width of notification popups in specified number of pixels. + ''; + }; + + height = mkOption { + default = 100; + type = types.nullOr types.int; + description = '' + Set maximum height of notification popups. Notifications whose text + takes up less space are shrunk to fit. + ''; + }; + + margin = mkOption { + default = "10"; + type = types.nullOr types.str; + description = '' + Set margin of each edge specified in pixels. Specify single value to + apply margin on all sides. Two comma-seperated values will set + vertical and horizontal edges seperately. Four comma-seperated will + give each edge a seperate value. + For example: 10,20,5 will set top margin to 10, left and right to 20 + and bottom to five. + ''; + }; + + padding = mkOption { + default = "5"; + type = types.nullOr types.str; + description = '' + Set padding of each edge specified in pixels. Specify single value to + apply margin on all sides. Two comma-seperated values will set + vertical and horizontal edges seperately. Four comma-seperated will + give each edge a seperate value. + For example: 10,20,5 will set top margin to 10, left and right to 20 + and bottom to five. + ''; + }; + + borderSize = mkOption { + default = 1; + type = types.nullOr types.int; + description = '' + Set popup border size to the specified number of pixels. + ''; + }; + + borderColor = mkOption { + default = "#4C7899FF"; + type = types.nullOr types.str; + description = '' + Set popup border color to a specific color, represented in hex color + code. + ''; + }; + + borderRadius = mkOption { + default = 0; + type = types.nullOr types.int; + description = '' + Set popup corner radius to the specified number of pixels. + ''; + }; + + progressColor = mkOption { + default = "over #5588AAFF"; + type = types.nullOr types.str; + description = '' + Set popup progress indicator color to a specific color, + represented in hex color code. To draw the progress + indicator on top of the background color, use the + <literal>over</literal> attribute. To replace the background + color, use the <literal>source</literal> attribute (this can + be useful when the notification is semi-transparent). + ''; + }; + + icons = mkOption { + default = true; + type = types.nullOr types.bool; + description = '' + Whether or not to show icons in notifications. + ''; + }; + + maxIconSize = mkOption { + default = 64; + type = types.nullOr types.int; + description = '' + Set maximum icon size to the specified number of pixels. + ''; + }; + + iconPath = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + Paths to search for icons when a notification specifies a name + instead of a full path. Colon-delimited. This approximates the search + algorithm used by the XDG Icon Theme Specification, but does not + support any of the theme metadata. Therefore, if you want to search + parent themes, you'll need to add them to the path manually. + </para><para> + The <filename>/usr/share/icons/hicolor</filename> and + <filename>/usr/share/pixmaps</filename> directories are + always searched. + ''; + }; + + markup = mkOption { + default = true; + type = types.nullOr types.bool; + description = '' + If 1, enable Pango markup. If 0, disable Pango markup. If enabled, + Pango markup will be interpreted in your format specifier and in the + body of notifications. + ''; + }; + + actions = mkOption { + default = true; + type = types.nullOr types.bool; + description = '' + Applications may request an action to be associated with activating a + notification. Disabling this will cause mako to ignore these requests. + ''; + }; + + format = mkOption { + default = "<b>%s</b>\\n%b"; + type = types.nullOr types.str; + description = '' + Set notification format string to format. See FORMAT SPECIFIERS for + more information. To change this for grouped notifications, set it + within a grouped criteria. + ''; + }; + + defaultTimeout = mkOption { + default = 0; + type = types.nullOr types.int; + description = '' + Set the default timeout to timeout in milliseconds. To disable the + timeout, set it to zero. + ''; + }; + + ignoreTimeout = mkOption { + default = false; + type = types.nullOr types.bool; + description = '' + If set, mako will ignore the expire timeout sent by notifications + and use the one provided by default-timeout instead. + ''; + }; + + groupBy = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + A comma-separated list of criteria fields that will be compared to + other visible notifications to determine if this one should form a + group with them. All listed criteria must be exactly equal for two + notifications to group. + ''; + }; + + }; + }; + + config = let + boolToString = v: if v then "true" else "false"; + optionalBoolean = name: val: + lib.optionalString (val != null) "${name}=${boolToString val}"; + optionalInteger = name: val: + lib.optionalString (val != null) "${name}=${toString val}"; + optionalString = name: val: + lib.optionalString (val != null) "${name}=${val}"; + in mkIf cfg.enable { + home.packages = [ pkgs.mako ]; + xdg.configFile."mako/config".text = '' + ${optionalInteger "max-visible" cfg.maxVisible} + ${optionalString "sort" cfg.sort} + ${optionalString "output" cfg.output} + ${optionalString "layer" cfg.layer} + ${optionalString "anchor" cfg.anchor} + + ${optionalString "font" cfg.font} + ${optionalString "background-color" cfg.backgroundColor} + ${optionalString "text-color" cfg.textColor} + ${optionalInteger "width" cfg.width} + ${optionalInteger "height" cfg.height} + ${optionalString "margin" cfg.margin} + ${optionalString "padding" cfg.padding} + ${optionalInteger "border-size" cfg.borderSize} + ${optionalString "border-color" cfg.borderColor} + ${optionalInteger "border-radius" cfg.borderRadius} + ${optionalString "progress-color" cfg.progressColor} + ${optionalBoolean "icons" cfg.icons} + ${optionalInteger "max-icon-size" cfg.maxIconSize} + ${optionalString "icon-path" cfg.iconPath} + ${optionalBoolean "markup" cfg.markup} + ${optionalBoolean "actions" cfg.actions} + ${optionalString "format" cfg.format} + ${optionalInteger "default-timeout" cfg.defaultTimeout} + ${optionalBoolean "ignore-timeout" cfg.ignoreTimeout} + ${optionalString "group-by" cfg.groupBy} + ''; + }; +} diff --git a/home-manager/modules/services/mpd.nix b/home-manager/modules/services/mpd.nix index 2aa1cd3a9fe..13b3ae78f26 100644 --- a/home-manager/modules/services/mpd.nix +++ b/home-manager/modules/services/mpd.nix @@ -42,7 +42,7 @@ in { }; musicDirectory = mkOption { - type = types.path; + type = with types; either path str; default = "${config.home.homeDirectory}/music"; defaultText = "$HOME/music"; apply = toString; # Prevent copies to Nix store. diff --git a/home-manager/modules/services/picom.nix b/home-manager/modules/services/picom.nix new file mode 100644 index 00000000000..4c4da8de697 --- /dev/null +++ b/home-manager/modules/services/picom.nix @@ -0,0 +1,311 @@ +{ config, lib, pkgs, ... }: + +with lib; +with builtins; + +let + + cfg = config.services.picom; + + configFile = pkgs.writeText "picom.conf" (optionalString cfg.fade '' + # fading + fading = true; + fade-delta = ${toString cfg.fadeDelta}; + fade-in-step = ${elemAt cfg.fadeSteps 0}; + fade-out-step = ${elemAt cfg.fadeSteps 1}; + fade-exclude = ${toJSON cfg.fadeExclude}; + '' + optionalString cfg.shadow '' + + # shadows + shadow = true; + shadow-offset-x = ${toString (elemAt cfg.shadowOffsets 0)}; + shadow-offset-y = ${toString (elemAt cfg.shadowOffsets 1)}; + shadow-opacity = ${cfg.shadowOpacity}; + shadow-exclude = ${toJSON cfg.shadowExclude}; + '' + optionalString cfg.blur '' + + # blur + blur-background = true; + blur-background-exclude = ${toJSON cfg.blurExclude}; + '' + '' + + # opacity + active-opacity = ${cfg.activeOpacity}; + inactive-opacity = ${cfg.inactiveOpacity}; + inactive-dim = ${cfg.inactiveDim}; + opacity-rule = ${toJSON cfg.opacityRule}; + + wintypes: + { + dock = { shadow = ${toJSON (!cfg.noDockShadow)}; }; + dnd = { shadow = ${toJSON (!cfg.noDNDShadow)}; }; + popup_menu = { opacity = ${cfg.menuOpacity}; }; + dropdown_menu = { opacity = ${cfg.menuOpacity}; }; + }; + + # other options + backend = ${toJSON cfg.backend}; + vsync = ${toJSON cfg.vSync}; + refresh-rate = ${toString cfg.refreshRate}; + '' + cfg.extraOptions); + +in { + + options.services.picom = { + enable = mkEnableOption "Picom X11 compositor"; + + blur = mkOption { + type = types.bool; + default = false; + description = '' + Enable background blur on transparent windows. + ''; + }; + + blurExclude = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "class_g = 'slop'" "class_i = 'polybar'" ]; + description = '' + List of windows to exclude background blur. + See the + <citerefentry> + <refentrytitle>picom</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + man page for more examples. + ''; + }; + + experimentalBackends = mkOption { + type = types.bool; + default = false; + description = '' + Whether to use the new experimental backends. + ''; + }; + + fade = mkOption { + type = types.bool; + default = false; + description = '' + Fade windows in and out. + ''; + }; + + fadeDelta = mkOption { + type = types.int; + default = 10; + example = 5; + description = '' + Time between fade animation step (in ms). + ''; + }; + + fadeSteps = mkOption { + type = types.listOf types.str; + default = [ "0.028" "0.03" ]; + example = [ "0.04" "0.04" ]; + description = '' + Opacity change between fade steps (in and out). + ''; + }; + + fadeExclude = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "window_type *= 'menu'" "name ~= 'Firefox$'" "focused = 1" ]; + description = '' + List of conditions of windows that should not be faded. + See the + <citerefentry> + <refentrytitle>picom</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + man page for more examples. + ''; + }; + + shadow = mkOption { + type = types.bool; + default = false; + description = '' + Draw window shadows. + ''; + }; + + shadowOffsets = mkOption { + type = types.listOf types.int; + default = [ (-15) (-15) ]; + example = [ (-10) (-15) ]; + description = '' + Horizontal and vertical offsets for shadows (in pixels). + ''; + }; + + shadowOpacity = mkOption { + type = types.str; + default = "0.75"; + example = "0.8"; + description = '' + Window shadows opacity (number in range 0 - 1). + ''; + }; + + shadowExclude = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "window_type *= 'menu'" "name ~= 'Firefox$'" "focused = 1" ]; + description = '' + List of conditions of windows that should have no shadow. + See the + <citerefentry> + <refentrytitle>picom</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + man page for more examples. + ''; + }; + + noDockShadow = mkOption { + type = types.bool; + default = true; + description = '' + Avoid shadow on docks. + ''; + }; + + noDNDShadow = mkOption { + type = types.bool; + default = true; + description = '' + Avoid shadow on drag-and-drop windows. + ''; + }; + + activeOpacity = mkOption { + type = types.str; + default = "1.0"; + example = "0.8"; + description = '' + Opacity of active windows. + ''; + }; + + inactiveDim = mkOption { + type = types.str; + default = "0.0"; + example = "0.2"; + description = '' + Dim inactive windows. + ''; + }; + + inactiveOpacity = mkOption { + type = types.str; + default = "1.0"; + example = "0.8"; + description = '' + Opacity of inactive windows. + ''; + }; + + menuOpacity = mkOption { + type = types.str; + default = "1.0"; + example = "0.8"; + description = '' + Opacity of dropdown and popup menu. + ''; + }; + + opacityRule = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "87:class_i ?= 'scratchpad'" "91:class_i ?= 'xterm'" ]; + description = '' + List of opacity rules. + See the + <citerefentry> + <refentrytitle>picom</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + man page for more examples. + ''; + }; + + backend = mkOption { + type = types.str; + default = "glx"; + description = '' + Backend to use: <literal>glx</literal> or <literal>xrender</literal>. + ''; + }; + + vSync = mkOption { + type = types.bool; + default = false; + description = '' + Enable vertical synchronization. + ''; + }; + + refreshRate = mkOption { + type = types.int; + default = 0; + example = 60; + description = '' + Screen refresh rate (0 = automatically detect). + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.picom; + defaultText = literalExample "pkgs.picom"; + example = literalExample "pkgs.picom"; + description = '' + picom derivation to use. + ''; + }; + + extraOptions = mkOption { + type = types.str; + default = ""; + example = '' + unredir-if-possible = true; + dbe = true; + ''; + description = '' + Additional Picom configuration. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + systemd.user.services.picom = { + Unit = { + Description = "Picom X11 compositor"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = let + experimentalBackendsFlag = + if cfg.experimentalBackends then " --experimental-backends" else ""; + in { + ExecStart = "${cfg.package}/bin/picom --config ${configFile}" + + experimentalBackendsFlag; + Restart = "always"; + RestartSec = 3; + } // optionalAttrs (cfg.backend == "glx") { + # Temporarily fixes corrupt colours with Mesa 18. + Environment = [ "allow_rgb10_configs=false" ]; + }; + }; + }; +} diff --git a/home-manager/modules/services/pulseeffects.nix b/home-manager/modules/services/pulseeffects.nix new file mode 100644 index 00000000000..445b1c0a1f8 --- /dev/null +++ b/home-manager/modules/services/pulseeffects.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.pulseeffects; + + presetOpts = optionalString (cfg.preset != "") "--load-preset ${cfg.preset}"; + +in { + meta.maintainers = [ maintainers.jonringer ]; + + options.services.pulseeffects = { + enable = mkEnableOption "Pulseeffects daemon"; + + preset = mkOption { + type = types.str; + default = ""; + description = '' + Which preset to use when starting pulseeffects. + Will likely need to launch pulseeffects to initially create preset. + ''; + }; + }; + + config = mkIf cfg.enable { + # running pulseeffects will just attach itself to gapplication service + # at-spi2-core is to minimize journalctl noise of: + # "AT-SPI: Error retrieving accessibility bus address: org.freedesktop.DBus.Error.ServiceUnknown: The name org.a11y.Bus was not provided by any .service files" + home.packages = [ pkgs.pulseeffects pkgs.at-spi2-core ]; + + # Will need to add `services.dbus.packages = with pkgs; [ gnome3.dconf ];` + # to /etc/nixos/configuration.nix for daemon to work correctly + + systemd.user.services.pulseeffects = { + Unit = { + Description = "Pulseeffects daemon"; + Requires = [ "dbus.service" ]; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" "pulseaudio.service" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = + "${pkgs.pulseeffects}/bin/pulseeffects --gapplication-service ${presetOpts}"; + ExecStop = "${pkgs.pulseeffects}/bin/pulseeffects --quit"; + Restart = "on-failure"; + RestartSec = 5; + }; + }; + }; +} diff --git a/home-manager/modules/services/screen-locker.nix b/home-manager/modules/services/screen-locker.nix index 30591a7d1a5..554d64f9abe 100644 --- a/home-manager/modules/services/screen-locker.nix +++ b/home-manager/modules/services/screen-locker.nix @@ -17,6 +17,14 @@ in { example = "\${pkgs.i3lock}/bin/i3lock -n -c 000000"; }; + enableDetectSleep = mkOption { + type = types.bool; + default = true; + description = '' + Whether to reset timers when awaking from sleep. + ''; + }; + inactiveInterval = mkOption { type = types.int; default = 10; @@ -42,7 +50,6 @@ in { Extra command-line arguments to pass to <command>xss-lock</command>. ''; }; - }; config = mkIf cfg.enable { @@ -58,10 +65,10 @@ in { Service = { ExecStart = concatStringsSep " " ([ "${pkgs.xautolock}/bin/xautolock" - "-detectsleep" "-time ${toString cfg.inactiveInterval}" "-locker '${pkgs.systemd}/bin/loginctl lock-session $XDG_SESSION_ID'" - ] ++ cfg.xautolockExtraOptions); + ] ++ optional cfg.enableDetectSleep "-detectsleep" + ++ cfg.xautolockExtraOptions); }; }; diff --git a/home-manager/modules/services/spotifyd.nix b/home-manager/modules/services/spotifyd.nix index bc231814eba..dfe0ecd318e 100644 --- a/home-manager/modules/services/spotifyd.nix +++ b/home-manager/modules/services/spotifyd.nix @@ -14,6 +14,18 @@ in { options.services.spotifyd = { enable = mkEnableOption "SpotifyD connect"; + package = mkOption { + type = types.package; + default = pkgs.spotifyd; + defaultText = literalExample "pkgs.spotifyd"; + example = + literalExample "(pkgs.spotifyd.override { withKeyring = true; })"; + description = '' + The <literal>spotifyd</literal> package to use. + Can be used to specify extensions. + ''; + }; + settings = mkOption { type = types.attrsOf (types.attrsOf types.str); default = { }; @@ -21,7 +33,7 @@ in { example = literalExample '' { global = { - user = "Alex"; + username = "Alex"; password = "foo"; device_name = "nix"; }; @@ -31,7 +43,7 @@ in { }; config = mkIf cfg.enable { - home.packages = [ pkgs.spotifyd ]; + home.packages = [ cfg.package ]; systemd.user.services.spotifyd = { Unit = { @@ -43,7 +55,7 @@ in { Service = { ExecStart = - "${pkgs.spotifyd}/bin/spotifyd --no-daemon --config-path ${configFile}"; + "${cfg.package}/bin/spotifyd --no-daemon --config-path ${configFile}"; Restart = "always"; RestartSec = 12; }; diff --git a/home-manager/modules/services/status-notifier-watcher.nix b/home-manager/modules/services/status-notifier-watcher.nix index 3c3e54877b4..ed0537e22e1 100644 --- a/home-manager/modules/services/status-notifier-watcher.nix +++ b/home-manager/modules/services/status-notifier-watcher.nix @@ -34,7 +34,14 @@ in { Before = [ "taffybar.service" ]; }; - Service = { ExecStart = "${cfg.package}/bin/status-notifier-watcher"; }; + Service = { + ExecStart = "${cfg.package}/bin/status-notifier-watcher"; + # Delay the unit start a bit to allow the program to get fully + # set up before letting dependent services start. This is + # brittle and a better solution using, e.g., `BusName=` might + # be possible. + ExecStartPost = "${pkgs.coreutils}/bin/sleep 1"; + }; Install = { WantedBy = [ "graphical-session.target" "taffybar.service" ]; diff --git a/home-manager/modules/services/syncthing.nix b/home-manager/modules/services/syncthing.nix index 2ef10540164..4622ac2e941 100644 --- a/home-manager/modules/services/syncthing.nix +++ b/home-manager/modules/services/syncthing.nix @@ -19,6 +19,8 @@ with lib; config = mkMerge [ (mkIf config.services.syncthing.enable { + home.packages = [ (getOutput "man" pkgs.syncthing) ]; + systemd.user.services = { syncthing = { Unit = { diff --git a/home-manager/modules/services/udiskie.nix b/home-manager/modules/services/udiskie.nix index 2444d68ff93..ca31021cb5c 100644 --- a/home-manager/modules/services/udiskie.nix +++ b/home-manager/modules/services/udiskie.nix @@ -81,9 +81,7 @@ in { PartOf = [ "graphical-session.target" ]; }; - Service = { - ExecStart = "${pkgs.udiskie}/bin/udiskie -2 ${commandArgs}"; - }; + Service = { ExecStart = "${pkgs.udiskie}/bin/udiskie ${commandArgs}"; }; Install = { WantedBy = [ "graphical-session.target" ]; }; }; diff --git a/home-manager/modules/services/unison.nix b/home-manager/modules/services/unison.nix index 93c59e8fd62..a9cf23fb66e 100644 --- a/home-manager/modules/services/unison.nix +++ b/home-manager/modules/services/unison.nix @@ -60,7 +60,7 @@ let }; }; - serialiseArg = key: val: "-${key}=${escapeShellArg val}"; + serialiseArg = key: val: escapeShellArg "-${key}=${escape [ "=" ] val}"; serialiseArgs = args: concatStringsSep " " (mapAttrsToList serialiseArg args); diff --git a/home-manager/modules/services/window-managers/i3-sway/i3.nix b/home-manager/modules/services/window-managers/i3-sway/i3.nix new file mode 100644 index 00000000000..f7124e6fd23 --- /dev/null +++ b/home-manager/modules/services/window-managers/i3-sway/i3.nix @@ -0,0 +1,257 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession.windowManager.i3; + + commonOptions = import ./lib/options.nix { + inherit config lib cfg pkgs; + moduleName = "i3"; + isGaps = cfg.package == pkgs.i3-gaps; + }; + + configModule = types.submodule { + options = { + inherit (commonOptions) + fonts window floating focus assigns modifier workspaceLayout + workspaceAutoBackAndForth keycodebindings colors bars startup gaps menu + terminal; + + keybindings = mkOption { + type = types.attrsOf (types.nullOr types.str); + default = mapAttrs (n: mkOptionDefault) { + "${cfg.config.modifier}+Return" = "exec ${cfg.config.terminal}"; + "${cfg.config.modifier}+Shift+q" = "kill"; + "${cfg.config.modifier}+d" = "exec ${cfg.config.menu}"; + + "${cfg.config.modifier}+Left" = "focus left"; + "${cfg.config.modifier}+Down" = "focus down"; + "${cfg.config.modifier}+Up" = "focus up"; + "${cfg.config.modifier}+Right" = "focus right"; + + "${cfg.config.modifier}+Shift+Left" = "move left"; + "${cfg.config.modifier}+Shift+Down" = "move down"; + "${cfg.config.modifier}+Shift+Up" = "move up"; + "${cfg.config.modifier}+Shift+Right" = "move right"; + + "${cfg.config.modifier}+h" = "split h"; + "${cfg.config.modifier}+v" = "split v"; + "${cfg.config.modifier}+f" = "fullscreen toggle"; + + "${cfg.config.modifier}+s" = "layout stacking"; + "${cfg.config.modifier}+w" = "layout tabbed"; + "${cfg.config.modifier}+e" = "layout toggle split"; + + "${cfg.config.modifier}+Shift+space" = "floating toggle"; + "${cfg.config.modifier}+space" = "focus mode_toggle"; + + "${cfg.config.modifier}+a" = "focus parent"; + + "${cfg.config.modifier}+Shift+minus" = "move scratchpad"; + "${cfg.config.modifier}+minus" = "scratchpad show"; + + "${cfg.config.modifier}+1" = "workspace number 1"; + "${cfg.config.modifier}+2" = "workspace number 2"; + "${cfg.config.modifier}+3" = "workspace number 3"; + "${cfg.config.modifier}+4" = "workspace number 4"; + "${cfg.config.modifier}+5" = "workspace number 5"; + "${cfg.config.modifier}+6" = "workspace number 6"; + "${cfg.config.modifier}+7" = "workspace number 7"; + "${cfg.config.modifier}+8" = "workspace number 8"; + "${cfg.config.modifier}+9" = "workspace number 9"; + "${cfg.config.modifier}+0" = "workspace number 10"; + + "${cfg.config.modifier}+Shift+1" = + "move container to workspace number 1"; + "${cfg.config.modifier}+Shift+2" = + "move container to workspace number 2"; + "${cfg.config.modifier}+Shift+3" = + "move container to workspace number 3"; + "${cfg.config.modifier}+Shift+4" = + "move container to workspace number 4"; + "${cfg.config.modifier}+Shift+5" = + "move container to workspace number 5"; + "${cfg.config.modifier}+Shift+6" = + "move container to workspace number 6"; + "${cfg.config.modifier}+Shift+7" = + "move container to workspace number 7"; + "${cfg.config.modifier}+Shift+8" = + "move container to workspace number 8"; + "${cfg.config.modifier}+Shift+9" = + "move container to workspace number 9"; + "${cfg.config.modifier}+Shift+0" = + "move container to workspace number 10"; + + "${cfg.config.modifier}+Shift+c" = "reload"; + "${cfg.config.modifier}+Shift+r" = "restart"; + "${cfg.config.modifier}+Shift+e" = + "exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'"; + + "${cfg.config.modifier}+r" = "mode resize"; + }; + defaultText = "Default i3 keybindings."; + description = '' + An attribute set that assigns a key press to an action using a key symbol. + See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>. + </para><para> + Consider to use <code>lib.mkOptionDefault</code> function to extend or override + default keybindings instead of specifying all of them from scratch. + ''; + example = literalExample '' + let + modifier = config.xsession.windowManager.i3.config.modifier; + in lib.mkOptionDefault { + "''${modifier}+Return" = "exec i3-sensible-terminal"; + "''${modifier}+Shift+q" = "kill"; + "''${modifier}+d" = "exec \${pkgs.dmenu}/bin/dmenu_run"; + } + ''; + }; + + modes = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { + resize = { + "Left" = "resize shrink width 10 px or 10 ppt"; + "Down" = "resize grow height 10 px or 10 ppt"; + "Up" = "resize shrink height 10 px or 10 ppt"; + "Right" = "resize grow width 10 px or 10 ppt"; + "Escape" = "mode default"; + "Return" = "mode default"; + }; + }; + description = '' + An attribute set that defines binding modes and keybindings + inside them + + Only basic keybinding is supported (bindsym keycomb action), + for more advanced setup use 'i3.extraConfig'. + ''; + }; + }; + }; + + commonFunctions = import ./lib/functions.nix { + inherit cfg lib; + moduleName = "i3"; + }; + + inherit (commonFunctions) + keybindingsStr keycodebindingsStr modeStr assignStr barStr gapsStr + floatingCriteriaStr windowCommandsStr colorSetStr; + + startupEntryStr = { command, always, notification, workspace, ... }: '' + ${if always then "exec_always" else "exec"} ${ + if (notification && workspace == null) then "" else "--no-startup-id" + } ${ + if (workspace == null) then + command + else + "i3-msg 'workspace ${workspace}; exec ${command}'" + } + ''; + + configFile = pkgs.writeText "i3.conf" ((if cfg.config != null then + with cfg.config; '' + font pango:${concatStringsSep ", " fonts} + floating_modifier ${floating.modifier} + new_window ${if window.titlebar then "normal" else "pixel"} ${ + toString window.border + } + new_float ${if floating.titlebar then "normal" else "pixel"} ${ + toString floating.border + } + hide_edge_borders ${window.hideEdgeBorders} + force_focus_wrapping ${if focus.forceWrapping then "yes" else "no"} + focus_follows_mouse ${if focus.followMouse then "yes" else "no"} + focus_on_window_activation ${focus.newWindow} + mouse_warping ${if focus.mouseWarping then "output" else "none"} + workspace_layout ${workspaceLayout} + workspace_auto_back_and_forth ${ + if workspaceAutoBackAndForth then "yes" else "no" + } + + client.focused ${colorSetStr colors.focused} + client.focused_inactive ${colorSetStr colors.focusedInactive} + client.unfocused ${colorSetStr colors.unfocused} + client.urgent ${colorSetStr colors.urgent} + client.placeholder ${colorSetStr colors.placeholder} + client.background ${colors.background} + + ${keybindingsStr { inherit keybindings; }} + ${keycodebindingsStr keycodebindings} + ${concatStringsSep "\n" (mapAttrsToList modeStr modes)} + ${concatStringsSep "\n" (mapAttrsToList assignStr assigns)} + ${concatStringsSep "\n" (map barStr bars)} + ${optionalString (gaps != null) gapsStr} + ${concatStringsSep "\n" (map floatingCriteriaStr floating.criteria)} + ${concatStringsSep "\n" (map windowCommandsStr window.commands)} + ${concatStringsSep "\n" (map startupEntryStr startup)} + '' + else + "") + "\n" + cfg.extraConfig); + +in { + options = { + xsession.windowManager.i3 = { + enable = mkEnableOption "i3 window manager."; + + package = mkOption { + type = types.package; + default = pkgs.i3; + defaultText = literalExample "pkgs.i3"; + example = literalExample "pkgs.i3-gaps"; + description = '' + i3 package to use. + If 'i3.config.gaps' settings are specified, 'pkgs.i3-gaps' will be set as a default package. + ''; + }; + + config = mkOption { + type = types.nullOr configModule; + default = { }; + description = "i3 configuration options."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = + "Extra configuration lines to add to ~/.config/i3/config."; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ cfg.package ]; + xsession.windowManager.command = "${cfg.package}/bin/i3"; + xdg.configFile."i3/config" = { + source = configFile; + onChange = '' + i3Socket=''${XDG_RUNTIME_DIR:-/run/user/$UID}/i3/ipc-socket.* + if [ -S $i3Socket ]; then + echo "Reloading i3" + $DRY_RUN_CMD ${cfg.package}/bin/i3-msg -s $i3Socket reload 1>/dev/null + fi + ''; + }; + } + + (mkIf (cfg.config != null) { + xsession.windowManager.i3.package = + mkDefault (if (cfg.config.gaps != null) then pkgs.i3-gaps else pkgs.i3); + }) + + (mkIf (cfg.config != null + && (any (s: s.workspace != null) cfg.config.startup)) { + warnings = [ + ("'xsession.windowManager.i3.config.startup.*.workspace' is deprecated, " + + "use 'xsession.windowManager.i3.config.assigns' instead." + + "See https://github.com/rycee/home-manager/issues/265.") + ]; + }) + ]); +} diff --git a/home-manager/modules/services/window-managers/i3-sway/lib/functions.nix b/home-manager/modules/services/window-managers/i3-sway/lib/functions.nix new file mode 100644 index 00000000000..9391e6e92fc --- /dev/null +++ b/home-manager/modules/services/window-managers/i3-sway/lib/functions.nix @@ -0,0 +1,127 @@ +{ cfg, lib, moduleName }: + +with lib; + +rec { + criteriaStr = criteria: + "[${ + concatStringsSep " " (mapAttrsToList (k: v: ''${k}="${v}"'') criteria) + }]"; + + keybindingsStr = { keybindings, bindsymArgs ? "" }: + concatStringsSep "\n" (mapAttrsToList (keycomb: action: + optionalString (action != null) "bindsym ${ + lib.optionalString (bindsymArgs != "") "${bindsymArgs} " + }${keycomb} ${action}") keybindings); + + keycodebindingsStr = keycodebindings: + concatStringsSep "\n" (mapAttrsToList (keycomb: action: + optionalString (action != null) "bindcode ${keycomb} ${action}") + keycodebindings); + + colorSetStr = c: + concatStringsSep " " [ + c.border + c.background + c.text + c.indicator + c.childBorder + ]; + barColorSetStr = c: concatStringsSep " " [ c.border c.background c.text ]; + + modeStr = name: keybindings: '' + mode "${name}" { + ${keybindingsStr { inherit keybindings; }} + } + ''; + + assignStr = workspace: criteria: + concatStringsSep "\n" + (map (c: "assign ${criteriaStr c} ${workspace}") criteria); + + barStr = { id, fonts, mode, hiddenState, position, workspaceButtons + , workspaceNumbers, command, statusCommand, colors, trayOutput, extraConfig + , ... }: + let colorsNotNull = lib.filterAttrs (n: v: v != null) colors != { }; + in '' + bar { + ${optionalString (id != null) "id ${id}"} + ${ + optionalString (fonts != [ ]) + "font pango:${concatStringsSep ", " fonts}" + } + ${optionalString (mode != null) "mode ${mode}"} + ${optionalString (hiddenState != null) "hidden_state ${hiddenState}"} + ${optionalString (position != null) "position ${position}"} + ${ + optionalString (statusCommand != null) + "status_command ${statusCommand}" + } + ${moduleName}bar_command ${command} + ${ + optionalString (workspaceButtons != null) + "workspace_buttons ${if workspaceButtons then "yes" else "no"}" + } + ${ + optionalString (workspaceNumbers != null) + "strip_workspace_numbers ${if !workspaceNumbers then "yes" else "no"}" + } + ${optionalString (trayOutput != null) "tray_output ${trayOutput}"} + ${optionalString colorsNotNull "colors {"} + ${ + optionalString (colors.background != null) + "background ${colors.background}" + } + ${ + optionalString (colors.statusline != null) + "statusline ${colors.statusline}" + } + ${ + optionalString (colors.separator != null) + "separator ${colors.separator}" + } + ${ + optionalString (colors.focusedWorkspace != null) + "focused_workspace ${barColorSetStr colors.focusedWorkspace}" + } + ${ + optionalString (colors.activeWorkspace != null) + "active_workspace ${barColorSetStr colors.activeWorkspace}" + } + ${ + optionalString (colors.inactiveWorkspace != null) + "inactive_workspace ${barColorSetStr colors.inactiveWorkspace}" + } + ${ + optionalString (colors.urgentWorkspace != null) + "urgent_workspace ${barColorSetStr colors.urgentWorkspace}" + } + ${ + optionalString (colors.bindingMode != null) + "binding_mode ${barColorSetStr colors.bindingMode}" + } + ${optionalString colorsNotNull "}"} + ${extraConfig} + } + ''; + + gapsStr = with cfg.config.gaps; '' + ${optionalString (inner != null) "gaps inner ${toString inner}"} + ${optionalString (outer != null) "gaps outer ${toString outer}"} + ${optionalString (horizontal != null) + "gaps horizontal ${toString horizontal}"} + ${optionalString (vertical != null) "gaps vertical ${toString vertical}"} + ${optionalString (top != null) "gaps top ${toString top}"} + ${optionalString (bottom != null) "gaps bottom ${toString bottom}"} + ${optionalString (left != null) "gaps left ${toString left}"} + ${optionalString (right != null) "gaps right ${toString right}"} + + ${optionalString smartGaps "smart_gaps on"} + ${optionalString (smartBorders != "off") "smart_borders ${smartBorders}"} + ''; + + floatingCriteriaStr = criteria: + "for_window ${criteriaStr criteria} floating enable"; + windowCommandsStr = { command, criteria, ... }: + "for_window ${criteriaStr criteria} ${command}"; +} diff --git a/home-manager/modules/services/window-managers/i3-sway/lib/options.nix b/home-manager/modules/services/window-managers/i3-sway/lib/options.nix new file mode 100644 index 00000000000..edfdcd4feae --- /dev/null +++ b/home-manager/modules/services/window-managers/i3-sway/lib/options.nix @@ -0,0 +1,763 @@ +{ config, lib, moduleName, cfg, pkgs, capitalModuleName ? moduleName +, isGaps ? true }: + +with lib; + +let + fonts = mkOption { + type = types.listOf types.str; + default = [ "monospace 8" ]; + description = '' + Font list used for window titles. Only FreeType fonts are supported. + The order here is important (e.g. icons font should go before the one used for text). + ''; + example = [ "FontAwesome 10" "Terminus 10" ]; + }; + + startupModule = types.submodule { + options = { + command = mkOption { + type = types.str; + description = "Command that will be executed on startup."; + }; + + always = mkOption { + type = types.bool; + default = false; + description = "Whether to run command on each ${moduleName} restart."; + }; + } // optionalAttrs (moduleName == "i3") { + notification = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable startup-notification support for the command. + See <option>--no-startup-id</option> option description in the i3 user guide. + ''; + }; + + workspace = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Launch application on a particular workspace. DEPRECATED: + Use <varname><link linkend="opt-xsession.windowManager.i3.config.assigns">xsession.windowManager.i3.config.assigns</link></varname> + instead. See <link xlink:href="https://github.com/rycee/home-manager/issues/265"/>. + ''; + }; + }; + + }; + + barModule = types.submodule { + options = let + versionAtLeast2009 = versionAtLeast config.home.stateVersion "20.09"; + mkNullableOption = { type, default, ... }@args: + mkOption (args // optionalAttrs versionAtLeast2009 { + type = types.nullOr type; + default = null; + example = default; + } // { + defaultText = literalExample '' + ${ + if isString default then default else "See code" + } for state version < 20.09, + null for state version β₯ 20.09 + ''; + }); + in { + fonts = fonts // optionalAttrs versionAtLeast2009 { default = [ ]; }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Extra configuration lines for this bar."; + }; + + id = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Specifies the bar ID for the configured bar instance. + If this option is missing, the ID is set to bar-x, where x corresponds + to the position of the embedding bar block in the config file. + ''; + }; + + mode = mkNullableOption { + type = types.enum [ "dock" "hide" "invisible" ]; + default = "dock"; + description = "Bar visibility mode."; + }; + + hiddenState = mkNullableOption { + type = types.enum [ "hide" "show" ]; + default = "hide"; + description = "The default bar mode when 'bar.mode' == 'hide'."; + }; + + position = mkNullableOption { + type = types.enum [ "top" "bottom" ]; + default = "bottom"; + description = "The edge of the screen ${moduleName}bar should show up."; + }; + + workspaceButtons = mkNullableOption { + type = types.bool; + default = true; + description = "Whether workspace buttons should be shown or not."; + }; + + workspaceNumbers = mkNullableOption { + type = types.bool; + default = true; + description = + "Whether workspace numbers should be displayed within the workspace buttons."; + }; + + command = mkOption { + type = types.str; + default = "${cfg.package}/bin/${moduleName}bar"; + defaultText = "i3bar"; + description = "Command that will be used to start a bar."; + example = if moduleName == "i3" then + "\${pkgs.i3-gaps}/bin/i3bar -t" + else + "\${pkgs.waybar}/bin/waybar"; + }; + + statusCommand = mkOption { + type = types.nullOr types.str; + default = + if versionAtLeast2009 then null else "${pkgs.i3status}/bin/i3status"; + example = "i3status"; + description = "Command that will be used to get status lines."; + }; + + colors = mkOption { + type = types.submodule { + options = { + background = mkNullableOption { + type = types.str; + default = "#000000"; + description = "Background color of the bar."; + }; + + statusline = mkNullableOption { + type = types.str; + default = "#ffffff"; + description = "Text color to be used for the statusline."; + }; + + separator = mkNullableOption { + type = types.str; + default = "#666666"; + description = "Text color to be used for the separator."; + }; + + focusedWorkspace = mkNullableOption { + type = barColorSetModule; + default = { + border = "#4c7899"; + background = "#285577"; + text = "#ffffff"; + }; + description = '' + Border, background and text color for a workspace button when the workspace has focus. + ''; + }; + + activeWorkspace = mkNullableOption { + type = barColorSetModule; + default = { + border = "#333333"; + background = "#5f676a"; + text = "#ffffff"; + }; + description = '' + Border, background and text color for a workspace button when the workspace is active. + ''; + }; + + inactiveWorkspace = mkNullableOption { + type = barColorSetModule; + default = { + border = "#333333"; + background = "#222222"; + text = "#888888"; + }; + description = '' + Border, background and text color for a workspace button when the workspace does not + have focus and is not active. + ''; + }; + + urgentWorkspace = mkNullableOption { + type = barColorSetModule; + default = { + border = "#2f343a"; + background = "#900000"; + text = "#ffffff"; + }; + description = '' + Border, background and text color for a workspace button when the workspace contains + a window with the urgency hint set. + ''; + }; + + bindingMode = mkNullableOption { + type = barColorSetModule; + default = { + border = "#2f343a"; + background = "#900000"; + text = "#ffffff"; + }; + description = + "Border, background and text color for the binding mode indicator"; + }; + }; + }; + default = { }; + description = '' + Bar color settings. All color classes can be specified using submodules + with 'border', 'background', 'text', fields and RGB color hex-codes as values. + See default values for the reference. + Note that 'background', 'status', and 'separator' parameters take a single RGB value. + + See <link xlink:href="https://i3wm.org/docs/userguide.html#_colors"/>. + ''; + }; + + trayOutput = mkNullableOption { + type = types.str; + default = "primary"; + description = "Where to output tray."; + }; + }; + }; + + barColorSetModule = types.submodule { + options = { + border = mkOption { + type = types.str; + visible = false; + }; + + background = mkOption { + type = types.str; + visible = false; + }; + + text = mkOption { + type = types.str; + visible = false; + }; + }; + }; + + colorSetModule = types.submodule { + options = { + border = mkOption { + type = types.str; + visible = false; + }; + + childBorder = mkOption { + type = types.str; + visible = false; + }; + + background = mkOption { + type = types.str; + visible = false; + }; + + text = mkOption { + type = types.str; + visible = false; + }; + + indicator = mkOption { + type = types.str; + visible = false; + }; + }; + }; + + windowCommandModule = types.submodule { + options = { + command = mkOption { + type = types.str; + description = "${capitalModuleName}wm command to execute."; + example = "border pixel 1"; + }; + + criteria = mkOption { + type = criteriaModule; + description = + "Criteria of the windows on which command should be executed."; + example = { title = "x200: ~/work"; }; + }; + }; + }; + + criteriaModule = types.attrsOf types.str; +in { + inherit fonts; + + window = mkOption { + type = types.submodule { + options = { + titlebar = mkOption { + type = types.bool; + default = !isGaps; + defaultText = if moduleName == "i3" then + "xsession.windowManager.i3.package != nixpkgs.i3-gaps (titlebar should be disabled for i3-gaps)" + else + "false"; + description = "Whether to show window titlebars."; + }; + + border = mkOption { + type = types.int; + default = 2; + description = "Window border width."; + }; + + hideEdgeBorders = mkOption { + type = types.enum [ "none" "vertical" "horizontal" "both" "smart" ]; + default = "none"; + description = "Hide window borders adjacent to the screen edges."; + }; + + commands = mkOption { + type = types.listOf windowCommandModule; + default = [ ]; + description = '' + List of commands that should be executed on specific windows. + See <option>for_window</option> ${moduleName}wm option documentation. + ''; + example = [{ + command = "border pixel 1"; + criteria = { class = "XTerm"; }; + }]; + }; + }; + }; + default = { }; + description = "Window titlebar and border settings."; + }; + + floating = mkOption { + type = types.submodule { + options = { + titlebar = mkOption { + type = types.bool; + default = !isGaps; + defaultText = if moduleName == "i3" then + "xsession.windowManager.i3.package != nixpkgs.i3-gaps (titlebar should be disabled for i3-gaps)" + else + "false"; + description = "Whether to show floating window titlebars."; + }; + + border = mkOption { + type = types.int; + default = 2; + description = "Floating windows border width."; + }; + + modifier = mkOption { + type = + types.enum [ "Shift" "Control" "Mod1" "Mod2" "Mod3" "Mod4" "Mod5" ]; + default = cfg.config.modifier; + defaultText = "${moduleName}.config.modifier"; + description = + "Modifier key that can be used to drag floating windows."; + example = "Mod4"; + }; + + criteria = mkOption { + type = types.listOf criteriaModule; + default = [ ]; + description = + "List of criteria for windows that should be opened in a floating mode."; + example = [ + { "title" = "Steam - Update News"; } + { "class" = "Pavucontrol"; } + ]; + }; + }; + }; + default = { }; + description = "Floating window settings."; + }; + + focus = mkOption { + type = types.submodule { + options = { + newWindow = mkOption { + type = types.enum [ "smart" "urgent" "focus" "none" ]; + default = "smart"; + description = '' + This option modifies focus behavior on new window activation. + + See <link xlink:href="https://i3wm.org/docs/userguide.html#focus_on_window_activation"/> + ''; + example = "none"; + }; + + followMouse = mkOption { + type = if moduleName == "sway" then + types.either (types.enum [ "yes" "no" "always" ]) types.bool + else + types.bool; + default = if moduleName == "sway" then "yes" else true; + description = "Whether focus should follow the mouse."; + apply = val: + if (moduleName == "sway" && isBool val) then + (if val then "yes" else "no") + else + val; + }; + + forceWrapping = mkOption { + type = types.bool; + default = false; + description = '' + Whether to force focus wrapping in tabbed or stacked container. + + See <link xlink:href="https://i3wm.org/docs/userguide.html#_focus_wrapping"/> + ''; + }; + + mouseWarping = mkOption { + type = types.bool; + default = true; + description = '' + Whether mouse cursor should be warped to the center of the window when switching focus + to a window on a different output. + ''; + }; + }; + }; + default = { }; + description = "Focus related settings."; + }; + + assigns = mkOption { + type = types.attrsOf (types.listOf criteriaModule); + default = { }; + description = '' + An attribute set that assigns applications to workspaces based + on criteria. + ''; + example = literalExample '' + { + "1: web" = [{ class = "^Firefox$"; }]; + "0: extra" = [{ class = "^Firefox$"; window_role = "About"; }]; + } + ''; + }; + + modifier = mkOption { + type = types.enum [ "Shift" "Control" "Mod1" "Mod2" "Mod3" "Mod4" "Mod5" ]; + default = "Mod1"; + description = "Modifier key that is used for all default keybindings."; + example = "Mod4"; + }; + + workspaceLayout = mkOption { + type = types.enum [ "default" "stacked" "tabbed" ]; + default = "default"; + example = "tabbed"; + description = '' + The mode in which new containers on workspace level will + start. + ''; + }; + + workspaceAutoBackAndForth = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Assume you are on workspace "1: www" and switch to "2: IM" using + mod+2 because somebody sent you a message. You donβt need to remember + where you came from now, you can just press $mod+2 again to switch + back to "1: www". + ''; + }; + + keycodebindings = mkOption { + type = types.attrsOf (types.nullOr types.str); + default = { }; + description = '' + An attribute set that assigns keypress to an action using key code. + See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>. + ''; + example = { "214" = "exec /bin/script.sh"; }; + }; + + colors = mkOption { + type = types.submodule { + options = { + background = mkOption { + type = types.str; + default = "#ffffff"; + description = '' + Background color of the window. Only applications which do not cover + the whole area expose the color. + ''; + }; + + focused = mkOption { + type = colorSetModule; + default = { + border = "#4c7899"; + background = "#285577"; + text = "#ffffff"; + indicator = "#2e9ef4"; + childBorder = "#285577"; + }; + description = "A window which currently has the focus."; + }; + + focusedInactive = mkOption { + type = colorSetModule; + default = { + border = "#333333"; + background = "#5f676a"; + text = "#ffffff"; + indicator = "#484e50"; + childBorder = "#5f676a"; + }; + description = '' + A window which is the focused one of its container, + but it does not have the focus at the moment. + ''; + }; + + unfocused = mkOption { + type = colorSetModule; + default = { + border = "#333333"; + background = "#222222"; + text = "#888888"; + indicator = "#292d2e"; + childBorder = "#222222"; + }; + description = "A window which is not focused."; + }; + + urgent = mkOption { + type = colorSetModule; + default = { + border = "#2f343a"; + background = "#900000"; + text = "#ffffff"; + indicator = "#900000"; + childBorder = "#900000"; + }; + description = "A window which has its urgency hint activated."; + }; + + placeholder = mkOption { + type = colorSetModule; + default = { + border = "#000000"; + background = "#0c0c0c"; + text = "#ffffff"; + indicator = "#000000"; + childBorder = "#0c0c0c"; + }; + description = '' + Background and text color are used to draw placeholder window + contents (when restoring layouts). Border and indicator are ignored. + ''; + }; + }; + }; + default = { }; + description = '' + Color settings. All color classes can be specified using submodules + with 'border', 'background', 'text', 'indicator' and 'childBorder' fields + and RGB color hex-codes as values. See default values for the reference. + Note that '${moduleName}.config.colors.background' parameter takes a single RGB value. + + See <link xlink:href="https://i3wm.org/docs/userguide.html#_changing_colors"/>. + ''; + }; + + bars = mkOption { + type = types.listOf barModule; + default = if versionAtLeast config.home.stateVersion "20.09" then [{ + mode = "dock"; + hiddenState = "hide"; + position = "bottom"; + workspaceButtons = true; + workspaceNumbers = true; + statusCommand = "${pkgs.i3status}/bin/i3status"; + fonts = [ "monospace 8" ]; + trayOutput = "primary"; + colors = { + background = "#000000"; + statusline = "#ffffff"; + separator = "#666666"; + focusedWorkspace = { + border = "#4c7899"; + background = "#285577"; + text = "#ffffff"; + }; + activeWorkspace = { + border = "#333333"; + background = "#5f676a"; + text = "#ffffff"; + }; + inactiveWorkspace = { + border = "#333333"; + background = "#222222"; + text = "#888888"; + }; + urgentWorkspace = { + border = "#2f343a"; + background = "#900000"; + text = "#ffffff"; + }; + bindingMode = { + border = "#2f343a"; + background = "#900000"; + text = "#ffffff"; + }; + }; + }] else + [ { } ]; + description = '' + ${capitalModuleName} bars settings blocks. Set to empty list to remove bars completely. + ''; + }; + + startup = mkOption { + type = types.listOf startupModule; + default = [ ]; + description = '' + Commands that should be executed at startup. + + See <link xlink:href="https://i3wm.org/docs/userguide.html#_automatically_starting_applications_on_i3_startup"/>. + ''; + example = literalExample '' + [ + { command = "systemctl --user restart polybar"; always = true; notification = false; } + { command = "dropbox start"; notification = false; } + { command = "firefox"; workspace = "1: web"; } + ]; + ''; + }; + + gaps = mkOption { + type = types.nullOr (types.submodule { + options = { + inner = mkOption { + type = types.nullOr types.int; + default = null; + description = "Inner gaps value."; + example = 12; + }; + + outer = mkOption { + type = types.nullOr types.int; + default = null; + description = "Outer gaps value."; + example = 5; + }; + + horizontal = mkOption { + type = types.nullOr types.int; + default = null; + description = "Horizontal gaps value."; + example = 5; + }; + + vertical = mkOption { + type = types.nullOr types.int; + default = null; + description = "Vertical gaps value."; + example = 5; + }; + + top = mkOption { + type = types.nullOr types.int; + default = null; + description = "Top gaps value."; + example = 5; + }; + + left = mkOption { + type = types.nullOr types.int; + default = null; + description = "Left gaps value."; + example = 5; + }; + + bottom = mkOption { + type = types.nullOr types.int; + default = null; + description = "Bottom gaps value."; + example = 5; + }; + + right = mkOption { + type = types.nullOr types.int; + default = null; + description = "Right gaps value."; + example = 5; + }; + + smartGaps = mkOption { + type = types.bool; + default = false; + description = '' + This option controls whether to disable all gaps (outer and inner) + on workspace with a single container. + ''; + example = true; + }; + + smartBorders = mkOption { + type = types.enum [ "on" "off" "no_gaps" ]; + default = "off"; + description = '' + This option controls whether to disable container borders on + workspace with a single container. + ''; + }; + }; + }); + default = null; + description = if moduleName == "sway" then '' + Gaps related settings. + '' else '' + i3Gaps related settings. The i3-gaps package must be used for these features to work. + ''; + }; + + terminal = mkOption { + type = types.str; + default = if moduleName == "i3" then + "i3-sensible-terminal" + else + "${pkgs.rxvt-unicode-unwrapped}/bin/urxvt"; + description = "Default terminal to run."; + example = "alacritty"; + }; + + menu = mkOption { + type = types.str; + default = if moduleName == "sway" then + "${pkgs.dmenu}/bin/dmenu_path | ${pkgs.dmenu}/bin/dmenu | ${pkgs.findutils}/bin/xargs swaymsg exec --" + else + "${pkgs.dmenu}/bin/dmenu_run"; + description = "Default launcher to use."; + example = "bemenu-run"; + }; +} diff --git a/home-manager/modules/services/window-managers/i3-sway/sway.nix b/home-manager/modules/services/window-managers/i3-sway/sway.nix new file mode 100644 index 00000000000..8f0ee608104 --- /dev/null +++ b/home-manager/modules/services/window-managers/i3-sway/sway.nix @@ -0,0 +1,416 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.wayland.windowManager.sway; + + commonOptions = import ./lib/options.nix { + inherit config lib cfg pkgs; + moduleName = "sway"; + capitalModuleName = "Sway"; + }; + + configModule = types.submodule { + options = { + inherit (commonOptions) + fonts window floating focus assigns workspaceLayout + workspaceAutoBackAndForth modifier keycodebindings colors bars startup + gaps menu terminal; + + left = mkOption { + type = types.str; + default = "h"; + description = "Home row direction key for moving left."; + }; + + down = mkOption { + type = types.str; + default = "j"; + description = "Home row direction key for moving down."; + }; + + up = mkOption { + type = types.str; + default = "k"; + description = "Home row direction key for moving up."; + }; + + right = mkOption { + type = types.str; + default = "l"; + description = "Home row direction key for moving right."; + }; + + keybindings = mkOption { + type = types.attrsOf (types.nullOr types.str); + default = mapAttrs (n: mkOptionDefault) { + "${cfg.config.modifier}+Return" = "exec ${cfg.config.terminal}"; + "${cfg.config.modifier}+Shift+q" = "kill"; + "${cfg.config.modifier}+d" = "exec ${cfg.config.menu}"; + + "${cfg.config.modifier}+${cfg.config.left}" = "focus left"; + "${cfg.config.modifier}+${cfg.config.down}" = "focus down"; + "${cfg.config.modifier}+${cfg.config.up}" = "focus up"; + "${cfg.config.modifier}+${cfg.config.right}" = "focus right"; + + "${cfg.config.modifier}+Left" = "focus left"; + "${cfg.config.modifier}+Down" = "focus down"; + "${cfg.config.modifier}+Up" = "focus up"; + "${cfg.config.modifier}+Right" = "focus right"; + + "${cfg.config.modifier}+Shift+${cfg.config.left}" = "move left"; + "${cfg.config.modifier}+Shift+${cfg.config.down}" = "move down"; + "${cfg.config.modifier}+Shift+${cfg.config.up}" = "move up"; + "${cfg.config.modifier}+Shift+${cfg.config.right}" = "move right"; + + "${cfg.config.modifier}+Shift+Left" = "move left"; + "${cfg.config.modifier}+Shift+Down" = "move down"; + "${cfg.config.modifier}+Shift+Up" = "move up"; + "${cfg.config.modifier}+Shift+Right" = "move right"; + + "${cfg.config.modifier}+b" = "splith"; + "${cfg.config.modifier}+v" = "splitv"; + "${cfg.config.modifier}+f" = "fullscreen toggle"; + "${cfg.config.modifier}+a" = "focus parent"; + + "${cfg.config.modifier}+s" = "layout stacking"; + "${cfg.config.modifier}+w" = "layout tabbed"; + "${cfg.config.modifier}+e" = "layout toggle split"; + + "${cfg.config.modifier}+Shift+space" = "floating toggle"; + "${cfg.config.modifier}+space" = "focus mode_toggle"; + + "${cfg.config.modifier}+1" = "workspace number 1"; + "${cfg.config.modifier}+2" = "workspace number 2"; + "${cfg.config.modifier}+3" = "workspace number 3"; + "${cfg.config.modifier}+4" = "workspace number 4"; + "${cfg.config.modifier}+5" = "workspace number 5"; + "${cfg.config.modifier}+6" = "workspace number 6"; + "${cfg.config.modifier}+7" = "workspace number 7"; + "${cfg.config.modifier}+8" = "workspace number 8"; + "${cfg.config.modifier}+9" = "workspace number 9"; + + "${cfg.config.modifier}+Shift+1" = + "move container to workspace number 1"; + "${cfg.config.modifier}+Shift+2" = + "move container to workspace number 2"; + "${cfg.config.modifier}+Shift+3" = + "move container to workspace number 3"; + "${cfg.config.modifier}+Shift+4" = + "move container to workspace number 4"; + "${cfg.config.modifier}+Shift+5" = + "move container to workspace number 5"; + "${cfg.config.modifier}+Shift+6" = + "move container to workspace number 6"; + "${cfg.config.modifier}+Shift+7" = + "move container to workspace number 7"; + "${cfg.config.modifier}+Shift+8" = + "move container to workspace number 8"; + "${cfg.config.modifier}+Shift+9" = + "move container to workspace number 9"; + + "${cfg.config.modifier}+Shift+minus" = "move scratchpad"; + "${cfg.config.modifier}+minus" = "scratchpad show"; + + "${cfg.config.modifier}+Shift+c" = "reload"; + "${cfg.config.modifier}+Shift+e" = + "exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'"; + + "${cfg.config.modifier}+r" = "mode resize"; + }; + defaultText = "Default sway keybindings."; + description = '' + An attribute set that assigns a key press to an action using a key symbol. + See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>. + </para><para> + Consider to use <code>lib.mkOptionDefault</code> function to extend or override + default keybindings instead of specifying all of them from scratch. + ''; + example = literalExample '' + let + modifier = config.wayland.windowManager.sway.config.modifier; + in lib.mkOptionDefault { + "''${modifier}+Return" = "exec ${cfg.config.terminal}"; + "''${modifier}+Shift+q" = "kill"; + "''${modifier}+d" = "exec ${cfg.config.menu}"; + } + ''; + }; + + bindkeysToCode = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to make use of <option>--to-code</option> in keybindings. + ''; + }; + + input = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { }; + example = { "*" = { xkb_variant = "dvorak"; }; }; + description = '' + An attribute set that defines input modules. See man sway_input for options. + ''; + }; + + output = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { }; + example = { "HDMI-A-2" = { bg = "~/path/to/background.png fill"; }; }; + description = '' + An attribute set that defines output modules. See man sway_output for options. + ''; + }; + + modes = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { + resize = { + "${cfg.config.left}" = "resize shrink width 10 px"; + "${cfg.config.down}" = "resize grow height 10 px"; + "${cfg.config.up}" = "resize shrink height 10 px"; + "${cfg.config.right}" = "resize grow width 10 px"; + "Left" = "resize shrink width 10 px"; + "Down" = "resize grow height 10 px"; + "Up" = "resize shrink height 10 px"; + "Right" = "resize grow width 10 px"; + "Escape" = "mode default"; + "Return" = "mode default"; + }; + }; + description = '' + An attribute set that defines binding modes and keybindings + inside them + + Only basic keybinding is supported (bindsym keycomb action), + for more advanced setup use 'sway.extraConfig'. + ''; + }; + }; + }; + + wrapperOptions = types.submodule { + options = let + mkWrapperFeature = default: description: + mkOption { + type = types.bool; + inherit default; + example = !default; + description = "Whether to make use of the ${description}"; + }; + in { + base = mkWrapperFeature true '' + base wrapper to execute extra session commands and prepend a + dbus-run-session to the sway command. + ''; + gtk = mkWrapperFeature false '' + wrapGAppsHook wrapper to execute sway with required environment + variables for GTK applications. + ''; + }; + }; + + commonFunctions = import ./lib/functions.nix { + inherit cfg lib; + moduleName = "sway"; + }; + + inherit (commonFunctions) + keybindingsStr keycodebindingsStr modeStr assignStr barStr gapsStr + floatingCriteriaStr windowCommandsStr colorSetStr; + + startupEntryStr = { command, always, ... }: '' + ${if always then "exec_always" else "exec"} ${command} + ''; + + inputStr = name: attrs: '' + input "${name}" { + ${concatStringsSep "\n" + (mapAttrsToList (name: value: "${name} ${value}") attrs)} + } + ''; + + outputStr = name: attrs: '' + output "${name}" { + ${concatStringsSep "\n" + (mapAttrsToList (name: value: "${name} ${value}") attrs)} + } + ''; + + configFile = pkgs.writeText "sway.conf" ((if cfg.config != null then + with cfg.config; '' + font pango:${concatStringsSep ", " fonts} + floating_modifier ${floating.modifier} + default_border ${if window.titlebar then "normal" else "pixel"} ${ + toString window.border + } + default_floating_border ${ + if floating.titlebar then "normal" else "pixel" + } ${toString floating.border} + hide_edge_borders ${window.hideEdgeBorders} + focus_wrapping ${if focus.forceWrapping then "yes" else "no"} + focus_follows_mouse ${focus.followMouse} + focus_on_window_activation ${focus.newWindow} + mouse_warping ${if focus.mouseWarping then "output" else "none"} + workspace_layout ${workspaceLayout} + workspace_auto_back_and_forth ${ + if workspaceAutoBackAndForth then "yes" else "no" + } + + client.focused ${colorSetStr colors.focused} + client.focused_inactive ${colorSetStr colors.focusedInactive} + client.unfocused ${colorSetStr colors.unfocused} + client.urgent ${colorSetStr colors.urgent} + client.placeholder ${colorSetStr colors.placeholder} + client.background ${colors.background} + + ${keybindingsStr { + inherit keybindings; + bindsymArgs = + lib.optionalString (cfg.config.bindkeysToCode) "--to-code"; + }} + ${keycodebindingsStr keycodebindings} + ${concatStringsSep "\n" (mapAttrsToList inputStr input)} + ${concatStringsSep "\n" (mapAttrsToList outputStr output)} + ${concatStringsSep "\n" (mapAttrsToList modeStr modes)} + ${concatStringsSep "\n" (mapAttrsToList assignStr assigns)} + ${concatStringsSep "\n" (map barStr bars)} + ${optionalString (gaps != null) gapsStr} + ${concatStringsSep "\n" (map floatingCriteriaStr floating.criteria)} + ${concatStringsSep "\n" (map windowCommandsStr window.commands)} + ${concatStringsSep "\n" (map startupEntryStr startup)} + '' + else + "") + "\n" + (if cfg.systemdIntegration then '' + exec "systemctl --user import-environment; systemctl --user start sway-session.target" + '' else + "") + cfg.extraConfig); + + defaultSwayPackage = pkgs.sway.override { + extraSessionCommands = cfg.extraSessionCommands; + extraOptions = cfg.extraOptions; + withBaseWrapper = cfg.wrapperFeatures.base; + withGtkWrapper = cfg.wrapperFeatures.gtk; + }; + +in { + meta.maintainers = [ maintainers.alexarice ]; + + options.wayland.windowManager.sway = { + enable = mkEnableOption "sway wayland compositor"; + + package = mkOption { + type = with types; nullOr package; + default = defaultSwayPackage; + defaultText = literalExample "${pkgs.sway}"; + description = '' + Sway package to use. Will override the options + 'wrapperFeatures', 'extraSessionCommands', and 'extraOptions'. + Set to <code>null</code> to not add any Sway package to your + path. This should be done if you want to use the NixOS Sway + module to install Sway. + ''; + }; + + systemdIntegration = mkOption { + type = types.bool; + default = pkgs.stdenv.isLinux; + example = false; + description = '' + Whether to enable <filename>sway-session.target</filename> on + sway startup. This links to + <filename>graphical-session.target</filename>. + ''; + }; + + xwayland = mkOption { + type = types.bool; + default = true; + description = '' + Enable xwayland, which is needed for the default configuration of sway. + ''; + }; + + wrapperFeatures = mkOption { + type = wrapperOptions; + default = { }; + example = { gtk = true; }; + description = '' + Attribute set of features to enable in the wrapper. + ''; + }; + + extraSessionCommands = mkOption { + type = types.lines; + default = ""; + example = '' + export SDL_VIDEODRIVER=wayland + # needs qt5.qtwayland in systemPackages + export QT_QPA_PLATFORM=wayland + export QT_WAYLAND_DISABLE_WINDOWDECORATION="1" + # Fix for some Java AWT applications (e.g. Android Studio), + # use this if they aren't displayed properly: + export _JAVA_AWT_WM_NONREPARENTING=1 + ''; + description = '' + Shell commands executed just before Sway is started. + ''; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "--verbose" + "--debug" + "--unsupported-gpu" + "--my-next-gpu-wont-be-nvidia" + ]; + description = '' + Command line arguments passed to launch Sway. Please DO NOT report + issues if you use an unsupported GPU (proprietary drivers). + ''; + }; + + config = mkOption { + type = types.nullOr configModule; + default = { }; + description = "Sway configuration options."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = + "Extra configuration lines to add to ~/.config/sway/config."; + }; + }; + + config = mkIf cfg.enable { + home.packages = optional (cfg.package != null) cfg.package + ++ optional cfg.xwayland pkgs.xwayland; + xdg.configFile."sway/config" = { + source = configFile; + onChange = '' + swaySocket=''${XDG_RUNTIME_DIR:-/run/user/$UID}/sway-ipc.$UID.$(${pkgs.procps}/bin/pgrep -x sway).sock + if [ -S $swaySocket ]; then + echo "Reloading sway" + $DRY_RUN_CMD ${pkgs.sway}/bin/swaymsg -s $swaySocket reload + fi + ''; + }; + systemd.user.targets.sway-session = mkIf cfg.systemdIntegration { + Unit = { + Description = "sway compositor session"; + Documentation = [ "man:systemd.special(7)" ]; + BindsTo = [ "graphical-session.target" ]; + Wants = [ "graphical-session-pre.target" ]; + After = [ "graphical-session-pre.target" ]; + }; + }; + }; +} diff --git a/home-manager/modules/services/window-managers/i3.nix b/home-manager/modules/services/window-managers/i3.nix deleted file mode 100644 index 7a4ec90b1cd..00000000000 --- a/home-manager/modules/services/window-managers/i3.nix +++ /dev/null @@ -1,856 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - - cfg = config.xsession.windowManager.i3; - - commonOptions = { - fonts = mkOption { - type = types.listOf types.str; - default = ["monospace 8"]; - description = '' - Font list used for window titles. Only FreeType fonts are supported. - The order here is improtant (e.g. icons font should go before the one used for text). - ''; - example = [ "FontAwesome 10" "Terminus 10" ]; - }; - }; - - startupModule = types.submodule { - options = { - command = mkOption { - type = types.str; - description = "Command that will be executed on startup."; - }; - - always = mkOption { - type = types.bool; - default = false; - description = "Whether to run command on each i3 restart."; - }; - - notification = mkOption { - type = types.bool; - default = true; - description = '' - Whether to enable startup-notification support for the command. - See <option>--no-startup-id</option> option description in the i3 user guide. - ''; - }; - - workspace = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Launch application on a particular workspace. DEPRECATED: - Use <varname><link linkend="opt-xsession.windowManager.i3.config.assigns">xsession.windowManager.i3.config.assigns</link></varname> - instead. See <link xlink:href="https://github.com/rycee/home-manager/issues/265"/>. - ''; - }; - }; - }; - - barColorSetModule = types.submodule { - options = { - border = mkOption { - type = types.str; - visible = false; - }; - - background = mkOption { - type = types.str; - visible = false; - }; - - text = mkOption { - type = types.str; - visible = false; - }; - }; - }; - - colorSetModule = types.submodule { - options = { - border = mkOption { - type = types.str; - visible = false; - }; - - childBorder = mkOption { - type = types.str; - visible = false; - }; - - background = mkOption { - type = types.str; - visible = false; - }; - - text = mkOption { - type = types.str; - visible = false; - }; - - indicator = mkOption { - type = types.str; - visible = false; - }; - }; - }; - - barModule = types.submodule { - options = { - inherit (commonOptions) fonts; - - extraConfig = mkOption { - type = types.lines; - default = ""; - description = "Extra configuration lines for this bar."; - }; - - id = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Specifies the bar ID for the configured bar instance. - If this option is missing, the ID is set to bar-x, where x corresponds - to the position of the embedding bar block in the config file. - ''; - }; - - mode = mkOption { - type = types.enum [ "dock" "hide" "invisible" ]; - default = "dock"; - description = "Bar visibility mode."; - }; - - hiddenState = mkOption { - type = types.enum [ "hide" "show" ]; - default = "hide"; - description = "The default bar mode when 'bar.mode' == 'hide'."; - }; - - position = mkOption { - type = types.enum [ "top" "bottom" ]; - default = "bottom"; - description = "The edge of the screen i3bar should show up."; - }; - - workspaceButtons = mkOption { - type = types.bool; - default = true; - description = "Whether workspace buttons should be shown or not."; - }; - - workspaceNumbers = mkOption { - type = types.bool; - default = true; - description = "Whether workspace numbers should be displayed within the workspace buttons."; - }; - - command = mkOption { - type = types.str; - default = "${cfg.package}/bin/i3bar"; - defaultText = "i3bar"; - description = "Command that will be used to start a bar."; - example = "\${pkgs.i3-gaps}/bin/i3bar -t"; - }; - - statusCommand = mkOption { - type = types.str; - default = "${pkgs.i3status}/bin/i3status"; - description = "Command that will be used to get status lines."; - }; - - colors = mkOption { - type = types.submodule { - options = { - background = mkOption { - type = types.str; - default = "#000000"; - description = "Background color of the bar."; - }; - - statusline = mkOption { - type = types.str; - default = "#ffffff"; - description = "Text color to be used for the statusline."; - }; - - separator = mkOption { - type = types.str; - default = "#666666"; - description = "Text color to be used for the separator."; - }; - - focusedWorkspace = mkOption { - type = barColorSetModule; - default = { border = "#4c7899"; background = "#285577"; text = "#ffffff"; }; - description = '' - Border, background and text color for a workspace button when the workspace has focus. - ''; - }; - - activeWorkspace = mkOption { - type = barColorSetModule; - default = { border = "#333333"; background = "#5f676a"; text = "#ffffff"; }; - description = '' - Border, background and text color for a workspace button when the workspace is active. - ''; - }; - - inactiveWorkspace = mkOption { - type = barColorSetModule; - default = { border = "#333333"; background = "#222222"; text = "#888888"; }; - description = '' - Border, background and text color for a workspace button when the workspace does not - have focus and is not active. - ''; - }; - - urgentWorkspace = mkOption { - type = barColorSetModule; - default = { border = "#2f343a"; background = "#900000"; text = "#ffffff"; }; - description = '' - Border, background and text color for a workspace button when the workspace contains - a window with the urgency hint set. - ''; - }; - - bindingMode = mkOption { - type = barColorSetModule; - default = { border = "#2f343a"; background = "#900000"; text = "#ffffff"; }; - description = "Border, background and text color for the binding mode indicator"; - }; - }; - }; - default = {}; - description = '' - Bar color settings. All color classes can be specified using submodules - with 'border', 'background', 'text', fields and RGB color hex-codes as values. - See default values for the reference. - Note that 'background', 'status', and 'separator' parameters take a single RGB value. - - See <link xlink:href="https://i3wm.org/docs/userguide.html#_colors"/>. - ''; - }; - - trayOutput = mkOption { - type = types.str; - default = "primary"; - description = "Where to output tray."; - }; - }; - }; - - windowCommandModule = types.submodule { - options = { - command = mkOption { - type = types.str; - description = "i3wm command to execute."; - example = "border pixel 1"; - }; - - criteria = mkOption { - type = criteriaModule; - description = "Criteria of the windows on which command should be executed."; - example = { title = "x200: ~/work"; }; - }; - }; - }; - - criteriaModule = types.attrsOf types.str; - - configModule = types.submodule { - options = { - inherit (commonOptions) fonts; - - window = mkOption { - type = types.submodule { - options = { - titlebar = mkOption { - type = types.bool; - default = cfg.package != pkgs.i3-gaps; - defaultText = "xsession.windowManager.i3.package != nixpkgs.i3-gaps (titlebar should be disabled for i3-gaps)"; - description = "Whether to show window titlebars."; - }; - - border = mkOption { - type = types.int; - default = 2; - description = "Window border width."; - }; - - hideEdgeBorders = mkOption { - type = types.enum [ "none" "vertical" "horizontal" "both" "smart" ]; - default = "none"; - description = "Hide window borders adjacent to the screen edges."; - }; - - commands = mkOption { - type = types.listOf windowCommandModule; - default = []; - description = '' - List of commands that should be executed on specific windows. - See <option>for_window</option> i3wm option documentation. - ''; - example = [ { command = "border pixel 1"; criteria = { class = "XTerm"; }; } ]; - }; - }; - }; - default = {}; - description = "Window titlebar and border settings."; - }; - - floating = mkOption { - type = types.submodule { - options = { - titlebar = mkOption { - type = types.bool; - default = cfg.package != pkgs.i3-gaps; - defaultText = "xsession.windowManager.i3.package != nixpkgs.i3-gaps (titlebar should be disabled for i3-gaps)"; - description = "Whether to show floating window titlebars."; - }; - - border = mkOption { - type = types.int; - default = 2; - description = "Floating windows border width."; - }; - - modifier = mkOption { - type = types.enum [ "Shift" "Control" "Mod1" "Mod2" "Mod3" "Mod4" "Mod5" ]; - default = cfg.config.modifier; - defaultText = "i3.config.modifier"; - description = "Modifier key that can be used to drag floating windows."; - example = "Mod4"; - }; - - criteria = mkOption { - type = types.listOf criteriaModule; - default = []; - description = "List of criteria for windows that should be opened in a floating mode."; - example = [ {"title" = "Steam - Update News";} {"class" = "Pavucontrol";} ]; - }; - }; - }; - default = {}; - description = "Floating window settings."; - }; - - focus = mkOption { - type = types.submodule { - options = { - newWindow = mkOption { - type = types.enum [ "smart" "urgent" "focus" "none" ]; - default = "smart"; - description = '' - This option modifies focus behavior on new window activation. - - See <link xlink:href="https://i3wm.org/docs/userguide.html#focus_on_window_activation"/> - ''; - example = "none"; - }; - - followMouse = mkOption { - type = types.bool; - default = true; - description = "Whether focus should follow the mouse."; - }; - - forceWrapping = mkOption { - type = types.bool; - default = false; - description = '' - Whether to force focus wrapping in tabbed or stacked container. - - See <link xlink:href="https://i3wm.org/docs/userguide.html#_focus_wrapping"/> - ''; - }; - - mouseWarping = mkOption { - type = types.bool; - default = true; - description = '' - Whether mouse cursor should be warped to the center of the window when switching focus - to a window on a different output. - ''; - }; - }; - }; - default = {}; - description = "Focus related settings."; - }; - - assigns = mkOption { - type = types.attrsOf (types.listOf criteriaModule); - default = {}; - description = '' - An attribute set that assigns applications to workspaces based - on criteria. - ''; - example = literalExample '' - { - "1: web" = [{ class = "^Firefox$"; }]; - "0: extra" = [{ class = "^Firefox$"; window_role = "About"; }]; - } - ''; - }; - - modifier = mkOption { - type = types.enum [ "Shift" "Control" "Mod1" "Mod2" "Mod3" "Mod4" "Mod5" ]; - default = "Mod1"; - description = "Modifier key that is used for all default keybindings."; - example = "Mod4"; - }; - - workspaceLayout = mkOption { - type = types.enum [ "default" "stacked" "tabbed" ]; - default = "default"; - example = "tabbed"; - description = '' - The mode in which new containers on workspace level will - start. - ''; - }; - - workspaceAutoBackAndForth = mkOption { - type = types.bool; - default = false; - example = true; - description = '' - Assume you are on workspace "1: www" and switch to "2: IM" using - mod+2 because somebody sent you a message. You donβt need to remember - where you came from now, you can just press $mod+2 again to switch - back to "1: www". - ''; - }; - - keybindings = mkOption { - type = types.attrsOf (types.nullOr types.str); - default = mapAttrs (n: mkOptionDefault) { - "${cfg.config.modifier}+Return" = "exec i3-sensible-terminal"; - "${cfg.config.modifier}+Shift+q" = "kill"; - "${cfg.config.modifier}+d" = "exec ${pkgs.dmenu}/bin/dmenu_run"; - - "${cfg.config.modifier}+Left" = "focus left"; - "${cfg.config.modifier}+Down" = "focus down"; - "${cfg.config.modifier}+Up" = "focus up"; - "${cfg.config.modifier}+Right" = "focus right"; - - "${cfg.config.modifier}+Shift+Left" = "move left"; - "${cfg.config.modifier}+Shift+Down" = "move down"; - "${cfg.config.modifier}+Shift+Up" = "move up"; - "${cfg.config.modifier}+Shift+Right" = "move right"; - - "${cfg.config.modifier}+h" = "split h"; - "${cfg.config.modifier}+v" = "split v"; - "${cfg.config.modifier}+f" = "fullscreen toggle"; - - "${cfg.config.modifier}+s" = "layout stacking"; - "${cfg.config.modifier}+w" = "layout tabbed"; - "${cfg.config.modifier}+e" = "layout toggle split"; - - "${cfg.config.modifier}+Shift+space" = "floating toggle"; - "${cfg.config.modifier}+space" = "focus mode_toggle"; - - "${cfg.config.modifier}+a" = "focus parent"; - - "${cfg.config.modifier}+Shift+minus" = "move scratchpad"; - "${cfg.config.modifier}+minus" = "scratchpad show"; - - "${cfg.config.modifier}+1" = "workspace number 1"; - "${cfg.config.modifier}+2" = "workspace number 2"; - "${cfg.config.modifier}+3" = "workspace number 3"; - "${cfg.config.modifier}+4" = "workspace number 4"; - "${cfg.config.modifier}+5" = "workspace number 5"; - "${cfg.config.modifier}+6" = "workspace number 6"; - "${cfg.config.modifier}+7" = "workspace number 7"; - "${cfg.config.modifier}+8" = "workspace number 8"; - "${cfg.config.modifier}+9" = "workspace number 9"; - "${cfg.config.modifier}+0" = "workspace number 10"; - - "${cfg.config.modifier}+Shift+1" = "move container to workspace number 1"; - "${cfg.config.modifier}+Shift+2" = "move container to workspace number 2"; - "${cfg.config.modifier}+Shift+3" = "move container to workspace number 3"; - "${cfg.config.modifier}+Shift+4" = "move container to workspace number 4"; - "${cfg.config.modifier}+Shift+5" = "move container to workspace number 5"; - "${cfg.config.modifier}+Shift+6" = "move container to workspace number 6"; - "${cfg.config.modifier}+Shift+7" = "move container to workspace number 7"; - "${cfg.config.modifier}+Shift+8" = "move container to workspace number 8"; - "${cfg.config.modifier}+Shift+9" = "move container to workspace number 9"; - "${cfg.config.modifier}+Shift+0" = "move container to workspace number 10"; - - "${cfg.config.modifier}+Shift+c" = "reload"; - "${cfg.config.modifier}+Shift+r" = "restart"; - "${cfg.config.modifier}+Shift+e" = "exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'"; - - "${cfg.config.modifier}+r" = "mode resize"; - }; - defaultText = "Default i3 keybindings."; - description = '' - An attribute set that assigns a key press to an action using a key symbol. - See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>. - </para><para> - Consider to use <code>lib.mkOptionDefault</code> function to extend or override - default keybindings instead of specifying all of them from scratch. - ''; - example = literalExample '' - let - modifier = xsession.windowManager.i3.config.modifier; - in - - lib.mkOptionDefault { - "''${modifier}+Return" = "exec i3-sensible-terminal"; - "''${modifier}+Shift+q" = "kill"; - "''${modifier}+d" = "exec \${pkgs.dmenu}/bin/dmenu_run"; - } - ''; - }; - - keycodebindings = mkOption { - type = types.attrsOf (types.nullOr types.str); - default = {}; - description = '' - An attribute set that assigns keypress to an action using key code. - See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>. - ''; - example = { "214" = "exec --no-startup-id /bin/script.sh"; }; - }; - - colors = mkOption { - type = types.submodule { - options = { - background = mkOption { - type = types.str; - default = "#ffffff"; - description = '' - Background color of the window. Only applications which do not cover - the whole area expose the color. - ''; - }; - - focused = mkOption { - type = colorSetModule; - default = { - border = "#4c7899"; background = "#285577"; text = "#ffffff"; - indicator = "#2e9ef4"; childBorder = "#285577"; - }; - description = "A window which currently has the focus."; - }; - - focusedInactive = mkOption { - type = colorSetModule; - default = { - border = "#333333"; background = "#5f676a"; text = "#ffffff"; - indicator = "#484e50"; childBorder = "#5f676a"; - }; - description = '' - A window which is the focused one of its container, - but it does not have the focus at the moment. - ''; - }; - - unfocused = mkOption { - type = colorSetModule; - default = { - border = "#333333"; background = "#222222"; text = "#888888"; - indicator = "#292d2e"; childBorder = "#222222"; - }; - description = "A window which is not focused."; - }; - - urgent = mkOption { - type = colorSetModule; - default = { - border = "#2f343a"; background = "#900000"; text = "#ffffff"; - indicator = "#900000"; childBorder = "#900000"; - }; - description = "A window which has its urgency hint activated."; - }; - - placeholder = mkOption { - type = colorSetModule; - default = { - border = "#000000"; background = "#0c0c0c"; text = "#ffffff"; - indicator = "#000000"; childBorder = "#0c0c0c"; - }; - description = '' - Background and text color are used to draw placeholder window - contents (when restoring layouts). Border and indicator are ignored. - ''; - }; - }; - }; - default = {}; - description = '' - Color settings. All color classes can be specified using submodules - with 'border', 'background', 'text', 'indicator' and 'childBorder' fields - and RGB color hex-codes as values. See default values for the reference. - Note that 'i3.config.colors.background' parameter takes a single RGB value. - - See <link xlink:href="https://i3wm.org/docs/userguide.html#_changing_colors"/>. - ''; - }; - - modes = mkOption { - type = types.attrsOf (types.attrsOf types.str); - default = { - resize = { - "Left" = "resize shrink width 10 px or 10 ppt"; - "Down" = "resize grow height 10 px or 10 ppt"; - "Up" = "resize shrink height 10 px or 10 ppt"; - "Right" = "resize grow width 10 px or 10 ppt"; - "Escape" = "mode default"; - "Return" = "mode default"; - }; - }; - description = '' - An attribute set that defines binding modes and keybindings - inside them - - Only basic keybinding is supported (bindsym keycomb action), - for more advanced setup use 'i3.extraConfig'. - ''; - }; - - bars = mkOption { - type = types.listOf barModule; - default = [{}]; - description = '' - i3 bars settings blocks. Set to empty list to remove bars completely. - ''; - }; - - startup = mkOption { - type = types.listOf startupModule; - default = []; - description = '' - Commands that should be executed at startup. - - See <link xlink:href="https://i3wm.org/docs/userguide.html#_automatically_starting_applications_on_i3_startup"/>. - ''; - example = literalExample '' - [ - { command = "systemctl --user restart polybar"; always = true; notification = false; } - { command = "dropbox start"; notification = false; } - { command = "firefox"; workspace = "1: web"; } - ]; - ''; - }; - - gaps = mkOption { - type = types.nullOr (types.submodule { - options = { - inner = mkOption { - type = types.nullOr types.int; - default = null; - description = "Inner gaps value."; - example = 12; - }; - - outer = mkOption { - type = types.nullOr types.int; - default = null; - description = "Outer gaps value."; - example = 5; - }; - - smartGaps = mkOption { - type = types.bool; - default = false; - description = '' - This option controls whether to disable all gaps (outer and inner) - on workspace with a single container. - ''; - example = true; - }; - - smartBorders = mkOption { - type = types.enum [ "on" "off" "no_gaps" ]; - default = "off"; - description = '' - This option controls whether to disable container borders on - workspace with a single container. - ''; - }; - }; - }); - default = null; - description = '' - i3gaps related settings. - Note that i3-gaps package should be set for this options to take effect. - ''; - }; - }; - }; - - keybindingsStr = keybindings: concatStringsSep "\n" ( - mapAttrsToList (keycomb: action: optionalString (action != null) "bindsym ${keycomb} ${action}") keybindings - ); - - keycodebindingsStr = keycodebindings: concatStringsSep "\n" ( - mapAttrsToList (keycomb: action: optionalString (action != null) "bindcode ${keycomb} ${action}") keycodebindings - ); - - colorSetStr = c: concatStringsSep " " [ c.border c.background c.text c.indicator c.childBorder ]; - barColorSetStr = c: concatStringsSep " " [ c.border c.background c.text ]; - - criteriaStr = criteria: "[${concatStringsSep " " (mapAttrsToList (k: v: ''${k}="${v}"'') criteria)}]"; - - modeStr = name: keybindings: '' - mode "${name}" { - ${keybindingsStr keybindings} - } - ''; - - assignStr = workspace: criteria: concatStringsSep "\n" ( - map (c: "assign ${criteriaStr c} ${workspace}") criteria - ); - - barStr = { - id, fonts, mode, hiddenState, position, workspaceButtons, - workspaceNumbers, command, statusCommand, colors, trayOutput, extraConfig, ... - }: '' - bar { - ${optionalString (id != null) "id ${id}"} - font pango:${concatStringsSep ", " fonts} - mode ${mode} - hidden_state ${hiddenState} - position ${position} - status_command ${statusCommand} - i3bar_command ${command} - workspace_buttons ${if workspaceButtons then "yes" else "no"} - strip_workspace_numbers ${if !workspaceNumbers then "yes" else "no"} - tray_output ${trayOutput} - colors { - background ${colors.background} - statusline ${colors.statusline} - separator ${colors.separator} - focused_workspace ${barColorSetStr colors.focusedWorkspace} - active_workspace ${barColorSetStr colors.activeWorkspace} - inactive_workspace ${barColorSetStr colors.inactiveWorkspace} - urgent_workspace ${barColorSetStr colors.urgentWorkspace} - binding_mode ${barColorSetStr colors.bindingMode} - } - ${extraConfig} - } - ''; - - gapsStr = with cfg.config.gaps; '' - ${optionalString (inner != null) "gaps inner ${toString inner}"} - ${optionalString (outer != null) "gaps outer ${toString outer}"} - ${optionalString smartGaps "smart_gaps on"} - ${optionalString (smartBorders != "off") "smart_borders ${smartBorders}"} - ''; - - floatingCriteriaStr = criteria: "for_window ${criteriaStr criteria} floating enable"; - windowCommandsStr = { command, criteria, ... }: "for_window ${criteriaStr criteria} ${command}"; - - startupEntryStr = { command, always, notification, workspace, ... }: '' - ${if always then "exec_always" else "exec"} ${ - if (notification && workspace == null) then "" else "--no-startup-id" - } ${ - if (workspace == null) then - command - else - "i3-msg 'workspace ${workspace}; exec ${command}'" - } - ''; - - configFile = pkgs.writeText "i3.conf" ((if cfg.config != null then with cfg.config; '' - font pango:${concatStringsSep ", " fonts} - floating_modifier ${floating.modifier} - new_window ${if window.titlebar then "normal" else "pixel"} ${toString window.border} - new_float ${if floating.titlebar then "normal" else "pixel"} ${toString floating.border} - hide_edge_borders ${window.hideEdgeBorders} - force_focus_wrapping ${if focus.forceWrapping then "yes" else "no"} - focus_follows_mouse ${if focus.followMouse then "yes" else "no"} - focus_on_window_activation ${focus.newWindow} - mouse_warping ${if focus.mouseWarping then "output" else "none"} - workspace_layout ${workspaceLayout} - workspace_auto_back_and_forth ${if workspaceAutoBackAndForth then "yes" else "no"} - - client.focused ${colorSetStr colors.focused} - client.focused_inactive ${colorSetStr colors.focusedInactive} - client.unfocused ${colorSetStr colors.unfocused} - client.urgent ${colorSetStr colors.urgent} - client.placeholder ${colorSetStr colors.placeholder} - client.background ${colors.background} - - ${keybindingsStr keybindings} - ${keycodebindingsStr keycodebindings} - ${concatStringsSep "\n" (mapAttrsToList modeStr modes)} - ${concatStringsSep "\n" (mapAttrsToList assignStr assigns)} - ${concatStringsSep "\n" (map barStr bars)} - ${optionalString (gaps != null) gapsStr} - ${concatStringsSep "\n" (map floatingCriteriaStr floating.criteria)} - ${concatStringsSep "\n" (map windowCommandsStr window.commands)} - ${concatStringsSep "\n" (map startupEntryStr startup)} - '' else "") + "\n" + cfg.extraConfig); - -in - -{ - options = { - xsession.windowManager.i3 = { - enable = mkEnableOption "i3 window manager."; - - package = mkOption { - type = types.package; - default = pkgs.i3; - defaultText = literalExample "pkgs.i3"; - example = literalExample "pkgs.i3-gaps"; - description = '' - i3 package to use. - If 'i3.config.gaps' settings are specified, 'pkgs.i3-gaps' will be set as a default package. - ''; - }; - - config = mkOption { - type = types.nullOr configModule; - default = {}; - description = "i3 configuration options."; - }; - - extraConfig = mkOption { - type = types.lines; - default = ""; - description = "Extra configuration lines to add to ~/.config/i3/config."; - }; - }; - }; - - config = mkIf cfg.enable (mkMerge [ - { - home.packages = [ cfg.package ]; - xsession.windowManager.command = "${cfg.package}/bin/i3"; - xdg.configFile."i3/config" = { - source = configFile; - onChange = '' - i3Socket=''${XDG_RUNTIME_DIR:-/run/user/$UID}/i3/ipc-socket.* - if [ -S $i3Socket ]; then - echo "Reloading i3" - $DRY_RUN_CMD ${cfg.package}/bin/i3-msg -s $i3Socket reload 1>/dev/null - fi - ''; - }; - } - - (mkIf (cfg.config != null) { - xsession.windowManager.i3.package = mkDefault ( - if (cfg.config.gaps != null) then pkgs.i3-gaps else pkgs.i3 - ); - }) - - (mkIf (cfg.config != null && (any (s: s.workspace != null) cfg.config.startup)) { - warnings = [ - ("'xsession.windowManager.i3.config.startup.*.workspace' is deprecated, " - + "use 'xsession.windowManager.i3.config.assigns' instead." - + "See https://github.com/rycee/home-manager/issues/265.") - ]; - }) - ]); -} diff --git a/home-manager/modules/services/xscreensaver.nix b/home-manager/modules/services/xscreensaver.nix index 73a365aa730..ac6194e70c1 100644 --- a/home-manager/modules/services/xscreensaver.nix +++ b/home-manager/modules/services/xscreensaver.nix @@ -40,6 +40,10 @@ in { Description = "XScreenSaver"; After = [ "graphical-session-pre.target" ]; PartOf = [ "graphical-session.target" ]; + + # Make sure the service is restarted if the settings change. + X-Restart-Triggers = + [ (builtins.hashString "md5" (builtins.toJSON cfg.settings)) ]; }; Service = { diff --git a/home-manager/modules/systemd-activate.rb b/home-manager/modules/systemd-activate.rb index 8382c840e93..31d06d8fc19 100644 --- a/home-manager/modules/systemd-activate.rb +++ b/home-manager/modules/systemd-activate.rb @@ -1,6 +1,5 @@ require 'set' require 'open3' -require 'shellwords' @dry_run = ENV['DRY_RUN'] @verbose = ENV['VERBOSE'] @@ -28,33 +27,36 @@ def setup_services(old_gen_path, new_gen_path, start_timeout_ms_string) exit if old_services.empty? && new_services.empty? + all_services = get_active_targets_units(new_units_path) + maybe_changed = all_services & old_services + changed_services = get_changed_services(old_units_path, new_units_path, maybe_changed) + unchanged_oneshots = get_oneshot_services(maybe_changed - changed_services) + # These services should be running when this script is finished - services_to_run = get_services_to_run(new_units_path) - maybe_changed_services = services_to_run & old_services + services_to_run = all_services - unchanged_oneshots # Only stop active services, otherwise we might get a 'service not loaded' error # for inactive services that were removed in the current generation. to_stop = get_active_units(old_services - new_services) - to_restart = get_changed_services(old_units_path, new_units_path, maybe_changed_services) + to_restart = changed_services to_start = get_inactive_units(services_to_run - to_restart) - raise "daemon-reload failed" unless run_cmd('systemctl --user daemon-reload') + raise "daemon-reload failed" unless run_cmd('systemctl', '--user', 'daemon-reload') - # Exclude services that aren't allowed to be manually started or stopped + # Exclude units that shouldn't be (re)started or stopped no_manual_start, no_manual_stop, no_restart = get_restricted_units(to_stop + to_restart + to_start) - to_stop -= no_manual_stop + no_restart + notify_skipped_units(to_restart & no_restart) + to_stop -= no_manual_stop to_restart -= no_manual_stop + no_manual_start + no_restart to_start -= no_manual_start - puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty? - if to_stop.empty? && to_start.empty? && to_restart.empty? print_service_msg("All services are already running", services_to_run) else puts "Setting up services" if @verbose - systemctl('stop', to_stop) - systemctl('start', to_start) - systemctl('restart', to_restart) + systemctl_action('stop', to_stop) + systemctl_action('start', to_start) + systemctl_action('restart', to_restart) started_services = to_start + to_restart if start_timeout_ms > 0 && !started_services.empty? && !@dry_run failed = wait_and_get_failed_services(started_services, start_timeout_ms) @@ -76,7 +78,7 @@ def get_services(dir) end def get_service_files(dir) - Dir.chdir(dir) { Dir['*.{service,socket}'] } + Dir.chdir(dir) { Dir['*[^@].{service,socket,timer}'] } end def get_changed_services(dir_a, dir_b, services) @@ -89,24 +91,24 @@ end TargetDirRegexp = /^(.*\.target)\.wants$/ -# @return all services wanted by active targets -def get_services_to_run(units_dir) +# @return all units wanted by active targets +def get_active_targets_units(units_dir) return Set.new unless Dir.exists?(units_dir) targets = Dir.entries(units_dir).map { |entry| entry[TargetDirRegexp, 1] }.compact active_targets = get_active_units(targets) - services_to_run = active_targets.map do |target| + active_units = active_targets.map do |target| get_service_files(File.join(units_dir, "#{target}.wants")) end.flatten - Set.new(services_to_run) + Set.new(active_units) end # @return true on success -def run_cmd(cmd) +def run_cmd(*cmd) print_cmd cmd - @dry_run || system(cmd) + @dry_run || system(*cmd) end -def systemctl(cmd, services) +def systemctl_action(cmd, services) return if services.empty? verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing" @@ -114,7 +116,7 @@ def systemctl(cmd, services) cmd = ['systemctl', '--user', cmd, *services] if @dry_run - puts cmd + puts cmd.join(' ') return end @@ -131,32 +133,44 @@ def systemctl(cmd, services) end end +def systemctl(*cmd) + output, _ = Open3.capture2('systemctl', '--user', *cmd) + output +end + def print_cmd(cmd) - puts cmd if @verbose || @dry_run + puts [*cmd].join(' ') if @verbose || @dry_run end def get_active_units(units) - get_units_by_activity(units, true) + filter_units(units) { |state| state == 'active' } end def get_inactive_units(units) - get_units_by_activity(units, false) + filter_units(units) { |state| state != 'active' } +end + +def get_failed_units(units) + filter_units(units) { |state| state == 'failed' } end -def get_units_by_activity(units, active) +def filter_units(units) return [] if units.empty? - units = units.to_a - is_active = `systemctl --user is-active #{units.shelljoin}`.split + states = systemctl('is-active', *units).split + units.select.with_index { |_, i| yield states[i] } +end + +def get_oneshot_services(units) + return [] if units.empty? + types = systemctl('show', '-p', 'Type', *units).split units.select.with_index do |_, i| - (is_active[i] == 'active') == active + types[i] == 'Type=oneshot' end end def get_restricted_units(units) - units = units.to_a - infos = `systemctl --user show -p RefuseManualStart -p RefuseManualStop #{units.shelljoin}` + infos = systemctl('show', '-p', 'RefuseManualStart', '-p', 'RefuseManualStop', *units) .split("\n\n") - no_restart = [] no_manual_start = [] no_manual_stop = [] infos.zip(units).each do |info, unit| @@ -164,14 +178,9 @@ def get_restricted_units(units) no_manual_start << unit if no_start.end_with?('yes') no_manual_stop << unit if no_stop.end_with?('yes') end - # Regular expression that indicates that a service should not be - # restarted even if a change has been detected. - restartRe = /^[ \t]*X-RestartIfChanged[ \t]*=[ \t]*false[ \t]*(?:#.*)?$/ - units.each do |unit| - if `systemctl --user cat #{unit.shellescape}` =~ restartRe - no_restart << unit - end - end + # Get units that should not be restarted even if a change has been detected. + no_restart_regexp = /^\s*X-RestartIfChanged\s*=\s*false\b/ + no_restart = units.select { |unit| systemctl('cat', unit) =~ no_restart_regexp } [no_manual_start, no_manual_stop, no_restart] end @@ -180,13 +189,13 @@ def wait_and_get_failed_services(services, start_timeout_ms) # Force the previous message to always be visible before sleeping STDOUT.flush sleep(start_timeout_ms / 1000.0) - get_inactive_units(services) + get_failed_units(services) end def show_failed_services_status(services) puts services.each do |service| - run_cmd("systemctl --user status #{service.shellescape}") + run_cmd('systemctl', '--user', 'status', service) puts end end @@ -200,4 +209,8 @@ def print_service_msg(msg, services) end end +def notify_skipped_units(no_restart) + puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty? +end + setup_services(*ARGV) diff --git a/home-manager/modules/systemd.nix b/home-manager/modules/systemd.nix index 56164020577..66fffadf737 100644 --- a/home-manager/modules/systemd.nix +++ b/home-manager/modules/systemd.nix @@ -228,7 +228,7 @@ in # running this from the NixOS module then XDG_RUNTIME_DIR is not # set and systemd commands will fail. We'll therefore have to # set it ourselves in that case. - home.activation.reloadSystemD = hm.dag.entryAfter ["linkGeneration"] ( + home.activation.reloadSystemd = hm.dag.entryAfter ["linkGeneration"] ( let autoReloadCmd = '' ${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \ @@ -249,7 +249,7 @@ in if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then if [[ $systemdStatus == 'degraded' ]]; then warnEcho "The user systemd session is degraded:" - ${systemctl} --user --state=failed + ${systemctl} --user --no-pager --state=failed warnEcho "Attempting to reload services anyway..." fi diff --git a/home-manager/modules/targets/darwin.nix b/home-manager/modules/targets/darwin.nix new file mode 100644 index 00000000000..cd7d8e289cf --- /dev/null +++ b/home-manager/modules/targets/darwin.nix @@ -0,0 +1,16 @@ +{ config, lib, pkgs, ... }: + +{ + # Disabled for now due to conflicting behavior with nix-darwin. See + # https://github.com/rycee/home-manager/issues/1341#issuecomment-687286866 + config = lib.mkIf (false && pkgs.stdenv.hostPlatform.isDarwin) { + # Install MacOS applications to the user environment. + home.file."Applications/Home Manager Apps".source = let + apps = pkgs.buildEnv { + name = "home-manager-applications"; + paths = config.home.packages; + pathsToLink = "/Applications"; + }; + in "${apps}/Applications"; + }; +} diff --git a/home-manager/modules/targets/generic-linux.nix b/home-manager/modules/targets/generic-linux.nix new file mode 100644 index 00000000000..47fcc87b3c0 --- /dev/null +++ b/home-manager/modules/targets/generic-linux.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + profileDirectory = config.home.profileDirectory; + +in { + options.targets.genericLinux = { + enable = mkEnableOption "" // { + description = '' + Whether to enable settings that make Home Manager work better on + GNU/Linux distributions other than NixOS. + ''; + }; + + extraXdgDataDirs = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "/usr/share" "/usr/local/share" ]; + description = '' + List of directory names to add to <envar>XDG_DATA_DIRS</envar>. + ''; + }; + }; + + config = mkIf config.targets.genericLinux.enable { + home.sessionVariables = let + profiles = + [ "\${NIX_STATE_DIR:-/nix/var/nix}/profiles/default" profileDirectory ]; + dataDirs = concatStringsSep ":" + (map (profile: "${profile}/share") profiles + ++ config.targets.genericLinux.extraXdgDataDirs); + in { XDG_DATA_DIRS = "${dataDirs}\${XDG_DATA_DIRS:+:}$XDG_DATA_DIRS"; }; + + home.sessionVariablesExtra = '' + . "${pkgs.nix}/etc/profile.d/nix.sh" + ''; + + # We need to source both nix.sh and hm-session-vars.sh as noted in + # https://github.com/rycee/home-manager/pull/797#issuecomment-544783247 + programs.bash.initExtra = '' + . "${pkgs.nix}/etc/profile.d/nix.sh" + . "${profileDirectory}/etc/profile.d/hm-session-vars.sh" + ''; + + systemd.user.sessionVariables = { + NIX_PATH = "$HOME/.nix-defexpr/channels\${NIX_PATH:+:}$NIX_PATH"; + }; + }; +} diff --git a/home-manager/modules/xresources.nix b/home-manager/modules/xresources.nix index b74d671befb..dc59e50c46e 100644 --- a/home-manager/modules/xresources.nix +++ b/home-manager/modules/xresources.nix @@ -28,7 +28,11 @@ in { options = { xresources.properties = mkOption { - type = types.nullOr types.attrs; + type = with types; + let + prim = either bool (either int str); + entry = either prim (listOf prim); + in nullOr (attrsOf entry); default = null; example = literalExample '' { @@ -71,17 +75,18 @@ in { }; }; - config = mkIf (cfg.properties != null || cfg.extraConfig != "") { - home.file.".Xresources" = { - text = concatStringsSep "\n" ([ ] - ++ optional (cfg.extraConfig != "") cfg.extraConfig - ++ optionals (cfg.properties != null) - (mapAttrsToList formatLine cfg.properties)) + "\n"; - onChange = '' - if [[ -v DISPLAY ]] ; then - $DRY_RUN_CMD ${pkgs.xorg.xrdb}/bin/xrdb -merge $HOME/.Xresources - fi - ''; + config = mkIf ((cfg.properties != null && cfg.properties != { }) + || cfg.extraConfig != "") { + home.file.".Xresources" = { + text = concatStringsSep "\n" ([ ] + ++ optional (cfg.extraConfig != "") cfg.extraConfig + ++ optionals (cfg.properties != null) + (mapAttrsToList formatLine cfg.properties)) + "\n"; + onChange = '' + if [[ -v DISPLAY ]] ; then + $DRY_RUN_CMD ${pkgs.xorg.xrdb}/bin/xrdb -merge $HOME/.Xresources + fi + ''; + }; }; - }; } |