diff --git a/.github/README.md b/.github/README.md index 20778e9..a7dbade 100644 --- a/.github/README.md +++ b/.github/README.md @@ -7,10 +7,11 @@ This is a nix flake for the Zen browser. - Linux and MacOS support - Available for _x86_64_ and _aarch64_ - Support for _twilight_ and _beta_ -- Policies can be modified via Home Manager and unwrapped package override +- [Policies can be modified via Home Manager and unwrapped package override](#policies) - Fast & Automatic updates via GitHub Actions - Browser update checks are disabled by default - The default twilight version is reliable and reproducible +- [Declarative \[Work\]Spaces (including themes, icons, containers)](#spaces) ## Installation @@ -123,94 +124,177 @@ experiment with other program options and help with further documentation. } ``` +### Policies + - `policies` (attrsOf anything): You can also modify the **extensions** and **preferences** from here. - **Some common policies:** +#### Some common policies - ```nix - { - programs.zen-browser.policies = { - AutofillAddressEnabled = true; - AutofillCreditCardEnabled = false; - DisableAppUpdate = true; - DisableFeedbackCommands = true; - DisableFirefoxStudies = true; - DisablePocket = true; - DisableTelemetry = true; - DontCheckDefaultBrowser = true; - NoDefaultBookmarks = true; - OfferToSaveLogins = false; - EnableTrackingProtection = { - Value = true; - Locked = true; - Cryptomining = true; - Fingerprinting = true; +```nix +{ + programs.zen-browser.policies = { + AutofillAddressEnabled = true; + AutofillCreditCardEnabled = false; + DisableAppUpdate = true; + DisableFeedbackCommands = true; + DisableFirefoxStudies = true; + DisablePocket = true; + DisableTelemetry = true; + DontCheckDefaultBrowser = true; + NoDefaultBookmarks = true; + OfferToSaveLogins = false; + EnableTrackingProtection = { + Value = true; + Locked = true; + Cryptomining = true; + Fingerprinting = true; + }; + }; +} +``` + +For more policies [read this](https://mozilla.github.io/policy-templates/). + +#### Preferences + +```nix +{ + programs.zen-browser.policies = let + mkLockedAttrs = builtins.mapAttrs (_: value: { + Value = value; + Status = "locked"; + }); + in { + Preferences = mkLockedAttrs { + "browser.tabs.warnOnClose" = false; + # and so on... + }; + }; +} +``` + +##### Zen-specific preferences + +Check [this comment](https://github.com/0xc000022070/zen-browser-flake/issues/59#issuecomment-2964607780). + +#### Extensions + +```nix +{ + programs.zen-browser.policies = let + mkExtensionSettings = builtins.mapAttrs (_: pluginId: { + install_url = "https://addons.mozilla.org/firefox/downloads/latest/${pluginId}/latest.xpi"; + installation_mode = "force_installed"; + }); + in { + ExtensionSettings = mkExtensionSettings { + "wappalyzer@crunchlabz.com" = "wappalyzer"; + "{85860b32-02a8-431a-b2b1-40fbd64c9c69}" = "github-file-icons"; + }; + }; +} +``` + +To setup your own extensions you should: + + 1. [Go to Add-ons for Firefox](https://addons.mozilla.org/en-US/firefox/). + 2. Go to the page of the extension that you want to declare. + 3. Go to "_See all versions_". + 4. Copy the link from any button to "Download file". + 5. Exec **wget** with the output of this command: + + ```bash + echo "" \ + | sed -E 's|https://addons.mozilla.org/firefox/downloads/file/[0-9]+/([^/]+)-[^/]+\.xpi|\1|' \ + | tr '_' '-' \ + | awk '{print "https://addons.mozilla.org/firefox/downloads/latest/" $1 "/latest.xpi"}' + ``` + + 6. Run `unzip -*.xpi -d my-extension && cd my-extension`. + 7. Run `cat manifest.json | jq -r '.browser_specific_settings.gecko.id'` and use the result + for the _entry key_. + 8. Don't forget to add the `install_url` and set `installation_mode` to `force_installed`. + +### Spaces + +> [!WARNING] +> Spaces declaration may change your rebuild experience with Home Manager. Due to limitations +> on how Zen handles spaces, the updating of them is done via a activation script on your +> `home-manager-.service`. This may cause the service to fail, to prevent this, +> it is recommended to close your Zen browser instance before rebuilding. + +- `profiles.*.spaces` (attrsOf submodule): Declare profile's \[work\]spaces. + - `name` (string) Name of space, defaults to submodule/attribute name. + - `id` (string) **Required.** UUID v4 of space. **Changing this after a rebuild will re-create the space as + a new one,** losing opened tabs, groups, etc. If `spacesForce` is true, the space with the previous UUID will be deleted. + - `position` (unsigned integer) Position/order of space in the left bar. + - `icon` (null or (string or path)) Emoji, URI or file path for icon to be used as space icon. + - `container` (null or unsigned integer) Container ID to be used as default in space. + - `theme.type` (nullOr string) Type of theme, defaults to "gradient". + - `theme.color` (listOf submodule) List of JSON colors to be used as theme: + - `red` (integer between 0 and 255) Red value of color (first value of "c" array in JSON object). + - `green` (integer between 0 and 255) Green value of color (second value of "c" array in JSON object). + - `blue` (integer between 0 and 255) Blue value of color (third value of "c" array in JSON object). + - `custom` (boolean) Is custom color ("isCustom" in JSON object). + - `algorithm` (enum of "complementary", "floating" or "analogous") color algorithm (defaults to "floating"). + - `lightness` (integer) Lightness of color. + - `position.x` (integer) X Position of color in gradient picker on Zen browser. + - `position.y` (integer) Y Position of color in gradient picker on Zen browser. + - `type` (enum of "undefined" or "explicit-lightness") Type of color (default to "undefined"). + - `theme.opacity` (null or float) Opacity of theme (defaults to 0.5). + - `theme.rotation` (null or integer) Rotation of theme gradient (defaults to null). + - `theme.texture` (null or float) Amount of texture of theme (defaults to 0.0). +- `profiles.*.spacesForce` (boolean) Whether to delete existing spaces not declared in the configuration. + Recommended to make spaces fully declarative (defaults to false). + +```nix +{ + programs.zen-browser = { + enable = true; + profiles."default" = { + containersForce = true; + containers = { + Personal = { + color = "purple"; + icon = "fingerprint"; + id = 1; + }; + Work = { + color = "blue"; + icon = "briefcase"; + id = 2; + }; + Shopping = { + color = "yellow"; + icon = "dollarsign"; + id = 3; + }; + }; + spacesForce = true; + spaces = let + containers = config.programs.zen-browser.profiles."default".containers; + in { + "Space" = { + id = "c6de089c-410d-4206-961d-ab11f988d40a"; + position = 1000; + }; + "Work" = { + id = "cdd10fab-4fc5-494b-9041-325e5759195b"; + icon = "chrome://browser/skin/zen-icons/selectable/star-2.svg"; + container = containers."Work".id; + position = 2000; + }; + "Shopping" = { + id = "78aabdad-8aae-4fe0-8ff0-2a0c6c4ccc24"; + icon = "💸"; + container = containers."Shopping".id; + position = 3000; + }; }; }; - } - ``` - - For more policies [read this](https://mozilla.github.io/policy-templates/). - - **Preferences:** - - ```nix - { - programs.zen-browser.policies = let - mkLockedAttrs = builtins.mapAttrs (_: value: { - Value = value; - Status = "locked"; - }); - in { - Preferences = mkLockedAttrs { - "browser.tabs.warnOnClose" = false; - # and so on... - }; - }; - } - ``` - - **Zen-specific preferences:** - - Check [this comment](https://github.com/0xc000022070/zen-browser-flake/issues/59#issuecomment-2964607780). - - **Extensions:** - - ```nix - { - programs.zen-browser.policies = let - mkExtensionSettings = builtins.mapAttrs (_: pluginId: { - install_url = "https://addons.mozilla.org/firefox/downloads/latest/${pluginId}/latest.xpi"; - installation_mode = "force_installed"; - }); - in { - ExtensionSettings = mkExtensionSettings { - "wappalyzer@crunchlabz.com" = "wappalyzer"; - "{85860b32-02a8-431a-b2b1-40fbd64c9c69}" = "github-file-icons"; - }; - }; - } - ``` - - To setup your own extensions you should: - - 1. [Go to Add-ons for Firefox](https://addons.mozilla.org/en-US/firefox/). - 2. Go to the page of the extension that you want to declare. - 3. Go to "_See all versions_". - 4. Copy the link from any button to "Download file". - 5. Exec **wget** with the output of this command: - - ```bash - echo "" \ - | sed -E 's|https://addons.mozilla.org/firefox/downloads/file/[0-9]+/([^/]+)-[^/]+\.xpi|\1|' \ - | tr '_' '-' \ - | awk '{print "https://addons.mozilla.org/firefox/downloads/latest/" $1 "/latest.xpi"}' - ``` - - 6. Run `unzip -*.xpi -d my-extension && cd my-extension`. - 7. Run `cat manifest.json | jq -r '.browser_specific_settings.gecko.id'` and use the result - for the _entry key_. - 8. Don't forget to add the `install_url` and set `installation_mode` to `force_installed`. + }; +} +``` ## 1Password diff --git a/hm-module.nix b/hm-module.nix index e988079..12da9ee 100644 --- a/hm-module.nix +++ b/hm-module.nix @@ -29,11 +29,13 @@ linuxConfigPath = ".zen"; darwinConfigPath = "Library/Application Support/Zen"; - configPath = "${config.home.homeDirectory}/${( - if pkgs.stdenv.isDarwin - then darwinConfigPath - else linuxConfigPath - )}"; + configPath = "${ + ( + if pkgs.stdenv.isDarwin + then darwinConfigPath + else linuxConfigPath + ) + }"; mkFirefoxModule = import "${home-manager.outPath}/modules/programs/firefox/mkFirefoxModule.nix"; in { @@ -59,113 +61,134 @@ in { options = setAttrByPath modulePath { profiles = mkOption { type = with types; - attrsOf (submodule ({...}: { - options = { - spacesForce = mkOption { - type = bool; - description = "Whether delete existing spaces not declared in the configuration."; - default = false; - }; - spaces = mkOption { - type = attrsOf (submodule ({name, ...}: { - options = { - name = mkOption { - type = str; - description = "Name of the space."; - default = name; - }; - id = mkOption { - type = str; - description = "REQUIRED. Unique Version 4 UUID for space."; - }; - position = mkOption { - type = ints.unsigned; - description = "Position of space in the left bar."; - default = 1000; - }; - icon = mkOption { - type = nullOr (either str path); - description = "Emoji or icon URI to be used as space icon."; - apply = v: - if isPath v - then "file://${v}" - else v; - default = null; - }; - container = mkOption { - type = nullOr ints.unsigned; - description = "Container ID to be used in space"; - default = null; - }; - theme.type = mkOption { - type = nullOr str; - default = "gradient"; - }; - theme.colors = mkOption { - type = nullOr (listOf (submodule ({...}: { - options = { - red = mkOption { - type = int; - default = 0; - }; - green = mkOption { - type = int; - default = 0; - }; - blue = mkOption { - type = int; - default = 0; - }; - custom = mkOption { - type = bool; - default = false; - }; - algorithm = mkOption { - type = enum ["complementary" "floating" "analogous"]; - default = "floating"; - }; - primary = mkOption { - type = bool; - default = true; - }; - lightness = mkOption { - type = int; - default = 0; - }; - position.x = mkOption { - type = int; - default = 0; - }; - position.y = mkOption { - type = int; - default = 0; - }; - type = mkOption { - type = enum ["undefined" "explicit-lightness"]; - default = "undefined"; - }; - }; - }))); - default = []; - }; - theme.opacity = mkOption { - type = nullOr float; - default = 0.5; - }; - theme.rotation = mkOption { - type = nullOr int; - default = null; - }; - theme.texture = mkOption { - type = nullOr float; - default = 0.0; - }; + attrsOf ( + submodule ( + {...}: { + options = { + spacesForce = mkOption { + type = bool; + description = "Whether to delete existing spaces not declared in the configuration."; + default = false; }; - })); - default = {}; - }; - }; - })); + spaces = mkOption { + type = attrsOf ( + submodule ( + {name, ...}: { + options = { + name = mkOption { + type = str; + description = "Name of the space."; + default = name; + }; + id = mkOption { + type = str; + description = "REQUIRED. Unique Version 4 UUID for space."; + }; + position = mkOption { + type = ints.unsigned; + description = "Position of space in the left bar."; + default = 1000; + }; + icon = mkOption { + type = nullOr (either str path); + description = "Emoji or icon URI to be used as space icon."; + apply = v: + if isPath v + then "file://${v}" + else v; + default = null; + }; + container = mkOption { + type = nullOr ints.unsigned; + description = "Container ID to be used in space"; + default = null; + }; + theme.type = mkOption { + type = nullOr str; + default = "gradient"; + }; + theme.colors = mkOption { + type = nullOr ( + listOf ( + submodule ( + {...}: { + options = { + red = mkOption { + type = ints.between 0 255; + default = 0; + }; + green = mkOption { + type = ints.between 0 255; + default = 0; + }; + blue = mkOption { + type = ints.between 0 255; + default = 0; + }; + custom = mkOption { + type = bool; + default = false; + }; + algorithm = mkOption { + type = enum [ + "complementary" + "floating" + "analogous" + ]; + default = "floating"; + }; + primary = mkOption { + type = bool; + default = true; + }; + lightness = mkOption { + type = int; + default = 0; + }; + position.x = mkOption { + type = int; + default = 0; + }; + position.y = mkOption { + type = int; + default = 0; + }; + type = mkOption { + type = enum [ + "undefined" + "explicit-lightness" + ]; + default = "undefined"; + }; + }; + } + ) + ) + ); + default = []; + }; + theme.opacity = mkOption { + type = nullOr float; + default = 0.5; + }; + theme.rotation = mkOption { + type = nullOr int; + default = null; + }; + theme.texture = mkOption { + type = nullOr float; + default = 0.0; + }; + }; + } + ) + ); + default = {}; + }; + }; + } + ) + ); }; }; @@ -187,140 +210,187 @@ in { }; }; - systemd.user.services."zen-browser-spaces-activation" = let + home.file = let + inherit + (builtins) + isNull + toJSON + toString + ; inherit (lib) - attrByPath - concatMapAttrsStringSep - concatMapStringsSep concatStringsSep - elemAt + concatMapStringsSep + concatMapAttrsStringSep filterAttrs getExe getExe' - isStringLike - lists + mapAttrs' mapAttrsToList + nameValuePair optionalString pipe ; + in (mapAttrs' (profileName: profile: let + sqlite3 = getExe' pkgs.sqlite "sqlite3"; + scriptFile = "${configPath}/${profileName}/places_update.sh"; + placesFile = "${config.home.homeDirectory}/${configPath}/${profileName}/places.sqlite"; - hasSpaces = pipe cfg.profiles [ - (mapAttrsToList (n: v: v.spaces != {})) - (lists.any (v: v)) - ]; + insertSpaces = '' + # Reference: https://github.com/zen-browser/desktop/blob/4e2dfd8a138fd28767bb4799a3ca9d8aab80430e/src/zen/workspaces/ZenWorkspacesStorage.mjs#L25-L55 + ${sqlite3} "${placesFile}" "${ + concatStringsSep " " [ + "CREATE TABLE IF NOT EXISTS zen_workspaces (" + "id INTEGER PRIMARY KEY," + "uuid TEXT UNIQUE NOT NULL," + "name TEXT NOT NULL," + "icon TEXT," + "container_id INTEGER," + "position INTEGER NOT NULL DEFAULT 0," + "created_at INTEGER NOT NULL," + "updated_at INTEGER NOT NULL" + ");" + ] + }" || exit 1 - # Reference: https://github.com/zen-browser/desktop/blob/4e2dfd8a138fd28767bb4799a3ca9d8aab80430e/src/zen/workspaces/ZenWorkspacesStorage.mjs#L25-L55 - initSpacesTable = pkgs.writeText "init.sql" '' - CREATE TABLE IF NOT EXISTS zen_workspaces ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - name TEXT NOT NULL, - icon TEXT, - container_id INTEGER, - position INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, + columns=($(${sqlite3} "${placesFile}" "SELECT name FROM pragma_table_info('zen_workspaces');")) + if [[ ! "''${columns[@]}" =~ "theme_type" ]]; then + ${sqlite3} "${placesFile}" "ALTER TABLE zen_workspaces ADD COLUMN theme_type TEXT;" || exit 1 + fi + if [[ ! "''${columns[@]}" =~ "theme_colors" ]]; then + ${sqlite3} "${placesFile}" "ALTER TABLE zen_workspaces ADD COLUMN theme_colors TEXT;" || exit 1 + fi + if [[ ! "''${columns[@]}" =~ "theme_opacity" ]]; then + ${sqlite3} "${placesFile}" "ALTER TABLE zen_workspaces ADD COLUMN theme_opacity REAL;" || exit 1 + fi + if [[ ! "''${columns[@]}" =~ "theme_rotation" ]]; then + ${sqlite3} "${placesFile}" "ALTER TABLE zen_workspaces ADD COLUMN theme_rotation INTEGER;" || exit 1 + fi + if [[ ! "''${columns[@]}" =~ "theme_texture" ]]; then + ${sqlite3} "${placesFile}" "ALTER TABLE zen_workspaces ADD COLUMN theme_texture REAL;" || exit 1 + fi - theme_type TEXT, - theme_colors TEXT, - theme_opacity REALj - theme_rotation INTEGER, - theme_texture REAL - ) - ''; + # Reference: https://github.com/zen-browser/desktop/blob/4e2dfd8a138fd28767bb4799a3ca9d8aab80430e/src/zen/workspaces/ZenWorkspacesStorage.mjs#L141-L149 + ${sqlite3} "${placesFile}" "${ + (concatStringsSep " " [ + "INSERT OR REPLACE INTO zen_workspaces (" + "uuid," + "name," + "icon," + "container_id," + "position," - # Source: https://github.com/zen-browser/desktop/blob/4e2dfd8a138fd28767bb4799a3ca9d8aab80430e/src/zen/workspaces/ZenWorkspacesStorage.mjs#L141-L149 - updateSpacesTable = spaces: - pkgs.writeText "insert.sql" '' - INSERT OR REPLACE INTO zen_workspaces ( - uuid, - name, - icon, - container_id, - "position", + "theme_type," + "theme_colors," + "theme_opacity," + "theme_rotation," + "theme_texture," - theme_type, - theme_colors, - theme_opacity, - theme_rotation, - theme_texture, - - created_at, - updated_at - ) VALUES ${pipe spaces [ - (mapAttrsToList (_: space: [ - "{${space.id}}" - space.name - (attrByPath ["icon"] null space) - (attrByPath ["container"] null space) - (attrByPath ["position"] 0 space) - (attrByPath ["theme" "type"] "gradient" space) - (map (color: { - inherit (color) algorithm lightness position type; - c = [color.red color.green color.blue]; - isCustom = color.custom; - isPrimary = color.primary; - }) (attrByPath ["theme" "colors"] [] space)) - (attrByPath ["theme" "opacity"] 0.5 space) - (attrByPath ["theme" "rotation"] null space) - (attrByPath ["theme" "texture"] 0.0 space) - ])) - (map (row: - map ( - v: - with builtins; - if isStringLike v - then "'${v}'" - else if (isList v) || (isAttrs v) - then "'${toJSON v}'" - else if isNull v - then "NULL" - else toString v + "created_at," + "updated_at" + ") VALUES " + ]) + + (pipe profile.spaces [ + (mapAttrsToList (_: s: [ + "'{${s.id}}'" + "'${s.name}'" + ( + if isNull s.icon + then "NULL" + else "'${s.icon}'" ) - row)) - (map (row: - row - ++ [ - "COALESCE((SELECT created_at FROM zen_workspaces WHERE uuid = ${elemAt row 0}), strftime('%s', 'now'))" - "strftime('%s', 'now')" - ])) + ( + if isNull s.container + then "NULL" + else toString s.container + ) + (toString s.position) + ( + if isNull s.theme.type + then "NULL" + else "'${s.theme.type}'" + ) + ( + if isNull s.theme.colors + then "NULL" + else "'${toJSON (map (c: { + inherit (c) algorithm lightness position type; + c = [c.red c.green c.blue]; + isCustom = c.custom; + isPrimary = c.primary; + }) + s.theme.colors)}'" + ) + ( + if isNull s.theme.opacity + then "NULL" + else toString s.theme.opacity + ) + ( + if isNull s.theme.rotation + then "NULL" + else toString s.theme.rotation + ) + ( + if isNull s.theme.texture + then "NULL" + else toString s.theme.texture + ) + "COALESCE((SELECT created_at FROM zen_workspaces WHERE uuid = '{${s.id}}'), strftime('%s', 'now'))" + "strftime('%s', 'now')" + ])) (map (row: concatStringsSep "," row)) (concatMapStringsSep "," (row: "(${row})")) - ]} - ''; + ]) + }" || exit 1 + ''; - filterSpacesTable = spaces: - pkgs.writeText "filter.sql" '' - DELETE FROM zen_workspaces ${ - if spaces != {} - then "WHERE " - else "" - }${ - concatMapAttrsStringSep " AND " (n: v: "NOT uuid = '{${v.id}}'") spaces - } - ''; + deleteSpaces = '' + ${sqlite3} "${placesFile}" "DELETE FROM zen_workspaces ${ + if profile.spaces != {} + then "WHERE " + else "" + }${concatMapAttrsStringSep " AND " (_: s: "NOT uuid = '{${s.id}}'") profile.spaces}" || exit 1 + ''; in - mkIf hasSpaces { - Install.WantedBy = ["default.target"]; - Unit.After = ["home-manager-${config.home.username}.service"]; - Service = { - Type = "oneshot"; - ExecStart = let - sqlite3 = getExe' pkgs.sqlite "sqlite3"; - in - pipe cfg.profiles [ - (filterAttrs (_: v: v.spaces != {})) - (mapAttrsToList (n: v: (pkgs.writeShellScriptBin "zen-browser-spaces-${n}" '' - mkdir -p "${configPath}/${n}" - ${sqlite3} "${configPath}/${n}/places.sqlite" ".read ${initSpacesTable}" - ${sqlite3} "${configPath}/${n}/places.sqlite" ".read ${updateSpacesTable v.spaces}" - ${optionalString v.spacesForce - ''${sqlite3} "${configPath}/${n}/places.sqlite" ".read ${filterSpacesTable v.spaces}"''} - ''))) - (list: map (pkg: getExe pkg) list) - ]; - }; - }; + nameValuePair scriptFile { + source = getExe (pkgs.writeShellScriptBin "places_update_${profileName}" '' + # This file is generated by Zen browser Home Manager module, please to not change it since it + # will be overridden and executed on every rebuild of the home environment. + + function update_spaces() { + ${optionalString (profile.spaces != {}) insertSpaces} + ${optionalString (profile.spacesForce) deleteSpaces} + } + + error="$(update_spaces 2>&1 1>/dev/null)" + if [[ "$?" -ne 0 ]]; then + if [[ "$error" == *"database is locked"* ]]; then + echo "$error" + + YELLOW="\033[1;33m" + NC="\033[0m" + echo -e "zen-update-places:''${YELLOW} Atempted to update the \"zen_workspaces\" table with values declared in \"programs.zen.profiles.\"${profileName}\".spaces\".''${NC}" + echo -e "zen-update-places:''${YELLOW} Failed to update \"${placesFile}\" due to a Zen browser instance for profile \"${profileName}\" being opened, please close''${NC}" + echo -e "zen-update-places:''${YELLOW} Zen browser and rebuild the home environment to rerun \"home-manager-${config.home.username}.service\" and update places.sqlite.''${NC}" + else + echo "$error" + fi + exit 1 + else + exit 0 + fi + ''); + onChange = '' + ${config.home.homeDirectory}/${scriptFile} + if [[ "$?" -ne 0 ]]; then + RED="\033[0;31m" + NC="\033[0m" + echo -e "zen-update-places:''${RED} Failed to update places.sqlite file for Zen browser \"${profileName}\" profile.''${NC}" + fi + ''; + executable = true; + force = true; + }) (filterAttrs (_: profile: profile.spaces != {} || profile.spacesForce) cfg.profiles)); }; }