From 47d459f583c99b4b38958bf66ae5bdf196bc8a65 Mon Sep 17 00:00:00 2001 From: "Gustavo \"Guz\" L de Mello" Date: Mon, 2 Jun 2025 22:15:04 -0300 Subject: [PATCH] feat: qutebrowser profiles (inspired by qbpm) --- flake.nix | 3 +- home/guz-lite/browser.nix | 19 ++ modules/home-manager/qutebrowser-profiles.nix | 302 ++++++++++++++++++ 3 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 modules/home-manager/qutebrowser-profiles.nix diff --git a/flake.nix b/flake.nix index 92c5c03..c94bf80 100644 --- a/flake.nix +++ b/flake.nix @@ -195,6 +195,7 @@ }; neovim = inputs.neovim.homeManagerModules.default; eww = ./modules/home-manager/eww.nix; + qutebrowser-profiles = ./modules/home-manager/qutebrowser-profiles.nix; }; packages = forAllSystems ({ @@ -208,7 +209,7 @@ neovim = inputs.neovim.packages.${pkgs.system}.default; devkit = - (import ./packages/devkit {inherit pkgs inputs;}) + (import ./packages/devkit {inherit inputs pkgs;}) // { neovim = self.packages.${pkgs.system}.neovim; }; diff --git a/home/guz-lite/browser.nix b/home/guz-lite/browser.nix index 6ef183a..93d94ac 100644 --- a/home/guz-lite/browser.nix +++ b/home/guz-lite/browser.nix @@ -3,6 +3,10 @@ pkgs, ... }: { + imports = [ + self.homeManagerModules.qutebrowser-profiles + ]; + programs.qutebrowser.enable = true; programs.qutebrowser.settings = { auto_save.session = true; @@ -88,4 +92,19 @@ ]; }) ]; + programs.qutebrowser.profiles = { + "art" = {}; + "personal" = {}; + "work" = { + searchEngines = { + # Languages + go = "https://pkg.go.dev/search?q={}"; + }; + }; + "job" = { + settings.confirm_quit = ["always"]; + }; + "shopping" = {}; + "goverment" = {}; + }; } diff --git a/modules/home-manager/qutebrowser-profiles.nix b/modules/home-manager/qutebrowser-profiles.nix new file mode 100644 index 0000000..9c24db5 --- /dev/null +++ b/modules/home-manager/qutebrowser-profiles.nix @@ -0,0 +1,302 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit + (lib) + concatStringsSep + literalExpression + mapAttrsToList + mkIf + mkOption + types + ; + + cfg = config.programs.qutebrowser; + + profileType = with lib; + with types; + submodule ({ + config, + lib, + ... + }: let + scfg = config; + in { + options = with lib; { + name = mkOption { + type = with types; nullOr str; + default = null; + }; + extendDefault = mkOption { + type = with types; bool; + default = true; + }; + titleFormat = mkOption { + type = with types; str; + default = "{perc}{current_title}{title_sep} qutebrowser ({profile_name})"; + }; + desktopName = mkOption { + type = with types; nullOr str; + default = null; + }; + aliases = mkOption { + type = with types; attrsOf str; + default = {}; + }; + loadAutoconfig = mkOption { + type = with types; bool; + default = cfg.loadAutoconfig; + }; + searchEngines = mkOption { + type = with types; attrsOf str; + default = {}; + }; + settings = mkOption { + type = with types; attrsOf anything; + default = {}; + }; + keyMappings = mkOption { + type = with types; attrsOf str; + default = {}; + }; + enableDefaultBindings = mkOption { + type = with types; bool; + default = cfg.enableDefaultBindings; + }; + keyBindings = mkOption { + type = with types; attrsOf (attrsOf (nullOr (separatedString " ;; "))); + default = {}; + }; + quickmarks = mkOption { + type = with types; attrsOf str; + default = {}; + apply = v: + if scfg.extendDefault + then cfg.quickmarks // v + else v; + }; + greasemonkey = mkOption { + type = with types; listOf package; + default = []; + apply = v: + if scfg.extendDefault + then cfg.greasemonkey ++ v + else v; + }; + extraConfig = mkOption { + type = with types; lines; + default = ""; + }; + }; + }); + + formatLine = o: n: v: let + formatValue = v: + if v == null + then "None" + else if builtins.isBool v + then + ( + if v + then "True" + else "False" + ) + else if builtins.isString v + then ''"${v}"'' + else if builtins.isList v + then "[${concatStringsSep ", " (map formatValue v)}]" + else builtins.toString v; + in + if builtins.isAttrs v + then concatStringsSep "\n" (mapAttrsToList (formatLine "${o}${n}.") v) + else "${o}${n} = ${formatValue v}"; + + formatDictLine = o: n: v: ''${o}['${n}'] = "${v}"''; + + formatKeyBindings = m: b: let + formatKeyBinding = m: k: c: + if c == null + then ''config.unbind("${k}", mode="${m}")'' + else ''config.bind("${k}", "${lib.escape [''"''] c}", mode="${m}")''; + in + concatStringsSep "\n" (mapAttrsToList (formatKeyBinding m) b); + + formatQuickmarks = n: s: "${n} ${s}"; +in + with lib; { + options.programs.qutebrowser = { + profileChooser = mkOption { + type = with types; either bool (enum ["override"]); + default = "override"; + }; + profileChooserDMenu = mkOption { + type = with types; package; + default = pkgs.writeShellScriptBin "rofi" "${getExe config.programs.rofi.finalPackage} -dmenu $@"; + }; + profileChooserUserscript = mkOption { + type = with types; nullOr str; + default = "W"; + }; + profiles = mkOption { + type = with types; attrsOf profileType; + default = {}; + }; + }; + config = let + profiles = mapAttrsToList (n: v: n) cfg.profiles; + profilesFile = pkgs.writeText "qutebrowser-profiles" (concatStringsSep "\n" (["DEFAULT"] ++ profiles)); + + chooser = pkgs.writeShellScriptBin "qutebrowser-chooser" '' + profile="$(cat ${profilesFile} | ${getExe cfg.profileChooserDMenu})" + case "$profile" in + "DEFAULT") + ${getExe pkgs.qutebrowser} $@ + ;; + ${concatMapStringsSep "\n" (n: let + profileDir = "${config.xdg.dataHome}/qutebrowser-profiles/${n}"; + in '' + "${n}") + ${getExe pkgs.qutebrowser} -B ${profileDir} -C ${profileDir}/config.py --desktop-file-name qutebrowser-profile-${n} $@ + ;; + '') + profiles} + esac + ''; + userscript = pkgs.writeShellScriptBin "qutebrowser-profiles-userscript" '' + function close() { + echo "close" >> "$QUTE_FIFO" + } + ${getExe chooser} & close + ''; + in + mkIf (cfg.enable && cfg.profiles != {}) { + programs.qutebrowser.package = pkgs.stdenv.mkDerivation { + inherit (pkgs.qutebrowser) name pname meta; + buildCommand = let + desktopEntry = pkgs.makeDesktopItem { + name = "qutebrowser"; + desktopName = "qutebrowser"; + exec = "${getExe ( + if cfg.profileChooser == "override" + then chooser + else pkgs.package + )} %u"; + }; + chooserDesktopEntry = pkgs.makeDesktopItem { + name = "qutebrowser chooser"; + desktopName = "qutebrowser-chooser"; + exec = "${getExe chooser} %u"; + }; + in '' + mkdir -p $out/bin + cp ${getExe pkgs.qutebrowser} $out/bin + ${ + if cfg.profileChooser != false + then "cp ${getExe chooser} $out/bin" + else "" + } + + mkdir -p $out/share/applications + cp ${desktopEntry}/share/applications/qutebrowser.desktop $out/share/applications/qutebrowser.desktop + ${ + if cfg.profileChooser == true + then "cp ${chooserDesktopEntry}/share/applications/qutebrowser-chooser.desktop $out/share/applications/qutebrowser-chooser.desktop" + else "" + } + ''; + dontBuild = true; + }; + + xdg.desktopEntries = mergeAttrsList (mapAttrsToList (n: v: let + profileDir = "qutebrowser-profiles/${n}"; + in { + "qutebrowser-profile-${n}" = { + name = + if isNull v.desktopName + then "${ + if isNull v.name + then n + else v.name + } (qutebrowser profile)" + else v.desktopName; + exec = + "${getExe pkgs.qutebrowser}" + + " -B ${config.xdg.dataHome}/${profileDir}" + + " -C ${config.xdg.dataHome}/${profileDir}/config.py" + + " --desktop-file-name qutebrowser-profile-${n}" + + " %u"; + }; + }) + cfg.profiles); + + xdg.dataFile = mergeAttrsList (mapAttrsToList (n: v: let + profileDir = "qutebrowser-profiles/${n}"; + in { + "${profileDir}/config.py" = { + text = concatStringsSep "\n" ( + [ + ( + if v.loadAutoconfig + then "config.load_autoconfig()" + else "config.load_autoconfig(False)" + ) + ] + ++ lib.optional (v.titleFormat != "") '' + c.window.title_format = '${replaceStrings ["{profile}" "{profile_name}"] [ + n + ( + if isNull v.name + then n + else v.name + ) + ] + v.titleFormat}' + '' + ++ lib.optional (v.extendDefault) '' + config.source(r'${config.xdg.configHome}/qutebrowser/config.py') + '' + ++ mapAttrsToList (formatLine "c.") v.settings + ++ mapAttrsToList (formatDictLine "c.aliases") v.aliases + ++ mapAttrsToList (formatDictLine "c.url.searchengines") v.searchEngines + ++ mapAttrsToList (formatDictLine "c.bindings.key_mappings") v.keyMappings + ++ lib.optional (!v.enableDefaultBindings) "c.bindings.default = {}" + ++ mapAttrsToList formatKeyBindings v.keyBindings + ++ lib.optional (cfg.profileChooserUserscript != "" && !(isNull cfg.profileChooserUserscript)) + "config.bind(\"${cfg.profileChooserUserscript}\", \"spawn --userscript ${getExe userscript}\", mode=\"normal\")" + ++ lib.optional (v.extraConfig != "") cfg.extraConfig + ); + 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 + ''; + }; + + "${profileDir}/quickmarks" = mkIf (v.quickmarks != {}) { + text = concatStringsSep "\n" ( + mapAttrsToList formatQuickmarks v.quickmarks + ); + }; + + "${profileDir}/data/greasemonkey" = mkIf (v.greasemonkey != []) { + source = pkgs.linkFarmFromDrvs "greasemonkey-userscripts" v.greasemonkey; + }; + }) + cfg.profiles); + }; + }