diff options
Diffstat (limited to 'home-manager/modules')
206 files changed, 26832 insertions, 0 deletions
diff --git a/home-manager/modules/accounts/email.nix b/home-manager/modules/accounts/email.nix new file mode 100644 index 00000000000..1e7aff94611 --- /dev/null +++ b/home-manager/modules/accounts/email.nix @@ -0,0 +1,396 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.accounts.email; + + gpgModule = types.submodule { + options = { + key = mkOption { + type = types.str; + description = '' + The key to use as listed in <command>gpg --list-keys</command>. + ''; + }; + + signByDefault = mkOption { + type = types.bool; + default = false; + description = "Sign messages by default."; + }; + + encryptByDefault = mkOption { + type = types.bool; + default = false; + description = "Encrypt outgoing messages by default."; + }; + }; + }; + + signatureModule = types.submodule { + options = { + text = mkOption { + type = types.str; + default = ""; + example = '' + -- + Luke Skywalker + May the force be with you. + ''; + description = '' + Signature content. + ''; + }; + + showSignature = mkOption { + type = types.enum [ "append" "attach" "none" ]; + default = "none"; + description = "Method to communicate the signature."; + }; + }; + }; + + tlsModule = types.submodule { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable TLS/SSL. + ''; + }; + + useStartTls = mkOption { + type = types.bool; + default = false; + description = '' + Whether to use STARTTLS. + ''; + }; + + certificatesFile = mkOption { + type = types.path; + default = config.accounts.email.certificatesFile; + defaultText = "config.accounts.email.certificatesFile"; + description = '' + Path to file containing certificate authorities that should + be used to validate the connection authenticity. If + <literal>null</literal> then the system default is used. + Note, if set then the system default may still be accepted. + ''; + }; + }; + }; + + imapModule = types.submodule { + options = { + host = mkOption { + type = types.str; + example = "imap.example.org"; + description = '' + Hostname of IMAP server. + ''; + }; + + port = mkOption { + type = types.nullOr types.port; + default = null; + example = 993; + description = '' + The port on which the IMAP server listens. If + <literal>null</literal> then the default port is used. + ''; + }; + + tls = mkOption { + type = tlsModule; + default = { }; + description = '' + Configuration for secure connections. + ''; + }; + }; + }; + + smtpModule = types.submodule { + options = { + host = mkOption { + type = types.str; + example = "smtp.example.org"; + description = '' + Hostname of SMTP server. + ''; + }; + + port = mkOption { + type = types.nullOr types.port; + default = null; + example = 465; + description = '' + The port on which the SMTP server listens. If + <literal>null</literal> then the default port is used. + ''; + }; + + tls = mkOption { + type = tlsModule; + default = { }; + description = '' + Configuration for secure connections. + ''; + }; + }; + }; + + maildirModule = types.submodule ({ config, ... }: { + options = { + path = mkOption { + type = types.str; + description = '' + Path to maildir directory where mail for this account is + stored. This is relative to the base maildir path. + ''; + }; + + absPath = mkOption { + type = types.path; + readOnly = true; + internal = true; + default = "${cfg.maildirBasePath}/${config.path}"; + description = '' + A convenience option whose value is the absolute path of + this maildir. + ''; + }; + }; + }); + + mailAccountOpts = { name, config, ... }: { + options = { + name = mkOption { + type = types.str; + readOnly = true; + description = '' + Unique identifier of the account. This is set to the + attribute name of the account configuration. + ''; + }; + + primary = mkOption { + type = types.bool; + default = false; + description = '' + Whether this is the primary account. Only one account may be + set as primary. + ''; + }; + + flavor = mkOption { + type = types.enum [ "plain" "gmail.com" "runbox.com" ]; + default = "plain"; + description = '' + Some email providers have peculiar behavior that require + special treatment. This option is therefore intended to + indicate the nature of the provider. + </para><para> + When this indicates a specific provider then, for example, + the IMAP and SMTP server configuration may be set + automatically. + ''; + }; + + address = mkOption { + type = types.strMatching ".*@.*"; + example = "jane.doe@example.org"; + description = "The email address of this account."; + }; + + aliases = mkOption { + type = types.listOf (types.strMatching ".*@.*"); + default = [ ]; + example = [ "webmaster@example.org" "admin@example.org" ]; + description = "Alternative email addresses of this account."; + }; + + realName = mkOption { + type = types.str; + example = "Jane Doe"; + description = "Name displayed when sending mails."; + }; + + userName = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The server username of this account. This will be used as + the SMTP and IMAP user name. + ''; + }; + + passwordCommand = mkOption { + type = types.nullOr (types.either types.str (types.listOf types.str)); + default = null; + apply = p: if isString p then splitString " " p else p; + example = "secret-tool lookup email me@example.org"; + description = '' + A command, which when run writes the account password on + standard output. + ''; + }; + + folders = mkOption { + type = types.submodule { + options = { + inbox = mkOption { + type = types.str; + default = "Inbox"; + description = '' + Relative path of the inbox mail. + ''; + }; + + sent = mkOption { + type = types.nullOr types.str; + default = "Sent"; + description = '' + Relative path of the sent mail folder. + ''; + }; + + drafts = mkOption { + type = types.str; + default = "Drafts"; + description = '' + Relative path of the drafts mail folder. + ''; + }; + + trash = mkOption { + type = types.str; + default = "Trash"; + description = '' + Relative path of the deleted mail folder. + ''; + }; + }; + }; + default = { }; + description = '' + Standard email folders. + ''; + }; + + imap = mkOption { + type = types.nullOr imapModule; + default = null; + description = '' + The IMAP configuration to use for this account. + ''; + }; + + signature = mkOption { + type = signatureModule; + default = { }; + description = '' + Signature configuration. + ''; + }; + + gpg = mkOption { + type = types.nullOr gpgModule; + default = null; + description = '' + GPG configuration. + ''; + }; + + smtp = mkOption { + type = types.nullOr smtpModule; + default = null; + description = '' + The SMTP configuration to use for this account. + ''; + }; + + maildir = mkOption { + type = types.nullOr maildirModule; + defaultText = { path = "\${name}"; }; + description = '' + Maildir configuration for this account. + ''; + }; + }; + + config = mkMerge [ + { + name = name; + maildir = mkOptionDefault { path = "${name}"; }; + } + + (mkIf (config.flavor == "gmail.com") { + userName = mkDefault config.address; + + imap = { host = "imap.gmail.com"; }; + + smtp = { + host = "smtp.gmail.com"; + port = if config.smtp.tls.useStartTls then 587 else 465; + }; + }) + + (mkIf (config.flavor == "runbox.com") { + imap = { host = "mail.runbox.com"; }; + + smtp = { host = "mail.runbox.com"; }; + }) + ]; + }; + +in { + options.accounts.email = { + certificatesFile = mkOption { + type = types.path; + default = "/etc/ssl/certs/ca-certificates.crt"; + description = '' + Path to default file containing certificate authorities that + should be used to validate the connection authenticity. This + path may be overridden on a per-account basis. + ''; + }; + + maildirBasePath = mkOption { + type = types.str; + default = "${config.home.homeDirectory}/Maildir"; + defaultText = "$HOME/Maildir"; + apply = 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 + directory. + ''; + }; + + accounts = mkOption { + type = types.attrsOf (types.submodule mailAccountOpts); + default = { }; + description = "List of email 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); + }) + ]; + }; +} diff --git a/home-manager/modules/default.nix b/home-manager/modules/default.nix new file mode 100644 index 00000000000..7f3494e4dea --- /dev/null +++ b/home-manager/modules/default.nix @@ -0,0 +1,62 @@ +{ configuration +, pkgs +, lib ? pkgs.stdenv.lib + + # Whether to check that each option has a matching declaration. +, check ? true +}: + +with lib; + +let + + collectFailed = cfg: + map (x: x.message) (filter (x: !x.assertion) cfg.assertions); + + showWarnings = res: + let + f = w: x: builtins.trace "[1;31mwarning: ${w}[0m" x; + in + fold f res res.config.warnings; + + extendedLib = import ./lib/stdlib-extended.nix pkgs.lib; + + hmModules = + import ./modules.nix { + inherit check pkgs; + lib = extendedLib; + }; + + rawModule = extendedLib.evalModules { + modules = [ configuration ] ++ hmModules; + specialArgs = { + modulesPath = builtins.toString ./.; + }; + }; + + module = showWarnings ( + let + failed = collectFailed rawModule.config; + failedStr = concatStringsSep "\n" (map (x: "- ${x}") failed); + in + if failed == [] + then rawModule + else throw "\nFailed assertions:\n${failedStr}" + ); + +in + +{ + inherit (module) options config; + + activationPackage = module.config.home.activationPackage; + + # For backwards compatibility. Please use activationPackage instead. + activation-script = module.config.home.activationPackage; + + newsDisplay = rawModule.config.news.display; + newsEntries = + sort (a: b: a.time > b.time) ( + filter (a: a.condition) rawModule.config.news.entries + ); +} diff --git a/home-manager/modules/files.nix b/home-manager/modules/files.nix new file mode 100644 index 00000000000..09ecf715497 --- /dev/null +++ b/home-manager/modules/files.nix @@ -0,0 +1,331 @@ +{ pkgs, config, lib, ... }: + +with lib; + +let + + cfg = config.home.file; + + homeDirectory = config.home.homeDirectory; + + fileType = (import lib/file-type.nix { + inherit homeDirectory lib pkgs; + }).fileType; + + sourceStorePath = file: + let + sourcePath = toString file.source; + sourceName = config.lib.strings.storeFileName (baseNameOf sourcePath); + in + if builtins.hasContext sourcePath + then file.source + else builtins.path { path = file.source; name = sourceName; }; + +in + +{ + options = { + home.file = mkOption { + description = "Attribute set of files to link into the user home."; + default = {}; + type = fileType "<envar>HOME</envar>" homeDirectory; + }; + + home-files = mkOption { + type = types.package; + internal = true; + description = "Package to contain all home files"; + }; + }; + + 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} + + # A symbolic link whose target path matches this pattern will be + # 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" + + 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" + if [[ -e "$backup" ]]; then + errorEcho "Existing file '$backup' would be clobbered by backing up '$targetPath'" + collision=1 + else + 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 of '$sourcePath'" + collision=1 + fi + fi + done + + if [[ -v collision ]] ; then + errorEcho "Please move the above files and try again or use -b <ext> to move automatically." + exit 1 + fi + ''; + in + '' + function checkNewGenCollision() { + local newGenFiles + newGenFiles="$(readlink -e "$newGenPath/home-files")" + find "$newGenFiles" \( -type f -or -type l \) \ + -exec bash ${check} "$newGenFiles" {} + + } + + checkNewGenCollision || exit 1 + '' + ); + + # This activation script will + # + # 1. Remove files from the old generation that are not in the new + # generation. + # + # 2. Switch over the Home Manager gcroot and current profile + # links. + # + # 3. Symlink files from the new generation into $HOME. + # + # This order is needed to ensure that we always know which links + # belong to which generation. Specifically, if we're moving from + # generation A to generation B having sets of home file links FA + # and FB, respectively then cleaning before linking produces state + # transitions similar to + # + # FA → FA ∩ FB → (FA ∩ FB) ∪ FB = FB + # + # and a failure during the intermediate state FA ∩ FB will not + # result in lost links because this set of links are in both the + # source and target generation. + home.activation.linkGeneration = hm.dag.entryAfter ["writeBoundary"] ( + let + link = pkgs.writeText "link" '' + newGenFiles="$1" + shift + for sourcePath in "$@" ; do + relativePath="''${sourcePath#$newGenFiles/}" + targetPath="$HOME/$relativePath" + if [[ -e "$targetPath" && ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then + backup="$targetPath.$HOME_MANAGER_BACKUP_EXT" + $DRY_RUN_CMD mv $VERBOSE_ARG "$targetPath" "$backup" || errorEcho "Moving '$targetPath' failed!" + fi + $DRY_RUN_CMD mkdir -p $VERBOSE_ARG "$(dirname "$targetPath")" + $DRY_RUN_CMD ln -nsf $VERBOSE_ARG "$sourcePath" "$targetPath" + done + ''; + + cleanup = pkgs.writeText "cleanup" '' + . ${./lib-bash/color-echo.sh} + + # A symbolic link whose target path matches this pattern will be + # considered part of a Home Manager generation. + homeFilePattern="$(readlink -e "${builtins.storeDir}")/*-home-manager-files/*" + + newGenFiles="$1" + shift 1 + for relativePath in "$@" ; do + targetPath="$HOME/$relativePath" + if [[ -e "$newGenFiles/$relativePath" ]] ; then + $VERBOSE_ECHO "Checking $targetPath: exists" + elif [[ ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then + 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" + + # Recursively delete empty parent directories. + targetDir="$(dirname "$relativePath")" + if [[ "$targetDir" != "." ]] ; then + pushd "$HOME" > /dev/null + + # Call rmdir with a relative path excluding $HOME. + # Otherwise, it might try to delete $HOME and exit + # with a permission error. + $DRY_RUN_CMD rmdir $VERBOSE_ARG \ + -p --ignore-fail-on-non-empty \ + "$targetDir" + + popd > /dev/null + fi + fi + done + ''; + in + '' + function linkNewGen() { + echo "Creating home file links in $HOME" + + local newGenFiles + newGenFiles="$(readlink -e "$newGenPath/home-files")" + find "$newGenFiles" \( -type f -or -type l \) \ + -exec bash ${link} "$newGenFiles" {} + + } + + function cleanOldGen() { + if [[ ! -v oldGenPath ]] ; then + return + fi + + echo "Cleaning up orphan links from $HOME" + + local newGenFiles oldGenFiles + newGenFiles="$(readlink -e "$newGenPath/home-files")" + oldGenFiles="$(readlink -e "$oldGenPath/home-files")" + + # Apply the cleanup script on each leaf in the old + # generation. The find command below will print the + # relative path of the entry. + find "$oldGenFiles" '(' -type f -or -type l ')' -printf '%P\0' \ + | xargs -0 bash ${cleanup} "$newGenFiles" + } + + cleanOldGen + + if [[ ! -v oldGenPath || "$oldGenPath" != "$newGenPath" ]] ; then + echo "Creating profile generation $newGenNum" + $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" + fi + + linkNewGen + '' + ); + + home.activation.checkFilesChanged = hm.dag.entryBefore ["linkGeneration"] ( + '' + declare -A changedFiles + '' + concatMapStrings (v: '' + cmp --quiet "${sourceStorePath v}" "${homeDirectory}/${v.target}" \ + && changedFiles["${v.target}"]=0 \ + || changedFiles["${v.target}"]=1 + '') (filter (v: v.onChange != "") (attrValues cfg)) + ); + + home.activation.onFilesChange = hm.dag.entryAfter ["linkGeneration"] ( + concatMapStrings (v: '' + if [[ ${"$\{changedFiles"}["${v.target}"]} -eq 1 ]]; then + ${v.onChange} + fi + '') (filter (v: v.onChange != "") (attrValues cfg)) + ); + + # Symlink directories and files that have the right execute bit. + # Copy files that need their execute bit changed. + home-files = pkgs.runCommand + "home-manager-files" + { + nativeBuildInputs = [ pkgs.xorg.lndir ]; + preferLocalBuild = true; + allowSubstitutes = false; + } + ('' + mkdir -p $out + + # Needed in case /nix is a symbolic link. + realOut="$(realpath -m "$out")" + + function insertFile() { + local source="$1" + local relTarget="$2" + local executable="$3" + local recursive="$4" + + # Figure out the real absolute path to the target. + local target + target="$(realpath -m "$realOut/$relTarget")" + + # Target path must be within $HOME. + if [[ ! $target == $realOut* ]] ; then + echo "Error installing file '$relTarget' outside \$HOME" >&2 + exit 1 + fi + + mkdir -p "$(dirname "$target")" + if [[ -d $source ]]; then + if [[ $recursive ]]; then + mkdir -p "$target" + lndir -silent "$source" "$target" + else + ln -s "$source" "$target" + fi + else + [[ -x $source ]] && isExecutable=1 || isExecutable="" + + # Link the file into the home file directory if possible, + # i.e., if the executable bit of the source is the same we + # expect for the target. Otherwise, we copy the file and + # set the executable bit to the expected value. + if [[ $executable == inherit || $isExecutable == $executable ]]; then + ln -s "$source" "$target" + else + cp "$source" "$target" + + if [[ $executable == inherit ]]; then + # Don't change file mode if it should match the source. + : + elif [[ $executable ]]; then + chmod +x "$target" + else + chmod -x "$target" + fi + fi + fi + } + '' + concatStrings ( + mapAttrsToList (n: v: '' + 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 new file mode 100644 index 00000000000..2947433993e --- /dev/null +++ b/home-manager/modules/home-environment.nix @@ -0,0 +1,564 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.home; + + languageSubModule = types.submodule { + options = { + base = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use unless overridden by a more specific option. + ''; + }; + + ctype = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + 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). + ''; + }; + + monetary = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for formatting currencies and money amounts. + ''; + }; + + 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; + description = '' + The language to use for paper sizes. + ''; + }; + + name = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + 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. + ''; + }; + + }; + }; + + keyboardSubModule = types.submodule { + options = { + layout = mkOption { + type = with types; nullOr str; + default = + if versionAtLeast config.home.stateVersion "19.09" + then null + else "us"; + defaultText = literalExample "null"; + description = '' + Keyboard layout. If <literal>null</literal>, then the system + configuration will be used. + </para><para> + This defaults to <literal>null</literal> for state + version ≥ 19.09 and <literal>"us"</literal> otherwise. + ''; + }; + + model = mkOption { + type = with types; nullOr str; + default = null; + example = "presario"; + description = '' + Keyboard model. + ''; + }; + + options = mkOption { + type = types.listOf types.str; + default = []; + example = ["grp:caps_toggle" "grp_led:scroll"]; + description = '' + X keyboard options; layout switching goes here. + ''; + }; + + variant = mkOption { + type = with types; nullOr str; + default = + if versionAtLeast config.home.stateVersion "19.09" + then null + else ""; + defaultText = literalExample "null"; + example = "colemak"; + description = '' + X keyboard variant. If <literal>null</literal>, then the + system configuration will be used. + </para><para> + This defaults to <literal>null</literal> for state + version ≥ 19.09 and <literal>""</literal> otherwise. + ''; + }; + }; + }; + +in + +{ + meta.maintainers = [ maintainers.rycee ]; + + imports = [ + (mkRemovedOptionModule [ "home" "sessionVariableSetter" ] '' + Session variables are now always set through the shell. This is + done automatically if the shell configuration is managed by Home + Manager. If not, then you must source the + + ~/.nix-profile/etc/profile.d/hm-session-vars.sh + + file yourself. + '') + ]; + + options = { + home.username = mkOption { + type = types.str; + 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 = 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 { + type = types.path; + defaultText = "~/.nix-profile"; + internal = true; + readOnly = true; + description = '' + The profile directory where Home Manager generations are + installed. + ''; + }; + + home.language = mkOption { + type = languageSubModule; + default = {}; + description = "Language configuration."; + }; + + home.keyboard = mkOption { + type = types.nullOr keyboardSubModule; + default = {}; + description = '' + Keyboard configuration. Set to <literal>null</literal> to + disable Home Manager keyboard management. + ''; + }; + + home.sessionVariables = mkOption { + default = {}; + type = types.attrs; + example = { EDITOR = "emacs"; GS_OPTIONS = "-sPAPERSIZE=a4"; }; + description = '' + Environment variables to always set at login. + </para><para> + The values may refer to other environment variables using + POSIX.2 style variable references. For example, a variable + <varname>parameter</varname> may be referenced as + <code>$parameter</code> or <code>''${parameter}</code>. A + default value <literal>foo</literal> may be given as per + <code>''${parameter:-foo}</code> and, similarly, an alternate + value <literal>bar</literal> can be given as per + <code>''${parameter:+bar}</code>. + </para><para> + Note, these variables may be set in any order so no session + variable may have a runtime dependency on another session + variable. In particular code like + <programlisting language="nix"> + home.sessionVariables = { + FOO = "Hello"; + BAR = "$FOO World!"; + }; + </programlisting> + may not work as expected. If you need to reference another + session variable, then do so inside Nix instead. The above + example then becomes + <programlisting language="nix"> + home.sessionVariables = { + FOO = "Hello"; + BAR = "''${config.home.sessionVariables.FOO} World!"; + }; + </programlisting> + ''; + }; + + 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 = []; + description = "The set of packages to appear in the user environment."; + }; + + home.extraOutputsToInstall = mkOption { + type = types.listOf types.str; + default = []; + example = [ "doc" "info" "devdoc" ]; + description = '' + List of additional package outputs of the packages + <varname>home.packages</varname> that should be installed into + the user environment. + ''; + }; + + home.path = mkOption { + internal = true; + description = "The derivation installing the user packages."; + }; + + home.emptyActivationPath = mkOption { + internal = true; + default = false; + type = types.bool; + description = '' + Whether the activation script should start with an empty + <envar>PATH</envar> variable. When <literal>false</literal> + then the user's <envar>PATH</envar> will be used. + ''; + }; + + home.activation = mkOption { + type = hm.types.dagOf types.str; + default = {}; + example = literalExample '' + { + myActivationAction = lib.hm.dag.entryAfter ["writeBoundary"] ''' + $DRY_RUN_CMD ln -s $VERBOSE_ARG \ + ''${builtins.toPath ./link-me-directly} $HOME + '''; + } + ''; + description = '' + The activation scripts blocks to run when activating a Home + Manager generation. Any entry here should be idempotent, + meaning running twice or more times produces the same result + as running it once. + + </para><para> + + If the script block produces any observable side effect, such + as writing or deleting files, then it + <emphasis>must</emphasis> be placed after the special + <literal>writeBoundary</literal> script block. Prior to the + write boundary one can place script blocks that verifies, but + does not modify, the state of the system and exits if an + unexpected state is found. For example, the + <literal>checkLinkTargets</literal> script block checks for + collisions between non-managed files and files defined in + <varname><link linkend="opt-home.file">home.file</link></varname>. + + </para><para> + + A script block should respect the <varname>DRY_RUN</varname> + variable, if it is set then the actions taken by the script + should be logged to standard out and not actually performed. + The variable <varname>DRY_RUN_CMD</varname> is set to + <command>echo</command> if dry run is enabled. + + </para><para> + + A script block should also respect the + <varname>VERBOSE</varname> variable, and if set print + information on standard out that may be useful for debugging + any issue that may arise. The variable + <varname>VERBOSE_ARG</varname> is set to + <option>--verbose</option> if verbose output is enabled. + ''; + }; + + home.activationPackage = mkOption { + internal = true; + type = types.package; + description = "The package containing the complete activation script."; + }; + + home.extraBuilderCommands = mkOption { + type = types.lines; + default = ""; + internal = true; + description = '' + Extra commands to run in the Home Manager generation builder. + ''; + }; + + home.extraProfileCommands = mkOption { + type = types.lines; + default = ""; + internal = true; + description = '' + Extra commands to run in the Home Manager profile builder. + ''; + }; + }; + + config = { + assertions = [ + { + assertion = config.home.username != ""; + message = "Username could not be determined"; + } + { + assertion = config.home.homeDirectory != ""; + message = "Home directory could not be determined"; + } + ]; + + 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 "/etc/profiles/per-user/${cfg.username}" + else cfg.homeDirectory + "/.nix-profile"; + + home.sessionVariables = + let + maybeSet = n: v: optionalAttrs (v != null) { ${n} = v; }; + in + (maybeSet "LANG" cfg.language.base) + // + (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_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. + ( + pkgs.writeTextFile { + name = "hm-session-vars.sh"; + destination = "/etc/profile.d/hm-session-vars.sh"; + text = '' + # Only source this once. + if [ -n "$__HM_SESS_VARS_SOURCED" ]; then return; fi + export __HM_SESS_VARS_SOURCED=1 + + ${config.lib.shell.exportAll cfg.sessionVariables} + '' + cfg.sessionVariablesExtra; + } + ) + ]; + + # A dummy entry acting as a boundary between the activation + # script's "check" and the "write" phases. + home.activation.writeBoundary = hm.dag.entryAnywhere ""; + + # Install packages to the user environment. + # + # Note, sometimes our target may not allow modification of the Nix + # store and then we cannot rely on `nix-env -i`. This is the case, + # for example, if we are running as a NixOS module and building a + # virtual machine. Then we must instead rely on an external + # mechanism for installing packages, which in NixOS is provided by + # the `users.users.<name?>.packages` option. The activation + # command is still needed since some modules need to run their + # activation commands after the packages are guaranteed to be + # installed. + # + # In case the user has moved from a user-install of Home Manager + # to a submodule managed one we attempt to uninstall the + # `home-manager-path` package if it is installed. + home.activation.installPackages = hm.dag.entryAfter ["writeBoundary"] ( + if config.submoduleSupport.externalPackageInstall + then + '' + if nix-env -q | grep '^home-manager-path$'; then + $DRY_RUN_CMD nix-env -e home-manager-path + fi + '' + else + '' + $DRY_RUN_CMD nix-env -i ${cfg.path} + '' + ); + + home.activationPackage = + let + mkCmd = res: '' + noteEcho Activating ${res.name} + ${res.data} + ''; + sortedCommands = hm.dag.topoSort cfg.activation; + activationCmds = + if sortedCommands ? result then + concatStringsSep "\n" (map mkCmd sortedCommands.result) + else + abort ("Dependency cycle in activation script: " + + builtins.toJSON sortedCommands); + + # Programs that always should be available on the activation + # script's PATH. + activationBinPaths = lib.makeBinPath [ + pkgs.bash + pkgs.coreutils + pkgs.diffutils # For `cmp` and `diff`. + pkgs.findutils + pkgs.gnugrep + pkgs.gnused + pkgs.ncurses # For `tput`. + ] + + optionalString (!cfg.emptyActivationPath) "\${PATH:+:}$PATH"; + + activationScript = pkgs.writeScript "activation-script" '' + #!${pkgs.runtimeShell} + + set -eu + set -o pipefail + + cd $HOME + + export PATH="${activationBinPaths}" + + . ${./lib-bash/color-echo.sh} + + ${builtins.readFile ./lib-bash/activation-init.sh} + + ${activationCmds} + ''; + in + pkgs.runCommand + "home-manager-generation" + { + preferLocalBuild = true; + allowSubstitutes = false; + } + '' + mkdir -p $out + + cp ${activationScript} $out/activate + + substituteInPlace $out/activate \ + --subst-var-by GENERATION_DIR $out + + ln -s ${config.home-files} $out/home-files + ln -s ${cfg.path} $out/home-path + + ${cfg.extraBuilderCommands} + ''; + + home.path = pkgs.buildEnv { + name = "home-manager-path"; + + paths = cfg.packages; + inherit (cfg) extraOutputsToInstall; + + postBuild = cfg.extraProfileCommands; + + meta = { + description = "Environment of packages installed through home-manager"; + }; + }; + }; +} diff --git a/home-manager/modules/lib-bash/activation-init.sh b/home-manager/modules/lib-bash/activation-init.sh new file mode 100755 index 00000000000..f95008ee75b --- /dev/null +++ b/home-manager/modules/lib-bash/activation-init.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +function setupVars() { + 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=$( \ + nix-env --list-generations --profile "$genProfilePath" \ + | tail -1 \ + | sed -E 's/ *([[:digit:]]+) .*/\1/') + + if [[ -n $greatestGenNum ]] ; then + oldGenNum=$greatestGenNum + newGenNum=$((oldGenNum + 1)) + else + newGenNum=1 + fi + + 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 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" + errorEcho " rm $profilesPath/home-manager*" + errorEcho " rm $gcPath/current-home" + errorEcho "and trying home-manager switch again. Good luck!" + exit 1 + fi +} + +if [[ -v VERBOSE ]]; then + export VERBOSE_ECHO=echo + export VERBOSE_ARG="--verbose" +else + export VERBOSE_ECHO=true + export VERBOSE_ARG="" +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 + echo "This is a dry run" + export DRY_RUN_CMD=echo +else + $VERBOSE_ECHO "This is a live run" + export DRY_RUN_CMD="" +fi + +if [[ -v VERBOSE ]]; then + echo -n "Using Nix version: " + nix-env --version +fi + +$VERBOSE_ECHO "Activation variables:" +if [[ -v oldGenNum ]] ; then + $VERBOSE_ECHO " oldGenNum=$oldGenNum" + $VERBOSE_ECHO " oldGenPath=$oldGenPath" +else + $VERBOSE_ECHO " oldGenNum undefined (first run?)" + $VERBOSE_ECHO " oldGenPath undefined (first run?)" +fi +$VERBOSE_ECHO " newGenPath=$newGenPath" +$VERBOSE_ECHO " newGenNum=$newGenNum" +$VERBOSE_ECHO " newGenGcPath=$newGenGcPath" +$VERBOSE_ECHO " genProfilePath=$genProfilePath" diff --git a/home-manager/modules/lib-bash/color-echo.sh b/home-manager/modules/lib-bash/color-echo.sh new file mode 100644 index 00000000000..ef708b29c4d --- /dev/null +++ b/home-manager/modules/lib-bash/color-echo.sh @@ -0,0 +1,37 @@ +# The check for terminal output and color support is heavily inspired +# by https://unix.stackexchange.com/a/10065. + +function setupColors() { + normalColor="" + errorColor="" + warnColor="" + noteColor="" + + # Check if stdout is a terminal. + if [[ -t 1 ]]; then + # See if it supports colors. + local ncolors + ncolors=$(tput colors) + + if [[ -n "$ncolors" && "$ncolors" -ge 8 ]]; then + normalColor="$(tput sgr0)" + errorColor="$(tput bold)$(tput setaf 1)" + warnColor="$(tput setaf 3)" + noteColor="$(tput bold)$(tput setaf 6)" + fi + fi +} + +setupColors + +function errorEcho() { + echo "${errorColor}$*${normalColor}" +} + +function warnEcho() { + echo "${warnColor}$*${normalColor}" +} + +function noteEcho() { + echo "${noteColor}$*${normalColor}" +} diff --git a/home-manager/modules/lib/dag.nix b/home-manager/modules/lib/dag.nix new file mode 100644 index 00000000000..cbe34129652 --- /dev/null +++ b/home-manager/modules/lib/dag.nix @@ -0,0 +1,117 @@ +# A generalization of Nixpkgs's `strings-with-deps.nix`. +# +# The main differences from the Nixpkgs version are +# +# - not specific to strings, i.e., any payload is OK, +# +# - the addition of the function `dagEntryBefore` indicating a +# "wanted by" relationship. + +{ lib }: + +with lib; + +rec { + + emptyDag = { }; + + isDag = dag: + let isEntry = e: (e ? data) && (e ? after) && (e ? before); + in builtins.isAttrs dag && all (x: x) (mapAttrsToList (n: isEntry) dag); + + # Takes an attribute set containing entries built by + # dagEntryAnywhere, dagEntryAfter, and dagEntryBefore to a + # topologically sorted list of entries. + # + # Internally this function uses the `toposort` function in + # `<nixpkgs/lib/lists.nix>` and its value is accordingly. + # + # Specifically, the result on success is + # + # { result = [{name = ?; data = ?;} …] } + # + # For example + # + # nix-repl> dagTopoSort { + # a = dagEntryAnywhere "1"; + # b = dagEntryAfter ["a" "c"] "2"; + # c = dagEntryBefore ["d"] "3"; + # d = dagEntryBefore ["e"] "4"; + # e = dagEntryAnywhere "5"; + # } == { + # result = [ + # { data = "1"; name = "a"; } + # { data = "3"; name = "c"; } + # { data = "2"; name = "b"; } + # { data = "4"; name = "d"; } + # { data = "5"; name = "e"; } + # ]; + # } + # true + # + # And the result on error is + # + # { + # cycle = [ {after = ?; name = ?; data = ?} … ]; + # loops = [ {after = ?; name = ?; data = ?} … ]; + # } + # + # For example + # + # nix-repl> dagTopoSort { + # a = dagEntryAnywhere "1"; + # b = dagEntryAfter ["a" "c"] "2"; + # c = dagEntryAfter ["d"] "3"; + # d = dagEntryAfter ["b"] "4"; + # e = dagEntryAnywhere "5"; + # } == { + # cycle = [ + # { after = ["a" "c"]; data = "2"; name = "b"; } + # { after = ["d"]; data = "3"; name = "c"; } + # { after = ["b"]; data = "4"; name = "d"; } + # ]; + # loops = [ + # { after = ["a" "c"]; data = "2"; name = "b"; } + # ]; + # } == {} + # true + dagTopoSort = dag: + let + dagBefore = dag: name: + mapAttrsToList (n: v: n) + (filterAttrs (n: v: any (a: a == name) v.before) dag); + normalizedDag = mapAttrs (n: v: { + name = n; + data = v.data; + after = v.after ++ dagBefore dag n; + }) dag; + before = a: b: any (c: a.name == c) b.after; + sorted = toposort before (mapAttrsToList (n: v: v) normalizedDag); + in if sorted ? result then { + result = map (v: { inherit (v) name data; }) sorted.result; + } else + sorted; + + # Applies a function to each element of the given DAG. + dagMap = f: dag: mapAttrs (n: v: v // { data = f n v.data; }) dag; + + # Create a DAG entry with no particular dependency information. + dagEntryAnywhere = data: { + inherit data; + before = [ ]; + after = [ ]; + }; + + dagEntryBetween = before: after: data: { inherit data before after; }; + + dagEntryAfter = after: data: { + inherit data after; + before = [ ]; + }; + + dagEntryBefore = before: data: { + inherit data before; + after = [ ]; + }; + +} diff --git a/home-manager/modules/lib/default.nix b/home-manager/modules/lib/default.nix new file mode 100644 index 00000000000..7c2c72f709c --- /dev/null +++ b/home-manager/modules/lib/default.nix @@ -0,0 +1,26 @@ +{ lib }: + +rec { + dag = + let + d = import ./dag.nix { inherit lib; }; + in + { + empty = d.emptyDag; + isDag = d.isDag; + topoSort = d.dagTopoSort; + map = d.dagMap; + entryAnywhere = d.dagEntryAnywhere; + entryBetween = d.dagEntryBetween; + entryAfter = d.dagEntryAfter; + 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 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 new file mode 100644 index 00000000000..56a3a1286a0 --- /dev/null +++ b/home-manager/modules/lib/file-type.nix @@ -0,0 +1,112 @@ +{ homeDirectory, lib, pkgs }: + +with lib; + +{ + # Constructs a type suitable for a `home.file` like option. The + # target path may be either absolute or relative, in which case it + # is relative the `basePath` argument (which itself must be an + # absolute path). + # + # Arguments: + # - basePathDesc docbook compatible description of the base path + # - basePath the file base path + fileType = basePathDesc: basePath: types.attrsOf (types.submodule ( + { name, config, ... }: { + options = { + target = mkOption { + type = types.str; + apply = p: + let + absPath = if hasPrefix "/" p then p else "${basePath}/${p}"; + in + removePrefix (homeDirectory + "/") absPath; + defaultText = literalExample "<name>"; + description = '' + Path to target file relative to ${basePathDesc}. + ''; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + 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 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. + ''; + }; + + executable = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Set the execute bit. If <literal>null</literal>, defaults to the mode + of the <varname>source</varname> file or to <literal>false</literal> + for files created through the <varname>text</varname> option. + ''; + }; + + recursive = mkOption { + type = types.bool; + default = false; + description = '' + If the file source is a directory, then this option + determines whether the directory should be recursively + linked to the target location. This option has no effect + if the source is a file. + </para><para> + If <literal>false</literal> (the default) then the target + will be a symbolic link to the source directory. If + <literal>true</literal> then the target will be a + directory structure matching the source's but whose leafs + are symbolic links to the files of the source directory. + ''; + }; + + onChange = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands to run when file has changed between + generations. The script will be run + <emphasis>after</emphasis> the new files have been linked + 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 = { + target = mkDefault name; + source = mkIf (config.text != null) ( + mkDefault (pkgs.writeTextFile { + inherit (config) executable text; + name = hm.strings.storeFileName name; + }) + ); + }; + } + )); +} 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/shell.nix b/home-manager/modules/lib/shell.nix new file mode 100644 index 00000000000..5e5743f51ea --- /dev/null +++ b/home-manager/modules/lib/shell.nix @@ -0,0 +1,11 @@ +{ lib }: + +rec { + # Produces a Bourne shell like variable export statement. + export = n: v: ''export ${n}="${toString v}"''; + + # Given an attribute set containing shell variable names and their + # assignment, this function produces a string containing an export + # statement for each set entry. + exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars); +} diff --git a/home-manager/modules/lib/stdlib-extended.nix b/home-manager/modules/lib/stdlib-extended.nix new file mode 100644 index 00000000000..93f2397cee8 --- /dev/null +++ b/home-manager/modules/lib/stdlib-extended.nix @@ -0,0 +1,7 @@ +# Just a convenience function that returns the given Nixpkgs standard +# library extended with the HM library. + +nixpkgsLib: + +let mkHmLib = import ./.; +in nixpkgsLib.extend (self: super: { hm = mkHmLib { lib = super; }; }) diff --git a/home-manager/modules/lib/strings.nix b/home-manager/modules/lib/strings.nix new file mode 100644 index 00000000000..fe7b2fa3061 --- /dev/null +++ b/home-manager/modules/lib/strings.nix @@ -0,0 +1,22 @@ +{ lib }: + +with lib; + +{ + # Figures out a valid Nix store name for the given path. + storeFileName = path: + let + # All characters that are considered safe. Note "-" is not + # included to avoid "-" followed by digit being interpreted as a + # version. + safeChars = [ "+" "." "_" "?" "=" ] ++ lowerChars ++ upperChars + ++ stringToCharacters "0123456789"; + + empties = l: genList (x: "") (length l); + + unsafeInName = + stringToCharacters (replaceStrings safeChars (empties safeChars) path); + + safeName = replaceStrings unsafeInName (empties unsafeInName) path; + in "hm_" + safeName; +} diff --git a/home-manager/modules/lib/types-dag.nix b/home-manager/modules/lib/types-dag.nix new file mode 100644 index 00000000000..2efb12645d4 --- /dev/null +++ b/home-manager/modules/lib/types-dag.nix @@ -0,0 +1,99 @@ +{ dag, lib }: + +with lib; + +let + + isDagEntry = e: isAttrs e && (e ? data) && (e ? after) && (e ? before); + + dagContentType = elemType: + 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 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 + maybeConvert = n: v: if isDagEntry v then v else dag.entryAnywhere v; + in map (def: def // { value = mapAttrs maybeConvert def.value; }); + + attrEquivalent = types.attrsOf (dagContentType elemType); + in mkOptionType rec { + name = "dagOf"; + description = "DAG of ${elemType.description}s"; + check = isAttrs; + merge = loc: defs: attrEquivalent.merge loc (convertAllToDags defs); + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<name>" ]); + getSubModules = elemType.getSubModules; + substSubModules = m: dagOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + }; + + # A directed acyclic graph of some inner type OR a list of that + # inner type. This is a temporary hack for use by the + # `programs.ssh.matchBlocks` and is only guaranteed to be vaguely + # correct! + # + # In particular, adding a dependency on one of the "unnamed-N-M" + # entries generated by a list value is almost guaranteed to destroy + # the list's order. + # + # This function will be removed in version 20.09. + listOrDagOf = elemType: + let + paddedIndexStr = list: i: + let padWidth = stringLength (toString (length list)); + in fixedWidthNumber padWidth i; + + convertAll = loc: defs: + let + convertListValue = namePrefix: def: + let + vs = def.value; + pad = paddedIndexStr 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: def: + if isList def.value then + convertListValue "unnamed-${paddedIndexStr defs i}" def + else + def.value; + in imap1 (i: def: def // { value = convertValue i def; }) defs; + + dagType = dagOf elemType; + in mkOptionType rec { + 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 new file mode 100644 index 00000000000..64a6b4a34fa --- /dev/null +++ b/home-manager/modules/lib/types.nix @@ -0,0 +1,90 @@ +{ lib, dag ? import ./dag.nix { inherit lib; } +, gvariant ? import ./gvariant.nix { inherit lib; } }: + +with lib; + +let + + typesDag = import ./types-dag.nix { inherit dag lib; }; + + # 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" + + " containing a selection of the values of the input set"; + check = isFunction; + merge = _loc: defs: as: concatMap (select: select as) (getValues defs); + }; + + overlayFunction = mkOptionType { + name = "overlayFunction"; + 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; + }; + + 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/lib/zsh.nix b/home-manager/modules/lib/zsh.nix new file mode 100644 index 00000000000..c6901350f50 --- /dev/null +++ b/home-manager/modules/lib/zsh.nix @@ -0,0 +1,30 @@ +{ lib }: + +rec { + # Produces a Zsh shell like value + toZshValue = v: + if builtins.isBool v then + if v then "true" else "false" + else if builtins.isString v then + ''"${v}"'' + else if builtins.isList v then + "(${lib.concatStringsSep " " (map toZshValue v)})" + else + ''"${toString v}"''; + + # Produces a Zsh shell like definition statement + define = n: v: "${n}=${toZshValue v}"; + + # Given an attribute set containing shell variable names and their + # assignments, this function produces a string containing a definition + # statement for each set entry. + defineAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList define vars); + + # Produces a Zsh shell like export statement + export = n: v: "export ${define n v}"; + + # Given an attribute set containing shell variable names and their + # assignments, this function produces a string containing an export + # statement for each set entry. + exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars); +} diff --git a/home-manager/modules/manual.nix b/home-manager/modules/manual.nix new file mode 100644 index 00000000000..ab01c45003e --- /dev/null +++ b/home-manager/modules/manual.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.manual; + + docs = import ../doc { inherit lib pkgs; }; + +in + +{ + options = { + manual.html.enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to install the HTML manual. This also installs the + <command>home-manager-help</command> tool, which opens a local + copy of the Home Manager manual in the system web browser. + ''; + }; + + manual.manpages.enable = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to install the configuration manual page. The manual can + be reached by <command>man home-configuration.nix</command>. + </para><para> + When looking at the manual page pretend that all references to + NixOS stuff are actually references to Home Manager stuff. + Thanks! + ''; + }; + + manual.json.enable = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to install a JSON formatted list of all Home Manager + options. This can be located at + <filename><profile directory>/share/doc/home-manager/options.json</filename>, + and may be used for navigating definitions, auto-completing, + and other miscellaneous tasks. + ''; + }; + }; + + config = { + home.packages = mkMerge [ + (mkIf cfg.html.enable [ docs.manual.html docs.manual.htmlOpenTool ]) + (mkIf cfg.manpages.enable [ docs.manPages ]) + (mkIf cfg.json.enable [ docs.options.json ]) + ]; + + # Whether a dependency on nmd should be introduced. + home.extraBuilderCommands = + mkIf (cfg.html.enable || cfg.manpages.enable || cfg.json.enable) '' + mkdir $out/lib + ln -s ${docs.nmdSrc} $out/lib/nmd + ''; + }; + +} diff --git a/home-manager/modules/misc/dconf.nix b/home-manager/modules/misc/dconf.nix new file mode 100644 index 00000000000..5fc7748a76b --- /dev/null +++ b/home-manager/modules/misc/dconf.nix @@ -0,0 +1,73 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.dconf; + + toDconfIni = generators.toINI { mkKeyValue = mkIniKeyValue; }; + + mkIniKeyValue = key: value: + "${key}=${toString (hm.gvariant.mkValue value)}"; + +in + +{ + meta.maintainers = [ maintainers.gnidorah maintainers.rycee ]; + + options = { + dconf = { + enable = mkOption { + type = types.bool; + default = true; + visible = false; + description = '' + Whether to enable dconf settings. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (attrsOf hm.types.gvariant); + default = {}; + example = literalExample '' + { + "org/gnome/calculator" = { + button-mode = "programming"; + show-thousands = true; + base = 10; + word-size = 64; + window-position = lib.hm.gvariant.mkTuple [100 100]; + }; + } + ''; + description = '' + Settings to write to the dconf configuration system. + ''; + }; + }; + }; + + config = mkIf (cfg.enable && cfg.settings != {}) { + home.activation.dconfSettings = hm.dag.entryAfter ["installPackages"] ( + let + iniFile = pkgs.writeText "hm-dconf.ini" (toDconfIni cfg.settings); + in + '' + if [[ -v DBUS_SESSION_BUS_ADDRESS ]]; then + DCONF_DBUS_RUN_SESSION="" + else + DCONF_DBUS_RUN_SESSION="${pkgs.dbus}/bin/dbus-run-session" + fi + + if [[ -v DRY_RUN ]]; then + echo $DCONF_DBUS_RUN_SESSION ${pkgs.dconf}/bin/dconf load / "<" ${iniFile} + else + $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/fontconfig.nix b/home-manager/modules/misc/fontconfig.nix new file mode 100644 index 00000000000..795ab3a74f6 --- /dev/null +++ b/home-manager/modules/misc/fontconfig.nix @@ -0,0 +1,105 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.fonts.fontconfig; + + profileDirectory = config.home.profileDirectory; + +in { + meta.maintainers = [ maintainers.rycee ]; + + imports = [ + (mkRenamedOptionModule [ "fonts" "fontconfig" "enableProfileFonts" ] [ + "fonts" + "fontconfig" + "enable" + ]) + ]; + + options = { + fonts.fontconfig = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable fontconfig configuration. This will, for + example, allow fontconfig to discover fonts and + configurations installed through + <varname>home.packages</varname> and + <command>nix-env</command>. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + # Create two dummy files in /lib/fontconfig to make sure that + # buildEnv creates a real directory path. These files are removed + # in home.extraProfileCommands below so the packages will not + # become "runtime" dependencies. + home.packages = [ + (pkgs.writeTextFile { + name = "hm-dummy1"; + destination = "/lib/fontconfig/hm-dummy1"; + text = "dummy"; + }) + + (pkgs.writeTextFile { + name = "hm-dummy2"; + destination = "/lib/fontconfig/hm-dummy2"; + text = "dummy"; + }) + ]; + + home.extraProfileCommands = '' + if [[ -d $out/lib/X11/fonts || -d $out/share/fonts ]]; then + export FONTCONFIG_FILE="$(pwd)/fonts.conf" + + cat > $FONTCONFIG_FILE << EOF + <?xml version='1.0'?> + <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'> + <fontconfig> + <dir>$out/lib/X11/fonts</dir> + <dir>$out/share/fonts</dir> + <cachedir>$out/lib/fontconfig/cache</cachedir> + </fontconfig> + EOF + + ${getBin pkgs.fontconfig}/bin/fc-cache -f + rm -f $out/lib/fontconfig/cache/CACHEDIR.TAG + rmdir --ignore-fail-on-non-empty -p $out/lib/fontconfig/cache + + rm "$FONTCONFIG_FILE" + unset FONTCONFIG_FILE + fi + + # Remove hacky dummy files. + rm $out/lib/fontconfig/hm-dummy? + rmdir --ignore-fail-on-non-empty -p $out/lib/fontconfig + ''; + + xdg.configFile = { + "fontconfig/conf.d/10-hm-fonts.conf".text = '' + <?xml version='1.0'?> + + <!-- Generated by Home Manager. --> + + <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'> + <fontconfig> + <include ignore_missing="yes">${config.home.path}/etc/fonts/conf.d</include> + <include ignore_missing="yes">${config.home.path}/etc/fonts/fonts.conf</include> + + <dir>${config.home.path}/lib/X11/fonts</dir> + <dir>${config.home.path}/share/fonts</dir> + <dir>${profileDirectory}/lib/X11/fonts</dir> + <dir>${profileDirectory}/share/fonts</dir> + + <cachedir>${config.home.path}/lib/fontconfig/cache</cachedir> + </fontconfig> + ''; + }; + }; +} diff --git a/home-manager/modules/misc/gtk.nix b/home-manager/modules/misc/gtk.nix new file mode 100644 index 00000000000..bf25aaaf664 --- /dev/null +++ b/home-manager/modules/misc/gtk.nix @@ -0,0 +1,164 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.gtk; + cfg2 = config.gtk.gtk2; + cfg3 = config.gtk.gtk3; + + toGtk3Ini = generators.toINI { + mkKeyValue = key: value: + let + 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'}"; + + themeType = types.submodule { + options = { + package = mkOption { + type = types.nullOr types.package; + default = null; + example = literalExample "pkgs.gnome3.gnome_themes_standard"; + description = '' + Package providing the theme. This package will be installed + to your profile. If <literal>null</literal> then the theme + is assumed to already be available in your profile. + ''; + }; + + name = mkOption { + type = types.str; + example = "Adwaita"; + description = "The name of the theme within the package."; + }; + }; + }; + +in { + meta.maintainers = [ maintainers.rycee ]; + + imports = [ + (mkRemovedOptionModule [ "gtk" "gtk3" "waylandSupport" ] '' + This options is not longer needed and can be removed. + '') + ]; + + options = { + gtk = { + enable = mkEnableOption "GTK 2/3 configuration"; + + font = mkOption { + type = types.nullOr hm.types.fontType; + default = null; + description = '' + The font to use in GTK+ 2/3 applications. + ''; + }; + + iconTheme = mkOption { + type = types.nullOr themeType; + default = null; + description = "The icon theme to use."; + }; + + theme = mkOption { + type = types.nullOr themeType; + default = null; + description = "The GTK+2/3 theme to use."; + }; + + gtk2 = { + extraConfig = mkOption { + type = types.lines; + default = ""; + example = "gtk-can-change-accels = 1"; + description = '' + Extra configuration lines to add verbatim to + <filename>~/.gtkrc-2.0</filename>. + ''; + }; + }; + + 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; + }; + description = '' + Extra configuration options to add to + <filename>~/.config/gtk-3.0/settings.ini</filename>. + ''; + }; + + extraCss = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to add verbatim to + <filename>~/.config/gtk-3.0/gtk.css</filename>. + ''; + }; + }; + }; + }; + + 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/lib.nix b/home-manager/modules/misc/lib.nix new file mode 100644 index 00000000000..13c00dc59a6 --- /dev/null +++ b/home-manager/modules/misc/lib.nix @@ -0,0 +1,14 @@ +{ lib, ... }: + +{ + options = { + lib = lib.mkOption { + type = lib.types.attrsOf lib.types.attrs; + default = { }; + description = '' + This option allows modules to define helper functions, + constants, etc. + ''; + }; + }; +} diff --git a/home-manager/modules/misc/news.nix b/home-manager/modules/misc/news.nix new file mode 100644 index 00000000000..b031af99685 --- /dev/null +++ b/home-manager/modules/misc/news.nix @@ -0,0 +1,1678 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + + cfg = config.news; + + hostPlatform = pkgs.stdenv.hostPlatform; + + entryModule = types.submodule ({ config, ... }: { + options = { + id = mkOption { + internal = true; + type = types.str; + description = '' + A unique entry identifier. By default it is a base16 + formatted hash of the entry message. + ''; + }; + + time = mkOption { + internal = true; + type = types.str; + example = "2017-07-10T21:55:04+00:00"; + description = '' + News entry time stamp in ISO-8601 format. Must be in UTC + (ending in '+00:00'). + ''; + }; + + condition = mkOption { + internal = true; + default = true; + description = "Whether the news entry should be active."; + }; + + message = mkOption { + internal = true; + type = types.str; + description = "The news entry content."; + }; + }; + + config = { + id = mkDefault (builtins.hashString "sha256" config.message); + }; + }); + +in + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + news = { + display = mkOption { + type = types.enum [ "silent" "notify" "show" ]; + default = "notify"; + description = '' + How unread and relevant news should be presented when + running <command>home-manager build</command> and + <command>home-manager switch</command>. + + </para><para> + + The options are + + <variablelist> + <varlistentry> + <term><literal>silent</literal></term> + <listitem> + <para> + Do not print anything during build or switch. The + <command>home-manager news</command> command still + works for viewing the entries. + </para> + </listitem> + </varlistentry> + <varlistentry> + <term><literal>notify</literal></term> + <listitem> + <para> + The number of unread and relevant news entries will be + printed to standard output. The <command>home-manager + news</command> command can later be used to view the + entries. + </para> + </listitem> + </varlistentry> + <varlistentry> + <term><literal>show</literal></term> + <listitem> + <para> + A pager showing unread news entries is opened. + </para> + </listitem> + </varlistentry> + </variablelist> + ''; + }; + + entries = mkOption { + internal = true; + type = types.listOf entryModule; + default = []; + description = "News entries."; + }; + }; + }; + + config = { + # Add news entries in chronological order (i.e., latest time + # should be at the bottom of the list). The time should be + # formatted as given in the output of + # + # date --iso-8601=second --universal + # + news.entries = [ + { + time = "2017-09-01T10:56:28+00:00"; + message = '' + Hello! This is a news entry and it represents an + experimental new feature of Home Manager. The idea is to + inform you when something of importance happens in Home + Manager or its modules. + + We will try to not disturb you about the same news more than + once so the next time you run + + home-manager switch + + or + + home-manager build + + it should not notify you about this text again. + + News items may be conditional and will then only show if the + condition holds, for example if they are relevant to your + configuration. + + If you want to see all relevant news then please use the + + home-manager news + + command. + + Since this is an experimental feature any positive or + negative feedback would be greatly appreciated. For example, + by commenting in https://git.io/v5BJL. + ''; + } + + { + time = "2017-09-10T22:15:19+00:00"; + condition = config.programs.zsh.enable; + message = '' + Home Manager now offers its own minimal zsh plugin manager + under the 'programs.zsh.plugins' option path. By statically + sourcing your plugins it achieves no startup overhead. + ''; + } + + { + time = "2017-09-12T13:11:48+00:00"; + condition = ( + config.programs.zsh.enable && + config.programs.zsh.shellAliases != {} + ); + message = '' + Aliases defined in 'programs.zsh.shellAliases' + are now have the highest priority. Such aliases will + not be redefined by the code in 'programs.zsh.initExtra' + or any external plugins. + ''; + } + + { + time = "2017-09-12T14:22:18+00:00"; + message = '' + A new service is available: 'services.blueman-applet'. + ''; + } + + { + time = "2017-09-13T11:30:22+00:00"; + message = '' + A new service is available: 'services.compton'. + ''; + } + + { + time = "2017-09-20T14:47:14+00:00"; + message = '' + A new service is available: 'services.screen-locker'. + ''; + } + + { + time = "2017-09-22T12:09:01+00:00"; + condition = isString config.programs.git.extraConfig; + message = '' + The 'programs.git.extraConfig' parameter now accepts + attributes instead of strings which allows more flexible + configuration. + + The string parameter type will be deprecated in the future, + please change your configuration file accordingly. + + For example, if your configuration includes + + programs.git.extraConfig = ''' + [core] + editor = vim + '''; + + then you can now change it to + + programs.git.extraConfig = { + core = { + editor = "vim"; + }; + }; + ''; + } + + { + time = "2017-09-27T07:28:54+00:00"; + message = '' + A new program module is available: 'programs.command-not-found'. + + Note, this differs from the NixOS system command-not-found + tool in that NIX_AUTO_INSTALL is not supported. + ''; + } + + { + time = "2017-09-28T12:39:36+00:00"; + message = '' + A new program module is available: 'programs.rofi'; + ''; + } + + { + time = "2017-10-02T11:15:03+00:00"; + condition = config.services.udiskie.enable; + message = '' + The udiskie service now defaults to automatically mounting + new devices. Previous behavior was to not automatically + mount. To restore this previous behavior add + + services.udiskie.automount = false; + + to your Home Manager configuration. + ''; + } + + { + time = "2017-10-04T18:36:07+00:00"; + message = '' + A new module is available: 'xsession.windowManager.xmonad'. + ''; + } + + { + time = "2017-10-06T08:21:43+00:00"; + message = '' + A new service is available: 'services.polybar'. + ''; + } + + { + time = "2017-10-09T16:38:34+00:00"; + message = '' + A new module is available: 'fonts.fontconfig'. + + In particular, the Boolean option + + fonts.fontconfig.enableProfileFonts + + was added for those who do not use NixOS and want to install + font packages using 'nix-env' or 'home.packages'. If you are + using NixOS then you do not need to enable this option. + ''; + } + + { + time = "2017-10-12T11:21:45+00:00"; + condition = config.programs.zsh.enable; + message = '' + A new option in zsh module is available: 'programs.zsh.sessionVariables'. + + This option can be used to set zsh specific session variables which + will be set only on zsh launch. + ''; + } + + { + time = "2017-10-15T13:59:47+00:00"; + message = '' + A new module is available: 'programs.man'. + + This module is enabled by default and makes sure that manual + pages are installed for packages in 'home.packages'. + ''; + } + + { + time = "2017-10-20T12:15:27+00:00"; + condition = with config.systemd.user; + services != {} || sockets != {} || targets != {} || timers != {}; + message = '' + Home Manager's interaction with systemd is now done using + 'systemctl' from Nixpkgs, not the 'systemctl' in '$PATH'. + + If you are using a distribution whose systemd is + incompatible with the version in Nixpkgs then you can + override this behavior by adding + + systemd.user.systemctlPath = "/usr/bin/systemctl" + + to your configuration. Home Manager will then use your + chosen version. + ''; + } + + { + time = "2017-10-23T23:10:29+00:00"; + condition = !config.programs.home-manager.enable; + message = '' + Unfortunately, due to some internal restructuring it is no + longer possible to install the home-manager command when + having + + home-manager = import ./home-manager { inherit pkgs; }; + + in the '~/.config/nixpkgs/config.nix' package override + section. Attempting to use the above override will now + result in the error "cannot coerce a set to a string". + + To resolve this please delete the override from the + 'config.nix' file and either link the Home Manager overlay + + $ ln -s ~/.config/nixpkgs/home-manager/overlay.nix \ + ~/.config/nixpkgs/overlays/home-manager.nix + + or add + + programs.home-manager.enable = true; + + to your Home Manager configuration. The latter is + recommended as the home-manager tool then is updated + automatically whenever you do a switch. + ''; + } + + { + time = "2017-10-23T23:26:17+00:00"; + message = '' + A new module is available: 'nixpkgs'. + + Like the identically named NixOS module, this allows you to + set Nixpkgs options and define Nixpkgs overlays. Note, the + changes you make here will not automatically apply to Nix + commands run outside Home Manager. + ''; + } + + { + time = "2017-10-28T23:39:55+00:00"; + message = '' + A new module is available: 'xdg'. + + If enabled, this module allows configuration of the XDG base + directory paths. + + Whether the module is enabled or not, it also offers the + option 'xdg.configFile', which acts much like 'home.file' + except the target path is relative to the XDG configuration + directory. That is, unless `XDG_CONFIG_HOME` is configured + otherwise, the assignment + + xdg.configFile.hello.text = "hello world"; + + will result in a file '$HOME/.config/hello'. + + Most modules in Home Manager that previously were hard coded + to write configuration to '$HOME/.config' now use this + option and will therefore honor the XDG configuration + directory. + ''; + } + + { + time = "2017-10-31T11:46:07+00:00"; + message = '' + A new window manager module is available: 'xsession.windowManager.i3'. + ''; + } + + { + time = "2017-11-12T00:18:59+00:00"; + message = '' + A new program module is available: 'programs.neovim'. + ''; + } + + { + time = "2017-11-14T19:56:49+00:00"; + condition = with config.xsession.windowManager; ( + i3.enable && i3.config != null && i3.config.startup != [] + ); + message = '' + A new 'notification' option was added to + xsession.windowManager.i3.startup submodule. + + Startup commands are now executed with the startup-notification + support enabled by default. Please, set 'notification' to false + where --no-startup-id option is necessary. + ''; + } + + { + time = "2017-11-17T10:36:10+00:00"; + condition = config.xsession.windowManager.i3.enable; + message = '' + The i3 window manager module has been extended with the following options: + + i3.config.keycodebindings + i3.config.window.commands + i3.config.window.hideEdgeBorders + i3.config.focus.mouseWarping + ''; + } + + { + time = "2017-11-26T21:57:23+00:00"; + message = '' + Two new modules are available: + + 'services.kbfs' and 'services.keybase' + ''; + } + + { + time = "2017-12-07T22:23:11+00:00"; + message = '' + A new module is available: 'services.parcellite' + ''; + } + + { + time = "2017-12-11T17:23:12+00:00"; + condition = config.home.activation ? reloadSystemd; + message = '' + The Boolean option 'systemd.user.startServices' is now + available. When enabled the current naive systemd unit + reload logic is replaced by a more sophisticated one that + attempts to automatically start, stop, and restart units as + necessary. + ''; + } + + { + time = "2018-02-02T11:15:00+00:00"; + message = '' + A new program configuration is available: 'programs.mercurial' + ''; + } + + { + time = "2018-02-03T10:00:00+00:00"; + message = '' + A new module is available: 'services.stalonetray' + ''; + } + + { + time = "2018-02-04T22:58:49+00:00"; + condition = config.xsession.enable; + message = '' + A new option 'xsession.pointerCursor' is now available. It + allows specifying the pointer cursor theme and size. The + settings will be applied in the xsession, Xresources, and + GTK configurations. + ''; + } + + { + time = "2018-02-06T20:23:34+00:00"; + message = '' + It is now possible to use Home Manager as a NixOS module. + This allows you to prepare user environments from the system + configuration file, which often is more convenient than + using the 'home-manager' tool. It also opens up additional + possibilities, for example, to automatically configure user + environments in NixOS declarative containers or on systems + deployed through NixOps. + + This feature should be considered experimental for now and + some critial limitations apply. For example, it is currently + not possible to use 'nixos-rebuild build-vm' when using the + Home Manager NixOS module. That said, it should be + reasonably robust and stable for simpler use cases. + + To make Home Manager available in your NixOS system + configuration you can add + + imports = [ + "''${builtins.fetchTarball https://github.com/rycee/home-manager/archive/master.tar.gz}/nixos" + ]; + + to your 'configuration.nix' file. This will introduce a new + NixOS option called 'home-manager.users' whose type is an + attribute set mapping user names to Home Manager + configurations. + + For example, a NixOS configuration may include the lines + + users.users.eve.isNormalUser = true; + home-manager.users.eve = { + home.packages = [ pkgs.atool pkgs.httpie ]; + programs.bash.enable = true; + }; + + and after a 'nixos-rebuild switch' the user eve's + environment should include a basic Bash configuration and + the packages atool and httpie. + + More detailed documentation on the intricacies of this new + feature is slowly forthcoming. + ''; + } + + { + time = "2018-02-09T21:14:42+00:00"; + condition = with config.programs.rofi; enable && colors != null; + message = '' + The new and preferred way to configure the rofi theme is + using rasi themes through the 'programs.rofi.theme' option. + This option can take as value either the name of a + pre-installed theme or the path to a theme file. + + A rasi theme can be generated from an Xresources config + using 'rofi -dump-theme'. + + The option 'programs.rofi.colors' is still supported but may + become deprecated and removed in the future. + ''; + } + + { + time = "2018-02-19T21:45:26+00:00"; + message = '' + A new module is available: 'programs.pidgin' + ''; + } + + { + time = "2018-03-04T06:54:26+00:00"; + message = '' + A new module is available: 'services.unclutter' + ''; + } + + { + time = "2018-03-07T21:38:27+00:00"; + message = '' + A new module is available: 'programs.fzf'. + ''; + } + + { + time = "2018-03-25T06:49:57+00:00"; + condition = with config.programs.ssh; enable && matchBlocks != {}; + message = '' + Options set through the 'programs.ssh' module are now placed + at the end of the SSH configuration file. This was done to + make it possible to override global options such as + 'ForwardAgent' or 'Compression' inside a host match block. + + If you truly need to override an SSH option across all match + blocks then the new option + + programs.ssh.extraOptionOverrides + + can be used. + ''; + } + + { + time = "2018-04-19T07:42:01+00:00"; + message = '' + A new module is available: 'programs.autorandr'. + ''; + } + + { + time = "2018-04-19T15:44:55+00:00"; + condition = config.programs.git.enable; + message = '' + A new option 'programs.git.includes' is available. Additional + Git configuration files may be included via + + programs.git.includes = [ + { path = "~/path/to/config.inc"; } + ]; + + or conditionally via + + programs.git.includes = [ + { path = "~/path/to/config.inc"; condition = "gitdir:~/src/"; } + ]; + + and the corresponding '[include]' or '[includeIf]' sections will be + appended to the main Git configuration file. + ''; + } + + { + time = "2018-05-01T20:49:31+00:00"; + message = '' + A new module is available: 'services.mbsync'. + ''; + } + { + time = "2018-05-03T12:34:47+00:00"; + message = '' + A new module is available: 'services.flameshot'. + ''; + } + + { + time = "2018-05-18T18:34:15+00:00"; + message = '' + A new module is available: 'qt' + + At the moment this module allows you to set up Qt to use the + GTK+ theme, and not much else. + ''; + } + + { + time = "2018-06-05T01:36:45+00:00"; + message = '' + A new module is available: 'services.kdeconnect'. + ''; + } + + { + time = "2018-06-09T09:11:59+00:00"; + message = '' + A new module is available: `programs.newsboat`. + ''; + } + + { + time = "2018-07-01T14:33:15+00:00"; + message = '' + A new module is available: 'accounts.email'. + + As the name suggests, this new module offers a number of + options for configuring email accounts. This, for example, + includes the email address and owner's real name but also + server settings for IMAP and SMTP. + + The intent is to have a central location for account + specific configuration that other modules can use. + + Note, this module is still somewhat experimental and its + structure should not be seen as final. Feedback is greatly + appreciated, both positive and negative. + ''; + } + + { + time = "2018-07-01T16:07:04+00:00"; + message = '' + A new module is available: 'programs.mbsync'. + ''; + } + + { + time = "2018-07-01T16:12:20+00:00"; + message = '' + A new module is available: 'programs.notmuch'. + ''; + } + + { + time = "2018-07-07T15:48:56+00:00"; + message = '' + A new module is available: 'xsession.windowManager.awesome'. + ''; + } + + { + time = "2018-07-18T20:14:11+00:00"; + message = '' + A new module is available: 'services.mpd'. + ''; + } + + { + time = "2018-07-31T13:33:39+00:00"; + message = '' + A new module is available: 'services.status-notifier-watcher'. + ''; + } + + { + time = "2018-07-31T13:47:06+00:00"; + message = '' + A new module is available: 'programs.direnv'. + ''; + } + + { + time = "2018-08-17T20:30:14+00:00"; + message = '' + A new module is available: 'programs.fish'. + ''; + } + + { + time = "2018-08-18T19:03:42+00:00"; + condition = config.services.gpg-agent.enable; + message = '' + A new option is available: 'services.gpg-agent.extraConfig'. + + Extra lines may be appended to $HOME/.gnupg/gpg-agent.conf + using this option. + ''; + } + + { + time = "2018-08-19T20:46:09+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new modules is available: 'programs.chromium'. + ''; + } + + { + time = "2018-08-20T20:27:26+00:00"; + message = '' + A new module is available: 'programs.msmtp'. + ''; + } + + { + time = "2018-08-21T20:13:50+00:00"; + message = '' + A new module is available: 'services.pasystray'. + ''; + } + + { + time = "2018-08-29T20:27:04+00:00"; + message = '' + A new module is available: 'programs.offlineimap'. + ''; + } + + { + time = "2018-09-18T21:25:14+00:00"; + message = '' + A new module is available: 'programs.taskwarrior'. + ''; + } + + { + time = "2018-09-18T21:43:54+00:00"; + message = '' + A new module is available: 'programs.zathura'. + ''; + } + + { + time = "2018-09-20T19:26:40+00:00"; + message = '' + A new module is available: 'programs.noti'. + ''; + } + + { + time = "2018-09-20T22:10:45+00:00"; + message = '' + A new module is available: 'programs.go'. + ''; + } + + { + time = "2018-09-27T17:48:08+00:00"; + message = '' + A new module is available: 'programs.obs-studio'. + ''; + } + + { + time = "2018-09-28T21:38:48+00:00"; + message = '' + A new module is available: 'programs.alot'. + ''; + } + + { + time = "2018-10-20T09:30:57+00:00"; + message = '' + A new module is available: 'programs.urxvt'. + ''; + } + + { + time = "2018-11-13T23:08:03+00:00"; + message = '' + A new module is available: 'programs.tmux'. + ''; + } + + { + time = "2018-11-18T18:55:15+00:00"; + message = '' + A new module is available: 'programs.astroid'. + ''; + } + + { + time = "2018-11-18T21:41:51+00:00"; + message = '' + A new module is available: 'programs.afew'. + ''; + } + + { + time = "2018-11-19T00:40:34+00:00"; + message = '' + A new nix-darwin module is available. Use it the same way the NixOS + module is used. A major limitation is that Home Manager services don't + work, as they depend explicitly on Linux and systemd user services. + However, 'home.file' and 'home.packages' do work. Everything else is + untested at this time. + ''; + } + + { + time = "2018-11-24T16:22:19+00:00"; + message = '' + A new option 'home.stateVersion' is available. Its function + is much like the 'system.stateVersion' option in NixOS. + + Briefly, the state version indicates a stable set of option + defaults. In the future, whenever Home Manager changes an + option default in a way that may cause program breakage it + will do so only for the unstable state version, currently + 19.03. Once 19.03 becomes the stable version only backwards + compatible changes will be made and 19.09 becomes the + unstable state version. + + The default value for this option is 18.09 but it may still + be a good idea to explicitly add + + home.stateVersion = "18.09"; + + to your Home Manager configuration. + ''; + } + + { + time = "2018-11-25T22:10:15+00:00"; + message = '' + A new module is available: 'services.nextcloud-client'. + ''; + } + + { + time = "2018-11-25T22:55:12+00:00"; + message = '' + A new module is available: 'programs.vscode'. + ''; + } + + { + time = "2018-12-04T21:54:38+00:00"; + condition = config.programs.beets.settings != {}; + message = '' + A new option 'programs.beets.enable' has been added. + Starting with state version 19.03 this option defaults to + false. For earlier versions it defaults to true if + 'programs.beets.settings' is non-empty. + + It is recommended to explicitly add + + programs.beets.enable = true; + + to your configuration. + ''; + } + + { + time = "2018-12-12T21:02:05+00:00"; + message = '' + A new module is available: 'programs.jq'. + ''; + } + + { + time = "2018-12-24T16:26:16+00:00"; + message = '' + A new module is available: 'dconf'. + + Note, on NixOS you may need to add + + services.dbus.packages = with pkgs; [ gnome3.dconf ]; + + to the system configuration for this module to work as + expected. In particular if you get the error message + + The name ca.desrt.dconf was not provided by any .service files + + when activating your Home Manager configuration. + ''; + } + + { + time = "2018-12-28T12:32:30+00:00"; + message = '' + A new module is available: 'programs.opam'. + ''; + } + + { + time = "2019-01-18T00:21:56+00:00"; + message = '' + A new module is available: 'programs.matplotlib'. + ''; + } + + { + time = "2019-01-26T13:20:37+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.xembed-sni-proxy'. + ''; + } + + { + time = "2019-01-28T23:36:10+00:00"; + message = '' + A new module is available: 'programs.irssi'. + ''; + } + + { + time = "2019-02-09T14:09:58+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.emacs'. + + This module provides a user service that runs the Emacs + configured in + + programs.emacs + + as an Emacs daemon. + ''; + } + + { + time = "2019-02-16T20:33:56+00:00"; + condition = hostPlatform.isLinux; + message = '' + When using Home Manager as a NixOS submodule it is now + possible to install packages using the NixOS + + users.users.<name?>.packages + + option. This is enabled by adding + + home-manager.useUserPackages = true; + + to your NixOS system configuration. This mode of operation + is necessary if you want to use 'nixos-rebuild build-vm'. + ''; + } + + { + time = "2019-02-17T21:11:24+00:00"; + message = '' + A new module is available: 'programs.keychain'. + ''; + } + + { + time = "2019-02-24T00:32:23+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new service is available: 'services.mpdris2'. + ''; + } + + { + time = "2019-03-19T22:56:20+00:00"; + message = '' + A new module is available: 'programs.bat'. + ''; + } + + { + time = "2019-03-19T23:07:34+00:00"; + message = '' + A new module is available: 'programs.lsd'. + ''; + } + + { + time = "2019-04-09T20:10:22+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.xcape'. + ''; + } + + { + time = "2019-04-11T22:50:10+00:00"; + condition = hostPlatform.isLinux; + message = '' + The type used for the systemd unit options under + + systemd.user.services, systemd.user.sockets, etc. + + has been changed to offer more robust merging of configurations. + + If you don't override values within systemd units then you are not + affected by this change. Unfortunately, if you do override unit values + you may encounter errors due to this change. + + In particular, if you get an error saying that a "unique option" is + "defined multiple times" then you need to use 'lib.mkForce'. For + example, + + systemd.user.services.foo.Service.ExecStart = "/foo/bar"; + + becomes + + systemd.user.services.foo.Service.ExecStart = lib.mkForce "/foo/bar"; + + We had to make this change because the old merging was causing too + many confusing situations for people. Apologies for potentially + breaking your configuration! + ''; + } + + { + time = "2019-04-14T15:35:16+00:00"; + message = '' + A new module is available: 'programs.skim'. + ''; + } + + { + time = "2019-04-22T12:43:20+00:00"; + message = '' + A new module is available: 'programs.alacritty'. + ''; + } + + { + time = "2019-04-26T22:53:48+00:00"; + condition = config.programs.vscode.enable; + message = '' + A new module is available: 'programs.vscode.haskell'. + + Enable to add Haskell IDE Engine and syntax highlighting + support to your VSCode. + ''; + } + + { + time = "2019-05-04T23:56:39+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.rsibreak'. + ''; + } + + { + time = "2019-05-07T20:49:29+00:00"; + message = '' + A new module is available: 'programs.mpv'. + ''; + } + + { + time = "2019-05-30T17:49:29+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.xsuspender'. + ''; + } + + { + time = "2019-06-03T21:47:10+00:00"; + message = '' + A new module is available: 'programs.gpg'. + ''; + } + + { + time = "2019-06-09T12:19:18+00:00"; + message = '' + Collisions between unmanaged and managed files can now be + automatically resolved by moving the target file to a new + path instead of failing the switch operation. To enable + this, use the new '-b' command line argument. For example, + + home-manager -b bck switch + + where 'bck' is the suffix to give the moved file. In this + case a colliding file 'foo.conf' will be moved to + 'foo.conf.bck'. + ''; + } + + { + time = "2019-06-19T17:49:29+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: `services.getmail`. + ''; + } + + { + time = "2019-07-02T09:27:56+00:00"; + message = '' + A new module is available: 'programs.broot'. + ''; + } + + { + time = "2019-07-17T19:30:29+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.taskwarrior-sync'. + ''; + } + + { + time = "2019-07-17T20:05:29+00:00"; + message = '' + A new module is available: 'programs.kakoune'. + ''; + } + + { + time = "2019-08-08T11:49:35+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.hound'. + ''; + } + + { + time = "2019-08-17T12:24:58+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.muchsync'. + ''; + } + + { + time = "2019-08-18T14:22:41+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.dwm-status'. + ''; + } + + { + time = "2019-08-28T10:18:07+00:00"; + condition = config.programs.vim.enable; + message = '' + The 'programs.vim.plugins' option now accepts packages. + Specifying them as strings is deprecated. + ''; + } + + { + time = "2019-09-17T19:33:49+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.sxhkd'. + ''; + } + + { + time = "2019-09-26T21:05:24+00:00"; + message = '' + A new module is available: 'programs.starship'. + ''; + } + + { + time = "2019-09-26T21:47:13+00:00"; + message = '' + A new module is available: 'programs.rtorrent'. + ''; + } + + { + time = "2019-11-04T20:56:29+00:00"; + message = '' + A new module is available: 'programs.pazi'. + ''; + } + + { + time = "2019-11-05T21:54:04+00:00"; + condition = config.programs.zsh.enable; + message = '' + The 'programs.zsh.history.path' option behavior and the + default value has changed for state version 20.03 and above. + + Specifically, '$HOME' will no longer be prepended to the + option value, which allows specifying absolute paths (e.g. + using the xdg module). Also, the default value is fixed to + '$HOME/.zsh_history' and 'dotDir' path is not prepended to + it anymore. + ''; + } + + { + time = "2019-11-17T18:47:40+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.lorri'. + ''; + } + + { + time = "2019-11-24T17:46:57+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.spotifyd'. + ''; + } + + { + time = "2019-11-29T21:18:48+00:00"; + message = '' + A new module is available: 'programs.password-store'. + ''; + } + + { + time = "2019-11-29T21:18:48+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.password-store-sync'. + ''; + } + + { + time = "2019-11-29T22:46:49+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.unison'. + ''; + } + + { + time = "2019-12-01T22:10:23+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'xdg.mime'. + + If enabled, which it is by default, this module will create + the XDG mime database and desktop file database caches from + programs installed via Home Manager. + ''; + } + + { + time = "2019-12-08T19:48:26+00:00"; + message = '' + A new module is available: 'programs.readline'. + ''; + } + + { + time = "2020-01-11T11:49:51+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.cbatticon'. + ''; + } + + { + time = "2020-01-26T12:42:33+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'xsession.windowManager.bspwm'. + ''; + } + + { + time = "2020-01-26T12:49:40+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.grobi'. + ''; + } + + { + time = "2020-01-26T19:37:57+00:00"; + message = '' + 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 new file mode 100644 index 00000000000..511dbec10b2 --- /dev/null +++ b/home-manager/modules/misc/nixpkgs.nix @@ -0,0 +1,152 @@ +# Adapted from Nixpkgs. + +{ config, lib, pkgs, pkgsPath, ... }: + +with lib; + +let + + isConfig = x: + builtins.isAttrs x || builtins.isFunction x; + + optCall = f: x: + if builtins.isFunction f + then f x + else f; + + mergeConfig = lhs_: rhs_: + let + lhs = optCall lhs_ { inherit pkgs; }; + rhs = optCall rhs_ { inherit pkgs; }; + in + lhs // rhs // + optionalAttrs (lhs ? packageOverrides) { + packageOverrides = pkgs: + optCall lhs.packageOverrides pkgs // + optCall (attrByPath ["packageOverrides"] ({}) rhs) pkgs; + } // + optionalAttrs (lhs ? perlPackageOverrides) { + perlPackageOverrides = pkgs: + optCall lhs.perlPackageOverrides pkgs // + optCall (attrByPath ["perlPackageOverrides"] ({}) rhs) pkgs; + }; + + configType = mkOptionType { + name = "nixpkgs-config"; + description = "nixpkgs config"; + check = x: + let traceXIfNot = c: + if c x then true + else lib.traceSeqN 1 x false; + in traceXIfNot isConfig; + merge = args: fold (def: mergeConfig def.value) {}; + }; + + overlayType = mkOptionType { + name = "nixpkgs-overlay"; + description = "nixpkgs overlay"; + check = builtins.isFunction; + merge = lib.mergeOneOption; + }; + + _pkgs = import pkgsPath ( + filterAttrs (n: v: v != null) config.nixpkgs + ); + +in + +{ + options.nixpkgs = { + config = mkOption { + default = null; + example = { allowBroken = true; }; + type = types.nullOr configType; + description = '' + The configuration of the Nix Packages collection. (For + details, see the Nixpkgs documentation.) It allows you to set + package configuration options. + + </para><para> + + If <literal>null</literal>, then configuration is taken from + the fallback location, for example, + <filename>~/.config/nixpkgs/config.nix</filename>. + + </para><para> + + Note, this option will not apply outside your Home Manager + configuration like when installing manually through + <command>nix-env</command>. If you want to apply it both + inside and outside Home Manager you can put it in a separate + file and include something like + + <programlisting language="nix"> + nixpkgs.config = import ./nixpkgs-config.nix; + xdg.configFile."nixpkgs/config.nix".source = ./nixpkgs-config.nix; + </programlisting> + + in your Home Manager configuration. + ''; + }; + + overlays = mkOption { + default = null; + example = literalExample + '' + [ (self: super: { + openssh = super.openssh.override { + hpnSupport = true; + withKerberos = true; + kerberos = self.libkrb5; + }; + }; + ) ] + ''; + type = types.nullOr (types.listOf overlayType); + description = '' + List of overlays to use with the Nix Packages collection. (For + details, see the Nixpkgs documentation.) It allows you to + override packages globally. This is a function that takes as + an argument the <emphasis>original</emphasis> Nixpkgs. The + first argument should be used for finding dependencies, and + the second should be used for overriding recipes. + + </para><para> + + If <literal>null</literal>, then the overlays are taken from + the fallback location, for example, + <filename>~/.config/nixpkgs/overlays</filename>. + + </para><para> + + Like <varname>nixpkgs.config</varname> this option only + applies within the Home Manager configuration. See + <varname>nixpkgs.config</varname> for a suggested setup that + works both internally and externally. + ''; + }; + + system = mkOption { + type = types.str; + example = "i686-linux"; + internal = true; + description = '' + Specifies the Nix platform type for which the user environment + should be built. If unset, it defaults to the platform type of + your host system. Specifying this option is useful when doing + distributed multi-platform deployment, or when building + virtual machines. + ''; + }; + }; + + config = { + _module.args = { + pkgs = mkOverride modules.defaultPriority _pkgs; + pkgs_i686 = + if _pkgs.stdenv.isLinux && _pkgs.stdenv.hostPlatform.isx86 + then _pkgs.pkgsi686Linux + else { }; + }; + }; +} diff --git a/home-manager/modules/misc/numlock.nix b/home-manager/modules/misc/numlock.nix new file mode 100644 index 00000000000..c823f6dbdd2 --- /dev/null +++ b/home-manager/modules/misc/numlock.nix @@ -0,0 +1,31 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession.numlock; + +in { + meta.maintainers = [ maintainers.evanjs ]; + + options = { xsession.numlock.enable = mkEnableOption "Num Lock"; }; + + config = mkIf cfg.enable { + systemd.user.services.numlockx = { + Unit = { + Description = "NumLockX"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.numlockx}/bin/numlockx"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/misc/pam.nix b/home-manager/modules/misc/pam.nix new file mode 100644 index 00000000000..f54f4b95089 --- /dev/null +++ b/home-manager/modules/misc/pam.nix @@ -0,0 +1,32 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + vars = config.pam.sessionVariables; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + pam.sessionVariables = mkOption { + default = { }; + type = types.attrs; + example = { EDITOR = "vim"; }; + description = '' + Environment variables that will be set for the PAM session. + The variable values must be as described in + <citerefentry> + <refentrytitle>pam_env.conf</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry>. + ''; + }; + }; + + config = mkIf (vars != { }) { + home.file.".pam_environment".text = concatStringsSep "\n" + (mapAttrsToList (n: v: ''${n} OVERRIDE="${toString v}"'') vars) + "\n"; + }; +} diff --git a/home-manager/modules/misc/qt.nix b/home-manager/modules/misc/qt.nix new file mode 100644 index 00000000000..ff38f842c81 --- /dev/null +++ b/home-manager/modules/misc/qt.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.qt; + +in { + meta.maintainers = [ maintainers.rycee ]; + + imports = [ + (mkChangedOptionModule [ "qt" "useGtkTheme" ] [ "qt" "platformTheme" ] + (config: + if getAttrFromPath [ "qt" "useGtkTheme" ] config then "gtk" else null)) + ]; + + options = { + qt = { + enable = mkEnableOption "Qt 4 and 5 configuration"; + + platformTheme = mkOption { + type = types.nullOr (types.enum [ "gtk" "gnome" ]); + default = null; + example = "gnome"; + relatedPackages = + [ "qgnomeplatform" [ "libsForQt5" "qtstyleplugins" ] ]; + description = '' + Selects the platform theme to use for Qt applications.</para> + <para>The options are + <variablelist> + <varlistentry> + <term><literal>gtk</literal></term> + <listitem><para>Use GTK theme with + <link xlink:href="https://github.com/qt/qtstyleplugins">qtstyleplugins</link> + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>gnome</literal></term> + <listitem><para>Use GNOME theme with + <link xlink:href="https://github.com/FedoraQt/QGnomePlatform">qgnomeplatform</link> + </para></listitem> + </varlistentry> + </variablelist> + ''; + }; + }; + }; + + config = mkIf (cfg.enable && cfg.platformTheme != null) { + home.sessionVariables.QT_QPA_PLATFORMTHEME = + if cfg.platformTheme == "gnome" then "gnome" else "gtk2"; + + home.packages = if cfg.platformTheme == "gnome" then + [ pkgs.qgnomeplatform ] + else + [ pkgs.libsForQt5.qtstyleplugins ]; + + xsession.importedVariables = [ "QT_QPA_PLATFORMTHEME" ]; + + # Enable GTK+ style for Qt4 in either case. + # It doesn’t support the platform theme packages. + home.activation.useGtkThemeInQt4 = hm.dag.entryAfter [ "writeBoundary" ] '' + $DRY_RUN_CMD ${pkgs.crudini}/bin/crudini $VERBOSE_ARG \ + --set "${config.xdg.configHome}/Trolltech.conf" Qt style GTK+ + ''; + }; +} diff --git a/home-manager/modules/misc/submodule-support.nix b/home-manager/modules/misc/submodule-support.nix new file mode 100644 index 00000000000..ff80291cadf --- /dev/null +++ b/home-manager/modules/misc/submodule-support.nix @@ -0,0 +1,32 @@ +{ lib, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.rycee ]; + + options.submoduleSupport = { + enable = mkOption { + type = types.bool; + default = false; + internal = true; + description = '' + Whether the Home Manager module system is used as a submodule + in, for example, NixOS or nix-darwin. + ''; + }; + + externalPackageInstall = mkOption { + type = types.bool; + default = false; + internal = true; + description = '' + Whether the packages of <option>home.packages</option> are + installed separately from the Home Manager activation script. + In NixOS, for example, this may be accomplished by installing + the packages through + <option>users.users.‹name?›.packages</option>. + ''; + }; + }; +} 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 new file mode 100644 index 00000000000..fbeb3ec539a --- /dev/null +++ b/home-manager/modules/misc/version.nix @@ -0,0 +1,24 @@ +{ config, lib, ... }: + +with lib; + +{ + options = { + home.stateVersion = mkOption { + 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 + configuration defaults in a way that is incompatible with + stateful data. This could, for example, include switching the + default data format or location of a file. + </para><para> + The <emphasis>state version</emphasis> indicates which default + settings are in effect and will therefore help avoid breaking + program configurations. Switching to a higher state version + typically requires performing some manual steps, such as data + conversion or moving files. + ''; + }; + }; +} 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 new file mode 100644 index 00000000000..81d2ba0fcbe --- /dev/null +++ b/home-manager/modules/misc/xdg-mime-apps.nix @@ -0,0 +1,88 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xdg.mimeApps; + + strListOrSingleton = with types; + coercedTo (either (listOf str) str) toList (listOf str); + +in { + meta.maintainers = with maintainers; [ pacien ]; + + options.xdg.mimeApps = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to manage <filename>$XDG_CONFIG_HOME/mimeapps.list</filename>. + </para> + <para> + The generated file is read-only. + ''; + }; + + # descriptions from + # https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-1.0.1.html + + associations.added = mkOption { + type = types.attrsOf strListOrSingleton; + default = { }; + example = literalExample '' + { + "mimetype1" = [ "foo1.desktop" "foo2.desktop" "foo3.desktop" ]; + "mimetype2" = "foo4.desktop"; + } + ''; + description = '' + Defines additional associations of applications with + mimetypes, as if the .desktop file was listing this mimetype + in the first place. + ''; + }; + + associations.removed = mkOption { + type = types.attrsOf strListOrSingleton; + default = { }; + example = { "mimetype1" = "foo5.desktop"; }; + description = '' + Removes associations of applications with mimetypes, as if the + .desktop file was <emphasis>not</emphasis> listing this + mimetype in the first place. + ''; + }; + + defaultApplications = mkOption { + type = types.attrsOf strListOrSingleton; + default = { }; + example = literalExample '' + { + "mimetype1" = [ "default1.desktop" "default2.desktop" ]; + } + ''; + description = '' + The default application to be used for a given mimetype. This + is, for instance, the one that will be started when + double-clicking on a file in a file manager. If the + application is no longer installed, the next application in + the list is attempted, and so on. + ''; + }; + }; + + config = mkIf cfg.enable { + # Deprecated but still used by some applications. + xdg.dataFile."applications/mimeapps.list".source = + config.xdg.configFile."mimeapps.list".source; + + xdg.configFile."mimeapps.list".text = + let joinValues = mapAttrs (n: concatStringsSep ";"); + in generators.toINI { } { + "Added Associations" = joinValues cfg.associations.added; + "Removed Associations" = joinValues cfg.associations.removed; + "Default Applications" = joinValues cfg.defaultApplications; + }; + }; +} diff --git a/home-manager/modules/misc/xdg-mime.nix b/home-manager/modules/misc/xdg-mime.nix new file mode 100644 index 00000000000..5999e1299c9 --- /dev/null +++ b/home-manager/modules/misc/xdg-mime.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xdg.mime; + +in { + options = { + xdg.mime.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to install programs and files to support the + XDG Shared MIME-info specification and XDG MIME Applications + specification at + <link xlink:href="https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html"/> + and + <link xlink:href="https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html"/>, + respectively. + ''; + }; + }; + + config = mkIf config.xdg.mime.enable { + 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 && -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 \ + -V $out/share/mime > /dev/null + fi + + if [[ -w $out/share/applications ]]; then + ${pkgs.buildPackages.desktop-file-utils}/bin/update-desktop-database \ + $out/share/applications + fi + ''; + }; + +} diff --git a/home-manager/modules/misc/xdg-user-dirs.nix b/home-manager/modules/misc/xdg-user-dirs.nix new file mode 100644 index 00000000000..a1db6b115a1 --- /dev/null +++ b/home-manager/modules/misc/xdg-user-dirs.nix @@ -0,0 +1,110 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xdg.userDirs; + +in { + meta.maintainers = with maintainers; [ pacien ]; + + imports = [ + (mkRenamedOptionModule [ "xdg" "userDirs" "publishShare" ] [ + "xdg" + "userDirs" + "publicShare" + ]) + ]; + + options.xdg.userDirs = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to manage <filename>$XDG_CONFIG_HOME/user-dirs.dirs</filename>. + </para> + <para> + The generated file is read-only. + ''; + }; + + # Well-known directory list from + # https://gitlab.freedesktop.org/xdg/xdg-user-dirs/blob/master/man/user-dirs.dirs.xml + + desktop = mkOption { + type = types.str; + default = "$HOME/Desktop"; + description = "The Desktop directory."; + }; + + documents = mkOption { + type = types.str; + default = "$HOME/Documents"; + description = "The Documents directory."; + }; + + download = mkOption { + type = types.str; + default = "$HOME/Downloads"; + description = "The Downloads directory."; + }; + + music = mkOption { + type = types.str; + default = "$HOME/Music"; + description = "The Music directory."; + }; + + pictures = mkOption { + type = types.str; + default = "$HOME/Pictures"; + description = "The Pictures directory."; + }; + + publicShare = mkOption { + type = types.str; + default = "$HOME/Public"; + description = "The Public share directory."; + }; + + templates = mkOption { + type = types.str; + default = "$HOME/Templates"; + description = "The Templates directory."; + }; + + videos = mkOption { + type = types.str; + default = "$HOME/Videos"; + description = "The Videos directory."; + }; + + extraConfig = mkOption { + type = with types; attrsOf str; + default = { }; + example = { XDG_MISC_DIR = "$HOME/Misc"; }; + description = "Other user directories."; + }; + }; + + config = mkIf cfg.enable { + 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 new file mode 100644 index 00000000000..7420e8e92b3 --- /dev/null +++ b/home-manager/modules/misc/xdg.nix @@ -0,0 +1,112 @@ +{ options, config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xdg; + + dag = config.lib.dag; + + fileType = (import ../lib/file-type.nix { + inherit (config.home) homeDirectory; + inherit lib pkgs; + }).fileType; + + defaultCacheHome = "${config.home.homeDirectory}/.cache"; + defaultConfigHome = "${config.home.homeDirectory}/.config"; + defaultDataHome = "${config.home.homeDirectory}/.local/share"; + + getXdgDir = name: fallback: + let + value = builtins.getEnv name; + in + if value != "" then value else fallback; + +in + +{ + options.xdg = { + enable = mkEnableOption "management of XDG base directories"; + + cacheHome = mkOption { + type = types.path; + defaultText = "~/.cache"; + description = '' + Absolute path to directory holding application caches. + ''; + }; + + configFile = mkOption { + type = fileType "<varname>xdg.configHome</varname>" cfg.configHome; + default = {}; + description = '' + Attribute set of files to link into the user's XDG + configuration home. + ''; + }; + + configHome = mkOption { + type = types.path; + defaultText = "~/.config"; + description = '' + Absolute path to directory holding application configurations. + ''; + }; + + dataFile = mkOption { + type = fileType "<varname>xdg.dataHome</varname>" cfg.dataHome; + default = {}; + description = '' + Attribute set of files to link into the user's XDG + data home. + ''; + }; + + dataHome = mkOption { + type = types.path; + defaultText = "~/.local/share"; + description = '' + Absolute path to directory holding application data. + ''; + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + xdg.cacheHome = mkDefault defaultCacheHome; + xdg.configHome = mkDefault defaultConfigHome; + xdg.dataHome = mkDefault defaultDataHome; + + home.sessionVariables = { + XDG_CACHE_HOME = cfg.cacheHome; + XDG_CONFIG_HOME = cfg.configHome; + XDG_DATA_HOME = cfg.dataHome; + }; + }) + + # 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 + cfg.dataFile + { + "${config.xdg.cacheHome}/.keep".text = ""; + } + ]; + } + ]; +} diff --git a/home-manager/modules/modules.nix b/home-manager/modules/modules.nix new file mode 100644 index 00000000000..08c978b177d --- /dev/null +++ b/home-manager/modules/modules.nix @@ -0,0 +1,221 @@ +{ pkgs + + # Note, this should be "the standard library" + HM extensions. +, lib + + # Whether to enable module type checking. +, check ? true + +# If disabled, the pkgs attribute passed to this function is used instead. +, useNixpkgsModule ? true +}: + +with lib; + +let + + hostPlatform = pkgs.stdenv.hostPlatform; + + loadModule = file: { condition ? true }: { + inherit file condition; + }; + + allModules = [ + (loadModule ./accounts/email.nix { }) + (loadModule ./files.nix { }) + (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 { 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 { }) + (loadModule ./programs/bat.nix { }) + (loadModule ./programs/beets.nix { }) + (loadModule ./programs/broot.nix { }) + (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 { }) + (loadModule ./programs/feh.nix { }) + (loadModule ./programs/firefox.nix { }) + (loadModule ./programs/fish.nix { }) + (loadModule ./programs/fzf.nix { }) + (loadModule ./programs/getmail.nix { condition = hostPlatform.isLinux; }) + (loadModule ./programs/git.nix { }) + (loadModule ./programs/gnome-terminal.nix { }) + (loadModule ./programs/go.nix { }) + (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 { }) + (loadModule ./programs/skim.nix { }) + (loadModule ./programs/starship.nix { }) + (loadModule ./programs/ssh.nix { }) + (loadModule ./programs/taskwarrior.nix { }) + (loadModule ./programs/termite.nix { }) + (loadModule ./programs/texlive.nix { }) + (loadModule ./programs/tmux.nix { }) + (loadModule ./programs/urxvt.nix { }) + (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; }) + (loadModule ./services/muchsync.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/network-manager-applet.nix { }) + (loadModule ./services/nextcloud-client.nix { }) + (loadModule ./services/owncloud-client.nix { }) + (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; }) + (loadModule ./services/screen-locker.nix { }) + (loadModule ./services/stalonetray.nix { }) + (loadModule ./services/status-notifier-watcher.nix { }) + (loadModule ./services/spotifyd.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/sxhkd.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/syncthing.nix { }) + (loadModule ./services/taffybar.nix { }) + (loadModule ./services/tahoe-lafs.nix { }) + (loadModule ./services/taskwarrior-sync.nix { condition = hostPlatform.isLinux; }) + (loadModule ./services/udiskie.nix { }) + (loadModule ./services/unclutter.nix { }) + (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-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 { }) + (loadModule (pkgs.path + "/nixos/modules/misc/assertions.nix") { }) + (loadModule (pkgs.path + "/nixos/modules/misc/meta.nix") { }) + ]; + + modules = map (getAttr "file") (filter (getAttr "condition") allModules); + + 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 + + modules ++ [ pkgsModule ] 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/afew.nix b/home-manager/modules/programs/afew.nix new file mode 100644 index 00000000000..99bae88c0ee --- /dev/null +++ b/home-manager/modules/programs/afew.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.afew; + +in + +{ + options.programs.afew = { + enable = mkEnableOption "the afew initial tagging script for Notmuch"; + + extraConfig = mkOption { + type = types.lines; + default = '' + [SpamFilter] + [KillThreadsFilter] + [ListMailsFilter] + [ArchiveSentMailsFilter] + [InboxFilter] + ''; + example = '' + [SpamFilter] + + [Filter.0] + query = from:pointyheaded@boss.com + tags = -new;+boss + message = Message from above + + [InboxFilter] + ''; + description = '' + Extra lines added to afew configuration file. Available + configuration options are described in the afew manual: + <link xlink:href="https://afew.readthedocs.io/en/latest/configuration.html" />. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.afew ]; + + xdg.configFile."afew/config".text = '' + # Generated by Home Manager. + # See https://afew.readthedocs.io/ + + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/alacritty.nix b/home-manager/modules/programs/alacritty.nix new file mode 100644 index 00000000000..ea908f2b056 --- /dev/null +++ b/home-manager/modules/programs/alacritty.nix @@ -0,0 +1,59 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.alacritty; + +in { + options = { + 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 = { }; + example = literalExample '' + { + window.dimensions = { + lines = 3; + columns = 200; + }; + key_bindings = [ + { + key = "K"; + mods = "Control"; + chars = "\\x0c"; + } + ]; + } + ''; + description = '' + Configuration written to + <filename>~/.config/alacritty/alacritty.yml</filename>. See + <link xlink:href="https://github.com/jwilm/alacritty/blob/master/alacritty.yml"/> + for the default configuration. + ''; + }; + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xdg.configFile."alacritty/alacritty.yml" = mkIf (cfg.settings != { }) { + text = + replaceStrings [ "\\\\" ] [ "\\" ] (builtins.toJSON cfg.settings); + }; + }) + ]; +} diff --git a/home-manager/modules/programs/alot-accounts.nix b/home-manager/modules/programs/alot-accounts.nix new file mode 100644 index 00000000000..89ae28f9c8e --- /dev/null +++ b/home-manager/modules/programs/alot-accounts.nix @@ -0,0 +1,58 @@ +pkgs: +{ config, lib, ... }: + +with lib; + +{ + options.alot = { + sendMailCommand = mkOption { + type = types.nullOr types.str; + description = '' + Command to send a mail. If msmtp is enabled for the account, + then this is set to + <command>msmtpq --read-envelope-from --read-recipients</command>. + ''; + }; + + contactCompletion = mkOption { + type = types.attrsOf types.str; + default = { + type = "shellcommand"; + command = + "'${pkgs.notmuch}/bin/notmuch address --format=json --output=recipients date:6M..'"; + regexp = "'\\[?{" + '' + "name": "(?P<name>.*)", "address": "(?P<email>.+)", "name-addr": ".*"'' + + "}[,\\]]?'"; + shellcommand_external_filtering = "False"; + }; + example = literalExample '' + { + type = "shellcommand"; + command = "abook --mutt-query"; + regexp = "'^(?P<email>[^@]+@[^\t]+)\t+(?P<name>[^\t]+)'"; + ignorecase = "True"; + } + ''; + description = '' + Contact completion configuration as expected per alot. + See <link xlink:href="http://alot.readthedocs.io/en/latest/configuration/contacts_completion.html">alot's wiki</link> for + explanation about possible values. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra settings to add to this Alot account configuration. + ''; + }; + }; + + config = mkIf config.notmuch.enable { + alot.sendMailCommand = mkOptionDefault (if config.msmtp.enable then + "msmtpq --read-envelope-from --read-recipients" + else + null); + }; +} diff --git a/home-manager/modules/programs/alot.nix b/home-manager/modules/programs/alot.nix new file mode 100644 index 00000000000..e907cd3e0ac --- /dev/null +++ b/home-manager/modules/programs/alot.nix @@ -0,0 +1,237 @@ +# alot config loader is sensitive to leading space ! +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.alot; + + alotAccounts = + filter (a: a.notmuch.enable) (attrValues config.accounts.email.accounts); + + boolStr = v: if v then "True" else "False"; + + 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. + ''; + }; + + 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>. + ''; + }; + + 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"/>. + ''; + }; + + focus = mkOption { + type = types.nullOr types.str; + default = null; + description = "How to display the tag when focused."; + }; + }; + }; + + 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. + ''; + }; + + hooks = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the hooks file. + ''; + }; + + 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. + ''; + }; + + 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. + ''; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./alot-accounts.nix pkgs)); + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.alot ]; + + xdg.configFile."alot/config".text = configFile; + + xdg.configFile."alot/hooks.py" = mkIf (cfg.hooks != "") { + text = '' + # Generated by Home Manager. + '' + 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-accounts.nix b/home-manager/modules/programs/astroid-accounts.nix new file mode 100644 index 00000000000..17544ff7899 --- /dev/null +++ b/home-manager/modules/programs/astroid-accounts.nix @@ -0,0 +1,32 @@ +{ config, lib, ... }: + +with lib; + +{ + options.astroid = { + enable = mkEnableOption "Astroid"; + + sendMailCommand = mkOption { + type = types.str; + description = '' + Command to send a mail. If msmtp is enabled for the account, + then this is set to + <command>msmtpq --read-envelope-from --read-recipients</command>. + ''; + }; + + extraConfig = mkOption { + type = types.attrs; + default = { }; + example = { select_query = ""; }; + description = '' + Extra settings to add to this astroid account configuration. + ''; + }; + }; + + config = mkIf config.notmuch.enable { + astroid.sendMailCommand = mkIf config.msmtp.enable + (mkOptionDefault "msmtpq --read-envelope-from --read-recipients"); + }; +} diff --git a/home-manager/modules/programs/astroid-config-template.json b/home-manager/modules/programs/astroid-config-template.json new file mode 100644 index 00000000000..87e3f764f9c --- /dev/null +++ b/home-manager/modules/programs/astroid-config-template.json @@ -0,0 +1,113 @@ +{ + "astroid": { + "config": { + "version": "11" + }, + "debug": { + "dryrun_sending": "false" + }, + "hints": { + "level": "0" + }, + "log": { + "syslog": "false", + "stdout": "true", + "level": "info" + } + }, + "startup": { + "queries": { + "inbox": "tag:inbox" + } + }, + "terminal": { + "height": "10", + "font_description": "default" + }, + "thread_index": { + "page_jump_rows": "6", + "sort_order": "newest", + "cell": { + "font_description": "default", + "line_spacing": "2", + "date_length": "10", + "message_count_length": "4", + "authors_length": "20", + "subject_color": "#807d74", + "subject_color_selected": "#000000", + "background_color_selected": "", + "background_color_marked": "#fff584", + "background_color_marked_selected": "#bcb559", + "tags_length": "80", + "tags_upper_color": "#e5e5e5", + "tags_lower_color": "#333333", + "tags_alpha": "0.5", + "hidden_tags": "attachment,flagged,unread" + } + }, + "general": { + "time": { + "clock_format": "local", + "same_year": "%b %-e", + "diff_year": "%x" + } + }, + "editor": { + "charset": "utf-8", + "save_draft_on_force_quit": "true", + "attachment_words": "attach", + "attachment_directory": "~", + "markdown_processor": "marked" + }, + "mail": { + "reply": { + "quote_line": "Excerpts from %1's message of %2:", + "mailinglist_reply_to_sender": "true" + }, + "forward": { + "quote_line": "Forwarding %1's message of %2:", + "disposition": "inline" + }, + "sent_tags": "sent", + "message_id_fqdn": "", + "message_id_user": "", + "user_agent": "default", + "send_delay": "2", + "close_on_success": "false", + "format_flowed": "false" + }, + "poll": { + "interval": "60", + "always_full_refresh": "false" + }, + "attachment": { + "external_open_cmd": "xdg-open" + }, + "thread_view": { + "open_html_part_external": "false", + "preferred_type": "plain", + "preferred_html_only": "false", + "allow_remote_when_encrypted": "false", + "open_external_link": "xdg-open", + "default_save_directory": "~", + "indent_messages": "false", + "gravatar": { + "enable": "true" + }, + "mark_unread_delay": "0.5", + "expand_flagged": "true" + }, + "crypto": { + "gpg": { + "path": "gpg2", + "always_trust": "true", + "enabled": "true" + } + }, + "saved_searches": { + "show_on_startup": "false", + "save_history": "true", + "history_lines_to_show": "15", + "history_lines": "1000" + } +} diff --git a/home-manager/modules/programs/astroid.nix b/home-manager/modules/programs/astroid.nix new file mode 100644 index 00000000000..af12b10edbb --- /dev/null +++ b/home-manager/modules/programs/astroid.nix @@ -0,0 +1,127 @@ +{ config, lib, pkgs, ... }: + +with lib; +with builtins; + +let + + cfg = config.programs.astroid; + + astroidAccounts = + filterAttrs (n: v: v.astroid.enable) config.accounts.email.accounts; + + boolOpt = b: if b then "true" else "false"; + + accountAttr = account: + with account; + { + email = address; + name = realName; + sendmail = astroid.sendMailCommand; + additional_sent_tags = ""; + default = boolOpt primary; + save_drafts_to = "${maildir.absPath}/${folders.drafts}"; + save_sent = "true"; + save_sent_to = "${maildir.absPath}/${folders.sent}"; + select_query = ""; + } // optionalAttrs (signature.showSignature != "none") { + signature_attach = boolOpt (signature.showSignature == "attach"); + signature_default_on = boolOpt (signature.showSignature != "none"); + signature_file = pkgs.writeText "signature.txt" signature.text; + signature_file_markdown = "false"; + signature_separate = "true"; # prepends '--\n' to the signature + } // optionalAttrs (gpg != null) { + always_gpg_sign = boolOpt gpg.signByDefault; + gpgkey = gpg.key; + } // astroid.extraConfig; + + # See https://github.com/astroidmail/astroid/wiki/Configuration-Reference + configFile = mailAccounts: + let + template = fromJSON (readFile ./astroid-config-template.json); + astroidConfig = foldl' recursiveUpdate template [ + { + astroid.notmuch_config = "${config.xdg.configHome}/notmuch/notmuchrc"; + accounts = mapAttrs (n: accountAttr) astroidAccounts; + crypto.gpg.path = "${pkgs.gnupg}/bin/gpg"; + } + cfg.extraConfig + cfg.externalEditor + ]; + in builtins.toJSON astroidConfig; + +in { + options = { + programs.astroid = { + enable = mkEnableOption "Astroid"; + + pollScript = mkOption { + type = types.str; + default = ""; + example = "mbsync gmail"; + description = '' + Script to run to fetch/update mails. + ''; + }; + + externalEditor = mkOption { + type = types.nullOr types.str; + default = null; + # Converts it into JSON that can be merged into the configuration. + apply = cmd: + optionalAttrs (cmd != null) { + editor = { + "external_editor" = "true"; + "cmd" = cmd; + }; + }; + example = + "nvim-qt -- -c 'set ft=mail' '+set fileencoding=utf-8' '+set ff=unix' '+set enc=utf-8' '+set fo+=w' %1"; + description = '' + You can use <code>%1</code>, <code>%2</code>, and + <code>%3</code> to refer respectively to: + <orderedlist numeration="arabic"> + <listitem><para>file name</para></listitem> + <listitem><para>server name</para></listitem> + <listitem><para>socket ID</para></listitem> + </orderedlist> + See <link xlink:href='https://github.com/astroidmail/astroid/wiki/Customizing-editor' />. + ''; + }; + + extraConfig = mkOption { + type = types.attrs; + default = { }; + example = { poll.interval = 0; }; + description = '' + JSON config that will override the default Astroid configuration. + ''; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./astroid-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.astroid ]; + + xdg.configFile."astroid/config".source = pkgs.runCommand "out.json" { + json = configFile astroidAccounts; + preferLocalBuild = true; + allowSubstitutes = false; + } '' + echo -n "$json" | ${pkgs.jq}/bin/jq . > $out + ''; + + xdg.configFile."astroid/poll.sh" = { + executable = true; + text = '' + # Generated by Home Manager + + ${cfg.pollScript} + ''; + }; + }; +} diff --git a/home-manager/modules/programs/autorandr.nix b/home-manager/modules/programs/autorandr.nix new file mode 100644 index 00000000000..40cad704db9 --- /dev/null +++ b/home-manager/modules/programs/autorandr.nix @@ -0,0 +1,364 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.autorandr; + + matrixOf = n: m: elemType: + mkOptionType rec { + name = "matrixOf"; + description = + "${toString n}×${toString m} matrix of ${elemType.description}s"; + check = xss: + let listOfSize = l: xs: isList xs && length xs == l; + in listOfSize n xss + && all (xs: listOfSize m xs && all elemType.check xs) xss; + merge = mergeOneOption; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]); + getSubModules = elemType.getSubModules; + substSubModules = mod: matrixOf n m (elemType.substSubModules mod); + functor = (defaultFunctor name) // { wrapped = elemType; }; + }; + + profileModule = types.submodule { + options = { + fingerprint = mkOption { + type = types.attrsOf types.str; + description = '' + Output name to EDID mapping. + Use <code>autorandr --fingerprint</code> to get current setup values. + ''; + default = { }; + }; + + config = mkOption { + type = types.attrsOf configModule; + description = "Per output profile configuration."; + default = { }; + }; + + hooks = mkOption { + type = profileHooksModule; + description = "Profile hook scripts."; + default = { }; + }; + }; + }; + + configModule = types.submodule { + options = { + enable = mkOption { + type = types.bool; + description = "Whether to enable the output."; + 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"; + default = false; + }; + + position = mkOption { + type = types.str; + description = "Output position"; + default = ""; + example = "5760x0"; + }; + + mode = mkOption { + type = types.str; + description = "Output resolution."; + default = ""; + example = "3840x2160"; + }; + + rate = mkOption { + type = types.str; + description = "Output framerate."; + default = ""; + example = "60.00"; + }; + + gamma = mkOption { + type = types.str; + description = "Output gamma configuration."; + default = ""; + example = "1.0:0.909:0.833"; + }; + + rotate = mkOption { + type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]); + description = "Output rotate configuration."; + default = null; + example = "left"; + }; + + transform = mkOption { + type = types.nullOr (matrixOf 3 3 types.float); + default = null; + example = literalExample '' + [ + [ 0.6 0.0 0.0 ] + [ 0.0 0.6 0.0 ] + [ 0.0 0.0 1.0 ] + ] + ''; + description = '' + Refer to + <citerefentry> + <refentrytitle>xrandr</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for the documentation of the transform matrix. + ''; + }; + + dpi = mkOption { + type = types.nullOr types.ints.positive; + description = "Output DPI configuration."; + default = null; + example = 96; + }; + + scale = mkOption { + type = types.nullOr (types.submodule { + options = { + method = mkOption { + type = types.enum [ "factor" "pixel" ]; + description = "Output scaling method."; + default = "factor"; + example = "pixel"; + }; + + x = mkOption { + type = types.either types.float types.ints.positive; + description = "Horizontal scaling factor/pixels."; + }; + + y = mkOption { + type = types.either types.float types.ints.positive; + description = "Vertical scaling factor/pixels."; + }; + }; + }); + description = '' + Output scale configuration. + </para><para> + Either configure by pixels or a scaling factor. When using pixel method the + <citerefentry> + <refentrytitle>xrandr</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + option + <parameter class="command">--scale-from</parameter> + will be used; when using factor method the option + <parameter class="command">--scale</parameter> + will be used. + </para><para> + This option is a shortcut version of the transform option and they are mutually + exclusive. + ''; + default = null; + example = literalExample '' + { + x = 1.25; + y = 1.25; + } + ''; + }; + }; + }; + + hookType = types.lines; + + globalHooksModule = types.submodule { + options = { + postswitch = mkOption { + type = types.attrsOf hookType; + description = "Postswitch hook executed after mode switch."; + default = { }; + }; + + preswitch = mkOption { + type = types.attrsOf hookType; + description = "Preswitch hook executed before mode switch."; + default = { }; + }; + + predetect = mkOption { + type = types.attrsOf hookType; + description = '' + Predetect hook executed before autorandr attempts to run xrandr. + ''; + default = { }; + }; + }; + }; + + profileHooksModule = types.submodule { + options = { + postswitch = mkOption { + type = hookType; + description = "Postswitch hook executed after mode switch."; + default = ""; + }; + + preswitch = mkOption { + type = hookType; + description = "Preswitch hook executed before mode switch."; + default = ""; + }; + + predetect = mkOption { + type = hookType; + description = '' + Predetect hook executed before autorandr attempts to run xrandr. + ''; + default = ""; + }; + }; + }; + + hookToFile = folder: name: hook: + nameValuePair "autorandr/${folder}/${name}" { + source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook"; + }; + profileToFiles = name: profile: + with profile; + mkMerge ([ + { + "autorandr/${name}/setup".text = concatStringsSep "\n" + (mapAttrsToList fingerprintToString fingerprint); + "autorandr/${name}/config".text = + concatStringsSep "\n" (mapAttrsToList configToString profile.config); + } + (mkIf (hooks.postswitch != "") + (listToAttrs [ (hookToFile name "postswitch" hooks.postswitch) ])) + (mkIf (hooks.preswitch != "") + (listToAttrs [ (hookToFile name "preswitch" hooks.preswitch) ])) + (mkIf (hooks.predetect != "") + (listToAttrs [ (hookToFile name "predetect" hooks.predetect) ])) + ]); + fingerprintToString = name: edid: "${name} ${edid}"; + configToString = name: config: + 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 + ''; + +in { + options = { + programs.autorandr = { + enable = mkEnableOption "Autorandr"; + + hooks = mkOption { + type = globalHooksModule; + description = "Global hook scripts"; + default = { }; + example = literalExample '' + { + postswitch = { + "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart"; + "change-background" = readFile ./change-background.sh; + "change-dpi" = ''' + case "$AUTORANDR_CURRENT_PROFILE" in + default) + DPI=120 + ;; + home) + DPI=192 + ;; + work) + DPI=144 + ;; + *) + echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE" + exit 1 + esac + + echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge + ''' + }; + } + ''; + }; + + profiles = mkOption { + type = types.attrsOf profileModule; + description = "Autorandr profiles specification."; + default = { }; + example = literalExample '' + { + "work" = { + fingerprint = { + eDP1 = "<EDID>"; + DP1 = "<EDID>"; + }; + config = { + eDP1.enable = false; + DP1 = { + enable = true; + crtc = 0; + primary = true; + position = "0x0"; + mode = "3840x2160"; + gamma = "1.0:0.909:0.833"; + rate = "60.00"; + rotate = "left"; + }; + }; + hooks.postswitch = readFile ./work-postswitch.sh; + }; + } + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = flatten (mapAttrsToList (profile: + { config, ... }: + mapAttrsToList (output: opts: { + assertion = opts.scale == null || opts.transform == null; + message = '' + Cannot use the profile output options 'scale' and 'transform' simultaneously. + Check configuration for: programs.autorandr.profiles.${profile}.config.${output} + ''; + }) config) cfg.profiles); + + home.packages = [ pkgs.autorandr ]; + xdg.configFile = mkMerge ([ + (mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch) + (mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch) + (mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect) + (mkMerge (mapAttrsToList profileToFiles cfg.profiles)) + ]); + }; + + meta.maintainers = [ maintainers.uvnikita ]; +} diff --git a/home-manager/modules/programs/bash.nix b/home-manager/modules/programs/bash.nix new file mode 100644 index 00000000000..45fe368bddc --- /dev/null +++ b/home-manager/modules/programs/bash.nix @@ -0,0 +1,224 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.bash; + +in + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.bash = { + enable = mkEnableOption "GNU Bourne-Again SHell"; + + historySize = mkOption { + type = types.int; + default = 10000; + description = "Number of history lines to keep in memory."; + }; + + historyFile = mkOption { + type = types.str; + default = "$HOME/.bash_history"; + description = "Location of the bash history file."; + }; + + historyFileSize = mkOption { + type = types.int; + default = 100000; + description = "Number of history lines to keep on file."; + }; + + historyControl = mkOption { + type = types.listOf (types.enum [ + "erasedups" + "ignoredups" + "ignorespace" + ]); + default = []; + description = "Controlling how commands are saved on the history list."; + }; + + historyIgnore = mkOption { + type = types.listOf types.str; + default = []; + example = [ "ls" "cd" "exit" ]; + description = "List of commands that should not be saved to the history list."; + }; + + shellOptions = mkOption { + type = types.listOf types.str; + default = [ + # Append to history file rather than replacing it. + "histappend" + + # check the window size after each command and, if + # necessary, update the values of LINES and COLUMNS. + "checkwinsize" + + # Extended globbing. + "extglob" + "globstar" + + # Warn if closing shell with running jobs. + "checkjobs" + ]; + description = "Shell options to set."; + }; + + sessionVariables = mkOption { + default = {}; + type = types.attrs; + example = { MAILCHECK = 30; }; + description = '' + Environment variables that will be set for the Bash session. + ''; + }; + + shellAliases = mkOption { + default = {}; + type = types.attrsOf types.str; + 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. + ''; + }; + + enableAutojump = mkOption { + default = false; + type = types.bool; + description = "Enable the autojump navigation tool."; + }; + + profileExtra = mkOption { + default = ""; + type = types.lines; + description = '' + Extra commands that should be run when initializing a login + shell. + ''; + }; + + bashrcExtra = mkOption { + # Hide for now, may want to rename in the future. + visible = false; + default = ""; + type = types.lines; + description = '' + Extra commands that should be added to + <filename>~/.bashrc</filename>. + ''; + }; + + initExtra = mkOption { + default = ""; + type = types.lines; + description = '' + Extra commands that should be run when initializing an + interactive shell. + ''; + }; + + logoutExtra = mkOption { + default = ""; + type = types.lines; + description = '' + Extra commands that should be run when logging out of an + interactive shell. + ''; + }; + }; + }; + + config = ( + let + aliasesStr = concatStringsSep "\n" ( + mapAttrsToList (k: v: "alias ${k}=${escapeShellArg v}") cfg.shellAliases + ); + + shoptsStr = concatStringsSep "\n" ( + map (v: "shopt -s ${v}") cfg.shellOptions + ); + + sessionVarsStr = config.lib.shell.exportAll cfg.sessionVariables; + + historyControlStr = + concatStringsSep "\n" (mapAttrsToList (n: v: "${n}=${v}") ( + { + HISTFILE = "\"${cfg.historyFile}\""; + HISTFILESIZE = toString cfg.historyFileSize; + HISTSIZE = toString cfg.historySize; + } + // optionalAttrs (cfg.historyControl != []) { + HISTCONTROL = concatStringsSep ":" cfg.historyControl; + } + // optionalAttrs (cfg.historyIgnore != []) { + HISTIGNORE = concatStringsSep ":" cfg.historyIgnore; + } + )); + in mkIf cfg.enable { + programs.bash.bashrcExtra = '' + # Commands that should be applied only for interactive shells. + if [[ $- == *i* ]]; then + ${historyControlStr} + + ${shoptsStr} + + ${aliasesStr} + + ${optionalString cfg.enableAutojump + ". ${pkgs.autojump}/share/autojump/autojump.bash"} + + ${cfg.initExtra} + fi + ''; + + home.file.".bash_profile".text = '' + # -*- mode: sh -*- + + # include .profile if it exists + [[ -f ~/.profile ]] && . ~/.profile + + # include .bashrc if it exists + [[ -f ~/.bashrc ]] && . ~/.bashrc + ''; + + home.file.".profile".text = '' + # -*- mode: sh -*- + + . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh" + + ${sessionVarsStr} + + ${cfg.profileExtra} + ''; + + home.file.".bashrc".text = '' + # -*- mode: sh -*- + + ${cfg.bashrcExtra} + ''; + + home.file.".bash_logout" = mkIf (cfg.logoutExtra != "") { + text = '' + # -*- mode: sh -*- + + ${cfg.logoutExtra} + ''; + }; + + home.packages = + optional (cfg.enableAutojump) pkgs.autojump; + } + ); +} diff --git a/home-manager/modules/programs/bat.nix b/home-manager/modules/programs/bat.nix new file mode 100644 index 00000000000..e2b30ea9333 --- /dev/null +++ b/home-manager/modules/programs/bat.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.bat; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.bat = { + enable = mkEnableOption "bat, a cat clone with wings"; + + config = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { + theme = "TwoDark"; + pager = "less -FR"; + }; + description = '' + Bat configuration. + ''; + }; + + 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 = 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/beets.nix b/home-manager/modules/programs/beets.nix new file mode 100644 index 00000000000..1a45bbea1c7 --- /dev/null +++ b/home-manager/modules/programs/beets.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.beets; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.beets = { + enable = mkOption { + type = types.bool; + default = if versionAtLeast config.home.stateVersion "19.03" then + false + else + cfg.settings != { }; + defaultText = "false"; + description = '' + Whether to enable the beets music library manager. This + defaults to <literal>false</literal> for state + version ≥ 19.03. For earlier versions beets is enabled if + <option>programs.beets.settings</option> is non-empty. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.beets; + defaultText = literalExample "pkgs.beets"; + example = + literalExample "(pkgs.beets.override { enableCheck = true; })"; + description = '' + The <literal>beets</literal> package to use. + Can be used to specify extensions. + ''; + }; + + settings = mkOption { + type = types.attrs; + default = { }; + description = '' + Configuration written to + <filename>~/.config/beets/config.yaml</filename> + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xdg.configFile."beets/config.yaml".text = + builtins.toJSON config.programs.beets.settings; + }; +} diff --git a/home-manager/modules/programs/broot.nix b/home-manager/modules/programs/broot.nix new file mode 100644 index 00000000000..6951e035d32 --- /dev/null +++ b/home-manager/modules/programs/broot.nix @@ -0,0 +1,256 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.broot; + + configFile = config: + pkgs.runCommand "conf.toml" { + buildInputs = [ pkgs.remarshal ]; + preferLocalBuild = true; + allowSubstitutes = false; + } '' + remarshal -if json -of toml \ + < ${pkgs.writeText "verbs.json" (builtins.toJSON config)} \ + > $out + ''; + + brootConf = { + verbs = + mapAttrsToList (name: value: value // { invocation = name; }) cfg.verbs; + skin = cfg.skin; + }; + +in { + meta.maintainers = [ maintainers.aheaume ]; + + options.programs.broot = { + enable = mkEnableOption "Broot, a better way to navigate directories"; + + 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. + ''; + }; + + verbs = mkOption { + type = with types; attrsOf (attrsOf (either bool str)); + default = { + "p" = { execution = ":parent"; }; + "edit" = { + shortcut = "e"; + execution = "$EDITOR {file}"; + }; + "create {subpath}" = { execution = "$EDITOR {directory}/{subpath}"; }; + "view" = { execution = "less {file}"; }; + }; + example = literalExample '' + { + "p" = { execution = ":parent"; }; + "edit" = { shortcut = "e"; execution = "$EDITOR {file}" ; }; + "create {subpath}" = { execution = "$EDITOR {directory}/{subpath}"; }; + "view" = { execution = "less {file}"; }; + "blop {name}\\.{type}" = { + execution = "/bin/mkdir {parent}/{type} && /usr/bin/nvim {parent}/{type}/{name}.{type}"; + from_shell = true; + }; + } + ''; + description = '' + Define new verbs. The attribute name indicates how the verb is + called by the user, with placeholders for arguments. + </para><para> + The possible attributes are: + </para> + + <para> + <variablelist> + <varlistentry> + <term><literal>execution</literal> (mandatory)</term> + <listitem><para>how the verb is executed</para></listitem> + </varlistentry> + <varlistentry> + <term><literal>shortcut</literal> (optional)</term> + <listitem><para>an alternate way to call the verb (without + the arguments part)</para></listitem> + </varlistentry> + <varlistentry> + <term><literal>leave_broot</literal> (optional)</term> + <listitem><para>whether to quit broot on execution + (default: <literal>true</literal>)</para></listitem> + </varlistentry> + <varlistentry> + <term><literal>from_shell</literal> (optional)</term> + <listitem><para>whether the verb must be executed from the + parent shell (default: + <literal>false</literal>)</para></listitem> + </varlistentry> + </variablelist> + ''; + }; + + skin = mkOption { + type = types.attrsOf types.str; + default = { }; + example = literalExample '' + { + status_normal_fg = "grayscale(18)"; + status_normal_bg = "grayscale(3)"; + status_error_fg = "red"; + status_error_bg = "yellow"; + tree_fg = "red"; + selected_line_bg = "grayscale(7)"; + permissions_fg = "grayscale(12)"; + size_bar_full_bg = "red"; + size_bar_void_bg = "black"; + directory_fg = "lightyellow"; + input_fg = "cyan"; + flag_value_fg = "lightyellow"; + table_border_fg = "red"; + code_fg = "lightyellow"; + } + ''; + description = '' + Color configuration. + </para><para> + Complete list of keys (expected to change before the v1 of broot): + + <itemizedlist> + <listitem><para><literal>char_match</literal></para></listitem> + <listitem><para><literal>code</literal></para></listitem> + <listitem><para><literal>directory</literal></para></listitem> + <listitem><para><literal>exe</literal></para></listitem> + <listitem><para><literal>file</literal></para></listitem> + <listitem><para><literal>file_error</literal></para></listitem> + <listitem><para><literal>flag_label</literal></para></listitem> + <listitem><para><literal>flag_value</literal></para></listitem> + <listitem><para><literal>input</literal></para></listitem> + <listitem><para><literal>link</literal></para></listitem> + <listitem><para><literal>permissions</literal></para></listitem> + <listitem><para><literal>selected_line</literal></para></listitem> + <listitem><para><literal>size_bar_full</literal></para></listitem> + <listitem><para><literal>size_bar_void</literal></para></listitem> + <listitem><para><literal>size_text</literal></para></listitem> + <listitem><para><literal>spinner</literal></para></listitem> + <listitem><para><literal>status_error</literal></para></listitem> + <listitem><para><literal>status_normal</literal></para></listitem> + <listitem><para><literal>table_border</literal></para></listitem> + <listitem><para><literal>tree</literal></para></listitem> + <listitem><para><literal>unlisted</literal></para></listitem> + </itemizedlist></para> + + <para> + Add <literal>_fg</literal> for a foreground color and + <literal>_bg</literal> for a background colors. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.broot ]; + + xdg.configFile."broot/conf.toml".source = configFile brootConf; + + # Dummy file to prevent broot from trying to reinstall itself + 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. + mkAfter '' + # This script was automatically generated by the broot function + # More information can be found in https://github.com/Canop/broot + # This function starts broot and executes the command + # it produces, if any. + # It's needed because some shell commands, like `cd`, + # have no useful effect if executed in a subshell. + function br { + f=$(mktemp) + ( + set +e + broot --outcmd "$f" "$@" + code=$? + if [ "$code" != 0 ]; then + rm -f "$f" + exit "$code" + fi + ) + code=$? + if [ "$code" != 0 ]; then + return "$code" + fi + d=$(cat "$f") + rm -f "$f" + eval "$d" + } + ''); + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + # This script was automatically generated by the broot function + # More information can be found in https://github.com/Canop/broot + # This function starts broot and executes the command + # it produces, if any. + # It's needed because some shell commands, like `cd`, + # have no useful effect if executed in a subshell. + function br { + f=$(mktemp) + ( + set +e + broot --outcmd "$f" "$@" + code=$? + if [ "$code" != 0 ]; then + rm -f "$f" + exit "$code" + fi + ) + code=$? + if [ "$code" != 0 ]; then + return "$code" + fi + d=$(cat "$f") + rm -f "$f" + eval "$d" + } + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + # This script was automatically generated by the broot function + # More information can be found in https://github.com/Canop/broot + # This function starts broot and executes the command + # it produces, if any. + # It's needed because some shell commands, like `cd`, + # have no useful effect if executed in a subshell. + function br + set f (mktemp) + broot --outcmd $f $argv + if test $status -ne 0 + rm -f "$f" + return "$code" + end + set d (cat "$f") + rm -f "$f" + eval "$d" + end + ''; + }; +} diff --git a/home-manager/modules/programs/browserpass.nix b/home-manager/modules/programs/browserpass.nix new file mode 100644 index 00000000000..10a2883c871 --- /dev/null +++ b/home-manager/modules/programs/browserpass.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let browsers = [ "chrome" "chromium" "firefox" "vivaldi" ]; +in { + options = { + programs.browserpass = { + enable = mkEnableOption "the browserpass extension host application"; + + browsers = mkOption { + type = types.listOf (types.enum browsers); + default = browsers; + example = [ "firefox" ]; + description = "Which browsers to install browserpass for"; + }; + }; + }; + + config = mkIf config.programs.browserpass.enable { + home.file = foldl' (a: b: a // b) { } (concatMap (x: + with pkgs.stdenv; + if x == "chrome" then + let + dir = if isDarwin then + "Library/Application Support/Google/Chrome/NativeMessagingHosts" + else + ".config/google-chrome/NativeMessagingHosts"; + in [{ + "${dir}/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json"; + "${dir}/../policies/managed/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/policies/chromium/com.github.browserpass.native.json"; + }] + else if x == "chromium" then + let + dir = if isDarwin then + "Library/Application Support/Chromium/NativeMessagingHosts" + else + ".config/chromium/NativeMessagingHosts"; + in [ + { + "${dir}/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json"; + } + { + "${dir}/../policies/managed/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/policies/chromium/com.github.browserpass.native.json"; + } + ] + else if x == "firefox" then + let + dir = if isDarwin then + "Library/Application Support/Mozilla/NativeMessagingHosts" + else + ".mozilla/native-messaging-hosts"; + in [{ + "${dir}/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/hosts/firefox/com.github.browserpass.native.json"; + }] + else if x == "vivaldi" then + let + dir = if isDarwin then + "Library/Application Support/Vivaldi/NativeMessagingHosts" + else + ".config/vivaldi/NativeMessagingHosts"; + in [{ + "${dir}/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json"; + "${dir}/../policies/managed/com.github.browserpass.native.json".source = + "${pkgs.browserpass}/lib/browserpass/policies/chromium/com.github.browserpass.native.json"; + }] + else + throw "unknown browser ${x}") config.programs.browserpass.browsers); + }; +} diff --git a/home-manager/modules/programs/chromium.nix b/home-manager/modules/programs/chromium.nix new file mode 100644 index 00000000000..4e35c07b90c --- /dev/null +++ b/home-manager/modules/programs/chromium.nix @@ -0,0 +1,92 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + browserModule = defaultPkg: name: visible: + let browser = (builtins.parseDrvName defaultPkg.name).name; + in { + enable = mkOption { + inherit visible; + default = false; + example = true; + description = "Whether to enable ${name}."; + type = lib.types.bool; + }; + + package = mkOption { + inherit visible; + type = types.package; + default = defaultPkg; + defaultText = literalExample "pkgs.${browser}"; + description = "The ${name} package to use."; + }; + + extensions = mkOption { + inherit visible; + type = types.listOf types.str; + default = [ ]; + example = literalExample '' + [ + "chlffgpmiacpedhhbkiomidkjlcfhogd" # pushbullet + "mbniclmhobmnbdlbpiphghaielnnpgdp" # lightshot + "gcbommkclmclpchllfjekcdonpmejbdp" # https everywhere + "cjpalhdlnbpafiamejdnhcphjbkeiagm" # ublock origin + ] + ''; + description = '' + List of ${name} extensions to install. + To find the extension ID, check its URL on the + <link xlink:href="https://chrome.google.com/webstore/category/extensions">Chrome Web Store</link>. + ''; + }; + }; + + browserConfig = cfg: + let + + browser = (builtins.parseDrvName cfg.package.name).name; + + darwinDirs = { + chromium = "Chromium"; + google-chrome = "Google/Chrome"; + google-chrome-beta = "Google/Chrome Beta"; + google-chrome-dev = "Google/Chrome Dev"; + }; + + configDir = if pkgs.stdenv.isDarwin then + "Library/Application Support/${getAttr browser darwinDirs}" + else + "${config.xdg.configHome}/${browser}"; + + extensionJson = ext: { + name = "${configDir}/External Extensions/${ext}.json"; + value.text = builtins.toJSON { + external_update_url = + "https://clients2.google.com/service/update2/crx"; + }; + }; + + in mkIf cfg.enable { + home.packages = [ cfg.package ]; + home.file = listToAttrs (map extensionJson cfg.extensions); + }; + +in { + options.programs = { + chromium = browserModule pkgs.chromium "Chromium" true; + google-chrome = browserModule pkgs.google-chrome "Google Chrome" false; + google-chrome-beta = + browserModule pkgs.google-chrome-beta "Google Chrome Beta" false; + google-chrome-dev = + browserModule pkgs.google-chrome-dev "Google Chrome Dev" false; + }; + + config = mkMerge [ + (browserConfig config.programs.chromium) + (browserConfig config.programs.google-chrome) + (browserConfig config.programs.google-chrome-beta) + (browserConfig config.programs.google-chrome-dev) + ]; +} diff --git a/home-manager/modules/programs/command-not-found/command-not-found.nix b/home-manager/modules/programs/command-not-found/command-not-found.nix new file mode 100644 index 00000000000..b79fde0f619 --- /dev/null +++ b/home-manager/modules/programs/command-not-found/command-not-found.nix @@ -0,0 +1,59 @@ +# Adapted from Nixpkgs. + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.command-not-found; + commandNotFound = pkgs.substituteAll { + name = "command-not-found"; + dir = "bin"; + src = ./command-not-found.pl; + isExecutable = true; + inherit (pkgs) perl; + inherit (cfg) dbPath; + perlFlags = concatStrings (map (path: "-I ${path}/lib/perl5/site_perl ") [ + pkgs.perlPackages.DBI + pkgs.perlPackages.DBDSQLite + pkgs.perlPackages.StringShellQuote + ]); + }; + + shInit = commandNotFoundHandlerName: '' + # This function is called whenever a command is not found. + ${commandNotFoundHandlerName}() { + local p=${commandNotFound}/bin/command-not-found + if [ -x $p -a -f ${cfg.dbPath} ]; then + # Run the helper program. + $p "$@" + else + echo "$1: command not found" >&2 + return 127 + fi + } + ''; + +in { + options.programs.command-not-found = { + enable = mkEnableOption "command-not-found hook for interactive shell"; + + dbPath = mkOption { + default = + "/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite"; + description = '' + Absolute path to <filename>programs.sqlite</filename>. By + default this file will be provided by your channel + (nixexprs.tar.xz). + ''; + type = types.path; + }; + }; + + config = mkIf cfg.enable { + programs.bash.initExtra = shInit "command_not_found_handle"; + programs.zsh.initExtra = shInit "command_not_found_handler"; + + home.packages = [ commandNotFound ]; + }; +} diff --git a/home-manager/modules/programs/command-not-found/command-not-found.pl b/home-manager/modules/programs/command-not-found/command-not-found.pl new file mode 100644 index 00000000000..997dfec649b --- /dev/null +++ b/home-manager/modules/programs/command-not-found/command-not-found.pl @@ -0,0 +1,44 @@ +#! @perl@/bin/perl -w @perlFlags@ + +use strict; +use DBI; +use DBD::SQLite; +use String::ShellQuote; +use Config; + +my $program = $ARGV[0]; + +my $dbPath = "@dbPath@"; + +my $dbh = DBI->connect("dbi:SQLite:dbname=$dbPath", "", "") + or die "cannot open database `$dbPath'"; +$dbh->{RaiseError} = 0; +$dbh->{PrintError} = 0; + +my $system = $ENV{"NIX_SYSTEM"} // $Config{myarchname}; + +my $res = $dbh->selectall_arrayref( + "select package from Programs where system = ? and name = ?", + { Slice => {} }, $system, $program); + +if (!defined $res || scalar @$res == 0) { + print STDERR "$program: command not found\n"; +} elsif (scalar @$res == 1) { + my $package = @$res[0]->{package}; + if ($ENV{"NIX_AUTO_RUN"} // "") { + exec("nix-shell", "-p", $package, "--run", shell_quote("exec", @ARGV)); + } else { + print STDERR <<EOF; +The program ‘$program’ is currently not installed. You can install it by typing: + nix-env -iA nixos.$package +EOF + } +} else { + print STDERR <<EOF; +The program ‘$program’ is currently not installed. It is provided by +several packages. You can install it by typing one of the following: +EOF + print STDERR " nix-env -iA nixos.$_->{package}\n" foreach @$res; +} + +exit 127; 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 new file mode 100644 index 00000000000..1d1374b8e26 --- /dev/null +++ b/home-manager/modules/programs/direnv.nix @@ -0,0 +1,107 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.direnv; + 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.rycee ]; + + options.programs.direnv = { + enable = mkEnableOption "direnv, the environment switcher"; + + config = mkOption { + type = types.attrs; + default = { }; + description = '' + Configuration written to + <filename>~/.config/direnv/config.toml</filename>. + </para><para> + See + <citerefentry> + <refentrytitle>direnv.toml</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>. + for the full list of options. + ''; + }; + + stdlib = mkOption { + type = types.lines; + default = ""; + description = '' + Custom stdlib written to + <filename>~/.config/direnv/direnvrc</filename>. + ''; + }; + + 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. + ''; + }; + + 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 { + home.packages = [ pkgs.direnv ]; + + xdg.configFile."direnv/config.toml" = + mkIf (cfg.config != { }) { source = configFile cfg.config; }; + + 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. + mkAfter '' + eval "$(${pkgs.direnv}/bin/direnv hook bash)" + ''); + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + eval "$(${pkgs.direnv}/bin/direnv hook zsh)" + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + eval (${pkgs.direnv}/bin/direnv hook fish) + ''; + }; +} diff --git a/home-manager/modules/programs/eclipse.nix b/home-manager/modules/programs/eclipse.nix new file mode 100644 index 00000000000..21973ab937e --- /dev/null +++ b/home-manager/modules/programs/eclipse.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.eclipse; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + 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; + example = true; + description = '' + Whether to enable the Lombok Java Agent in Eclipse. This is + necessary to use the Lombok class annotations. + ''; + }; + + jvmArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "JVM arguments to use for the Eclipse process."; + }; + + plugins = mkOption { + type = types.listOf types.package; + default = [ ]; + description = "Plugins that should be added to Eclipse."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ + (pkgs.eclipses.eclipseWithPlugins { + 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 new file mode 100644 index 00000000000..b785f71358c --- /dev/null +++ b/home-manager/modules/programs/emacs.nix @@ -0,0 +1,73 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.emacs; + + # Copied from all-packages.nix, with modifications to support + # overrides. + emacsPackages = let epkgs = pkgs.emacsPackagesFor cfg.package; + in epkgs.overrideScope' cfg.overrides; + + emacsWithPackages = emacsPackages.emacsWithPackages; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.emacs = { + enable = mkEnableOption "Emacs"; + + package = mkOption { + type = types.package; + default = pkgs.emacs; + defaultText = literalExample "pkgs.emacs"; + example = literalExample "pkgs.emacs25-nox"; + description = "The Emacs package to use."; + }; + + extraPackages = mkOption { + default = self: [ ]; + type = hm.types.selectorFunction; + defaultText = "epkgs: []"; + example = literalExample "epkgs: [ epkgs.emms epkgs.magit ]"; + description = '' + Extra packages available to Emacs. To get a list of + available packages run: + <command>nix-env -f '<nixpkgs>' -qaP -A emacsPackages</command>. + ''; + }; + + overrides = mkOption { + default = self: super: { }; + type = hm.types.overlayFunction; + defaultText = "self: super: {}"; + example = literalExample '' + self: super: rec { + haskell-mode = self.melpaPackages.haskell-mode; + # ... + }; + ''; + description = '' + Allows overriding packages within the Emacs package set. + ''; + }; + + finalPackage = mkOption { + type = types.package; + visible = false; + readOnly = true; + description = '' + The Emacs package including any overrides and extra packages. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.finalPackage ]; + programs.emacs.finalPackage = emacsWithPackages cfg.extraPackages; + }; +} diff --git a/home-manager/modules/programs/feh.nix b/home-manager/modules/programs/feh.nix new file mode 100644 index 00000000000..b1b33697e95 --- /dev/null +++ b/home-manager/modules/programs/feh.nix @@ -0,0 +1,70 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.feh; + + disableBinding = func: key: func; + enableBinding = func: key: "${func} ${toString key}"; + +in { + options.programs.feh = { + enable = mkEnableOption "feh - a fast and light image viewer"; + + buttons = mkOption { + default = { }; + type = with types; attrsOf (nullOr (either str int)); + example = { + zoom_in = 4; + zoom_out = "C-4"; + }; + description = '' + Override feh's default mouse button mapping. If you want to disable an + action, set its value to null. + See <link xlink:href="https://man.finalrewind.org/1/feh/#x425554544f4e53"/> for + default bindings and available commands. + ''; + }; + + keybindings = mkOption { + default = { }; + type = types.attrsOf (types.nullOr types.str); + example = { + zoom_in = "plus"; + zoom_out = "minus"; + }; + description = '' + Override feh's default keybindings. If you want to disable a keybinding + set its value to null. + See <link xlink:href="https://man.finalrewind.org/1/feh/#x4b455953"/> for + default bindings and available commands. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = ((filterAttrs (n: v: v == "") cfg.keybindings) == { }); + message = + "To disable a keybinding, use `null` instead of an empty string."; + }]; + + home.packages = [ pkgs.feh ]; + + xdg.configFile."feh/buttons".text = '' + ${concatStringsSep "\n" (mapAttrsToList disableBinding + (filterAttrs (n: v: v == null) cfg.buttons))} + ${concatStringsSep "\n" (mapAttrsToList enableBinding + (filterAttrs (n: v: v != null) cfg.buttons))} + ''; + + xdg.configFile."feh/keys".text = '' + ${concatStringsSep "\n" (mapAttrsToList disableBinding + (filterAttrs (n: v: v == null) cfg.keybindings))} + ${concatStringsSep "\n" (mapAttrsToList enableBinding + (filterAttrs (n: v: v != null) cfg.keybindings))} + ''; + }; +} diff --git a/home-manager/modules/programs/firefox.nix b/home-manager/modules/programs/firefox.nix new file mode 100644 index 00000000000..d5003f59edc --- /dev/null +++ b/home-manager/modules/programs/firefox.nix @@ -0,0 +1,319 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inherit (pkgs.stdenv.hostPlatform) isDarwin; + + cfg = config.programs.firefox; + + mozillaConfigPath = + if isDarwin + then "Library/Application Support/Mozilla" + else ".mozilla"; + + firefoxConfigPath = + if isDarwin + then "Library/Application Support/Firefox" + else "${mozillaConfigPath}/firefox"; + + profilesPath = + if isDarwin + 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}" { + Name = profile.name; + Path = + if isDarwin + then "Profiles/${profile.path}" + else profile.path; + IsRelative = 1; + Default = if profile.isDefault then 1 else 0; + } + ) // { + General = { + StartWithLastProfile = 1; + }; + }; + + profilesIni = generators.toINI {} profiles; + + mkUserJs = prefs: extraPrefs: '' + // Generated by Home Manager. + + ${concatStrings (mapAttrsToList (name: value: '' + user_pref("${name}", ${builtins.toJSON value}); + '') prefs)} + + ${extraPrefs} + ''; + +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"; + + package = mkOption { + type = types.package; + default = + if versionAtLeast config.home.stateVersion "19.09" + then pkgs.firefox + else pkgs.firefox-unwrapped; + defaultText = literalExample "pkgs.firefox"; + description = '' + The Firefox package to use. If state version ≥ 19.09 then + this should be a wrapped Firefox package. For earlier state + versions it should be an unwrapped Firefox package. + ''; + }; + + extensions = mkOption { + type = types.listOf types.package; + default = []; + example = literalExample '' + with pkgs.nur.repos.rycee.firefox-addons; [ + https-everywhere + privacy-badger + ] + ''; + description = '' + 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. + ''; + }; + + profiles = mkOption { + type = types.attrsOf (types.submodule ({config, name, ...}: { + options = { + name = mkOption { + type = types.str; + default = name; + description = "Profile name."; + }; + + id = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + Profile ID. This should be set to a unique number per profile. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (either bool (either int str)); + default = {}; + example = literalExample '' + { + "browser.startup.homepage" = "https://nixos.org"; + "browser.search.region" = "GB"; + "browser.search.isUS" = false; + "distribution.searchplugins.defaultLocale" = "en-GB"; + "general.useragent.locale" = "en-GB"; + "browser.bookmarks.showMobileBookmarks" = true; + } + ''; + description = "Attribute set of Firefox preferences."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra preferences to add to <filename>user.js</filename>. + ''; + }; + + userChrome = mkOption { + type = types.lines; + default = ""; + description = "Custom Firefox user chrome CSS."; + example = '' + /* Hide tab bar in FF Quantum */ + @-moz-document url("chrome://browser/content/browser.xul") { + #TabsToolbar { + visibility: collapse !important; + margin-bottom: 21px !important; + } + + #sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header { + visibility: collapse !important; + } + } + ''; + }; + + 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; + description = "Profile path."; + }; + + isDefault = mkOption { + type = types.bool; + default = config.id == 0; + defaultText = "true if profile ID is 0"; + description = "Whether this is a default profile."; + }; + }; + })); + default = {}; + description = "Attribute set of Firefox profiles."; + }; + + enableAdobeFlash = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the unfree Adobe Flash plugin."; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + ( + let + defaults = + catAttrs "name" (filter (a: a.isDefault) (attrValues cfg.profiles)); + in { + assertion = cfg.profiles == {} || length defaults == 1; + message = + "Must have exactly one default Firefox profile but found " + + toString (length defaults) + + optionalString (length defaults > 1) + (", namely " + concatStringsSep ", " defaults); + } + ) + + ( + let + duplicates = + filterAttrs (_: v: length v != 1) + (zipAttrs + (mapAttrsToList (n: v: { "${toString v.id}" = n; }) + (cfg.profiles))); + + mkMsg = n: v: " - ID ${n} is used by ${concatStringsSep ", " v}"; + in { + assertion = duplicates == {}; + message = + "Must not have Firefox profiles with duplicate IDs but\n" + + concatStringsSep "\n" (mapAttrsToList mkMsg duplicates); + } + ) + ]; + + home.packages = + let + # The configuration expected by the Firefox wrapper. + fcfg = { + enableAdobeFlash = cfg.enableAdobeFlash; + }; + + # A bit of hackery to force a config into the wrapper. + browserName = cfg.package.browserName + or (builtins.parseDrvName cfg.package.name).name; + + # The configuration expected by the Firefox wrapper builder. + bcfg = setAttrByPath [browserName] fcfg; + + package = + if isDarwin then + cfg.package + else if versionAtLeast config.home.stateVersion "19.09" then + cfg.package.override { cfg = fcfg; } + else + (pkgs.wrapFirefox.override { config = bcfg; }) cfg.package { }; + in + [ package ]; + + home.file = mkMerge ( + [{ + "${mozillaConfigPath}/${extensionPath}" = mkIf (cfg.extensions != []) { + source = "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; + recursive = true; + }; + + "${firefoxConfigPath}/profiles.ini" = mkIf (cfg.profiles != {}) { + text = profilesIni; + }; + }] + ++ flip mapAttrsToList cfg.profiles (_: profile: { + "${profilesPath}/${profile.path}/chrome/userChrome.css" = + mkIf (profile.userChrome != "") { + 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 new file mode 100644 index 00000000000..730afa79262 --- /dev/null +++ b/home-manager/modules/programs/fish.nix @@ -0,0 +1,460 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.fish; + + 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. + ''; + }; + + name = mkOption { + type = types.str; + description = '' + The name of the plugin. + ''; + }; + }; + }); + + 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, 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. + ''; + }; + + shellAliases = mkOption { + type = with types; attrsOf str; + default = { }; + 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. + ''; + }; + + shellAbbrs = mkOption { + type = with types; attrsOf str; + default = { }; + example = { + l = "less"; + gco = "git checkout"; + }; + description = '' + 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. + ''; + }; + + shellInit = mkOption { + type = types.lines; + default = ""; + description = '' + Shell script code called during fish shell + initialisation. + ''; + }; + + loginShellInit = mkOption { + type = types.lines; + default = ""; + description = '' + Shell script code called during fish login shell + initialisation. + ''; + }; + + interactiveShellInit = mkOption { + type = types.lines; + default = ""; + description = '' + Shell script code called during interactive fish shell + initialisation. + ''; + }; + + promptInit = mkOption { + type = types.lines; + default = ""; + description = '' + Shell script code used to initialise fish prompt. + ''; + }; + }; + + 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 (mkMerge [ + { + home.packages = [ cfg.package ]; + + 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 ? "", ... }: + let + 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 \ + | xargs python ${cfg.package}/share/fish/tools/create_manpage_completions.py --directory $out \ + > /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); + }; + + 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; + } + + # 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 new file mode 100644 index 00000000000..3aee57768ea --- /dev/null +++ b/home-manager/modules/programs/fzf.nix @@ -0,0 +1,146 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.fzf; + +in { + options.programs.fzf = { + enable = mkEnableOption "fzf - a command-line fuzzy finder"; + + defaultCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "fd --type f"; + description = '' + The command that gets executed as the default source for fzf + when running. + ''; + }; + + defaultOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--height 40%" "--border" ]; + description = '' + Extra command line options given to fzf by default. + ''; + }; + + fileWidgetCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "fd --type f"; + description = '' + The command that gets executed as the source for fzf for the + CTRL-T keybinding. + ''; + }; + + fileWidgetOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--preview 'head {}'" ]; + description = '' + Command line options for the CTRL-T keybinding. + ''; + }; + + changeDirWidgetCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "fd --type d"; + description = '' + The command that gets executed as the source for fzf for the + ALT-C keybinding. + ''; + }; + + changeDirWidgetOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--preview 'tree -C {} | head -200'" ]; + description = '' + Command line options for the ALT-C keybinding. + ''; + }; + + historyWidgetCommand = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The command that gets executed as the source for fzf for the + CTRL-R keybinding. + ''; + }; + + historyWidgetOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--sort" "--exact" ]; + description = '' + Command line options for the CTRL-R keybinding. + ''; + }; + + 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 = [ pkgs.fzf ]; + + home.sessionVariables = mapAttrs (n: v: toString v) + (filterAttrs (n: v: v != [ ] && v != null) { + FZF_ALT_C_COMMAND = cfg.changeDirWidgetCommand; + FZF_ALT_C_OPTS = cfg.changeDirWidgetOptions; + FZF_CTRL_R_COMMAND = cfg.historyWidgetCommand; + FZF_CTRL_R_OPTS = cfg.historyWidgetOptions; + FZF_CTRL_T_COMMAND = cfg.fileWidgetCommand; + FZF_CTRL_T_OPTS = cfg.fileWidgetOptions; + FZF_DEFAULT_COMMAND = cfg.defaultCommand; + FZF_DEFAULT_OPTS = cfg.defaultOptions; + }); + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then + . ${pkgs.fzf}/share/fzf/completion.bash + . ${pkgs.fzf}/share/fzf/key-bindings.bash + fi + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + if [[ $options[zle] = on ]]; then + . ${pkgs.fzf}/share/fzf/completion.zsh + . ${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-accounts.nix b/home-manager/modules/programs/getmail-accounts.nix new file mode 100644 index 00000000000..24eb4fb588a --- /dev/null +++ b/home-manager/modules/programs/getmail-accounts.nix @@ -0,0 +1,49 @@ +{ config, lib, ... }: + +with lib; + +{ + options.getmail = { + enable = mkEnableOption "the getmail mail retriever for this account"; + + destinationCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "\${pkgs.maildrop}/bin/maildrop"; + description = '' + Specify a command delivering the incoming mail to your maildir. + ''; + }; + + mailboxes = mkOption { + type = types.nonEmptyListOf types.str; + default = [ ]; + example = [ "INBOX" "INBOX.spam" ]; + description = '' + A non-empty list of mailboxes. To download all mail you can + use the <literal>ALL</literal> mailbox. + ''; + }; + + delete = mkOption { + type = types.bool; + default = false; + description = '' + Enable if you want to delete read messages from the server. Most + users should either enable <literal>delete</literal> or disable + <literal>readAll</literal>. + ''; + }; + + readAll = mkOption { + type = types.bool; + default = true; + description = '' + Enable if you want to fetch all, even the read messages from the + server. Most users should either enable <literal>delete</literal> or + disable <literal>readAll</literal>. + ''; + }; + + }; +} diff --git a/home-manager/modules/programs/getmail.nix b/home-manager/modules/programs/getmail.nix new file mode 100644 index 00000000000..f83c469ff24 --- /dev/null +++ b/home-manager/modules/programs/getmail.nix @@ -0,0 +1,63 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + accounts = + filter (a: a.getmail.enable) (attrValues config.accounts.email.accounts); + + renderAccountConfig = account: + with account; + let + passCmd = concatMapStringsSep ", " (x: "'${x}'") passwordCommand; + renderedMailboxes = + concatMapStringsSep ", " (x: "'${x}'") getmail.mailboxes; + retrieverType = if imap.tls.enable then + "SimpleIMAPSSLRetriever" + else + "SimpleIMAPRetriever"; + destination = if getmail.destinationCommand != null then { + destinationType = "MDA_external"; + destinationPath = getmail.destinationCommand; + } else { + destinationType = "Maildir"; + destinationPath = "${maildir.absPath}/"; + }; + renderGetmailBoolean = v: if v then "true" else "false"; + in '' + # Generated by Home-Manager. + [retriever] + type = ${retrieverType} + server = ${imap.host} + ${optionalString (imap.port != null) "port = ${toString imap.port}"} + username = ${userName} + password_command = (${passCmd}) + mailboxes = ( ${renderedMailboxes} ) + + [destination] + type = ${destination.destinationType} + path = ${destination.destinationPath} + + [options] + delete = ${renderGetmailBoolean getmail.delete} + read_all = ${renderGetmailBoolean getmail.readAll} + ''; + getmailEnabled = length (filter (a: a.getmail.enable) accounts) > 0; + # Watch out! This is used by the getmail.service too! + renderConfigFilepath = a: + ".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; }) + accounts); + }; +} diff --git a/home-manager/modules/programs/git.nix b/home-manager/modules/programs/git.nix new file mode 100644 index 00000000000..312269de316 --- /dev/null +++ b/home-manager/modules/programs/git.nix @@ -0,0 +1,360 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.git; + + # create [section "subsection"] keys from "section.subsection" attrset names + mkSectionName = name: + let + containsQuote = strings.hasInfix ''"'' name; + sections = splitString "." name; + section = head sections; + subsections = tail sections; + subsection = concatStringsSep "." subsections; + in if containsQuote || subsections == [ ] then + name + 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 { 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 + recurse = path: value: + if isAttrs value then + mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value + else if length path > 1 then { + ${concatStringsSep "." (reverseList (tail path))}.${head path} = value; + } else { + ${head path} = value; + }; + in attrs: foldl recursiveUpdate { } (flatten (recurse [ ] attrs)); + + gitToIni = attrs: + let toIni = generators.toINI { inherit mkKeyValue mkSectionName; }; + in toIni (gitFlattenAttrs attrs); + + gitIniType = with types; + let + primitiveType = either str (either bool int); + multipleType = either primitiveType (listOf primitiveType); + sectionType = attrsOf multipleType; + supersectionType = attrsOf (either multipleType sectionType); + in attrsOf supersectionType; + + signModule = types.submodule { + options = { + key = mkOption { + type = types.str; + description = "The default GPG signing key fingerprint."; + }; + + signByDefault = mkOption { + type = types.bool; + default = false; + description = "Whether commits should be signed by default."; + }; + + gpgPath = mkOption { + type = types.str; + default = "${pkgs.gnupg}/bin/gpg2"; + defaultText = "\${pkgs.gnupg}/bin/gpg2"; + description = "Path to GnuPG binary to use."; + }; + }; + }; + + includeModule = types.submodule ({ config, ... }: { + options = { + condition = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Include this configuration only when <varname>condition</varname> + matches. Allowed conditions are described in + <citerefentry> + <refentrytitle>git-config</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>. + ''; + }; + + path = mkOption { + type = with types; either str path; + description = "Path of the configuration file to include."; + }; + + contents = mkOption { + type = types.attrs; + default = { }; + description = '' + Configuration to include. If empty then a path must be given. + ''; + }; + }; + + config.path = mkIf (config.contents != { }) + (mkDefault (pkgs.writeText "contents" (gitToIni config.contents))); + }); + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.git = { + enable = mkEnableOption "Git"; + + package = mkOption { + type = types.package; + default = pkgs.git; + defaultText = literalExample "pkgs.git"; + description = '' + Git package to install. Use <varname>pkgs.gitAndTools.gitFull</varname> + to gain access to <command>git send-email</command> for instance. + ''; + }; + + userName = mkOption { + type = types.nullOr types.str; + default = null; + description = "Default user name to use."; + }; + + userEmail = mkOption { + type = types.nullOr types.str; + default = null; + description = "Default user email to use."; + }; + + aliases = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { co = "checkout"; }; + description = "Git aliases to define."; + }; + + signing = mkOption { + type = types.nullOr signModule; + default = null; + description = "Options related to signing commits using GnuPG."; + }; + + extraConfig = mkOption { + type = types.either types.lines gitIniType; + default = { }; + example = { + core = { whitespace = "trailing-space,space-before-tab"; }; + url."ssh://git@host".insteadOf = "otherhost"; + }; + description = '' + Additional configuration to add. The use of string values is + deprecated and will be removed in the future. + ''; + }; + + iniContent = mkOption { + type = gitIniType; + internal = true; + }; + + ignores = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "*~" "*.swp" ]; + description = "List of paths that should be globally ignored."; + }; + + attributes = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "*.pdf diff=pdf" ]; + description = "List of defining attributes set globally."; + }; + + includes = mkOption { + type = types.listOf includeModule; + default = [ ]; + example = literalExample '' + [ + { path = "~/path/to/config.inc"; } + { + path = "~/path/to/conditional.inc"; + condition = "gitdir:~/src/dir"; + } + ] + ''; + description = "List of configuration files to include."; + }; + + lfs = { + enable = mkEnableOption "Git Large File Storage"; + + skipSmudge = mkOption { + type = types.bool; + default = false; + description = '' + Skip automatic downloading of objects on clone or pull. + This requires a manual <command>git lfs pull</command> + every time a new commit is checked out on your repository. + ''; + }; + }; + + 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. + ''; + }; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ cfg.package ]; + + programs.git.iniContent.user = { + name = mkIf (cfg.userName != null) cfg.userName; + email = mkIf (cfg.userEmail != null) cfg.userEmail; + }; + + xdg.configFile = { + "git/config".text = gitToIni cfg.iniContent; + + "git/ignore" = mkIf (cfg.ignores != [ ]) { + text = concatStringsSep "\n" cfg.ignores + "\n"; + }; + + "git/attributes" = mkIf (cfg.attributes != [ ]) { + text = concatStringsSep "\n" cfg.attributes + "\n"; + }; + }; + } + + { + programs.git.iniContent = let + hasSmtp = name: account: account.smtp != null; + + genIdentity = name: account: + with account; + nameValuePair "sendemail.${name}" ({ + 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; + } // optionalAttrs (smtp.port != null) { + smtpServerPort = smtp.port; + }); + in mapAttrs' genIdentity + (filterAttrs hasSmtp config.accounts.email.accounts); + } + + (mkIf (cfg.signing != null) { + programs.git.iniContent = { + user.signingKey = cfg.signing.key; + commit.gpgSign = cfg.signing.signByDefault; + gpg.program = cfg.signing.gpgPath; + }; + }) + + (mkIf (cfg.aliases != { }) { programs.git.iniContent.alias = cfg.aliases; }) + + (mkIf (lib.isAttrs cfg.extraConfig) { + programs.git.iniContent = cfg.extraConfig; + }) + + (mkIf (lib.isString cfg.extraConfig) { + warnings = ['' + Using programs.git.extraConfig as a string option is + deprecated and will be removed in the future. Please + change to using it as an attribute set instead. + '']; + + xdg.configFile."git/config".text = cfg.extraConfig; + }) + + (mkIf (cfg.includes != [ ]) { + xdg.configFile."git/config".text = let + include = i: + with i; + if condition != null then { + includeIf.${condition}.path = "${path}"; + } else { + include.path = "${path}"; + }; + in mkAfter + (concatStringsSep "\n" (map gitToIni (map include cfg.includes))); + }) + + (mkIf cfg.lfs.enable { + home.packages = [ pkgs.git-lfs ]; + + programs.git.iniContent.filter.lfs = + let skipArg = optional cfg.lfs.skipSmudge "--skip"; + in { + clean = "git-lfs clean -- %f"; + process = + concatStringsSep " " ([ "git-lfs" "filter-process" ] ++ skipArg); + required = true; + smudge = concatStringsSep " " + ([ "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 new file mode 100644 index 00000000000..f1b15862130 --- /dev/null +++ b/home-manager/modules/programs/gnome-terminal.nix @@ -0,0 +1,238 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.gnome-terminal; + + backForeSubModule = types.submodule ({ ... }: { + options = { + foreground = mkOption { + type = types.str; + description = "The foreground color."; + }; + + background = mkOption { + type = types.str; + description = "The background color."; + }; + }; + }); + + profileColorsSubModule = types.submodule ({ ... }: { + options = { + foregroundColor = mkOption { + type = types.str; + description = "The foreground color."; + }; + + backgroundColor = mkOption { + type = types.str; + description = "The background color."; + }; + + boldColor = mkOption { + default = null; + type = types.nullOr types.str; + description = "The bold color, null to use same as foreground."; + }; + + palette = mkOption { + type = types.listOf types.str; + description = "The terminal palette."; + }; + + cursor = mkOption { + default = null; + type = types.nullOr backForeSubModule; + description = "The color for the terminal cursor."; + }; + + highlight = mkOption { + default = null; + type = types.nullOr backForeSubModule; + description = "The colors for the terminal’s highlighted area."; + }; + }; + }); + + profileSubModule = types.submodule ({ name, config, ... }: { + options = { + default = mkOption { + default = false; + type = types.bool; + description = "Whether this should be the default profile."; + }; + + visibleName = mkOption { + type = types.str; + description = "The profile name."; + }; + + colors = mkOption { + default = null; + type = types.nullOr profileColorsSubModule; + 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" ]; + description = "The cursor shape."; + }; + + font = mkOption { + default = null; + type = types.nullOr types.str; + description = "The font name, null to use system default."; + }; + + allowBold = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + If <literal>true</literal>, allow applications in the + terminal to make text boldface. + ''; + }; + + scrollOnOutput = mkOption { + default = true; + type = types.bool; + description = "Whether to scroll when output is written."; + }; + + showScrollbar = mkOption { + default = true; + type = types.bool; + description = "Whether the scroll bar should be visible."; + }; + + scrollbackLines = mkOption { + default = 10000; + type = types.nullOr types.int; + description = '' + 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."; + }; + }; + }); + + buildProfileSet = pcfg: + { + visible-name = pcfg.visibleName; + scrollbar-policy = if pcfg.showScrollbar then "always" else "never"; + scrollback-lines = pcfg.scrollbackLines; + cursor-shape = pcfg.cursorShape; + 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; + font = pcfg.font; + }) // (if (pcfg.colors == null) then { + use-theme-colors = true; + } else + ({ + use-theme-colors = false; + foreground-color = pcfg.colors.foregroundColor; + background-color = pcfg.colors.backgroundColor; + palette = pcfg.colors.palette; + } // optionalAttrs (pcfg.allowBold != null) { + allow-bold = pcfg.allowBold; + } // (if (pcfg.colors.boldColor == null) then { + bold-color-same-as-fg = true; + } else { + bold-color-same-as-fg = false; + bold-color = pcfg.colors.boldColor; + }) // (if (pcfg.colors.cursor != null) then { + cursor-colors-set = true; + cursor-foreground-color = pcfg.colors.cursor.foreground; + cursor-background-color = pcfg.colors.cursor.background; + } else { + cursor-colors-set = false; + }) // (if (pcfg.colors.highlight != null) then { + highlight-colors-set = true; + highlight-foreground-color = pcfg.colors.highlight.foreground; + highlight-background-color = pcfg.colors.highlight.background; + } else { + highlight-colors-set = false; + }))); + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.gnome-terminal = { + enable = mkEnableOption "Gnome Terminal"; + + showMenubar = mkOption { + default = true; + type = types.bool; + description = "Whether to show the menubar by default"; + }; + + themeVariant = mkOption { + default = "default"; + type = types.enum [ "default" "light" "dark" "system" ]; + description = "The theme variation to request"; + }; + + profile = mkOption { + default = { }; + type = types.attrsOf profileSubModule; + description = "A set of Gnome Terminal profiles."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.gnome3.gnome-terminal ]; + + dconf.settings = let dconfPath = "org/gnome/terminal/legacy"; + in { + "${dconfPath}" = { + default-show-menubar = cfg.showMenubar; + theme-variant = cfg.themeVariant; + schema-version = 3; + }; + + "${dconfPath}/profiles:" = { + default = head (attrNames (filterAttrs (n: v: v.default) cfg.profile)); + list = attrNames cfg.profile; + }; + } // mapAttrs' + (n: v: nameValuePair ("${dconfPath}/profiles:/:${n}") (buildProfileSet v)) + cfg.profile; + + programs.bash.enableVteIntegration = true; + programs.zsh.enableVteIntegration = true; + }; +} diff --git a/home-manager/modules/programs/go.nix b/home-manager/modules/programs/go.nix new file mode 100644 index 00000000000..4b85ec854ad --- /dev/null +++ b/home-manager/modules/programs/go.nix @@ -0,0 +1,105 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.go; + +in { + meta.maintainers = [ maintainers.rvolosatovs ]; + + options = { + programs.go = { + enable = mkEnableOption "Go"; + + package = mkOption { + type = types.package; + default = pkgs.go; + defaultText = literalExample "pkgs.go"; + description = "The Go package to use."; + }; + + packages = mkOption { + type = with types; attrsOf path; + default = { }; + example = literalExample '' + { + "golang.org/x/text" = builtins.fetchGit "https://go.googlesource.com/text"; + "golang.org/x/time" = builtins.fetchGit "https://go.googlesource.com/time"; + } + ''; + description = "Packages to add to GOPATH."; + }; + + goPath = mkOption { + type = with types; nullOr str; + default = null; + example = "go"; + description = '' + Primary <envar>GOPATH</envar> relative to + <envar>HOME</envar>. It will be exported first and therefore + used by default by the Go tooling. + ''; + }; + + extraGoPaths = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "extraGoPath1" "extraGoPath2" ]; + description = let goPathOpt = "programs.go.goPath"; + in '' + Extra <envar>GOPATH</envar>s relative to <envar>HOME</envar> appended + after + <varname><link linkend="opt-${goPathOpt}">${goPathOpt}</link></varname>, + if that option is set. + ''; + }; + + goBin = mkOption { + type = with types; nullOr str; + default = null; + 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. + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ cfg.package ]; + + home.file = let + goPath = if cfg.goPath != null then cfg.goPath else "go"; + mkSrc = n: v: { "${goPath}/src/${n}".source = v; }; + in foldl' (a: b: a // b) { } (mapAttrsToList mkSrc cfg.packages); + } + + (mkIf (cfg.goPath != null) { + home.sessionVariables.GOPATH = concatStringsSep ":" (map builtins.toPath + (map (path: "${config.home.homeDirectory}/${path}") + ([ cfg.goPath ] ++ cfg.extraGoPaths))); + }) + + (mkIf (cfg.goBin != null) { + 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/gpg.nix b/home-manager/modules/programs/gpg.nix new file mode 100644 index 00000000000..4588c59c882 --- /dev/null +++ b/home-manager/modules/programs/gpg.nix @@ -0,0 +1,61 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.gpg; + + cfgText = + concatStringsSep "\n" + (attrValues + (mapAttrs (key: value: + if isString value + then "${key} ${value}" + else optionalString value key) + cfg.settings)); + +in { + options.programs.gpg = { + enable = mkEnableOption "GnuPG"; + + settings = mkOption { + type = types.attrsOf (types.either types.str types.bool); + example = { + no-comments = false; + s2k-cipher-algo = "AES128"; + }; + description = '' + GnuPG configuration options. Available options are described + in the gpg manpage: + <link xlink:href="https://gnupg.org/documentation/manpage.html"/>. + ''; + }; + }; + + config = mkIf cfg.enable { + programs.gpg.settings = { + personal-cipher-preferences = mkDefault "AES256 AES192 AES"; + personal-digest-preferences = mkDefault "SHA512 SHA384 SHA256"; + personal-compress-preferences = mkDefault "ZLIB BZIP2 ZIP Uncompressed"; + default-preference-list = mkDefault "SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed"; + cert-digest-algo = mkDefault "SHA512"; + s2k-digest-algo = mkDefault "SHA512"; + s2k-cipher-algo = mkDefault "AES256"; + charset = mkDefault "utf-8"; + fixed-list-mode = mkDefault true; + no-comments = mkDefault true; + no-emit-version = mkDefault true; + keyid-format = mkDefault "0xlong"; + list-options = mkDefault "show-uid-validity"; + verify-options = mkDefault "show-uid-validity"; + with-fingerprint = mkDefault true; + require-cross-certification = mkDefault true; + no-symkey-cache = mkDefault true; + use-agent = mkDefault true; + }; + + home.packages = [ pkgs.gnupg ]; + + home.file.".gnupg/gpg.conf".text = cfgText; + }; +} diff --git a/home-manager/modules/programs/home-manager.nix b/home-manager/modules/programs/home-manager.nix new file mode 100644 index 00000000000..9039a59d7c5 --- /dev/null +++ b/home-manager/modules/programs/home-manager.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.home-manager; + + dag = config.lib.dag; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.home-manager = { + enable = mkEnableOption "Home Manager"; + + path = mkOption { + type = types.nullOr types.str; + default = null; + example = "$HOME/devel/home-manager"; + description = '' + The default path to use for Home Manager. If this path does + not exist then + <filename>$HOME/.config/nixpkgs/home-manager</filename> and + <filename>$HOME/.nixpkgs/home-manager</filename> will be + attempted. + ''; + }; + }; + }; + + config = mkIf (cfg.enable && !config.submoduleSupport.enable) { + home.packages = + [ (pkgs.callPackage ../../home-manager { inherit (cfg) path; }) ]; + }; +} diff --git a/home-manager/modules/programs/htop.nix b/home-manager/modules/programs/htop.nix new file mode 100644 index 00000000000..1fb397cdc38 --- /dev/null +++ b/home-manager/modules/programs/htop.nix @@ -0,0 +1,416 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.htop; + + list = xs: concatMapStrings (x: "${toString x} ") xs; + + bool = b: if b then "1" else "0"; + + fields = { + PID = 0; + COMM = 1; + STATE = 2; + PPID = 3; + PGRP = 4; + SESSION = 5; + TTY_NR = 6; + TPGID = 7; + MINFLT = 9; + MAJFLT = 11; + PRIORITY = 17; + NICE = 18; + STARTTIME = 20; + PROCESSOR = 37; + M_SIZE = 38; + M_RESIDENT = 39; + ST_UID = 45; + PERCENT_CPU = 46; + PERCENT_MEM = 47; + USER = 48; + TIME = 49; + NLWP = 50; + TGID = 51; + CMINFLT = 10; + CMAJFLT = 12; + UTIME = 13; + STIME = 14; + CUTIME = 15; + CSTIME = 16; + M_SHARE = 40; + M_TRS = 41; + M_DRS = 42; + M_LRS = 43; + M_DT = 44; + CTID = 99; + VPID = 100; + VXID = 102; + RCHAR = 102; + WCHAR = 103; + SYSCR = 104; + SYSCW = 105; + RBYTES = 106; + WBYTES = 107; + CNCLWB = 108; + IO_READ_RATE = 109; + IO_WRITE_RATE = 110; + IO_RATE = 111; + CGROUP = 112; + OOM = 113; + IO_PRIORITY = 114; + M_PSS = 118; + M_SWAP = 119; + M_PSSWP = 120; + }; + + # Mapping from names to defaults + meters = { + Clock = 2; + LoadAverage = 2; + Load = 2; + Memory = 1; + Swap = 1; + Tasks = 2; + Uptime = 2; + Battery = 2; + 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 + meterEnum = types.enum (attrNames meters); + meterSubmodule = types.submodule { + options = { + kind = mkOption { + type = types.enum (attrNames meters); + example = "AllCPUs"; + description = "What kind of meter."; + }; + + mode = mkOption { + type = types.enum [ 1 2 3 4 ]; + example = 2; + description = + "Which mode the meter should use, one of 1(Bar) 2(Text) 3(Graph) 4(LED)."; + }; + }; + }; + in types.coercedTo meterEnum (m: { + kind = m; + mode = meters.${m}; + }) meterSubmodule; + + meterType = types.submodule { + options = { + left = mkOption { + description = "Meters shown in the left header."; + default = [ "AllCPUs" "Memory" "Swap" ]; + example = [ + "Memory" + "LeftCPUs2" + "RightCPUs2" + { + kind = "CPU"; + mode = 3; + } + ]; + type = types.listOf singleMeterType; + }; + right = mkOption { + description = "Meters shown in the right header."; + default = [ "Tasks" "LoadAverage" "Uptime" ]; + example = [ + { + kind = "Clock"; + mode = 4; + } + "Uptime" + "Tasks" + ]; + type = types.listOf singleMeterType; + }; + }; + }; + +in { + options.programs.htop = { + enable = mkEnableOption "htop"; + + fields = mkOption { + type = types.listOf (types.enum (attrNames fields)); + default = [ + "PID" + "USER" + "PRIORITY" + "NICE" + "M_SIZE" + "M_RESIDENT" + "M_SHARE" + "STATE" + "PERCENT_CPU" + "PERCENT_MEM" + "TIME" + "COMM" + ]; + example = [ + "PID" + "USER" + "PRIORITY" + "PERCENT_CPU" + "M_RESIDENT" + "PERCENT_MEM" + "TIME" + "COMM" + ]; + description = "Active fields shown in the table."; + }; + + sortKey = mkOption { + type = types.enum (attrNames fields); + default = "PERCENT_CPU"; + example = "TIME"; + description = "Which field to use for sorting."; + }; + + sortDescending = mkOption { + type = types.bool; + default = true; + description = "Whether to sort descending or not."; + }; + + hideThreads = mkOption { + type = types.bool; + default = false; + description = "Hide threads."; + }; + + hideKernelThreads = mkOption { + type = types.bool; + default = true; + description = "Hide kernel threads."; + }; + + hideUserlandThreads = mkOption { + type = types.bool; + default = false; + description = "Hide userland process threads."; + }; + + shadowOtherUsers = mkOption { + type = types.bool; + default = false; + description = "Shadow other users' processes."; + }; + + showThreadNames = mkOption { + type = types.bool; + default = false; + description = "Show custom thread names."; + }; + + showProgramPath = mkOption { + type = types.bool; + default = true; + description = "Show program path."; + }; + + highlightBaseName = mkOption { + type = types.bool; + default = false; + description = "Highlight program <quote>basename</quote>."; + }; + + highlightMegabytes = mkOption { + type = types.bool; + default = true; + description = "Highlight large numbers in memory counters."; + }; + + highlightThreads = mkOption { + type = types.bool; + default = true; + description = "Display threads in a different color."; + }; + + treeView = mkOption { + type = types.bool; + default = false; + description = "Tree view."; + }; + + headerMargin = mkOption { + type = types.bool; + default = true; + description = "Leave a margin around header."; + }; + + detailedCpuTime = mkOption { + type = types.bool; + default = false; + description = + "Detailed CPU time (System/IO-Wait/Hard-IRQ/Soft-IRQ/Steal/Guest)."; + }; + + cpuCountFromZero = mkOption { + type = types.bool; + default = false; + 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; + description = "Update process names on every refresh."; + }; + + accountGuestInCpuMeter = mkOption { + type = types.bool; + default = false; + description = "Add guest time in CPU meter percentage."; + }; + + colorScheme = mkOption { + type = types.enum [ 0 1 2 3 4 5 6 ]; + default = 0; + example = 6; + description = "Which color scheme to use."; + }; + + enableMouse = mkOption { + type = types.bool; + default = true; + description = "Enable mouse support."; + }; + + delay = mkOption { + type = types.int; + default = 15; + example = 2; + description = "Set the delay between updates, in tenths of seconds."; + }; + + meters = mkOption { + description = "Meters shown in the header."; + default = { + left = [ "AllCPUs" "Memory" "Swap" ]; + right = [ "Tasks" "LoadAverage" "Uptime" ]; + }; + example = { + left = [ + "Memory" + "CPU" + "LeftCPUs2" + "RightCPUs2" + { + kind = "CPU"; + mode = 3; + } + ]; + right = [ + { + kind = "Clock"; + mode = 4; + } + "Uptime" + "Tasks" + "LoadAverage" + { + kind = "Battery"; + mode = 1; + } + ]; + }; + type = meterType; + }; + + vimMode = mkOption { + type = types.bool; + default = false; + description = "Vim key bindings."; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.htop ]; + + xdg.configFile."htop/htoprc".text = let + leftMeters = map (m: m.kind) cfg.meters.left; + leftModes = map (m: m.mode) cfg.meters.left; + rightMeters = map (m: m.kind) cfg.meters.right; + rightModes = map (m: m.mode) cfg.meters.right; + in '' + # This file is regenerated by home-manager + # when options are changed in the config + fields=${list (map (n: fields.${n}) cfg.fields)} + sort_key=${toString (fields.${cfg.sortKey})} + sort_direction=${bool cfg.sortDescending} + hide_threads=${bool cfg.hideThreads} + hide_kernel_threads=${bool cfg.hideKernelThreads} + hide_userland_threads=${bool cfg.hideUserlandThreads} + shadow_other_users=${bool cfg.shadowOtherUsers} + show_thread_names=${bool cfg.showThreadNames} + show_program_path=${bool cfg.showProgramPath} + highlight_base_name=${bool cfg.highlightBaseName} + highlight_megabytes=${bool cfg.highlightMegabytes} + highlight_threads=${bool cfg.highlightThreads} + tree_view=${bool cfg.treeView} + 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 new file mode 100644 index 00000000000..a7d2692b515 --- /dev/null +++ b/home-manager/modules/programs/info.nix @@ -0,0 +1,63 @@ +# 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. + +# 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. + +# 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, ... }: + +with lib; + +let + + cfg = config.programs.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 { + 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. + '') + ]; + + options.programs.info.enable = mkEnableOption "GNU Info"; + + config = mkIf cfg.enable { + 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/irssi.nix b/home-manager/modules/programs/irssi.nix new file mode 100644 index 00000000000..fc8fa8e6132 --- /dev/null +++ b/home-manager/modules/programs/irssi.nix @@ -0,0 +1,211 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.irssi; + + boolStr = b: if b then "yes" else "no"; + quoteStr = s: escape ["\""] s; + + assignFormat = set: + concatStringsSep "\n" + (mapAttrsToList (k: v: " ${k} = \"${quoteStr v}\";") set); + + chatnetString = + concatStringsSep "\n" + (flip mapAttrsToList cfg.networks + (k: v: '' + ${k} = { + type = "${v.type}"; + nick = "${quoteStr v.nick}"; + autosendcmd = "${concatMapStringsSep ";" quoteStr v.autoCommands}"; + }; + '')); + + serversString = + concatStringsSep ",\n" + (flip mapAttrsToList cfg.networks + (k: v: '' + { + chatnet = "${k}"; + address = "${v.server.address}"; + port = "${toString v.server.port}"; + use_ssl = "${boolStr v.server.ssl.enable}"; + ssl_verify = "${boolStr v.server.ssl.verify}"; + autoconnect = "${boolStr v.server.autoConnect}"; + } + '')); + + channelString = + concatStringsSep ",\n" + (flip mapAttrsToList cfg.networks + (k: v: + concatStringsSep ",\n" + (flip mapAttrsToList v.channels + (c: cv: '' + { + chatnet = "${k}"; + name = "${c}"; + autojoin = "${boolStr cv.autoJoin}"; + } + '')))); + + channelType = types.submodule { + options = { + name = mkOption { + type = types.nullOr types.str; + visible = false; + default = null; + description = "Name of the channel."; + }; + + autoJoin = mkOption { + type = types.bool; + default = false; + description = "Whether to join this channel on connect."; + }; + }; + }; + + networkType = types.submodule ({ name, ...}: { + options = { + name = mkOption { + visible = false; + default = name; + type = types.str; + }; + + nick = mkOption { + type = types.str; + description = "Nickname in that network."; + }; + + type = mkOption { + type = types.str; + description = "Type of the network."; + default = "IRC"; + }; + + autoCommands = mkOption { + type = types.listOf types.str; + default = []; + description = "List of commands to execute on connect."; + }; + + server = { + address = mkOption { + type = types.str; + description = "Address of the chat server."; + }; + + port = mkOption { + type = types.port; + default = 6667; + description = "Port of the chat server."; + }; + + ssl = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether SSL should be used."; + }; + + verify = mkOption { + type = types.bool; + default = true; + description = "Whether the SSL certificate should be verified."; + }; + }; + + autoConnect = mkOption { + type = types.bool; + default = false; + description = "Whether Irssi connects to the server on launch."; + }; + }; + + channels = mkOption { + description = "Channels for the given network."; + type = types.attrsOf channelType; + default = {}; + }; + }; + }); + +in + +{ + + options = { + programs.irssi = { + enable = mkEnableOption "the Irssi chat client"; + + extraConfig = mkOption { + default = ""; + description = "These lines are appended to the Irssi configuration."; + type = types.str; + }; + + aliases = mkOption { + default = {}; + example = { J = "join"; BYE = "quit";}; + description = "An attribute set that maps aliases to commands."; + type = types.attrsOf types.str; + }; + + networks = mkOption { + default = {}; + example = literalExample '' + { + freenode = { + nick = "hmuser"; + server = { + address = "chat.freenode.net"; + port = 6697; + autoConnect = true; + }; + channels = { + nixos.autoJoin = true; + }; + }; + } + ''; + description = "An attribute set of chat networks."; + type = types.attrsOf networkType; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.irssi ]; + + home.file.".irssi/config".text = '' + settings = { + core = { + settings_autosave = "no"; + }; + }; + + aliases = { + ${assignFormat cfg.aliases} + }; + + chatnets = { + ${chatnetString} + }; + + servers = ( + ${serversString} + ); + + channels = ( + ${channelString} + ); + + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/jq.nix b/home-manager/modules/programs/jq.nix new file mode 100644 index 00000000000..6c89df0df93 --- /dev/null +++ b/home-manager/modules/programs/jq.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.jq; + + colorType = mkOption { + type = types.str; + description = "ANSI color definition"; + example = "1;31"; + visible = false; + }; + + colorsType = types.submodule { + options = { + null = colorType; + false = colorType; + true = colorType; + numbers = colorType; + strings = colorType; + arrays = colorType; + objects = colorType; + }; + }; + +in { + options = { + programs.jq = { + enable = mkEnableOption "the jq command-line JSON processor"; + + colors = mkOption { + description = '' + The colors used in colored JSON output.</para> + + <para>See <link xlink:href="https://stedolan.github.io/jq/manual/#Colors"/>. + ''; + + example = literalExample '' + { + null = "1;30"; + false = "0;31"; + true = "0;32"; + numbers = "0;36"; + strings = "0;33"; + arrays = "1;35"; + objects = "1;37"; + } + ''; + + default = { + null = "1;30"; + false = "0;39"; + true = "0;39"; + numbers = "0;39"; + strings = "0;32"; + arrays = "1;39"; + objects = "1;39"; + }; + + type = colorsType; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.jq ]; + + home.sessionVariables = let c = cfg.colors; + in { + JQ_COLORS = + "${c.null}:${c.false}:${c.true}:${c.numbers}:${c.strings}:${c.arrays}:${c.objects}"; + }; + }; +} diff --git a/home-manager/modules/programs/kakoune.nix b/home-manager/modules/programs/kakoune.nix new file mode 100644 index 00000000000..6db311a1376 --- /dev/null +++ b/home-manager/modules/programs/kakoune.nix @@ -0,0 +1,659 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.kakoune; + + hook = types.submodule { + options = { + name = mkOption { + type = types.enum [ + "NormalBegin" + "NormalIdle" + "NormalEnd" + "NormalKey" + "InsertBegin" + "InsertIdle" + "InsertEnd" + "InsertKey" + "InsertChar" + "InsertDelete" + "InsertMove" + "WinCreate" + "WinClose" + "WinResize" + "WinDisplay" + "WinSetOption" + "BufSetOption" + "BufNewFile" + "BufOpenFile" + "BufCreate" + "BufWritePre" + "BufWritePost" + "BufReload" + "BufClose" + "BufOpenFifo" + "BufReadFifo" + "BufCloseFifo" + "RuntimeError" + "ModeChange" + "PromptIdle" + "GlobalSetOption" + "KakBegin" + "KakEnd" + "FocusIn" + "FocusOut" + "RawKey" + "InsertCompletionShow" + "InsertCompletionHide" + "InsertCompletionSelect" + "ModuleLoaded" + ]; + example = "SetOption"; + description = '' + The name of the hook. For a description, see + <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/hooks.asciidoc#default-hooks"/>. + ''; + }; + + once = mkOption { + type = types.bool; + default = false; + description = '' + Remove the hook after running it once. + ''; + }; + + group = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Add the hook to the named group. + ''; + }; + + option = mkOption { + type = types.nullOr types.str; + default = null; + example = "filetype=latex"; + description = '' + Additional option to pass to the hook. + ''; + }; + + commands = mkOption { + type = types.lines; + default = ""; + example = "set-option window indentwidth 2"; + description = '' + Commands to run when the hook is activated. + ''; + }; + }; + }; + + keyMapping = types.submodule { + options = { + mode = mkOption { + type = types.str; + example = "user"; + description = '' + The mode in which the mapping takes effect. + ''; + }; + + docstring = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Optional documentation text to display in info boxes. + ''; + }; + + key = mkOption { + type = types.str; + example = "<a-x>"; + description = '' + The key to be mapped. See + <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/mapping.asciidoc#mappable-keys"/> + for possible values. + ''; + }; + + effect = mkOption { + type = types.str; + example = ":wq<ret>"; + description = '' + The sequence of keys to be mapped. + ''; + }; + }; + }; + + configModule = types.submodule { + options = { + colorScheme = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Set the color scheme. To see available schemes, enter + <command>colorscheme</command> at the kakoune prompt. + ''; + }; + + tabStop = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + description = '' + The width of a tab in spaces. The kakoune default is + <literal>6</literal>. + ''; + }; + + indentWidth = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + description = '' + The width of an indentation in spaces. + The kakoune default is <literal>4</literal>. + If <literal>0</literal>, a tab will be used instead. + ''; + }; + + incrementalSearch = mkOption { + type = types.bool; + default = true; + description = '' + Execute a search as it is being typed. + ''; + }; + + alignWithTabs = mkOption { + type = types.bool; + default = false; + description = '' + Use tabs for the align command. + ''; + }; + + autoInfo = mkOption { + type = types.nullOr + (types.listOf (types.enum [ "command" "onkey" "normal" ])); + default = null; + example = [ "command" "normal" ]; + description = '' + Contexts in which to display automatic information box. + The kakoune default is <literal>[ "command" "onkey" ]</literal>. + ''; + }; + + autoComplete = mkOption { + type = types.nullOr (types.listOf (types.enum [ "insert" "prompt" ])); + default = null; + description = '' + Modes in which to display possible completions. + The kakoune default is <literal>[ "insert" "prompt" ]</literal>. + ''; + }; + + autoReload = mkOption { + type = types.nullOr (types.enum [ "yes" "no" "ask" ]); + default = null; + description = '' + Reload buffers when an external modification is detected. + The kakoune default is <literal>"ask"</literal>. + ''; + }; + + scrollOff = mkOption { + type = types.nullOr (types.submodule { + options = { + lines = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + The number of lines to keep visible around the cursor. + ''; + }; + + columns = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + The number of columns to keep visible around the cursor. + ''; + }; + }; + }); + default = null; + description = '' + How many lines and columns to keep visible around the cursor. + ''; + }; + + ui = mkOption { + type = types.nullOr (types.submodule { + options = { + setTitle = mkOption { + type = types.bool; + default = false; + description = '' + Change the title of the terminal emulator. + ''; + }; + + statusLine = mkOption { + type = types.enum [ "top" "bottom" ]; + default = "bottom"; + description = '' + Where to display the status line. + ''; + }; + + assistant = mkOption { + type = types.enum [ "clippy" "cat" "dilbert" "none" ]; + default = "clippy"; + description = '' + The assistant displayed in info boxes. + ''; + }; + + enableMouse = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable mouse support. + ''; + }; + + changeColors = mkOption { + type = types.bool; + default = true; + description = '' + Change color palette. + ''; + }; + + wheelDownButton = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Button to send for wheel down events. + ''; + }; + + wheelUpButton = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Button to send for wheel up events. + ''; + }; + + shiftFunctionKeys = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + description = '' + Amount by which shifted function keys are offset. That + is, if the terminal sends F13 for Shift-F1, this + should be <literal>12</literal>. + ''; + }; + + useBuiltinKeyParser = mkOption { + type = types.bool; + default = false; + description = '' + Bypass ncurses key parser and use an internal one. + ''; + }; + }; + }); + default = null; + description = '' + Settings for the ncurses interface. + ''; + }; + + showMatching = mkOption { + type = types.bool; + default = false; + description = '' + Highlight the matching char of the character under the + selections' cursor using the <literal>MatchingChar</literal> + face. + ''; + }; + + wrapLines = mkOption { + type = types.nullOr (types.submodule { + options = { + enable = mkEnableOption "the wrap lines highlighter"; + + word = mkOption { + type = types.bool; + default = false; + description = '' + Wrap at word boundaries instead of codepoint boundaries. + ''; + }; + + indent = mkOption { + type = types.bool; + default = false; + description = '' + Preserve line indentation when wrapping. + ''; + }; + + maxWidth = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + description = '' + Wrap text at maxWidth, even if the window is wider. + ''; + }; + + marker = mkOption { + type = types.nullOr types.str; + default = null; + example = "⏎"; + description = '' + Prefix wrapped lines with marker text. + If not <literal>null</literal>, + the marker text will be displayed in the indentation if possible. + ''; + }; + }; + }); + default = null; + description = '' + Settings for the wrap lines highlighter. + ''; + }; + + numberLines = mkOption { + type = types.nullOr (types.submodule { + options = { + enable = mkEnableOption "the number lines highlighter"; + + relative = mkOption { + type = types.bool; + default = false; + description = '' + Show line numbers relative to the main cursor line. + ''; + }; + + highlightCursor = mkOption { + type = types.bool; + default = false; + description = '' + Highlight the cursor line with a separate face. + ''; + }; + + separator = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + String that separates the line number column from the + buffer contents. The kakoune default is + <literal>"|"</literal>. + ''; + }; + }; + }); + default = null; + description = '' + Settings for the number lines highlighter. + ''; + }; + + showWhitespace = mkOption { + type = types.nullOr (types.submodule { + options = { + enable = mkEnableOption "the show whitespace highlighter"; + + lineFeed = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The character to display for line feeds. + The kakoune default is <literal>"¬"</literal>. + ''; + }; + + space = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The character to display for spaces. + The kakoune default is <literal>"·"</literal>. + ''; + }; + + nonBreakingSpace = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The character to display for non-breaking spaces. + The kakoune default is <literal>"⍽"</literal>. + ''; + }; + + tab = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The character to display for tabs. + The kakoune default is <literal>"→"</literal>. + ''; + }; + + tabStop = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The character to append to tabs to reach the width of a tabstop. + The kakoune default is <literal>" "</literal>. + ''; + }; + }; + }); + default = null; + description = '' + Settings for the show whitespaces highlighter. + ''; + }; + + keyMappings = mkOption { + type = types.listOf keyMapping; + default = [ ]; + description = '' + User-defined key mappings. For documentation, see + <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/mapping.asciidoc"/>. + ''; + }; + + hooks = mkOption { + type = types.listOf hook; + default = [ ]; + description = '' + Global hooks. For documentation, see + <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/hooks.asciidoc"/>. + ''; + }; + }; + }; + + kakouneWithPlugins = pkgs.wrapKakoune pkgs.kakoune-unwrapped { + configure = { plugins = cfg.plugins; }; + }; + + configFile = let + wrapOptions = with cfg.config.wrapLines; + concatStrings [ + "${optionalString word " -word"}" + "${optionalString indent " -indent"}" + "${optionalString (marker != null) " -marker ${marker}"}" + "${optionalString (maxWidth != null) " -width ${toString maxWidth}"}" + ]; + + numberLinesOptions = with cfg.config.numberLines; + concatStrings [ + "${optionalString relative " -relative "}" + "${optionalString highlightCursor " -hlcursor"}" + "${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"}" + "ncurses_status_on_top=${ + if (statusLine == "top") then "true" else "false" + }" + "ncurses_assistant=${assistant}" + "ncurses_enable_mouse=${if enableMouse then "true" else "false"}" + "ncurses_change_colors=${if changeColors then "true" else "false"}" + "${optionalString (wheelDownButton != null) + "ncurses_wheel_down_button=${wheelDownButton}"}" + "${optionalString (wheelUpButton != null) + "ncurses_wheel_up_button=${wheelUpButton}"}" + "${optionalString (shiftFunctionKeys != null) + "ncurses_shift_function_key=${toString shiftFunctionKeys}"}" + "ncurses_builtin_key_parser=${ + if useBuiltinKeyParser then "true" else "false" + }" + ]; + + 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" + "${km.mode} ${km.key} '${km.effect}'" + "${optionalString (km.docstring != null) + "-docstring '${km.docstring}'"}" + ]; + + hookString = h: + concatStringsSep " " [ + "hook" + "${optionalString (h.group != null) "-group ${group}"}" + "${optionalString (h.once) "-once"}" + "global" + "${h.name}" + "${optionalString (h.option != null) h.option}" + "%{ ${h.commands} }" + ]; + + cfgStr = with cfg.config; + concatStringsSep "\n" ([ "# Generated by home-manager" ] + ++ optional (colorScheme != null) "colorscheme ${colorScheme}" + ++ optional (tabStop != null) + "set-option global tabstop ${toString tabStop}" + ++ optional (indentWidth != null) + "set-option global indentwidth ${toString indentWidth}" + ++ optional (!incrementalSearch) "set-option global incsearch false" + ++ optional (alignWithTabs) "set-option global aligntab true" + ++ optional (autoInfo != null) + "set-option global autoinfo ${concatStringsSep "|" autoInfo}" + ++ optional (autoComplete != null) + "set-option global autocomplete ${concatStringsSep "|" autoComplete}" + ++ optional (autoReload != null) + "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 + }" + + ++ [ "# UI options" ] + ++ optional (ui != null) "set-option global ui_options ${uiOptions}" + + ++ [ "# User modes" ] ++ userModeStrings ++ [ "# Key mappings" ] + ++ map keyMappingString keyMappings + + ++ [ "# Hooks" ] ++ map hookString hooks); + in pkgs.writeText "kakrc" + (optionalString (cfg.config != null) cfgStr + "\n" + cfg.extraConfig); + +in { + options = { + programs.kakoune = { + enable = mkEnableOption "the kakoune text editor"; + + config = mkOption { + type = types.nullOr configModule; + default = { }; + description = "kakoune configuration options."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to add to + <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 = [ kakouneWithPlugins ]; + xdg.configFile."kak/kakrc".source = configFile; + }; +} diff --git a/home-manager/modules/programs/keychain.nix b/home-manager/modules/programs/keychain.nix new file mode 100644 index 00000000000..6e26bd232ce --- /dev/null +++ b/home-manager/modules/programs/keychain.nix @@ -0,0 +1,115 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.keychain; + + flags = cfg.extraFlags ++ optional (cfg.agents != [ ]) + "--agents ${concatStringsSep "," cfg.agents}" + ++ optional (cfg.inheritType != null) "--inherit ${cfg.inheritType}"; + + shellCommand = + "${cfg.package}/bin/keychain --eval ${concatStringsSep " " flags} ${ + concatStringsSep " " cfg.keys + }"; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.keychain = { + enable = mkEnableOption "keychain"; + + package = mkOption { + type = types.package; + default = pkgs.keychain; + defaultText = literalExample "pkgs.keychain"; + description = '' + Keychain package to install. + ''; + }; + + keys = mkOption { + type = types.listOf types.str; + default = [ "id_rsa" ]; + description = '' + Keys to add to keychain. + ''; + }; + + agents = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Agents to add. + ''; + }; + + inheritType = mkOption { + type = + types.nullOr (types.enum [ "local" "any" "local-once" "any-once" ]); + default = null; + description = '' + Inherit type to attempt from agent variables from the environment. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = [ "--quiet" ]; + description = '' + Extra flags to pass to keychain. + ''; + }; + + enableBashIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Bash integration. + ''; + }; + + enableFishIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Fish integration. + ''; + }; + + enableZshIntegration = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable Zsh integration. + ''; + }; + + enableXsessionIntegration = mkOption { + default = true; + type = types.bool; + visible = pkgs.stdenv.hostPlatform.isLinux; + description = '' + Whether to run keychain from your <filename>~/.xsession</filename>. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + eval "$(${shellCommand})" + ''; + programs.fish.interactiveShellInit = mkIf cfg.enableFishIntegration '' + eval (${shellCommand}) + ''; + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + eval "$(${shellCommand})" + ''; + xsession.initExtra = mkIf cfg.enableXsessionIntegration '' + eval "$(${shellCommand})" + ''; + }; +} 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/lesspipe.nix b/home-manager/modules/programs/lesspipe.nix new file mode 100644 index 00000000000..a7a51ffe2a2 --- /dev/null +++ b/home-manager/modules/programs/lesspipe.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.lesspipe = { + enable = mkEnableOption "lesspipe preprocessor for less"; + }; + }; + + config = mkIf config.programs.lesspipe.enable { + home.sessionVariables = { + LESSOPEN = "|${pkgs.lesspipe}/bin/lesspipe.sh %s"; + }; + }; +} 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/lsd.nix b/home-manager/modules/programs/lsd.nix new file mode 100644 index 00000000000..ab1880ff828 --- /dev/null +++ b/home-manager/modules/programs/lsd.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.lsd; + + aliases = { + ls = "${pkgs.lsd}/bin/lsd"; + ll = "ls -l"; + la = "ls -a"; + lt = "ls --tree"; + lla = "ls -la"; + }; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.lsd = { + enable = mkEnableOption "lsd"; + + enableAliases = mkOption { + default = false; + type = types.bool; + description = '' + Whether to enable recommended lsd aliases. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.lsd ]; + + programs.bash.shellAliases = mkIf cfg.enableAliases aliases; + + programs.zsh.shellAliases = mkIf cfg.enableAliases aliases; + + programs.fish.shellAliases = mkIf cfg.enableAliases aliases; + }; +} diff --git a/home-manager/modules/programs/man.nix b/home-manager/modules/programs/man.nix new file mode 100644 index 00000000000..b235b02fe2d --- /dev/null +++ b/home-manager/modules/programs/man.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: + +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>. + ''; + }; + + 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/matplotlib.nix b/home-manager/modules/programs/matplotlib.nix new file mode 100644 index 00000000000..da80c116770 --- /dev/null +++ b/home-manager/modules/programs/matplotlib.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: + +with lib; + +let + + cfg = config.programs.matplotlib; + + formatLine = o: n: v: + let + formatValue = v: + if isBool v then (if v then "True" else "False") else toString v; + in if isAttrs v then + concatStringsSep "\n" (mapAttrsToList (formatLine "${o}${n}.") v) + else + (if v == "" then "" else "${o}${n}: ${formatValue v}"); + +in { + meta.maintainers = [ maintainers.rprospero ]; + + options.programs.matplotlib = { + enable = mkEnableOption "matplotlib, a plotting library for python"; + + config = mkOption { + default = { }; + type = types.attrs; + description = '' + Add terms to the <filename>matplotlibrc</filename> file to + control the default matplotlib behavior. + ''; + example = literalExample '' + { + backend = "Qt5Agg"; + axes = { + grid = true; + facecolor = "black"; + edgecolor = "FF9900"; + }; + grid.color = "FF9900"; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional commands for matplotlib that will be added to the + <filename>matplotlibrc</filename> file. + ''; + }; + }; + + config = mkIf cfg.enable { + xdg.configFile."matplotlib/matplotlibrc".text = concatStringsSep "\n" ([ ] + ++ mapAttrsToList (formatLine "") cfg.config + ++ optional (cfg.extraConfig != "") cfg.extraConfig) + "\n"; + }; +} diff --git a/home-manager/modules/programs/mbsync-accounts.nix b/home-manager/modules/programs/mbsync-accounts.nix new file mode 100644 index 00000000000..4de1965fe3f --- /dev/null +++ b/home-manager/modules/programs/mbsync-accounts.nix @@ -0,0 +1,105 @@ +{ lib, ... }: + +with lib; + +let + + extraConfigType = with lib.types; attrsOf (either (either str int) bool); + +in { + options.mbsync = { + enable = mkEnableOption "synchronization using mbsync"; + + flatten = mkOption { + type = types.nullOr types.str; + default = null; + example = "."; + description = '' + If set, flattens the hierarchy within the maildir by + substituting the canonical hierarchy delimiter + <literal>/</literal> with this value. + ''; + }; + + create = mkOption { + type = types.enum [ "none" "maildir" "imap" "both" ]; + default = "none"; + example = "maildir"; + description = '' + Automatically create missing mailboxes within the + given mail store. + ''; + }; + + remove = mkOption { + type = types.enum [ "none" "maildir" "imap" "both" ]; + default = "none"; + example = "imap"; + description = '' + Propagate mailbox deletions to the given mail store. + ''; + }; + + expunge = mkOption { + type = types.enum [ "none" "maildir" "imap" "both" ]; + default = "none"; + example = "both"; + description = '' + Permanently remove messages marked for deletion from + the given mail store. + ''; + }; + + patterns = mkOption { + type = types.listOf types.str; + default = [ "*" ]; + description = '' + Pattern of mailboxes to synchronize. + ''; + }; + + extraConfig.channel = mkOption { + type = extraConfigType; + default = { }; + example = literalExample '' + { + MaxMessages = 10000; + MaxSize = "1m"; + }; + ''; + description = '' + Per channel extra configuration. + ''; + }; + + extraConfig.local = mkOption { + type = extraConfigType; + default = { }; + description = '' + Local store extra configuration. + ''; + }; + + extraConfig.remote = mkOption { + type = extraConfigType; + default = { }; + description = '' + Remote store extra configuration. + ''; + }; + + extraConfig.account = mkOption { + type = extraConfigType; + default = { }; + example = literalExample '' + { + PipelineDepth = 10; + Timeout = 60; + }; + ''; + description = '' + Account section extra configuration. + ''; + }; + }; +} diff --git a/home-manager/modules/programs/mbsync.nix b/home-manager/modules/programs/mbsync.nix new file mode 100644 index 00000000000..f2814b393d0 --- /dev/null +++ b/home-manager/modules/programs/mbsync.nix @@ -0,0 +1,168 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.mbsync; + + # Accounts for which mbsync is enabled. + mbsyncAccounts = + filter (a: a.mbsync.enable) (attrValues config.accounts.email.accounts); + + genTlsConfig = tls: + { + SSLType = if !tls.enable then + "None" + else if tls.useStartTls then + "STARTTLS" + else + "IMAPS"; + } // optionalAttrs (tls.enable && tls.certificatesFile != null) { + CertificateFile = toString tls.certificatesFile; + }; + + masterSlaveMapping = { + none = "None"; + imap = "Master"; + maildir = "Slave"; + both = "Both"; + }; + + genSection = header: entries: + let + escapeValue = escape [ ''"'' ]; + hasSpace = v: builtins.match ".* .*" v != null; + genValue = n: v: + if isList v then + concatMapStringsSep " " (genValue n) v + else if isBool v then + (if v then "yes" else "no") + else if isInt v then + toString v + else if isString v && hasSpace v then + ''"${escapeValue v}"'' + else if isString v then + v + else + let prettyV = lib.generators.toPretty { } v; + in throw "mbsync: unexpected value for option ${n}: '${prettyV}'"; + in '' + ${header} + ${concatStringsSep "\n" + (mapAttrsToList (n: v: "${n} ${genValue n v}") entries)} + ''; + + genAccountConfig = account: + with account; + genSection "IMAPAccount ${name}" ({ + Host = imap.host; + User = userName; + PassCmd = toString passwordCommand; + } // genTlsConfig imap.tls + // optionalAttrs (imap.port != null) { Port = toString imap.port; } + // mbsync.extraConfig.account) + "\n" + + genSection "IMAPStore ${name}-remote" + ({ Account = name; } // mbsync.extraConfig.remote) + "\n" + + genSection "MaildirStore ${name}-local" ({ + Path = "${maildir.absPath}/"; + Inbox = "${maildir.absPath}/${folders.inbox}"; + SubFolders = "Verbatim"; + } // optionalAttrs (mbsync.flatten != null) { Flatten = mbsync.flatten; } + // mbsync.extraConfig.local) + "\n" + genSection "Channel ${name}" ({ + Master = ":${name}-remote:"; + Slave = ":${name}-local:"; + Patterns = mbsync.patterns; + Create = masterSlaveMapping.${mbsync.create}; + Remove = masterSlaveMapping.${mbsync.remove}; + Expunge = masterSlaveMapping.${mbsync.expunge}; + SyncState = "*"; + } // mbsync.extraConfig.channel) + "\n"; + + genGroupConfig = name: channels: + let + genGroupChannel = n: boxes: "Channel ${n}:${concatStringsSep "," boxes}"; + in concatStringsSep "\n" + ([ "Group ${name}" ] ++ mapAttrsToList genGroupChannel channels); + +in { + options = { + programs.mbsync = { + enable = mkEnableOption "mbsync IMAP4 and Maildir mailbox synchronizer"; + + package = mkOption { + type = types.package; + default = pkgs.isync; + defaultText = literalExample "pkgs.isync"; + example = literalExample "pkgs.isync"; + description = "The package to use for the mbsync binary."; + }; + + groups = mkOption { + type = types.attrsOf (types.attrsOf (types.listOf types.str)); + default = { }; + example = literalExample '' + { + inboxes = { + account1 = [ "Inbox" ]; + account2 = [ "Inbox" ]; + }; + } + ''; + description = '' + Definition of groups. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to add to the mbsync configuration. + ''; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./mbsync-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + assertions = let + checkAccounts = pred: msg: + let badAccounts = filter pred mbsyncAccounts; + in { + assertion = badAccounts == [ ]; + message = "mbsync: ${msg} for accounts: " + + concatMapStringsSep ", " (a: a.name) badAccounts; + }; + in [ + (checkAccounts (a: a.maildir == null) "Missing maildir configuration") + (checkAccounts (a: a.imap == null) "Missing IMAP configuration") + (checkAccounts (a: a.passwordCommand == null) "Missing passwordCommand") + (checkAccounts (a: a.userName == null) "Missing username") + ]; + + home.packages = [ cfg.package ]; + + programs.notmuch.new.ignore = [ ".uidvalidity" ".mbsyncstate" ]; + + home.file.".mbsyncrc".text = let + accountsConfig = map genAccountConfig mbsyncAccounts; + groupsConfig = mapAttrsToList genGroupConfig cfg.groups; + in concatStringsSep "\n" (['' + # Generated by Home Manager. + ''] ++ optional (cfg.extraConfig != "") cfg.extraConfig ++ accountsConfig + ++ groupsConfig) + "\n"; + + home.activation = mkIf (mbsyncAccounts != [ ]) { + createMaildir = + hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] '' + $DRY_RUN_CMD mkdir -m700 -p $VERBOSE_ARG ${ + concatMapStringsSep " " (a: a.maildir.absPath) mbsyncAccounts + } + ''; + }; + }; +} 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/mercurial.nix b/home-manager/modules/programs/mercurial.nix new file mode 100644 index 00000000000..8e9a3befbaf --- /dev/null +++ b/home-manager/modules/programs/mercurial.nix @@ -0,0 +1,100 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.mercurial; + +in { + + options = { + programs.mercurial = { + enable = mkEnableOption "Mercurial"; + + package = mkOption { + type = types.package; + default = pkgs.mercurial; + defaultText = literalExample "pkgs.mercurial"; + description = "Mercurial package to install."; + }; + + userName = mkOption { + type = types.str; + description = "Default user name to use."; + }; + + userEmail = mkOption { + type = types.str; + description = "Default user email to use."; + }; + + aliases = mkOption { + type = types.attrs; + default = { }; + description = "Mercurial aliases to define."; + }; + + extraConfig = mkOption { + type = types.either types.attrs types.lines; + default = { }; + description = "Additional configuration to add."; + }; + + iniContent = mkOption { + type = types.attrsOf types.attrs; + internal = true; + }; + + ignores = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "*~" "*.swp" ]; + description = "List of globs for files to be globally ignored."; + }; + + ignoresRegexp = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "^.*~$" "^.*\\.swp$" ]; + description = + "List of regular expressions for files to be globally ignored."; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ cfg.package ]; + + programs.mercurial.iniContent.ui = { + username = cfg.userName + " <" + cfg.userEmail + ">"; + }; + + xdg.configFile."hg/hgrc".text = generators.toINI { } cfg.iniContent; + } + + (mkIf (cfg.ignores != [ ] || cfg.ignoresRegexp != [ ]) { + programs.mercurial.iniContent.ui.ignore = + "${config.xdg.configHome}/hg/hgignore_global"; + + xdg.configFile."hg/hgignore_global".text = '' + syntax: glob + '' + concatStringsSep "\n" cfg.ignores + "\n" + '' + syntax: regexp + '' + concatStringsSep "\n" cfg.ignoresRegexp + "\n"; + }) + + (mkIf (cfg.aliases != { }) { + programs.mercurial.iniContent.alias = cfg.aliases; + }) + + (mkIf (lib.isAttrs cfg.extraConfig) { + programs.mercurial.iniContent = cfg.extraConfig; + }) + + (mkIf (lib.isString cfg.extraConfig) { + xdg.configFile."hg/hgrc".text = cfg.extraConfig; + }) + ]); +} diff --git a/home-manager/modules/programs/mpv.nix b/home-manager/modules/programs/mpv.nix new file mode 100644 index 00000000000..a5b0517fe0a --- /dev/null +++ b/home-manager/modules/programs/mpv.nix @@ -0,0 +1,143 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + inherit (builtins) typeOf stringLength; + + cfg = config.programs.mpv; + + mpvOption = with types; either str (either int (either bool float)); + mpvOptions = with types; attrsOf mpvOption; + mpvProfiles = with types; attrsOf mpvOptions; + mpvBindings = with types; attrsOf str; + + renderOption = option: + rec { + int = toString option; + float = int; + + bool = if option then "yes" else "no"; + + string = option; + }.${typeOf option}; + + renderOptions = options: + concatStringsSep "\n" (mapAttrsToList (name: value: + let + rendered = renderOption value; + length = toString (stringLength rendered); + in "${name}=%${length}%${rendered}") options); + + renderProfiles = profiles: + concatStringsSep "\n" (mapAttrsToList (name: value: '' + [${name}] + ${renderOptions value} + '') profiles); + + renderBindings = bindings: + concatStringsSep "\n" + (mapAttrsToList (name: value: "${name} ${value}") bindings); + +in { + options = { + programs.mpv = { + enable = mkEnableOption "mpv"; + + scripts = mkOption { + type = with types; listOf (either package str); + default = [ ]; + example = literalExample "[ pkgs.mpvScripts.mpris ]"; + description = '' + List of scripts to use with mpv. + ''; + }; + + config = mkOption { + description = '' + Configuration written to + <filename>~/.config/mpv/mpv.conf</filename>. See + <citerefentry> + <refentrytitle>mpv</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for the full list of options. + ''; + type = mpvOptions; + default = { }; + example = literalExample '' + { + profile = "gpu-hq"; + force-window = "yes"; + ytdl-format = "bestvideo+bestaudio"; + cache-default = 4000000; + } + ''; + }; + + profiles = mkOption { + description = '' + Sub-configuration options for specific profiles written to + <filename>~/.config/mpv/mpv.conf</filename>. See + <option>programs.mpv.config</option> for more information. + ''; + type = mpvProfiles; + default = { }; + example = literalExample '' + { + fast = { + vo = "vdpau"; + }; + "protocol.dvd" = { + profile-desc = "profile for dvd:// streams"; + alang = "en"; + }; + } + ''; + }; + + bindings = mkOption { + description = '' + Input configuration written to + <filename>~/.config/mpv/input.conf</filename>. See + <citerefentry> + <refentrytitle>mpv</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for the full list of options. + ''; + type = mpvBindings; + default = { }; + example = literalExample '' + { + WHEEL_UP = "seek 10"; + WHEEL_DOWN = "seek -10"; + "Alt+0" = "set window-scale 0.5"; + } + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ + (if cfg.scripts == [ ] then + pkgs.mpv + else + pkgs.wrapMpv pkgs.mpv-unwrapped { scripts = cfg.scripts; }) + ]; + } + (mkIf (cfg.config != { } || cfg.profiles != { }) { + xdg.configFile."mpv/mpv.conf".text = '' + ${optionalString (cfg.config != { }) (renderOptions cfg.config)} + ${optionalString (cfg.profiles != { }) (renderProfiles cfg.profiles)} + ''; + }) + (mkIf (cfg.bindings != { }) { + xdg.configFile."mpv/input.conf".text = renderBindings cfg.bindings; + }) + ]); + + meta.maintainers = with maintainers; [ tadeokondrak ]; +} diff --git a/home-manager/modules/programs/msmtp-accounts.nix b/home-manager/modules/programs/msmtp-accounts.nix new file mode 100644 index 00000000000..894cef51742 --- /dev/null +++ b/home-manager/modules/programs/msmtp-accounts.nix @@ -0,0 +1,48 @@ +{ config, lib, ... }: + +with lib; + +{ + options.msmtp = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable msmtp. + </para><para> + If enabled then it is possible to use the + <parameter class="command">--account</parameter> command line + option to send a message for a given account using the + <command>msmtp</command> or <command>msmtpq</command> tool. + For example, <command>msmtp --account=private</command> would + send using the account defined in + <option>accounts.email.accounts.private</option>. If the + <parameter class="command">--account</parameter> option is not + given then the primary account will be used. + ''; + }; + + tls.fingerprint = mkOption { + type = + types.nullOr (types.strMatching "([[:alnum:]]{2}:)+[[:alnum:]]{2}"); + default = null; + example = "my:SH:a2:56:ha:sh"; + description = '' + Fingerprint of a trusted TLS certificate. + The fingerprint can be obtained by executing + <command>msmtp --serverinfo --tls --tls-certcheck=off</command>. + ''; + }; + + extraConfig = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { auth = "login"; }; + description = '' + Extra configuration options to add to <filename>~/.msmtprc</filename>. + See <link xlink:href="https://marlam.de/msmtp/msmtprc.txt"/> for + examples. + ''; + }; + }; +} diff --git a/home-manager/modules/programs/msmtp.nix b/home-manager/modules/programs/msmtp.nix new file mode 100644 index 00000000000..7b6704860e0 --- /dev/null +++ b/home-manager/modules/programs/msmtp.nix @@ -0,0 +1,75 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.msmtp; + + msmtpAccounts = + filter (a: a.msmtp.enable) (attrValues config.accounts.email.accounts); + + onOff = p: if p then "on" else "off"; + + accountStr = account: + with account; + concatStringsSep "\n" ([ "account ${name}" ] + ++ mapAttrsToList (n: v: n + " " + v) ({ + host = smtp.host; + from = address; + auth = "on"; + user = userName; + tls = onOff smtp.tls.enable; + tls_starttls = onOff smtp.tls.useStartTls; + tls_trust_file = smtp.tls.certificatesFile; + } // optionalAttrs (msmtp.tls.fingerprint != null) { + tls_fingerprint = msmtp.tls.fingerprint; + } // optionalAttrs (smtp.port != null) { port = toString smtp.port; } + // optionalAttrs (passwordCommand != null) { + # msmtp requires the password to finish with a newline. + passwordeval = + ''${pkgs.bash}/bin/bash -c "${toString passwordCommand}; echo"''; + } // msmtp.extraConfig) ++ optional primary '' + + account default : ${name}''); + + configFile = mailAccounts: '' + # Generated by Home Manager. + + ${cfg.extraConfig} + + ${concatStringsSep "\n\n" (map accountStr mailAccounts)} + ''; + +in { + + options = { + programs.msmtp = { + enable = mkEnableOption "msmtp"; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration lines to add to <filename>~/.msmtprc</filename>. + See <link xlink:href="https://marlam.de/msmtp/msmtprc.txt"/> for examples. + ''; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./msmtp-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.msmtp ]; + + xdg.configFile."msmtp/config".text = configFile msmtpAccounts; + + home.sessionVariables = { + MSMTP_QUEUE = "${config.xdg.dataHome}/msmtp/queue"; + MSMTP_LOG = "${config.xdg.dataHome}/msmtp/queue.log"; + }; + }; +} 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 new file mode 100644 index 00000000000..009cf1fa7e8 --- /dev/null +++ b/home-manager/modules/programs/neomutt-accounts.nix @@ -0,0 +1,36 @@ +{ config, lib, ... }: + +with lib; + +{ + options.neomutt = { + enable = mkEnableOption "NeoMutt"; + + sendMailCommand = mkOption { + type = types.nullOr types.str; + 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. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = "color status cyan default"; + description = '' + Extra lines to add to the folder hook for this account. + ''; + }; + }; +} diff --git a/home-manager/modules/programs/neomutt.nix b/home-manager/modules/programs/neomutt.nix new file mode 100644 index 00000000000..f2a6bbfff08 --- /dev/null +++ b/home-manager/modules/programs/neomutt.nix @@ -0,0 +1,312 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.neomutt; + + neomuttAccounts = + filter (a: a.neomutt.enable) (attrValues config.accounts.email.accounts); + + sidebarModule = types.submodule { + options = { + enable = mkEnableOption "sidebar support"; + + width = mkOption { + type = types.int; + default = 22; + description = "Width of the sidebar"; + }; + + shortPath = mkOption { + type = types.bool; + default = true; + description = '' + By default sidebar shows the full path of the mailbox, but + with this enabled only the relative name is shown. + ''; + }; + + format = mkOption { + type = types.str; + default = "%B%?F? [%F]?%* %?N?%N/?%S"; + description = '' + Sidebar format. Check neomutt documentation for details. + ''; + }; + }; + }; + + sortOptions = [ + "date" + "date-received" + "from" + "mailbox-order" + "score" + "size" + "spam" + "subject" + "threads" + "to" + ]; + + bindModule = types.submodule { + options = { + map = mkOption { + type = types.enum [ + "alias" + "attach" + "browser" + "compose" + "editor" + "generic" + "index" + "mix" + "pager" + "pgp" + "postpone" + "query" + "smime" + ]; + default = "index"; + description = "Select the menu to bind the command to."; + }; + + key = mkOption { + type = types.str; + example = "<left>"; + description = "The key to bind."; + }; + + action = mkOption { + type = types.str; + example = "<enter-command>toggle sidebar_visible<enter><refresh>"; + description = "Specify the action to take."; + }; + }; + }; + + yesno = x: if x then "yes" else "no"; + setOption = n: v: if v == null then "unset ${n}" else "set ${n}=${v}"; + escape = replaceStrings [ "%" ] [ "%25" ]; + + accountFilename = account: config.xdg.configHome + "/neomutt/" + account.name; + + genCommonFolderHooks = account: + with account; { + from = "'${address}'"; + realname = "'${realName}'"; + spoolfile = "'+${folders.inbox}'"; + record = if folders.sent == null then null else "'+${folders.sent}'"; + postponed = "'+${folders.drafts}'"; + trash = "'+${folders.trash}'"; + }; + + mtaSection = account: + with account; + let passCmd = concatStringsSep " " passwordCommand; + in if neomutt.sendMailCommand != null then { + sendmail = "'${neomutt.sendMailCommand}'"; + } else + let + smtpProto = if smtp.tls.enable then "smtps" else "smtp"; + smtpPort = if smtp.port != null then ":${toString smtp.port}" else ""; + smtpBaseUrl = + "${smtpProto}://${escape userName}@${smtp.host}${smtpPort}"; + in { + smtp_url = "'${smtpBaseUrl}'"; + smtp_pass = "'`${passCmd}`'"; + }; + + genMaildirAccountConfig = account: + with account; + let + folderHook = mapAttrsToList setOption (genCommonFolderHooks account // { + folder = "'${account.maildir.absPath}'"; + }) ++ optional (neomutt.extraConfig != "") neomutt.extraConfig; + in '' + ${concatStringsSep "\n" folderHook} + ''; + + registerAccount = account: + with account; '' + # register account ${name} + mailboxes "${account.maildir.absPath}/${folders.inbox}" + folder-hook ${account.maildir.absPath}/ " \ + source ${accountFilename account} " + ''; + + mraSection = account: + with account; + if account.maildir != null then + genMaildirAccountConfig account + else + throw "Only maildir is supported at the moment"; + + optionsStr = attrs: concatStringsSep "\n" (mapAttrsToList setOption attrs); + + sidebarSection = '' + # Sidebar + set sidebar_visible = yes + set sidebar_short_path = ${yesno cfg.sidebar.shortPath} + set sidebar_width = ${toString cfg.sidebar.width} + set sidebar_format = '${cfg.sidebar.format}' + ''; + + bindSection = concatMapStringsSep "\n" + (bind: ''bind ${bind.map} ${bind.key} "${bind.action}"'') cfg.binds; + + macroSection = concatMapStringsSep "\n" + (bind: ''macro ${bind.map} ${bind.key} "${bind.action}"'') cfg.macros; + + mailCheckSection = '' + set mail_check_stats + set mail_check_stats_interval = ${toString cfg.checkStatsInterval} + ''; + + notmuchSection = account: + with account; '' + # notmuch section + set nm_default_uri = "notmuch://${config.accounts.email.maildirBasePath}" + virtual-mailboxes "My INBOX" "notmuch://?query=tag:inbox" + ''; + + accountStr = account: + with account; + '' + # Generated by Home Manager. + set ssl_force_tls = yes + set certificate_file=${config.accounts.email.certificatesFile} + + # GPG section + set crypt_use_gpgme = yes + set crypt_autosign = ${yesno (gpg.signByDefault or false)} + set pgp_use_gpg_agent = yes + set mbox_type = ${if maildir != null then "Maildir" else "mbox"} + set sort = "${cfg.sort}" + + # MTA section + ${optionsStr (mtaSection account)} + + ${optionalString (cfg.checkStatsInterval != null) mailCheckSection} + + ${optionalString cfg.sidebar.enable sidebarSection} + + # MRA section + ${mraSection account} + + # Extra configuration + ${account.neomutt.extraConfig} + '' + optionalString (account.signature.showSignature != "none") '' + set signature = ${pkgs.writeText "signature.txt" account.signature.text} + '' + optionalString account.notmuch.enable (notmuchSection account); + +in { + options = { + programs.neomutt = { + enable = mkEnableOption "the NeoMutt mail client"; + + sidebar = mkOption { + type = sidebarModule; + default = { }; + description = "Options related to the sidebar."; + }; + + binds = mkOption { + type = types.listOf bindModule; + default = [ ]; + description = "List of keybindings."; + }; + + macros = mkOption { + type = types.listOf bindModule; + default = [ ]; + description = "List of macros."; + }; + + sort = mkOption { + # 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."; + }; + + vimKeys = mkOption { + type = types.bool; + default = false; + description = "Enable vim-like bindings."; + }; + + checkStatsInterval = mkOption { + type = types.nullOr types.int; + default = null; + example = 60; + description = "Enable and set the interval of automatic mail check."; + }; + + editor = mkOption { + type = types.str; + default = "$EDITOR"; + description = "Select the editor used for writing mail."; + }; + + settings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Extra configuration appended to the end."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Extra configuration appended to the end."; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./neomutt-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.neomutt ]; + home.file = let + rcFile = account: { + "${accountFilename account}".text = accountStr account; + }; + in foldl' (a: b: a // b) { } (map rcFile neomuttAccounts); + + xdg.configFile."neomutt/neomuttrc" = mkIf (neomuttAccounts != [ ]) { + text = let primary = filter (a: a.primary) neomuttAccounts; + in '' + # Generated by Home Manager. + set header_cache = "${config.xdg.cacheHome}/neomutt/headers/" + set message_cachedir = "${config.xdg.cacheHome}/neomutt/messages/" + set editor = "${cfg.editor}" + set implicit_autoview = yes + + alternative_order text/enriched text/plain text + + set delete = yes + + # Binds + ${bindSection} + + # Macros + ${macroSection} + + ${optionalString cfg.vimKeys + "source ${pkgs.neomutt}/share/doc/neomutt/vim-keys/vim-keys.rc"} + + # Extra configuration + ${optionsStr cfg.settings} + + ${cfg.extraConfig} + '' + concatMapStringsSep "\n" registerAccount neomuttAccounts + + # source primary account + "source ${accountFilename (builtins.head primary)}"; + }; + }; +} diff --git a/home-manager/modules/programs/neovim.nix b/home-manager/modules/programs/neovim.nix new file mode 100644 index 00000000000..858f5576ad1 --- /dev/null +++ b/home-manager/modules/programs/neovim.nix @@ -0,0 +1,219 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.neovim; + + extraPythonPackageType = mkOptionType { + name = "extra-python-packages"; + description = "python packages in python.withPackages format"; + check = with types; (x: if isFunction x + then isList (x pkgs.pythonPackages) + else false); + merge = mergeOneOption; + }; + + extraPython3PackageType = mkOptionType { + name = "extra-python3-packages"; + description = "python3 packages in python.withPackages format"; + check = with types; (x: if isFunction x + then isList (x pkgs.python3Packages) + else false); + merge = mergeOneOption; + }; + + moduleConfigure = + optionalAttrs (cfg.extraConfig != "") { + customRC = cfg.extraConfig; + } + // optionalAttrs (cfg.plugins != []) { + packages.home-manager.start = cfg.plugins; + }; + +in + +{ + options = { + programs.neovim = { + enable = mkEnableOption "Neovim"; + + viAlias = mkOption { + type = types.bool; + default = false; + description = '' + Symlink <command>vi</command> to <command>nvim</command> binary. + ''; + }; + + vimAlias = mkOption { + type = types.bool; + default = false; + description = '' + 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>. + ''; + }; + + withNodeJs = mkOption { + type = types.bool; + default = false; + description = '' + Enable node provider. Set to <literal>true</literal> to + use Node plugins. + ''; + }; + + withPython = mkOption { + type = types.bool; + default = true; + description = '' + Enable Python 2 provider. Set to <literal>true</literal> to + use Python 2 plugins. + ''; + }; + + extraPythonPackages = mkOption { + type = with types; either extraPythonPackageType (listOf package); + default = (_: []); + defaultText = "ps: []"; + example = literalExample "(ps: with ps; [ pandas jedi ])"; + description = '' + A function in python.withPackages format, which returns a + list of Python 2 packages required for your plugins to work. + ''; + }; + + withRuby = mkOption { + type = types.nullOr types.bool; + default = true; + description = '' + Enable ruby provider. + ''; + }; + + withPython3 = mkOption { + type = types.bool; + default = true; + description = '' + Enable Python 3 provider. Set to <literal>true</literal> to + use Python 3 plugins. + ''; + }; + + extraPython3Packages = mkOption { + type = with types; either extraPython3PackageType (listOf package); + default = (_: []); + defaultText = "ps: []"; + example = literalExample "(ps: with ps; [ python-language-server ])"; + description = '' + A function in python.withPackages format, which returns a + list of Python 3 packages required for your plugins to work. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.neovim-unwrapped; + defaultText = literalExample "pkgs.neovim-unwrapped"; + description = "The package to use for the neovim binary."; + }; + + finalPackage = mkOption { + type = types.package; + visible = false; + readOnly = true; + description = "Resulting customized neovim package."; + }; + + configure = mkOption { + type = types.attrs; + default = {}; + example = literalExample '' + configure = { + customRC = $'''' + " here your custom configuration goes! + $''''; + packages.myVimPackage = with pkgs.vimPlugins; { + # loaded on launch + start = [ fugitive ]; + # manually loadable by calling `:packadd $plugin-name` + opt = [ ]; + }; + }; + ''; + description = '' + Generate your init file from your list of plugins and custom commands, + and loads it from the store via <command>nvim -u /nix/store/hash-vimrc</command> + + </para><para> + + This option is mutually exclusive with <varname>extraConfig</varname> + and <varname>plugins</varname>. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + set nocompatible + set nobackup + ''; + description = '' + Custom vimrc lines. + + </para><para> + + This option is mutually exclusive with <varname>configure</varname>. + ''; + }; + + plugins = mkOption { + type = with types; listOf package; + default = [ ]; + example = literalExample "[ pkgs.vimPlugins.yankring ]"; + description = '' + List of vim plugins to install. + + </para><para> + + This option is mutually exclusive with <varname>configure</varname>. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.configure == { } || moduleConfigure == { }; + message = "The programs.neovim option configure is mutually exclusive" + + " with extraConfig and plugins."; + } + ]; + + home.packages = [ cfg.finalPackage ]; + + programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package { + inherit (cfg) + extraPython3Packages withPython3 + extraPythonPackages withPython + withNodeJs withRuby viAlias vimAlias; + + 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 new file mode 100644 index 00000000000..793b30680bf --- /dev/null +++ b/home-manager/modules/programs/newsboat.nix @@ -0,0 +1,123 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.newsboat; + wrapQuote = x: ''"${x}"''; + +in { + options = { + programs.newsboat = { + enable = mkEnableOption "the Newsboat feed reader"; + + urls = mkOption { + type = types.listOf (types.submodule { + options = { + url = mkOption { + type = types.str; + example = "http://example.com"; + description = "Feed URL."; + }; + + tags = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "foo" "bar" ]; + description = "Feed tags."; + }; + + title = mkOption { + type = types.nullOr types.str; + default = null; + example = "ORF News"; + description = "Feed title."; + }; + }; + }); + default = [ ]; + example = [{ + url = "http://example.com"; + tags = [ "foo" "bar" ]; + }]; + description = "List of news feeds."; + }; + + maxItems = mkOption { + type = types.int; + default = 0; + description = "Maximum number of items per feed, 0 for infinite."; + }; + + reloadThreads = mkOption { + type = types.int; + default = 5; + description = "How many threads to use for updating the feeds."; + }; + + autoReload = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable automatic reloading while newsboat is running. + ''; + }; + + reloadTime = mkOption { + type = types.nullOr types.int; + default = 60; + description = "Time in minutes between reloads."; + }; + + browser = mkOption { + type = types.str; + default = "${pkgs.xdg_utils}/bin/xdg-open"; + description = "External browser to use."; + }; + + queries = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { "foo" = ''rssurl =~ "example.com"''; }; + description = "A list of queries to use."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration values that will be appended to the end. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.newsboat ]; + home.file.".newsboat/urls".text = let + mkUrlEntry = u: + concatStringsSep " " ([ u.url ] ++ map wrapQuote u.tags + ++ optional (u.title != null) (wrapQuote "~${u.title}")); + urls = map mkUrlEntry cfg.urls; + + mkQueryEntry = n: v: ''"query:${n}:${escape [ ''"'' ] v}"''; + queries = mapAttrsToList mkQueryEntry cfg.queries; + 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} + browser ${cfg.browser} + reload-threads ${toString cfg.reloadThreads} + auto-reload ${if cfg.autoReload then "yes" else "no"} + ${optionalString (cfg.reloadTime != null) + (toString "reload-time ${toString cfg.reloadTime}")} + prepopulate-query-feeds yes + + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/noti.nix b/home-manager/modules/programs/noti.nix new file mode 100644 index 00000000000..348555eef51 --- /dev/null +++ b/home-manager/modules/programs/noti.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.noti; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.noti = { + enable = mkEnableOption "Noti"; + + settings = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { }; + description = '' + Configuration written to + <filename>~/.config/noti/noti.yaml</filename>. + </para><para> + See + <citerefentry> + <refentrytitle>noti.yaml</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry>. + for the full list of options. + ''; + example = literalExample '' + { + say = { + voice = "Alex"; + }; + slack = { + token = "1234567890abcdefg"; + channel = "@jaime"; + }; + } + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.noti ]; + + xdg.configFile."noti/noti.yaml" = + mkIf (cfg.settings != { }) { text = generators.toYAML { } cfg.settings; }; + }; + +} diff --git a/home-manager/modules/programs/notmuch-accounts.nix b/home-manager/modules/programs/notmuch-accounts.nix new file mode 100644 index 00000000000..fd4a811d73d --- /dev/null +++ b/home-manager/modules/programs/notmuch-accounts.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + options.notmuch = { enable = lib.mkEnableOption "notmuch indexing"; }; +} diff --git a/home-manager/modules/programs/notmuch.nix b/home-manager/modules/programs/notmuch.nix new file mode 100644 index 00000000000..9070d755671 --- /dev/null +++ b/home-manager/modules/programs/notmuch.nix @@ -0,0 +1,195 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.notmuch; + + mkIniKeyValue = key: value: + let + tweakVal = v: + if isString v then + v + else if isList v then + concatMapStringsSep ";" tweakVal v + else if isBool v then + (if v then "true" else "false") + else + toString v; + in "${key}=${tweakVal value}"; + + notmuchIni = recursiveUpdate { + database = { path = config.accounts.email.maildirBasePath; }; + + maildir = { synchronize_flags = cfg.maildir.synchronizeFlags; }; + + new = { + ignore = cfg.new.ignore; + tags = cfg.new.tags; + }; + + user = let + accounts = filter (a: a.notmuch.enable) + (attrValues config.accounts.email.accounts); + primary = filter (a: a.primary) accounts; + secondaries = filter (a: !a.primary) accounts; + in { + name = catAttrs "realName" primary; + primary_email = catAttrs "address" primary; + other_email = catAttrs "aliases" primary ++ catAttrs "address" secondaries + ++ catAttrs "aliases" secondaries; + }; + + search = { exclude_tags = cfg.search.excludeTags; }; + } cfg.extraConfig; + +in { + options = { + programs.notmuch = { + enable = mkEnableOption "Notmuch mail indexer"; + + new = mkOption { + type = types.submodule { + options = { + ignore = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + A list to specify files and directories that will not be + searched for messages by <command>notmuch new</command>. + ''; + }; + + tags = mkOption { + type = types.listOf types.str; + default = [ "unread" "inbox" ]; + example = [ "new" ]; + description = '' + A list of tags that will be added to all messages + incorporated by <command>notmuch new</command>. + ''; + }; + }; + }; + default = { }; + description = '' + Options related to email processing performed by + <command>notmuch new</command>. + ''; + }; + + extraConfig = mkOption { + type = types.attrsOf (types.attrsOf types.str); + default = { }; + description = '' + Options that should be appended to the notmuch configuration file. + ''; + }; + + hooks = { + preNew = mkOption { + type = types.lines; + default = ""; + example = "mbsync --all"; + description = '' + Bash statements run before scanning or importing new + messages into the database. + ''; + }; + + postNew = mkOption { + type = types.lines; + default = ""; + example = '' + notmuch tag +nixos -- tag:new and from:nixos1@discoursemail.com + ''; + description = '' + Bash statements run after new messages have been imported + into the database and initial tags have been applied. + ''; + }; + + postInsert = mkOption { + type = types.lines; + default = ""; + description = '' + Bash statements run after a message has been inserted + into the database and initial tags have been applied. + ''; + }; + }; + + maildir = { + synchronizeFlags = mkOption { + type = types.bool; + default = true; + description = '' + Whether to synchronize Maildir flags. + ''; + }; + }; + + search = { + excludeTags = mkOption { + type = types.listOf types.str; + default = [ "deleted" "spam" ]; + example = [ "trash" "spam" ]; + description = '' + A list of tags that will be excluded from search results by + default. Using an excluded tag in a query will override that + exclusion. + ''; + }; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./notmuch-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = notmuchIni.user.name != [ ]; + message = "notmuch: Must have a user name set."; + } + { + assertion = notmuchIni.user.primary_email != [ ]; + message = "notmuch: Must have a user primary email address set."; + } + ]; + + home.packages = [ pkgs.notmuch ]; + + home.sessionVariables = { + NOTMUCH_CONFIG = "${config.xdg.configHome}/notmuch/notmuchrc"; + NMBGIT = "${config.xdg.dataHome}/notmuch/nmbug"; + }; + + xdg.configFile."notmuch/notmuchrc".text = + let toIni = generators.toINI { mkKeyValue = mkIniKeyValue; }; + in '' + # Generated by Home Manager. + + '' + toIni notmuchIni; + + home.file = let + hook = name: cmds: { + "${notmuchIni.database.path}/.notmuch/hooks/${name}".source = + pkgs.writeShellScript name '' + export PATH="${pkgs.notmuch}/bin''${PATH:+:}$PATH" + export NOTMUCH_CONFIG="${config.xdg.configHome}/notmuch/notmuchrc" + export NMBGIT="${config.xdg.dataHome}/notmuch/nmbug" + + ${cmds} + ''; + }; + in optionalAttrs (cfg.hooks.preNew != "") (hook "pre-new" cfg.hooks.preNew) + // optionalAttrs (cfg.hooks.postNew != "") + (hook "post-new" cfg.hooks.postNew) + // optionalAttrs (cfg.hooks.postInsert != "") + (hook "post-insert" cfg.hooks.postInsert); + }; +} 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/obs-studio.nix b/home-manager/modules/programs/obs-studio.nix new file mode 100644 index 00000000000..6df5978384c --- /dev/null +++ b/home-manager/modules/programs/obs-studio.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.obs-studio; + package = pkgs.obs-studio; + + mkPluginEnv = packages: + let + pluginDirs = map (pkg: "${pkg}/share/obs/obs-plugins") packages; + plugins = concatMapStringsSep " " (p: "${p}/*") pluginDirs; + in pkgs.runCommand "obs-studio-plugins" { + preferLocalBuild = true; + allowSubstitutes = false; + } '' + mkdir $out + [[ '${plugins}' ]] || exit 0 + for plugin in ${plugins}; do + ln -s "$plugin" $out/ + done + ''; + +in { + meta.maintainers = [ maintainers.adisbladis ]; + + options = { + programs.obs-studio = { + enable = mkEnableOption "obs-studio"; + + plugins = mkOption { + default = [ ]; + example = literalExample "[ pkgs.obs-linuxbrowser ]"; + description = "Optional OBS plugins."; + type = types.listOf types.package; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ package ]; + + xdg.configFile."obs-studio/plugins" = + mkIf (cfg.plugins != [ ]) { source = mkPluginEnv cfg.plugins; }; + }; +} diff --git a/home-manager/modules/programs/offlineimap-accounts.nix b/home-manager/modules/programs/offlineimap-accounts.nix new file mode 100644 index 00000000000..afc7a019972 --- /dev/null +++ b/home-manager/modules/programs/offlineimap-accounts.nix @@ -0,0 +1,51 @@ +{ config, lib, ... }: + +with lib; + +let + + extraConfigType = with types; attrsOf (either (either str int) bool); + +in { + options.offlineimap = { + enable = mkEnableOption "OfflineIMAP"; + + extraConfig.account = mkOption { + type = extraConfigType; + default = { }; + example = { autorefresh = 20; }; + description = '' + Extra configuration options to add to the account section. + ''; + }; + + extraConfig.local = mkOption { + type = extraConfigType; + default = { }; + example = { sync_deletes = true; }; + description = '' + Extra configuration options to add to the local account + section. + ''; + }; + + extraConfig.remote = mkOption { + type = extraConfigType; + default = { }; + example = { + maxconnections = 2; + expunge = false; + }; + description = '' + Extra configuration options to add to the remote account + section. + ''; + }; + + postSyncHookCommand = mkOption { + type = types.lines; + default = ""; + description = "Command to run after fetching new mails."; + }; + }; +} diff --git a/home-manager/modules/programs/offlineimap.nix b/home-manager/modules/programs/offlineimap.nix new file mode 100644 index 00000000000..b6ba847e9b7 --- /dev/null +++ b/home-manager/modules/programs/offlineimap.nix @@ -0,0 +1,178 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.offlineimap; + + accounts = filter (a: a.offlineimap.enable) + (attrValues config.accounts.email.accounts); + + toIni = generators.toINI { + mkKeyValue = key: value: + let + value' = if isBool value then + (if value then "yes" else "no") + else + toString value; + in "${key} = ${value'}"; + }; + + # Generates a script to fetch only a specific account. + # + # Note, these scripts are not actually created and installed at the + # moment. It will need some thinking on whether this is a good idea + # and whether other modules should have some similar functionality. + # + # Perhaps have a single tool `email` that wraps the command? + # Something like + # + # $ email <account name> <program name> <program args> + genOfflineImapScript = account: + with account; + pkgs.writeShellScriptBin "offlineimap-${name}" '' + exec ${pkgs.offlineimap}/bin/offlineimap -a${account.name} "$@" + ''; + + accountStr = account: + with account; + let + postSyncHook = optionalAttrs (offlineimap.postSyncHookCommand != "") { + postsynchook = pkgs.writeShellScriptBin "postsynchook" + offlineimap.postSyncHookCommand + "/bin/postsynchook"; + }; + + localType = + if account.flavor == "gmail.com" then "GmailMaildir" else "Maildir"; + + remoteType = if account.flavor == "gmail.com" then "Gmail" else "IMAP"; + + remoteHost = + optionalAttrs (imap.host != null) { remotehost = imap.host; }; + + remotePort = + optionalAttrs ((imap.port or null) != null) { remoteport = imap.port; }; + + ssl = if imap.tls.enable then { + ssl = true; + sslcacertfile = imap.tls.certificatesFile; + starttls = imap.tls.useStartTls; + } else { + ssl = false; + }; + + remotePassEval = + let arglist = concatMapStringsSep "," (x: "'${x}'") passwordCommand; + in optionalAttrs (passwordCommand != null) { + remotepasseval = ''get_pass("${name}", [${arglist}])''; + }; + in toIni { + "Account ${name}" = { + localrepository = "${name}-local"; + remoterepository = "${name}-remote"; + } // postSyncHook // offlineimap.extraConfig.account; + + "Repository ${name}-local" = { + type = localType; + localfolders = maildir.absPath; + } // offlineimap.extraConfig.local; + + "Repository ${name}-remote" = { + type = remoteType; + remoteuser = userName; + } // remoteHost // remotePort // remotePassEval // ssl + // offlineimap.extraConfig.remote; + }; + + extraConfigType = with types; attrsOf (either (either str int) bool); + +in { + options = { + programs.offlineimap = { + enable = mkEnableOption "OfflineIMAP"; + + pythonFile = mkOption { + type = types.lines; + default = '' + import subprocess + + def get_pass(service, cmd): + return subprocess.check_output(cmd, ) + ''; + description = '' + Python code that can then be used in other parts of the + configuration. + ''; + }; + + extraConfig.general = mkOption { + type = extraConfigType; + default = { }; + example = { + maxage = 30; + ui = "blinkenlights"; + }; + description = '' + Extra configuration options added to the + <option>general</option> section. + ''; + }; + + extraConfig.default = mkOption { + type = extraConfigType; + default = { }; + example = { gmailtrashfolder = "[Gmail]/Papierkorb"; }; + description = '' + Extra configuration options added to the + <option>DEFAULT</option> section. + ''; + }; + + extraConfig.mbnames = mkOption { + type = extraConfigType; + default = { }; + example = literalExample '' + { + filename = "~/.config/mutt/mailboxes"; + header = "'mailboxes '"; + peritem = "'+%(accountname)s/%(foldername)s'"; + sep = "' '"; + footer = "'\\n'"; + } + ''; + description = '' + Extra configuration options added to the + <code>mbnames</code> section. + ''; + }; + }; + + accounts.email.accounts = mkOption { + type = with types; + attrsOf (submodule (import ./offlineimap-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.offlineimap ]; + + xdg.configFile."offlineimap/get_settings.py".text = cfg.pythonFile; + + xdg.configFile."offlineimap/config".text = '' + # Generated by Home Manager. + # See https://github.com/OfflineIMAP/offlineimap/blob/master/offlineimap.conf + # for an exhaustive list of options. + '' + toIni ({ + general = { + accounts = concatMapStringsSep "," (a: a.name) accounts; + pythonfile = "${config.xdg.configHome}/offlineimap/get_settings.py"; + metadata = "${config.xdg.dataHome}/offlineimap"; + } // cfg.extraConfig.general; + } // optionalAttrs (cfg.extraConfig.mbnames != { }) { + mbnames = { enabled = true; } // cfg.extraConfig.mbnames; + } // optionalAttrs (cfg.extraConfig.default != { }) { + DEFAULT = cfg.extraConfig.default; + }) + "\n" + concatStringsSep "\n" (map accountStr accounts); + }; +} diff --git a/home-manager/modules/programs/opam.nix b/home-manager/modules/programs/opam.nix new file mode 100644 index 00000000000..a61ff7878df --- /dev/null +++ b/home-manager/modules/programs/opam.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.opam; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.opam = { + enable = mkEnableOption "Opam"; + + package = mkOption { + type = types.package; + default = pkgs.opam; + defaultText = literalExample "pkgs.opam"; + description = "Opam package to install."; + }; + + 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. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + eval "$(${cfg.package}/bin/opam env --shell=bash)" + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + eval "$(${cfg.package}/bin/opam env --shell=zsh)" + ''; + }; +} diff --git a/home-manager/modules/programs/password-store.nix b/home-manager/modules/programs/password-store.nix new file mode 100644 index 00000000000..db31146a1ba --- /dev/null +++ b/home-manager/modules/programs/password-store.nix @@ -0,0 +1,62 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.password-store; + +in { + meta.maintainers = with maintainers; [ pacien ]; + + options.programs.password-store = { + enable = mkEnableOption "Password store"; + + package = mkOption { + type = types.package; + default = pkgs.pass; + defaultText = literalExample "pkgs.pass"; + example = literalExample '' + pkgs.pass.withExtensions (exts: [ exts.pass-otp ]) + ''; + description = '' + The <literal>pass</literal> package to use. + Can be used to specify extensions. + ''; + }; + + settings = mkOption rec { + type = with types; attrsOf str; + apply = mergeAttrs default; + default = { + PASSWORD_STORE_DIR = "${config.xdg.dataHome}/password-store"; + }; + defaultText = literalExample '' + { PASSWORD_STORE_DIR = "$XDG_DATA_HOME/password-store"; } + ''; + example = literalExample '' + { + PASSWORD_STORE_DIR = "/some/directory"; + PASSWORD_STORE_KEY = "12345678"; + PASSWORD_STORE_CLIP_TIME = "60"; + } + ''; + description = '' + The <literal>pass</literal> environment variables dictionary. + </para><para> + See the "Environment variables" section of + <citerefentry> + <refentrytitle>pass</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + and the extension man pages for more information about the + available keys. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + home.sessionVariables = cfg.settings; + }; +} diff --git a/home-manager/modules/programs/pazi.nix b/home-manager/modules/programs/pazi.nix new file mode 100644 index 00000000000..e1a08eb615a --- /dev/null +++ b/home-manager/modules/programs/pazi.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.pazi; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.pazi = { + enable = mkEnableOption "pazi"; + + 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 = [ pkgs.pazi ]; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + eval "$(${pkgs.pazi}/bin/pazi init bash)" + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + eval "$(${pkgs.pazi}/bin/pazi init zsh)" + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + ${pkgs.pazi}/bin/pazi init fish | source + ''; + }; +} diff --git a/home-manager/modules/programs/pidgin.nix b/home-manager/modules/programs/pidgin.nix new file mode 100644 index 00000000000..a375fd1b2bd --- /dev/null +++ b/home-manager/modules/programs/pidgin.nix @@ -0,0 +1,34 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.pidgin; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.pidgin = { + enable = mkEnableOption "Pidgin messaging client"; + + package = mkOption { + type = types.package; + default = pkgs.pidgin; + defaultText = literalExample "pkgs.pidgin"; + description = "The Pidgin package to use."; + }; + + plugins = mkOption { + default = [ ]; + example = literalExample "[ pkgs.pidgin-otr pkgs.pidgin-osd ]"; + description = "Plugins that should be available to Pidgin."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ (cfg.package.override { inherit (cfg) plugins; }) ]; + }; +} 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/readline.nix b/home-manager/modules/programs/readline.nix new file mode 100644 index 00000000000..2f79df6e103 --- /dev/null +++ b/home-manager/modules/programs/readline.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.readline; + + mkSetVariableStr = n: v: + let + mkValueStr = v: + if v == true then + "on" + else if v == false then + "off" + else if isInt v then + toString v + else if isString v then + v + else + abort ("values ${toPretty v} is of unsupported type"); + in "set ${n} ${mkValueStr v}"; + + mkBindingStr = k: v: ''"${k}": ${v}''; + +in { + options.programs.readline = { + enable = mkEnableOption "readline"; + + bindings = mkOption { + default = { }; + type = types.attrsOf types.str; + example = literalExample '' + { "\\C-h" = "backward-kill-word"; } + ''; + description = "Readline bindings."; + }; + + variables = mkOption { + type = with types; attrsOf (either str (either int bool)); + default = { }; + example = { expand-tilde = true; }; + description = '' + Readline customization variable assignments. + ''; + }; + + includeSystemConfig = mkOption { + type = types.bool; + default = true; + description = "Whether to include the system-wide configuration."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Configuration lines appended unchanged to the end of the + <filename>~/.inputrc</filename> file. + ''; + }; + }; + + config = mkIf cfg.enable { + home.file.".inputrc".text = let + configStr = concatStringsSep "\n" + (optional cfg.includeSystemConfig "$include /etc/inputrc" + ++ mapAttrsToList mkSetVariableStr cfg.variables + ++ mapAttrsToList mkBindingStr cfg.bindings); + in '' + # Generated by Home Manager. + + ${configStr} + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/rofi.nix b/home-manager/modules/programs/rofi.nix new file mode 100644 index 00000000000..734bcc423e6 --- /dev/null +++ b/home-manager/modules/programs/rofi.nix @@ -0,0 +1,338 @@ +{ config, lib, pkgs, ... }: + +with lib; +with builtins; + +let + + cfg = config.programs.rofi; + + colorOption = description: + mkOption { + type = types.str; + description = description; + }; + + rowColorSubmodule = types.submodule { + options = { + background = colorOption "Background color"; + foreground = colorOption "Foreground color"; + backgroundAlt = colorOption "Alternative background color"; + highlight = mkOption { + type = types.submodule { + options = { + background = colorOption "Highlight background color"; + foreground = colorOption "Highlight foreground color"; + }; + }; + description = "Color settings for highlighted row."; + }; + }; + }; + + windowColorSubmodule = types.submodule { + options = { + background = colorOption "Window background color"; + border = colorOption "Window border color"; + separator = colorOption "Separator color"; + }; + }; + + colorsSubmodule = types.submodule { + options = { + window = mkOption { + default = null; + type = windowColorSubmodule; + description = "Window color settings."; + }; + rows = mkOption { + default = null; + type = types.submodule { + options = { + normal = mkOption { + default = null; + type = types.nullOr rowColorSubmodule; + description = "Normal row color settings."; + }; + active = mkOption { + default = null; + type = types.nullOr rowColorSubmodule; + description = "Active row color settings."; + }; + urgent = mkOption { + default = null; + type = types.nullOr rowColorSubmodule; + description = "Urgent row color settings."; + }; + }; + }; + description = "Rows color settings."; + }; + }; + }; + + valueToString = value: + if isBool value then (if value then "true" else "else") else toString value; + + windowColorsToString = window: + concatStringsSep ", " (with window; [ background border separator ]); + + rowsColorsToString = rows: '' + ${optionalString (rows.normal != null) + (setOption "color-normal" (rowColorsToString rows.normal))} + ${optionalString (rows.active != null) + (setOption "color-active" (rowColorsToString rows.active))} + ${optionalString (rows.urgent != null) + (setOption "color-urgent" (rowColorsToString rows.urgent))} + ''; + + rowColorsToString = row: + concatStringsSep ", " (with row; [ + background + foreground + backgroundAlt + highlight.background + highlight.foreground + ]); + + setOption = name: value: + optionalString (value != null) "rofi.${name}: ${valueToString value}"; + + setColorScheme = colors: + optionalString (colors != null) '' + ${optionalString (colors.window != null) setOption "color-window" + (windowColorsToString colors.window)} + ${optionalString (colors.rows != null) (rowsColorsToString colors.rows)} + ''; + + locationsMap = { + center = 0; + top-left = 1; + top = 2; + top-right = 3; + right = 4; + bottom-right = 5; + bottom = 6; + bottom-left = 7; + left = 8; + }; + + themeName = if (cfg.theme == null) then + null + else if (lib.isString cfg.theme) then + cfg.theme + else + lib.removeSuffix ".rasi" (baseNameOf cfg.theme); + + themePath = if (lib.isString cfg.theme) then null else cfg.theme; + +in { + options.programs.rofi = { + 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; + description = "Window width"; + example = 100; + }; + + lines = mkOption { + default = null; + type = types.nullOr types.int; + description = "Number of lines"; + example = 10; + }; + + borderWidth = mkOption { + default = null; + type = types.nullOr types.int; + description = "Border width"; + example = 1; + }; + + rowHeight = mkOption { + default = null; + type = types.nullOr types.int; + description = "Row height (in chars)"; + example = 1; + }; + + padding = mkOption { + default = null; + type = types.nullOr types.int; + description = "Padding"; + example = 400; + }; + + font = mkOption { + default = null; + type = types.nullOr types.str; + example = "Droid Sans Mono 14"; + description = "Font to use."; + }; + + scrollbar = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Whether to show a scrollbar."; + }; + + terminal = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + Path to the terminal which will be used to run console applications + ''; + example = "\${pkgs.gnome3.gnome_terminal}/bin/gnome-terminal"; + }; + + separator = mkOption { + default = null; + type = types.nullOr (types.enum [ "none" "dash" "solid" ]); + description = "Separator style"; + example = "solid"; + }; + + cycle = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Whether to cycle through the results list."; + }; + + fullscreen = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Whether to run rofi fullscreen."; + }; + + location = mkOption { + default = "center"; + type = types.enum (builtins.attrNames locationsMap); + description = "The location rofi appears on the screen."; + }; + + xoffset = mkOption { + default = 0; + type = types.int; + description = '' + Offset in the x-axis in pixels relative to the chosen location. + ''; + }; + + yoffset = mkOption { + default = 0; + type = types.int; + description = '' + Offset in the y-axis in pixels relative to the chosen location. + ''; + }; + + colors = mkOption { + default = null; + type = types.nullOr colorsSubmodule; + description = '' + Color scheme settings. Colors can be specified in CSS color + formats. This option may become deprecated in the future and + therefore the <varname>programs.rofi.theme</varname> option + should be used whenever possible. + ''; + example = literalExample '' + colors = { + window = { + background = "argb:583a4c54"; + border = "argb:582a373e"; + separator = "#c3c6c8"; + }; + + rows = { + normal = { + background = "argb:58455a64"; + foreground = "#fafbfc"; + backgroundAlt = "argb:58455a64"; + highlight = { + background = "#00bcd4"; + foreground = "#fafbfc"; + }; + }; + }; + }; + ''; + }; + + theme = mkOption { + default = null; + type = with types; nullOr (either str path); + example = "Arc"; + description = '' + Name of theme or path to theme file in rasi format. Available + named themes can be viewed using the + <command>rofi-theme-selector</command> tool. + ''; + }; + + configPath = mkOption { + default = "${config.xdg.configHome}/rofi/config"; + defaultText = "$XDG_CONFIG_HOME/rofi/config"; + type = types.str; + description = "Path where to put generated configuration file."; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = "Additional configuration to add."; + }; + + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = cfg.theme == null || cfg.colors == null; + message = '' + Cannot use the rofi options 'theme' and 'colors' simultaneously. + ''; + }]; + + home.packages = [ cfg.package ]; + + home.file."${cfg.configPath}".text = '' + ${setOption "width" cfg.width} + ${setOption "lines" cfg.lines} + ${setOption "font" cfg.font} + ${setOption "bw" cfg.borderWidth} + ${setOption "eh" cfg.rowHeight} + ${setOption "padding" cfg.padding} + ${setOption "separator-style" cfg.separator} + ${setOption "hide-scrollbar" + (if (cfg.scrollbar != null) then (!cfg.scrollbar) else cfg.scrollbar)} + ${setOption "terminal" cfg.terminal} + ${setOption "cycle" cfg.cycle} + ${setOption "fullscreen" cfg.fullscreen} + ${setOption "location" (builtins.getAttr cfg.location locationsMap)} + ${setOption "xoffset" cfg.xoffset} + ${setOption "yoffset" cfg.yoffset} + + ${setColorScheme cfg.colors} + ${setOption "theme" themeName} + + ${cfg.extraConfig} + ''; + + xdg.dataFile = mkIf (themePath != null) { + "rofi/themes/${themeName}.rasi".source = themePath; + }; + }; +} diff --git a/home-manager/modules/programs/rtorrent.nix b/home-manager/modules/programs/rtorrent.nix new file mode 100644 index 00000000000..7beeb2e4221 --- /dev/null +++ b/home-manager/modules/programs/rtorrent.nix @@ -0,0 +1,34 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.rtorrent; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.rtorrent = { + enable = mkEnableOption "rTorrent"; + + settings = mkOption { + type = types.lines; + default = ""; + description = '' + Configuration written to + <filename>~/.config/rtorrent/rtorrent.rc</filename>. See + <link xlink:href="https://github.com/rakshasa/rtorrent/wiki/Config-Guide" /> + for explanation about possible values. + ''; + }; + + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.rtorrent ]; + + xdg.configFile."rtorrent/rtorrent.rc" = + mkIf (cfg.settings != "") { text = cfg.settings; }; + }; +} diff --git a/home-manager/modules/programs/skim.nix b/home-manager/modules/programs/skim.nix new file mode 100644 index 00000000000..c90fe1b1a35 --- /dev/null +++ b/home-manager/modules/programs/skim.nix @@ -0,0 +1,124 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.skim; + +in { + options.programs.skim = { + enable = mkEnableOption "skim - a command-line fuzzy finder"; + + defaultCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "fd --type f"; + description = '' + The command that gets executed as the default source for skim + when running. + ''; + }; + + defaultOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--height 40%" "--prompt ⟫" ]; + description = '' + Extra command line options given to skim by default. + ''; + }; + + fileWidgetCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "fd --type f"; + description = '' + The command that gets executed as the source for skim for the + CTRL-T keybinding. + ''; + }; + + fileWidgetOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--preview 'head {}'" ]; + description = '' + Command line options for the CTRL-T keybinding. + ''; + }; + + changeDirWidgetCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "fd --type d"; + description = '' + The command that gets executed as the source for skim for the + ALT-C keybinding. + ''; + }; + + changeDirWidgetOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--preview 'tree -C {} | head -200'" ]; + description = '' + Command line options for the ALT-C keybinding. + ''; + }; + + historyWidgetOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--tac" "--exact" ]; + description = '' + Command line options for the CTRL-R keybinding. + ''; + }; + + 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. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.skim ]; + + home.sessionVariables = mapAttrs (n: v: toString v) + (filterAttrs (n: v: v != [ ] && v != null) { + SKIM_ALT_C_COMMAND = cfg.changeDirWidgetCommand; + SKIM_ALT_C_OPTS = cfg.changeDirWidgetOptions; + SKIM_CTRL_R_OPTS = cfg.historyWidgetOptions; + SKIM_CTRL_T_COMMAND = cfg.fileWidgetCommand; + SKIM_CTRL_T_OPTS = cfg.fileWidgetOptions; + SKIM_DEFAULT_COMMAND = cfg.defaultCommand; + SKIM_DEFAULT_OPTIONS = cfg.defaultOptions; + }); + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then + . ${pkgs.skim}/share/skim/completion.bash + . ${pkgs.skim}/share/skim/key-bindings.bash + fi + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + if [[ $options[zle] = on ]]; then + . ${pkgs.skim}/share/skim/completion.zsh + . ${pkgs.skim}/share/skim/key-bindings.zsh + fi + ''; + }; +} diff --git a/home-manager/modules/programs/ssh.nix b/home-manager/modules/programs/ssh.nix new file mode 100644 index 00000000000..ae1f221803c --- /dev/null +++ b/home-manager/modules/programs/ssh.nix @@ -0,0 +1,492 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.ssh; + + isPath = x: builtins.substring 0 1 (toString x) == "/"; + + addressPort = entry: + if isPath entry.address + then " ${entry.address}" + else " [${entry.address}]:${toString entry.port}"; + + yn = flag: if flag then "yes" else "no"; + + unwords = builtins.concatStringsSep " "; + + bindOptions = { + address = mkOption { + type = types.str; + default = "localhost"; + example = "example.org"; + description = "The address where to bind the port."; + }; + + port = mkOption { + type = types.port; + example = 8080; + description = "Specifies port number to bind on bind address."; + }; + }; + + dynamicForwardModule = types.submodule { + options = bindOptions; + }; + + forwardModule = types.submodule { + options = { + bind = bindOptions; + + host = { + address = mkOption { + type = types.str; + example = "example.org"; + description = "The address where to forward the traffic to."; + }; + + port = mkOption { + type = types.port; + example = 80; + description = "Specifies port number to forward the traffic to."; + }; + }; + }; + }; + + matchBlockModule = types.submodule ({ dagName, ... }: { + options = { + host = mkOption { + type = types.str; + example = "*.example.org"; + description = '' + The host pattern used by this conditional block. + ''; + }; + + port = mkOption { + type = types.nullOr types.port; + default = null; + description = "Specifies port number to connect on remote host."; + }; + + forwardAgent = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Whether the connection to the authentication agent (if any) + will be forwarded to the remote machine. + ''; + }; + + forwardX11 = mkOption { + type = types.bool; + default = false; + description = '' + Specifies whether X11 connections will be automatically redirected + over the secure channel and <envar>DISPLAY</envar> set. + ''; + }; + + forwardX11Trusted = mkOption { + type = types.bool; + default = false; + description = '' + Specifies whether remote X11 clients will have full access to the + original X11 display. + ''; + }; + + identitiesOnly = mkOption { + type = types.bool; + default = false; + description = '' + Specifies that ssh should only use the authentication + identity explicitly configured in the + <filename>~/.ssh/config</filename> files or passed on the + ssh command-line, even if <command>ssh-agent</command> + offers more identities. + ''; + }; + + identityFile = mkOption { + type = with types; either (listOf str) (nullOr str); + default = []; + apply = p: + if p == null then [] + else if isString p then [p] + else p; + description = '' + Specifies files from which the user identity is read. + Identities will be tried in the given order. + ''; + }; + + user = mkOption { + type = types.nullOr types.str; + default = null; + description = "Specifies the user to log in as."; + }; + + hostname = mkOption { + type = types.nullOr types.str; + default = null; + description = "Specifies the real host name to log into."; + }; + + serverAliveInterval = mkOption { + type = types.int; + default = 0; + description = + "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 = []; + description = '' + Environment variables to send from the local host to the + server. + ''; + }; + + compression = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Specifies whether to use compression. Omitted from the host + block when <literal>null</literal>. + ''; + }; + + checkHostIP = mkOption { + type = types.bool; + default = true; + description = '' + Check the host IP address in the + <filename>known_hosts</filename> file. + ''; + }; + + proxyCommand = mkOption { + type = types.nullOr types.str; + default = null; + description = "The command to use to connect to the server."; + }; + + proxyJump = mkOption { + type = types.nullOr types.str; + default = null; + description = "The proxy host to use to connect to the server."; + }; + + certificateFile = mkOption { + type = with types; either (listOf str) (nullOr str); + default = []; + apply = p: + if p == null then [] + else if isString p then [p] + else p; + description = '' + Specifies files from which the user certificate is read. + ''; + }; + + addressFamily = mkOption { + default = null; + type = types.nullOr (types.enum ["any" "inet" "inet6"]); + description = '' + Specifies which address family to use when connecting. + ''; + }; + + localForwards = mkOption { + type = types.listOf forwardModule; + default = []; + example = literalExample '' + [ + { + bind.port = 8080; + host.address = "10.0.0.13"; + host.port = 80; + } + ]; + ''; + description = '' + Specify local port forwardings. See + <citerefentry> + <refentrytitle>ssh_config</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry> for <literal>LocalForward</literal>. + ''; + }; + + remoteForwards = mkOption { + type = types.listOf forwardModule; + default = []; + example = literalExample '' + [ + { + bind.port = 8080; + host.address = "10.0.0.13"; + host.port = 80; + } + ]; + ''; + description = '' + Specify remote port forwardings. See + <citerefentry> + <refentrytitle>ssh_config</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry> for <literal>RemoteForward</literal>. + ''; + }; + + dynamicForwards = mkOption { + type = types.listOf dynamicForwardModule; + default = []; + example = literalExample '' + [ { port = 8080; } ]; + ''; + description = '' + Specify dynamic port forwardings. See + <citerefentry> + <refentrytitle>ssh_config</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry> for <literal>DynamicForward</literal>. + ''; + }; + + extraOptions = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Extra configuration options for the host."; + }; + }; + + config.host = mkDefault dagName; + }); + + matchBlockStr = cf: concatStringsSep "\n" ( + ["Host ${cf.host}"] + ++ optional (cf.port != null) " Port ${toString cf.port}" + ++ optional (cf.forwardAgent != null) " ForwardAgent ${yn cf.forwardAgent}" + ++ optional cf.forwardX11 " ForwardX11 yes" + ++ optional cf.forwardX11Trusted " ForwardX11Trusted yes" + ++ optional cf.identitiesOnly " IdentitiesOnly yes" + ++ optional (cf.user != null) " User ${cf.user}" + ++ optional (cf.hostname != null) " HostName ${cf.hostname}" + ++ optional (cf.addressFamily != null) " AddressFamily ${cf.addressFamily}" + ++ optional (cf.sendEnv != []) " SendEnv ${unwords cf.sendEnv}" + ++ optional (cf.serverAliveInterval != 0) + " 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}" + ++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}" + ++ map (file: " IdentityFile ${file}") cf.identityFile + ++ map (file: " CertificateFile ${file}") cf.certificateFile + ++ map (f: " LocalForward" + addressPort f.bind + addressPort f.host) cf.localForwards + ++ map (f: " RemoteForward" + addressPort f.bind + addressPort f.host) cf.remoteForwards + ++ map (f: " DynamicForward" + addressPort f) cf.dynamicForwards + ++ mapAttrsToList (n: v: " ${n} ${v}") cf.extraOptions + ); + +in + +{ + meta.maintainers = [ maintainers.rycee ]; + + options.programs.ssh = { + enable = mkEnableOption "SSH client configuration"; + + forwardAgent = mkOption { + default = false; + type = types.bool; + description = '' + Whether the connection to the authentication agent (if any) + will be forwarded to the remote machine. + ''; + }; + + compression = mkOption { + default = false; + type = types.bool; + description = "Specifies whether to use compression."; + }; + + serverAliveInterval = mkOption { + type = types.int; + default = 0; + description = '' + Set default timeout in seconds after which response will be requested. + ''; + }; + + 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; + description = '' + Indicates that + <citerefentry> + <refentrytitle>ssh</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + should hash host names and addresses when they are added to + the known hosts file. + ''; + }; + + userKnownHostsFile = mkOption { + type = types.str; + default = "~/.ssh/known_hosts"; + description = '' + Specifies one or more files to use for the user host key + database, separated by whitespace. The default is + <filename>~/.ssh/known_hosts</filename>. + ''; + }; + + controlMaster = mkOption { + default = "no"; + type = types.enum ["yes" "no" "ask" "auto" "autoask"]; + description = '' + Configure sharing of multiple sessions over a single network connection. + ''; + }; + + controlPath = mkOption { + type = types.str; + default = "~/.ssh/master-%r@%n:%p"; + description = '' + Specify path to the control socket used for connection sharing. + ''; + }; + + controlPersist = mkOption { + type = types.str; + default = "no"; + example = "10m"; + description = '' + Whether control socket should remain open in the background. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration. + ''; + }; + + extraOptionOverrides = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Extra SSH configuration options that take precedence over any + host specific configuration. + ''; + }; + + matchBlocks = mkOption { + type = hm.types.listOrDagOf matchBlockModule; + default = {}; + example = literalExample '' + { + "john.example.com" = { + hostname = "example.com"; + user = "john"; + }; + foo = lib.hm.dag.entryBefore ["john.example.com"] { + hostname = "example.com"; + identityFile = "/home/john/.ssh/foo_rsa"; + }; + }; + ''; + description = '' + Specify per-host settings. Note, if the order of rules matter + 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> + for more information. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = + let + # `builtins.any`/`lib.lists.any` does not return `true` if there are no elements. + any' = pred: items: if items == [] then true else any pred items; + # Check that if `entry.address` is defined, and is a path, that `entry.port` has not + # been defined. + noPathWithPort = entry: entry ? address && isPath entry.address -> !(entry ? port); + checkDynamic = block: any' noPathWithPort block.dynamicForwards; + checkBindAndHost = fwd: noPathWithPort fwd.bind && noPathWithPort fwd.host; + checkLocal = block: any' checkBindAndHost block.localForwards; + checkRemote = block: any' checkBindAndHost block.remoteForwards; + checkMatchBlock = block: all (fn: fn block) [ checkLocal checkRemote checkDynamic ]; + in any' checkMatchBlock (map (block: block.data) (builtins.attrValues cfg.matchBlocks)); + message = "Forwarded paths cannot have ports."; + } + ]; + + 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 (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} + ControlPath ${cfg.controlPath} + ControlPersist ${cfg.controlPersist} + + ${replaceStrings ["\n"] ["\n "] cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/starship.nix b/home-manager/modules/programs/starship.nix new file mode 100644 index 00000000000..8462d331501 --- /dev/null +++ b/home-manager/modules/programs/starship.nix @@ -0,0 +1,109 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.starship; + + 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.marsam ]; + + options.programs.starship = { + enable = mkEnableOption "starship"; + + package = mkOption { + type = types.package; + default = pkgs.starship; + defaultText = literalExample "pkgs.starship"; + description = "The package to use for the starship binary."; + }; + + settings = mkOption { + 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>. + </para><para> + See <link xlink:href="https://starship.rs/config/" /> for the full list + of options. + ''; + }; + + 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 ]; + + xdg.configFile."starship.toml" = + mkIf (cfg.settings != { }) { source = configFile cfg.settings; }; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then + eval "$(${cfg.package}/bin/starship init bash)" + fi + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + if [ -z "$INSIDE_EMACS" ]; then + eval "$(${cfg.package}/bin/starship init zsh)" + fi + ''; + + 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/taskwarrior.nix b/home-manager/modules/programs/taskwarrior.nix new file mode 100644 index 00000000000..cf95511f8ef --- /dev/null +++ b/home-manager/modules/programs/taskwarrior.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.taskwarrior; + + themePath = theme: "${pkgs.taskwarrior}/share/doc/task/rc/${theme}.theme"; + + includeTheme = location: + if location == null then + "" + else if isString location then + "include ${themePath location}" + else + "include ${location}"; + + formatValue = value: + if isBool value then + if value then "true" else "false" + else if isList value then + concatMapStringsSep "," formatValue value + else + toString value; + + formatLine = key: value: "${key}=${formatValue value}"; + + formatSet = key: values: + (concatStringsSep "\n" + (mapAttrsToList (subKey: subValue: formatPair "${key}.${subKey}" subValue) + values)); + + formatPair = key: value: + if isAttrs value then formatSet key value else formatLine key value; + +in { + options = { + programs.taskwarrior = { + enable = mkEnableOption "Task Warrior"; + + config = mkOption { + type = types.attrs; + default = { }; + example = literalExample '' + { + confirmation = false; + report.minimal.filter = "status:pending"; + report.active.columns = [ "id" "start" "entry.age" "priority" "project" "due" "description" ]; + report.active.labels = [ "ID" "Started" "Age" "Priority" "Project" "Due" "Description" ]; + taskd = { + certificate = "/path/to/cert"; + key = "/path/to/key"; + ca = "/path/to/ca"; + server = "host.domain:53589"; + credentials = "Org/First Last/cf31f287-ee9e-43a8-843e-e8bbd5de4294"; + }; + } + ''; + description = '' + Key-value configuration written to + <filename>~/.taskrc</filename>. + ''; + }; + + dataLocation = mkOption { + type = types.str; + default = "${config.xdg.dataHome}/task"; + defaultText = "$XDG_DATA_HOME/task"; + description = '' + Location where Task Warrior will store its data. + </para><para> + Home Manager will attempt to create this directory. + ''; + }; + + colorTheme = mkOption { + type = with types; nullOr (either str path); + default = null; + example = "dark-blue-256"; + description = '' + Either one of the default provided theme as string, or a + path to a theme configuration file. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional content written at the end of + <filename>~/.taskrc</filename>. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.taskwarrior ]; + + home.file."${cfg.dataLocation}/.keep".text = ""; + + home.file.".taskrc".text = '' + data.location=${cfg.dataLocation} + ${includeTheme cfg.colorTheme} + + ${concatStringsSep "\n" (mapAttrsToList formatPair cfg.config)} + + ${cfg.extraConfig} + ''; + }; +} diff --git a/home-manager/modules/programs/termite.nix b/home-manager/modules/programs/termite.nix new file mode 100644 index 00000000000..e3d704424e8 --- /dev/null +++ b/home-manager/modules/programs/termite.nix @@ -0,0 +1,387 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.termite; + + vteInitStr = '' + # See https://github.com/thestinger/termite#id1 + if [[ $TERM == xterm-termite ]]; then + . ${pkgs.termite.vte-ng}/etc/profile.d/vte.sh + fi + ''; + +in { + options = { + programs.termite = { + enable = mkEnableOption "Termite VTE-based terminal"; + + allowBold = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Allow the output of bold characters when the bold escape sequence appears. + ''; + }; + + audibleBell = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Have the terminal beep on the terminal bell."; + }; + + clickableUrl = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Auto-detected URLs can be clicked on to open them in your browser. + Only enabled if a browser is configured or detected. + ''; + }; + + dynamicTitle = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Settings dynamic title allows the terminal and the shell to + update the terminal's title. + ''; + }; + + fullscreen = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Enables entering fullscreen mode by pressing F11."; + }; + + mouseAutohide = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Automatically hide the mouse pointer when you start typing. + ''; + }; + + scrollOnOutput = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Scroll to the bottom when the shell generates output."; + }; + + scrollOnKeystroke = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Scroll to the bottom automatically when a key is pressed. + ''; + }; + + searchWrap = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Search from top again when you hit the bottom."; + }; + + urgentOnBell = mkOption { + default = null; + type = types.nullOr types.bool; + description = "Sets the window as urgent on the terminal bell."; + }; + + font = mkOption { + default = null; + example = "Monospace 12"; + type = types.nullOr types.str; + description = "The font description for the terminal's font."; + }; + + geometry = mkOption { + default = null; + example = "640x480"; + type = types.nullOr types.str; + description = "The default window geometry for new terminal windows."; + }; + + iconName = mkOption { + default = null; + example = "terminal"; + type = types.nullOr types.str; + description = + "The name of the icon to be used for the terminal process."; + }; + + scrollbackLines = mkOption { + default = null; + example = 10000; + type = types.nullOr types.int; + description = + "Set the number of lines to limit the terminal's scrollback."; + }; + + browser = mkOption { + default = null; + type = types.nullOr types.str; + example = "${pkgs.xdg_utils}/xdg-open"; + description = '' + Set the default browser for opening links. If its not set, $BROWSER is read. + If that's not set, url hints will be disabled. + ''; + }; + + cursorBlink = mkOption { + default = null; + example = "system"; + type = types.nullOr (types.enum [ "system" "on" "off" ]); + description = '' + Specify the how the terminal's cursor should behave. + Accepts system to respect the gtk global configuration, + on and off to explicitly enable or disable them. + ''; + }; + + cursorShape = mkOption { + default = null; + example = "block"; + type = types.nullOr (types.enum [ "block" "underline" "ibeam" ]); + description = '' + Specify how the cursor should look. Accepts block, ibeam and underline. + ''; + }; + + filterUnmatchedUrls = mkOption { + default = null; + type = types.nullOr types.bool; + description = + "Whether to hide url hints not matching input in url hints mode."; + }; + + modifyOtherKeys = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Emit escape sequences for extra keys, + like the modifyOtherKeys resource for + <citerefentry> + <refentrytitle>xterm</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry>. + ''; + }; + + sizeHints = mkOption { + default = null; + type = types.nullOr types.bool; + description = '' + Enable size hints. Locks the terminal resizing + to increments of the terminal's cell size. + Requires a window manager that respects scroll hints. + ''; + }; + + scrollbar = mkOption { + default = null; + type = types.nullOr (types.enum [ "off" "left" "right" ]); + description = "Scrollbar position."; + }; + + backgroundColor = mkOption { + default = null; + example = "rgba(63, 63, 63, 0.8)"; + type = types.nullOr types.str; + description = "Background color value."; + }; + + cursorColor = mkOption { + default = null; + example = "#dcdccc"; + type = types.nullOr types.str; + description = "Cursor color value."; + }; + + cursorForegroundColor = mkOption { + default = null; + example = "#dcdccc"; + type = types.nullOr types.str; + description = "Cursor foreground color value."; + }; + + foregroundColor = mkOption { + default = null; + example = "#dcdccc"; + type = types.nullOr types.str; + description = "Foreground color value."; + }; + + foregroundBoldColor = mkOption { + default = null; + example = "#ffffff"; + type = types.nullOr types.str; + description = "Foreground bold color value."; + }; + + highlightColor = mkOption { + default = null; + example = "#2f2f2f"; + type = types.nullOr types.str; + description = "highlight color value."; + }; + + hintsActiveBackgroundColor = mkOption { + default = null; + example = "#3f3f3f"; + type = types.nullOr types.str; + description = "Hints active background color value."; + }; + + hintsActiveForegroundColor = mkOption { + default = null; + example = "#e68080"; + type = types.nullOr types.str; + description = "Hints active foreground color value."; + }; + + hintsBackgroundColor = mkOption { + default = null; + example = "#3f3f3f"; + type = types.nullOr types.str; + description = "Hints background color value."; + }; + + hintsForegroundColor = mkOption { + default = null; + example = "#dcdccc"; + type = types.nullOr types.str; + description = "Hints foreground color value."; + }; + + hintsBorderColor = mkOption { + default = null; + example = "#3f3f3f"; + type = types.nullOr types.str; + description = "Hints border color value."; + }; + + hintsBorderWidth = mkOption { + default = null; + example = "0.5"; + type = types.nullOr types.str; + description = "Hints border width."; + }; + + hintsFont = mkOption { + default = null; + example = "Monospace 12"; + type = types.nullOr types.str; + description = "The font description for the hints font."; + }; + + hintsPadding = mkOption { + default = null; + example = 2; + type = types.nullOr types.int; + description = "Hints padding."; + }; + + hintsRoundness = mkOption { + default = null; + example = "0.2"; + type = types.nullOr types.str; + description = "Hints roundness."; + }; + + optionsExtra = mkOption { + default = ""; + example = "fullscreen = true"; + type = types.lines; + description = + "Extra options that should be added to [options] section."; + }; + + colorsExtra = mkOption { + default = ""; + example = '' + color0 = #3f3f3f + color1 = #705050 + color2 = #60b48a + ''; + type = types.lines; + description = + "Extra colors options that should be added to [colors] section."; + }; + + hintsExtra = mkOption { + default = ""; + example = "border = #3f3f3f"; + type = types.lines; + description = + "Extra hints options that should be added to [hints] section."; + }; + }; + }; + + 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.termite ]; + xdg.configFile."termite/config".text = '' + [options] + ${optionalBoolean "allow_bold" cfg.allowBold} + ${optionalBoolean "audible_bell" cfg.audibleBell} + ${optionalString "browser" cfg.browser} + ${optionalBoolean "clickable_url" cfg.clickableUrl} + ${optionalString "cursor_blink" cfg.cursorBlink} + ${optionalString "cursor_shape" cfg.cursorShape} + ${optionalBoolean "dynamic_title" cfg.dynamicTitle} + ${optionalBoolean "filter_unmatched_urls" cfg.filterUnmatchedUrls} + ${optionalString "font" cfg.font} + ${optionalBoolean "fullscreen" cfg.fullscreen} + ${optionalString "geometry" cfg.geometry} + ${optionalString "icon_name" cfg.iconName} + ${optionalBoolean "modify_other_keys" cfg.modifyOtherKeys} + ${optionalBoolean "mouse_autohide" cfg.mouseAutohide} + ${optionalBoolean "scroll_on_keystroke" cfg.scrollOnKeystroke} + ${optionalBoolean "scroll_on_output" cfg.scrollOnOutput} + ${optionalInteger "scrollback_lines" cfg.scrollbackLines} + ${optionalString "scrollbar" cfg.scrollbar} + ${optionalBoolean "search_wrap" cfg.searchWrap} + ${optionalBoolean "size_hints" cfg.sizeHints} + ${optionalBoolean "urgent_on_bell" cfg.urgentOnBell} + + ${cfg.optionsExtra} + + [colors] + ${optionalString "background" cfg.backgroundColor} + ${optionalString "cursor" cfg.cursorColor} + ${optionalString "cursor_foreground" cfg.cursorForegroundColor} + ${optionalString "foreground" cfg.foregroundColor} + ${optionalString "foreground_bold" cfg.foregroundBoldColor} + ${optionalString "highlight" cfg.highlightColor} + + ${cfg.colorsExtra} + + [hints] + ${optionalString "active_background" cfg.hintsActiveBackgroundColor} + ${optionalString "active_foreground" cfg.hintsActiveForegroundColor} + ${optionalString "background" cfg.hintsBackgroundColor} + ${optionalString "border" cfg.hintsBorderColor} + ${optionalInteger "border_width" cfg.hintsBorderWidth} + ${optionalString "font" cfg.hintsFont} + ${optionalString "foreground" cfg.hintsForegroundColor} + ${optionalInteger "padding" cfg.hintsPadding} + ${optionalInteger "roundness" cfg.hintsRoundness} + + ${cfg.hintsExtra} + ''; + + programs.bash.initExtra = vteInitStr; + programs.zsh.initExtra = vteInitStr; + }); +} diff --git a/home-manager/modules/programs/texlive.nix b/home-manager/modules/programs/texlive.nix new file mode 100644 index 00000000000..08a376d654a --- /dev/null +++ b/home-manager/modules/programs/texlive.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.texlive; + + texlivePkgs = cfg.extraPackages pkgs.texlive; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + programs.texlive = { + enable = mkEnableOption "Texlive"; + + extraPackages = mkOption { + default = tpkgs: { inherit (tpkgs) collection-basic; }; + defaultText = "tpkgs: { inherit (tpkgs) collection-basic; }"; + example = literalExample '' + tpkgs: { inherit (tpkgs) collection-fontsrecommended algorithms; } + ''; + description = "Extra packages available to Texlive."; + }; + + package = mkOption { + type = types.package; + description = "Resulting customized Texlive package."; + readOnly = true; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = texlivePkgs != { }; + message = "Must provide at least one extra package in" + + " 'programs.texlive.extraPackages'."; + }]; + + home.packages = [ cfg.package ]; + + programs.texlive.package = pkgs.texlive.combine texlivePkgs; + }; +} diff --git a/home-manager/modules/programs/tmux.nix b/home-manager/modules/programs/tmux.nix new file mode 100644 index 00000000000..a71c302ac6f --- /dev/null +++ b/home-manager/modules/programs/tmux.nix @@ -0,0 +1,316 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.tmux; + + pluginName = p: if types.package.check p then p.pname else p.plugin.pname; + + pluginModule = types.submodule { + options = { + plugin = mkOption { + type = types.package; + description = "Path of the configuration file to include."; + }; + + extraConfig = mkOption { + type = types.lines; + description = "Additional configuration for the associated plugin."; + default = ""; + }; + }; + }; + + defaultKeyMode = "emacs"; + defaultResize = 5; + defaultShortcut = "b"; + defaultTerminal = "screen"; + + 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} + + ${optionalString cfg.newSession "new-session"} + + ${optionalString cfg.reverseSplit '' + bind v split-window -h + bind s split-window -v + ''} + + set -g status-keys ${cfg.keyMode} + set -g mode-keys ${cfg.keyMode} + + ${optionalString (cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize) '' + bind h select-pane -L + bind j select-pane -D + bind k select-pane -U + bind l select-pane -R + + bind -r H resize-pane -L ${toString cfg.resizeAmount} + bind -r J resize-pane -D ${toString cfg.resizeAmount} + bind -r K resize-pane -U ${toString cfg.resizeAmount} + bind -r L resize-pane -R ${toString cfg.resizeAmount} + ''} + + ${optionalString (cfg.shortcut != defaultShortcut) '' + # rebind main key: C-${cfg.shortcut} + unbind C-${defaultShortcut} + set -g prefix C-${cfg.shortcut} + bind ${cfg.shortcut} send-prefix + bind C-${cfg.shortcut} last-window + ''} + + ${optionalString cfg.disableConfirmationPrompt '' + bind-key & kill-window + bind-key x kill-pane + ''} + + setw -g aggressive-resize ${boolToStr cfg.aggressiveResize} + 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} + ''; + + 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 + +{ + options = { + programs.tmux = { + aggressiveResize = mkOption { + default = false; + type = types.bool; + description = '' + Resize the window to the size of the smallest session for + which it is the current window. + ''; + }; + + baseIndex = mkOption { + default = 0; + example = 1; + type = types.ints.unsigned; + description = "Base index for windows and panes."; + }; + + clock24 = mkOption { + default = false; + type = types.bool; + description = "Use 24 hour clock."; + }; + + customPaneNavigationAndResize = mkOption { + default = false; + type = types.bool; + description = '' + Override the hjkl and HJKL bindings for pane navigation and + resizing in VI mode. + ''; + }; + + disableConfirmationPrompt = mkOption { + default = false; + type = types.bool; + description = '' + Disable confirmation prompt before killing a pane or window + ''; + }; + + enable = mkEnableOption "tmux"; + + escapeTime = mkOption { + default = 500; + example = 0; + type = types.ints.unsigned; + description = '' + Time in milliseconds for which tmux waits after an escape is + input. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional configuration to add to + <filename>tmux.conf</filename>. + ''; + }; + + historyLimit = mkOption { + default = 2000; + example = 5000; + type = types.ints.positive; + description = "Maximum number of lines held in window history."; + }; + + keyMode = mkOption { + default = defaultKeyMode; + example = "vi"; + type = types.enum [ "emacs" "vi" ]; + description = "VI or Emacs style shortcuts."; + }; + + newSession = mkOption { + default = false; + type = types.bool; + description = '' + Automatically spawn a session if trying to attach and none + are running. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.tmux; + defaultText = literalExample "pkgs.tmux"; + example = literalExample "pkgs.tmux"; + description = "The tmux package to install"; + }; + + reverseSplit = mkOption { + default = false; + type = types.bool; + description = "Reverse the window split shortcuts."; + }; + + resizeAmount = mkOption { + default = defaultResize; + example = 10; + type = types.ints.positive; + description = "Number of lines/columns when resizing."; + }; + + sensibleOnTop = mkOption { + type = types.bool; + default = true; + description = '' + Run the sensible plugin at the top of the configuration. It + is possible to override the sensible settings using the + <option>programs.tmux.extraConfig</option> option. + ''; + }; + + shortcut = mkOption { + default = defaultShortcut; + example = "a"; + type = types.str; + description = '' + CTRL following by this key is used as the main shortcut. + ''; + }; + + terminal = mkOption { + default = defaultTerminal; + example = "screen-256color"; + type = types.str; + description = "Set the $TERM variable."; + }; + + secureSocket = mkOption { + default = pkgs.stdenv.isLinux; + type = types.bool; + description = '' + Store tmux socket under <filename>/run</filename>, which is more + secure than <filename>/tmp</filename>, but as a downside it doesn't + survive user logout. + ''; + }; + + tmuxp.enable = mkEnableOption "tmuxp"; + + tmuxinator.enable = mkEnableOption "tmuxinator"; + + plugins = mkOption { + type = with types; + listOf (either package pluginModule) + // { description = "list of plugin packages or submodules"; }; + description = '' + List of tmux plugins to be included at the end of your tmux + configuration. The sensible plugin, however, is defaulted to + run at the top of your configuration. + ''; + default = [ ]; + example = literalExample '' + with pkgs; [ + tmuxPlugins.cpu + { + plugin = tmuxPlugins.resurrect; + extraConfig = "set -g @resurrect-strategy-nvim 'session'"; + } + { + plugin = tmuxPlugins.continuum; + extraConfig = ''' + set -g @continuum-restore 'on' + set -g @continuum-save-interval '60' # minutes + '''; + } + ] + ''; + }; + }; + }; + + config = mkIf cfg.enable ( + mkMerge ([ + { + home.packages = [ cfg.package ] + ++ optional cfg.tmuxinator.enable pkgs.tmuxinator + ++ optional cfg.tmuxp.enable pkgs.tmuxp; + } + (mkIf cfg.secureSocket { + home.sessionVariables = { + TMUX_TMPDIR = ''''${XDG_RUNTIME_DIR:-"/run/user/\$(id -u)"}''; + }; + }) + + # 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/urxvt.nix b/home-manager/modules/programs/urxvt.nix new file mode 100644 index 00000000000..e4c72bfe272 --- /dev/null +++ b/home-manager/modules/programs/urxvt.nix @@ -0,0 +1,156 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.urxvt; + +in { + options.programs.urxvt = { + enable = mkEnableOption "rxvt-unicode terminal emulator"; + + package = mkOption { + type = types.package; + default = pkgs.rxvt_unicode; + defaultText = literalExample "pkgs.rxvt_unicode"; + description = "rxvt-unicode package to install."; + }; + + fonts = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of fonts to be used."; + example = [ "xft:Droid Sans Mono Nerd Font:size=9" ]; + }; + + keybindings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Mapping of keybindings to actions"; + example = literalExample '' + { + "Shift-Control-C" = "eval:selection_to_clipboard"; + "Shift-Control-V" = "eval:paste_clipboard"; + } + ''; + }; + + iso14755 = mkOption { + type = types.bool; + default = true; + description = + "ISO14755 support for viewing and entering unicode characters."; + }; + + scroll = { + bar = mkOption { + type = types.submodule { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to enable the scrollbar"; + }; + + style = mkOption { + type = types.enum [ "rxvt" "plain" "next" "xterm" ]; + default = "plain"; + description = "Scrollbar style."; + }; + + align = mkOption { + type = types.enum [ "top" "bottom" "center" ]; + default = "center"; + description = "Scrollbar alignment."; + }; + + position = mkOption { + type = types.enum [ "left" "right" ]; + default = "right"; + description = "Scrollbar position."; + }; + + floating = mkOption { + type = types.bool; + default = true; + description = + "Whether to display an rxvt scrollbar without a trough."; + }; + }; + }; + default = { }; + description = "Scrollbar settings."; + }; + + lines = mkOption { + type = types.ints.unsigned; + default = 10000; + description = "Number of lines to save in the scrollback buffer."; + }; + + keepPosition = mkOption { + type = types.bool; + default = true; + description = + "Whether to keep a scroll position when TTY receives new lines."; + }; + + scrollOnKeystroke = mkOption { + type = types.bool; + default = true; + description = "Whether to scroll to bottom on keyboard input."; + }; + + scrollOnOutput = mkOption { + type = types.bool; + default = false; + description = "Whether to scroll to bottom on TTY output."; + }; + }; + + transparent = mkOption { + type = types.bool; + default = false; + description = "Whether to enable pseudo-transparency."; + }; + + shading = mkOption { + type = types.ints.between 0 200; + default = 100; + description = + "Darken (0 .. 99) or lighten (101 .. 200) the transparent background."; + }; + + extraConfig = mkOption { + default = { }; + type = types.attrs; + description = "Additional configuration to add."; + example = { "shading" = 15; }; + }; + + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xresources.properties = { + "URxvt.scrollBar" = cfg.scroll.bar.enable; + "URxvt.scrollstyle" = cfg.scroll.bar.style; + "URxvt.scrollBar_align" = cfg.scroll.bar.align; + "URxvt.scrollBar_right" = cfg.scroll.bar.position == "right"; + "URxvt.scrollBar_floating" = cfg.scroll.bar.floating; + "URxvt.saveLines" = cfg.scroll.lines; + "URxvt.scrollWithBuffer" = cfg.scroll.keepPosition; + "URxvt.scrollTtyKeypress" = cfg.scroll.scrollOnKeystroke; + "URxvt.scrollTtyOutput" = cfg.scroll.scrollOnOutput; + "URxvt.transparent" = cfg.transparent; + "URxvt.shading" = cfg.shading; + "URxvt.iso14755" = cfg.iso14755; + } // flip mapAttrs' cfg.keybindings + (kb: action: nameValuePair "URxvt.keysym.${kb}" action) + // optionalAttrs (cfg.fonts != [ ]) { + "URxvt.font" = concatStringsSep "," cfg.fonts; + } // flip mapAttrs' cfg.extraConfig (k: v: nameValuePair "URxvt.${k}" v); + }; +} diff --git a/home-manager/modules/programs/vim.nix b/home-manager/modules/programs/vim.nix new file mode 100644 index 00000000000..3325bf22516 --- /dev/null +++ b/home-manager/modules/programs/vim.nix @@ -0,0 +1,171 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.vim; + defaultPlugins = [ pkgs.vimPlugins.vim-sensible ]; + + knownSettings = { + background = types.enum [ "dark" "light" ]; + backupdir = types.listOf types.str; + copyindent = types.bool; + directory = types.listOf types.str; + expandtab = types.bool; + hidden = types.bool; + history = types.int; + ignorecase = types.bool; + modeline = types.bool; + mouse = types.enum [ "n" "v" "i" "c" "h" "a" "r" ]; + mousefocus = types.bool; + mousehide = types.bool; + mousemodel = types.enum [ "extend" "popup" "popup_setpos" ]; + number = types.bool; + relativenumber = types.bool; + shiftwidth = types.int; + smartcase = types.bool; + tabstop = types.int; + undodir = types.listOf types.str; + undofile = types.bool; + }; + + vimSettingsType = types.submodule { + options = let + opt = name: type: + mkOption { + type = types.nullOr type; + default = null; + visible = false; + }; + in mapAttrs opt knownSettings; + }; + + setExpr = name: value: + let + v = if isBool value then + (if value then "" else "no") + name + else + "${name}=${ + if isList value then concatStringsSep "," value else toString value + }"; + in optionalString (value != null) ("set " + v); + + plugins = let + vpkgs = pkgs.vimPlugins; + getPkg = p: + if isDerivation p then + [ p ] + else + optional (isString p && hasAttr p vpkgs) vpkgs.${p}; + in concatMap getPkg cfg.plugins; + +in { + options = { + programs.vim = { + enable = mkEnableOption "Vim"; + + plugins = mkOption { + type = with types; listOf (either str package); + default = defaultPlugins; + example = literalExample "[ pkgs.vimPlugins.YankRing ]"; + description = '' + List of vim plugins to install. To get a list of supported plugins run: + <command>nix-env -f '<nixpkgs>' -qaP -A vimPlugins</command>. + + </para><para> + + Note: String values are deprecated, please use actual packages. + ''; + }; + + settings = mkOption { + type = vimSettingsType; + default = { }; + example = literalExample '' + { + expandtab = true; + history = 1000; + background = "dark"; + } + ''; + description = '' + At attribute set of Vim 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 Vim documentation for detailed descriptions of these + options. Note, use <varname>extraConfig</varname> to + manually set any options not listed above. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + set nocompatible + set nobackup + ''; + description = "Custom .vimrc lines"; + }; + + package = mkOption { + type = types.package; + description = "Resulting customized vim package"; + readOnly = true; + }; + }; + }; + + config = (let + customRC = '' + ${concatStringsSep "\n" (filter (v: v != "") (mapAttrsToList setExpr + (builtins.intersectAttrs knownSettings cfg.settings)))} + + ${cfg.extraConfig} + ''; + + vim = pkgs.vim_configurable.customize { + name = "vim"; + vimrcConfig = { + inherit customRC; + + packages.home-manager.start = plugins; + }; + }; + in mkIf cfg.enable { + assertions = let + packagesNotFound = + filter (p: isString p && (!hasAttr p pkgs.vimPlugins)) cfg.plugins; + in [{ + assertion = packagesNotFound == [ ]; + message = "Following VIM plugin not found in pkgs.vimPlugins: ${ + concatMapStringsSep ", " (p: ''"${p}"'') packagesNotFound + }"; + }]; + + warnings = let stringPlugins = filter isString cfg.plugins; + in optional (stringPlugins != [ ]) '' + Specifying VIM plugins using strings is deprecated, found ${ + concatMapStringsSep ", " (p: ''"${p}"'') stringPlugins + } as strings. + ''; + + home.packages = [ cfg.package ]; + + programs.vim = { + package = vim; + plugins = defaultPlugins; + }; + }); +} diff --git a/home-manager/modules/programs/vscode.nix b/home-manager/modules/programs/vscode.nix new file mode 100644 index 00000000000..099760c834a --- /dev/null +++ b/home-manager/modules/programs/vscode.nix @@ -0,0 +1,145 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.vscode; + + vscodePname = cfg.package.pname; + + configDir = { + "vscode" = "Code"; + "vscode-insiders" = "Code - Insiders"; + "vscodium" = "VSCodium"; + }.${vscodePname}; + + extensionDir = { + "vscode" = "vscode"; + "vscode-insiders" = "vscode-insiders"; + "vscodium" = "vscode-oss"; + }.${vscodePname}; + + userDir = + if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/${configDir}/User" + else + "${config.xdg.configHome}/${configDir}/User"; + + configFilePath = "${userDir}/settings.json"; + keybindingsFilePath = "${userDir}/keybindings.json"; + + # TODO: On Darwin where are the extensions? + extensionPath = ".${extensionDir}/extensions"; +in + +{ + options = { + programs.vscode = { + enable = mkEnableOption "Visual Studio Code"; + + package = mkOption { + type = types.package; + default = pkgs.vscode; + example = literalExample "pkgs.vscodium"; + description = '' + Version of Visual Studio Code to install. + ''; + }; + + userSettings = mkOption { + type = types.attrs; + default = {}; + example = literalExample '' + { + "update.channel" = "none"; + "[nix]"."editor.tabSize" = 2; + } + ''; + description = '' + Configuration written to Visual Studio Code's + <filename>settings.json</filename>. + ''; + }; + + 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 = []; + example = literalExample "[ pkgs.vscode-extensions.bbenoist.Nix ]"; + description = '' + The extensions Visual Studio Code should be started with. + These will override but not delete manually installed ones. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + # Adapted from https://discourse.nixos.org/t/vscode-extensions-setup/1801/2 + home.file = + let + subDir = "share/vscode/extensions"; + toPaths = path: + # 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 + (a: b: a // b) + { + "${configFilePath}" = + 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/vscode/haskell.nix b/home-manager/modules/programs/vscode/haskell.nix new file mode 100644 index 00000000000..ee84e707102 --- /dev/null +++ b/home-manager/modules/programs/vscode/haskell.nix @@ -0,0 +1,62 @@ +{ pkgs, config, lib, ... }: + +with lib; + +let + + cfg = config.programs.vscode.haskell; + + defaultHieNixExe = hie-nix.hies + "/bin/hie-wrapper"; + defaultHieNixExeText = + literalExample ''"''${pkgs.hie-nix.hies}/bin/hie-wrapper"''; + + hie-nix = pkgs.hie-nix or (abort '' + vscode.haskell: pkgs.hie-nix missing. Please add an overlay such as: + ${exampleOverlay} + ''); + + exampleOverlay = '' + nixpkgs.overlays = [ + (self: super: { hie-nix = import ~/src/hie-nix {}; }) + ] + ''; + +in { + options.programs.vscode.haskell = { + enable = mkEnableOption "Haskell integration for Visual Studio Code"; + + hie.enable = mkOption { + type = types.bool; + default = true; + description = "Whether to enable Haskell IDE engine integration."; + }; + + hie.executablePath = mkOption { + type = types.path; + default = defaultHieNixExe; + defaultText = defaultHieNixExeText; + description = '' + The path to the Haskell IDE Engine executable. + </para><para> + Because hie-nix is not packaged in Nixpkgs, you need to add it as an + overlay or set this option. Example overlay configuration: + <programlisting language="nix">${exampleOverlay}</programlisting> + ''; + example = literalExample '' + (import ~/src/haskell-ide-engine {}).hies + "/bin/hie-wrapper"; + ''; + }; + }; + + config = mkIf cfg.enable { + programs.vscode.userSettings = mkIf cfg.hie.enable { + "languageServerHaskell.enableHIE" = true; + "languageServerHaskell.hieExecutablePath" = cfg.hie.executablePath; + }; + + programs.vscode.extensions = + [ pkgs.vscode-extensions.justusadam.language-haskell ] + ++ lib.optional cfg.hie.enable + pkgs.vscode-extensions.alanz.vscode-hie-server; + }; +} 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/z-lua.nix b/home-manager/modules/programs/z-lua.nix new file mode 100644 index 00000000000..d722ac6a2f0 --- /dev/null +++ b/home-manager/modules/programs/z-lua.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.z-lua; + + aliases = { + zz = "z -c"; # restrict matches to subdirs of $PWD + zi = "z -i"; # cd with interactive selection + zf = "z -I"; # use fzf to select in multiple matches + zb = "z -b"; # quickly cd to the parent directory + zh = "z -I -t ."; # fzf + }; + +in { + meta.maintainers = [ maintainers.marsam ]; + + options.programs.z-lua = { + enable = mkEnableOption "z.lua"; + + options = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "enhanced" "once" "fzf" ]; + description = '' + List of options to pass to z.lua. + ''; + }; + + 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. + ''; + }; + + enableAliases = mkOption { + default = false; + type = types.bool; + description = '' + Whether to enable recommended z.lua aliases. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.z-lua ]; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + eval "$(${pkgs.z-lua}/bin/z --init bash ${ + concatStringsSep " " cfg.options + })" + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + eval "$(${pkgs.z-lua}/bin/z --init zsh ${ + concatStringsSep " " cfg.options + })" + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + source (${pkgs.z-lua}/bin/z --init fish ${ + concatStringsSep " " cfg.options + } | psub) + ''; + + programs.bash.shellAliases = mkIf cfg.enableAliases aliases; + + programs.zsh.shellAliases = mkIf cfg.enableAliases aliases; + }; +} diff --git a/home-manager/modules/programs/zathura.nix b/home-manager/modules/programs/zathura.nix new file mode 100644 index 00000000000..d9f3c1af1fd --- /dev/null +++ b/home-manager/modules/programs/zathura.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.zathura; + + formatLine = n: v: + let + formatValue = v: + if isBool v then (if v then "true" else "false") else toString v; + in ''set ${n} "${formatValue v}"''; + +in { + meta.maintainers = [ maintainers.rprospero ]; + + options.programs.zathura = { + enable = mkEnableOption '' + Zathura, a highly customizable and functional document viewer + focused on keyboard interaction''; + + options = mkOption { + default = { }; + type = with types; attrsOf (either str (either bool int)); + description = '' + Add <option>:set</option> command options to zathura and make + them permanent. See + <citerefentry> + <refentrytitle>zathurarc</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry> + for the full list of options. + ''; + example = { + default-bg = "#000000"; + default-fg = "#FFFFFF"; + }; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional commands for zathura that will be added to the + <filename>zathurarc</filename> file. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.zathura ]; + + xdg.configFile."zathura/zathurarc".text = concatStringsSep "\n" ([ ] + ++ optional (cfg.extraConfig != "") cfg.extraConfig + ++ mapAttrsToList formatLine cfg.options) + "\n"; + }; +} 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 new file mode 100644 index 00000000000..ed65d5fe487 --- /dev/null +++ b/home-manager/modules/programs/zsh.nix @@ -0,0 +1,508 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.zsh; + + relToDotDir = file: (optionalString (cfg.dotDir != null) (cfg.dotDir + "/")) + file; + + pluginsDir = if cfg.dotDir != null then + relToDotDir "plugins" else ".zsh/plugins"; + + envVarsStr = config.lib.zsh.exportAll cfg.sessionVariables; + localVarsStr = config.lib.zsh.defineAll cfg.localVariables; + + aliasesStr = concatStringsSep "\n" ( + 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 = { + emacs = "bindkey -e"; + viins = "bindkey -v"; + vicmd = "bindkey -a"; + }; + + stateVersion = config.home.stateVersion; + + historyModule = types.submodule ({ config, ... }: { + options = { + size = mkOption { + type = types.int; + default = 10000; + description = "Number of history lines to keep."; + }; + + save = mkOption { + type = types.int; + defaultText = 10000; + default = config.size; + description = "Number of history lines to save."; + }; + + path = mkOption { + type = types.str; + default = if versionAtLeast stateVersion "20.03" + then "$HOME/.zsh_history" + else relToDotDir ".zsh_history"; + example = literalExample ''"''${config.xdg.dataHome}/zsh/zsh_history"''; + description = "History file location"; + }; + + ignoreDups = mkOption { + type = types.bool; + default = true; + description = '' + Do not enter command lines into the history list + if they are duplicates of the previous event. + ''; + }; + + ignoreSpace = mkOption { + type = types.bool; + default = true; + description = '' + Do not enter command lines into the history list + if the first character is a space. + ''; + }; + + expireDuplicatesFirst = mkOption { + type = types.bool; + default = false; + description = "Expire duplicates first."; + }; + + extended = mkOption { + type = types.bool; + default = false; + description = "Save timestamp into the history file."; + }; + + share = mkOption { + type = types.bool; + default = true; + description = "Share command history between zsh sessions."; + }; + }; + }); + + pluginModule = types.submodule ({ config, ... }: { + options = { + src = mkOption { + type = types.path; + description = '' + Path to the plugin folder. + + Will be added to <envar>fpath</envar> and <envar>PATH</envar>. + ''; + }; + + name = mkOption { + type = types.str; + description = '' + The name of the plugin. + + Don't forget to add <option>file</option> + if the script name does not follow convention. + ''; + }; + + file = mkOption { + type = types.str; + description = "The plugin script to source."; + }; + }; + + config.file = mkDefault "${config.name}.plugin.zsh"; + }); + + ohMyZshModule = types.submodule { + options = { + enable = mkEnableOption "oh-my-zsh"; + + plugins = mkOption { + default = []; + example = [ "git" "sudo" ]; + type = types.listOf types.str; + description = '' + List of oh-my-zsh plugins + ''; + }; + + custom = mkOption { + default = ""; + type = types.str; + example = "$HOME/my_customizations"; + description = '' + Path to a custom oh-my-zsh package to override config of + oh-my-zsh. See <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/wiki/Customization"/> + for more information. + ''; + }; + + theme = mkOption { + default = ""; + example = "robbyrussell"; + type = types.str; + description = '' + 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. + ''; + }; + }; + }; + +in + +{ + options = { + programs.zsh = { + enable = mkEnableOption "Z shell (Zsh)"; + + autocd = mkOption { + default = null; + description = '' + Automatically enter into a directory if typed directly into shell. + ''; + 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"; + description = '' + Directory where the zsh configuration and more should be located, + relative to the users home directory. The default is the home + directory. + ''; + type = types.nullOr types.str; + }; + + shellAliases = mkOption { + default = {}; + 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. + ''; + 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 = '' + Enable zsh completion. Don't forget to add + <programlisting language="nix"> + environment.pathsToLink = [ "/share/zsh" ]; + </programlisting> + to your system configuration to get completion for system packages (e.g. systemd). + ''; + type = types.bool; + }; + + enableAutosuggestions = mkOption { + default = false; + description = "Enable zsh autosuggestions"; + }; + + history = mkOption { + type = historyModule; + default = {}; + description = "Options related to commands history configuration."; + }; + + defaultKeymap = mkOption { + type = types.nullOr (types.enum (attrNames bindkeyCommands)); + default = null; + example = "emacs"; + description = "The default base keymap to use."; + }; + + sessionVariables = mkOption { + default = {}; + type = types.attrs; + example = { MAILCHECK = 30; }; + description = "Environment variables that will be set for zsh session."; + }; + + initExtraBeforeCompInit = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to <filename>.zshrc</filename> before compinit."; + }; + + initExtra = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to <filename>.zshrc</filename>."; + }; + + envExtra = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to <filename>.zshenv</filename>."; + }; + + profileExtra = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to <filename>.zprofile</filename>."; + }; + + loginExtra = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to <filename>.zlogin</filename>."; + }; + + logoutExtra = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to <filename>.zlogout</filename>."; + }; + + plugins = mkOption { + type = types.listOf pluginModule; + default = []; + example = literalExample '' + [ + { + # will source zsh-autosuggestions.plugin.zsh + name = "zsh-autosuggestions"; + src = pkgs.fetchFromGitHub { + owner = "zsh-users"; + repo = "zsh-autosuggestions"; + rev = "v0.4.0"; + sha256 = "0z6i9wjjklb4lvr7zjhbphibsyx51psv50gm07mbb0kj9058j6kc"; + }; + } + { + name = "enhancd"; + file = "init.sh"; + src = pkgs.fetchFromGitHub { + owner = "b4b4r07"; + repo = "enhancd"; + rev = "v2.2.1"; + sha256 = "0iqa9j09fwm6nj5rpip87x3hnvbbz9w9ajgm6wkrd5fls8fn8i5g"; + }; + } + ] + ''; + description = "Plugins to source in <filename>.zshrc</filename>."; + }; + + oh-my-zsh = mkOption { + type = ohMyZshModule; + default = {}; + description = "Options to configure oh-my-zsh."; + }; + + localVariables = mkOption { + type = types.attrs; + default = {}; + example = { POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=["dir" "vcs"]; }; + description = '' + Extra local variables defined at the top of <filename>.zshrc</filename>. + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + (mkIf (cfg.envExtra != "") { + home.file."${relToDotDir ".zshenv"}".text = cfg.envExtra; + }) + + (mkIf (cfg.profileExtra != "") { + home.file."${relToDotDir ".zprofile"}".text = cfg.profileExtra; + }) + + (mkIf (cfg.loginExtra != "") { + home.file."${relToDotDir ".zlogin"}".text = cfg.loginExtra; + }) + + (mkIf (cfg.logoutExtra != "") { + home.file."${relToDotDir ".zlogout"}".text = cfg.logoutExtra; + }) + + (mkIf cfg.oh-my-zsh.enable { + home.file."${relToDotDir ".zshenv"}".text = '' + ZSH="${pkgs.oh-my-zsh}/share/oh-my-zsh"; + ZSH_CACHE_DIR="${config.xdg.cacheHome}/oh-my-zsh"; + ''; + }) + + (mkIf (cfg.dotDir != null) { + home.file."${relToDotDir ".zshenv"}".text = '' + ZDOTDIR=${zdotdir} + ''; + + # When dotDir is set, only use ~/.zshenv to source ZDOTDIR/.zshenv, + # This is so that if ZDOTDIR happens to be + # already set correctly (by e.g. spawning a zsh inside a zsh), all env + # vars still get exported + home.file.".zshenv".text = '' + source ${zdotdir}/.zshenv + ''; + }) + + { + home.packages = with pkgs; [ zsh ] + ++ optional cfg.enableCompletion nix-zsh-completions + ++ optional cfg.oh-my-zsh.enable oh-my-zsh; + + 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 + + HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help" + + ${optionalString (cfg.defaultKeymap != null) '' + # Use ${cfg.defaultKeymap} keymap as the default. + ${getAttr cfg.defaultKeymap bindkeyCommands} + ''} + + ${localVarsStr} + + ${cfg.initExtraBeforeCompInit} + + ${concatStrings (map (plugin: '' + path+="$HOME/${pluginsDir}/${plugin.name}" + fpath+="$HOME/${pluginsDir}/${plugin.name}" + '') cfg.plugins)} + + # Oh-My-Zsh calls compinit during initialization, + # calling it twice causes sight start up slowdown + # as all $fpath entries will be traversed again. + ${optionalString (cfg.enableCompletion && !cfg.oh-my-zsh.enable) + "autoload -U compinit && compinit" + } + + ${optionalString cfg.enableAutosuggestions + "source ${pkgs.zsh-autosuggestions}/share/zsh-autosuggestions/zsh-autosuggestions.zsh" + } + + # Environment variables + . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh" + ${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})" + } + ${optionalString (cfg.oh-my-zsh.custom != "") + "ZSH_CUSTOM=\"${cfg.oh-my-zsh.custom}\"" + } + ${optionalString (cfg.oh-my-zsh.theme != "") + "ZSH_THEME=\"${cfg.oh-my-zsh.theme}\"" + } + source $ZSH/oh-my-zsh.sh + ''} + + ${concatStrings (map (plugin: '' + if [ -f "$HOME/${pluginsDir}/${plugin.name}/${plugin.file}" ]; then + source "$HOME/${pluginsDir}/${plugin.name}/${plugin.file}" + fi + '') cfg.plugins)} + + # History options should be set in .zshrc and after oh-my-zsh sourcing. + # See https://github.com/rycee/home-manager/issues/177. + HISTSIZE="${toString cfg.history.size}" + SAVEHIST="${toString cfg.history.save}" + ${if versionAtLeast config.home.stateVersion "20.03" + then ''HISTFILE="${cfg.history.path}"'' + else ''HISTFILE="$HOME/${cfg.history.path}"''} + mkdir -p "$(dirname "$HISTFILE")" + + setopt HIST_FCNTL_LOCK + ${if cfg.history.ignoreDups then "setopt" else "unsetopt"} HIST_IGNORE_DUPS + ${if cfg.history.ignoreSpace then "setopt" else "unsetopt"} HIST_IGNORE_SPACE + ${if cfg.history.expireDuplicatesFirst then "setopt" else "unsetopt"} HIST_EXPIRE_DUPS_FIRST + ${if cfg.history.share then "setopt" else "unsetopt"} SHARE_HISTORY + ${if cfg.history.extended then "setopt" else "unsetopt"} EXTENDED_HISTORY + ${if cfg.autocd != null then "${if cfg.autocd then "setopt" else "unsetopt"} autocd" else ""} + + ${cfg.initExtra} + + # Aliases + ${aliasesStr} + + # Global Aliases + ${globalAliasesStr} + ''; + } + + (mkIf cfg.oh-my-zsh.enable { + # Make sure we create a cache directory since some plugins expect it to exist + # See: https://github.com/rycee/home-manager/issues/761 + home.file."${config.xdg.cacheHome}/oh-my-zsh/.keep".text = ""; + }) + + (mkIf (cfg.plugins != []) { + # Many plugins require compinit to be called + # but allow the user to opt out. + programs.zsh.enableCompletion = mkDefault true; + + home.file = + foldl' (a: b: a // b) {} + (map (plugin: { "${pluginsDir}/${plugin.name}".source = plugin.src; }) + cfg.plugins); + }) + ]); +} diff --git a/home-manager/modules/services/blueman-applet.nix b/home-manager/modules/services/blueman-applet.nix new file mode 100644 index 00000000000..5a57acccc27 --- /dev/null +++ b/home-manager/modules/services/blueman-applet.nix @@ -0,0 +1,36 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.blueman-applet = { + enable = mkEnableOption "" // { + description = '' + Whether to enable the Blueman applet. + </para><para> + Note, for the applet to work, the 'blueman' service should + be enabled system-wide. You can enable it in the system + configuration using + <programlisting language="nix"> + services.blueman.enable = true; + </programlisting> + ''; + }; + }; + }; + + config = mkIf config.services.blueman-applet.enable { + systemd.user.services.blueman-applet = { + Unit = { + Description = "Blueman applet"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { ExecStart = "${pkgs.blueman}/bin/blueman-applet"; }; + }; + }; +} diff --git a/home-manager/modules/services/cbatticon.nix b/home-manager/modules/services/cbatticon.nix new file mode 100644 index 00000000000..0de69c5f9ec --- /dev/null +++ b/home-manager/modules/services/cbatticon.nix @@ -0,0 +1,118 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.cbatticon; + + package = pkgs.cbatticon; + + makeCommand = commandName: commandArg: + optional (commandArg != null) + (let cmd = pkgs.writeShellScript commandName commandArg; + in "--${commandName} ${cmd}"); + + commandLine = concatStringsSep " " ([ "${package}/bin/cbatticon" ] + ++ makeCommand "command-critical-level" cfg.commandCriticalLevel + ++ makeCommand "command-left-click" cfg.commandLeftClick + ++ optional (cfg.iconType != null) "--icon-type ${cfg.iconType}" + ++ optional (cfg.lowLevelPercent != null) + "--low-level ${toString cfg.lowLevelPercent}" + ++ optional (cfg.criticalLevelPercent != null) + "--critical-level ${toString cfg.criticalLevelPercent}" + ++ optional (cfg.updateIntervalSeconds != null) + "--update-interval ${toString cfg.updateIntervalSeconds}" + ++ optional (cfg.hideNotification != null && cfg.hideNotification) + "--hide-notification"); + +in { + meta.maintainers = [ maintainers.pmiddend ]; + + options = { + services.cbatticon = { + enable = mkEnableOption "cbatticon"; + + commandCriticalLevel = mkOption { + type = types.nullOr types.lines; + default = null; + example = '' + notify-send "battery critical!" + ''; + description = '' + Command to execute when the critical battery level is reached. + ''; + }; + + commandLeftClick = mkOption { + type = types.nullOr types.lines; + default = null; + description = '' + Command to execute when left clicking on the tray icon. + ''; + }; + + iconType = mkOption { + type = + types.nullOr (types.enum [ "standard" "notification" "symbolic" ]); + default = null; + example = "symbolic"; + description = "Icon type to display in the system tray."; + }; + + lowLevelPercent = mkOption { + type = types.nullOr (types.ints.between 0 100); + default = null; + example = 20; + description = '' + Low level percentage of the battery in percent (without the + percent symbol). + ''; + }; + + criticalLevelPercent = mkOption { + type = types.nullOr (types.ints.between 0 100); + default = null; + example = 5; + description = '' + Critical level percentage of the battery in percent (without + the percent symbol). + ''; + }; + + updateIntervalSeconds = mkOption { + type = types.nullOr types.ints.positive; + default = null; + example = 5; + description = '' + Number of seconds between updates of the battery information. + ''; + }; + + hideNotification = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Hide the notification popups."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ package ]; + + systemd.user.services.cbatticon = { + Unit = { + Description = "cbatticon system tray battery icon"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = commandLine; + Restart = "on-abort"; + }; + }; + }; +} 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 new file mode 100644 index 00000000000..0b8e7232b45 --- /dev/null +++ b/home-manager/modules/services/compton.nix @@ -0,0 +1,43 @@ +{ config, lib, pkgs, ... }: + +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 config.services.compton.enable { + warnings = [ + "Obsolete option `services.compton.enable' is used. It was renamed to `services.picom.enable'." + ]; + + 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 new file mode 100644 index 00000000000..5fbbb884a8e --- /dev/null +++ b/home-manager/modules/services/dunst.nix @@ -0,0 +1,161 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.dunst; + + eitherStrBoolIntList = with types; + either str (either bool (either int (listOf str))); + + toDunstIni = generators.toINI { + mkKeyValue = key: value: + let + value' = if isBool value then + (if value then "yes" else "no") + else if isString value then + ''"${value}"'' + else + toString value; + in "${key}=${value'}"; + }; + + themeType = types.submodule { + options = { + package = mkOption { + type = types.package; + example = literalExample "pkgs.gnome3.adwaita-icon-theme"; + description = "Package providing the theme."; + }; + + name = mkOption { + type = types.str; + example = "Adwaita"; + description = "The name of the theme within the package."; + }; + + size = mkOption { + type = types.str; + default = "32x32"; + example = "16x16"; + description = "The desired icon size."; + }; + }; + }; + + hicolorTheme = { + package = pkgs.hicolor-icon-theme; + name = "hicolor"; + size = "32x32"; + }; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.dunst = { + enable = mkEnableOption "the dunst notification daemon"; + + iconTheme = mkOption { + type = themeType; + default = hicolorTheme; + description = "Set the icon theme."; + }; + + settings = mkOption { + type = with types; attrsOf (attrsOf eitherStrBoolIntList); + default = { }; + description = "Configuration written to ~/.config/dunstrc"; + example = literalExample '' + { + global = { + geometry = "300x5-30+50"; + transparency = 10; + frame_color = "#eceff1"; + font = "Droid Sans 9"; + }; + + urgency_normal = { + background = "#37474f"; + foreground = "#eceff1"; + timeout = 10; + }; + }; + ''; + }; + }; + }; + + 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"; + + services.dunst.settings.global.icon_path = let + useCustomTheme = cfg.iconTheme.package != hicolorTheme.package + || cfg.iconTheme.name != hicolorTheme.name || cfg.iconTheme.size + != hicolorTheme.size; + + basePaths = [ + "/run/current-system/sw" + config.home.profileDirectory + cfg.iconTheme.package + ] ++ optional useCustomTheme hicolorTheme.package; + + themes = [ cfg.iconTheme ] ++ optional useCustomTheme + (hicolorTheme // { size = cfg.iconTheme.size; }); + + categories = [ + "actions" + "animations" + "apps" + "categories" + "devices" + "emblems" + "emotes" + "filesystem" + "intl" + "mimetypes" + "places" + "status" + "stock" + ]; + in concatStringsSep ":" (concatMap (theme: + concatMap (basePath: + map (category: + "${basePath}/share/icons/${theme.name}/${theme.size}/${category}") + categories) basePaths) themes); + + systemd.user.services.dunst = { + Unit = { + Description = "Dunst notification daemon"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "dbus"; + BusName = "org.freedesktop.Notifications"; + ExecStart = "${pkgs.dunst}/bin/dunst"; + }; + }; + } + + (mkIf (cfg.settings != { }) { + xdg.configFile."dunst/dunstrc" = { + text = toDunstIni cfg.settings; + onChange = '' + pkillVerbose="" + if [[ -v VERBOSE ]]; then + pkillVerbose="-e" + fi + $DRY_RUN_CMD ${pkgs.procps}/bin/pkill -u $USER $pkillVerbose dunst || true + unset pkillVerbose + ''; + }; + }) + ]); +} diff --git a/home-manager/modules/services/dwm-status.nix b/home-manager/modules/services/dwm-status.nix new file mode 100644 index 00000000000..7a19e5e5fc9 --- /dev/null +++ b/home-manager/modules/services/dwm-status.nix @@ -0,0 +1,65 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.dwm-status; + + features = [ "audio" "backlight" "battery" "cpu_load" "network" "time" ]; + + configText = builtins.toJSON ({ inherit (cfg) order; } // cfg.extraConfig); + + configFile = pkgs.writeText "dwm-status.json" configText; + +in { + options = { + services.dwm-status = { + enable = mkEnableOption "dwm-status user service"; + + package = mkOption { + type = types.package; + default = pkgs.dwm-status; + defaultText = literalExample "pkgs.dwm-status"; + example = "pkgs.dwm-status.override { enableAlsaUtils = false; }"; + description = "Which dwm-status package to use."; + }; + + order = mkOption { + type = types.listOf (types.enum features); + description = "List of enabled features in order."; + }; + + extraConfig = mkOption { + type = types.attrs; + default = { }; + example = literalExample '' + { + separator = "#"; + + battery = { + notifier_levels = [ 2 5 10 15 20 ]; + }; + + time = { + format = "%H:%M"; + }; + } + ''; + description = "Extra config of dwm-status."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.dwm-status = { + Unit = { + Description = "DWM status service"; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { ExecStart = "${cfg.package}/bin/dwm-status ${configFile}"; }; + }; + }; +} diff --git a/home-manager/modules/services/emacs.nix b/home-manager/modules/services/emacs.nix new file mode 100644 index 00000000000..a73b750c513 --- /dev/null +++ b/home-manager/modules/services/emacs.nix @@ -0,0 +1,144 @@ +{ config, lib, pkgs, ... }: + +with lib; + +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 { + meta.maintainers = [ maintainers.tadfisher ]; + + 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>. + ''; + }; + }; + + # 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/flameshot.nix b/home-manager/modules/services/flameshot.nix new file mode 100644 index 00000000000..c8659d51d1e --- /dev/null +++ b/home-manager/modules/services/flameshot.nix @@ -0,0 +1,39 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.flameshot; + package = pkgs.flameshot; + +in { + meta.maintainers = [ maintainers.hamhut1066 ]; + + options = { services.flameshot = { enable = mkEnableOption "Flameshot"; }; }; + + config = mkIf cfg.enable { + home.packages = [ package ]; + + systemd.user.services.flameshot = { + Unit = { + Description = "Flameshot screenshot tool"; + After = [ + "graphical-session-pre.target" + "polybar.service" + "stalonetray.service" + "taffybar.service" + ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${package}/bin/flameshot"; + Restart = "on-abort"; + }; + }; + }; +} 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/getmail.nix b/home-manager/modules/services/getmail.nix new file mode 100644 index 00000000000..e7a1b1a4627 --- /dev/null +++ b/home-manager/modules/services/getmail.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.getmail; + + accounts = + filter (a: a.getmail.enable) (attrValues config.accounts.email.accounts); + + # Note: The getmail service does not expect a path, but just the filename! + renderConfigFilepath = a: + if a.primary then "getmailrc" else "getmail${a.name}"; + configFiles = + concatMapStringsSep " " (a: " --rcfile ${renderConfigFilepath a}") accounts; +in { + options = { + services.getmail = { + enable = mkEnableOption + "the getmail systemd service to automatically retrieve mail"; + + frequency = mkOption { + type = types.str; + default = "*:0/5"; + example = "hourly"; + description = '' + The refresh frequency. Check <literal>man systemd.time</literal> for + more information on the syntax. If you use a gpg-agent in + combination with the passwordCommand, keep the poll + frequency below the cache-ttl value (as set by the + <literal>default</literal>) to avoid pinentry asking + permanently for a password. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.getmail = { + Unit = { Description = "getmail email fetcher"; }; + Service = { ExecStart = "${pkgs.getmail}/bin/getmail ${configFiles}"; }; + }; + + systemd.user.timers.getmail = { + Unit = { Description = "getmail email fetcher"; }; + Timer = { + OnCalendar = "${cfg.frequency}"; + Unit = "getmail.service"; + }; + Install = { WantedBy = [ "timers.target" ]; }; + }; + + }; +} diff --git a/home-manager/modules/services/gnome-keyring.nix b/home-manager/modules/services/gnome-keyring.nix new file mode 100644 index 00000000000..ce39cea93f9 --- /dev/null +++ b/home-manager/modules/services/gnome-keyring.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.gnome-keyring; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.gnome-keyring = { + enable = mkEnableOption "GNOME Keyring"; + + components = mkOption { + type = types.listOf (types.enum [ "pkcs11" "secrets" "ssh" ]); + default = [ ]; + description = '' + The GNOME keyring components to start. If empty then the + default set of components will be started. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.gnome-keyring = { + Unit = { + Description = "GNOME Keyring"; + PartOf = [ "graphical-session-pre.target" ]; + }; + + Service = { + ExecStart = let + args = concatStringsSep " " ([ "--start" "--foreground" ] + ++ optional (cfg.components != [ ]) + ("--components=" + concatStringsSep "," cfg.components)); + in "${pkgs.gnome3.gnome-keyring}/bin/gnome-keyring-daemon ${args}"; + Restart = "on-abort"; + }; + + Install = { WantedBy = [ "graphical-session-pre.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/gpg-agent.nix b/home-manager/modules/services/gpg-agent.nix new file mode 100644 index 00000000000..16a4723fea7 --- /dev/null +++ b/home-manager/modules/services/gpg-agent.nix @@ -0,0 +1,281 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.gpg-agent; + + gpgInitStr = '' + GPG_TTY="$(tty)" + export GPG_TTY + '' + + optionalString cfg.enableSshSupport + "${pkgs.gnupg}/bin/gpg-connect-agent updatestartuptty /bye > /dev/null"; + +in + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.gpg-agent = { + enable = mkEnableOption "GnuPG private key agent"; + + defaultCacheTtl = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Set the time a cache entry is valid to the given number of + seconds. + ''; + }; + + defaultCacheTtlSsh = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Set the time a cache entry used for SSH keys is valid to the + given number of seconds. + ''; + }; + + maxCacheTtl = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Set the maximum time a cache entry is valid to n seconds. After this + time a cache entry will be expired even if it has been accessed + recently or has been set using gpg-preset-passphrase. The default is + 2 hours (7200 seconds). + ''; + }; + + maxCacheTtlSsh = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Set the maximum time a cache entry used for SSH keys is valid to n + seconds. After this time a cache entry will be expired even if it has + been accessed recently or has been set using gpg-preset-passphrase. + The default is 2 hours (7200 seconds). + ''; + }; + + enableSshSupport = mkOption { + type = types.bool; + default = false; + description = '' + Whether to use the GnuPG key agent for SSH keys. + ''; + }; + + sshKeys = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + Which GPG keys (by keygrip) to expose as SSH keys. + ''; + }; + + enableExtraSocket = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable extra socket of the GnuPG key agent (useful for GPG + Agent forwarding). + ''; + }; + + verbose = mkOption { + type = types.bool; + default = false; + description = '' + Whether to produce verbose output. + ''; + }; + + grabKeyboardAndMouse = mkOption { + type = types.bool; + default = true; + description = '' + Tell the pinentry to grab the keyboard and mouse. This + option should in general be used to avoid X-sniffing + attacks. When disabled, this option passes + <option>no-grab</option> setting to gpg-agent. + ''; + }; + + enableScDaemon = mkOption { + type = types.bool; + default = true; + description = '' + Make use of the scdaemon tool. This option has the effect of + enabling the ability to do smartcard operations. When + disabled, this option passes + <option>disable-scdaemon</option> setting to gpg-agent. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + allow-emacs-pinentry + allow-loopback-pinentry + ''; + description = '' + Extra configuration lines to append to the gpg-agent + configuration file. + ''; + }; + + pinentryFlavor = mkOption { + type = types.nullOr (types.enum pkgs.pinentry.flavors); + example = "gnome3"; + default = "gtk2"; + description = '' + Which pinentry interface to use. If not + <literal>null</literal>, it sets + <option>pinentry-program</option> in + <filename>gpg-agent.conf</filename>. Beware that + <literal>pinentry-gnome3</literal> may not work on non-Gnome + systems. You can fix it by adding the following to your + system configuration: + <programlisting language="nix"> + services.dbus.packages = [ pkgs.gcr ]; + </programlisting> + For this reason, the default is <literal>gtk2</literal> for + now. + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.file.".gnupg/gpg-agent.conf".text = concatStringsSep "\n" ( + optional (cfg.enableSshSupport) "enable-ssh-support" + ++ + optional (!cfg.grabKeyboardAndMouse) "no-grab" + ++ + optional (!cfg.enableScDaemon) "disable-scdaemon" + ++ + optional (cfg.defaultCacheTtl != null) + "default-cache-ttl ${toString cfg.defaultCacheTtl}" + ++ + optional (cfg.defaultCacheTtlSsh != null) + "default-cache-ttl-ssh ${toString cfg.defaultCacheTtlSsh}" + ++ + optional (cfg.maxCacheTtl != null) + "max-cache-ttl ${toString cfg.maxCacheTtl}" + ++ + optional (cfg.maxCacheTtlSsh != null) + "max-cache-ttl-ssh ${toString cfg.maxCacheTtlSsh}" + ++ + optional (cfg.pinentryFlavor != null) + "pinentry-program ${pkgs.pinentry.${cfg.pinentryFlavor}}/bin/pinentry" + ++ + [ cfg.extraConfig ] + ); + + home.sessionVariables = + optionalAttrs cfg.enableSshSupport { + SSH_AUTH_SOCK = "$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)"; + }; + + programs.bash.initExtra = gpgInitStr; + programs.zsh.initExtra = gpgInitStr; + } + + (mkIf (cfg.sshKeys != null) { + # Trailing newlines are important + home.file.".gnupg/sshcontrol".text = concatMapStrings (s: "${s}\n") cfg.sshKeys; + }) + + # The systemd units below are direct translations of the + # descriptions in the + # + # ${pkgs.gnupg}/share/doc/gnupg/examples/systemd-user + # + # directory. + { + systemd.user.services.gpg-agent = { + Unit = { + Description = "GnuPG cryptographic agent and passphrase cache"; + Documentation = "man:gpg-agent(1)"; + Requires = "gpg-agent.socket"; + After = "gpg-agent.socket"; + # This is a socket-activated service: + RefuseManualStart = true; + }; + + Service = { + ExecStart = "${pkgs.gnupg}/bin/gpg-agent --supervised" + + optionalString cfg.verbose " --verbose"; + ExecReload = "${pkgs.gnupg}/bin/gpgconf --reload gpg-agent"; + }; + }; + + systemd.user.sockets.gpg-agent = { + Unit = { + Description = "GnuPG cryptographic agent and passphrase cache"; + Documentation = "man:gpg-agent(1)"; + }; + + Socket = { + ListenStream = "%t/gnupg/S.gpg-agent"; + FileDescriptorName = "std"; + SocketMode = "0600"; + DirectoryMode = "0700"; + }; + + Install = { + WantedBy = [ "sockets.target" ]; + }; + }; + } + + (mkIf cfg.enableSshSupport { + systemd.user.sockets.gpg-agent-ssh = { + Unit = { + Description = "GnuPG cryptographic agent (ssh-agent emulation)"; + Documentation = "man:gpg-agent(1) man:ssh-add(1) man:ssh-agent(1) man:ssh(1)"; + }; + + Socket = { + ListenStream = "%t/gnupg/S.gpg-agent.ssh"; + FileDescriptorName = "ssh"; + Service = "gpg-agent.service"; + SocketMode = "0600"; + DirectoryMode = "0700"; + }; + + Install = { + WantedBy = [ "sockets.target" ]; + }; + }; + }) + + (mkIf cfg.enableExtraSocket { + systemd.user.sockets.gpg-agent-extra = { + Unit = { + Description = "GnuPG cryptographic agent and passphrase cache (restricted)"; + Documentation = "man:gpg-agent(1) man:ssh(1)"; + }; + + Socket = { + ListenStream = "%t/gnupg/S.gpg-agent.extra"; + FileDescriptorName = "extra"; + Service = "gpg-agent.service"; + SocketMode = "0600"; + DirectoryMode = "0700"; + }; + + Install = { + WantedBy = [ "sockets.target" ]; + }; + }; + }) + ]); +} diff --git a/home-manager/modules/services/grobi.nix b/home-manager/modules/services/grobi.nix new file mode 100644 index 00000000000..4dfc5d6331f --- /dev/null +++ b/home-manager/modules/services/grobi.nix @@ -0,0 +1,97 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.grobi; + + eitherStrBoolIntList = with types; + either str (either bool (either int (listOf str))); + +in { + meta.maintainers = [ maintainers.mbrgm ]; + + options = { + services.grobi = { + enable = mkEnableOption "the grobi display setup daemon"; + + executeAfter = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "setxkbmap dvorak" ]; + description = '' + Commands to be run after an output configuration was + changed. The Nix value declared here will be translated to + JSON and written to the <option>execute_after</option> key + in <filename>~/.config/grobi.conf</filename>. + ''; + }; + + rules = mkOption { + type = with types; listOf (attrsOf eitherStrBoolIntList); + default = [ ]; + example = literalExample '' + [ + { + name = "Home"; + outputs_connected = [ "DP-2" ]; + configure_single = "DP-2"; + primary = true; + atomic = true; + execute_after = [ + "${pkgs.xorg.xrandr}/bin/xrandr --dpi 96" + "${pkgs.xmonad-with-packages}/bin/xmonad --restart"; + ]; + } + { + name = "Mobile"; + outputs_disconnected = [ "DP-2" ]; + configure_single = "eDP-1"; + primary = true; + atomic = true; + execute_after = [ + "${pkgs.xorg.xrandr}/bin/xrandr --dpi 120" + "${pkgs.xmonad-with-packages}/bin/xmonad --restart"; + ]; + } + ] + ''; + description = '' + These are the rules grobi tries to match to the current + output configuration. The rules are evaluated top to bottom, + the first matching rule is applied and processing stops. See + <link xlink:href="https://github.com/fd0/grobi/blob/master/doc/grobi.conf"/> + for more information. The Nix value declared here will be + translated to JSON and written to the <option>rules</option> + key in <filename>~/.config/grobi.conf</filename>. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.grobi = { + Unit = { + Description = "grobi display auto config daemon"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "simple"; + ExecStart = "${pkgs.grobi}/bin/grobi watch -v"; + Restart = "always"; + RestartSec = "2s"; + Environment = "PATH=${pkgs.xorg.xrandr}/bin:${pkgs.bash}/bin"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + + xdg.configFile."grobi.conf".text = builtins.toJSON { + execute_after = cfg.executeAfter; + rules = cfg.rules; + }; + }; +} diff --git a/home-manager/modules/services/hound.nix b/home-manager/modules/services/hound.nix new file mode 100644 index 00000000000..00589f3405f --- /dev/null +++ b/home-manager/modules/services/hound.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.hound; + + configFile = pkgs.writeText "hound-config.json" (builtins.toJSON { + max-concurrent-indexers = cfg.maxConcurrentIndexers; + dbpath = cfg.databasePath; + repos = cfg.repositories; + health-check-url = "/healthz"; + }); + + houndOptions = [ "--addr ${cfg.listenAddress}" "--conf ${configFile}" ]; + +in { + meta.maintainers = [ maintainers.adisbladis ]; + + options.services.hound = { + enable = mkEnableOption "hound"; + + maxConcurrentIndexers = mkOption { + type = types.ints.positive; + default = 2; + description = "Limit the amount of concurrent indexers."; + }; + + databasePath = mkOption { + type = types.path; + default = "${config.xdg.dataHome}/hound"; + defaultText = "$XDG_DATA_HOME/hound"; + description = "The Hound database path."; + }; + + listenAddress = mkOption { + type = types.str; + default = "localhost:6080"; + description = "Listen address of the Hound daemon."; + }; + + repositories = mkOption { + type = types.attrsOf (types.uniq types.attrs); + default = { }; + example = literalExample '' + { + SomeGitRepo = { + url = "https://www.github.com/YourOrganization/RepoOne.git"; + ms-between-poll = 10000; + exclude-dot-files = true; + }; + } + ''; + description = "The repository configuration."; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.hound ]; + + systemd.user.services.hound = { + Unit = { Description = "Hound source code search engine"; }; + + Install = { WantedBy = [ "default.target" ]; }; + + Service = { + Environment = "PATH=${makeBinPath [ pkgs.mercurial pkgs.git ]}"; + ExecStart = + "${pkgs.hound}/bin/houndd ${concatStringsSep " " houndOptions}"; + }; + }; + }; +} diff --git a/home-manager/modules/services/imapnotify-accounts.nix b/home-manager/modules/services/imapnotify-accounts.nix new file mode 100644 index 00000000000..94bdce5dfb4 --- /dev/null +++ b/home-manager/modules/services/imapnotify-accounts.nix @@ -0,0 +1,33 @@ +{ lib, ... }: + +with lib; + +{ + options.imapnotify = { + enable = mkEnableOption "imapnotify"; + + onNotify = mkOption { + type = with types; either str (attrsOf str); + default = ""; + example = "\${pkgs.isync}/bin/mbsync test-%s"; + description = "Shell commands to run on any event."; + }; + + onNotifyPost = mkOption { + type = with types; either str (attrsOf str); + default = ""; + example = { + mail = + "\${pkgs.notmuch}/bin/notmuch new && \${pkgs.libnotify}/bin/notify-send 'New mail arrived'"; + }; + description = "Shell commands to run after onNotify event."; + }; + + boxes = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "Inbox" "[Gmail]/MyLabel" ]; + description = "IMAP folders to watch."; + }; + }; +} diff --git a/home-manager/modules/services/imapnotify.nix b/home-manager/modules/services/imapnotify.nix new file mode 100644 index 00000000000..b59b006e335 --- /dev/null +++ b/home-manager/modules/services/imapnotify.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.imapnotify; + + safeName = lib.replaceChars [ "@" ":" "\\" "[" "]" ] [ "-" "-" "-" "" "" ]; + + imapnotifyAccounts = + filter (a: a.imapnotify.enable) (attrValues config.accounts.email.accounts); + + genAccountUnit = account: + let name = safeName account.name; + in { + name = "imapnotify-${name}"; + value = { + Unit = { Description = "imapnotify for ${name}"; }; + + Service = { + ExecStart = + "${pkgs.imapnotify}/bin/imapnotify -c ${genAccountConfig account}"; + } // optionalAttrs account.notmuch.enable { + Environment = + "NOTMUCH_CONFIG=${config.xdg.configHome}/notmuch/notmuchrc"; + }; + + Install = { WantedBy = [ "default.target" ]; }; + }; + }; + + genAccountConfig = account: + pkgs.writeText "imapnotify-${safeName account.name}-config.js" (let + port = if account.imap.port != null then + account.imap.port + else if account.imap.tls.enable then + 993 + else + 143; + + toJSON = builtins.toJSON; + in '' + var child_process = require('child_process'); + + function getStdout(cmd) { + var stdout = child_process.execSync(cmd); + return stdout.toString().trim(); + } + + exports.host = ${toJSON account.imap.host} + exports.port = ${toJSON port}; + exports.tls = ${toJSON account.imap.tls.enable}; + exports.username = ${toJSON account.userName}; + exports.password = getStdout("${toString account.passwordCommand}"); + exports.onNotify = ${toJSON account.imapnotify.onNotify}; + exports.onNotifyPost = ${toJSON account.imapnotify.onNotifyPost}; + exports.boxes = ${toJSON account.imapnotify.boxes}; + ''); + +in { + meta.maintainers = [ maintainers.nickhu ]; + + options = { + services.imapnotify = { enable = mkEnableOption "imapnotify"; }; + + accounts.email.accounts = mkOption { + type = with types; attrsOf (submodule (import ./imapnotify-accounts.nix)); + }; + }; + + config = mkIf cfg.enable { + assertions = let + checkAccounts = pred: msg: + let badAccounts = filter pred imapnotifyAccounts; + in { + assertion = badAccounts == [ ]; + message = "imapnotify: Missing ${msg} for accounts: " + + concatMapStringsSep ", " (a: a.name) badAccounts; + }; + in [ + (checkAccounts (a: a.maildir == null) "maildir configuration") + (checkAccounts (a: a.imap == null) "IMAP configuration") + (checkAccounts (a: a.passwordCommand == null) "password command") + (checkAccounts (a: a.userName == null) "username") + ]; + + systemd.user.services = listToAttrs (map genAccountUnit imapnotifyAccounts); + }; +} 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/kbfs.nix b/home-manager/modules/services/kbfs.nix new file mode 100644 index 00000000000..863f4feea3b --- /dev/null +++ b/home-manager/modules/services/kbfs.nix @@ -0,0 +1,67 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.kbfs; + +in + +{ + options = { + services.kbfs = { + enable = mkEnableOption "Keybase File System"; + + mountPoint = mkOption { + type = types.str; + default = "keybase"; + description = '' + Mount point for the Keybase filesystem, relative to + <envar>HOME</envar>. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + example = [ + "-label kbfs" + "-mount-type normal" + ]; + description = '' + Additional flags to pass to the Keybase filesystem on launch. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.kbfs = { + Unit = { + Description = "Keybase File System"; + Requires = [ "keybase.service" ]; + After = [ "keybase.service" ]; + }; + + Service = + let + mountPoint = "\"%h/${cfg.mountPoint}\""; + in { + Environment = "PATH=/run/wrappers/bin KEYBASE_SYSTEMD=1"; + ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mountPoint}"; + ExecStart ="${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} ${mountPoint}"; + ExecStopPost = "/run/wrappers/bin/fusermount -u ${mountPoint}"; + Restart = "on-failure"; + PrivateTmp = true; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + + home.packages = [ pkgs.kbfs ]; + services.keybase.enable = true; + }; +} diff --git a/home-manager/modules/services/kdeconnect.nix b/home-manager/modules/services/kdeconnect.nix new file mode 100644 index 00000000000..82de1f0eb7c --- /dev/null +++ b/home-manager/modules/services/kdeconnect.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.kdeconnect; + package = pkgs.kdeconnect; + +in { + meta.maintainers = [ maintainers.adisbladis ]; + + options = { + services.kdeconnect = { + enable = mkEnableOption "KDE connect"; + + indicator = mkOption { + type = types.bool; + default = false; + description = "Whether to enable kdeconnect-indicator service."; + }; + + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + home.packages = [ package ]; + + systemd.user.services.kdeconnect = { + Unit = { + Description = + "Adds communication between your desktop and your smartphone"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${package}/libexec/kdeconnectd"; + Restart = "on-abort"; + }; + }; + }) + + (mkIf cfg.indicator { + systemd.user.services.kdeconnect-indicator = { + Unit = { + Description = "kdeconnect-indicator"; + After = [ + "graphical-session-pre.target" + "polybar.service" + "taffybar.service" + "stalonetray.service" + ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${package}/bin/kdeconnect-indicator"; + Restart = "on-abort"; + }; + }; + }) + + ]; +} diff --git a/home-manager/modules/services/keepassx.nix b/home-manager/modules/services/keepassx.nix new file mode 100644 index 00000000000..dc37066e20c --- /dev/null +++ b/home-manager/modules/services/keepassx.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.keepassx = { + enable = mkEnableOption "the KeePassX password manager"; + }; + }; + + config = mkIf config.services.keepassx.enable { + systemd.user.services.keepassx = { + Unit = { + Description = "KeePassX password manager"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { ExecStart = "${pkgs.keepassx}/bin/keepassx -min -lock"; }; + }; + }; +} diff --git a/home-manager/modules/services/keybase.nix b/home-manager/modules/services/keybase.nix new file mode 100644 index 00000000000..2d0a06b06a7 --- /dev/null +++ b/home-manager/modules/services/keybase.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.keybase; + +in + +{ + options = { + services.keybase = { + enable = mkEnableOption "Keybase"; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.keybase ]; + + systemd.user.services.keybase = { + Unit = { + Description = "Keybase service"; + }; + + Service = { + ExecStart = "${pkgs.keybase}/bin/keybase service --auto-forked"; + Restart = "on-failure"; + PrivateTmp = true; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; +} 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 new file mode 100644 index 00000000000..6183699088b --- /dev/null +++ b/home-manager/modules/services/lorri.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.lorri; + +in { + meta.maintainers = [ maintainers.gerschtli ]; + + 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 = [ cfg.package ]; + + systemd.user = { + services.lorri = { + Unit = { + Description = "lorri build daemon"; + Requires = "lorri.socket"; + After = "lorri.socket"; + RefuseManualStart = true; + }; + + Service = { + ExecStart = "${cfg.package}/bin/lorri daemon"; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = "read-only"; + Restart = "on-failure"; + Environment = let + path = with pkgs; + makeSearchPath "bin" [ nix gitMinimal gnutar gzip ]; + in [ "PATH=${path}" ]; + }; + }; + + sockets.lorri = { + Unit = { Description = "Socket for lorri build daemon"; }; + + Socket = { + ListenStream = "%t/lorri/daemon.socket"; + RuntimeDirectory = "lorri"; + }; + + Install = { WantedBy = [ "sockets.target" ]; }; + }; + }; + }; +} 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/mbsync.nix b/home-manager/modules/services/mbsync.nix new file mode 100644 index 00000000000..ac6ac1ef78a --- /dev/null +++ b/home-manager/modules/services/mbsync.nix @@ -0,0 +1,104 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.mbsync; + + mbsyncOptions = [ "--all" ] ++ optional (cfg.verbose) "--verbose" + ++ optional (cfg.configFile != null) "--config ${cfg.configFile}"; + +in { + meta.maintainers = [ maintainers.pjones ]; + + options.services.mbsync = { + enable = mkEnableOption "mbsync"; + + package = mkOption { + type = types.package; + default = pkgs.isync; + defaultText = literalExample "pkgs.isync"; + example = literalExample "pkgs.isync"; + description = "The package to use for the mbsync binary."; + }; + + frequency = mkOption { + type = types.str; + default = "*:0/5"; + description = '' + How often to run mbsync. 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. + ''; + }; + + verbose = mkOption { + type = types.bool; + default = true; + description = '' + Whether mbsync should produce verbose output. + ''; + }; + + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Optional configuration file to link to use instead of + the default file (<filename>~/.mbsyncrc</filename>). + ''; + }; + + preExec = mkOption { + type = types.nullOr types.str; + default = null; + example = "mkdir -p %h/mail"; + description = '' + An optional command to run before mbsync executes. This is + useful for creating the directories mbsync is going to use. + ''; + }; + + postExec = mkOption { + type = types.nullOr types.str; + default = null; + example = "\${pkgs.mu}/bin/mu index"; + description = '' + An optional command to run after mbsync executes successfully. + This is useful for running mailbox indexing tools. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.mbsync = { + Unit = { Description = "mbsync mailbox synchronization"; }; + + Service = { + Type = "oneshot"; + ExecStart = + "${cfg.package}/bin/mbsync ${concatStringsSep " " mbsyncOptions}"; + } // (optionalAttrs (cfg.postExec != null) { + ExecStartPost = cfg.postExec; + }) // (optionalAttrs (cfg.preExec != null) { + ExecStartPre = cfg.preExec; + }); + }; + + systemd.user.timers.mbsync = { + Unit = { Description = "mbsync mailbox synchronization"; }; + + Timer = { + OnCalendar = cfg.frequency; + Unit = "mbsync.service"; + }; + + Install = { WantedBy = [ "timers.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/mpd.nix b/home-manager/modules/services/mpd.nix new file mode 100644 index 00000000000..13b3ae78f26 --- /dev/null +++ b/home-manager/modules/services/mpd.nix @@ -0,0 +1,150 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + name = "mpd"; + + cfg = config.services.mpd; + + mpdConf = pkgs.writeText "mpd.conf" '' + music_directory "${cfg.musicDirectory}" + playlist_directory "${cfg.playlistDirectory}" + ${lib.optionalString (cfg.dbFile != null) '' + db_file "${cfg.dbFile}" + ''} + state_file "${cfg.dataDir}/state" + sticker_file "${cfg.dataDir}/sticker.sql" + + ${optionalString (cfg.network.listenAddress != "any") + ''bind_to_address "${cfg.network.listenAddress}"''} + ${optionalString (cfg.network.port != 6600) + ''port "${toString cfg.network.port}"''} + + ${cfg.extraConfig} + ''; + +in { + + ###### interface + + options = { + + services.mpd = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable MPD, the music player daemon. + ''; + }; + + musicDirectory = mkOption { + type = with types; either path str; + default = "${config.home.homeDirectory}/music"; + defaultText = "$HOME/music"; + apply = toString; # Prevent copies to Nix store. + description = '' + The directory where mpd reads music from. + ''; + }; + + playlistDirectory = mkOption { + type = types.path; + default = "${cfg.dataDir}/playlists"; + defaultText = ''''${dataDir}/playlists''; + apply = toString; # Prevent copies to Nix store. + description = '' + The directory where mpd stores playlists. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra directives added to to the end of MPD's configuration + file, <filename>mpd.conf</filename>. Basic configuration + like file location and uid/gid is added automatically to the + beginning of the file. For available options see + <citerefentry> + <refentrytitle>mpd.conf</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry>. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "${config.xdg.dataHome}/${name}"; + defaultText = "$XDG_DATA_HOME/mpd"; + apply = toString; # Prevent copies to Nix store. + description = '' + The directory where MPD stores its state, tag cache, + playlists etc. + ''; + }; + + network = { + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + example = "any"; + description = '' + The address for the daemon to listen on. + Use <literal>any</literal> to listen on all addresses. + ''; + }; + + port = mkOption { + type = types.port; + default = 6600; + description = '' + The TCP port on which the the daemon will listen. + ''; + }; + + }; + + dbFile = mkOption { + type = types.nullOr types.str; + default = "${cfg.dataDir}/tag_cache"; + defaultText = ''''${dataDir}/tag_cache''; + description = '' + The path to MPD's database. If set to + <literal>null</literal> the parameter is omitted from the + configuration. + ''; + }; + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + systemd.user.services.mpd = { + Unit = { + After = [ "network.target" "sound.target" ]; + Description = "Music Player Daemon"; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon ${mpdConf}"; + Type = "notify"; + ExecStartPre = ''${pkgs.bash}/bin/bash -c "${pkgs.coreutils}/bin/mkdir -p '${cfg.dataDir}' '${cfg.playlistDirectory}'"''; + }; + }; + }; + +} diff --git a/home-manager/modules/services/mpdris2.nix b/home-manager/modules/services/mpdris2.nix new file mode 100644 index 00000000000..cb8cefba6bd --- /dev/null +++ b/home-manager/modules/services/mpdris2.nix @@ -0,0 +1,101 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.mpdris2; + + toIni = generators.toINI { + mkKeyValue = key: value: + let + value' = if isBool value then + (if value then "True" else "False") + else + toString value; + in "${key} = ${value'}"; + }; + + mpdris2Conf = { + Connection = { + host = cfg.mpd.host; + port = cfg.mpd.port; + music_dir = cfg.mpd.musicDirectory; + }; + + Bling = { + notify = cfg.notifications; + mmkeys = cfg.multimediaKeys; + }; + }; + +in { + meta.maintainers = [ maintainers.pjones ]; + + options.services.mpdris2 = { + enable = mkEnableOption "mpDris2 the MPD to MPRIS2 bridge"; + notifications = mkEnableOption "song change notifications"; + multimediaKeys = mkEnableOption "multimedia key support"; + + package = mkOption { + type = types.package; + default = pkgs.mpdris2; + defaultText = literalExample "pkgs.mpdris2"; + description = "The mpDris2 package to use."; + }; + + mpd = { + host = mkOption { + type = types.str; + default = config.services.mpd.network.listenAddress; + defaultText = "config.services.mpd.network.listenAddress"; + example = "192.168.1.1"; + description = "The address where MPD is listening for connections."; + }; + + port = mkOption { + type = types.port; + default = config.services.mpd.network.port; + defaultText = "config.services.mpd.network.port"; + description = '' + The port number where MPD is listening for connections. + ''; + }; + + musicDirectory = mkOption { + type = types.nullOr types.path; + default = config.services.mpd.musicDirectory; + defaultText = "config.services.mpd.musicDirectory"; + description = '' + If set, mpDris2 will use this directory to access music artwork. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = config.services.mpd.enable; + message = "The mpdris2 module requires 'services.mpd.enable = true'."; + }]; + + xdg.configFile."mpDris2/mpDris2.conf".text = toIni mpdris2Conf; + + systemd.user.services.mpdris2 = { + Install = { WantedBy = [ "default.target" ]; }; + + Unit = { + Description = "MPRIS 2 support for MPD"; + After = [ "mpd.service" ]; + }; + + Service = { + Type = "simple"; + Restart = "on-failure"; + RestartSec = "5s"; + ExecStart = "${cfg.package}/bin/mpDris2"; + BusName = "org.mpris.MediaPlayer2.mpd"; + }; + }; + }; +} diff --git a/home-manager/modules/services/muchsync.nix b/home-manager/modules/services/muchsync.nix new file mode 100644 index 00000000000..b7004418d35 --- /dev/null +++ b/home-manager/modules/services/muchsync.nix @@ -0,0 +1,203 @@ +{ config, lib, pkgs, ... }: + +with lib; + +# Documentation was partially copied from the muchsync manual. +# See http://www.muchsync.org/muchsync.html + +let + cfg = config.services.muchsync; + syncOptions = { + options = { + frequency = mkOption { + type = types.str; + default = "*:0/5"; + description = '' + How often to run <command>muchsync</command>. This + value is passed to the systemd timer configuration as the + <literal>OnCalendar</literal> option. See + <citerefentry> + <refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum> + </citerefentry> + for more information about the format. + ''; + }; + + sshCommand = mkOption { + type = types.str; + default = "${pkgs.openssh}/bin/ssh -CTaxq"; + defaultText = "ssh -CTaxq"; + description = '' + Specifies a command line to pass to <command>/bin/sh</command> + to execute a command on another machine. + </para><para> + Note that because this string is passed to the shell, + special characters including spaces may need to be escaped. + ''; + }; + + upload = mkOption { + type = types.bool; + default = true; + description = '' + Whether to propagate local changes to the remote. + ''; + }; + + local = { + checkForModifiedFiles = mkOption { + type = types.bool; + default = false; + description = '' + Check for locally modified files. + Without this option, muchsync assumes that files in a maildir are + never edited. + </para><para> + <option>checkForModifiedFiles</option> disables certain + optimizations so as to make muchsync at least check the timestamp on + every file, which will detect modified files at the cost of a longer + startup time. + </para><para> + This option is useful if your software regularly modifies the + contents of mail files (e.g., because you are running offlineimap + with "synclabels = yes"). + ''; + }; + + importNew = mkOption { + type = types.bool; + default = true; + description = '' + Whether to begin the synchronisation by running + <command>notmuch new</command> locally. + ''; + }; + }; + + remote = { + host = mkOption { + type = types.str; + description = '' + Remote SSH host to synchronize with. + ''; + }; + + muchsyncPath = mkOption { + type = types.str; + default = ""; + defaultText = "$PATH/muchsync"; + description = '' + Specifies the path to muchsync on the server. + Ordinarily, muchsync should be in the default PATH on the server + so this option is not required. + However, this option is useful if you have to install muchsync in + a non-standard place or wish to test development versions of the + code. + ''; + }; + + checkForModifiedFiles = mkOption { + type = types.bool; + default = false; + description = '' + Check for modified files on the remote side. + Without this option, muchsync assumes that files in a maildir are + never edited. + </para><para> + <option>checkForModifiedFiles</option> disables certain + optimizations so as to make muchsync at least check the timestamp on + every file, which will detect modified files at the cost of a longer + startup time. + </para><para> + This option is useful if your software regularly modifies the + contents of mail files (e.g., because you are running offlineimap + with "synclabels = yes"). + ''; + }; + + importNew = mkOption { + type = types.bool; + default = true; + description = '' + Whether to begin the synchronisation by running + <command>notmuch new</command> on the remote side. + ''; + }; + }; + }; + }; + +in { + meta.maintainers = with maintainers; [ pacien ]; + + options.services.muchsync = { + remotes = mkOption { + type = with types; attrsOf (submodule syncOptions); + default = { }; + example = literalExample '' + { + server = { + frequency = "*:0/10"; + remote.host = "server.tld"; + }; + } + ''; + description = '' + Muchsync remotes to synchronise with. + ''; + }; + }; + + config = let + mapRemotes = gen: + with attrsets; + mapAttrs' + (name: remoteCfg: nameValuePair "muchsync-${name}" (gen name remoteCfg)) + cfg.remotes; + in mkIf (cfg.remotes != { }) { + assertions = [{ + assertion = config.programs.notmuch.enable; + message = '' + The muchsync module requires 'programs.notmuch.enable = true'. + ''; + }]; + + systemd.user.services = mapRemotes (name: remoteCfg: { + Unit = { Description = "muchsync sync service (${name})"; }; + Service = { + CPUSchedulingPolicy = "idle"; + IOSchedulingClass = "idle"; + Environment = [ + ''"PATH=${pkgs.notmuch}/bin"'' + ''"NOTMUCH_CONFIG=${config.home.sessionVariables.NOTMUCH_CONFIG}"'' + ''"NMBGIT=${config.home.sessionVariables.NMBGIT}"'' + ]; + ExecStart = concatStringsSep " " ([ "${pkgs.muchsync}/bin/muchsync" ] + ++ [ "-s ${escapeShellArg remoteCfg.sshCommand}" ] + ++ optional (!remoteCfg.upload) "--noup" + + # local configuration + ++ optional remoteCfg.local.checkForModifiedFiles "-F" + ++ optional (!remoteCfg.local.importNew) "--nonew" + + # remote configuration + ++ [ (escapeShellArg remoteCfg.remote.host) ] + ++ optional (remoteCfg.remote.muchsyncPath != "") + "-r ${escapeShellArg remoteCfg.remote.muchsyncPath}" + ++ optional remoteCfg.remote.checkForModifiedFiles "-F" + ++ optional (!remoteCfg.remote.importNew) "--nonew"); + }; + }); + + systemd.user.timers = mapRemotes (name: remoteCfg: { + Unit = { Description = "muchsync periodic sync (${name})"; }; + Timer = { + Unit = "muchsync-${name}.service"; + OnCalendar = remoteCfg.frequency; + Persistent = true; + }; + Install = { WantedBy = [ "timers.target" ]; }; + }); + }; +} diff --git a/home-manager/modules/services/network-manager-applet.nix b/home-manager/modules/services/network-manager-applet.nix new file mode 100644 index 00000000000..bf57ed65091 --- /dev/null +++ b/home-manager/modules/services/network-manager-applet.nix @@ -0,0 +1,36 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.network-manager-applet; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.network-manager-applet = { + enable = mkEnableOption "the Network Manager applet"; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.network-manager-applet = { + Unit = { + Description = "Network Manager applet"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = toString + ([ "${pkgs.networkmanagerapplet}/bin/nm-applet" "--sm-disable" ] + ++ optional config.xsession.preferStatusNotifierItems + "--indicator"); + }; + }; + }; +} diff --git a/home-manager/modules/services/nextcloud-client.nix b/home-manager/modules/services/nextcloud-client.nix new file mode 100644 index 00000000000..555ca11ad64 --- /dev/null +++ b/home-manager/modules/services/nextcloud-client.nix @@ -0,0 +1,26 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.nextcloud-client = { enable = mkEnableOption "Nextcloud Client"; }; + }; + + config = mkIf config.services.nextcloud-client.enable { + systemd.user.services.nextcloud-client = { + Unit = { + Description = "Nextcloud Client"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${pkgs.nextcloud-client}/bin/nextcloud"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/owncloud-client.nix b/home-manager/modules/services/owncloud-client.nix new file mode 100644 index 00000000000..d55d8ffa2a4 --- /dev/null +++ b/home-manager/modules/services/owncloud-client.nix @@ -0,0 +1,26 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.owncloud-client = { enable = mkEnableOption "Owncloud Client"; }; + }; + + config = mkIf config.services.owncloud-client.enable { + systemd.user.services.owncloud-client = { + Unit = { + Description = "Owncloud Client"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${pkgs.owncloud-client}/bin/owncloud"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/parcellite.nix b/home-manager/modules/services/parcellite.nix new file mode 100644 index 00000000000..ce04238613b --- /dev/null +++ b/home-manager/modules/services/parcellite.nix @@ -0,0 +1,35 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.parcellite; + package = pkgs.parcellite; + +in { + meta.maintainers = [ maintainers.gleber ]; + + options = { + services.parcellite = { enable = mkEnableOption "Parcellite"; }; + }; + + config = mkIf cfg.enable { + home.packages = [ package ]; + + systemd.user.services.parcellite = { + Unit = { + Description = "Lightweight GTK+ clipboard manager"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = "${package}/bin/parcellite"; + Restart = "on-abort"; + }; + }; + }; +} diff --git a/home-manager/modules/services/password-store-sync.nix b/home-manager/modules/services/password-store-sync.nix new file mode 100644 index 00000000000..81933914980 --- /dev/null +++ b/home-manager/modules/services/password-store-sync.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + serviceCfg = config.services.password-store-sync; + programCfg = config.programs.password-store; + +in { + meta.maintainers = with maintainers; [ pacien ]; + + options.services.password-store-sync = { + enable = mkEnableOption "Password store periodic sync"; + + frequency = mkOption { + type = types.str; + default = "*:0/5"; + description = '' + How often to synchronise the password store git repository with its + default upstream. + </para><para> + This value is passed to the systemd timer configuration as the + <literal>onCalendar</literal> option. + See + <citerefentry> + <refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum> + </citerefentry> + for more information about the format. + ''; + }; + }; + + config = mkIf serviceCfg.enable { + assertions = [{ + assertion = programCfg.enable; + message = "The 'services.password-store-sync' module requires" + + " 'programs.password-store.enable = true'."; + }]; + + systemd.user.services.password-store-sync = { + Unit = { Description = "Password store sync"; }; + + Service = { + CPUSchedulingPolicy = "idle"; + IOSchedulingClass = "idle"; + Environment = let + makeEnvironmentPairs = + mapAttrsToList (key: value: "${key}=${builtins.toJSON value}"); + in makeEnvironmentPairs programCfg.settings; + ExecStart = toString (pkgs.writeShellScript "password-store-sync" '' + ${pkgs.pass}/bin/pass git pull --rebase && \ + ${pkgs.pass}/bin/pass git push + ''); + }; + }; + + systemd.user.timers.password-store-sync = { + Unit = { Description = "Password store periodic sync"; }; + + Timer = { + Unit = "password-store-sync.service"; + OnCalendar = serviceCfg.frequency; + Persistent = true; + }; + + Install = { WantedBy = [ "timers.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/pasystray.nix b/home-manager/modules/services/pasystray.nix new file mode 100644 index 00000000000..7c6651d9499 --- /dev/null +++ b/home-manager/modules/services/pasystray.nix @@ -0,0 +1,30 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.pltanton ]; + + options = { + services.pasystray = { enable = mkEnableOption "PulseAudio system tray"; }; + }; + + config = mkIf config.services.pasystray.enable { + systemd.user.services.pasystray = { + Unit = { + Description = "PulseAudio system tray"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Environment = + let toolPaths = makeBinPath [ pkgs.paprefs pkgs.pavucontrol ]; + in [ "PATH=${toolPaths}" ]; + ExecStart = "${pkgs.pasystray}/bin/pasystray"; + }; + }; + }; +} 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/polybar.nix b/home-manager/modules/services/polybar.nix new file mode 100644 index 00000000000..934a990638f --- /dev/null +++ b/home-manager/modules/services/polybar.nix @@ -0,0 +1,135 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.polybar; + + eitherStrBoolIntList = with types; + either str (either bool (either int (listOf str))); + + toPolybarIni = generators.toINI { + mkKeyValue = key: value: + let + quoted = v: + if hasPrefix " " v || hasSuffix " " v then ''"${v}"'' else v; + + value' = if isBool value then + (if value then "true" else "false") + else if (isString value && key != "include-file") then + quoted value + else + toString value; + in "${key}=${value'}"; + }; + + configFile = pkgs.writeText "polybar.conf" + (toPolybarIni cfg.config + "\n" + cfg.extraConfig); + +in { + options = { + services.polybar = { + enable = mkEnableOption "Polybar status bar"; + + package = mkOption { + type = types.package; + default = pkgs.polybar; + defaultText = literalExample "pkgs.polybar"; + description = "Polybar package to install."; + example = literalExample '' + pkgs.polybar.override { + i3GapsSupport = true; + alsaSupport = true; + iwSupport = true; + githubSupport = true; + } + ''; + }; + + config = mkOption { + type = types.coercedTo types.path + (p: { "section/base" = { include-file = "${p}"; }; }) + (types.attrsOf (types.attrsOf eitherStrBoolIntList)); + description = '' + Polybar configuration. Can be either path to a file, or set of attributes + that will be used to create the final configuration. + ''; + default = { }; + example = literalExample '' + { + "bar/top" = { + monitor = "\''${env:MONITOR:eDP1}"; + width = "100%"; + height = "3%"; + radius = 0; + modules-center = "date"; + }; + + "module/date" = { + type = "internal/date"; + internal = 5; + date = "%d.%m.%y"; + time = "%H:%M"; + label = "%time% %date%"; + }; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + description = "Additional configuration to add."; + default = ""; + example = '' + [module/date] + type = internal/date + interval = 5 + date = "%d.%m.%y" + time = %H:%M + format-prefix-foreground = \''${colors.foreground-alt} + label = %time% %date% + ''; + }; + + script = mkOption { + type = types.lines; + description = '' + This script will be used to start the polybars. + Set all necessary environment variables here and start all bars. + It can be assumed that <command>polybar</command> executable is in the <envar>PATH</envar>. + + Note, this script must start all bars in the background and then terminate. + ''; + example = "polybar bar &"; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + xdg.configFile."polybar/config".source = configFile; + + systemd.user.services.polybar = { + Unit = { + Description = "Polybar status bar"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + X-Restart-Triggers = + [ "${config.xdg.configFile."polybar/config".source}" ]; + }; + + Service = { + Type = "forking"; + Environment = "PATH=${cfg.package}/bin:/run/wrappers/bin"; + ExecStart = + let scriptPkg = pkgs.writeShellScriptBin "polybar-start" cfg.script; + in "${scriptPkg}/bin/polybar-start"; + Restart = "on-failure"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; + +} 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/random-background.nix b/home-manager/modules/services/random-background.nix new file mode 100644 index 00000000000..9deee8deb5c --- /dev/null +++ b/home-manager/modules/services/random-background.nix @@ -0,0 +1,97 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.random-background; + + flags = lib.concatStringsSep " " + ([ "--bg-${cfg.display}" "--no-fehbg" "--randomize" ] + ++ lib.optional (!cfg.enableXinerama) "--no-xinerama"); + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.random-background = { + enable = mkEnableOption "" // { + description = '' + Whether to enable random desktop background. + </para><para> + Note, if you are using NixOS and have set up a custom + desktop manager session for Home Manager, then the session + configuration must have the <option>bgSupport</option> + option set to <literal>true</literal> or the background + image set by this module may be overwritten. + ''; + }; + + imageDirectory = mkOption { + type = types.str; + example = "%h/backgrounds"; + description = '' + The directory of images from which a background should be + chosen. Should be formatted in a way understood by systemd, + e.g., '%h' is the home directory. + ''; + }; + + display = mkOption { + type = types.enum [ "center" "fill" "max" "scale" "tile" ]; + default = "fill"; + description = "Display background images according to this option."; + }; + + interval = mkOption { + default = null; + type = types.nullOr types.str; + example = "1h"; + description = '' + The duration between changing background image, set to null + to only set background when logging in. Should be formatted + as a duration understood by systemd. + ''; + }; + + enableXinerama = mkOption { + default = true; + type = types.bool; + description = '' + Will place a separate image per screen when enabled, + otherwise a single image will be stretched across all + screens. + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge ([ + { + systemd.user.services.random-background = { + Unit = { + Description = "Set random desktop background using feh"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "oneshot"; + ExecStart = "${pkgs.feh}/bin/feh ${flags} ${cfg.imageDirectory}"; + IOSchedulingClass = "idle"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + } + (mkIf (cfg.interval != null) { + systemd.user.timers.random-background = { + Unit = { Description = "Set random desktop background using feh"; }; + + Timer = { OnUnitActiveSec = cfg.interval; }; + + Install = { WantedBy = [ "timers.target" ]; }; + }; + }) + ])); +} diff --git a/home-manager/modules/services/redshift.nix b/home-manager/modules/services/redshift.nix new file mode 100644 index 00000000000..86cbab205f6 --- /dev/null +++ b/home-manager/modules/services/redshift.nix @@ -0,0 +1,164 @@ +# Adapted from Nixpkgs. + +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.redshift; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options.services.redshift = { + enable = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Enable Redshift to change your screen's colour temperature depending on + the time of day. + ''; + }; + + latitude = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Your current latitude, between <literal>-90.0</literal> and + <literal>90.0</literal>. Must be provided along with + longitude. + ''; + }; + + longitude = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Your current longitude, between <literal>-180.0</literal> and + <literal>180.0</literal>. Must be provided along with + latitude. + ''; + }; + + provider = mkOption { + type = types.enum [ "manual" "geoclue2" ]; + default = "manual"; + description = '' + The location provider to use for determining your location. If set to + <literal>manual</literal> you must also provide latitude/longitude. + If set to <literal>geoclue2</literal>, you must also enable the global + geoclue2 service. + ''; + }; + + temperature = { + day = mkOption { + type = types.int; + default = 5500; + description = '' + Colour temperature to use during the day, between + <literal>1000</literal> and <literal>25000</literal> K. + ''; + }; + night = mkOption { + type = types.int; + default = 3700; + description = '' + Colour temperature to use at night, between + <literal>1000</literal> and <literal>25000</literal> K. + ''; + }; + }; + + brightness = { + day = mkOption { + type = types.str; + default = "1"; + description = '' + Screen brightness to apply during the day, + between <literal>0.1</literal> and <literal>1.0</literal>. + ''; + }; + night = mkOption { + type = types.str; + default = "1"; + description = '' + Screen brightness to apply during the night, + between <literal>0.1</literal> and <literal>1.0</literal>. + ''; + }; + }; + + package = mkOption { + type = types.package; + default = pkgs.redshift; + defaultText = literalExample "pkgs.redshift"; + description = '' + redshift derivation to use. + ''; + }; + + tray = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Start the redshift-gtk tray applet. + ''; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "-v" "-m randr" ]; + description = '' + Additional command-line arguments to pass to + <command>redshift</command>. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = cfg.provider == "manual" -> cfg.latitude != null + && cfg.longitude != null; + message = "Must provide services.redshift.latitude and" + + " services.redshift.latitude when" + + " services.redshift.provider is set to \"manual\"."; + }]; + + systemd.user.services.redshift = { + Unit = { + Description = "Redshift colour temperature adjuster"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = let + providerString = if cfg.provider == "manual" then + "${cfg.latitude}:${cfg.longitude}" + else + cfg.provider; + + args = [ + "-l ${providerString}" + "-t ${toString cfg.temperature.day}:${ + toString cfg.temperature.night + }" + "-b ${toString cfg.brightness.day}:${toString cfg.brightness.night}" + ] ++ cfg.extraOptions; + + command = if cfg.tray then "redshift-gtk" else "redshift"; + in "${cfg.package}/bin/${command} ${concatStringsSep " " args}"; + RestartSec = 3; + Restart = "always"; + }; + }; + }; + +} diff --git a/home-manager/modules/services/rsibreak.nix b/home-manager/modules/services/rsibreak.nix new file mode 100644 index 00000000000..77eaa71f958 --- /dev/null +++ b/home-manager/modules/services/rsibreak.nix @@ -0,0 +1,32 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.rsibreak; + +in { + options.services.rsibreak = { + + enable = mkEnableOption "rsibreak"; + + }; + + config = mkIf cfg.enable { + systemd.user.services.rsibreak = { + Unit = { + Description = "RSI break timer"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${pkgs.rsibreak}/bin/rsibreak"; + }; + }; + }; +} diff --git a/home-manager/modules/services/screen-locker.nix b/home-manager/modules/services/screen-locker.nix new file mode 100644 index 00000000000..554d64f9abe --- /dev/null +++ b/home-manager/modules/services/screen-locker.nix @@ -0,0 +1,92 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.screen-locker; + +in { + + options.services.screen-locker = { + enable = mkEnableOption "screen locker for X session"; + + lockCmd = mkOption { + type = types.str; + description = "Locker command to run."; + 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; + description = '' + Inactive time interval in minutes after which session will be locked. + The minimum is 1 minute, and the maximum is 1 hour. + See <link xlink:href="https://linux.die.net/man/1/xautolock"/>. + ''; + }; + + xautolockExtraOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Extra command-line arguments to pass to <command>xautolock</command>. + ''; + }; + + xssLockExtraOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Extra command-line arguments to pass to <command>xss-lock</command>. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.xautolock-session = { + Unit = { + Description = "xautolock, session locker service"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = concatStringsSep " " ([ + "${pkgs.xautolock}/bin/xautolock" + "-time ${toString cfg.inactiveInterval}" + "-locker '${pkgs.systemd}/bin/loginctl lock-session $XDG_SESSION_ID'" + ] ++ optional cfg.enableDetectSleep "-detectsleep" + ++ cfg.xautolockExtraOptions); + }; + }; + + systemd.user.services.xss-lock = { + Unit = { + Description = "xss-lock, session locker service"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = concatStringsSep " " + ([ "${pkgs.xss-lock}/bin/xss-lock" "-s \${XDG_SESSION_ID}" ] + ++ cfg.xssLockExtraOptions ++ [ "-- ${cfg.lockCmd}" ]); + }; + }; + }; + +} diff --git a/home-manager/modules/services/spotifyd.nix b/home-manager/modules/services/spotifyd.nix new file mode 100644 index 00000000000..dfe0ecd318e --- /dev/null +++ b/home-manager/modules/services/spotifyd.nix @@ -0,0 +1,64 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.spotifyd; + + configFile = pkgs.writeText "spotifyd.conf" '' + ${generators.toINI { } cfg.settings} + ''; + +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 = { }; + description = "Configuration for spotifyd"; + example = literalExample '' + { + global = { + username = "Alex"; + password = "foo"; + device_name = "nix"; + }; + } + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + systemd.user.services.spotifyd = { + Unit = { + Description = "spotify daemon"; + Documentation = "https://github.com/Spotifyd/spotifyd"; + }; + + Install.WantedBy = [ "default.target" ]; + + Service = { + ExecStart = + "${cfg.package}/bin/spotifyd --no-daemon --config-path ${configFile}"; + Restart = "always"; + RestartSec = 12; + }; + }; + }; +} diff --git a/home-manager/modules/services/stalonetray.nix b/home-manager/modules/services/stalonetray.nix new file mode 100644 index 00000000000..cca60498963 --- /dev/null +++ b/home-manager/modules/services/stalonetray.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.stalonetray; + +in { + options = { + services.stalonetray = { + enable = mkEnableOption "Stalonetray system tray"; + + package = mkOption { + default = pkgs.stalonetray; + defaultText = literalExample "pkgs.stalonetray"; + type = types.package; + example = literalExample "pkgs.stalonetray"; + description = "The package to use for the Stalonetray binary."; + }; + + config = mkOption { + type = with types; attrsOf (nullOr (either str (either bool int))); + description = '' + Stalonetray configuration as a set of attributes. + ''; + default = { }; + example = { + geometry = "3x1-600+0"; + decorations = null; + icon_size = 30; + sticky = true; + background = "#cccccc"; + }; + }; + + extraConfig = mkOption { + type = types.lines; + description = "Additional configuration lines for stalonetrayrc."; + default = ""; + example = '' + geometry 3x1-600+0 + decorations none + icon_size 30 + sticky true + background "#cccccc" + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ cfg.package ]; + + systemd.user.services.stalonetray = { + Unit = { + Description = "Stalonetray system tray"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + ExecStart = "${cfg.package}/bin/stalonetray"; + Restart = "on-failure"; + }; + }; + } + + (mkIf (cfg.config != { }) { + home.file.".stalonetrayrc".text = let + valueToString = v: + if isBool v then + (if v then "true" else "false") + else if (v == null) then + "none" + else + ''"${toString v}"''; + in concatStrings (mapAttrsToList (k: v: '' + ${k} ${valueToString v} + '') cfg.config); + }) + + (mkIf (cfg.extraConfig != "") { + home.file.".stalonetrayrc".text = cfg.extraConfig; + }) + ]); +} diff --git a/home-manager/modules/services/status-notifier-watcher.nix b/home-manager/modules/services/status-notifier-watcher.nix new file mode 100644 index 00000000000..ed0537e22e1 --- /dev/null +++ b/home-manager/modules/services/status-notifier-watcher.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.status-notifier-watcher; + +in { + meta.maintainers = [ maintainers.pltanton ]; + + options = { + services.status-notifier-watcher = { + enable = mkEnableOption "Status Notifier Watcher"; + + package = mkOption { + default = pkgs.haskellPackages.status-notifier-item; + defaultText = + literalExample "pkgs.haskellPackages.status-notifier-item"; + type = types.package; + example = literalExample "pkgs.haskellPackages.status-notifier-item"; + description = + "The package to use for the status notifier watcher binary."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.status-notifier-watcher = { + Unit = { + Description = "SNI watcher"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + Before = [ "taffybar.service" ]; + }; + + 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/sxhkd.nix b/home-manager/modules/services/sxhkd.nix new file mode 100644 index 00000000000..d9f0a968515 --- /dev/null +++ b/home-manager/modules/services/sxhkd.nix @@ -0,0 +1,86 @@ +{config, lib, pkgs, ...}: + +with lib; + +let + + cfg = config.services.sxhkd; + + keybindingsStr = concatStringsSep "\n" ( + mapAttrsToList (hotkey: command: + optionalString (command != null) '' + ${hotkey} + ${command} + '' + ) + cfg.keybindings + ); + +in + +{ + options.services.sxhkd = { + enable = mkEnableOption "simple X hotkey daemon"; + + keybindings = mkOption { + type = types.attrsOf (types.nullOr types.str); + default = {}; + description = "An attribute set that assigns hotkeys to commands."; + example = literalExample '' + { + "super + shift + {r,c}" = "i3-msg {restart,reload}"; + "super + {s,w}" = "i3-msg {stacking,tabbed}"; + } + ''; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = "Additional configuration to add."; + example = literalExample '' + super + {_,shift +} {1-9,0} + i3-msg {workspace,move container to workspace} {1-10} + ''; + }; + + extraPath = mkOption { + default = ""; + type = types.envVar; + description = '' + Additional <envar>PATH</envar> entries to search for commands. + ''; + example = "/home/some-user/bin:/extra/path/bin"; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.sxhkd ]; + + xdg.configFile."sxhkd/sxhkdrc".text = concatStringsSep "\n" [ + keybindingsStr + cfg.extraConfig + ]; + + systemd.user.services.sxhkd = { + Unit = { + Description = "simple X hotkey daemon"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Environment = + "PATH=" + + "${config.home.profileDirectory}/bin" + + optionalString (cfg.extraPath != "") ":" + + cfg.extraPath; + ExecStart = "${pkgs.sxhkd}/bin/sxhkd"; + }; + + Install = { + WantedBy = [ "graphical-session.target" ]; + }; + }; + }; +} diff --git a/home-manager/modules/services/syncthing.nix b/home-manager/modules/services/syncthing.nix new file mode 100644 index 00000000000..4622ac2e941 --- /dev/null +++ b/home-manager/modules/services/syncthing.nix @@ -0,0 +1,70 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.syncthing = { + enable = mkEnableOption "Syncthing continuous file synchronization"; + + tray = mkOption { + type = types.bool; + default = false; + description = "Whether to enable QSyncthingTray service."; + }; + }; + }; + + config = mkMerge [ + (mkIf config.services.syncthing.enable { + home.packages = [ (getOutput "man" pkgs.syncthing) ]; + + systemd.user.services = { + syncthing = { + Unit = { + Description = + "Syncthing - Open Source Continuous File Synchronization"; + Documentation = "man:syncthing(1)"; + After = [ "network.target" ]; + }; + + Service = { + ExecStart = + "${pkgs.syncthing}/bin/syncthing -no-browser -no-restart -logflags=0"; + Restart = "on-failure"; + SuccessExitStatus = [ 3 4 ]; + RestartForceExitStatus = [ 3 4 ]; + }; + + Install = { WantedBy = [ "default.target" ]; }; + }; + }; + }) + + (mkIf config.services.syncthing.tray { + systemd.user.services = { + qsyncthingtray = { + Unit = { + Description = "QSyncthingTray"; + After = [ + "graphical-session-pre.target" + "polybar.service" + "taffybar.service" + "stalonetray.service" + ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${pkgs.qsyncthingtray}/bin/QSyncthingTray"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; + }) + ]; +} diff --git a/home-manager/modules/services/taffybar.nix b/home-manager/modules/services/taffybar.nix new file mode 100644 index 00000000000..5392755423d --- /dev/null +++ b/home-manager/modules/services/taffybar.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.taffybar; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.taffybar = { + enable = mkEnableOption "Taffybar"; + + package = mkOption { + default = pkgs.taffybar; + defaultText = literalExample "pkgs.taffybar"; + type = types.package; + example = literalExample "pkgs.taffybar"; + description = "The package to use for the Taffybar binary."; + }; + }; + }; + + config = mkIf config.services.taffybar.enable { + systemd.user.services.taffybar = { + Unit = { + Description = "Taffybar desktop bar"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + ExecStart = "${cfg.package}/bin/taffybar"; + Restart = "on-failure"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + + xsession.importedVariables = [ "GDK_PIXBUF_MODULE_FILE" ]; + }; +} diff --git a/home-manager/modules/services/tahoe-lafs.nix b/home-manager/modules/services/tahoe-lafs.nix new file mode 100644 index 00000000000..742b779b270 --- /dev/null +++ b/home-manager/modules/services/tahoe-lafs.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.tahoe-lafs = { enable = mkEnableOption "Tahoe-LAFS"; }; + }; + + config = mkIf config.services.tahoe-lafs.enable { + systemd.user.services.tahoe-lafs = { + Unit = { Description = "Tahoe-LAFS"; }; + + Service = { ExecStart = "${pkgs.tahoelafs}/bin/tahoe run -C %h/.tahoe"; }; + }; + }; +} diff --git a/home-manager/modules/services/taskwarrior-sync.nix b/home-manager/modules/services/taskwarrior-sync.nix new file mode 100644 index 00000000000..d16c0681bee --- /dev/null +++ b/home-manager/modules/services/taskwarrior-sync.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.taskwarrior-sync; + +in { + meta.maintainers = with maintainers; [ minijackson pacien ]; + + options.services.taskwarrior-sync = { + enable = mkEnableOption "Taskwarrior periodic sync"; + + frequency = mkOption { + type = types.str; + default = "*:0/5"; + description = '' + How often to run <literal>taskwarrior sync</literal>. This + value is passed to the systemd timer configuration as the + <literal>OnCalendar</literal> option. See + <citerefentry> + <refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum> + </citerefentry> + for more information about the format. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.taskwarrior-sync = { + Unit = { Description = "Taskwarrior sync"; }; + Service = { + CPUSchedulingPolicy = "idle"; + IOSchedulingClass = "idle"; + ExecStart = "${pkgs.taskwarrior}/bin/task synchronize"; + }; + }; + + systemd.user.timers.taskwarrior-sync = { + Unit = { Description = "Taskwarrior periodic sync"; }; + Timer = { + Unit = "taskwarrior-sync.service"; + OnCalendar = cfg.frequency; + }; + Install = { WantedBy = [ "timers.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/udiskie.nix b/home-manager/modules/services/udiskie.nix new file mode 100644 index 00000000000..ca31021cb5c --- /dev/null +++ b/home-manager/modules/services/udiskie.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.udiskie; + + commandArgs = concatStringsSep " " (map (opt: "-" + opt) [ + (if cfg.automount then "a" else "A") + (if cfg.notify then "n" else "N") + ({ + always = "t"; + auto = "s"; + never = "T"; + }.${cfg.tray}) + ] ++ optional config.xsession.preferStatusNotifierItems "--appindicator"); + +in { + meta.maintainers = [ maintainers.rycee ]; + + imports = [ + (mkRemovedOptionModule [ "services" "udiskie" "sni" ] '' + Support for Status Notifier Items is now configured globally through the + + xsession.preferStatusNotifierItems + + option. Please change to use that instead. + '') + ]; + + options = { + services.udiskie = { + enable = mkEnableOption "udiskie mount daemon"; + + automount = mkOption { + type = types.bool; + default = true; + description = "Whether to automatically mount new devices."; + }; + + notify = mkOption { + type = types.bool; + default = true; + description = "Whether to show pop-up notifications."; + }; + + tray = mkOption { + type = types.enum [ "always" "auto" "never" ]; + default = "auto"; + description = '' + Whether to display tray icon. + </para><para> + The options are + <variablelist> + <varlistentry> + <term><literal>always</literal></term> + <listitem><para>Always show tray icon.</para></listitem> + </varlistentry> + <varlistentry> + <term><literal>auto</literal></term> + <listitem><para> + Show tray icon only when there is a device available. + </para></listitem> + </varlistentry> + <varlistentry> + <term><literal>never</literal></term> + <listitem><para>Never show tray icon.</para></listitem> + </varlistentry> + </variablelist> + ''; + }; + }; + }; + + config = mkIf config.services.udiskie.enable { + systemd.user.services.udiskie = { + Unit = { + Description = "udiskie mount daemon"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { ExecStart = "${pkgs.udiskie}/bin/udiskie ${commandArgs}"; }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/unclutter.nix b/home-manager/modules/services/unclutter.nix new file mode 100644 index 00000000000..5e760639591 --- /dev/null +++ b/home-manager/modules/services/unclutter.nix @@ -0,0 +1,61 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let cfg = config.services.unclutter; + +in { + options.services.unclutter = { + + enable = mkEnableOption "unclutter"; + + package = mkOption { + description = "unclutter derivation to use."; + type = types.package; + default = pkgs.unclutter-xfixes; + defaultText = literalExample "pkgs.unclutter-xfixes"; + }; + + timeout = mkOption { + description = "Number of seconds before the cursor is marked inactive."; + type = types.int; + default = 1; + }; + + threshold = mkOption { + description = "Minimum number of pixels considered cursor movement."; + type = types.int; + default = 1; + }; + + extraOptions = mkOption { + description = "More arguments to pass to the unclutter command."; + type = types.listOf types.str; + default = [ ]; + example = [ "exclude-root" "ignore-scrolling" ]; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.unclutter = { + Unit = { + Description = "unclutter"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + ExecStart = '' + ${cfg.package}/bin/unclutter \ + --timeout ${toString cfg.timeout} \ + --jitter ${toString (cfg.threshold - 1)} \ + ${concatMapStrings (x: " --${x}") cfg.extraOptions} + ''; + RestartSec = 3; + Restart = "always"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/unison.nix b/home-manager/modules/services/unison.nix new file mode 100644 index 00000000000..a9cf23fb66e --- /dev/null +++ b/home-manager/modules/services/unison.nix @@ -0,0 +1,121 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.unison; + + pairOf = t: + let list = types.addCheck (types.listOf t) (l: length l == 2); + in list // { description = list.description + " of length 2"; }; + + pairOptions = { + options = { + stateDirectory = mkOption { + type = types.path; + default = "${config.xdg.dataHome}/unison"; + defaultText = "$XDG_DATA_HOME/unison"; + description = '' + Unison state directory to use. + ''; + }; + + commandOptions = mkOption rec { + type = with types; attrsOf str; + apply = mergeAttrs default; + default = { + repeat = "watch"; + sshcmd = "${pkgs.openssh}/bin/ssh"; + ui = "text"; + auto = "true"; + batch = "true"; + log = "false"; # don't log to file, handled by systemd + }; + description = '' + Additional command line options as a dictionary to pass to the + <literal>unison</literal> program. + </para><para> + See + <citerefentry> + <refentrytitle>unison</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + for a list of available options. + ''; + }; + + roots = mkOption { + type = pairOf types.str; + example = literalExample '' + [ + "/home/user/documents" + "ssh://remote/documents" + ] + ''; + description = '' + Pair of roots to synchronise. + ''; + }; + }; + }; + + serialiseArg = key: val: escapeShellArg "-${key}=${escape [ "=" ] val}"; + + serialiseArgs = args: concatStringsSep " " (mapAttrsToList serialiseArg args); + + makeDefs = gen: + mapAttrs' + (name: pairCfg: nameValuePair "unison-pair-${name}" (gen name pairCfg)) + cfg.pairs; + +in { + meta.maintainers = with maintainers; [ pacien ]; + + options.services.unison = { + enable = mkEnableOption "Unison synchronisation"; + + pairs = mkOption { + type = with types; attrsOf (submodule pairOptions); + default = { }; + example = literalExample '' + { + roots = [ + "/home/user/documents" + "ssh://remote/documents" + ]; + } + ''; + description = '' + Unison root pairs to keep synchronised. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services = makeDefs (name: pairCfg: { + Unit = { + Description = "Unison pair sync (${name})"; + # Retry forever, useful in case of network disruption. + StartLimitIntervalSec = 0; + }; + + Service = { + Restart = "always"; + RestartSec = 60; + + CPUSchedulingPolicy = "idle"; + IOSchedulingClass = "idle"; + + Environment = [ "UNISON='${toString pairCfg.stateDirectory}'" ]; + ExecStart = '' + ${pkgs.unison}/bin/unison \ + ${serialiseArgs pairCfg.commandOptions} \ + ${strings.concatMapStringsSep " " escapeShellArg pairCfg.roots} + ''; + }; + + Install = { WantedBy = [ "default.target" ]; }; + }); + }; +} diff --git a/home-manager/modules/services/window-managers/awesome.nix b/home-manager/modules/services/window-managers/awesome.nix new file mode 100644 index 00000000000..d2e2903f83b --- /dev/null +++ b/home-manager/modules/services/window-managers/awesome.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession.windowManager.awesome; + awesome = cfg.package; + getLuaPath = lib: dir: "${lib}/${dir}/lua/${pkgs.luaPackages.lua.luaversion}"; + makeSearchPath = lib.concatMapStrings (path: + " --search ${getLuaPath path "share"}" + + " --search ${getLuaPath path "lib"}"); + +in { + options = { + xsession.windowManager.awesome = { + enable = mkEnableOption "Awesome window manager."; + + package = mkOption { + type = types.package; + default = pkgs.awesome; + defaultText = literalExample "pkgs.awesome"; + description = "Package to use for running the Awesome WM."; + }; + + luaModules = mkOption { + default = [ ]; + type = types.listOf types.package; + description = '' + List of lua packages available for being + used in the Awesome configuration. + ''; + example = literalExample "[ luaPackages.oocairo ]"; + }; + + noArgb = mkOption { + default = false; + type = types.bool; + description = '' + Disable client transparency support, which can be greatly + detrimental to performance in some setups + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ awesome ]; + xsession.windowManager.command = "${awesome}/bin/awesome " + + optionalString cfg.noArgb "--no-argb " + makeSearchPath cfg.luaModules; + }; +} diff --git a/home-manager/modules/services/window-managers/bspwm/default.nix b/home-manager/modules/services/window-managers/bspwm/default.nix new file mode 100644 index 00000000000..9ea5adbc880 --- /dev/null +++ b/home-manager/modules/services/window-managers/bspwm/default.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession.windowManager.bspwm; + bspwm = cfg.package; + + camelToSnake = s: + builtins.replaceStrings lib.upperChars (map (c: "_${c}") lib.lowerChars) s; + + formatConfig = n: v: + let + formatList = x: + if isList x then + throw "can not convert 2-dimensional lists to bspwm format" + else + formatValue x; + + formatValue = v: + if isBool v then + (if v then "true" else "false") + else if isList v then + concatMapStringsSep ", " formatList v + else if isString v then + "${lib.strings.escapeShellArg v}" + else + toString v; + in "bspc config ${n} ${formatValue v}"; + + formatMonitors = n: v: "bspc monitor ${n} -d ${concatStringsSep " " v}"; + + formatRules = target: directiveOptions: + let + formatDirective = n: v: + if isBool v then + (if v then "${camelToSnake n}=on" else "${camelToSnake n}=off") + else if (n == "desktop" || n == "node") then + "${camelToSnake n}='${v}'" + else + "${camelToSnake n}=${lib.strings.escapeShellArg v}"; + + directives = + filterAttrs (n: v: v != null && !(lib.strings.hasPrefix "_" n)) + directiveOptions; + directivesStr = builtins.concatStringsSep " " + (mapAttrsToList formatDirective directives); + in "bspc rule -a ${target} ${directivesStr}"; + + formatStartupPrograms = map (s: "${s} &"); + +in { + options = import ./options.nix { + inherit pkgs; + inherit lib; + }; + + config = mkIf cfg.enable { + home.packages = [ bspwm ]; + xsession.windowManager.command = let + configFile = pkgs.writeShellScript "bspwmrc" (concatStringsSep "\n" + ((mapAttrsToList formatMonitors cfg.monitors) + ++ (mapAttrsToList formatConfig cfg.settings) + ++ (mapAttrsToList formatRules cfg.rules) ++ ['' + # java gui fixes + export _JAVA_AWT_WM_NONREPARENTING=1 + bspc rule -a sun-awt-X11-XDialogPeer state=floating + ''] ++ [ cfg.extraConfig ] + ++ (formatStartupPrograms cfg.startupPrograms))); + configCmdOpt = optionalString (cfg.settings != null) "-c ${configFile}"; + in "${cfg.package}/bin/bspwm ${configCmdOpt}"; + }; +} diff --git a/home-manager/modules/services/window-managers/bspwm/options.nix b/home-manager/modules/services/window-managers/bspwm/options.nix new file mode 100644 index 00000000000..58a58a1a782 --- /dev/null +++ b/home-manager/modules/services/window-managers/bspwm/options.nix @@ -0,0 +1,214 @@ +{ pkgs, lib }: + +with lib; + +let + + rule = types.submodule { + options = { + monitor = mkOption { + type = types.nullOr types.str; + default = null; + description = "The monitor where the rule should be applied."; + example = "HDMI-0"; + }; + + desktop = mkOption { + type = types.nullOr types.str; + default = null; + description = "The desktop where the rule should be applied."; + example = "^8"; + }; + + node = mkOption { + type = types.nullOr types.str; + default = null; + description = "The node where the rule should be applied."; + example = "1"; + }; + + state = mkOption { + type = types.nullOr + (types.enum [ "tiled" "pseudo_tiled" "floating" "fullscreen" ]); + default = null; + description = "The state in which a new window should spawn."; + example = "floating"; + }; + + layer = mkOption { + type = types.nullOr (types.enum [ "below" "normal" "above" ]); + default = null; + description = "The layer where a new window should spawn."; + example = "above"; + }; + + splitDir = mkOption { + type = types.nullOr (types.enum [ "north" "west" "south" "east" ]); + default = null; + description = "The direction where the container is going to be split."; + example = "south"; + }; + + splitRatio = mkOption { + type = types.nullOr types.float; + default = null; + description = '' + The ratio between the new window and the previous existing window in + the desktop. + ''; + example = 0.65; + }; + + hidden = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the node should occupy any space."; + example = true; + }; + + sticky = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the node should stay on the focused desktop."; + example = true; + }; + + private = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Whether the node should stay in the same tiling position and size. + ''; + example = true; + }; + + locked = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Whether the node should ignore <command>node --close</command> + messages. + ''; + example = true; + }; + + marked = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the node will be marked for deferred actions."; + example = true; + }; + + center = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Whether the node will be put in the center, in floating mode. + ''; + example = true; + }; + + follow = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether focus should follow the node when it is moved."; + example = true; + }; + + manage = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Whether the window should be managed by bspwm. If false, the window + will be ignored by bspwm entirely. This is useful for overlay apps, + e.g. screenshot tools. + ''; + example = true; + }; + + focus = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the node should gain focus on creation."; + example = true; + }; + + border = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether the node should have border."; + example = true; + }; + }; + }; + +in { + xsession.windowManager.bspwm = { + enable = mkEnableOption "bspwm window manager."; + + package = mkOption { + type = types.package; + default = pkgs.bspwm; + defaultText = literalExample "pkgs.bspwm"; + description = "bspwm package to use."; + example = literalExample "pkgs.bspwm-unstable"; + }; + + settings = mkOption { + type = with types; + let primitive = either bool (either int (either float str)); + in attrsOf (either primitive (listOf primitive)); + default = { }; + description = "bspwm configuration"; + example = { + "border_width" = 2; + "split_ratio" = 0.52; + "gapless_monocle" = true; + }; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional configuration to add."; + example = '' + bspc subscribe all > ~/bspc-report.log & + ''; + }; + + monitors = mkOption { + type = types.attrsOf (types.listOf types.str); + default = { }; + description = "bspc monitor configurations"; + example = { "HDMI-0" = [ "web" "terminal" "III" "IV" ]; }; + }; + + rules = mkOption { + type = types.attrsOf rule; + default = { }; + description = "bspc rules"; + example = literalExample '' + { + "Gimp" = { + desktop = "^8"; + state = "floating"; + follow = true; + }; + "Kupfer.py" = { + focus = true; + }; + "Screenkey" = { + manage = false; + }; + } + ''; + }; + + startupPrograms = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Programs to be executed during startup."; + example = [ "numlockx on" "tilda" ]; + }; + }; +} 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/xmonad.nix b/home-manager/modules/services/window-managers/xmonad.nix new file mode 100644 index 00000000000..7be03874a89 --- /dev/null +++ b/home-manager/modules/services/window-managers/xmonad.nix @@ -0,0 +1,101 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession.windowManager.xmonad; + + xmonad = pkgs.xmonad-with-packages.override { + ghcWithPackages = cfg.haskellPackages.ghcWithPackages; + packages = self: + cfg.extraPackages self ++ optionals cfg.enableContribAndExtras [ + self.xmonad-contrib + self.xmonad-extras + ]; + }; + +in { + options = { + xsession.windowManager.xmonad = { + enable = mkEnableOption "xmonad window manager"; + + haskellPackages = mkOption { + default = pkgs.haskellPackages; + defaultText = literalExample "pkgs.haskellPackages"; + example = literalExample "pkgs.haskell.packages.ghc784"; + description = '' + The <varname>haskellPackages</varname> used to build xmonad + and other packages. This can be used to change the GHC + version used to build xmonad and the packages listed in + <varname>extraPackages</varname>. + ''; + }; + + extraPackages = mkOption { + default = self: [ ]; + defaultText = "self: []"; + example = literalExample '' + haskellPackages: [ + haskellPackages.xmonad-contrib + haskellPackages.monad-logger + ] + ''; + description = '' + Extra packages available to GHC when rebuilding xmonad. The + value must be a function which receives the attribute set + defined in <varname>haskellPackages</varname> as the sole + argument. + ''; + }; + + enableContribAndExtras = mkOption { + default = false; + type = types.bool; + description = "Enable xmonad-{contrib,extras} in xmonad."; + }; + + config = mkOption { + type = types.nullOr types.path; + default = null; + example = literalExample '' + pkgs.writeText "xmonad.hs" ''' + import XMonad + main = xmonad defaultConfig + { terminal = "urxvt" + , modMask = mod4Mask + , borderWidth = 3 + } + ''' + ''; + description = '' + The configuration file to be used for xmonad. This must be + an absolute path or <literal>null</literal> in which case + <filename>~/.xmonad/xmonad.hs</filename> will not be managed + by Home Manager. + ''; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + home.packages = [ (lowPrio xmonad) ]; + xsession.windowManager.command = "${xmonad}/bin/xmonad"; + } + + (mkIf (cfg.config != null) { + home.file.".xmonad/xmonad.hs".source = cfg.config; + home.file.".xmonad/xmonad.hs".onChange = '' + echo "Recompiling xmonad" + $DRY_RUN_CMD ${config.xsession.windowManager.command} --recompile + + # Attempt to restart xmonad if X is running. + if [[ -v DISPLAY ]] ; then + echo "Restarting xmonad" + $DRY_RUN_CMD ${config.xsession.windowManager.command} --restart + fi + ''; + }) + ]); +} diff --git a/home-manager/modules/services/xcape.nix b/home-manager/modules/services/xcape.nix new file mode 100644 index 00000000000..f4f77caa331 --- /dev/null +++ b/home-manager/modules/services/xcape.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.xcape; + +in { + meta.maintainers = [ maintainers.nickhu ]; + + options = { + services.xcape = { + enable = mkEnableOption "xcape"; + + timeout = mkOption { + type = types.nullOr types.int; + default = null; + example = 500; + description = '' + If you hold a key longer than this timeout, xcape will not + generate a key event. Default is 500 ms. + ''; + }; + + mapExpression = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { + Shift_L = "Escape"; + Control_L = "Control_L|O"; + }; + description = '' + The value has the grammar <literal>Key[|OtherKey]</literal>. + </para> + <para> + The list of key names is found in the header file + <filename>X11/keysymdef.h</filename> (remove the + <literal>XK_</literal> prefix). Note that due to limitations + of X11 shifted keys must be specified as a shift key + followed by the key to be pressed rather than the actual + name of the character. For example to generate "{" the + expression <literal>Shift_L|bracketleft</literal> could be + used (assuming that you have a key with "{" above "["). + </para> + <para> + You can also specify keys in decimal (prefix #), octal (#0), + or hexadecimal (#0x). They will be interpreted as keycodes + unless no corresponding key name is found. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.xcape = { + Unit = { + Description = "xcape"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "forking"; + ExecStart = "${pkgs.xcape}/bin/xcape" + + optionalString (cfg.timeout != null) " -t ${toString cfg.timeout}" + + optionalString (cfg.mapExpression != { }) " -e '${ + builtins.concatStringsSep ";" + (attrsets.mapAttrsToList (n: v: "${n}=${v}") cfg.mapExpression) + }'"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/xembed-sni-proxy.nix b/home-manager/modules/services/xembed-sni-proxy.nix new file mode 100644 index 00000000000..ff63d108b77 --- /dev/null +++ b/home-manager/modules/services/xembed-sni-proxy.nix @@ -0,0 +1,45 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.xembed-sni-proxy; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.xembed-sni-proxy = { + enable = mkEnableOption "XEmbed SNI Proxy"; + + package = mkOption { + type = types.package; + default = pkgs.plasma-workspace; + defaultText = literalExample "pkgs.plasma-workspace"; + description = '' + Package containing the <command>xembedsniproxy</command> + program. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.xembed-sni-proxy = { + Unit = { + Description = "XEmbed SNI Proxy"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Environment = "PATH=${config.home.profileDirectory}/bin"; + ExecStart = "${cfg.package}/bin/xembedsniproxy"; + Restart = "on-abort"; + }; + }; + }; +} diff --git a/home-manager/modules/services/xscreensaver.nix b/home-manager/modules/services/xscreensaver.nix new file mode 100644 index 00000000000..ac6194e70c1 --- /dev/null +++ b/home-manager/modules/services/xscreensaver.nix @@ -0,0 +1,56 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.xscreensaver; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + services.xscreensaver = { + enable = mkEnableOption "XScreenSaver"; + + settings = mkOption { + type = with types; attrsOf (either bool (either int str)); + default = { }; + example = { + mode = "blank"; + lock = false; + fadeTicks = 20; + }; + description = '' + The settings to use for XScreenSaver. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + # To make the xscreensaver-command tool available. + home.packages = [ pkgs.xscreensaver ]; + + xresources.properties = + mapAttrs' (n: nameValuePair "xscreensaver.${n}") cfg.settings; + + systemd.user.services.xscreensaver = { + Unit = { + 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 = { + ExecStart = "${pkgs.xscreensaver}/bin/xscreensaver -no-splash"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/services/xsuspender.nix b/home-manager/modules/services/xsuspender.nix new file mode 100644 index 00000000000..2eb40f5dd34 --- /dev/null +++ b/home-manager/modules/services/xsuspender.nix @@ -0,0 +1,192 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.xsuspender; + + xsuspenderOptions = types.submodule { + options = { + matchWmClassContains = mkOption { + description = "Match windows that wm class contains string."; + type = types.nullOr types.str; + default = null; + }; + + matchWmClassGroupContains = mkOption { + description = "Match windows where wm class group contains string."; + type = types.nullOr types.str; + default = null; + }; + + matchWmNameContains = mkOption { + description = "Match windows where wm name contains string."; + type = types.nullOr types.str; + default = null; + }; + + suspendDelay = mkOption { + description = "Initial suspend delay in seconds."; + type = types.int; + default = 5; + }; + + resumeEvery = mkOption { + description = "Resume interval in seconds."; + type = types.int; + default = 50; + }; + + resumeFor = mkOption { + description = "Resume duration in seconds."; + type = types.int; + default = 5; + }; + + execSuspend = mkOption { + description = '' + Before suspending, execute this shell script. If it fails, + abort suspension. + ''; + type = types.nullOr types.str; + default = null; + example = ''echo "suspending window $XID of process $PID"''; + }; + + execResume = mkOption { + description = '' + Before resuming, execute this shell script. Resume the + process regardless script failure. + ''; + type = types.nullOr types.str; + default = null; + example = "echo resuming ..."; + }; + + sendSignals = mkOption { + description = '' + Whether to send SIGSTOP / SIGCONT signals or not. + If false just the exec scripts are run. + ''; + type = types.bool; + default = true; + }; + + suspendSubtreePattern = mkOption { + description = + "Also suspend descendant processes that match this regex."; + type = types.nullOr types.str; + default = null; + }; + + onlyOnBattery = mkOption { + description = "Whether to enable process suspend only on battery."; + type = types.bool; + default = false; + }; + + autoSuspendOnBattery = mkOption { + description = '' + Whether to auto-apply rules when switching to battery + power even if the window(s) didn't just lose focus. + ''; + type = types.bool; + default = true; + }; + + downclockOnBattery = mkOption { + description = '' + Limit CPU consumption for this factor when on battery power. + Value 1 means 50% decrease, 2 means 66%, 3 means 75% etc. + ''; + type = types.int; + default = 0; + }; + }; + }; + +in { + meta.maintainers = [ maintainers.offline ]; + + options = { + services.xsuspender = { + enable = mkEnableOption "XSuspender"; + + defaults = mkOption { + description = "XSuspender defaults."; + type = xsuspenderOptions; + default = { }; + }; + + rules = mkOption { + description = "Attribute set of XSuspender rules."; + type = types.attrsOf xsuspenderOptions; + default = { }; + example = { + Chromium = { + suspendDelay = 10; + matchWmClassContains = "chromium-browser"; + suspendSubtreePattern = "chromium"; + }; + }; + }; + + debug = mkOption { + description = "Whether to enable debug output."; + type = types.bool; + default = false; + }; + + iniContent = mkOption { + type = types.attrsOf types.attrs; + internal = true; + }; + }; + }; + + config = mkIf cfg.enable { + services.xsuspender.iniContent = let + mkSection = values: + filterAttrs (_: v: v != null) { + match_wm_class_contains = values.matchWmClassContains; + match_wm_class_group_contains = values.matchWmClassGroupContains; + match_wm_name_contains = values.matchWmNameContains; + suspend_delay = values.suspendDelay; + resume_every = values.resumeEvery; + resume_for = values.resumeFor; + exec_suspend = values.execSuspend; + exec_resume = values.execResume; + send_signals = values.sendSignals; + suspend_subtree_pattern = values.suspendSubtreePattern; + only_on_battery = values.onlyOnBattery; + auto_suspend_on_battery = values.autoSuspendOnBattery; + downclock_on_battery = values.downclockOnBattery; + }; + in { + Default = mkSection cfg.defaults; + } // mapAttrs (_: mkSection) cfg.rules; + + # To make the xsuspender tool available. + home.packages = [ pkgs.xsuspender ]; + + xdg.configFile."xsuspender.conf".text = generators.toINI { } cfg.iniContent; + + systemd.user.services.xsuspender = { + Unit = { + Description = "XSuspender"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + X-Restart-Triggers = + [ "${config.xdg.configFile."xsuspender.conf".source}" ]; + }; + + Service = { + ExecStart = "${pkgs.xsuspender}/bin/xsuspender"; + Environment = mkIf cfg.debug [ "G_MESSAGE_DEBUG=all" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }; +} diff --git a/home-manager/modules/systemd-activate.rb b/home-manager/modules/systemd-activate.rb new file mode 100644 index 00000000000..31d06d8fc19 --- /dev/null +++ b/home-manager/modules/systemd-activate.rb @@ -0,0 +1,216 @@ +require 'set' +require 'open3' + +@dry_run = ENV['DRY_RUN'] +@verbose = ENV['VERBOSE'] + +UnitsDir = 'home-files/.config/systemd/user' + +# 1. Stop all services from the old generation that are not present in the new generation. +# 2. Ensure all services from the new generation that are wanted by active targets are running: +# - Start services that are not already running. +# - Restart services whose unit config files have changed between generations. +# 3. If any services were (re)started, wait 'start_timeout_ms' and report services +# that failed to start. This helps debugging quickly failing services. +# +# Whenever service failures are detected, show the output of +# 'systemd --user status' for the affected services. +# +def setup_services(old_gen_path, new_gen_path, start_timeout_ms_string) + start_timeout_ms = start_timeout_ms_string.to_i + + old_units_path = File.join(old_gen_path, UnitsDir) unless old_gen_path.empty? + new_units_path = File.join(new_gen_path, UnitsDir) + + old_services = get_services(old_units_path) + new_services = get_services(new_units_path) + + 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 = 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 = changed_services + to_start = get_inactive_units(services_to_run - to_restart) + + raise "daemon-reload failed" unless run_cmd('systemctl', '--user', 'daemon-reload') + + # 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) + 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 + + 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_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) + if failed.empty? + print_service_msg("All services are running", services_to_run) + else + puts + puts "Error. These services failed to start:", failed + show_failed_services_status(failed) + exit 1 + end + end + end +end + +def get_services(dir) + services = get_service_files(dir) if dir && Dir.exists?(dir) + Set.new(services) +end + +def get_service_files(dir) + Dir.chdir(dir) { Dir['*[^@].{service,socket,timer}'] } +end + +def get_changed_services(dir_a, dir_b, services) + services.select do |service| + a = File.join(dir_a, service) + b = File.join(dir_b, service) + (File.size(a) != File.size(b)) || (File.read(a) != File.read(b)) + end +end + +TargetDirRegexp = /^(.*\.target)\.wants$/ + +# @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) + active_units = active_targets.map do |target| + get_service_files(File.join(units_dir, "#{target}.wants")) + end.flatten + Set.new(active_units) +end + +# @return true on success +def run_cmd(*cmd) + print_cmd cmd + @dry_run || system(*cmd) +end + +def systemctl_action(cmd, services) + return if services.empty? + + verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing" + puts "#{verb}: #{services.join(' ')}" + + cmd = ['systemctl', '--user', cmd, *services] + if @dry_run + puts cmd.join(' ') + return + end + + output, status = Open3.capture2e(*cmd) + print output + # Show status for failed services + unless status.success? + # Due to a bug in systemd, the '--user' argument is not always provided + output.scan(/systemctl (?:--user )?(status .*?)['"]/).flatten.each do |status_cmd| + puts + run_cmd("systemctl --user #{status_cmd}") + end + exit 1 + end +end + +def systemctl(*cmd) + output, _ = Open3.capture2('systemctl', '--user', *cmd) + output +end + +def print_cmd(cmd) + puts [*cmd].join(' ') if @verbose || @dry_run +end + +def get_active_units(units) + filter_units(units) { |state| state == 'active' } +end + +def get_inactive_units(units) + filter_units(units) { |state| state != 'active' } +end + +def get_failed_units(units) + filter_units(units) { |state| state == 'failed' } +end + +def filter_units(units) + return [] if units.empty? + 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| + types[i] == 'Type=oneshot' + end +end + +def get_restricted_units(units) + infos = systemctl('show', '-p', 'RefuseManualStart', '-p', 'RefuseManualStop', *units) + .split("\n\n") + no_manual_start = [] + no_manual_stop = [] + infos.zip(units).each do |info, unit| + no_start, no_stop = info.split("\n") + no_manual_start << unit if no_start.end_with?('yes') + no_manual_stop << unit if no_stop.end_with?('yes') + 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 + +def wait_and_get_failed_services(services, start_timeout_ms) + puts "Waiting #{start_timeout_ms} ms for services to fail" + # Force the previous message to always be visible before sleeping + STDOUT.flush + sleep(start_timeout_ms / 1000.0) + get_failed_units(services) +end + +def show_failed_services_status(services) + puts + services.each do |service| + run_cmd('systemctl', '--user', 'status', service) + puts + end +end + +def print_service_msg(msg, services) + return if services.empty? + if @verbose + puts "#{msg}:", services.to_a + else + puts msg + 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-activate.sh b/home-manager/modules/systemd-activate.sh new file mode 100644 index 00000000000..1c464693cfc --- /dev/null +++ b/home-manager/modules/systemd-activate.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +function isStartable() { + local service="$1" + [[ $(systemctl --user show -p RefuseManualStart "$service") == *=no ]] +} + +function isStoppable() { + if [[ -v oldGenPath ]] ; then + local service="$1" + [[ $(systemctl --user show -p RefuseManualStop "$service") == *=no ]] + fi +} + +function systemdPostReload() { + local workDir + workDir="$(mktemp -d)" + + if [[ -v oldGenPath ]] ; then + local oldUserServicePath="$oldGenPath/home-files/.config/systemd/user" + fi + + local newUserServicePath="$newGenPath/home-files/.config/systemd/user" + local oldServiceFiles="$workDir/old-files" + local newServiceFiles="$workDir/new-files" + local servicesDiffFile="$workDir/diff-files" + + if [[ ! (-v oldUserServicePath && -d "$oldUserServicePath") \ + && ! -d "$newUserServicePath" ]]; then + return + fi + + if [[ ! (-v oldUserServicePath && -d "$oldUserServicePath") ]]; then + touch "$oldServiceFiles" + else + find "$oldUserServicePath" \ + -maxdepth 1 -name '*.service' -exec basename '{}' ';' \ + | sort \ + > "$oldServiceFiles" + fi + + if [[ ! -d "$newUserServicePath" ]]; then + touch "$newServiceFiles" + else + find "$newUserServicePath" \ + -maxdepth 1 -name '*.service' -exec basename '{}' ';' \ + | sort \ + > "$newServiceFiles" + fi + + diff \ + --new-line-format='+%L' \ + --old-line-format='-%L' \ + --unchanged-line-format=' %L' \ + "$oldServiceFiles" "$newServiceFiles" \ + > "$servicesDiffFile" || true + + local -a maybeRestart=( $(grep '^ ' "$servicesDiffFile" | cut -c2-) ) + local -a maybeStop=( $(grep '^-' "$servicesDiffFile" | cut -c2-) ) + local -a maybeStart=( $(grep '^+' "$servicesDiffFile" | cut -c2-) ) + local -a toRestart=( ) + local -a toStop=( ) + local -a toStart=( ) + + for f in "${maybeRestart[@]}" ; do + if isStoppable "$f" \ + && isStartable "$f" \ + && systemctl --quiet --user is-active "$f" \ + && ! cmp --quiet \ + "$oldUserServicePath/$f" \ + "$newUserServicePath/$f" ; then + toRestart+=("$f") + fi + done + + for f in "${maybeStop[@]}" ; do + if isStoppable "$f" ; then + toStop+=("$f") + fi + done + + for f in "${maybeStart[@]}" ; do + if isStartable "$f" ; then + toStart+=("$f") + fi + done + + rm -r "$workDir" + + local sugg="" + + if [[ -n "${toRestart[@]}" ]] ; then + sugg="${sugg}systemctl --user restart ${toRestart[@]}\n" + fi + + if [[ -n "${toStop[@]}" ]] ; then + sugg="${sugg}systemctl --user stop ${toStop[@]}\n" + fi + + if [[ -n "${toStart[@]}" ]] ; then + sugg="${sugg}systemctl --user start ${toStart[@]}\n" + fi + + if [[ -n "$sugg" ]] ; then + echo "Suggested commands:" + echo -n -e "$sugg" + fi +} + +oldGenPath="$1" +newGenPath="$2" + +$DRY_RUN_CMD systemctl --user daemon-reload +systemdPostReload diff --git a/home-manager/modules/systemd.nix b/home-manager/modules/systemd.nix new file mode 100644 index 00000000000..66fffadf737 --- /dev/null +++ b/home-manager/modules/systemd.nix @@ -0,0 +1,268 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.systemd.user; + + enabled = cfg.services != {} + || cfg.sockets != {} + || cfg.targets != {} + || cfg.timers != {} + || cfg.paths != {} + || cfg.sessionVariables != {}; + + toSystemdIni = generators.toINI { + mkKeyValue = key: value: + let + value' = + if isBool value then (if value then "true" else "false") + else toString value; + in + "${key}=${value'}"; + }; + + buildService = style: name: serviceCfg: + let + filename = "${name}.${style}"; + pathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"] + ["-" "-" "-" "" "" ] + filename; + + # Needed because systemd derives unit names from the ultimate + # link target. + source = pkgs.writeTextFile { + name = pathSafeName; + text = toSystemdIni serviceCfg; + destination = "/${filename}"; + } + "/${filename}"; + + wantedBy = target: + { + name = "systemd/user/${target}.wants/${filename}"; + value = { inherit source; }; + }; + in + singleton { + name = "systemd/user/${filename}"; + value = { inherit source; }; + } + ++ + map wantedBy (serviceCfg.Install.WantedBy or []); + + buildServices = style: serviceCfgs: + concatLists (mapAttrsToList (buildService style) serviceCfgs); + + servicesStartTimeoutMs = builtins.toString cfg.servicesStartTimeoutMs; + + unitType = unitKind: with types; + let + primitive = either bool (either int str); + in + attrsOf (attrsOf (attrsOf (either primitive (listOf primitive)))) + // { + description = "systemd ${unitKind} unit configuration"; + }; + + unitDescription = type: '' + Definition of systemd per-user ${type} units. Attributes are + merged recursively. + </para><para> + Note that the attributes follow the capitalization and naming used + by systemd. More details can be found in + <citerefentry> + <refentrytitle>systemd.${type}</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry>. + ''; + + unitExample = type: literalExample '' + { + ${toLower type}-name = { + Unit = { + Description = "Example description"; + Documentation = [ "man:example(1)" "man:example(5)" ]; + }; + + ${type} = { + … + }; + } + }; + ''; + + sessionVariables = mkIf (cfg.sessionVariables != {}) { + "environment.d/10-home-manager.conf".text = + concatStringsSep "\n" ( + mapAttrsToList (n: v: "${n}=${toString v}") cfg.sessionVariables + ) + "\n"; + }; + +in + +{ + meta.maintainers = [ maintainers.rycee ]; + + options = { + systemd.user = { + systemctlPath = mkOption { + default = "${pkgs.systemd}/bin/systemctl"; + defaultText = "\${pkgs.systemd}/bin/systemctl"; + type = types.str; + description = '' + Absolute path to the <command>systemctl</command> tool. This + option may need to be set if running Home Manager on a + non-NixOS distribution. + ''; + }; + + services = mkOption { + default = {}; + type = unitType "service"; + description = unitDescription "service"; + example = unitExample "Service"; + }; + + sockets = mkOption { + default = {}; + type = unitType "socket"; + description = unitDescription "socket"; + example = unitExample "Socket"; + }; + + targets = mkOption { + default = {}; + type = unitType "target"; + description = unitDescription "target"; + example = unitExample "Target"; + }; + + timers = mkOption { + default = {}; + type = unitType "timer"; + description = unitDescription "timer"; + example = unitExample "Timer"; + }; + + paths = mkOption { + default = {}; + type = unitType "path"; + description = unitDescription "path"; + example = unitExample "Path"; + }; + + startServices = mkOption { + default = false; + type = types.bool; + description = '' + Start all services that are wanted by active targets. + Additionally, stop obsolete services from the previous + generation. + ''; + }; + + servicesStartTimeoutMs = mkOption { + default = 0; + type = types.int; + description = '' + How long to wait for started services to fail until their + start is considered successful. + ''; + }; + + sessionVariables = mkOption { + default = {}; + type = with types; attrsOf (either int str); + example = { EDITOR = "vim"; }; + description = '' + Environment variables that will be set for the user session. + The variable values must be as described in + <citerefentry> + <refentrytitle>environment.d</refentrytitle> + <manvolnum>5</manvolnum> + </citerefentry>. + ''; + }; + }; + }; + + config = mkMerge [ + { + assertions = [ + { + assertion = enabled -> pkgs.stdenv.isLinux; + message = + let + names = concatStringsSep ", " ( + attrNames ( + cfg.services // cfg.sockets // cfg.targets // cfg.timers // cfg.paths // cfg.sessionVariables + ) + ); + in + "Must use Linux for modules that require systemd: " + names; + } + ]; + } + + # If we run under a Linux system we assume that systemd is + # available, in particular we assume that systemctl is in PATH. + (mkIf pkgs.stdenv.isLinux { + xdg.configFile = mkMerge [ + (listToAttrs ( + (buildServices "service" cfg.services) + ++ + (buildServices "socket" cfg.sockets) + ++ + (buildServices "target" cfg.targets) + ++ + (buildServices "timer" cfg.timers) + ++ + (buildServices "path" cfg.paths) + )) + + sessionVariables + ]; + + # Run systemd service reload if user is logged in. If we're + # 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"] ( + let + autoReloadCmd = '' + ${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \ + "''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}" + ''; + + legacyReloadCmd = '' + bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath" + ''; + + ensureRuntimeDir = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"; + + systemctl = "${ensureRuntimeDir} ${cfg.systemctlPath}"; + in + '' + systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) + + if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then + if [[ $systemdStatus == 'degraded' ]]; then + warnEcho "The user systemd session is degraded:" + ${systemctl} --user --no-pager --state=failed + warnEcho "Attempting to reload services anyway..." + fi + + ${ensureRuntimeDir} \ + PATH=${dirOf cfg.systemctlPath}:$PATH \ + ${if cfg.startServices then autoReloadCmd else legacyReloadCmd} + else + echo "User systemd daemon not running. Skipping reload." + fi + + unset systemdStatus + '' + ); + }) + ]; +} 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/xcursor.nix b/home-manager/modules/xcursor.nix new file mode 100644 index 00000000000..63ceef387df --- /dev/null +++ b/home-manager/modules/xcursor.nix @@ -0,0 +1,83 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession.pointerCursor; + + cursorType = types.submodule { + options = { + package = mkOption { + type = types.package; + example = literalExample "pkgs.vanilla-dmz"; + description = "Package providing the cursor theme."; + }; + + name = mkOption { + type = types.str; + example = "Vanilla-DMZ"; + description = "The cursor name within the package."; + }; + + size = mkOption { + type = types.int; + default = 32; + example = 64; + description = "The cursor size."; + }; + + defaultCursor = mkOption { + type = types.str; + default = "left_ptr"; + example = "X_cursor"; + description = "The default cursor file to use within the package."; + }; + }; + }; + +in { + meta.maintainers = [ maintainers.league ]; + + options = { + xsession.pointerCursor = mkOption { + type = types.nullOr cursorType; + default = null; + description = '' + The X cursor theme and settings. The package + <varname>xorg.xcursorthemes</varname> contains cursors named + whiteglass, redglass, and handhelds. The package + <varname>vanilla-dmz</varname> contains cursors named Vanilla-DMZ + and Vanilla-DMZ-AA. Note: handhelds does not seem to work at + custom sizes. + ''; + }; + }; + + config = mkIf (cfg != null) { + + home.packages = [ cfg.package ]; + + xsession.initExtra = '' + ${pkgs.xorg.xsetroot}/bin/xsetroot -xcf ${cfg.package}/share/icons/${cfg.name}/cursors/${cfg.defaultCursor} ${ + toString cfg.size + } + ''; + + xresources.properties = { + "Xcursor.theme" = cfg.name; + "Xcursor.size" = cfg.size; + }; + + gtk.gtk2.extraConfig = '' + gtk-cursor-theme-name="${cfg.name}" + gtk-cursor-theme-size=${toString cfg.size} + ''; + + gtk.gtk3.extraConfig = { + "gtk-cursor-theme-name" = cfg.name; + "gtk-cursor-theme-size" = cfg.size; + }; + + }; +} diff --git a/home-manager/modules/xresources.nix b/home-manager/modules/xresources.nix new file mode 100644 index 00000000000..dc59e50c46e --- /dev/null +++ b/home-manager/modules/xresources.nix @@ -0,0 +1,92 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xresources; + + formatLine = n: v: + let + formatList = x: + if isList x then + throw "can not convert 2-dimensional lists to Xresources format" + else + formatValue x; + + formatValue = v: + if isBool v then + (if v then "true" else "false") + else if isList v then + concatMapStringsSep ", " formatList v + else + toString v; + in "${n}: ${formatValue v}"; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + xresources.properties = mkOption { + type = with types; + let + prim = either bool (either int str); + entry = either prim (listOf prim); + in nullOr (attrsOf entry); + default = null; + example = literalExample '' + { + "Emacs*toolBar" = 0; + "XTerm*faceName" = "dejavu sans mono"; + "XTerm*charClass" = [ "37:48" "45-47:48" "58:48" "64:48" "126:48" ]; + } + ''; + description = '' + X server resources that should be set. + Booleans are formatted as "true" or "false" respectively. + List elements are recursively formatted as a string and joined by commas. + All other values are directly formatted using builtins.toString. + Note, that 2-dimensional lists are not supported and specifying one will throw an exception. + If this and all other xresources options are + <code>null</code>, then this feature is disabled and no + <filename>~/.Xresources</filename> link is produced. + ''; + }; + + xresources.extraConfig = mkOption { + type = types.lines; + default = ""; + example = literalExample '' + builtins.readFile ( + pkgs.fetchFromGitHub { + owner = "solarized"; + repo = "xresources"; + rev = "025ceddbddf55f2eb4ab40b05889148aab9699fc"; + sha256 = "0lxv37gmh38y9d3l8nbnsm1mskcv10g3i83j0kac0a2qmypv1k9f"; + } + "/Xresources.dark" + ) + ''; + description = '' + Additional X server resources contents. + If this and all other xresources options are + <code>null</code>, then this feature is disabled and no + <filename>~/.Xresources</filename> link is produced. + ''; + }; + }; + + 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 + ''; + }; + }; +} diff --git a/home-manager/modules/xsession.nix b/home-manager/modules/xsession.nix new file mode 100644 index 00000000000..d32c2849163 --- /dev/null +++ b/home-manager/modules/xsession.nix @@ -0,0 +1,168 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession; + +in { + meta.maintainers = [ maintainers.rycee ]; + + options = { + xsession = { + enable = mkEnableOption "X Session"; + + scriptPath = mkOption { + type = types.str; + default = ".xsession"; + example = ".xsession-hm"; + description = '' + Path, relative <envar>HOME</envar>, where Home Manager + should write the X session script. + ''; + }; + + windowManager.command = mkOption { + type = types.str; + example = literalExample '' + let + xmonad = pkgs.xmonad-with-packages.override { + packages = self: [ self.xmonad-contrib self.taffybar ]; + }; + in + "''${xmonad}/bin/xmonad"; + ''; + description = '' + Window manager start command. + ''; + }; + + preferStatusNotifierItems = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether tray applets should prefer using the Status Notifier + Items (SNI) protocol, commonly called App Indicators. Note, + not all tray applets or status bars support SNI. + ''; + }; + + profileExtra = mkOption { + type = types.lines; + default = ""; + description = "Extra shell commands to run before session start."; + }; + + initExtra = mkOption { + type = types.lines; + default = ""; + description = "Extra shell commands to run during initialization."; + }; + + importedVariables = mkOption { + type = types.listOf (types.strMatching "[a-zA-Z_][a-zA-Z0-9_]*"); + example = [ "GDK_PIXBUF_ICON_LOADER" ]; + visible = false; + description = '' + Environment variables to import into the user systemd + session. The will be available for use by graphical + services. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + xsession.importedVariables = [ + "DBUS_SESSION_BUS_ADDRESS" + "DISPLAY" + "SSH_AUTH_SOCK" + "XAUTHORITY" + "XDG_DATA_DIRS" + "XDG_RUNTIME_DIR" + "XDG_SESSION_ID" + ]; + + systemd.user = { + services = mkIf (config.home.keyboard != null) { + setxkbmap = { + Unit = { + Description = "Set up keyboard in X"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + + Service = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = with config.home.keyboard; + let + args = optional (layout != null) "-layout '${layout}'" + ++ optional (variant != null) "-variant '${variant}'" + ++ optional (model != null) "-model '${model}'" + ++ map (v: "-option '${v}'") options; + in "${pkgs.xorg.setxkbmap}/bin/setxkbmap ${toString args}"; + }; + }; + }; + + # A basic graphical session target for Home Manager. + targets.hm-graphical-session = { + Unit = { + Description = "Home Manager X session"; + Requires = [ "graphical-session-pre.target" ]; + BindsTo = [ "graphical-session.target" ]; + }; + }; + }; + + home.file.".xprofile".text = '' + . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh" + + if [ -e "$HOME/.profile" ]; then + . "$HOME/.profile" + fi + + # If there are any running services from a previous session. + # Need to run this in xprofile because the NixOS xsession + # script starts up graphical-session.target. + systemctl --user stop graphical-session.target graphical-session-pre.target + + ${optionalString (cfg.importedVariables != [ ]) + ("systemctl --user import-environment " + + toString (unique cfg.importedVariables))} + + ${cfg.profileExtra} + + export HM_XPROFILE_SOURCED=1 + ''; + + home.file.${cfg.scriptPath} = { + executable = true; + text = '' + if [ -z "$HM_XPROFILE_SOURCED" ]; then + . ~/.xprofile + fi + unset HM_XPROFILE_SOURCED + + systemctl --user start hm-graphical-session.target + + ${cfg.initExtra} + + ${cfg.windowManager.command} + + systemctl --user stop graphical-session.target + systemctl --user stop graphical-session-pre.target + + # Wait until the units actually stop. + while [ -n "$(systemctl --user --no-legend --state=deactivating list-units)" ]; do + sleep 0.5 + done + ''; + }; + }; +} |