From 23e775a9a3f3bcd401dc6dce769b8f9d27e895cd Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Fri, 20 Mar 2026 16:46:39 +0100 Subject: [PATCH 1/7] wip --- modules/home/brave/default.nix | 170 ++++++++--- modules/home/brave/set-preferences.nix | 53 +--- modules/home/brave/set-search-engines.nix | 48 +--- modules/home/brave/settings.nix | 326 ++++++++++++++++++++++ 4 files changed, 475 insertions(+), 122 deletions(-) create mode 100644 modules/home/brave/settings.nix diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index 21650324..5ec121df 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -9,13 +9,126 @@ with lib; let cfg = config.modules.brave; inherit (pkgs.stdenv) isDarwin isLinux; + + settingsMod = import ./settings.nix { inherit lib; }; + outputs = settingsMod.mkOutputs cfg.settings; + + extensionType = types.submodule { + options = { + id = mkOption { + type = types.str; + description = "Chrome Web Store extension ID."; + }; + pinned = mkOption { + type = types.bool; + default = false; + description = "Whether to pin this extension to the toolbar."; + }; + }; + }; + + searchEngineType = types.submodule { + options = { + name = mkOption { + type = types.str; + description = "Display name of the search engine."; + }; + url = mkOption { + type = types.str; + description = "Search URL template. Use {searchTerms} as placeholder."; + }; + favicon_url = mkOption { + type = types.str; + default = ""; + description = "URL to the search engine's favicon."; + }; + }; + }; + + pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( + filterAttrs (_: ext: ext.pinned) cfg.extensions + ); + + # Merge the extension-pinning preference into the preferences coming from + # the settings submodule. + allPreferences = + outputs.preferences + ++ optional (pinnedExtensionIds != [ ]) { + path = [ + "extensions" + "pinned_extensions" + ]; + value = pinnedExtensionIds; + }; in { options.modules.brave = { enable = mkEnableOption "Brave"; + + isDefaultBrowser = mkOption { + type = types.bool; + default = false; + description = "Whether to set Brave as the default browser."; + }; + + extensions = mkOption { + type = types.attrsOf extensionType; + default = { }; + description = "Extensions to install, keyed by a human-readable name."; + }; + + searchEngines = mkOption { + type = types.attrsOf searchEngineType; + default = { }; + description = '' + Custom search engines. The attribute name is used as the keyword + (shortcut) for the engine. + ''; + }; + + settings = settingsMod.options; }; config = mkIf cfg.enable { + modules.brave = { + isDefaultBrowser = true; + + extensions = { + proton-pass = { + id = "ghmbeldphafepmbegfdlkpapadhbakde"; + pinned = true; + }; + unhook.id = "khncfooichmfjbepaaaebmommgaepoid"; + }; + + searchEngines = { + hm = { + name = "Home Manager Options"; + url = "https://home-manager-options.extranix.com/?query={searchTerms}"; + favicon_url = "https://nixos.org/favicon.ico"; + }; + nixo = { + name = "NixOS options"; + url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; + favicon_url = "https://nixos.org/favicon.ico"; + }; + nixp = { + name = "Nix packages"; + url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; + favicon_url = "https://nixos.org/favicon.ico"; + }; + std = { + name = "std's docs"; + url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; + favicon_url = "https://rust-lang.org/logos/rust-logo-blk.svg"; + }; + }; + + settings = { + ntp.background.color = config.modules.colorschemes.palette.primary.background; + }; + }; + programs.brave = { enable = true; package = @@ -34,48 +147,39 @@ in } else pkgs.brave; - extensions = [ - { id = "ghmbeldphafepmbegfdlkpapadhbakde"; } # Proton Pass - { id = "khncfooichmfjbepaaaebmommgaepoid"; } # Unhook - ]; + extensions = mapAttrsToList (_: ext: { inherit (ext) id; }) cfg.extensions; }; # See https://chromeenterprise.google/policies/ and # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy for # the available policies. modules.macOSPreferences.apps."com.brave.Browser" = { - forced = { - AutofillAddressEnabled = false; - AutofillCreditCardEnabled = false; - BookmarkBarEnabled = false; - BraveAIChatEnabled = false; - BraveNewsDisabled = true; - BraveRewardsDisabled = true; - BraveStatsPingEnabled = false; - BraveTalkDisabled = true; - BraveVPNDisabled = true; - BraveWalletDisabled = true; - BrowserSignin = 0; - HomepageIsNewTabPage = true; - NewTabPageLocation = "about:blank"; - PasswordManagerEnabled = false; - SyncDisabled = true; - }; + forced = outputs.policies; }; - home.activation = lib.mkIf isDarwin { - setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - run ${pkgs.defaultbrowser}/bin/defaultbrowser browser - ''; - setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] ( - import ./set-preferences.nix { inherit config pkgs lib; } - ); - setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] ( - import ./set-search-engines.nix { inherit config pkgs lib; } - ); - }; + home.activation = lib.mkIf isDarwin ( + { + setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] ( + import ./set-preferences.nix { + inherit config pkgs lib; + preferences = allPreferences; + } + ); + setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] ( + import ./set-search-engines.nix { + inherit config pkgs lib; + searchEngines = cfg.searchEngines; + } + ); + } + // optionalAttrs cfg.isDefaultBrowser { + setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + run ${pkgs.defaultbrowser}/bin/defaultbrowser browser + ''; + } + ); - xdg.mimeApps = lib.mkIf isLinux { + xdg.mimeApps = lib.mkIf (isLinux && cfg.isDefaultBrowser) { enable = true; defaultApplications = { "text/html" = [ "brave.desktop" ]; diff --git a/modules/home/brave/set-preferences.nix b/modules/home/brave/set-preferences.nix index 94eff6cf..ec8d86e6 100644 --- a/modules/home/brave/set-preferences.nix +++ b/modules/home/brave/set-preferences.nix @@ -2,64 +2,15 @@ config, pkgs, lib, + preferences, }: let - # None of these settings are documented anywhere AFAIK. They were found by - # copying the current Preferences file, manually changing settings in Brave - # via the UI, and then diffing the copied file against the latest version. - preferences = { - brave = { - # Hide the Brave search box in the new tab page. - brave_search.show-ntp-search = false; - new_tab_page = { - background = { - random = false; - selected_value = config.modules.colorschemes.palette.primary.background; - show_background_image = true; - type = "color"; - }; - show_stats = false; - }; - show_bookmarks_button = false; - show_side_panel_button = false; - }; - # Pin the Proton Pass extension to the toolbar. - extensions.pinned_extensions = [ - "ghmbeldphafepmbegfdlkpapadhbakde" - ]; - # Hide the top sites in the new tab page (yes, there's a typo in the key). - ntp.shortcust_visible = false; - # Remove all buttons from the toolbar. - toolbar.pinned_actions = [ ]; - }; - profile = "Default"; preferencesPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Preferences"; - # Flatten nested attrset into list of [{path, value}]. - flattenPrefs = - prefix: attrs: - lib.concatLists ( - lib.mapAttrsToList ( - k: v: - let - newPrefix = prefix ++ [ k ]; - in - if lib.isAttrs v then - flattenPrefs newPrefix v - else - [ - { - path = newPrefix; - value = v; - } - ] - ) attrs - ); - - prefUpdates = builtins.toJSON (flattenPrefs [ ] preferences); + prefUpdates = builtins.toJSON preferences; in '' is_brave_running() { diff --git a/modules/home/brave/set-search-engines.nix b/modules/home/brave/set-search-engines.nix index fd818a29..6b4f98c7 100644 --- a/modules/home/brave/set-search-engines.nix +++ b/modules/home/brave/set-search-engines.nix @@ -2,51 +2,23 @@ config, pkgs, lib, + searchEngines, }: let - searchEngines = { - hm = { - short_name = "Home Manager Options"; - url = "https://home-manager-options.extranix.com/?query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; - }; - nixo = { - short_name = "NixOS options"; - url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; - }; - nixp = { - short_name = "Nix packages"; - url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; - }; - std = { - short_name = "std's docs"; - url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; - favicon_url = "https://rust-lang.org/logos/rust-logo-blk.svg"; - }; - }; - - searchEnginesList = lib.mapAttrsToList ( - keyword: attrs: - attrs - // { - inherit keyword; - safe_for_autoreplace = 0; - created_by_policy = 1; - input_encodings = "UTF-8"; - } - ) searchEngines; + searchEnginesList = lib.mapAttrsToList (keyword: attrs: { + inherit keyword; + short_name = attrs.name; + inherit (attrs) url favicon_url; + safe_for_autoreplace = 0; + created_by_policy = 1; + input_encodings = "UTF-8"; + }) searchEngines; sqlScript = let nix2Sql = - v: - if builtins.isString v then - "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" - else - toString v; + v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; in '' -- Remove all policy-managed search engines. diff --git a/modules/home/brave/settings.nix b/modules/home/brave/settings.nix new file mode 100644 index 00000000..036fdffa --- /dev/null +++ b/modules/home/brave/settings.nix @@ -0,0 +1,326 @@ +# Each setting is defined as a typed option together with metadata describing +# where the value should be applied: +# +# - "policy" → macOS managed preference (com.brave.Browser forced policy) +# - "pref" → JSON preference file (~/.../Brave-Browser/Default/Preferences) +# +# Some policy keys use inverted logic (e.g. BraveNewsDisabled = true means +# news is *off*), so we track that with `inverted = true`. + +{ lib }: + +with lib; +let + # Helper: define a setting that maps to a macOS enterprise policy. + mkPolicySetting = + { + description, + type ? types.bool, + default, + policyKey, + inverted ? false, + }: + { + option = mkOption { inherit description type default; }; + dest = { + kind = "policy"; + key = policyKey; + inherit inverted; + }; + }; + + # Helper: define a setting that maps to a Brave JSON preference. + mkPrefSetting = + { + description, + type ? types.bool, + default, + prefPath, + }: + { + option = mkOption { inherit description type default; }; + dest = { + kind = "pref"; + path = prefPath; + }; + }; + + # ── Setting definitions ────────────────────────────────────────────── + + settingsDefs = { + autofill.address = mkPolicySetting { + description = "Whether to enable autofill for addresses."; + default = false; + policyKey = "AutofillAddressEnabled"; + }; + + autofill.creditCard = mkPolicySetting { + description = "Whether to enable autofill for credit cards."; + default = false; + policyKey = "AutofillCreditCardEnabled"; + }; + + bookmarkBar = mkPolicySetting { + description = "Whether to show the bookmark bar."; + default = false; + policyKey = "BookmarkBarEnabled"; + }; + + braveAIChat = mkPolicySetting { + description = "Whether to enable Brave's AI chat (Leo)."; + default = false; + policyKey = "BraveAIChatEnabled"; + }; + + braveNews = mkPolicySetting { + description = "Whether to enable Brave News."; + default = false; + policyKey = "BraveNewsDisabled"; + inverted = true; + }; + + braveRewards = mkPolicySetting { + description = "Whether to enable Brave Rewards."; + default = false; + policyKey = "BraveRewardsDisabled"; + inverted = true; + }; + + braveStatsPing = mkPolicySetting { + description = "Whether to enable Brave stats ping."; + default = false; + policyKey = "BraveStatsPingEnabled"; + }; + + braveTalk = mkPolicySetting { + description = "Whether to enable Brave Talk."; + default = false; + policyKey = "BraveTalkDisabled"; + inverted = true; + }; + + braveVPN = mkPolicySetting { + description = "Whether to enable Brave VPN."; + default = false; + policyKey = "BraveVPNDisabled"; + inverted = true; + }; + + braveWallet = mkPolicySetting { + description = "Whether to enable Brave Wallet."; + default = false; + policyKey = "BraveWalletDisabled"; + inverted = true; + }; + + browserSignin = mkPolicySetting { + description = "Whether to allow browser sign-in."; + default = false; + policyKey = "BrowserSignin"; + }; + + homepageIsNewTabPage = mkPolicySetting { + description = "Whether the homepage is the new tab page."; + default = true; + policyKey = "HomepageIsNewTabPage"; + }; + + newTabPageLocation = mkPolicySetting { + description = "URL to load in the new tab page."; + type = types.str; + default = "about:blank"; + policyKey = "NewTabPageLocation"; + }; + + passwordManager = mkPolicySetting { + description = "Whether to enable the built-in password manager."; + default = false; + policyKey = "PasswordManagerEnabled"; + }; + + sync = mkPolicySetting { + description = "Whether to enable Brave Sync."; + default = false; + policyKey = "SyncDisabled"; + inverted = true; + }; + + # ── New tab page preferences ─────────────────────────────────────── + + ntp.showSearchBox = mkPrefSetting { + description = "Whether to show the search box in the new tab page."; + default = false; + prefPath = [ + "brave" + "brave_search" + "show-ntp-search" + ]; + }; + + ntp.background.random = mkPrefSetting { + description = "Whether to use a random background in the new tab page."; + default = false; + prefPath = [ + "brave" + "new_tab_page" + "background" + "random" + ]; + }; + + ntp.background.color = mkPrefSetting { + description = "Background color for the new tab page (hex string)."; + type = types.str; + default = "#000000"; + prefPath = [ + "brave" + "new_tab_page" + "background" + "selected_value" + ]; + }; + + ntp.background.showImage = mkPrefSetting { + description = "Whether to show the background image in the new tab page."; + default = true; + prefPath = [ + "brave" + "new_tab_page" + "background" + "show_background_image" + ]; + }; + + ntp.background.type = mkPrefSetting { + description = ''The background type for the new tab page (e.g. "color").''; + type = types.str; + default = "color"; + prefPath = [ + "brave" + "new_tab_page" + "background" + "type" + ]; + }; + + ntp.showStats = mkPrefSetting { + description = "Whether to show stats in the new tab page."; + default = false; + prefPath = [ + "brave" + "new_tab_page" + "show_stats" + ]; + }; + + ntp.showTopSites = mkPrefSetting { + description = "Whether to show top sites in the new tab page."; + default = false; + # Yes, the typo in the key is from Brave itself. + prefPath = [ + "ntp" + "shortcust_visible" + ]; + }; + + # ── Toolbar preferences ──────────────────────────────────────────── + + showBookmarksButton = mkPrefSetting { + description = "Whether to show the bookmarks button in the toolbar."; + default = false; + prefPath = [ + "brave" + "show_bookmarks_button" + ]; + }; + + showSidePanelButton = mkPrefSetting { + description = "Whether to show the side panel button in the toolbar."; + default = false; + prefPath = [ + "brave" + "show_side_panel_button" + ]; + }; + + toolbar.pinnedActions = mkPrefSetting { + description = "List of actions pinned to the toolbar."; + type = types.listOf types.str; + default = [ ]; + prefPath = [ + "toolbar" + "pinned_actions" + ]; + }; + }; + + # ── Public API ───────────────────────────────────────────────────────── + + # Walk the (possibly nested) settingsDefs and extract only the `option` + # fields, preserving the nesting structure so it can be used directly in + # `options.modules.brave.settings = { ... }`. + extractOptions = defs: mapAttrs (_: v: if v ? option then v.option else extractOptions v) defs; + + # Collect all dest metadata paired with a thunk that reads the final + # value from `cfg.settings`. Returns a flat list of + # { dest = { kind, ... }; value = ; }. + collectEntries = + cfg: prefix: defs: + concatLists ( + mapAttrsToList ( + name: v: + let + attrPath = prefix ++ [ name ]; + value = getAttrFromPath attrPath cfg; + in + if v ? dest then + [ + { + inherit (v) dest; + inherit value; + } + ] + else + collectEntries cfg attrPath v + ) defs + ); +in +{ + inherit settingsDefs; + + # The options attrset to splice into the submodule. + options = extractOptions settingsDefs; + + # Given the final `cfg.settings` value, derive policies and preferences. + mkOutputs = + settingsCfg: + let + entries = collectEntries settingsCfg [ ] settingsDefs; + + policyEntries = filter (e: e.dest.kind == "policy") entries; + prefEntries = filter (e: e.dest.kind == "pref") entries; + in + { + policies = listToAttrs ( + map ( + e: + let + raw = e.value; + value = + if e.dest ? inverted && e.dest.inverted then + !raw + else if e.dest.key == "BrowserSignin" then + (if raw then 1 else 0) + else + raw; + in + nameValuePair e.dest.key value + ) policyEntries + ); + + preferences = map (e: { + path = e.dest.path; + value = e.value; + }) prefEntries; + }; +} From 63b7e9f83ae745bbaa4271af61683b2a61d81288 Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Fri, 20 Mar 2026 17:36:50 +0100 Subject: [PATCH 2/7] wip --- modules/home/brave/default.nix | 201 ++++++++++++++++------ modules/home/brave/search-engines.nix | 85 +++++++++ modules/home/brave/set-preferences.nix | 61 ------- modules/home/brave/set-search-engines.nix | 85 --------- modules/home/brave/settings.nix | 153 +++++++++------- 5 files changed, 322 insertions(+), 263 deletions(-) create mode 100644 modules/home/brave/search-engines.nix delete mode 100644 modules/home/brave/set-preferences.nix delete mode 100644 modules/home/brave/set-search-engines.nix diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index 5ec121df..1f322311 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -10,9 +10,6 @@ let cfg = config.modules.brave; inherit (pkgs.stdenv) isDarwin isLinux; - settingsMod = import ./settings.nix { inherit lib; }; - outputs = settingsMod.mkOutputs cfg.settings; - extensionType = types.submodule { options = { id = mkOption { @@ -27,39 +24,15 @@ let }; }; - searchEngineType = types.submodule { - options = { - name = mkOption { - type = types.str; - description = "Display name of the search engine."; - }; - url = mkOption { - type = types.str; - description = "Search URL template. Use {searchTerms} as placeholder."; - }; - favicon_url = mkOption { - type = types.str; - default = ""; - description = "URL to the search engine's favicon."; - }; - }; - }; - pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( filterAttrs (_: ext: ext.pinned) cfg.extensions ); - # Merge the extension-pinning preference into the preferences coming from - # the settings submodule. - allPreferences = - outputs.preferences - ++ optional (pinnedExtensionIds != [ ]) { - path = [ - "extensions" - "pinned_extensions" - ]; - value = pinnedExtensionIds; - }; + profile = "Default"; + + preferencesPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Preferences"; + + dbPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Web Data"; in { options.modules.brave = { @@ -78,15 +51,22 @@ in }; searchEngines = mkOption { - type = types.attrsOf searchEngineType; + type = types.submoduleWith { + modules = [ ./search-engines.nix ]; + specialArgs = { inherit pkgs; }; + }; default = { }; - description = '' - Custom search engines. The attribute name is used as the keyword - (shortcut) for the engine. - ''; + description = "Custom search engine configuration."; }; - settings = settingsMod.options; + settings = mkOption { + type = types.submoduleWith { + modules = [ ./settings.nix ]; + specialArgs = { inherit pkgs; }; + }; + default = { }; + description = "Brave browser settings."; + }; }; config = mkIf cfg.enable { @@ -101,7 +81,7 @@ in unhook.id = "khncfooichmfjbepaaaebmommgaepoid"; }; - searchEngines = { + searchEngines.engines = { hm = { name = "Home Manager Options"; url = "https://home-manager-options.extranix.com/?query={searchTerms}"; @@ -125,7 +105,38 @@ in }; settings = { + # Forward pinned extension IDs into the settings submodule so they + # end up in the JSON preference updates. + _pinnedExtensionIds = pinnedExtensionIds; + + # ── Policies ───────────────────────────────────────────────────── + autofill.address = false; + autofill.creditCard = false; + bookmarkBar = false; + braveAIChat = false; + braveNews = false; + braveRewards = false; + braveStatsPing = false; + braveTalk = false; + braveVPN = false; + braveWallet = false; + browserSignin = false; + homepageIsNewTabPage = true; + newTabPageLocation = "about:blank"; + passwordManager = false; + sync = false; + + # ── JSON preferences ───────────────────────────────────────────── + ntp.showSearchBox = false; + ntp.background.random = false; ntp.background.color = config.modules.colorschemes.palette.primary.background; + ntp.background.showImage = true; + ntp.background.type = "color"; + ntp.showStats = false; + ntp.showTopSites = false; + showBookmarksButton = false; + showSidePanelButton = false; + toolbar.pinnedActions = [ ]; }; }; @@ -151,26 +162,104 @@ in }; # See https://chromeenterprise.google/policies/ and - # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy for - # the available policies. - modules.macOSPreferences.apps."com.brave.Browser" = { - forced = outputs.policies; - }; + # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy + # for the available policies. + modules.macOSPreferences.apps."com.brave.Browser".forced = cfg.settings._policies; - home.activation = lib.mkIf isDarwin ( + home.activation = mkIf isDarwin ( { - setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] ( - import ./set-preferences.nix { - inherit config pkgs lib; - preferences = allPreferences; + setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + is_brave_running() { + /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 } - ); - setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] ( - import ./set-search-engines.nix { - inherit config pkgs lib; - searchEngines = cfg.searchEngines; + + apply_brave_preferences() { + # Exit early if Preferences doesn't exist yet. + [[ -f "${preferencesPath}" ]] || return 0 + + # Generate the checksum of the preference updates. + pref_hash=$(echo -n '${cfg.settings._prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) + hash_file="${config.xdg.cacheHome}/home-manager/brave-preferences.hash" + + # Exit early if the preferences haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then + echo "Brave preferences already up to date" + return 0 + fi + + # Brave writes the Preferences file on exit, so we need to quit + # it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + run /usr/bin/osascript -e 'quit app "Brave Browser"' + while is_brave_running; do /bin/sleep 0.5; done + fi + + # Apply each preference update. + run ${pkgs.jq}/bin/jq \ + --argjson updates '${cfg.settings._prefUpdates}' \ + 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ + "${preferencesPath}" > "${preferencesPath}.tmp" + + run mv "${preferencesPath}.tmp" "${preferencesPath}" + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + run /usr/bin/open -a "Brave Browser" + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$pref_hash" > "$hash_file" + } + + apply_brave_preferences + ''; + + setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + is_brave_running() { + /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 } - ); + + apply_brave_search_engines() { + # Exit early if Brave hasn't yet created the DB. + [[ -f "${dbPath}" ]] || return 0 + + # Generate the checksum of the SQL script. + script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${cfg.searchEngines._sqlScript} | cut -d' ' -f2) + hash_file="${config.xdg.cacheHome}/home-manager/brave-search-engines.hash" + + # Exit early if the search engines haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then + echo "Brave search engines already up to date" + return 0 + fi + + # The database is locked while Brave is running, so we need to + # quit it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + run /usr/bin/osascript -e 'quit app "Brave Browser"' + while is_brave_running; do /bin/sleep 0.5; done + fi + + # Apply SQL changes. + run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${cfg.searchEngines._sqlScript} + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + run /usr/bin/open -a "Brave Browser" + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$script_hash" > "$hash_file" + } + + apply_brave_search_engines + ''; } // optionalAttrs cfg.isDefaultBrowser { setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' @@ -179,7 +268,7 @@ in } ); - xdg.mimeApps = lib.mkIf (isLinux && cfg.isDefaultBrowser) { + xdg.mimeApps = mkIf (isLinux && cfg.isDefaultBrowser) { enable = true; defaultApplications = { "text/html" = [ "brave.desktop" ]; diff --git a/modules/home/brave/search-engines.nix b/modules/home/brave/search-engines.nix new file mode 100644 index 00000000..9346657f --- /dev/null +++ b/modules/home/brave/search-engines.nix @@ -0,0 +1,85 @@ +# Brave custom search engines submodule. +# +# Manages keyword-triggered search engines by writing directly to Brave's +# "Web Data" SQLite database via a home.activation script. +# +# Exposes a read-only `_sqlScript` derivation that the parent module wires +# into the activation script. + +{ + config, + lib, + pkgs, + ... +}: + +with lib; +let + engineType = types.submodule { + options = { + name = mkOption { + type = types.str; + description = "Display name of the search engine."; + }; + url = mkOption { + type = types.str; + description = "Search URL template. Use {searchTerms} as placeholder."; + }; + favicon_url = mkOption { + type = types.str; + default = ""; + description = "URL to the search engine's favicon."; + }; + }; + }; + + enginesList = mapAttrsToList (keyword: engine: { + inherit keyword; + short_name = engine.name; + inherit (engine) url favicon_url; + safe_for_autoreplace = 0; + created_by_policy = 1; + input_encodings = "UTF-8"; + }) config.engines; + + nix2Sql = + v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; + + sqlScript = '' + -- Remove all policy-managed search engines. + DELETE FROM keywords WHERE created_by_policy = 1; + + -- Insert the configured search engines. + ${concatMapStringsSep "\n" ( + engine: + let + columns = builtins.attrNames engine; + values = map (col: nix2Sql engine.${col}) columns; + in + "INSERT INTO keywords (${concatStringsSep ", " columns}) VALUES (${concatStringsSep ", " values});" + ) enginesList} + ''; +in +{ + options = { + engines = mkOption { + type = types.attrsOf engineType; + default = { }; + description = '' + Custom search engines. The attribute name is used as the keyword + (shortcut) for the engine. + ''; + }; + + _sqlScript = mkOption { + type = types.package; + readOnly = true; + internal = true; + description = "Derivation containing the SQL script to apply."; + }; + }; + + config = { + _sqlScript = pkgs.writeText "brave-search-engines.sql" sqlScript; + }; +} diff --git a/modules/home/brave/set-preferences.nix b/modules/home/brave/set-preferences.nix deleted file mode 100644 index ec8d86e6..00000000 --- a/modules/home/brave/set-preferences.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ - config, - pkgs, - lib, - preferences, -}: - -let - profile = "Default"; - - preferencesPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Preferences"; - - prefUpdates = builtins.toJSON preferences; -in -'' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_preferences() { - # Exit early if Preferences doesn't exist yet. - [[ -f "${preferencesPath}" ]] || return 0 - - # Generate the checksum of the preference updates. - pref_hash=$(echo -n '${prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/brave-preferences.hash" - - # Exit early if the preferences haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then - echo "Brave preferences already up to date" - return 0 - fi - - # Brave writes the Preferences file on exit, so we need to quit it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply each preference update. - run ${pkgs.jq}/bin/jq \ - --argjson updates '${prefUpdates}' \ - 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ - "${preferencesPath}" > "${preferencesPath}.tmp" - - run mv "${preferencesPath}.tmp" "${preferencesPath}" - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$pref_hash" > "$hash_file" - } - - apply_brave_preferences -'' diff --git a/modules/home/brave/set-search-engines.nix b/modules/home/brave/set-search-engines.nix deleted file mode 100644 index 6b4f98c7..00000000 --- a/modules/home/brave/set-search-engines.nix +++ /dev/null @@ -1,85 +0,0 @@ -{ - config, - pkgs, - lib, - searchEngines, -}: - -let - searchEnginesList = lib.mapAttrsToList (keyword: attrs: { - inherit keyword; - short_name = attrs.name; - inherit (attrs) url favicon_url; - safe_for_autoreplace = 0; - created_by_policy = 1; - input_encodings = "UTF-8"; - }) searchEngines; - - sqlScript = - let - nix2Sql = - v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; - in - '' - -- Remove all policy-managed search engines. - DELETE FROM keywords WHERE created_by_policy = 1; - - -- Insert the configured search engines. - ${lib.concatMapStringsSep "\n" ( - engine: - let - columns = builtins.attrNames engine; - values = map (column: nix2Sql engine.${column}) columns; - columnsStr = lib.concatStringsSep ", " columns; - valuesStr = lib.concatStringsSep ", " values; - in - "INSERT INTO keywords (${columnsStr}) VALUES (${valuesStr});" - ) searchEnginesList} - ''; - - sqlScriptFile = pkgs.writeText "brave-search-engines.sql" sqlScript; - - dbPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Data"; -in -'' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_search_engines() { - # Exit early if Brave hasn't yet created the DB. - [[ -f "${dbPath}" ]] || return 0 - - # Generate the checksum of the SQL script. - script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${sqlScriptFile} | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/brave-search-engines.hash" - - # Exit early if the search engines haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then - echo "Brave search engines already up to date" - return 0 - fi - - # The database is locked while Brave is running, so we need to quit it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply SQL changes. - run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${sqlScriptFile} - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$script_hash" > "$hash_file" - } - - apply_brave_search_engines -'' diff --git a/modules/home/brave/settings.nix b/modules/home/brave/settings.nix index 036fdffa..3f970be1 100644 --- a/modules/home/brave/settings.nix +++ b/modules/home/brave/settings.nix @@ -1,17 +1,27 @@ -# Each setting is defined as a typed option together with metadata describing -# where the value should be applied: +# Brave settings submodule. # -# - "policy" → macOS managed preference (com.brave.Browser forced policy) -# - "pref" → JSON preference file (~/.../Brave-Browser/Default/Preferences) +# Each setting is a typed option that knows where it needs to be applied: # -# Some policy keys use inverted logic (e.g. BraveNewsDisabled = true means -# news is *off*), so we track that with `inverted = true`. +# - "policy" → macOS managed preference (com.brave.Browser forced policy) +# - "pref" → Brave's JSON Preferences file (~/.../Default/Preferences) +# +# The submodule exposes two read-only outputs that the parent module uses +# to wire into the right destinations: +# +# - _policies → fed into modules.macOSPreferences +# - _prefUpdates → fed into a home.activation script -{ lib }: +{ + config, + lib, + pkgs, + ... +}: with lib; let - # Helper: define a setting that maps to a macOS enterprise policy. + # ── Helpers ──────────────────────────────────────────────────────────── + mkPolicySetting = { description, @@ -19,17 +29,17 @@ let default, policyKey, inverted ? false, + transform ? null, }: { option = mkOption { inherit description type default; }; dest = { kind = "policy"; key = policyKey; - inherit inverted; + inherit inverted transform; }; }; - # Helper: define a setting that maps to a Brave JSON preference. mkPrefSetting = { description, @@ -45,9 +55,12 @@ let }; }; - # ── Setting definitions ────────────────────────────────────────────── + # ── Setting definitions ──────────────────────────────────────────────── settingsDefs = { + + # ── Policies ───────────────────────────────────────────────────────── + autofill.address = mkPolicySetting { description = "Whether to enable autofill for addresses."; default = false; @@ -117,6 +130,7 @@ let description = "Whether to allow browser sign-in."; default = false; policyKey = "BrowserSignin"; + transform = v: if v then 1 else 0; }; homepageIsNewTabPage = mkPolicySetting { @@ -145,7 +159,7 @@ let inverted = true; }; - # ── New tab page preferences ─────────────────────────────────────── + # ── JSON preferences ───────────────────────────────────────────────── ntp.showSearchBox = mkPrefSetting { description = "Whether to show the search box in the new tab page."; @@ -158,7 +172,7 @@ let }; ntp.background.random = mkPrefSetting { - description = "Whether to use a random background in the new tab page."; + description = "Whether to use a random NTP background."; default = false; prefPath = [ "brave" @@ -181,7 +195,7 @@ let }; ntp.background.showImage = mkPrefSetting { - description = "Whether to show the background image in the new tab page."; + description = "Whether to show a background image in the new tab page."; default = true; prefPath = [ "brave" @@ -192,7 +206,7 @@ let }; ntp.background.type = mkPrefSetting { - description = ''The background type for the new tab page (e.g. "color").''; + description = ''The NTP background type (e.g. "color").''; type = types.str; default = "color"; prefPath = [ @@ -223,8 +237,6 @@ let ]; }; - # ── Toolbar preferences ──────────────────────────────────────────── - showBookmarksButton = mkPrefSetting { description = "Whether to show the bookmarks button in the toolbar."; default = false; @@ -254,24 +266,18 @@ let }; }; - # ── Public API ───────────────────────────────────────────────────────── + # ── Extract / collect ────────────────────────────────────────────────── - # Walk the (possibly nested) settingsDefs and extract only the `option` - # fields, preserving the nesting structure so it can be used directly in - # `options.modules.brave.settings = { ... }`. extractOptions = defs: mapAttrs (_: v: if v ? option then v.option else extractOptions v) defs; - # Collect all dest metadata paired with a thunk that reads the final - # value from `cfg.settings`. Returns a flat list of - # { dest = { kind, ... }; value = ; }. collectEntries = cfg: prefix: defs: concatLists ( mapAttrsToList ( name: v: let - attrPath = prefix ++ [ name ]; - value = getAttrFromPath attrPath cfg; + path = prefix ++ [ name ]; + value = getAttrFromPath path cfg; in if v ? dest then [ @@ -281,46 +287,71 @@ let } ] else - collectEntries cfg attrPath v + collectEntries cfg path v ) defs ); + + entries = collectEntries config [ ] settingsDefs; + + policyEntries = filter (e: e.dest.kind == "policy") entries; + prefEntries = filter (e: e.dest.kind == "pref") entries; in { - inherit settingsDefs; - - # The options attrset to splice into the submodule. - options = extractOptions settingsDefs; + options = (extractOptions settingsDefs) // { + # Private option set by the parent to forward pinned extension IDs + # into the preference updates. + _pinnedExtensionIds = mkOption { + type = types.listOf types.str; + default = [ ]; + internal = true; + }; - # Given the final `cfg.settings` value, derive policies and preferences. - mkOutputs = - settingsCfg: - let - entries = collectEntries settingsCfg [ ] settingsDefs; + # Read-only outputs consumed by the parent module. + _policies = mkOption { + type = types.attrs; + readOnly = true; + internal = true; + description = "Computed macOS enterprise policies."; + }; - policyEntries = filter (e: e.dest.kind == "policy") entries; - prefEntries = filter (e: e.dest.kind == "pref") entries; - in - { - policies = listToAttrs ( - map ( - e: - let - raw = e.value; - value = - if e.dest ? inverted && e.dest.inverted then - !raw - else if e.dest.key == "BrowserSignin" then - (if raw then 1 else 0) - else - raw; - in - nameValuePair e.dest.key value - ) policyEntries - ); - - preferences = map (e: { - path = e.dest.path; - value = e.value; - }) prefEntries; + _prefUpdates = mkOption { + type = types.str; + readOnly = true; + internal = true; + description = "JSON-encoded list of {path, value} preference patches."; }; + }; + + config = { + _policies = listToAttrs ( + map ( + e: + let + raw = e.value; + value = + if e.dest.transform != null then + e.dest.transform raw + else if e.dest.inverted then + !raw + else + raw; + in + nameValuePair e.dest.key value + ) policyEntries + ); + + _prefUpdates = builtins.toJSON ( + (map (e: { + inherit (e.dest) path; + inherit (e) value; + }) prefEntries) + ++ optional (config._pinnedExtensionIds != [ ]) { + path = [ + "extensions" + "pinned_extensions" + ]; + value = config._pinnedExtensionIds; + } + ); + }; } From 22349b07096034e4e538e82b180e292760ab3d4a Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Fri, 20 Mar 2026 18:07:38 +0100 Subject: [PATCH 3/7] wip --- modules/home/brave/default.nix | 140 ++------------------- modules/home/brave/search-engines.nix | 87 +++++++++---- modules/home/brave/settings.nix | 173 ++++++++++++++++---------- 3 files changed, 181 insertions(+), 219 deletions(-) diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index 1f322311..e6b232eb 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -23,18 +23,13 @@ let }; }; }; - - pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( - filterAttrs (_: ext: ext.pinned) cfg.extensions - ); - - profile = "Default"; - - preferencesPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Preferences"; - - dbPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Web Data"; in { + imports = [ + ./settings.nix + ./search-engines.nix + ]; + options.modules.brave = { enable = mkEnableOption "Brave"; @@ -49,24 +44,6 @@ in default = { }; description = "Extensions to install, keyed by a human-readable name."; }; - - searchEngines = mkOption { - type = types.submoduleWith { - modules = [ ./search-engines.nix ]; - specialArgs = { inherit pkgs; }; - }; - default = { }; - description = "Custom search engine configuration."; - }; - - settings = mkOption { - type = types.submoduleWith { - modules = [ ./settings.nix ]; - specialArgs = { inherit pkgs; }; - }; - default = { }; - description = "Brave browser settings."; - }; }; config = mkIf cfg.enable { @@ -81,7 +58,7 @@ in unhook.id = "khncfooichmfjbepaaaebmommgaepoid"; }; - searchEngines.engines = { + searchEngines = { hm = { name = "Home Manager Options"; url = "https://home-manager-options.extranix.com/?query={searchTerms}"; @@ -105,10 +82,6 @@ in }; settings = { - # Forward pinned extension IDs into the settings submodule so they - # end up in the JSON preference updates. - _pinnedExtensionIds = pinnedExtensionIds; - # ── Policies ───────────────────────────────────────────────────── autofill.address = false; autofill.creditCard = false; @@ -161,107 +134,8 @@ in extensions = mapAttrsToList (_: ext: { inherit (ext) id; }) cfg.extensions; }; - # See https://chromeenterprise.google/policies/ and - # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy - # for the available policies. - modules.macOSPreferences.apps."com.brave.Browser".forced = cfg.settings._policies; - home.activation = mkIf isDarwin ( - { - setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_preferences() { - # Exit early if Preferences doesn't exist yet. - [[ -f "${preferencesPath}" ]] || return 0 - - # Generate the checksum of the preference updates. - pref_hash=$(echo -n '${cfg.settings._prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/brave-preferences.hash" - - # Exit early if the preferences haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then - echo "Brave preferences already up to date" - return 0 - fi - - # Brave writes the Preferences file on exit, so we need to quit - # it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply each preference update. - run ${pkgs.jq}/bin/jq \ - --argjson updates '${cfg.settings._prefUpdates}' \ - 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ - "${preferencesPath}" > "${preferencesPath}.tmp" - - run mv "${preferencesPath}.tmp" "${preferencesPath}" - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$pref_hash" > "$hash_file" - } - - apply_brave_preferences - ''; - - setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_search_engines() { - # Exit early if Brave hasn't yet created the DB. - [[ -f "${dbPath}" ]] || return 0 - - # Generate the checksum of the SQL script. - script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${cfg.searchEngines._sqlScript} | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/brave-search-engines.hash" - - # Exit early if the search engines haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then - echo "Brave search engines already up to date" - return 0 - fi - - # The database is locked while Brave is running, so we need to - # quit it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply SQL changes. - run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${cfg.searchEngines._sqlScript} - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$script_hash" > "$hash_file" - } - - apply_brave_search_engines - ''; - } - // optionalAttrs cfg.isDefaultBrowser { + optionalAttrs cfg.isDefaultBrowser { setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' run ${pkgs.defaultbrowser}/bin/defaultbrowser browser ''; diff --git a/modules/home/brave/search-engines.nix b/modules/home/brave/search-engines.nix index 9346657f..9cb1d32f 100644 --- a/modules/home/brave/search-engines.nix +++ b/modules/home/brave/search-engines.nix @@ -1,10 +1,7 @@ -# Brave custom search engines submodule. +# Brave custom search engines module. # # Manages keyword-triggered search engines by writing directly to Brave's # "Web Data" SQLite database via a home.activation script. -# -# Exposes a read-only `_sqlScript` derivation that the parent module wires -# into the activation script. { config, @@ -15,6 +12,9 @@ with lib; let + cfg = config.modules.brave; + inherit (pkgs.stdenv) isDarwin; + engineType = types.submodule { options = { name = mkOption { @@ -40,7 +40,7 @@ let safe_for_autoreplace = 0; created_by_policy = 1; input_encodings = "UTF-8"; - }) config.engines; + }) cfg.searchEngines; nix2Sql = v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; @@ -59,27 +59,68 @@ let "INSERT INTO keywords (${concatStringsSep ", " columns}) VALUES (${concatStringsSep ", " values});" ) enginesList} ''; + + sqlScriptFile = pkgs.writeText "brave-search-engines.sql" sqlScript; + + profile = "Default"; + + dbPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Web Data"; in { - options = { - engines = mkOption { - type = types.attrsOf engineType; - default = { }; - description = '' - Custom search engines. The attribute name is used as the keyword - (shortcut) for the engine. - ''; - }; - - _sqlScript = mkOption { - type = types.package; - readOnly = true; - internal = true; - description = "Derivation containing the SQL script to apply."; - }; + options.modules.brave.searchEngines = mkOption { + type = types.attrsOf engineType; + default = { }; + description = '' + Custom search engines. The attribute name is used as the keyword + (shortcut) for the engine. + ''; }; - config = { - _sqlScript = pkgs.writeText "brave-search-engines.sql" sqlScript; + config = mkIf cfg.enable { + home.activation = mkIf isDarwin { + setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + is_brave_running() { + /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 + } + + apply_brave_search_engines() { + # Exit early if Brave hasn't yet created the DB. + [[ -f "${dbPath}" ]] || return 0 + + # Generate the checksum of the SQL script. + script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${sqlScriptFile} | cut -d' ' -f2) + hash_file="${config.xdg.cacheHome}/home-manager/brave-search-engines.hash" + + # Exit early if the search engines haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then + echo "Brave search engines already up to date" + return 0 + fi + + # The database is locked while Brave is running, so we need to + # quit it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + run /usr/bin/osascript -e 'quit app "Brave Browser"' + while is_brave_running; do /bin/sleep 0.5; done + fi + + # Apply SQL changes. + run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${sqlScriptFile} + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + run /usr/bin/open -a "Brave Browser" + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$script_hash" > "$hash_file" + } + + apply_brave_search_engines + ''; + }; }; } diff --git a/modules/home/brave/settings.nix b/modules/home/brave/settings.nix index 3f970be1..61a6d6b3 100644 --- a/modules/home/brave/settings.nix +++ b/modules/home/brave/settings.nix @@ -1,15 +1,13 @@ -# Brave settings submodule. +# Brave settings module. # # Each setting is a typed option that knows where it needs to be applied: # # - "policy" → macOS managed preference (com.brave.Browser forced policy) # - "pref" → Brave's JSON Preferences file (~/.../Default/Preferences) # -# The submodule exposes two read-only outputs that the parent module uses -# to wire into the right destinations: -# -# - _policies → fed into modules.macOSPreferences -# - _prefUpdates → fed into a home.activation script +# The config section wires everything to the right destinations: policies +# go to modules.macOSPreferences, preferences go to a home.activation +# script that patches the JSON file with jq. { config, @@ -20,6 +18,9 @@ with lib; let + cfg = config.modules.brave; + inherit (pkgs.stdenv) isDarwin; + # ── Helpers ──────────────────────────────────────────────────────────── mkPolicySetting = @@ -271,13 +272,13 @@ let extractOptions = defs: mapAttrs (_: v: if v ? option then v.option else extractOptions v) defs; collectEntries = - cfg: prefix: defs: + cfgValues: prefix: defs: concatLists ( mapAttrsToList ( name: v: let path = prefix ++ [ name ]; - value = getAttrFromPath path cfg; + value = getAttrFromPath path cfgValues; in if v ? dest then [ @@ -287,71 +288,117 @@ let } ] else - collectEntries cfg path v + collectEntries cfgValues path v ) defs ); - entries = collectEntries config [ ] settingsDefs; + entries = collectEntries cfg.settings [ ] settingsDefs; policyEntries = filter (e: e.dest.kind == "policy") entries; prefEntries = filter (e: e.dest.kind == "pref") entries; -in -{ - options = (extractOptions settingsDefs) // { - # Private option set by the parent to forward pinned extension IDs - # into the preference updates. - _pinnedExtensionIds = mkOption { - type = types.listOf types.str; - default = [ ]; - internal = true; - }; - # Read-only outputs consumed by the parent module. - _policies = mkOption { - type = types.attrs; - readOnly = true; - internal = true; - description = "Computed macOS enterprise policies."; + # ── Policies ─────────────────────────────────────────────────────────── + + policies = listToAttrs ( + map ( + e: + let + raw = e.value; + value = + if e.dest.transform != null then + e.dest.transform raw + else if e.dest.inverted then + !raw + else + raw; + in + nameValuePair e.dest.key value + ) policyEntries + ); + + # ── JSON preferences ────────────────────────────────────────────────── + + pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( + filterAttrs (_: ext: ext.pinned) cfg.extensions + ); + + preferences = + (map (e: { + inherit (e.dest) path; + inherit (e) value; + }) prefEntries) + ++ optional (pinnedExtensionIds != [ ]) { + path = [ + "extensions" + "pinned_extensions" + ]; + value = pinnedExtensionIds; }; - _prefUpdates = mkOption { - type = types.str; - readOnly = true; - internal = true; - description = "JSON-encoded list of {path, value} preference patches."; - }; - }; + prefUpdates = builtins.toJSON preferences; - config = { - _policies = listToAttrs ( - map ( - e: - let - raw = e.value; - value = - if e.dest.transform != null then - e.dest.transform raw - else if e.dest.inverted then - !raw - else - raw; - in - nameValuePair e.dest.key value - ) policyEntries - ); + profile = "Default"; - _prefUpdates = builtins.toJSON ( - (map (e: { - inherit (e.dest) path; - inherit (e) value; - }) prefEntries) - ++ optional (config._pinnedExtensionIds != [ ]) { - path = [ - "extensions" - "pinned_extensions" - ]; - value = config._pinnedExtensionIds; - } - ); + preferencesPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Preferences"; +in +{ + options.modules.brave.settings = extractOptions settingsDefs; + + config = mkIf cfg.enable { + # See https://chromeenterprise.google/policies/ and + # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy + # for the available policies. + modules.macOSPreferences.apps."com.brave.Browser".forced = policies; + + home.activation = mkIf isDarwin { + setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + is_brave_running() { + /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 + } + + apply_brave_preferences() { + # Exit early if Preferences doesn't exist yet. + [[ -f "${preferencesPath}" ]] || return 0 + + # Generate the checksum of the preference updates. + pref_hash=$(echo -n '${prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) + hash_file="${config.xdg.cacheHome}/home-manager/brave-preferences.hash" + + # Exit early if the preferences haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then + echo "Brave preferences already up to date" + return 0 + fi + + # Brave writes the Preferences file on exit, so we need to quit + # it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + run /usr/bin/osascript -e 'quit app "Brave Browser"' + while is_brave_running; do /bin/sleep 0.5; done + fi + + # Apply each preference update. + run ${pkgs.jq}/bin/jq \ + --argjson updates '${prefUpdates}' \ + 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ + "${preferencesPath}" > "${preferencesPath}.tmp" + + run mv "${preferencesPath}.tmp" "${preferencesPath}" + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + run /usr/bin/open -a "Brave Browser" + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$pref_hash" > "$hash_file" + } + + apply_brave_preferences + ''; + }; }; } From 142159534c947a19c9f2d8b667ab70f0795eb56d Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Fri, 20 Mar 2026 22:06:17 +0100 Subject: [PATCH 4/7] wip --- modules/home/brave/default.nix | 126 ++++---- modules/home/brave/profiles.nix | 276 ++++++++++++++++++ modules/home/brave/search-engines.nix | 126 -------- modules/home/brave/settings.nix | 404 -------------------------- 4 files changed, 349 insertions(+), 583 deletions(-) create mode 100644 modules/home/brave/profiles.nix delete mode 100644 modules/home/brave/search-engines.nix delete mode 100644 modules/home/brave/settings.nix diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index e6b232eb..583b3dde 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -25,10 +25,7 @@ let }; in { - imports = [ - ./settings.nix - ./search-engines.nix - ]; + imports = [ ./profiles.nix ]; options.modules.brave = { enable = mkEnableOption "Brave"; @@ -44,6 +41,18 @@ in default = { }; description = "Extensions to install, keyed by a human-readable name."; }; + + # See https://chromeenterprise.google/policies/ and + # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy + # for the available policies. + policies = mkOption { + type = types.attrs; + default = { }; + description = '' + Enterprise policies fed directly into + modules.macOSPreferences.apps."com.brave.Browser".forced. + ''; + }; }; config = mkIf cfg.enable { @@ -58,58 +67,67 @@ in unhook.id = "khncfooichmfjbepaaaebmommgaepoid"; }; - searchEngines = { - hm = { - name = "Home Manager Options"; - url = "https://home-manager-options.extranix.com/?query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; - }; - nixo = { - name = "NixOS options"; - url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; - }; - nixp = { - name = "Nix packages"; - url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; - }; - std = { - name = "std's docs"; - url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; - favicon_url = "https://rust-lang.org/logos/rust-logo-blk.svg"; - }; + policies = { + AutofillAddressEnabled = false; + AutofillCreditCardEnabled = false; + BookmarkBarEnabled = false; + BraveAIChatEnabled = false; + BraveNewsDisabled = true; + BraveRewardsDisabled = true; + BraveStatsPingEnabled = false; + BraveTalkDisabled = true; + BraveVPNDisabled = true; + BraveWalletDisabled = true; + BrowserSignin = 0; + HomepageIsNewTabPage = true; + NewTabPageLocation = "about:blank"; + PasswordManagerEnabled = false; + SyncDisabled = true; }; - settings = { - # ── Policies ───────────────────────────────────────────────────── - autofill.address = false; - autofill.creditCard = false; - bookmarkBar = false; - braveAIChat = false; - braveNews = false; - braveRewards = false; - braveStatsPing = false; - braveTalk = false; - braveVPN = false; - braveWallet = false; - browserSignin = false; - homepageIsNewTabPage = true; - newTabPageLocation = "about:blank"; - passwordManager = false; - sync = false; + profiles.Default = { + preferences = { + brave = { + brave_search."show-ntp-search" = false; + new_tab_page = { + background = { + random = false; + selected_value = config.modules.colorschemes.palette.primary.background; + show_background_image = true; + type = "color"; + }; + show_stats = false; + }; + show_bookmarks_button = false; + show_side_panel_button = false; + }; + # Yes, the typo in the key is from Brave itself. + ntp.shortcust_visible = false; + toolbar.pinned_actions = [ ]; + }; - # ── JSON preferences ───────────────────────────────────────────── - ntp.showSearchBox = false; - ntp.background.random = false; - ntp.background.color = config.modules.colorschemes.palette.primary.background; - ntp.background.showImage = true; - ntp.background.type = "color"; - ntp.showStats = false; - ntp.showTopSites = false; - showBookmarksButton = false; - showSidePanelButton = false; - toolbar.pinnedActions = [ ]; + searchEngines = { + hm = { + name = "Home Manager Options"; + url = "https://home-manager-options.extranix.com/?query={searchTerms}"; + favicon_url = "https://nixos.org/favicon.ico"; + }; + nixo = { + name = "NixOS options"; + url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; + favicon_url = "https://nixos.org/favicon.ico"; + }; + nixp = { + name = "Nix packages"; + url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; + favicon_url = "https://nixos.org/favicon.ico"; + }; + std = { + name = "std's docs"; + url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; + favicon_url = "https://rust-lang.org/logos/rust-logo-blk.svg"; + }; + }; }; }; @@ -134,6 +152,8 @@ in extensions = mapAttrsToList (_: ext: { inherit (ext) id; }) cfg.extensions; }; + modules.macOSPreferences.apps."com.brave.Browser".forced = cfg.policies; + home.activation = mkIf isDarwin ( optionalAttrs cfg.isDefaultBrowser { setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' diff --git a/modules/home/brave/profiles.nix b/modules/home/brave/profiles.nix new file mode 100644 index 00000000..0a20f72e --- /dev/null +++ b/modules/home/brave/profiles.nix @@ -0,0 +1,276 @@ +# Per-profile Brave configuration module. +# +# Each profile (e.g. "Default", "Profile 1") has: +# +# - preferences: a nested attrset that gets flattened into [{path, value}] +# pairs and applied to the profile's Preferences JSON file via jq. +# +# - searchEngines: keyword-triggered search engines written to the +# profile's Web Data SQLite database. +# +# Pinned extension IDs (from modules.brave.extensions) are automatically +# merged into each profile's preferences under extensions.pinned_extensions. + +{ + config, + lib, + pkgs, + ... +}: + +with lib; +let + cfg = config.modules.brave; + inherit (pkgs.stdenv) isDarwin; + + braveDataDir = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser"; + + # ── Extension pinning ────────────────────────────────────────────────── + + pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( + filterAttrs (_: ext: ext.pinned) cfg.extensions + ); + + # ── Preferences ──────────────────────────────────────────────────────── + + # Flatten a nested attrset into a list of [{path, value}] pairs suitable + # for jq's setpath(). + flattenPrefs = + prefix: attrs: + concatLists ( + mapAttrsToList ( + k: v: + let + path = prefix ++ [ k ]; + in + if isAttrs v && !(isList v) then + flattenPrefs path v + else + [ + { + inherit path; + value = v; + } + ] + ) attrs + ); + + mkPrefUpdates = + profileName: profileCfg: + let + merged = + profileCfg.preferences + // optionalAttrs (pinnedExtensionIds != [ ]) { + extensions.pinned_extensions = pinnedExtensionIds; + }; + in + builtins.toJSON (flattenPrefs [ ] merged); + + mkPreferencesActivation = + profileName: profileCfg: + let + prefUpdates = mkPrefUpdates profileName profileCfg; + preferencesPath = "${braveDataDir}/${profileName}/Preferences"; + hashName = "brave-preferences-${lib.strings.sanitizeDerivationName profileName}"; + in + nameValuePair "setBravePreferences-${profileName}" ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + is_brave_running() { + /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 + } + + apply_brave_preferences() { + # Exit early if Preferences doesn't exist yet. + [[ -f "${preferencesPath}" ]] || return 0 + + # Generate the checksum of the preference updates. + pref_hash=$(echo -n '${prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) + hash_file="${config.xdg.cacheHome}/home-manager/${hashName}.hash" + + # Exit early if the preferences haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then + echo "Brave preferences (${profileName}) already up to date" + return 0 + fi + + # Brave writes the Preferences file on exit, so we need to quit + # it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + run /usr/bin/osascript -e 'quit app "Brave Browser"' + while is_brave_running; do /bin/sleep 0.5; done + fi + + # Apply each preference update. + run ${pkgs.jq}/bin/jq \ + --argjson updates '${prefUpdates}' \ + 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ + "${preferencesPath}" > "${preferencesPath}.tmp" + + run mv "${preferencesPath}.tmp" "${preferencesPath}" + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + run /usr/bin/open -a "Brave Browser" + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$pref_hash" > "$hash_file" + } + + apply_brave_preferences + '' + ); + + # ── Search engines ───────────────────────────────────────────────────── + + searchEngineType = types.submodule { + options = { + name = mkOption { + type = types.str; + description = "Display name of the search engine."; + }; + url = mkOption { + type = types.str; + description = "Search URL template. Use {searchTerms} as placeholder."; + }; + favicon_url = mkOption { + type = types.str; + default = ""; + description = "URL to the search engine's favicon."; + }; + }; + }; + + nix2Sql = + v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; + + mkSqlScript = + profileCfg: + let + enginesList = mapAttrsToList (keyword: engine: { + inherit keyword; + short_name = engine.name; + inherit (engine) url favicon_url; + safe_for_autoreplace = 0; + created_by_policy = 1; + input_encodings = "UTF-8"; + }) profileCfg.searchEngines; + in + pkgs.writeText "brave-search-engines.sql" '' + -- Remove all policy-managed search engines. + DELETE FROM keywords WHERE created_by_policy = 1; + + -- Insert the configured search engines. + ${concatMapStringsSep "\n" ( + engine: + let + columns = builtins.attrNames engine; + values = map (col: nix2Sql engine.${col}) columns; + in + "INSERT INTO keywords (${concatStringsSep ", " columns}) VALUES (${concatStringsSep ", " values});" + ) enginesList} + ''; + + mkSearchEnginesActivation = + profileName: profileCfg: + let + sqlScriptFile = mkSqlScript profileCfg; + dbPath = "${braveDataDir}/${profileName}/Web Data"; + hashName = "brave-search-engines-${lib.strings.sanitizeDerivationName profileName}"; + in + nameValuePair "setBraveSearchEngines-${profileName}" ( + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + is_brave_running() { + /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 + } + + apply_brave_search_engines() { + # Exit early if Brave hasn't yet created the DB. + [[ -f "${dbPath}" ]] || return 0 + + # Generate the checksum of the SQL script. + script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${sqlScriptFile} | cut -d' ' -f2) + hash_file="${config.xdg.cacheHome}/home-manager/${hashName}.hash" + + # Exit early if the search engines haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then + echo "Brave search engines (${profileName}) already up to date" + return 0 + fi + + # The database is locked while Brave is running, so we need to + # quit it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + run /usr/bin/osascript -e 'quit app "Brave Browser"' + while is_brave_running; do /bin/sleep 0.5; done + fi + + # Apply SQL changes. + run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${sqlScriptFile} + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + run /usr/bin/open -a "Brave Browser" + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$script_hash" > "$hash_file" + } + + apply_brave_search_engines + '' + ); + + # ── Profile type ─────────────────────────────────────────────────────── + + profileType = types.submodule { + options = { + preferences = mkOption { + type = types.attrs; + default = { }; + description = '' + Nested attrset of Brave JSON preferences for this profile. + Keys mirror the structure of the Preferences JSON file (e.g. + `brave.new_tab_page.show_stats = false`). Pinned extension IDs + are merged automatically. + ''; + }; + searchEngines = mkOption { + type = types.attrsOf searchEngineType; + default = { }; + description = '' + Custom search engines for this profile. The attribute name is + used as the keyword (shortcut). + ''; + }; + }; + }; + + profilesWithPrefs = filterAttrs ( + _: p: p.preferences != { } || pinnedExtensionIds != [ ] + ) cfg.profiles; + profilesWithEngines = filterAttrs (_: p: p.searchEngines != { }) cfg.profiles; +in +{ + options.modules.brave.profiles = mkOption { + type = types.attrsOf profileType; + default = { }; + description = '' + Per-profile Brave configuration. Keys are profile directory names + (e.g. "Default", "Profile 1"). + ''; + }; + + config = mkIf cfg.enable { + home.activation = mkIf isDarwin ( + (mapAttrs' mkPreferencesActivation profilesWithPrefs) + // (mapAttrs' mkSearchEnginesActivation profilesWithEngines) + ); + }; +} diff --git a/modules/home/brave/search-engines.nix b/modules/home/brave/search-engines.nix deleted file mode 100644 index 9cb1d32f..00000000 --- a/modules/home/brave/search-engines.nix +++ /dev/null @@ -1,126 +0,0 @@ -# Brave custom search engines module. -# -# Manages keyword-triggered search engines by writing directly to Brave's -# "Web Data" SQLite database via a home.activation script. - -{ - config, - lib, - pkgs, - ... -}: - -with lib; -let - cfg = config.modules.brave; - inherit (pkgs.stdenv) isDarwin; - - engineType = types.submodule { - options = { - name = mkOption { - type = types.str; - description = "Display name of the search engine."; - }; - url = mkOption { - type = types.str; - description = "Search URL template. Use {searchTerms} as placeholder."; - }; - favicon_url = mkOption { - type = types.str; - default = ""; - description = "URL to the search engine's favicon."; - }; - }; - }; - - enginesList = mapAttrsToList (keyword: engine: { - inherit keyword; - short_name = engine.name; - inherit (engine) url favicon_url; - safe_for_autoreplace = 0; - created_by_policy = 1; - input_encodings = "UTF-8"; - }) cfg.searchEngines; - - nix2Sql = - v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; - - sqlScript = '' - -- Remove all policy-managed search engines. - DELETE FROM keywords WHERE created_by_policy = 1; - - -- Insert the configured search engines. - ${concatMapStringsSep "\n" ( - engine: - let - columns = builtins.attrNames engine; - values = map (col: nix2Sql engine.${col}) columns; - in - "INSERT INTO keywords (${concatStringsSep ", " columns}) VALUES (${concatStringsSep ", " values});" - ) enginesList} - ''; - - sqlScriptFile = pkgs.writeText "brave-search-engines.sql" sqlScript; - - profile = "Default"; - - dbPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Web Data"; -in -{ - options.modules.brave.searchEngines = mkOption { - type = types.attrsOf engineType; - default = { }; - description = '' - Custom search engines. The attribute name is used as the keyword - (shortcut) for the engine. - ''; - }; - - config = mkIf cfg.enable { - home.activation = mkIf isDarwin { - setBraveSearchEngines = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_search_engines() { - # Exit early if Brave hasn't yet created the DB. - [[ -f "${dbPath}" ]] || return 0 - - # Generate the checksum of the SQL script. - script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${sqlScriptFile} | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/brave-search-engines.hash" - - # Exit early if the search engines haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then - echo "Brave search engines already up to date" - return 0 - fi - - # The database is locked while Brave is running, so we need to - # quit it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply SQL changes. - run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${sqlScriptFile} - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$script_hash" > "$hash_file" - } - - apply_brave_search_engines - ''; - }; - }; -} diff --git a/modules/home/brave/settings.nix b/modules/home/brave/settings.nix deleted file mode 100644 index 61a6d6b3..00000000 --- a/modules/home/brave/settings.nix +++ /dev/null @@ -1,404 +0,0 @@ -# Brave settings module. -# -# Each setting is a typed option that knows where it needs to be applied: -# -# - "policy" → macOS managed preference (com.brave.Browser forced policy) -# - "pref" → Brave's JSON Preferences file (~/.../Default/Preferences) -# -# The config section wires everything to the right destinations: policies -# go to modules.macOSPreferences, preferences go to a home.activation -# script that patches the JSON file with jq. - -{ - config, - lib, - pkgs, - ... -}: - -with lib; -let - cfg = config.modules.brave; - inherit (pkgs.stdenv) isDarwin; - - # ── Helpers ──────────────────────────────────────────────────────────── - - mkPolicySetting = - { - description, - type ? types.bool, - default, - policyKey, - inverted ? false, - transform ? null, - }: - { - option = mkOption { inherit description type default; }; - dest = { - kind = "policy"; - key = policyKey; - inherit inverted transform; - }; - }; - - mkPrefSetting = - { - description, - type ? types.bool, - default, - prefPath, - }: - { - option = mkOption { inherit description type default; }; - dest = { - kind = "pref"; - path = prefPath; - }; - }; - - # ── Setting definitions ──────────────────────────────────────────────── - - settingsDefs = { - - # ── Policies ───────────────────────────────────────────────────────── - - autofill.address = mkPolicySetting { - description = "Whether to enable autofill for addresses."; - default = false; - policyKey = "AutofillAddressEnabled"; - }; - - autofill.creditCard = mkPolicySetting { - description = "Whether to enable autofill for credit cards."; - default = false; - policyKey = "AutofillCreditCardEnabled"; - }; - - bookmarkBar = mkPolicySetting { - description = "Whether to show the bookmark bar."; - default = false; - policyKey = "BookmarkBarEnabled"; - }; - - braveAIChat = mkPolicySetting { - description = "Whether to enable Brave's AI chat (Leo)."; - default = false; - policyKey = "BraveAIChatEnabled"; - }; - - braveNews = mkPolicySetting { - description = "Whether to enable Brave News."; - default = false; - policyKey = "BraveNewsDisabled"; - inverted = true; - }; - - braveRewards = mkPolicySetting { - description = "Whether to enable Brave Rewards."; - default = false; - policyKey = "BraveRewardsDisabled"; - inverted = true; - }; - - braveStatsPing = mkPolicySetting { - description = "Whether to enable Brave stats ping."; - default = false; - policyKey = "BraveStatsPingEnabled"; - }; - - braveTalk = mkPolicySetting { - description = "Whether to enable Brave Talk."; - default = false; - policyKey = "BraveTalkDisabled"; - inverted = true; - }; - - braveVPN = mkPolicySetting { - description = "Whether to enable Brave VPN."; - default = false; - policyKey = "BraveVPNDisabled"; - inverted = true; - }; - - braveWallet = mkPolicySetting { - description = "Whether to enable Brave Wallet."; - default = false; - policyKey = "BraveWalletDisabled"; - inverted = true; - }; - - browserSignin = mkPolicySetting { - description = "Whether to allow browser sign-in."; - default = false; - policyKey = "BrowserSignin"; - transform = v: if v then 1 else 0; - }; - - homepageIsNewTabPage = mkPolicySetting { - description = "Whether the homepage is the new tab page."; - default = true; - policyKey = "HomepageIsNewTabPage"; - }; - - newTabPageLocation = mkPolicySetting { - description = "URL to load in the new tab page."; - type = types.str; - default = "about:blank"; - policyKey = "NewTabPageLocation"; - }; - - passwordManager = mkPolicySetting { - description = "Whether to enable the built-in password manager."; - default = false; - policyKey = "PasswordManagerEnabled"; - }; - - sync = mkPolicySetting { - description = "Whether to enable Brave Sync."; - default = false; - policyKey = "SyncDisabled"; - inverted = true; - }; - - # ── JSON preferences ───────────────────────────────────────────────── - - ntp.showSearchBox = mkPrefSetting { - description = "Whether to show the search box in the new tab page."; - default = false; - prefPath = [ - "brave" - "brave_search" - "show-ntp-search" - ]; - }; - - ntp.background.random = mkPrefSetting { - description = "Whether to use a random NTP background."; - default = false; - prefPath = [ - "brave" - "new_tab_page" - "background" - "random" - ]; - }; - - ntp.background.color = mkPrefSetting { - description = "Background color for the new tab page (hex string)."; - type = types.str; - default = "#000000"; - prefPath = [ - "brave" - "new_tab_page" - "background" - "selected_value" - ]; - }; - - ntp.background.showImage = mkPrefSetting { - description = "Whether to show a background image in the new tab page."; - default = true; - prefPath = [ - "brave" - "new_tab_page" - "background" - "show_background_image" - ]; - }; - - ntp.background.type = mkPrefSetting { - description = ''The NTP background type (e.g. "color").''; - type = types.str; - default = "color"; - prefPath = [ - "brave" - "new_tab_page" - "background" - "type" - ]; - }; - - ntp.showStats = mkPrefSetting { - description = "Whether to show stats in the new tab page."; - default = false; - prefPath = [ - "brave" - "new_tab_page" - "show_stats" - ]; - }; - - ntp.showTopSites = mkPrefSetting { - description = "Whether to show top sites in the new tab page."; - default = false; - # Yes, the typo in the key is from Brave itself. - prefPath = [ - "ntp" - "shortcust_visible" - ]; - }; - - showBookmarksButton = mkPrefSetting { - description = "Whether to show the bookmarks button in the toolbar."; - default = false; - prefPath = [ - "brave" - "show_bookmarks_button" - ]; - }; - - showSidePanelButton = mkPrefSetting { - description = "Whether to show the side panel button in the toolbar."; - default = false; - prefPath = [ - "brave" - "show_side_panel_button" - ]; - }; - - toolbar.pinnedActions = mkPrefSetting { - description = "List of actions pinned to the toolbar."; - type = types.listOf types.str; - default = [ ]; - prefPath = [ - "toolbar" - "pinned_actions" - ]; - }; - }; - - # ── Extract / collect ────────────────────────────────────────────────── - - extractOptions = defs: mapAttrs (_: v: if v ? option then v.option else extractOptions v) defs; - - collectEntries = - cfgValues: prefix: defs: - concatLists ( - mapAttrsToList ( - name: v: - let - path = prefix ++ [ name ]; - value = getAttrFromPath path cfgValues; - in - if v ? dest then - [ - { - inherit (v) dest; - inherit value; - } - ] - else - collectEntries cfgValues path v - ) defs - ); - - entries = collectEntries cfg.settings [ ] settingsDefs; - - policyEntries = filter (e: e.dest.kind == "policy") entries; - prefEntries = filter (e: e.dest.kind == "pref") entries; - - # ── Policies ─────────────────────────────────────────────────────────── - - policies = listToAttrs ( - map ( - e: - let - raw = e.value; - value = - if e.dest.transform != null then - e.dest.transform raw - else if e.dest.inverted then - !raw - else - raw; - in - nameValuePair e.dest.key value - ) policyEntries - ); - - # ── JSON preferences ────────────────────────────────────────────────── - - pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( - filterAttrs (_: ext: ext.pinned) cfg.extensions - ); - - preferences = - (map (e: { - inherit (e.dest) path; - inherit (e) value; - }) prefEntries) - ++ optional (pinnedExtensionIds != [ ]) { - path = [ - "extensions" - "pinned_extensions" - ]; - value = pinnedExtensionIds; - }; - - prefUpdates = builtins.toJSON preferences; - - profile = "Default"; - - preferencesPath = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser/${profile}/Preferences"; -in -{ - options.modules.brave.settings = extractOptions settingsDefs; - - config = mkIf cfg.enable { - # See https://chromeenterprise.google/policies/ and - # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy - # for the available policies. - modules.macOSPreferences.apps."com.brave.Browser".forced = policies; - - home.activation = mkIf isDarwin { - setBravePreferences = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_preferences() { - # Exit early if Preferences doesn't exist yet. - [[ -f "${preferencesPath}" ]] || return 0 - - # Generate the checksum of the preference updates. - pref_hash=$(echo -n '${prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/brave-preferences.hash" - - # Exit early if the preferences haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then - echo "Brave preferences already up to date" - return 0 - fi - - # Brave writes the Preferences file on exit, so we need to quit - # it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply each preference update. - run ${pkgs.jq}/bin/jq \ - --argjson updates '${prefUpdates}' \ - 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ - "${preferencesPath}" > "${preferencesPath}.tmp" - - run mv "${preferencesPath}.tmp" "${preferencesPath}" - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$pref_hash" > "$hash_file" - } - - apply_brave_preferences - ''; - }; - }; -} From a2b6a992a9801a0fe1b96d9cb05f0f686d176ba3 Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Fri, 20 Mar 2026 22:33:11 +0100 Subject: [PATCH 5/7] wip --- modules/home/brave/default.nix | 227 ++++++++++++++++-- modules/home/brave/profiles.nix | 276 ---------------------- modules/home/brave/set-preferences.nix | 78 ++++++ modules/home/brave/set-search-engines.nix | 178 ++++++++++++++ 4 files changed, 457 insertions(+), 302 deletions(-) delete mode 100644 modules/home/brave/profiles.nix create mode 100644 modules/home/brave/set-preferences.nix create mode 100644 modules/home/brave/set-search-engines.nix diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index 583b3dde..fc80c846 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -10,6 +10,14 @@ let cfg = config.modules.brave; inherit (pkgs.stdenv) isDarwin isLinux; + braveDataDir = + if isDarwin then + "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser" + else + "${config.xdg.configHome}/BraveSoftware/Brave-Browser"; + + # ── Types ────────────────────────────────────────────────────────────── + extensionType = types.submodule { options = { id = mkOption { @@ -23,10 +31,152 @@ let }; }; }; + + searchEngineType = types.submodule { + options = { + name = mkOption { + type = types.str; + description = "Display name of the search engine."; + }; + url = mkOption { + type = types.str; + description = "Search URL template. Use {searchTerms} as placeholder."; + }; + favicon = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to a favicon image (any format ImageMagick can read). + The image is converted to 16x16 and 32x32 PNGs at activation + time and inserted into Brave's Favicons database. + ''; + }; + }; + }; + + profileType = types.submodule { + options = { + preferences = mkOption { + type = types.attrs; + default = { }; + description = '' + Nested attrset of Brave JSON preferences for this profile. + Keys mirror the structure of the Preferences JSON file (e.g. + `brave.new_tab_page.show_stats = false`). Pinned extension IDs + are merged automatically. + ''; + }; + searchEngines = mkOption { + type = types.attrsOf searchEngineType; + default = { }; + description = '' + Custom search engines for this profile. The attribute name is + used as the keyword (shortcut). + ''; + }; + }; + }; + + # ── Package wrapping ─────────────────────────────────────────────────── + + wrappedBrave = + let + disableFlags = concatStringsSep "," cfg.disabledFeatures; + in + if isDarwin then + pkgs.symlinkJoin { + name = "brave-wrapped"; + paths = [ pkgs.brave ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + rm "$out/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" + makeWrapper \ + "${pkgs.brave}/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \ + "$out/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \ + --add-flags "--disable-features=${disableFlags}" + ''; + } + else + pkgs.symlinkJoin { + name = "brave-wrapped"; + paths = [ pkgs.brave ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram "$out/bin/brave" \ + --add-flags "--disable-features=${disableFlags}" + ''; + }; + + # ── Extension pinning ────────────────────────────────────────────────── + + pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( + filterAttrs (_: ext: ext.pinned) cfg.extensions + ); + + # ── Preferences ──────────────────────────────────────────────────────── + + flattenPrefs = + prefix: attrs: + concatLists ( + mapAttrsToList ( + k: v: + let + path = prefix ++ [ k ]; + in + if isAttrs v && !(isList v) then + flattenPrefs path v + else + [ + { + inherit path; + value = v; + } + ] + ) attrs + ); + + mkPreferencesActivation = + profileName: profileCfg: + let + merged = + profileCfg.preferences + // optionalAttrs (pinnedExtensionIds != [ ]) { + extensions.pinned_extensions = pinnedExtensionIds; + }; + + prefUpdates = builtins.toJSON (flattenPrefs [ ] merged); + hashName = "brave-preferences-${strings.sanitizeDerivationName profileName}"; + in + nameValuePair "setBravePreferences-${profileName}" ( + lib.hm.dag.entryAfter [ "writeBoundary" ] ( + pkgs.callPackage ./set-preferences.nix { + preferencesPath = "${braveDataDir}/${profileName}/Preferences"; + inherit prefUpdates hashName isDarwin; + cacheHome = config.xdg.cacheHome; + } + ) + ); + + # ── Search engines ───────────────────────────────────────────────────── + + mkSearchEnginesActivation = + profileName: profileCfg: + let + hashName = "brave-search-engines-${strings.sanitizeDerivationName profileName}"; + in + nameValuePair "setBraveSearchEngines-${profileName}" ( + lib.hm.dag.entryAfter [ "writeBoundary" ] ( + pkgs.callPackage ./set-search-engines.nix { + engines = profileCfg.searchEngines; + dbPath = "${braveDataDir}/${profileName}/Web Data"; + faviconsDbPath = "${braveDataDir}/${profileName}/Favicons"; + inherit hashName isDarwin; + cacheHome = config.xdg.cacheHome; + } + ) + ); in { - imports = [ ./profiles.nix ]; - options.modules.brave = { enable = mkEnableOption "Brave"; @@ -42,6 +192,16 @@ in description = "Extensions to install, keyed by a human-readable name."; }; + disabledFeatures = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Chromium feature flags to disable via --disable-features. When + non-empty the Brave binary is wrapped with a makeWrapper that + injects the flag. + ''; + }; + # See https://chromeenterprise.google/policies/ and # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy # for the available policies. @@ -53,12 +213,23 @@ in modules.macOSPreferences.apps."com.brave.Browser".forced. ''; }; + + profiles = mkOption { + type = types.attrsOf profileType; + default = { }; + description = '' + Per-profile Brave configuration. Keys are profile directory names + (e.g. "Default", "Profile 1"). + ''; + }; }; config = mkIf cfg.enable { modules.brave = { isDefaultBrowser = true; + disabledFeatures = [ "GlobalMediaControls" ]; + extensions = { proton-pass = { id = "ghmbeldphafepmbegfdlkpapadhbakde"; @@ -110,22 +281,34 @@ in hm = { name = "Home Manager Options"; url = "https://home-manager-options.extranix.com/?query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; + favicon = pkgs.fetchurl { + url = "https://nixos.org/favicon.ico"; + hash = "sha256-D23q83m1MLh3TuYN2rytTsZ5Aski4LrwA4N16PgYaI4="; + }; }; nixo = { name = "NixOS options"; url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; + favicon = pkgs.fetchurl { + url = "https://nixos.org/favicon.ico"; + hash = "sha256-D23q83m1MLh3TuYN2rytTsZ5Aski4LrwA4N16PgYaI4="; + }; }; nixp = { name = "Nix packages"; url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; - favicon_url = "https://nixos.org/favicon.ico"; + favicon = pkgs.fetchurl { + url = "https://nixos.org/favicon.ico"; + hash = "sha256-D23q83m1MLh3TuYN2rytTsZ5Aski4LrwA4N16PgYaI4="; + }; }; std = { name = "std's docs"; url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; - favicon_url = "https://rust-lang.org/logos/rust-logo-blk.svg"; + favicon = pkgs.fetchurl { + url = "https://rust-lang.org/logos/rust-logo-blk.svg"; + hash = "sha256-bW4P0p4gFb7Fypvb3BHt4ehPt5LFYFmbWOVkNGgloik="; + }; }; }; }; @@ -133,34 +316,26 @@ in programs.brave = { enable = true; - package = - if isDarwin then - pkgs.symlinkJoin { - name = "brave-wrapped"; - paths = [ pkgs.brave ]; - nativeBuildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - rm "$out/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" - makeWrapper \ - "${pkgs.brave}/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \ - "$out/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \ - --add-flags "--disable-features=GlobalMediaControls" - ''; - } - else - pkgs.brave; + package = if cfg.disabledFeatures != [ ] then wrappedBrave else pkgs.brave; extensions = mapAttrsToList (_: ext: { inherit (ext) id; }) cfg.extensions; }; modules.macOSPreferences.apps."com.brave.Browser".forced = cfg.policies; - home.activation = mkIf isDarwin ( - optionalAttrs cfg.isDefaultBrowser { + home.activation = + ( + cfg.profiles + |> filterAttrs (_: p: p.preferences != { } || pinnedExtensionIds != [ ]) + |> mapAttrs' mkPreferencesActivation + ) + // ( + cfg.profiles |> filterAttrs (_: p: p.searchEngines != { }) |> mapAttrs' mkSearchEnginesActivation + ) + // optionalAttrs (isDarwin && cfg.isDefaultBrowser) { setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' run ${pkgs.defaultbrowser}/bin/defaultbrowser browser ''; - } - ); + }; xdg.mimeApps = mkIf (isLinux && cfg.isDefaultBrowser) { enable = true; diff --git a/modules/home/brave/profiles.nix b/modules/home/brave/profiles.nix deleted file mode 100644 index 0a20f72e..00000000 --- a/modules/home/brave/profiles.nix +++ /dev/null @@ -1,276 +0,0 @@ -# Per-profile Brave configuration module. -# -# Each profile (e.g. "Default", "Profile 1") has: -# -# - preferences: a nested attrset that gets flattened into [{path, value}] -# pairs and applied to the profile's Preferences JSON file via jq. -# -# - searchEngines: keyword-triggered search engines written to the -# profile's Web Data SQLite database. -# -# Pinned extension IDs (from modules.brave.extensions) are automatically -# merged into each profile's preferences under extensions.pinned_extensions. - -{ - config, - lib, - pkgs, - ... -}: - -with lib; -let - cfg = config.modules.brave; - inherit (pkgs.stdenv) isDarwin; - - braveDataDir = "${config.home.homeDirectory}/Library/Application Support/BraveSoftware/Brave-Browser"; - - # ── Extension pinning ────────────────────────────────────────────────── - - pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( - filterAttrs (_: ext: ext.pinned) cfg.extensions - ); - - # ── Preferences ──────────────────────────────────────────────────────── - - # Flatten a nested attrset into a list of [{path, value}] pairs suitable - # for jq's setpath(). - flattenPrefs = - prefix: attrs: - concatLists ( - mapAttrsToList ( - k: v: - let - path = prefix ++ [ k ]; - in - if isAttrs v && !(isList v) then - flattenPrefs path v - else - [ - { - inherit path; - value = v; - } - ] - ) attrs - ); - - mkPrefUpdates = - profileName: profileCfg: - let - merged = - profileCfg.preferences - // optionalAttrs (pinnedExtensionIds != [ ]) { - extensions.pinned_extensions = pinnedExtensionIds; - }; - in - builtins.toJSON (flattenPrefs [ ] merged); - - mkPreferencesActivation = - profileName: profileCfg: - let - prefUpdates = mkPrefUpdates profileName profileCfg; - preferencesPath = "${braveDataDir}/${profileName}/Preferences"; - hashName = "brave-preferences-${lib.strings.sanitizeDerivationName profileName}"; - in - nameValuePair "setBravePreferences-${profileName}" ( - lib.hm.dag.entryAfter [ "writeBoundary" ] '' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_preferences() { - # Exit early if Preferences doesn't exist yet. - [[ -f "${preferencesPath}" ]] || return 0 - - # Generate the checksum of the preference updates. - pref_hash=$(echo -n '${prefUpdates}' | ${pkgs.openssl}/bin/openssl dgst -sha256 | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/${hashName}.hash" - - # Exit early if the preferences haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then - echo "Brave preferences (${profileName}) already up to date" - return 0 - fi - - # Brave writes the Preferences file on exit, so we need to quit - # it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply each preference update. - run ${pkgs.jq}/bin/jq \ - --argjson updates '${prefUpdates}' \ - 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ - "${preferencesPath}" > "${preferencesPath}.tmp" - - run mv "${preferencesPath}.tmp" "${preferencesPath}" - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$pref_hash" > "$hash_file" - } - - apply_brave_preferences - '' - ); - - # ── Search engines ───────────────────────────────────────────────────── - - searchEngineType = types.submodule { - options = { - name = mkOption { - type = types.str; - description = "Display name of the search engine."; - }; - url = mkOption { - type = types.str; - description = "Search URL template. Use {searchTerms} as placeholder."; - }; - favicon_url = mkOption { - type = types.str; - default = ""; - description = "URL to the search engine's favicon."; - }; - }; - }; - - nix2Sql = - v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; - - mkSqlScript = - profileCfg: - let - enginesList = mapAttrsToList (keyword: engine: { - inherit keyword; - short_name = engine.name; - inherit (engine) url favicon_url; - safe_for_autoreplace = 0; - created_by_policy = 1; - input_encodings = "UTF-8"; - }) profileCfg.searchEngines; - in - pkgs.writeText "brave-search-engines.sql" '' - -- Remove all policy-managed search engines. - DELETE FROM keywords WHERE created_by_policy = 1; - - -- Insert the configured search engines. - ${concatMapStringsSep "\n" ( - engine: - let - columns = builtins.attrNames engine; - values = map (col: nix2Sql engine.${col}) columns; - in - "INSERT INTO keywords (${concatStringsSep ", " columns}) VALUES (${concatStringsSep ", " values});" - ) enginesList} - ''; - - mkSearchEnginesActivation = - profileName: profileCfg: - let - sqlScriptFile = mkSqlScript profileCfg; - dbPath = "${braveDataDir}/${profileName}/Web Data"; - hashName = "brave-search-engines-${lib.strings.sanitizeDerivationName profileName}"; - in - nameValuePair "setBraveSearchEngines-${profileName}" ( - lib.hm.dag.entryAfter [ "writeBoundary" ] '' - is_brave_running() { - /usr/bin/pgrep -x "Brave Browser" > /dev/null 2>&1 - } - - apply_brave_search_engines() { - # Exit early if Brave hasn't yet created the DB. - [[ -f "${dbPath}" ]] || return 0 - - # Generate the checksum of the SQL script. - script_hash=$(${pkgs.openssl}/bin/openssl dgst -sha256 ${sqlScriptFile} | cut -d' ' -f2) - hash_file="${config.xdg.cacheHome}/home-manager/${hashName}.hash" - - # Exit early if the search engines haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then - echo "Brave search engines (${profileName}) already up to date" - return 0 - fi - - # The database is locked while Brave is running, so we need to - # quit it first. - brave_was_running=0 - if is_brave_running; then - brave_was_running=1 - run /usr/bin/osascript -e 'quit app "Brave Browser"' - while is_brave_running; do /bin/sleep 0.5; done - fi - - # Apply SQL changes. - run ${pkgs.sqlite}/bin/sqlite3 "${dbPath}" < ${sqlScriptFile} - - # Restart Brave if it was running. - if [[ "$brave_was_running" -eq 1 ]]; then - run /usr/bin/open -a "Brave Browser" - fi - - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$script_hash" > "$hash_file" - } - - apply_brave_search_engines - '' - ); - - # ── Profile type ─────────────────────────────────────────────────────── - - profileType = types.submodule { - options = { - preferences = mkOption { - type = types.attrs; - default = { }; - description = '' - Nested attrset of Brave JSON preferences for this profile. - Keys mirror the structure of the Preferences JSON file (e.g. - `brave.new_tab_page.show_stats = false`). Pinned extension IDs - are merged automatically. - ''; - }; - searchEngines = mkOption { - type = types.attrsOf searchEngineType; - default = { }; - description = '' - Custom search engines for this profile. The attribute name is - used as the keyword (shortcut). - ''; - }; - }; - }; - - profilesWithPrefs = filterAttrs ( - _: p: p.preferences != { } || pinnedExtensionIds != [ ] - ) cfg.profiles; - profilesWithEngines = filterAttrs (_: p: p.searchEngines != { }) cfg.profiles; -in -{ - options.modules.brave.profiles = mkOption { - type = types.attrsOf profileType; - default = { }; - description = '' - Per-profile Brave configuration. Keys are profile directory names - (e.g. "Default", "Profile 1"). - ''; - }; - - config = mkIf cfg.enable { - home.activation = mkIf isDarwin ( - (mapAttrs' mkPreferencesActivation profilesWithPrefs) - // (mapAttrs' mkSearchEnginesActivation profilesWithEngines) - ); - }; -} diff --git a/modules/home/brave/set-preferences.nix b/modules/home/brave/set-preferences.nix new file mode 100644 index 00000000..8fabc7cb --- /dev/null +++ b/modules/home/brave/set-preferences.nix @@ -0,0 +1,78 @@ +# Returns a bash script that patches Brave's Preferences JSON file for a +# single profile using jq. Skips the update when the file doesn't exist +# yet or when nothing has changed (hash-based). +{ + lib, + jq, + openssl, + # ── + preferencesPath, + prefUpdates, + hashName, + cacheHome, + isDarwin, +}: + +let + pgrep = if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; + + quit = + if isDarwin then ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' else "pkill -TERM brave"; + + relaunch = if isDarwin then ''/usr/bin/open -a "Brave Browser"'' else "brave &"; +in +'' + is_brave_running() { + ${pgrep} > /dev/null 2>&1 + } + + quit_brave() { + ${quit} + while is_brave_running; do sleep 0.5; done + } + + relaunch_brave() { + run ${relaunch} + } + + apply_brave_preferences() { + # Exit early if Preferences doesn't exist yet. + [[ -f "${preferencesPath}" ]] || return 0 + + # Generate the checksum of the preference updates. + pref_hash=$(echo -n '${prefUpdates}' | ${lib.getExe' openssl "openssl"} dgst -sha256 | cut -d' ' -f2) + hash_file="${cacheHome}/home-manager/${hashName}.hash" + + # Exit early if the preferences haven't changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then + return 0 + fi + + # Brave writes the Preferences file on exit, so we need to quit it + # first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + quit_brave + fi + + # Apply each preference update. + run ${lib.getExe jq} \ + --argjson updates '${prefUpdates}' \ + 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ + "${preferencesPath}" > "${preferencesPath}.tmp" + + run mv "${preferencesPath}.tmp" "${preferencesPath}" + + # Restart Brave if it was running. + if [[ "$brave_was_running" -eq 1 ]]; then + relaunch_brave + fi + + # Store the hash. + run mkdir -p "$(dirname "$hash_file")" + run echo "$pref_hash" > "$hash_file" + } + + apply_brave_preferences +'' diff --git a/modules/home/brave/set-search-engines.nix b/modules/home/brave/set-search-engines.nix new file mode 100644 index 00000000..3f72d2a7 --- /dev/null +++ b/modules/home/brave/set-search-engines.nix @@ -0,0 +1,178 @@ +# Returns a bash script that writes custom search engines into Brave's +# "Web Data" SQLite database for a single profile, and optionally inserts +# favicons into the "Favicons" database. +{ + lib, + imagemagick, + openssl, + sqlite, + unixtools, + writeText, + # ── + engines, # attrsOf { name, url, favicon? } + dbPath, + faviconsDbPath, + hashName, + cacheHome, + isDarwin, +}: + +let + nix2Sql = + v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; + + enginesList = lib.mapAttrsToList (keyword: engine: { + inherit keyword; + short_name = engine.name; + inherit (engine) url; + # NOT NULL in the schema — set to our managed URL when a favicon is + # provided, empty string otherwise. + favicon_url = if engine ? favicon && engine.favicon != null then "nix-managed://${keyword}" else ""; + safe_for_autoreplace = 0; + created_by_policy = 1; + input_encodings = "UTF-8"; + }) engines; + + sqlScript = writeText "brave-search-engines.sql" '' + -- Remove all policy-managed search engines. + DELETE FROM keywords WHERE created_by_policy = 1; + + -- Insert the configured search engines. + ${lib.concatMapStringsSep "\n" ( + engine: + let + columns = builtins.attrNames engine; + values = map (col: nix2Sql engine.${col}) columns; + in + "INSERT INTO keywords (${lib.concatStringsSep ", " columns}) VALUES (${lib.concatStringsSep ", " values});" + ) enginesList} + ''; + + # Engines that have a favicon derivation. + enginesWithFavicons = lib.filterAttrs (_: e: e ? favicon && e.favicon != null) engines; + + # For each engine with a favicon, generate a SQL snippet that: + # 1. Inserts a row into `favicons` (the icon URL) + # 2. Inserts 16x16 and 32x32 PNG bitmaps into `favicon_bitmaps` + # 3. Updates the `favicon_url` in `keywords` to point to the icon URL + # + # The actual PNG conversion happens at build time via ImageMagick, and + # the binary data is hex-encoded so it can be inlined into the SQL as + # X'...' literals. + mkFaviconDerivation = + keyword: engine: + let + faviconUrl = "nix-managed://${keyword}"; + in + { + inherit keyword faviconUrl; + icon16 = engine.favicon; + icon32 = engine.favicon; + }; + + faviconEntries = lib.mapAttrsToList mkFaviconDerivation enginesWithFavicons; + + pgrep = if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; + + quit = + if isDarwin then ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' else "pkill -TERM brave"; + + relaunch = if isDarwin then ''/usr/bin/open -a "Brave Browser"'' else "brave &"; + + convert = lib.getExe' imagemagick "magick"; + sqlite3 = lib.getExe sqlite; +in +'' + is_brave_running() { + ${pgrep} > /dev/null 2>&1 + } + + quit_brave() { + ${quit} + while is_brave_running; do sleep 0.5; done + } + + relaunch_brave() { + run ${relaunch} + } + + apply_brave_search_engines() { + # Exit early if Brave hasn't yet created the DB. + [[ -f "${dbPath}" ]] || return 0 + + # Generate the checksum of the SQL script + favicon sources. + script_hash=$(${lib.getExe' openssl "openssl"} dgst -sha256 ${sqlScript} ${ + lib.concatMapStringsSep " " (e: toString e.icon16) faviconEntries + } | cut -d' ' -f2 | ${lib.getExe' openssl "openssl"} dgst -sha256 | cut -d' ' -f2) + hash_file="${cacheHome}/home-manager/${hashName}.hash" + + # Exit early if nothing has changed. + if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then + return 0 + fi + + # The database is locked while Brave is running, so we need to quit + # it first. + brave_was_running=0 + if is_brave_running; then + brave_was_running=1 + quit_brave + fi + + # Apply search engine changes. + run ${sqlite3} "${dbPath}" < ${sqlScript} + + # Apply favicon changes. + ${lib.concatMapStringsSep "\n" (entry: '' + if [[ -f "${faviconsDbPath}" ]]; then + # Convert the source image to 16x16 and 32x32 PNGs. + icon16=$(mktemp) + icon32=$(mktemp) + ${convert} "${toString entry.icon16}[0]" -resize 16x16 -background none -gravity center -extent 16x16 PNG32:"$icon16" + ${convert} "${toString entry.icon32}[0]" -resize 32x32 -background none -gravity center -extent 32x32 PNG32:"$icon32" + + hex16=$(${lib.getExe unixtools.xxd} -p "$icon16" | tr -d '\n') + hex32=$(${lib.getExe unixtools.xxd} -p "$icon32" | tr -d '\n') + + run ${sqlite3} "${faviconsDbPath}" < "$hash_file" + } + + apply_brave_search_engines +'' From 8e39882939d227618f5d20bf1bb751b3e3acdff7 Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Fri, 20 Mar 2026 23:54:30 +0100 Subject: [PATCH 6/7] wip --- modules/home/brave/default.nix | 97 +++++++++-------- modules/home/brave/set-preferences.nix | 52 +++------ modules/home/brave/set-search-engines.nix | 125 ++++++---------------- 3 files changed, 104 insertions(+), 170 deletions(-) diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index fc80c846..59808845 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -47,8 +47,8 @@ let default = null; description = '' Path to a favicon image (any format ImageMagick can read). - The image is converted to 16x16 and 32x32 PNGs at activation - time and inserted into Brave's Favicons database. + Rasterized to a 256x256 PNG at activation time and inserted + into Brave's Favicons database. ''; }; }; @@ -135,6 +135,8 @@ let ) attrs ); + # ── Per-profile activation entries ─────────────────────────────────── + mkPreferencesActivation = profileName: profileCfg: let @@ -143,26 +145,23 @@ let // optionalAttrs (pinnedExtensionIds != [ ]) { extensions.pinned_extensions = pinnedExtensionIds; }; - - prefUpdates = builtins.toJSON (flattenPrefs [ ] merged); - hashName = "brave-preferences-${strings.sanitizeDerivationName profileName}"; + sanitized = strings.sanitizeDerivationName profileName; in nameValuePair "setBravePreferences-${profileName}" ( lib.hm.dag.entryAfter [ "writeBoundary" ] ( pkgs.callPackage ./set-preferences.nix { preferencesPath = "${braveDataDir}/${profileName}/Preferences"; - inherit prefUpdates hashName isDarwin; - cacheHome = config.xdg.cacheHome; + prefUpdates = builtins.toJSON (flattenPrefs [ ] merged); + hashFile = "${config.xdg.cacheHome}/home-manager/brave-preferences-${sanitized}.hash"; + inherit isDarwin; } ) ); - # ── Search engines ───────────────────────────────────────────────────── - mkSearchEnginesActivation = profileName: profileCfg: let - hashName = "brave-search-engines-${strings.sanitizeDerivationName profileName}"; + sanitized = strings.sanitizeDerivationName profileName; in nameValuePair "setBraveSearchEngines-${profileName}" ( lib.hm.dag.entryAfter [ "writeBoundary" ] ( @@ -170,8 +169,8 @@ let engines = profileCfg.searchEngines; dbPath = "${braveDataDir}/${profileName}/Web Data"; faviconsDbPath = "${braveDataDir}/${profileName}/Favicons"; - inherit hashName isDarwin; - cacheHome = config.xdg.cacheHome; + hashFile = "${config.xdg.cacheHome}/home-manager/brave-search-engines-${sanitized}.hash"; + inherit isDarwin; } ) ); @@ -277,40 +276,52 @@ in toolbar.pinned_actions = [ ]; }; - searchEngines = { - hm = { - name = "Home Manager Options"; - url = "https://home-manager-options.extranix.com/?query={searchTerms}"; - favicon = pkgs.fetchurl { - url = "https://nixos.org/favicon.ico"; - hash = "sha256-D23q83m1MLh3TuYN2rytTsZ5Aski4LrwA4N16PgYaI4="; + searchEngines = + let + nixFavicon = pkgs.fetchurl { + url = "https://nixos.org/favicon.svg"; + hash = "sha256-UL/Eyk/e7Yrfz8uR9MZwB80a+S4HC9CjixpW8tpJMvY="; }; - }; - nixo = { - name = "NixOS options"; - url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; - favicon = pkgs.fetchurl { - url = "https://nixos.org/favicon.ico"; - hash = "sha256-D23q83m1MLh3TuYN2rytTsZ5Aski4LrwA4N16PgYaI4="; + in + { + hm = { + name = "Home Manager Options"; + url = "https://home-manager-options.extranix.com/?query={searchTerms}"; + favicon = nixFavicon; }; - }; - nixp = { - name = "Nix packages"; - url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; - favicon = pkgs.fetchurl { - url = "https://nixos.org/favicon.ico"; - hash = "sha256-D23q83m1MLh3TuYN2rytTsZ5Aski4LrwA4N16PgYaI4="; + nixo = { + name = "NixOS options"; + url = "https://search.nixos.org/options?channel=unstable&query={searchTerms}"; + favicon = nixFavicon; }; - }; - std = { - name = "std's docs"; - url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; - favicon = pkgs.fetchurl { - url = "https://rust-lang.org/logos/rust-logo-blk.svg"; - hash = "sha256-bW4P0p4gFb7Fypvb3BHt4ehPt5LFYFmbWOVkNGgloik="; + nixp = { + name = "Nix packages"; + url = "https://search.nixos.org/packages?channel=unstable&query={searchTerms}"; + favicon = nixFavicon; }; + std = + let + # The official Rust favicon uses @media (prefers-color-scheme) + # which rsvg-convert doesn't handle, rendering it black. + # We recolor it to white at build time. + rustFaviconBlk = pkgs.fetchurl { + name = "rust-favicon.svg"; + url = "https://rust-lang.org/static/images/favicon.svg"; + hash = "sha256-BEvjkUSrMEz56g6QFMP0duDFGUxiqlJbNmnK9dtawIg="; + }; + rustFavicon = pkgs.runCommand "rust-favicon-white.png" { } '' + ${lib.getExe' pkgs.imagemagick "magick"} \ + -density 384 -background none "${rustFaviconBlk}" \ + -fill white -colorize 100 \ + PNG32:"$out" + ''; + in + { + name = "std's docs"; + url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; + favicon = rustFavicon; + }; }; - }; }; }; @@ -329,7 +340,9 @@ in |> mapAttrs' mkPreferencesActivation ) // ( - cfg.profiles |> filterAttrs (_: p: p.searchEngines != { }) |> mapAttrs' mkSearchEnginesActivation + cfg.profiles + |> filterAttrs (_: p: p.searchEngines != { }) + |> mapAttrs' mkSearchEnginesActivation ) // optionalAttrs (isDarwin && cfg.isDefaultBrowser) { setBraveAsDefaultBrowser = lib.hm.dag.entryAfter [ "writeBoundary" ] '' diff --git a/modules/home/brave/set-preferences.nix b/modules/home/brave/set-preferences.nix index 8fabc7cb..66320f9d 100644 --- a/modules/home/brave/set-preferences.nix +++ b/modules/home/brave/set-preferences.nix @@ -1,6 +1,5 @@ # Returns a bash script that patches Brave's Preferences JSON file for a -# single profile using jq. Skips the update when the file doesn't exist -# yet or when nothing has changed (hash-based). +# single profile using jq. Quits and relaunches Brave if it's running. { lib, jq, @@ -8,12 +7,13 @@ # ── preferencesPath, prefUpdates, - hashName, - cacheHome, + hashFile, isDarwin, }: let + sha = lib.getExe' openssl "openssl"; + pgrep = if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; quit = @@ -22,41 +22,23 @@ let relaunch = if isDarwin then ''/usr/bin/open -a "Brave Browser"'' else "brave &"; in '' - is_brave_running() { - ${pgrep} > /dev/null 2>&1 - } - - quit_brave() { - ${quit} - while is_brave_running; do sleep 0.5; done - } - - relaunch_brave() { - run ${relaunch} - } - - apply_brave_preferences() { - # Exit early if Preferences doesn't exist yet. + _set_brave_preferences() { [[ -f "${preferencesPath}" ]] || return 0 - # Generate the checksum of the preference updates. - pref_hash=$(echo -n '${prefUpdates}' | ${lib.getExe' openssl "openssl"} dgst -sha256 | cut -d' ' -f2) - hash_file="${cacheHome}/home-manager/${hashName}.hash" + local pref_hash + pref_hash=$(echo -n '${prefUpdates}' | ${sha} dgst -sha256 | cut -d' ' -f2) - # Exit early if the preferences haven't changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$pref_hash" ]]; then + if [[ -f "${hashFile}" ]] && [[ "$(cat "${hashFile}")" == "$pref_hash" ]]; then return 0 fi - # Brave writes the Preferences file on exit, so we need to quit it - # first. - brave_was_running=0 - if is_brave_running; then + local brave_was_running=0 + if ${pgrep} > /dev/null 2>&1; then brave_was_running=1 - quit_brave + ${quit} + while ${pgrep} > /dev/null 2>&1; do sleep 0.5; done fi - # Apply each preference update. run ${lib.getExe jq} \ --argjson updates '${prefUpdates}' \ 'reduce $updates[] as $update (.; setpath($update.path; $update.value))' \ @@ -64,15 +46,13 @@ in run mv "${preferencesPath}.tmp" "${preferencesPath}" - # Restart Brave if it was running. if [[ "$brave_was_running" -eq 1 ]]; then - relaunch_brave + run ${relaunch} fi - # Store the hash. - run mkdir -p "$(dirname "$hash_file")" - run echo "$pref_hash" > "$hash_file" + run mkdir -p "$(dirname "${hashFile}")" + echo "$pref_hash" > "${hashFile}" } - apply_brave_preferences + _set_brave_preferences '' diff --git a/modules/home/brave/set-search-engines.nix b/modules/home/brave/set-search-engines.nix index 3f72d2a7..b437c146 100644 --- a/modules/home/brave/set-search-engines.nix +++ b/modules/home/brave/set-search-engines.nix @@ -1,6 +1,7 @@ # Returns a bash script that writes custom search engines into Brave's # "Web Data" SQLite database for a single profile, and optionally inserts -# favicons into the "Favicons" database. +# favicons into the "Favicons" database. Quits and relaunches Brave if +# it's running. { lib, imagemagick, @@ -12,8 +13,7 @@ engines, # attrsOf { name, url, favicon? } dbPath, faviconsDbPath, - hashName, - cacheHome, + hashFile, isDarwin, }: @@ -25,8 +25,6 @@ let inherit keyword; short_name = engine.name; inherit (engine) url; - # NOT NULL in the schema — set to our managed URL when a favicon is - # provided, empty string otherwise. favicon_url = if engine ? favicon && engine.favicon != null then "nix-managed://${keyword}" else ""; safe_for_autoreplace = 0; created_by_policy = 1; @@ -48,29 +46,18 @@ let ) enginesList} ''; - # Engines that have a favicon derivation. enginesWithFavicons = lib.filterAttrs (_: e: e ? favicon && e.favicon != null) engines; - # For each engine with a favicon, generate a SQL snippet that: - # 1. Inserts a row into `favicons` (the icon URL) - # 2. Inserts 16x16 and 32x32 PNG bitmaps into `favicon_bitmaps` - # 3. Updates the `favicon_url` in `keywords` to point to the icon URL - # - # The actual PNG conversion happens at build time via ImageMagick, and - # the binary data is hex-encoded so it can be inlined into the SQL as - # X'...' literals. - mkFaviconDerivation = - keyword: engine: - let - faviconUrl = "nix-managed://${keyword}"; - in - { - inherit keyword faviconUrl; - icon16 = engine.favicon; - icon32 = engine.favicon; - }; - - faviconEntries = lib.mapAttrsToList mkFaviconDerivation enginesWithFavicons; + faviconEntries = lib.mapAttrsToList (keyword: engine: { + inherit keyword; + faviconUrl = "nix-managed://${keyword}"; + src = engine.favicon; + }) enginesWithFavicons; + + sha = lib.getExe' openssl "openssl"; + convert = lib.getExe' imagemagick "magick"; + sqlite3 = lib.getExe sqlite; + xxd = lib.getExe unixtools.xxd; pgrep = if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; @@ -78,101 +65,55 @@ let if isDarwin then ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' else "pkill -TERM brave"; relaunch = if isDarwin then ''/usr/bin/open -a "Brave Browser"'' else "brave &"; - - convert = lib.getExe' imagemagick "magick"; - sqlite3 = lib.getExe sqlite; in '' - is_brave_running() { - ${pgrep} > /dev/null 2>&1 - } - - quit_brave() { - ${quit} - while is_brave_running; do sleep 0.5; done - } - - relaunch_brave() { - run ${relaunch} - } - - apply_brave_search_engines() { - # Exit early if Brave hasn't yet created the DB. + _set_brave_search_engines() { [[ -f "${dbPath}" ]] || return 0 - # Generate the checksum of the SQL script + favicon sources. - script_hash=$(${lib.getExe' openssl "openssl"} dgst -sha256 ${sqlScript} ${ - lib.concatMapStringsSep " " (e: toString e.icon16) faviconEntries - } | cut -d' ' -f2 | ${lib.getExe' openssl "openssl"} dgst -sha256 | cut -d' ' -f2) - hash_file="${cacheHome}/home-manager/${hashName}.hash" + local script_hash + script_hash=$(${sha} dgst -sha256 ${sqlScript} ${ + lib.concatMapStringsSep " " (e: toString e.src) faviconEntries + } | cut -d' ' -f2 | ${sha} dgst -sha256 | cut -d' ' -f2) - # Exit early if nothing has changed. - if [[ -f "$hash_file" ]] && [[ "$(cat "$hash_file")" == "$script_hash" ]]; then + if [[ -f "${hashFile}" ]] && [[ "$(cat "${hashFile}")" == "$script_hash" ]]; then return 0 fi - # The database is locked while Brave is running, so we need to quit - # it first. - brave_was_running=0 - if is_brave_running; then + local brave_was_running=0 + if ${pgrep} > /dev/null 2>&1; then brave_was_running=1 - quit_brave + ${quit} + while ${pgrep} > /dev/null 2>&1; do sleep 0.5; done fi - # Apply search engine changes. run ${sqlite3} "${dbPath}" < ${sqlScript} - # Apply favicon changes. ${lib.concatMapStringsSep "\n" (entry: '' if [[ -f "${faviconsDbPath}" ]]; then - # Convert the source image to 16x16 and 32x32 PNGs. - icon16=$(mktemp) - icon32=$(mktemp) - ${convert} "${toString entry.icon16}[0]" -resize 16x16 -background none -gravity center -extent 16x16 PNG32:"$icon16" - ${convert} "${toString entry.icon32}[0]" -resize 32x32 -background none -gravity center -extent 32x32 PNG32:"$icon32" + local icon hex + icon=$(mktemp) + ${convert} -density 384 -background none "${toString entry.src}[0]" -resize 256x256 -gravity center -extent 256x256 PNG32:"$icon" - hex16=$(${lib.getExe unixtools.xxd} -p "$icon16" | tr -d '\n') - hex32=$(${lib.getExe unixtools.xxd} -p "$icon32" | tr -d '\n') + hex=$(${xxd} -p "$icon" | tr -d '\n') run ${sqlite3} "${faviconsDbPath}" < "$hash_file" + run mkdir -p "$(dirname "${hashFile}")" + echo "$script_hash" > "${hashFile}" } - apply_brave_search_engines + _set_brave_search_engines '' From a54bc9b90bfc0eefda2f3a377b7f32d6aa35a37c Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Sat, 21 Mar 2026 00:07:13 +0100 Subject: [PATCH 7/7] wip --- modules/home/brave/default.nix | 67 ++++++++--------------- modules/home/brave/set-preferences.nix | 12 ++-- modules/home/brave/set-search-engines.nix | 44 ++++++++++----- 3 files changed, 60 insertions(+), 63 deletions(-) diff --git a/modules/home/brave/default.nix b/modules/home/brave/default.nix index 59808845..962f94bb 100644 --- a/modules/home/brave/default.nix +++ b/modules/home/brave/default.nix @@ -16,18 +16,18 @@ let else "${config.xdg.configHome}/BraveSoftware/Brave-Browser"; - # ── Types ────────────────────────────────────────────────────────────── + # ── Types ── extensionType = types.submodule { options = { id = mkOption { - type = types.str; - description = "Chrome Web Store extension ID."; + type = types.singleLineStr; + description = "Chrome Web Store extension ID"; }; pinned = mkOption { type = types.bool; default = false; - description = "Whether to pin this extension to the toolbar."; + description = "Whether to pin this extension to the toolbar"; }; }; }; @@ -35,20 +35,18 @@ let searchEngineType = types.submodule { options = { name = mkOption { - type = types.str; - description = "Display name of the search engine."; + type = types.singleLineStr; + description = "Display name of the search engine"; }; url = mkOption { - type = types.str; - description = "Search URL template. Use {searchTerms} as placeholder."; + type = types.singleLineStr; + description = "Search URL template. Use {searchTerms} as placeholder"; }; favicon = mkOption { type = types.nullOr types.path; default = null; description = '' - Path to a favicon image (any format ImageMagick can read). - Rasterized to a 256x256 PNG at activation time and inserted - into Brave's Favicons database. + Path to a favicon image, in any format ImageMagick can read ''; }; }; @@ -59,12 +57,7 @@ let preferences = mkOption { type = types.attrs; default = { }; - description = '' - Nested attrset of Brave JSON preferences for this profile. - Keys mirror the structure of the Preferences JSON file (e.g. - `brave.new_tab_page.show_stats = false`). Pinned extension IDs - are merged automatically. - ''; + description = "Nested attrset of Brave JSON preferences for this profile"; }; searchEngines = mkOption { type = types.attrsOf searchEngineType; @@ -77,7 +70,7 @@ let }; }; - # ── Package wrapping ─────────────────────────────────────────────────── + # ── Package wrapping ── wrappedBrave = let @@ -107,14 +100,12 @@ let ''; }; - # ── Extension pinning ────────────────────────────────────────────────── + # ── Per-profile activation entries ── pinnedExtensionIds = mapAttrsToList (_: ext: ext.id) ( filterAttrs (_: ext: ext.pinned) cfg.extensions ); - # ── Preferences ──────────────────────────────────────────────────────── - flattenPrefs = prefix: attrs: concatLists ( @@ -135,8 +126,6 @@ let ) attrs ); - # ── Per-profile activation entries ─────────────────────────────────── - mkPreferencesActivation = profileName: profileCfg: let @@ -182,35 +171,28 @@ in isDefaultBrowser = mkOption { type = types.bool; default = false; - description = "Whether to set Brave as the default browser."; + description = "Whether to set Brave as the default browser"; }; extensions = mkOption { type = types.attrsOf extensionType; default = { }; - description = "Extensions to install, keyed by a human-readable name."; + description = "Extensions to install, keyed by a human-readable name"; }; disabledFeatures = mkOption { type = types.listOf types.str; default = [ ]; - description = '' - Chromium feature flags to disable via --disable-features. When - non-empty the Brave binary is wrapped with a makeWrapper that - injects the flag. - ''; + description = "Chromium feature flags to disable via --disable-features"; }; # See https://chromeenterprise.google/policies/ and - # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy - # for the available policies. + # https://support.brave.app/hc/en-us/articles/360039248271-Group-Policy for + # the available policies. policies = mkOption { type = types.attrs; default = { }; - description = '' - Enterprise policies fed directly into - modules.macOSPreferences.apps."com.brave.Browser".forced. - ''; + description = "Enterprise policies"; }; profiles = mkOption { @@ -218,7 +200,7 @@ in default = { }; description = '' Per-profile Brave configuration. Keys are profile directory names - (e.g. "Default", "Profile 1"). + (e.g. "Default", "Profile-1", etc.). ''; }; }; @@ -301,17 +283,14 @@ in }; std = let - # The official Rust favicon uses @media (prefers-color-scheme) - # which rsvg-convert doesn't handle, rendering it black. - # We recolor it to white at build time. - rustFaviconBlk = pkgs.fetchurl { + rustFavicon = pkgs.fetchurl { name = "rust-favicon.svg"; url = "https://rust-lang.org/static/images/favicon.svg"; hash = "sha256-BEvjkUSrMEz56g6QFMP0duDFGUxiqlJbNmnK9dtawIg="; }; - rustFavicon = pkgs.runCommand "rust-favicon-white.png" { } '' + rustFaviconWhite = pkgs.runCommand "rust-favicon-white.png" { } '' ${lib.getExe' pkgs.imagemagick "magick"} \ - -density 384 -background none "${rustFaviconBlk}" \ + -density 384 -background none "${rustFavicon}" \ -fill white -colorize 100 \ PNG32:"$out" ''; @@ -319,7 +298,7 @@ in { name = "std's docs"; url = "https://doc.rust-lang.org/nightly/std/?search={searchTerms}"; - favicon = rustFavicon; + favicon = rustFaviconWhite; }; }; }; diff --git a/modules/home/brave/set-preferences.nix b/modules/home/brave/set-preferences.nix index 66320f9d..a742b431 100644 --- a/modules/home/brave/set-preferences.nix +++ b/modules/home/brave/set-preferences.nix @@ -1,5 +1,5 @@ -# Returns a bash script that patches Brave's Preferences JSON file for a -# single profile using jq. Quits and relaunches Brave if it's running. +# A script that patches Brave's Preferences JSON file for a single profile. +# Quits and relaunches Brave if it's running. { lib, jq, @@ -14,10 +14,14 @@ let sha = lib.getExe' openssl "openssl"; - pgrep = if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; + pgrep = + if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; quit = - if isDarwin then ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' else "pkill -TERM brave"; + if isDarwin then + ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' + else + "pkill -TERM brave"; relaunch = if isDarwin then ''/usr/bin/open -a "Brave Browser"'' else "brave &"; in diff --git a/modules/home/brave/set-search-engines.nix b/modules/home/brave/set-search-engines.nix index b437c146..e551ac05 100644 --- a/modules/home/brave/set-search-engines.nix +++ b/modules/home/brave/set-search-engines.nix @@ -1,7 +1,6 @@ -# Returns a bash script that writes custom search engines into Brave's -# "Web Data" SQLite database for a single profile, and optionally inserts -# favicons into the "Favicons" database. Quits and relaunches Brave if -# it's running. +# A script that writes custom search engines into Brave's "Web Data" SQLite +# database for a single profile, and optionally inserts favicons into the +# "Favicons" database. Quits and relaunches Brave if it's running. { lib, imagemagick, @@ -19,13 +18,21 @@ let nix2Sql = - v: if builtins.isString v then "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" else toString v; + v: + if builtins.isString v then + "'${builtins.replaceStrings [ "'" ] [ "''" ] v}'" + else + toString v; enginesList = lib.mapAttrsToList (keyword: engine: { inherit keyword; short_name = engine.name; inherit (engine) url; - favicon_url = if engine ? favicon && engine.favicon != null then "nix-managed://${keyword}" else ""; + favicon_url = + if engine ? favicon && engine.favicon != null then + "nix-managed://${keyword}" + else + ""; safe_for_autoreplace = 0; created_by_policy = 1; input_encodings = "UTF-8"; @@ -46,23 +53,30 @@ let ) enginesList} ''; - enginesWithFavicons = lib.filterAttrs (_: e: e ? favicon && e.favicon != null) engines; - - faviconEntries = lib.mapAttrsToList (keyword: engine: { - inherit keyword; - faviconUrl = "nix-managed://${keyword}"; - src = engine.favicon; - }) enginesWithFavicons; + faviconEntries = + engines + |> lib.filterAttrs (_: engine: engine ? favicon && engine.favicon != null) + |> lib.mapAttrsToList ( + keyword: engine: { + inherit keyword; + faviconUrl = "nix-managed://${keyword}"; + src = engine.favicon; + } + ); sha = lib.getExe' openssl "openssl"; convert = lib.getExe' imagemagick "magick"; sqlite3 = lib.getExe sqlite; xxd = lib.getExe unixtools.xxd; - pgrep = if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; + pgrep = + if isDarwin then ''/usr/bin/pgrep -x "Brave Browser"'' else "pgrep -x brave"; quit = - if isDarwin then ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' else "pkill -TERM brave"; + if isDarwin then + ''/usr/bin/osascript -e 'quit app "Brave Browser"' '' + else + "pkill -TERM brave"; relaunch = if isDarwin then ''/usr/bin/open -a "Brave Browser"'' else "brave &"; in