diff --git a/default.nix b/default.nix new file mode 100644 index 000000000..d9024779c --- /dev/null +++ b/default.nix @@ -0,0 +1,2 @@ +# Simply defer to the home-manager script derivation. +import ./home-manager diff --git a/home-manager/default.nix b/home-manager/default.nix new file mode 100644 index 000000000..d00edbfbd --- /dev/null +++ b/home-manager/default.nix @@ -0,0 +1,39 @@ +{ pkgs }: + +let + + homeManagerExpr = pkgs.writeText "home-manager.nix" '' + { pkgs ? import {}, confPath, modulesPath }: + + let + env = import modulesPath { + configuration = import confPath; + pkgs = pkgs; + }; + in + { + inherit (env) activation-script; + } + ''; + +in + +pkgs.stdenv.mkDerivation { + name = "home-manager"; + + phases = [ "installPhase" ]; + + installPhase = '' + install -v -D -m755 ${./home-manager} $out/bin/home-manager + + substituteInPlace $out/bin/home-manager \ + --subst-var-by bash "${pkgs.bash}" \ + --subst-var-by HOME_MANAGER_EXPR_PATH "${homeManagerExpr}" + ''; + + meta = with pkgs.stdenv.lib; { + description = "A user environment configurator"; + maintainers = [ maintainers.rycee ]; + platforms = platforms.linux; + }; +} diff --git a/home-manager/home-manager b/home-manager/home-manager new file mode 100644 index 000000000..63dac595f --- /dev/null +++ b/home-manager/home-manager @@ -0,0 +1,62 @@ +#!@bash@/bin/bash + +function doRebuild() { + if [[ -z "$1" ]] ; then + echo "Need to provide path to configuration file." + exit 1 + fi + + local wrkdir + wrkdir="$(mktemp -d)" + + nix-build --show-trace \ + "@HOME_MANAGER_EXPR_PATH@" \ + --argstr modulesPath "$HOME/.nixpkgs/home-manager/modules" \ + --argstr confPath "$1" \ + -A activation-script \ + -o "$wrkdir/activate" + + "$wrkdir/activate/libexec/home-activate" + + rm -rv "$wrkdir" +} + +function doListGens() { + ls --color=yes -gG --sort time "/nix/var/nix/gcroots/per-user/$(whoami)" \ + | cut -d' ' -f 4- +} + +function doListPackages() { + local outPath + outPath="$(nix-env -q --out-path | grep -o '/.*home-manager-path$')" + nix-store -q --references "$outPath" | sed 's/[^-]*-//' +} + +function doHelp() { + echo "Usage: $0 {help | rebuild CONF | generations | packages}" + echo + echo "Commands" + echo " help Print this help" + echo " rebuild Rebuild the current environment" + echo " generations List all home environment generations" + echo " packages List all packages installed in home-manager-path" +} + +case $1 in + rebuild) + doRebuild "$2" + ;; + generations) + doListGens + ;; + packages) + doListPackages + ;; + help|--help) + doHelp + ;; + *) + echo "Unknown command: $1" + doHelp + exit 1 +esac diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 000000000..6f42de2f4 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,47 @@ +{ configuration +, pkgs +, lib ? pkgs.stdenv.lib +}: + +let + + modules = [ + ./home-environment.nix + ./programs/bash.nix + ./programs/beets.nix + ./programs/eclipse.nix + ./programs/emacs.nix + ./programs/git.nix + ./programs/gnome-terminal.nix + ./programs/lesspipe.nix + ./programs/texlive.nix + ./services/dunst.nix + ./services/gnome-keyring.nix + ./services/gpg-agent.nix + ./services/keepassx.nix + ./services/network-manager-applet.nix + ./services/random-background.nix + ./services/taffybar.nix + ./services/tahoe-lafs.nix + ./services/udiskie.nix + ./services/xscreensaver.nix + ./systemd.nix + ./xsession.nix + ]; + + pkgsModule = { + config._module.args.pkgs = lib.mkForce pkgs; + }; + + module = lib.evalModules { + modules = [ configuration ] ++ modules ++ [ pkgsModule ]; + }; + +in + +{ + inherit (module) options config; + + activation-script = module.config.home.activationPackage; + home-path = module.config.home.path; +} diff --git a/modules/home-environment.nix b/modules/home-environment.nix new file mode 100644 index 000000000..27911d0f3 --- /dev/null +++ b/modules/home-environment.nix @@ -0,0 +1,314 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.home; + + languageSubModule = types.submodule { + options = { + base = mkOption { + type = types.str; + description = '' + The language to use unless overridden by a more specific option. + ''; + }; + + address = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for addresses. + ''; + }; + + monetary = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for formatting currencies and money amounts. + ''; + }; + + paper = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for paper sizes. + ''; + }; + + time = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The language to use for formatting times. + ''; + }; + }; + }; + + keyboardSubModule = types.submodule { + options = { + layout = mkOption { + type = types.str; + default = "us"; + description = '' + Keyboard layout. + ''; + }; + + model = mkOption { + type = types.str; + default = "pc104"; + example = "presario"; + description = '' + Keyboard model. + ''; + }; + + options = mkOption { + type = types.listOf types.str; + default = []; + example = ["grp:caps_toggle" "grp_led:scroll"]; + description = '' + X keyboard options; layout switching goes here. + ''; + }; + + variant = mkOption { + type = types.str; + default = ""; + example = "colemak"; + description = '' + X keyboard variant. + ''; + }; + }; + }; + +in + +{ + options = { + home.file = mkOption { + description = "Attribute set of files to link into the user home."; + default = {}; + type = types.loaOf (types.submodule ( + { name, config, ... }: { + options = { + target = mkOption { + type = types.str; + description = "Path to target file relative to $HOME."; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Text of the file."; + }; + + source = mkOption { + type = types.path; + description = "Path of the source file."; + }; + + mode = mkOption { + type = types.str; + default = "444"; + description = "The permissions to apply to the file."; + }; + }; + + config = { + target = mkDefault name; + source = mkIf (config.text != null) ( + let name' = "user-etc-" + baseNameOf name; + in mkDefault (pkgs.writeText name' config.text) + ); + }; + }) + ); + }; + + home.language = mkOption { + type = languageSubModule; + default = {}; + description = "Language configuration."; + }; + + home.keyboard = mkOption { + type = keyboardSubModule; + default = {}; + description = "Keyboard configuration."; + }; + + home.sessionVariables = mkOption { + default = {}; + type = types.attrs; + description = "Environment variables to always set at login."; + }; + + home.packages = mkOption { + type = types.listOf types.package; + default = []; + description = "The set of packages to appear in the user environment."; + }; + + home.path = mkOption { + internal = true; + description = "The derivation installing the user packages."; + }; + + home.activation = mkOption { + internal = true; + default = {}; + type = types.attrs; + description = "Activation scripts for the home environment."; + }; + + home.activationPackage = mkOption { + internal = true; + type = types.package; + description = "The package containing the complete activation script."; + }; + }; + + config = { + home.sessionVariables = + let + maybeSet = name: value: + listToAttrs (optional (value != null) { inherit name value; }); + in + (maybeSet "LANG" cfg.language.base) + // + (maybeSet "LC_ADDRESS" cfg.language.address) + // + (maybeSet "LC_MONETARY" cfg.language.monetary) + // + (maybeSet "LC_PAPER" cfg.language.paper) + // + (maybeSet "LC_TIME" cfg.language.time); + + home.activation.linkages = + let + pak = pkgs.stdenv.mkDerivation { + name = "home-environment"; + + phases = [ "installPhase" ]; + + installPhase = + concatStringsSep "\n" ( + mapAttrsToList (name: value: + "install -v -D -m${value.mode} ${value.source} $out/${value.target}" + ) cfg.file + ); + }; + + link = pkgs.writeText "link" '' + for sourcePath in "$@" ; do + basePath="''${sourcePath#/nix/store/*-home-environment/}" + targetPath="$HOME/$basePath" + mkdir -vp "$(dirname "$targetPath")" + ln -vsf "$sourcePath" "$targetPath" + done + ''; + + cleanup = pkgs.writeText "cleanup" '' + for sourcePath in "$@" ; do + basePath="''${sourcePath#/nix/store/*-home-environment/}" + targetPath="$HOME/$basePath" + echo -n "Checking $targetPath" + if [[ -f "${pak}/$basePath" ]] ; then + echo " exists" + else + echo " gone (deleting)" + rm -v "$targetPath" + rmdir --ignore-fail-on-non-empty -v -p "$(dirname "$targetPath")" + fi + done + ''; + in + '' + function setupVars() { + local gcPath="/nix/var/nix/gcroots/per-user/$(whoami)" + local greatestGenNum=( \ + $(find "$gcPath" -name 'home-*' \ + | sed 's/^.*-\([0-9]*\)$/\1/' \ + | sort -rn \ + | head -1) \ + ) + if [[ -n "$greatestGenNum" ]] ; then + oldGenNum=$greatestGenNum + newGenNum=$(($oldGenNum + 1)) + oldGenPath="$(readlink -e "$gcPath/home-$oldGenNum")" + else + newGenNum=1 + fi + newGenPath="${pak}"; + newGenGcPath="$gcPath/home-$newGenNum" + } + + # Set some vars, these can be used later on as well. + setupVars + + if [[ "$oldGenPath" != "$newGenPath" ]] ; then + ln -sfv "$newGenPath" "$newGenGcPath" + find "$newGenPath" -type f -print0 | xargs -0 bash ${link} + if [[ -n "$oldGenPath" ]] ; then + echo "Cleaning up orphan links from $HOME" + find "$oldGenPath" -type f -print0 | xargs -0 bash ${cleanup} + fi + else + echo "Same home files as previous generation ... doing nothing" + fi + ''; + + home.activation.installPackages = + '' + nix-env -i ${cfg.path} + ''; + + home.activationPackage = + let + addHeader = n: v: + v // { + text = '' + echo Activating ${n} + ${v.text} + ''; + }; + toDepString = n: v: if isString v then noDepEntry v else v; + activationWithDeps = + mapAttrs addHeader (mapAttrs toDepString cfg.activation); + activationCmds = + textClosureMap id activationWithDeps (attrNames activationWithDeps); + + sf = pkgs.writeText "activation-script" '' + #!${pkgs.stdenv.shell} + + ${activationCmds} + ''; + in + pkgs.stdenv.mkDerivation { + name = "activate-home"; + + phases = [ "installPhase" ]; + + installPhase = '' + install -v -D -m755 ${sf} $out/libexec/home-activate + ''; + }; + + home.path = pkgs.buildEnv { + name = "home-manager-path"; + + paths = cfg.packages; + + meta = { + description = "Environment of packages installed through home-manager"; + }; + }; + }; +} diff --git a/modules/lib/generators.nix b/modules/lib/generators.nix new file mode 100644 index 000000000..4190703e2 --- /dev/null +++ b/modules/lib/generators.nix @@ -0,0 +1,93 @@ +/* Functions that generate widespread file + * formats from nix data structures. + * + * They all follow a similar interface: + * generator { config-attrs } data + * + * Tests can be found in ./tests.nix + * Documentation in the manual, #sec-generators + */ +with import ; +let + libStr = import ; + libAttr = import ; + + flipMapAttrs = flip libAttr.mapAttrs; +in + +rec { + + /* Generate a line of key k and value v, separated by + * character sep. If sep appears in k, it is escaped. + * Helper for synaxes with different separators. + * + * mkKeyValueDefault ":" "f:oo" "bar" + * > "f\:oo:bar" + */ + mkKeyValueDefault = sep: k: v: + "${libStr.escape [sep] k}${sep}${toString v}"; + + + /* Generate a key-value-style config file from an attrset. + * + * mkKeyValue is the same as in toINI. + */ + toKeyValue = { + mkKeyValue ? mkKeyValueDefault "=" + }: attrs: + let mkLine = k: v: mkKeyValue k v + "\n"; + in libStr.concatStrings (libAttr.mapAttrsToList mkLine attrs); + + + /* Generate an INI-style config file from an + * attrset of sections to an attrset of key-value pairs. + * + * generators.toINI {} { + * foo = { hi = "${pkgs.hello}"; ciao = "bar"; }; + * baz = { "also, integers" = 42; }; + * } + * + *> [baz] + *> also, integers=42 + *> + *> [foo] + *> ciao=bar + *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10 + * + * The mk* configuration attributes can generically change + * the way sections and key-value strings are generated. + * + * For more examples see the test cases in ./tests.nix. + */ + toINI = { + # apply transformations (e.g. escapes) to section names + mkSectionName ? (name: libStr.escape [ "[" "]" ] name), + # format a setting line from key and value + mkKeyValue ? mkKeyValueDefault "=" + }: attrsOfAttrs: + let + # map function to string for each key val + mapAttrsToStringsSep = sep: mapFn: attrs: + libStr.concatStringsSep sep + (libAttr.mapAttrsToList mapFn attrs); + mkSection = sectName: sectValues: '' + [${mkSectionName sectName}] + '' + toKeyValue { inherit mkKeyValue; } sectValues; + in + # map input to ini sections + mapAttrsToStringsSep "\n" mkSection attrsOfAttrs; + + + /* Generates JSON from an arbitrary (non-function) value. + * For more information see the documentation of the builtin. + */ + toJSON = {}: builtins.toJSON; + + + /* YAML has been a strict superset of JSON since 1.2, so we + * use toJSON. Before it only had a few differences referring + * to implicit typing rules, so it should work with older + * parsers as well. + */ + toYAML = {}@args: toJSON args; +} diff --git a/modules/programs/bash.nix b/modules/programs/bash.nix new file mode 100644 index 000000000..f17b3f1bd --- /dev/null +++ b/modules/programs/bash.nix @@ -0,0 +1,159 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.bash; + +in + +{ + 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."; + }; + + 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 = []; + 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."; + }; + + shellAliases = mkOption { + default = {}; + example = { 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. The + aliases are added to all users' shells. + ''; + type = types.attrs; + }; + + 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 added to .profile."; + }; + + initExtra = mkOption { + default = ""; + type = types.lines; + description = "Extra commands that should be added to .bashrc."; + }; + }; + }; + + config = ( + let + aliasesStr = concatStringsSep "\n" ( + mapAttrsToList (k: v: "alias ${k}='${v}'") cfg.shellAliases + ); + + shoptsStr = concatStringsSep "\n" ( + map (v: "shopt -s ${v}") cfg.shellOptions + ); + + export = n: v: "export ${n}=\"${toString v}\""; + exportIfNonNull = n: v: optionalString (v != null) (export n v); + exportIfNonEmpty = n: v: optionalString (v != "") (export n v); + + histControlStr = concatStringsSep ":" cfg.historyControl; + histIgnoreStr = concatStringsSep ":" cfg.historyIgnore; + + envVarsStr = concatStringsSep "\n" ( + mapAttrsToList export config.home.sessionVariables + ); + in mkIf cfg.enable { + 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 -*- + + ${envVarsStr} + + ${cfg.profileExtra} + ''; + + home.file.".bashrc".text = '' + # -*- mode: sh -*- + + # Skip if not running interactively. + [ -z "$PS1" ] && return + + ${export "HISTSIZE" cfg.historySize} + ${export "HISTFILESIZE" cfg.historyFileSize} + ${exportIfNonEmpty "HISTCONTROL" histControlStr} + ${exportIfNonEmpty "HISTIGNORE" histIgnoreStr} + + ${shoptsStr} + + ${aliasesStr} + + ${cfg.initExtra} + + ${optionalString cfg.enableAutojump + ". ${pkgs.autojump}/share/autojump/autojump.bash"} + ''; + + home.packages = + optional (cfg.enableAutojump) pkgs.autojump; + } + ); +} diff --git a/modules/programs/beets.nix b/modules/programs/beets.nix new file mode 100644 index 000000000..46a17a971 --- /dev/null +++ b/modules/programs/beets.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.beets; + +in + +{ + options = { + programs.beets = { + settings = mkOption { + type = types.attrs; + default = {}; + description = "Configuration written to ~/.config/beets/config.yaml"; + }; + }; + }; + + config = mkIf (cfg.settings != {}) { + home.packages = [ pkgs.beets ]; + + home.file.".config/beets/config.yaml".text = + builtins.toJSON config.programs.beets.settings; + }; +} diff --git a/modules/programs/eclipse.nix b/modules/programs/eclipse.nix new file mode 100644 index 000000000..e9d4c0602 --- /dev/null +++ b/modules/programs/eclipse.nix @@ -0,0 +1,39 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.eclipse; + +in + +{ + options = { + programs.eclipse = { + enable = mkEnableOption "Eclipse"; + + 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 = pkgs.eclipses.eclipse-platform; + jvmArgs = cfg.jvmArgs; + plugins = cfg.plugins; + }) + ]; + }; +} diff --git a/modules/programs/emacs.nix b/modules/programs/emacs.nix new file mode 100644 index 000000000..22a4ed434 --- /dev/null +++ b/modules/programs/emacs.nix @@ -0,0 +1,29 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.emacs; + +in + +{ + options = { + programs.emacs = { + enable = mkEnableOption "Emacs"; + + extraPackages = mkOption { + default = self: []; + example = literalExample '' + epkgs: [ epkgs.emms epkgs.magit ] + ''; + description = "Extra packages available to Emacs."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ (pkgs.emacsWithPackages cfg.extraPackages) ]; + }; +} diff --git a/modules/programs/git.nix b/modules/programs/git.nix new file mode 100644 index 000000000..33fcbe428 --- /dev/null +++ b/modules/programs/git.nix @@ -0,0 +1,102 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.git; + + toINI = (import ../lib/generators.nix).toINI {}; + + signModule = types.submodule ( + { ... }: { + options = { + key = mkOption { + type = types.str; + default = null; + 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."; + }; + }; + } + ); + +in + +{ + options = { + programs.git = { + enable = mkEnableOption "Git"; + + package = mkOption { + type = types.package; + default = pkgs.git; + defaultText = "pkgs.git"; + description = "Git 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 = "Git aliases to define."; + }; + + signing = mkOption { + type = types.nullOr signModule; + default = null; + description = "Options related to signing commits using GnuPG."; + }; + + extraConfig = mkOption { + type = types.lines; + default = null; + description = "Additional configuration to add."; + }; + }; + }; + + config = mkIf cfg.enable ( + let + ini = { + user = { + name = cfg.userName; + email = cfg.userEmail; + } // optionalAttrs (cfg.signing != null) { + signingKey = cfg.signing.key; + }; + } // optionalAttrs (cfg.signing != null) { + commit.gpgSign = cfg.signing.signByDefault; + gpg.program = cfg.signing.gpgPath; + } // optionalAttrs (cfg.aliases != {}) { + alias = cfg.aliases; + }; + in + { + home.packages = [ cfg.package ]; + + home.file.".gitconfig".text = toINI ini + "\n" + cfg.extraConfig; + } + ); +} diff --git a/modules/programs/gnome-terminal.nix b/modules/programs/gnome-terminal.nix new file mode 100644 index 000000000..90cf46adf --- /dev/null +++ b/modules/programs/gnome-terminal.nix @@ -0,0 +1,186 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.gnome-terminal; + + 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."; + }; + }; + } + ); + + 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."; + }; + + 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."; + }; + + 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. + ''; + }; + }; + } + ); + + toINI = (import ../lib/generators.nix).toINI { mkKeyValue = mkIniKeyValue; }; + + mkIniKeyValue = key: value: + let + tweakVal = v: + if isString v then "'${v}'" + else if isList v then "[" + concatStringsSep "," (map tweakVal v) + "]" + else if isBool v && v then "true" + else if isBool v && !v then "false" + else toString v; + in + "${key}=${tweakVal value}"; + + buildProfileSet = pcfg: + { + visible-name = pcfg.visibleName; + scrollbar-policy = if pcfg.showScrollbar then "always" else "never"; + scrollback-lines = pcfg.scrollbackLines; + cursor-shape = pcfg.cursorShape; + } + // ( + 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; + } + // ( + if (pcfg.colors.boldColor == null) + then { bold-color-same-as-fg = true; } + else { + bold-color-same-as-fg = false; + bold-color = pcfg.colors.boldColor; + } + ) + ) + ); + + buildIniSet = cfg: + { + "/" = { + default-show-menubar = cfg.showMenubar; + schema-version = 3; + }; + } + // + { + "profiles:" = { + default = head (attrNames (filterAttrs (n: v: v.default) cfg.profile)); + list = attrNames cfg.profile; #mapAttrsToList (n: v: n) cfg.profile; + }; + } + // + mapAttrs' (name: value: + nameValuePair ("profiles:/:${name}") (buildProfileSet value) + ) cfg.profile; + +in + +{ + options = { + programs.gnome-terminal = { + enable = mkEnableOption "Gnome Terminal"; + + showMenubar = mkOption { + default = true; + type = types.bool; + description = "Whether to show the menubar by default"; + }; + + profile = mkOption { + default = {}; + type = types.loaOf profileSubModule; + }; + }; + }; + + config = mkIf cfg.enable { + home.activation.gnome-terminal = + let + sf = pkgs.writeText "gnome-terminal.ini" (toINI (buildIniSet cfg)); + dconfPath = "/org/gnome/terminal/legacy/"; + in + '' + ${pkgs.gnome3.dconf}/bin/dconf load ${dconfPath} < ${sf} + ''; + }; +} diff --git a/modules/programs/lesspipe.nix b/modules/programs/lesspipe.nix new file mode 100644 index 000000000..d250f535a --- /dev/null +++ b/modules/programs/lesspipe.nix @@ -0,0 +1,17 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + 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/modules/programs/texlive.nix b/modules/programs/texlive.nix new file mode 100644 index 000000000..afa8f7012 --- /dev/null +++ b/modules/programs/texlive.nix @@ -0,0 +1,32 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.texlive; + +in + +{ + options = { + programs.texlive = { + enable = mkEnableOption "Texlive"; + + extraPackages = mkOption { + default = self: {}; + example = literalExample '' + tpkgs: { inherit (tpkgs) collection-fontsrecommended algorithms; } + ''; + description = "Extra packages available to Texlive."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ + (pkgs.texlive.combine (cfg.extraPackages pkgs.texlive)) + ]; + + }; +} diff --git a/modules/services/dunst.nix b/modules/services/dunst.nix new file mode 100644 index 000000000..8042cf93d --- /dev/null +++ b/modules/services/dunst.nix @@ -0,0 +1,35 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.dunst = { + enable = mkEnableOption "the dunst notification daemon"; + + settings = mkOption { + type = types.attrs; + default = {}; + description = "Configuration written to ~/.config/dunstrc"; + }; + }; + }; + + config = mkIf config.services.dunst.enable { + systemd.user.services.dunst = { + Unit = { + Description = "Dunst notification daemon"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + + Service = { + # Type = "dbus"; + # BusName = "org.freedesktop.Notifications"; + ExecStart = "${pkgs.dunst}/bin/dunst"; + }; + }; + }; +} diff --git a/modules/services/gnome-keyring.nix b/modules/services/gnome-keyring.nix new file mode 100644 index 000000000..e50409065 --- /dev/null +++ b/modules/services/gnome-keyring.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.gnome-keyring; + +in + +{ + options = { + services.gnome-keyring = { + enable = mkEnableOption "GNOME Keyring"; + + components = mkOption { + type = types.listOf (types.enum ["pkcs11" "secrets" "ssh"]); + default = []; + description = '' + The GNOME keyring components to start. If empty then the + default set of components will be started. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.gnome-keyring = { + Unit = { + Description = "GNOME Keyring"; + }; + + Service = { + ExecStart = + let + args = concatStringsSep " " ( + [ "--start" "--foreground" ] + ++ optional (cfg.components != []) ( + "--components=" + concatStringsSep "," cfg.components + ) + ); + in + "${pkgs.gnome3.gnome_keyring}/bin/gnome-keyring-daemon ${args}"; + Restart = "on-abort"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + }; + }; +} diff --git a/modules/services/gpg-agent.nix b/modules/services/gpg-agent.nix new file mode 100644 index 000000000..ac93a871f --- /dev/null +++ b/modules/services/gpg-agent.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.gpg-agent; + +in + +{ + options = { + services.gpg-agent = { + enable = mkEnableOption "GnuPG private key agent"; + + defaultCacheTtl = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Set the time a cache entry is valid to the given number of seconds. + ''; + }; + + enableSshSupport = mkOption { + type = types.bool; + default = false; + description = "Whether to use the GnuPG key agent for SSH keys."; + }; + }; + }; + + config = mkIf cfg.enable { + home.file.".gnupg/gpg-agent.conf".text = concatStringsSep "\n" ( + optional cfg.enableSshSupport + "enable-ssh-support" + ++ + optional (cfg.defaultCacheTtl != null) + "default-cache-ttl ${toString cfg.defaultCacheTtl}" + ); + + home.sessionVariables = + optionalAttrs cfg.enableSshSupport { + SSH_AUTH_SOCK = "\${XDG_RUNTIME_DIR}/gnupg/S.gpg-agent.ssh"; + }; + + programs.bash.initExtra = '' + GPG_TTY="$(tty)" + export GPG_TTY + gpg-connect-agent updatestartuptty /bye > /dev/null + ''; + + systemd.user.services.gpg-agent = { + Unit = { + Description = "GnuPG private key agent"; + IgnoreOnIsolate = true; + }; + + Service = { + Type = "forking"; + ExecStart = "${pkgs.gnupg}/bin/gpg-agent --daemon --use-standard-socket"; + Restart = "on-abort"; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; +} diff --git a/modules/services/keepassx.nix b/modules/services/keepassx.nix new file mode 100644 index 000000000..799f0e5c3 --- /dev/null +++ b/modules/services/keepassx.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.keepassx = { + enable = mkEnableOption "the KeePassX password manager"; + }; + }; + + config = mkIf config.services.keepassx.enable { + systemd.user.services.keepassx = { + Unit = { + Description = "KeePassX password manager"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + + Service = { + ExecStart = "${pkgs.keepassx}/bin/keepassx -min -lock"; + }; + }; + }; +} diff --git a/modules/services/network-manager-applet.nix b/modules/services/network-manager-applet.nix new file mode 100644 index 000000000..8b529c5b6 --- /dev/null +++ b/modules/services/network-manager-applet.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.network-manager-applet = { + enable = mkEnableOption "the Network Manager applet"; + }; + }; + + config = mkIf config.services.network-manager-applet.enable { + systemd.user.services.network-manager-applet = { + Unit = { + Description = "Network Manager applet"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + + Service = { + ExecStart = "${pkgs.networkmanagerapplet}/bin/nm-applet --sm-disable"; + }; + }; + }; +} diff --git a/modules/services/random-background.nix b/modules/services/random-background.nix new file mode 100644 index 000000000..0dabb8721 --- /dev/null +++ b/modules/services/random-background.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.random-background; + +in + +{ + options = { + services.random-background = { + enable = mkEnableOption "random desktop background"; + + imageDirectory = mkOption { + type = types.str; + description = + '' + The directory of images from which a background should be + chosen. Should be formatted in a way understood by + systemd, e.g., '%h' is the home directory. + ''; + }; + + interval = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The duration between changing background image, set to null + to only set background when logging in. + + Should be formatted as a duration understood by systemd. + ''; + }; + }; + }; + + config = mkIf cfg.enable ( + mkMerge ([ + { + systemd.user.services.random-background = { + Unit = { + Description = "Set random desktop background using feh"; + }; + + Service = { + Type = "oneshot"; + ExecStart = "${pkgs.feh}/bin/feh --randomize --bg-fill %h/backgrounds/"; + IOSchedulingClass = "idle"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + }; + } + (mkIf (cfg.interval != null) { + systemd.user.timers.random-background = { + Unit = { + Description = "Set random desktop background using feh"; + }; + + Timer = { + OnUnitActiveSec = cfg.interval; + }; + + Install = { + WantedBy = [ "timers.target" ]; + }; + }; + }) + ])); +} diff --git a/modules/services/taffybar.nix b/modules/services/taffybar.nix new file mode 100644 index 000000000..f9ab2ee61 --- /dev/null +++ b/modules/services/taffybar.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.taffybar; + +in + +{ + options = { + services.taffybar = { + enable = mkEnableOption "Taffybar"; + + package = mkOption { + default = pkgs.taffybar; + defaultText = "pkgs.taffybar"; + type = types.package; + example = literalExample "pkgs.taffybar"; + description = "The package to use for the Taffybar binary."; + }; + }; + }; + + config = mkIf config.services.taffybar.enable { + systemd.user.services.taffybar = { + Unit = { + Description = "Taffybar desktop bar"; + }; + + Service = { + ExecStart = "${cfg.package}/bin/taffybar"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + }; + }; +} diff --git a/modules/services/tahoe-lafs.nix b/modules/services/tahoe-lafs.nix new file mode 100644 index 000000000..8d12b03e2 --- /dev/null +++ b/modules/services/tahoe-lafs.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.tahoe-lafs = { + enable = mkEnableOption "Tahoe-LAFS"; + }; + }; + + config = mkIf config.services.tahoe-lafs.enable { + systemd.user.services.tahoe-lafs = { + Unit = { + Description = "Tahoe-LAFS"; + }; + + Service = { + ExecStart = "${pkgs.tahoelafs}/bin/tahoe run -C %h/.tahoe"; + }; + }; + }; +} diff --git a/modules/services/udiskie.nix b/modules/services/udiskie.nix new file mode 100644 index 000000000..964629c7a --- /dev/null +++ b/modules/services/udiskie.nix @@ -0,0 +1,29 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.udiskie = { + enable = mkEnableOption "Udiskie mount daemon"; + }; + }; + + config = mkIf config.services.udiskie.enable { + systemd.user.services.udiskie = { + Unit = { + Description = "Udiskie mount daemon"; + Requires = [ "taffybar.service" ]; + After = [ "taffybar.service" ]; + }; + + Service = { + ExecStart = "${pkgs.pythonPackages.udiskie}/bin/udiskie -2 -A -n -s"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + }; + }; +} diff --git a/modules/services/xscreensaver.nix b/modules/services/xscreensaver.nix new file mode 100644 index 000000000..582615539 --- /dev/null +++ b/modules/services/xscreensaver.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.xscreensaver = { + enable = mkEnableOption "XScreenSaver"; + }; + }; + + config = mkIf config.services.xscreensaver.enable { + systemd.user.services.xscreensaver = { + Unit = { + Description = "XScreenSaver"; + }; + + Service = { + ExecStart = "${pkgs.xscreensaver}/bin/xscreensaver -no-splash"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + }; + }; +} diff --git a/modules/systemd.nix b/modules/systemd.nix new file mode 100644 index 000000000..b927b04ad --- /dev/null +++ b/modules/systemd.nix @@ -0,0 +1,119 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + toSystemdIni = (import lib/generators.nix).toINI { + mkKeyValue = key: value: + let + value' = + if isBool value then (if value then "true" else "false") + else toString value; + in + "${key}=${value'}"; + }; + + buildService = style: name: serviceCfg: + let + source = pkgs.writeText "${name}.${style}" (toSystemdIni serviceCfg); + + wantedBy = target: + { + name = ".config/systemd/user/${target}.wants/${name}.${style}"; + value = { inherit source; }; + }; + in + singleton { + name = ".config/systemd/user/${name}.${style}"; + value = { inherit source; }; + } + ++ + map wantedBy (serviceCfg.Install.WantedBy or []); + + buildServices = style: serviceCfgs: + concatLists (mapAttrsToList (buildService style) serviceCfgs); + +in + +{ + options = { + systemd.user = { + services = mkOption { + default = {}; + type = types.attrs; + description = "Definition of systemd per-user service units."; + }; + + timers = mkOption { + default = {}; + type = types.attrs; + description = "Definition of systemd per-user timers"; + }; + }; + }; + + config = { + home.file = + listToAttrs ( + (buildServices "service" config.systemd.user.services) + ++ + (buildServices "timer" config.systemd.user.timers) + ); + + home.activation.reloadSystemD = stringAfter ["linkages"] '' + function systemdPostReload() { + local servicesDiffFile="$(mktemp)" + + diff \ + --new-line-format='+%L' \ + --old-line-format='-%L' \ + --unchanged-line-format=' %L' \ + <(basename -a $(echo "$oldGenPath/.config/systemd/user/*.service") | sort) \ + <(basename -a $(echo "$newGenPath/.config/systemd/user/*.service") | sort) \ + > $servicesDiffFile + + local -a maybeRestart=( $(grep '^ ' $servicesDiffFile | cut -c2-) ) + local -a toStop=( $(grep '^-' $servicesDiffFile | cut -c2-) ) + local -a toStart=( $(grep '^+' $servicesDiffFile | cut -c2-) ) + local -a toRestart=( ) + + for f in ''${maybeRestart[@]} ; do + if systemctl --quiet --user is-active "$f" \ + && ! cmp --quiet \ + "$oldGenPath/.config/systemd/user/$f" \ + "$newGenPath/.config/systemd/user/$f" ; then + echo "Adding '$f' to restart list"; + toRestart+=("$f") + fi + done + + rm $servicesDiffFile + + sugg="" + + if [[ -n "''${toRestart[@]}" ]] ; then + sugg="$sugg\nsystemctl --user restart ''${toRestart[@]}" + fi + + if [[ -n "''${toStop[@]}" ]] ; then + sugg="$sugg\nsystemctl --user stop ''${toStop[@]}" + fi + + if [[ -n "''${toStart[@]}" ]] ; then + sugg="$sugg\nsystemctl --user start ''${toStart[@]}" + fi + + if [[ -n "$sugg" ]] ; then + echo "Suggested commands:" + echo -e "$sugg" + fi + } + + if [[ "$oldGenPath" != "$newGenPath" ]] ; then + systemctl --user daemon-reload + systemdPostReload + fi + ''; + }; +} diff --git a/modules/xsession.nix b/modules/xsession.nix new file mode 100644 index 000000000..ab6629743 --- /dev/null +++ b/modules/xsession.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xsession; + +in + +{ + options = { + xsession = { + enable = mkEnableOption "X Session"; + + windowManager = mkOption { + default = {}; + type = types.str; + description = "Path to window manager to exec."; + }; + + initExtra = mkOption { + type = types.lines; + default = ""; + description = "Extra shell commands to run during initialization."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.setxkbmap = { + Unit = { + Description = "Set up keyboard in X"; + }; + + Install = { + WantedBy = [ "xorg.target" ]; + }; + + Service = { + Type = "oneshot"; + ExecStart = + let + args = concatStringsSep " " ( + [ + "-layout '${config.home.keyboard.layout}'" + "-variant '${config.home.keyboard.variant}'" + ] ++ + (map (v: "-option '${v}'") config.home.keyboard.options) + ); + in + "${pkgs.xorg.setxkbmap}/bin/setxkbmap ${args}"; + }; + }; + + home.file.".xsession" = { + mode = "555"; + text = '' + # Rely on Bash to set session variables. + . "$HOME/.profile" + + systemctl --user import-environment DBUS_SESSION_BUS_ADDRESS + systemctl --user import-environment DISPLAY + systemctl --user import-environment SSH_AUTH_SOCK + systemctl --user import-environment XDG_DATA_DIRS + systemctl --user import-environment XDG_RUNTIME_DIR + systemctl --user start xorg.target + + ${cfg.initExtra} + + ${cfg.windowManager} + + systemctl --user stop xorg.target + ''; + }; + }; +}