{ config, lib, pkgs, ... }: let cfg = config.programs.qutebrowser; /* Flattens an attribute set of key values to stringified keys and their values. This is necessary, as qutebrowser only understands key value settings. ## Determining the keys and values Currently, this function will recursively iterate through the `configAttrs`, whilst adding up the attribute keys, until one of the following conditions apply: - The value is not an attribute set. - The value is “marked” as being a raw attribute set. This is done automatically via the `apply` functions in the option that take attribute sets (i.e., dictionaries) as values. ## Marking an attribute set as value Currently, this function assumes that a attribute set is meant as value, if it's key is `__raw`. For example: ```nix { bindings.command.__raw = { normal.L = "close"; }; } ``` # Type flattenSettings :: AttrSet -> [{key :: String; value :: Any}] # Arguments configAttrs : The configuration attribute set to flatten. # Examples flattenSettings {aliases.__raw = {c = "close";}; colors.background = "0xffffff00";} => [ {key = "aliases"; value = {c = "close";};} {key = "colors.background"; value = "0xffffff00";} ] */ flattenSettings = let isValue = aS: (builtins.attrNames aS) == ["__raw"]; in configAttrs: lib.collect (aS: (builtins.attrNames aS) == ["key" "value"]) ( lib.attrsets.mapAttrsRecursiveCond (a: !(isValue a)) ( path: value: assert builtins.isList path; { key = lib.concatStringsSep "." path; inherit value; } ) configAttrs ); pythonize = identCount: v: let nextIdentCount = identCount + 2; prevIdentCount = identCount - 2; ident = builtins.concatStringsSep "" (builtins.genList (_: " ") identCount); prevIdent = builtins.concatStringsSep "" (builtins.genList (_: " ") prevIdentCount); in if v == null then "None" else if builtins.isBool v then ( if v then "True" else "False" ) else if builtins.isString v then ''"${lib.escape ["\"" "\\"] v}"'' else if builtins.isInt v then builtins.toString v else if builtins.isList v then ( let items = builtins.map (pythonize nextIdentCount) v; in if (builtins.length items) < 2 then "[${builtins.concatStringsSep "," items}]" else "[\n${ident}${builtins.concatStringsSep ",\n${ident}" items}\n${prevIdent}]" ) else if builtins.isAttrs v then if (v ? __raw && v.__raw == null) # Make it possible to skip values, as we would otherwise unconditional override the # non-set default values with `{}`. then null else if (v ? __raw && builtins.isList v.__raw) # The raw value is already pre-rendered (e.g., used by the bindings.commands to use # the `config.bind` functions instead of setting the keys directly.) then v.__raw else ( # {key = "value"; other = "other";} -> "{'key': 'value', 'other': 'other'}" let items = lib.attrsets.mapAttrsToList (key: value: ''${pythonize nextIdentCount key}: ${pythonize nextIdentCount value}'') (v.__raw or v); in if (builtins.length items) == 0 then "{}" else "{\n${ident}${builtins.concatStringsSep ",\n${ident}" items}\n${prevIdent}}" ) else builtins.throw "Cannot serialize a '${builtins.typeOf v}' as python value."; configSet = keyValue: maybe_url: let value = pythonize 2 keyValue.value; in if value == null then "" else if builtins.isList value then builtins.concatStringsSep "\n" value else "config.set(${pythonize 2 keyValue.key}, ${value}${ if (maybe_url != null) then ", ${maybe_url}" else "" })"; urlConfigSet = url: conf: builtins.map (c: configSet c url) (flattenSettings conf); extraConfigSet = name: path_or_string: ''config.source("${ if (builtins.isPath path_or_string) then path_or_string else pkgs.writeText name path_or_string }")''; mkRaw = value: {__raw = value;}; formatQuickmarks = n: s: "${n} ${s}"; settingsType = lib.types.submodule { freeformType = lib.types.attrsOf lib.types.anything; options = { # A list of config keys, that take dicts and need special handling: # (See their configdata.yml for the source of this list) # aliases # bindings.commands # bindings.default # bindings.key_mappings # content.headers.custom # content.javascript.log # hints.selectors # statusbar.padding # url.searchengines aliases = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; }; bindings = let keyBindType = lib.types.nullOr (lib.types.attrsOf ( lib.types.attrsOf ( lib.types.nullOr ( lib.types.separatedString " ;; " ) ) )); in { key_mappings = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; }; # This is special, as we need to use the `config.bind` command, instead of the # normal one. commands = lib.mkOption { type = keyBindType; default = null; apply = v: if v == null then null else { __raw = lib.lists.flatten (lib.mapAttrsToList (mode: mappings: lib.mapAttrsToList (key: value: "config.bind(${pythonize 0 key}, ${pythonize 0 value}, mode=${pythonize 0 mode})") mappings) v); }; example = lib.literalExpression '' { normal = { "" = "spawn mpv {url}"; ",p" = "spawn --userscript qute-pass"; ",l" = '''config-cycle spellcheck.languages ["en-GB"] ["en-US"]'''; "" = mkMerge [ "config-cycle tabs.show never always" "config-cycle statusbar.show in-mode always" "config-cycle scrolling.bar never always" ]; }; prompt = { "" = "prompt-yes"; }; } ''; }; default = lib.mkOption { type = keyBindType; default = null; apply = mkRaw; }; }; content = { headers = { custom = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; }; }; javascript = { log = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; }; }; }; hints = { selectors = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; }; }; qt = { environ = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; }; }; url = { searchengines = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.str); default = null; apply = mkRaw; example = lib.literalExpression '' { w = "https://en.wikipedia.org/wiki/Special:Search?search={}&go=Go&ns0=1"; aw = "https://wiki.archlinux.org/?search={}"; nw = "https://wiki.nixos.org/index.php?search={}"; g = "https://www.google.com/search?hl=en&q={}"; } ''; }; }; statusbar = { padding = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf lib.types.int); default = null; apply = mkRaw; }; }; }; }; in { options.programs.qutebrowser = { enable = lib.mkEnableOption "qutebrowser"; package = lib.mkPackageOption pkgs "qutebrowser" {}; loadAutoconfig = lib.mkOption { type = lib.types.bool; default = false; description = '' Load settings configured via the GUI. ''; }; settings = lib.mkOption { type = settingsType; default = {}; description = '' Options to add to qutebrowser {file}`config.py` file. See for options. ''; example = lib.literalExpression '' { colors = { hints = { bg = "#000000"; fg = "#ffffff"; }; tabs.bar.bg = "#000000"; }; tabs.tabs_are_windows = true; } ''; }; quickmarks = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = {}; description = '' Quickmarks to add to qutebrowser's {file}`quickmarks` file. Note that when Home Manager manages your quickmarks, you cannot edit them at runtime. ''; example = lib.literalExpression '' { nixpkgs = "https://github.com/NixOS/nixpkgs"; home-manager = "https://github.com/nix-community/home-manager"; } ''; }; greasemonkey = lib.mkOption { type = lib.types.listOf lib.types.package; default = []; example = lib.literalExpression '' [ (pkgs.fetchurl { url = "https://raw.githubusercontent.com/afreakk/greasemonkeyscripts/1d1be041a65c251692ee082eda64d2637edf6444/youtube_sponsorblock.js"; sha256 = "sha256-e3QgDPa3AOpPyzwvVjPQyEsSUC9goisjBUDMxLwg8ZE="; }) (pkgs.writeText "some-script.js" ''' // ==UserScript== // @name Some Greasemonkey script // ==/UserScript== ''') ] ''; description = '' Greasemonkey userscripts to add to qutebrowser's {file}`greasemonkey` directory. ''; }; perUrlSettings = lib.mkOption { type = lib.types.attrsOf settingsType; default = {}; description = '' Options to set, as in {option}`settings` but per url pattern. See for options, but beware, that not every option supports being set per-url. ''; example = lib.literalExpression '' { "zoom.us" = { content = { autoplay = true; media.audio_capture = true; media.video_capture = true; }; }; "github.com".colors.webpage.darkmode.enabled = false; }; ''; }; extraConfig = lib.mkOption { type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.pathInStore); default = {}; description = '' Extra config snippets. The attribute name is their name, and the value their value. ''; example = lib.literalExpression '' { "redirects" = ' ' from PyQt5.QtCore import QUrl from qutebrowser.api import interceptor def intercept(info: interceptor.Request): if info.request_url.host() == 'www.youtube.com': new_url = QUrl(info.request_url) new_url.setHost('www.invidio.us') try: info.redirect(new_url) except interceptors.RedirectFailedException: pass interceptor.register(intercept) ' ' } ''; }; }; config = let qutebrowserConfig = lib.concatStringsSep "\n" ( [ ( if cfg.loadAutoconfig then "config.load_autoconfig()" else "config.load_autoconfig(False)" ) ] ++ builtins.map (c: configSet c null) (flattenSettings cfg.settings) ++ lib.lists.flatten (lib.mapAttrsToList urlConfigSet cfg.perUrlSettings) ++ lib.mapAttrsToList extraConfigSet cfg.extraConfig ); quickmarksFile = lib.optionals (cfg.quickmarks != {}) lib.concatStringsSep "\n" ( lib.mapAttrsToList formatQuickmarks cfg.quickmarks ); greasemonkeyDir = lib.optionals ( cfg.greasemonkey != [] ) pkgs.linkFarmFromDrvs "greasemonkey-userscripts" cfg.greasemonkey; in lib.mkIf cfg.enable { home = { packages = [cfg.package]; file = { ".qutebrowser/config.py" = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin { text = qutebrowserConfig; }; ".qutebrowser/quickmarks" = lib.mkIf (cfg.quickmarks != {} && pkgs.stdenv.hostPlatform.isDarwin) { text = quickmarksFile; }; ".qutebrowser/greasemonkey" = lib.mkIf (cfg.greasemonkey != [] && pkgs.stdenv.hostPlatform.isDarwin) { source = greasemonkeyDir; }; }; }; xdg = { configFile = { "qutebrowser/config.py" = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { text = qutebrowserConfig; onChange = '' hash="$(echo -n "$USER" | md5sum | cut -d' ' -f1)" socket="''${XDG_RUNTIME_DIR:-/run/user/$UID}/qutebrowser/ipc-$hash" if [[ -S $socket ]]; then command=${ lib.escapeShellArg ( builtins.toJSON { args = [":config-source"]; target_arg = null; protocol_version = 1; } ) } echo "$command" | ${pkgs.socat}/bin/socat -lf /dev/null - UNIX-CONNECT:"$socket" fi unset hash socket command ''; }; "qutebrowser/quickmarks" = lib.mkIf (cfg.quickmarks != {} && pkgs.stdenv.hostPlatform.isLinux) { text = quickmarksFile; }; "qutebrowser/greasemonkey" = lib.mkIf (cfg.greasemonkey != [] && pkgs.stdenv.hostPlatform.isLinux) { source = greasemonkeyDir; }; }; }; }; }