diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-06-06 21:06:07 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-06-06 21:06:07 +0200 |
commit | 0ec2002665ce7bbf3238dc225d348e1c74a00cef (patch) | |
tree | 5e943763e77494ca764d87cd769b617ef761aff0 | |
parent | pkgs/fupdate: Move cli test into a `tests` module (diff) | |
download | nixos-config-0ec2002665ce7bbf3238dc225d348e1c74a00cef.zip |
{modules,pkgs}/qutebrowser: Enable qutebrowser support
-rw-r--r-- | modules/by-name/gp/gpg/module.nix | 3 | ||||
-rw-r--r-- | modules/by-name/qu/qutebrowser/include/redirects.py | 64 | ||||
-rw-r--r-- | modules/by-name/qu/qutebrowser/module.hm.nix | 490 | ||||
-rw-r--r-- | modules/by-name/qu/qutebrowser/module.nix | 87 | ||||
-rw-r--r-- | modules/by-name/qu/qutebrowser/settings/default.nix | 534 | ||||
-rw-r--r-- | modules/by-name/qu/qutebrowser/settings/keybindings.nix | 289 | ||||
-rw-r--r-- | modules/by-name/ts/tskm/module.nix | 13 | ||||
-rw-r--r-- | modules/common/default.nix | 4 | ||||
-rw-r--r-- | pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch | 54 | ||||
-rw-r--r-- | pkgs/by-name/qu/qutebrowser-patched/package.nix | 6 | ||||
-rwxr-xr-x | update.sh | 1 |
11 files changed, 1538 insertions, 7 deletions
diff --git a/modules/by-name/gp/gpg/module.nix b/modules/by-name/gp/gpg/module.nix index 89d7b356..d5136e92 100644 --- a/modules/by-name/gp/gpg/module.nix +++ b/modules/by-name/gp/gpg/module.nix @@ -22,6 +22,8 @@ in { }; config = lib.mkIf cfg.enable { + soispha.programs.qutebrowser.key = "8321 ED3A 8DB9 99A5 1F3B F80F F268 2914 EA42 DE26"; + home-manager.users.soispha = { programs.gpg = { enable = true; @@ -45,6 +47,7 @@ in { } ]; }; + services = { gpg-agent = { enable = true; 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"; + }; +} diff --git a/modules/by-name/ts/tskm/module.nix b/modules/by-name/ts/tskm/module.nix index 9bf2b389..6f6517f8 100644 --- a/modules/by-name/ts/tskm/module.nix +++ b/modules/by-name/ts/tskm/module.nix @@ -23,13 +23,13 @@ in [name] ++ subprojects; - firefoxProfiles = builtins.listToAttrs (lib.imap0 (index: name: - lib.attrsets.nameValuePair name { + qutebrowserProfiles = builtins.listToAttrs ( + builtins.map (name: { inherit name; - # Add one here, so that we can have the default profile at id 0. - id = index + 1; + value = {}; }) - allProjectNames); + allProjectNames + ); contexts = builtins.concatStringsSep "\n" @@ -99,7 +99,8 @@ in { config = lib.mkIf cfg.enable { soispha.programs = { - firefox.profiles = firefoxProfiles; + qutebrowser.profiles = qutebrowserProfiles; + taskwarrior = { includeFiles = { tskm-contexts = contextsFile; diff --git a/modules/common/default.nix b/modules/common/default.nix index 4ac4f6c7..d5dfb7a3 100644 --- a/modules/common/default.nix +++ b/modules/common/default.nix @@ -179,6 +179,10 @@ i3bar-river.enable = true; i3status-rust.enable = true; + qutebrowser = { + enable = true; + }; + nvim = { enable = true; shell = pkgs.zsh; diff --git a/pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch b/pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch new file mode 100644 index 00000000..fa2e2482 --- /dev/null +++ b/pkgs/by-name/qu/qutebrowser-patched/0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch @@ -0,0 +1,54 @@ +From 8a0aa0e244fa565b8c55aab38cc5e84323c3b481 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Tue, 3 Jun 2025 12:43:44 +0200 +Subject: [PATCH] fix(standardpaths): Continue to work with xdg, while + `--basedir` is set + +This can be used to simulate firefox's profiles feature (i.e., completely separated +data/runtime/cache dirs), while still keeping to the xdg basedir standard. +--- + qutebrowser/utils/standarddir.py | 19 +++++++++++-------- + 1 file changed, 11 insertions(+), 8 deletions(-) + +diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py +index b82845a96..61319daed 100644 +--- a/qutebrowser/utils/standarddir.py ++++ b/qutebrowser/utils/standarddir.py +@@ -285,12 +285,11 @@ def _from_args( + The overridden path, or None if there is no override. + """ + basedir_suffix = { +- QStandardPaths.StandardLocation.ConfigLocation: 'config', +- QStandardPaths.StandardLocation.AppDataLocation: 'data', +- QStandardPaths.StandardLocation.AppLocalDataLocation: 'data', +- QStandardPaths.StandardLocation.CacheLocation: 'cache', +- QStandardPaths.StandardLocation.DownloadLocation: 'download', +- QStandardPaths.StandardLocation.RuntimeLocation: 'runtime', ++ QStandardPaths.StandardLocation.ConfigLocation: ('config', False), ++ QStandardPaths.StandardLocation.AppDataLocation: ('data', False), ++ QStandardPaths.StandardLocation.AppLocalDataLocation: ('data', False), ++ QStandardPaths.StandardLocation.CacheLocation: ('cache', True), ++ QStandardPaths.StandardLocation.RuntimeLocation: ('runtime', True), + } + + if getattr(args, 'basedir', None) is None: +@@ -298,10 +297,14 @@ def _from_args( + assert args is not None + + try: +- suffix = basedir_suffix[typ] ++ (suffix, extend) = basedir_suffix[typ] + except KeyError: # pragma: no cover + return None +- return os.path.abspath(os.path.join(args.basedir, suffix)) ++ ++ if extend: ++ return os.path.abspath(os.path.join(_writable_location(typ), os.path.basename(args.basedir))) ++ else: ++ return os.path.abspath(os.path.join(args.basedir, suffix)) + + + def _create(path: str) -> None: +-- +2.49.0 + diff --git a/pkgs/by-name/qu/qutebrowser-patched/package.nix b/pkgs/by-name/qu/qutebrowser-patched/package.nix new file mode 100644 index 00000000..1f2ea889 --- /dev/null +++ b/pkgs/by-name/qu/qutebrowser-patched/package.nix @@ -0,0 +1,6 @@ +{qutebrowser}: +qutebrowser.overrideAttrs (final: prev: { + pname = "${prev.pname}-patched"; + + patches = (prev.patches or []) ++ [./0001-fix-standardpaths-Continue-to-work-with-xdg-while-ba.patch]; +}) diff --git a/update.sh b/update.sh index ff10f870..8f7d338e 100755 --- a/update.sh +++ b/update.sh @@ -22,7 +22,6 @@ __update_sh_run() { } __update_sh_run nix flake update -__update_sh_run ./modules/by-name/fi/firefox/update_extensions.sh "$@" __update_sh_run ./pkgs/update_pkgs.sh "$@" # __update_sh_run nix flake check |