aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/qu/qutebrowser
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/by-name/qu/qutebrowser/include/redirects.py64
-rw-r--r--modules/by-name/qu/qutebrowser/module.hm.nix490
-rw-r--r--modules/by-name/qu/qutebrowser/module.nix87
-rw-r--r--modules/by-name/qu/qutebrowser/settings/default.nix534
-rw-r--r--modules/by-name/qu/qutebrowser/settings/keybindings.nix289
5 files changed, 1464 insertions, 0 deletions
diff --git a/modules/by-name/qu/qutebrowser/include/redirects.py b/modules/by-name/qu/qutebrowser/include/redirects.py
new file mode 100644
index 00000000..23456a25
--- /dev/null
+++ b/modules/by-name/qu/qutebrowser/include/redirects.py
@@ -0,0 +1,64 @@
+# Based on the redirect scripts from here:
+# - https://github.com/Phantop/dotfiles/blob/9f6256820dbb99cdf5afd373b74d93fe8b8972d7/qutebrowser/include/redirects.py
+# - https://gitlab.com/jgkamat/dotfiles/-/blob/0e5f05d347079a9839cb2c7878fd81d9850928ce/qutebrowser/.config/qutebrowser/pyconfig/redirectors.py
+
+from qutebrowser.api import interceptor, message
+
+import operator
+import typing
+
+from PyQt6.QtCore import QUrl
+
+
+def get_host(url: QUrl, base: bool = False) -> str:
+ host = url.host()
+ if base:
+ # www.someSite.com -> someSite.com
+ return ".".join(host.split(".")[-2:])
+ else:
+ return host
+
+
+def partial(func, *part_args):
+ def wrapper(*extra_args):
+ return func(*part_args, *extra_args)
+
+ return wrapper
+
+
+def farside_redir(target: str, url: QUrl) -> bool:
+ url.setHost("farside.link")
+ url.setPath("/" + target + url.path())
+ return True
+
+
+# Any return value other than a literal 'False' means we redirect
+REDIRECT_MAP: typing.Dict[str, typing.Callable[..., typing.Optional[bool]]] = {
+ "reddit.com": operator.methodcaller("setHost", "redlib.vhack.eu"),
+ # Source: https://libredirect.github.io/
+ "medium.com": partial(farside_redir, "scribe"),
+ "stackoverflow.com": partial(farside_redir, "anonymousoverflow"),
+ "goodreads.com": partial(farside_redir, "biblioreads"),
+}
+
+
+def rewrite(info: interceptor.Request):
+ if (
+ info.resource_type != interceptor.ResourceType.main_frame
+ or info.request_url.scheme() in {"data", "blob"}
+ ):
+ return
+
+ url = info.request_url
+
+ redir = REDIRECT_MAP.get(get_host(url, base=False))
+ if redir is None:
+ # Try again, but now only with the base host.
+ redir = REDIRECT_MAP.get(get_host(url, base=True))
+
+ if redir is not None and redir(url) is not False:
+ message.info("Redirecting to " + url.toString())
+ info.redirect(url)
+
+
+interceptor.register(rewrite)
diff --git a/modules/by-name/qu/qutebrowser/module.hm.nix b/modules/by-name/qu/qutebrowser/module.hm.nix
new file mode 100644
index 00000000..3f0f0d52
--- /dev/null
+++ b/modules/by-name/qu/qutebrowser/module.hm.nix
@@ -0,0 +1,490 @@
+{
+ 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 = {
+ "<Ctrl-v>" = "spawn mpv {url}";
+ ",p" = "spawn --userscript qute-pass";
+ ",l" = '''config-cycle spellcheck.languages ["en-GB"] ["en-US"]''';
+ "<F1>" = mkMerge [
+ "config-cycle tabs.show never always"
+ "config-cycle statusbar.show in-mode always"
+ "config-cycle scrolling.bar never always"
+ ];
+ };
+ prompt = {
+ "<Ctrl-y>" = "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 <https://qutebrowser.org/doc/help/settings.html>
+ 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 <https://qutebrowser.org/doc/help/settings.html> 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;
+ };
+ };
+ };
+ };
+}
diff --git a/modules/by-name/qu/qutebrowser/module.nix b/modules/by-name/qu/qutebrowser/module.nix
new file mode 100644
index 00000000..20ab8e4b
--- /dev/null
+++ b/modules/by-name/qu/qutebrowser/module.nix
@@ -0,0 +1,87 @@
+{
+ config,
+ lib,
+ pkgs,
+ self,
+ ...
+}: let
+ cfg = config.soispha.programs.qutebrowser;
+
+ qutebrowsersWithProfiles = let
+ xdg_data_home = config.home-manager.users.soispha.xdg.dataHome;
+ xdg_config_home = config.home-manager.users.soispha.xdg.configHome;
+
+ mkQutebrowser = name:
+ pkgs.runCommandLocal "qutebrowser-${name}" {
+ nativeBuildInputs = [
+ pkgs.makeBinaryWrapper
+ ];
+ }
+ ''
+ makeWrapper ${pkgs.qutebrowser-patched} "$out/bin/qutebrowser-${name}" \
+ --add-flags --no-err-windows \
+ --add-flags --basedir \
+ --add-flags "${xdg_data_home}/qutebrowser/${name}" \
+ --add-flags --config \
+ --add-flags "${xdg_config_home}/qutebrowser/config.py" \
+ '';
+ in
+ builtins.filter (val: val != null) (lib.mapAttrsToList (name: value:
+ if value.enable
+ then mkQutebrowser name
+ else null)
+ cfg.profiles);
+in {
+ options.soispha.programs.qutebrowser = {
+ enable = lib.mkEnableOption "qutebrowser";
+
+ key = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ The gpg key, used when decrypting the keepassxc connection key.
+ '';
+ };
+
+ profiles = lib.mkOption {
+ type = lib.types.attrsOf (lib.types.submodule {
+ options = {
+ enable =
+ (lib.mkEnableOption "this profile")
+ // {
+ default = true;
+ };
+ };
+ });
+
+ description = "A name enable map of profies to create besides the default `default` profile.";
+ default = {};
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ home-manager.users.soispha = {
+ disabledModules = [
+ "${self.inputs.home-manager}/modules/programs/qutebrowser.nix"
+ ];
+ imports = [
+ ./module.hm.nix
+ ];
+
+ home.packages = qutebrowsersWithProfiles;
+
+ programs.qutebrowser = {
+ enable = true;
+ package = pkgs.hello; # TODO: Set to null, once supported <2025-06-06>
+
+ settings = import ./settings {
+ inherit lib pkgs;
+ cfg = config.soispha.programs.qutebrowser;
+ };
+
+ extraConfig = {
+ "redirects" = ./include/redirects.py;
+ };
+ };
+ };
+ };
+}
diff --git a/modules/by-name/qu/qutebrowser/settings/default.nix b/modules/by-name/qu/qutebrowser/settings/default.nix
new file mode 100644
index 00000000..20e02e93
--- /dev/null
+++ b/modules/by-name/qu/qutebrowser/settings/default.nix
@@ -0,0 +1,534 @@
+{
+ lib,
+ pkgs,
+ cfg,
+}: let
+ millisecond = 1;
+ second = 1000 * millisecond;
+
+ wordlist =
+ pkgs.runCommandNoCCLocal "wordlist" {
+ nativeBuildInputs = [pkgs.python3];
+ }
+ ''
+ python ${
+ pkgs.writeText "normalize-words"
+ # python
+ ''
+ import string
+ import os
+
+ # Perform qutebrowser's normalizations, to reduce our dependency closure.
+ # See <//qutebrowser/qutebrowser/browser/hints.py> for them.
+ with open("${pkgs.scowl}/share/dict/words.txt", encoding = "latin1") as wordfile:
+ alphabet = set(string.ascii_lowercase)
+ hints = set()
+ lines = (line.rstrip().lower() for line in wordfile)
+ for word in lines:
+ if len(word) > 4:
+ # we don't need words longer than 4
+ continue
+ if set(word) - alphabet:
+ # contains none-alphabetic chars
+ continue
+ for i in range(len(word)):
+ # remove all prefixes of this word
+ hints.discard(word[:i + 1])
+ hints.add(word)
+
+ with open(os.environ["out"], "w") as out:
+ for word in hints:
+ out.write(word)
+ out.write("\n")
+ ''
+ }
+ '';
+in {
+ aliases = {
+ q = "close";
+ qa = "quit";
+ w = "session-save";
+ wq = "quit --save";
+ wqa = "quit --save";
+ read = "spawn --userscript readability";
+ };
+
+ auto_save = {
+ interval = 10 * second;
+ session = true; # Safe/restore the session
+ };
+
+ backend = "webengine";
+
+ bindings = {
+ default = {}; # Disable the default key bindings
+
+ commands = import ./keybindings.nix {inherit cfg lib;};
+
+ key_mappings = {
+ "<Ctrl-6>" = "<Ctrl-^>";
+ "<Ctrl-Enter>" = "<Ctrl-Return>";
+ "<Ctrl-I>" = "<Tab>";
+ "<Ctrl-J>" = "<Return>";
+ "<Ctrl-M>" = "<Return>";
+ "<Ctrl-[>" = "<Escape>";
+ "<Enter>" = "<Return>";
+ "<Shift-Enter>" = "<Return>";
+ "<Shift-Return>" = "<Return>";
+ };
+ };
+
+ changelog_after_upgrade = "minor";
+
+ fonts = {};
+ colors = {
+ # TODO(@bpeetz): Use a nord color scheme (or stylix). <2025-05-31>
+
+ # Make things readable.
+ messages.error.fg = "black";
+ completion.match.fg = "black";
+
+ webpage = {
+ darkmode = {
+ enabled = true;
+ algorithm = "lightness-cielab";
+ policy = {
+ images = "smart";
+ page = "smart";
+ };
+ threshold = {
+ background = 0; # 0 -> always
+ foreground = 256; # 256 -> always
+ };
+ };
+ preferred_color_scheme = "auto";
+ };
+ };
+
+ completion = {
+ cmd_history_max_items = -1; # No limit.
+ delay = 0; # delay before updating completions in milliseconds.
+ favorite_paths = [];
+ height = "50%";
+ min_chars = 1;
+ open_categories = ["searchengines" "quickmarks" "bookmarks" "history" "filesystem"];
+
+ quick = true; # Select, when only one left.
+
+ scrollbar = {
+ padding = 0;
+ width = 0;
+ };
+
+ show = "auto"; # Only show, when selected
+
+ shrink = true;
+
+ use_best_match = false;
+ web_history = {
+ exclude = [];
+ max_items = -1; # unlimited.
+ };
+ };
+
+ confirm_quit = [
+ "downloads"
+ ];
+
+ content = {
+ autoplay = false;
+
+ blocking = {
+ enabled = true;
+
+ # Sources for brave's adblocker
+ adblock = {
+ lists = [
+ "https://easylist.to/easylist/easylist.txt"
+ "https://easylist.to/easylist/easyprivacy.txt"
+ ];
+ };
+
+ # Sources for the (simpler) /etc/hosts based one.
+ hosts = {
+ block_subdomains = true;
+ lists = [];
+ };
+
+ method = "adblock"; # Only brave's
+ whitelist = [];
+ };
+
+ # Allow websites to read canvas elements.
+ canvas_reading = true;
+
+ cookies = {
+ store = true;
+ accept = "no-3rdparty";
+ };
+
+ default_encoding = "utf-8";
+
+ dns_prefetch = true;
+
+ fullscreen = {
+ # Fullscreen is limited to the browser window.
+ window = true;
+ overlay_timeout = 3 * second;
+ };
+
+ headers = {
+ accept_language = "en-CA,sv,en;q=0.9";
+ custom = {};
+ do_not_track = true;
+ referer = "same-domain";
+ user_agent = "Mozilla/5.0 ({os_info}) AppleWebKit/{webkit_version} (KHTML, like Gecko) {upstream_browser_key}/{upstream_browser_version_short} Safari/{webkit_version}";
+ };
+
+ hyperlink_auditing = false;
+
+ # Auto-load images.
+ images = true;
+
+ javascript = {
+ enabled = true;
+
+ can_open_tabs_automatically = false;
+ alert = true;
+ clipboard = "ask";
+ legacy_touch_events = "never";
+
+ log = {
+ error = "debug";
+ warning = "debug";
+ info = "debug";
+ unknown = "debug";
+ };
+
+ modal_dialog = false;
+ prompt = true;
+ };
+
+ local_content_can_access_file_urls = true;
+ local_content_can_access_remote_urls = false;
+
+ local_storage = true;
+
+ desktop_capture = "ask";
+ geolocation = "ask";
+ persistent_storage = "ask";
+ mouse_lock = "ask";
+ register_protocol_handler = "ask";
+ media = {
+ audio_capture = "ask";
+ audio_video_capture = "ask";
+ video_capture = "ask";
+ };
+
+ mute = false;
+
+ notifications = {
+ # TODO(@bpeetz): I might change that. <2025-05-31>
+ enabled = true;
+
+ presenter = "libnotify";
+ show_origin = true;
+ };
+
+ pdfjs = false;
+
+ # TODO(@bpeetz): What are “plugins in Web pages”? <2025-05-31>
+ plugins = false;
+
+ prefers_reduced_motion = false;
+ print_element_backgrounds = true;
+ private_browsing = false;
+ proxy = "system";
+
+ site_specific_quirks = {
+ enabled = true;
+ skip = [];
+ };
+
+ tls = {
+ certificate_errors = "ask";
+ };
+
+ unknown_url_scheme_policy = "allow-from-user-interaction";
+ user_stylesheets = [];
+
+ webgl = true;
+
+ webrtc_ip_handling_policy = "all-interfaces";
+ xss_auditing = false;
+ };
+
+ downloads = {
+ location = {
+ # Use OS default.
+ directory = null;
+ prompt = true;
+ remember = true;
+ suggestion = "path";
+ };
+
+ # TODO(@bpeetz): I might want to set that. <2025-05-31>
+ open_dispatcher = null;
+
+ position = "top";
+ prevent_mixed_content = true;
+
+ # Never remove downloads without my intervention.
+ remove_finished = -1;
+ };
+
+ editor = {
+ command = [
+ "${lib.getExe pkgs.alacritty}"
+ "--title"
+ "floating please"
+ "--command"
+ "nvim"
+ "-c"
+ "normal {line}G{column0}|"
+ "{file}"
+ ];
+ encoding = "utf-8";
+ remove_file = true;
+ };
+
+ fileselect = {
+ handler = "default";
+ };
+
+ hints = {
+ auto_follow = "unique-match";
+ auto_follow_timeout = 0;
+
+ chars = "asdfghjkl";
+ dictionary = "${wordlist}";
+
+ hide_unmatched_rapid_hints = true;
+ leave_on_load = false;
+
+ min_chars = 1;
+ next_regexes = ["\\bnext\\b" "\\bmore\\b" "\\bnewer\\b" "\\b[>→≫]\\b" "\\b(>>|»)\\b" "\\bcontinue\\b"];
+ prev_regexes = ["\\bprev(ious)?\\b" "\\bback\\b" "\\bolder\\b" "\\b[<←≪]\\b" "\\b(<<|«)\\b"];
+
+ scatter = true;
+ uppercase = false;
+ mode = "word";
+ };
+
+ input = {
+ escape_quits_reporter = false;
+ forward_unbound_keys = "auto";
+
+ insert_mode = {
+ auto_enter = true;
+ auto_leave = true;
+ auto_load = false;
+ leave_on_load = true;
+ plugins = false;
+ };
+
+ links_included_in_focus_chain = true;
+ match_counts = true;
+
+ media_keys = true;
+
+ mode_override = null;
+
+ mouse = {
+ back_forward_buttons = true;
+ rocker_gestures = false;
+ };
+
+ partial_timeout = 0;
+ spatial_navigation = false;
+ };
+
+ keyhint = {
+ blacklist = [];
+ delay = 250 * millisecond;
+ };
+
+ logging = {
+ level = {
+ console = "info";
+ ram = "debug";
+ };
+ };
+
+ messages = {
+ timeout = 3 * second;
+ };
+
+ new_instance_open_target = "tab";
+ new_instance_open_target_window = "last-focused";
+
+ prompt = {
+ filebrowser = true;
+ };
+
+ qt = {
+ args = [];
+
+ chromium = {
+ experimental_web_platform_features = "auto";
+ low_end_device_mode = "auto";
+ process_model = "process-per-site-instance";
+ sandboxing = "enable-all";
+ };
+
+ environ = {};
+ force_platform = null;
+ force_platformtheme = null;
+ force_software_rendering = "none";
+
+ highdpi = true;
+
+ workarounds = {
+ disable_accelerated_2d_canvas = "auto";
+ disable_hangouts_extension = false;
+ locale = false;
+ remove_service_workers = false;
+ };
+ };
+
+ scrolling = {
+ bar = "never";
+ smooth = false;
+ };
+
+ search = {
+ ignore_case = "smart";
+ incremental = true;
+ wrap = true;
+ wrap_messages = true;
+ };
+
+ session = {
+ default_name = null;
+ # TODO(@bpeetz): See https://github.com/qutebrowser/qutebrowser/issues/67 <2025-06-04>
+ lazy_restore = false;
+ };
+
+ spellcheck = {
+ # TODO(@bpeetz): Installing them (reproducibly) is simply not worth the hassle, as
+ # qutebrowser already provides quick access to an editor. <2025-06-04>
+ # languages = ["en-GB" "sv-SE" "de-DE"];
+ };
+
+ statusbar = {
+ padding = {
+ bottom = 5;
+ left = 5;
+ right = 5;
+ top = 5;
+ };
+
+ position = "top";
+ show = "always";
+ widgets = ["keypress" "search_match" "url" "scroll" "history" "tabs" "progress"];
+ };
+
+ tabs = {
+ background = true;
+ close_mouse_button = "middle";
+ close_mouse_button_on_bar = "new-tab";
+
+ favicons = {
+ scale = 1;
+ show = "always";
+ };
+
+ focus_stack_size = 10;
+
+ last_close = "default-page";
+
+ max_width = -1;
+ min_width = -1;
+
+ mode_on_change = "normal";
+
+ mousewheel_switching = true;
+
+ new_position = {
+ related = "next";
+ unrelated = "next";
+ stacking = true;
+ };
+
+ pinned = {
+ frozen = true;
+ shrink = true;
+ };
+
+ position = "top";
+
+ select_on_remove = "last-used";
+
+ show = "multiple";
+ show_switching_delay = 2 * second;
+
+ tabs_are_windows = false;
+ title = {
+ alignment = "center";
+ elide = "middle";
+ format = "{audio}{index}: {current_title}";
+ format_pinned = "{index}";
+ };
+
+ tooltips = true;
+ undo_stack_size = 100;
+ width = "15%";
+ wrap = true;
+ };
+
+ url = {
+ auto_search = "naive";
+ default_page = "qute://start";
+ incdec_segments = ["path" "query"];
+
+ open_base_url = false; # Of search engine.
+ searchengines = rec {
+ DEFAULT = leta;
+
+ leta = "https://leta.mullvad.net/search?q={}";
+ "@ls" = leta;
+
+ # NIX
+ "@np" = "https://search.nixos.org/packages?type=packages&query={}"; # Nix packages
+
+ "@ng" = "https://noogle.dev/q?term={}"; # Nix functions
+
+ "@no" = "https://search.nixos.org/options?type=options&query={}"; # NixOS options
+ "@nh" = "https://home-manager-options.extranix.com/?query={}&release=master"; # Home-Manager options
+
+ "@ni" = "https://github.com/NixOS/nixpkgs/issues?q=is%3Aissue+is%3Aopen+{}"; # Nixpkgs issues
+ "@nr" = "https://github.com/NixOS/nixpkgs/pulls?q=is%3Apr+is%3Aopen+{}"; # Nixpkgs pull requests
+
+ "@nt" = "https://nixpk.gs/pr-tracker.html?pr={}"; # Nixpkgs pull requests tracker
+ "@nw" = "https://wiki.nixos.org/w/index.php?search={}"; # NixOS Wiki
+
+ # RUST
+ "@rs" = "https://doc.rust-lang.org/std/?search={}"; # Rust std
+ "@rt" = "https://docs.rs/tokio/latest/tokio/index.html?search={}"; # Rust tokio
+
+ # OTHER
+ "@gs" = "https://scholar.google.com/scholar?hl=en&q={}"; # Google Scholar
+ "@wp" = "https://en.wikipedia.org/wiki/{}"; # Wikipedia
+ "@aw" = "https://wiki.archlinux.org/index.php?search={}"; # Arch Wiki
+ };
+
+ start_pages = ["qute://start"];
+ yank_ignored_parameters = ["ref" "utm_source" "utm_medium" "utm_campaign" "utm_term" "utm_content" "utm_name"];
+ };
+
+ window = {
+ hide_decoration = true;
+ title_format = "{perc}{current_title}{title_sep}qutebrowser";
+ transparent = false;
+ };
+}
diff --git a/modules/by-name/qu/qutebrowser/settings/keybindings.nix b/modules/by-name/qu/qutebrowser/settings/keybindings.nix
new file mode 100644
index 00000000..fde78457
--- /dev/null
+++ b/modules/by-name/qu/qutebrowser/settings/keybindings.nix
@@ -0,0 +1,289 @@
+{
+ cfg,
+ lib,
+}: {
+ caret = {
+ "<Escape>" = "mode-leave";
+
+ H = "scroll left";
+ T = "scroll down";
+ N = "scroll up";
+ S = "scroll right";
+ h = "move-to-prev-char";
+ t = "move-to-next-line";
+ n = "move-to-prev-line";
+ s = "move-to-next-char";
+
+ "$" = "move-to-end-of-line";
+ "0" = "move-to-start-of-line";
+ G = "move-to-end-of-document";
+ gg = "move-to-start-of-document";
+
+ b = "move-to-prev-word";
+ c = "mode-enter normal";
+ e = "move-to-end-of-word";
+ w = "move-to-next-word";
+
+ "{" = "move-to-end-of-prev-block";
+ "}" = "move-to-end-of-next-block";
+ "[" = "move-to-start-of-prev-block";
+ "]" = "move-to-start-of-next-block";
+
+ y = "yank selection";
+ Y = "yank selection -s";
+ V = "selection-toggle --line";
+ v = "selection-toggle";
+ "<Ctrl-Space>" = "selection-drop";
+ "<Return>" = "yank selection";
+ "<Space>" = "selection-toggle";
+ o = "selection-reverse";
+ };
+
+ command = {
+ "<Alt-B>" = "rl-backward-word";
+ "<Alt-Backspace>" = "rl-backward-kill-word";
+ "<Alt-D>" = "rl-kill-word";
+ "<Alt-F>" = "rl-forward-word";
+ "<Ctrl-?>" = "rl-delete-char";
+ "<Ctrl-A>" = "rl-beginning-of-line";
+ "<Ctrl-B>" = "rl-backward-char";
+ "<Ctrl-C>" = "completion-item-yank";
+ "<Ctrl-D>" = "completion-item-del";
+ "<Ctrl-E>" = "rl-end-of-line";
+ "<Ctrl-F>" = "rl-forward-char";
+ "<Ctrl-H>" = "rl-backward-delete-char";
+ "<Ctrl-K>" = "rl-kill-line";
+ "<Ctrl-N>" = "command-history-next";
+ "<Ctrl-P>" = "command-history-prev";
+ "<Ctrl-Return>" = "command-accept --rapid";
+ "<Ctrl-Shift-C>" = "completion-item-yank --sel";
+ "<Ctrl-Shift-Tab>" = "completion-item-focus prev-category";
+ "<Ctrl-Shift-W>" = "rl-filename-rubout";
+ "<Ctrl-Tab>" = "completion-item-focus next-category";
+ "<Ctrl-U>" = "rl-unix-line-discard";
+ "<Ctrl-W>" = "rl-rubout \" \"";
+ "<Ctrl-Y>" = "rl-yank";
+ "<Down>" = "completion-item-focus --history next";
+ "<Escape>" = "mode-leave";
+ "<PgDown>" = "completion-item-focus next-page";
+ "<PgUp>" = "completion-item-focus prev-page";
+ "<Return>" = "command-accept";
+ "<Shift-Delete>" = "completion-item-del";
+ "<Shift-Tab>" = "completion-item-focus prev";
+ "<Tab>" = "completion-item-focus next";
+ "<Up>" = "completion-item-focus --history prev";
+ };
+
+ hint = {
+ "<Ctrl-B>" = "hint all tab-bg";
+ "<Ctrl-F>" = "hint links";
+ "<Ctrl-R>" = "hint --rapid links tab-bg";
+ "<Escape>" = "mode-leave";
+ "<Return>" = "hint-follow";
+ };
+
+ insert = {
+ "<Ctrl-E>" = "edit-text";
+ "<Escape>" = "mode-leave";
+ "<Shift-Escape>" = "fake-key <Escape>";
+ "<Shift-Ins>" = "insert-text -- {primary}";
+ "<Ctrl-k>" = "spawn --userscript qute-keepassxc --key ${lib.escapeShellArg cfg.key}";
+ };
+
+ normal = {
+ # a
+ # b
+ # c
+ # d -> download management
+ # e -> [e]merge tab
+ # f -> `hint all`
+ # g -> tabs [g]oing
+ # h -> `back`
+ # i -> `mode-enter insert`
+ # j -> inputs
+ # k
+ # l -> `search-next`
+ # m -> in page movement
+ # n -> `scroll up`
+ # o
+ # p
+ # q -> `macro-record`
+ # r
+ # s -> `forward`
+ # t -> `scroll down`
+ # u -> `undo`
+ # v -> `mode-enter caret`
+ # w -> theme switching
+ # x -> hinting
+ # y -> yanking
+ # z
+
+ # Theme switching
+ wd = "set --temp colors.webpage.darkmode.enabled true";
+ wl = "set --temp colors.webpage.darkmode.enabled false";
+
+ # Tab [g]oing
+ "g$" = "tab-focus -1";
+ g0 = "tab-focus 1";
+ gh = "home";
+ gH = "history";
+ gs = "cmd-set-text --space :open";
+ gP = "open -- {primary}";
+ gp = "open -- {clipboard}";
+ go = "cmd-set-text :open {url:pretty}";
+ gt = "cmd-set-text --space :tab-select";
+ # TODO(@bpeetz): Make this to a lf tab listing. <2025-05-31>
+ "\\f" = "cmd-set-text --space --relative :tab-focus";
+
+ eP = "open --tab -- {primary}";
+ ep = "open --tab -- {clipboard}";
+ ea = "open --tab";
+ es = "cmd-set-text --space :open --tab";
+ eo = "cmd-set-text :open --tab --related {url:pretty}";
+ eh = "back --tab";
+ en = "forward --tab";
+ ec = "tab-clone";
+ em = "tab-move";
+ ed = "tab-close";
+ eR = "reload --force";
+ er = "reload";
+
+ # Download management
+ dm = "download";
+ dc = "download-cancel";
+ dp = "download-clear";
+ do = "download-open";
+ dr = "download-retry";
+
+ # In page hierarchy [m]ovement
+ mi = "navigate increment";
+ md = "navigate increment";
+ mp = "navigate prev";
+ mn = "navigate next";
+ mu = "navigate up";
+ mh = "navigate strip";
+
+ # Page movement
+ H = "scroll left";
+ t = "scroll down";
+ n = "scroll up";
+ S = "scroll right";
+ h = "back";
+ T = "tab-prev";
+ N = "tab-next";
+ s = "forward";
+ G = "scroll-to-perc";
+ gg = "scroll-to-perc 0";
+
+ l = "search-next";
+ L = "search-prev";
+
+ "+" = "zoom-in";
+ "-" = "zoom-out";
+ "=" = "zoom";
+
+ "." = "cmd-repeat-last";
+ "/" = "cmd-set-text /";
+ "?" = "cmd-set-text ?";
+ ":" = "cmd-set-text :";
+
+ # Hinting
+ xI = "hint images tab";
+ xi = "hint images";
+ xO = "hint links fill :open --tab --related {hint-url}";
+ xo = "hint links fill :open {hint-url}";
+ xY = "hint links yank-primary";
+ xy = "hint links yank";
+ xb = "hint all tab-bg";
+ xf = "hint all tab-fg";
+ xd = "hint links download";
+ xh = "hint all hover";
+ xr = "hint --rapid links tab-bg";
+ xt = "hint inputs";
+ f = "hint all";
+
+ # Inputs
+ jf = "hint inputs --first";
+ jk = "spawn --userscript qute-keepassxc --key ${lib.escapeShellArg cfg.key}";
+
+ # Yanking
+ yD = "yank domain --sel";
+ yM = "yank inline [{title}]({url:yank}) --sel";
+ yP = "yank pretty-url --sel";
+ yT = "yank title --sel";
+ yY = "yank --sel";
+ yd = "yank domain";
+ ym = "yank inline [{title}]({url:yank})";
+ yp = "yank pretty-url";
+ yt = "yank title";
+ yy = "yank";
+
+ "<Escape>" = "clear-keychain ;; search ;; fullscreen --leave ;; clear-messages";
+ "<Shift-Space>" = "scroll-page 0 -1";
+ "<Space>" = "scroll-page 0 1";
+ "<Ctrl-Return>" = "selection-follow --tab";
+ "<Return>" = "selection-follow";
+ "<back>" = "back";
+ "<forward>" = "forward";
+
+ i = "mode-enter insert";
+ "'" = "mode-enter jump_mark";
+ "`" = "mode-enter set_mark";
+ v = "mode-enter caret";
+ V = "mode-enter caret ;; selection-toggle --line";
+ "<Ctrl-V>" = "mode-enter passthrough";
+
+ q = "macro-record";
+ "@" = "macro-run";
+ u = "undo";
+ U = "undo --window";
+ };
+
+ passthrough = {
+ "<Shift-Escape>" = "mode-leave";
+ };
+
+ prompt = {
+ "<Alt-B>" = "rl-backward-word";
+ "<Alt-Backspace>" = "rl-backward-kill-word";
+ "<Alt-D>" = "rl-kill-word";
+ "<Alt-E>" = "prompt-fileselect-external";
+ "<Alt-F>" = "rl-forward-word";
+ "<Alt-Shift-Y>" = "prompt-yank --sel";
+ "<Alt-Y>" = "prompt-yank";
+ "<Ctrl-?>" = "rl-delete-char";
+ "<Ctrl-A>" = "rl-beginning-of-line";
+ "<Ctrl-B>" = "rl-backward-char";
+ "<Ctrl-E>" = "rl-end-of-line";
+ "<Ctrl-F>" = "rl-forward-char";
+ "<Ctrl-H>" = "rl-backward-delete-char";
+ "<Ctrl-K>" = "rl-kill-line";
+ "<Ctrl-P>" = "prompt-open-download --pdfjs";
+ "<Ctrl-Shift-W>" = "rl-filename-rubout";
+ "<Ctrl-U>" = "rl-unix-line-discard";
+ "<Ctrl-W>" = "rl-rubout \" \"";
+ "<Ctrl-X>" = "prompt-open-download";
+ "<Ctrl-Y>" = "rl-yank";
+ "<Down>" = "prompt-item-focus next";
+ "<Escape>" = "mode-leave";
+ "<Return>" = "prompt-accept";
+ "<Shift-Tab>" = "prompt-item-focus prev";
+ "<Tab>" = "prompt-item-focus next";
+ "<Up>" = "prompt-item-focus prev";
+ };
+
+ register = {
+ "<Escape>" = "mode-leave";
+ };
+
+ yesno = {
+ "<Alt-Shift-Y>" = "prompt-yank --sel";
+ "<Alt-Y>" = "prompt-yank";
+ "<Escape>" = "mode-leave";
+ "<Return>" = "prompt-accept";
+ N = "prompt-accept --save no";
+ Y = "prompt-accept --save yes";
+ n = "prompt-accept no";
+ y = "prompt-accept yes";
+ };
+}