diff options
Diffstat (limited to 'home-manager/modules/programs')
94 files changed, 13343 insertions, 0 deletions
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); + }) + ]); +} |