Created
August 4, 2026 00:20
-
-
Save cmhobbs/4831054d64020941cbfbacf5f48c1dbf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # new-vivaldi-profile.sh <client-name> | |
| # Creates a new Vivaldi profile from the profile named "Template". | |
| # Run with Vivaldi closed. | |
| set -euo pipefail | |
| CLIENT_NAME="${1:-}" | |
| if [[ -z "$CLIENT_NAME" ]]; then | |
| echo "Usage: $0 \"Client Name\"" | |
| exit 1 | |
| fi | |
| VIVALDI_DIR="$HOME/.var/app/com.vivaldi.Vivaldi/config/vivaldi" | |
| LOCAL_STATE="$VIVALDI_DIR/Local State" | |
| if [[ ! -f "$LOCAL_STATE" ]]; then | |
| echo "ERROR: Vivaldi config not found. Is Vivaldi installed as a Flatpak?" | |
| exit 1 | |
| fi | |
| # Vivaldi keeps Local State in memory and will overwrite our edits on its | |
| # next flush. Refuse to run if it's up. | |
| if flatpak ps --columns=application 2>/dev/null | grep -qx 'com.vivaldi.Vivaldi'; then | |
| echo "ERROR: Vivaldi is running. Close it (check for background instances) and re-run." | |
| exit 1 | |
| fi | |
| # Find Template profile directory name | |
| TEMPLATE_DIR=$(CLIENT_NAME="$CLIENT_NAME" LOCAL_STATE="$LOCAL_STATE" python3 -c " | |
| import json, os, sys | |
| with open(os.environ['LOCAL_STATE']) as f: | |
| state = json.load(f) | |
| for dir_name, info in state.get('profile', {}).get('info_cache', {}).items(): | |
| if info.get('name', '').lower() == 'template': | |
| print(dir_name) | |
| sys.exit(0) | |
| ") | |
| if [[ -z "$TEMPLATE_DIR" ]]; then | |
| echo "ERROR: No profile named 'Template' found. Create one in Vivaldi first." | |
| exit 1 | |
| fi | |
| TEMPLATE_PATH="$VIVALDI_DIR/$TEMPLATE_DIR" | |
| if [[ ! -d "$TEMPLATE_PATH" ]]; then | |
| echo "ERROR: Template directory not found: $TEMPLATE_PATH" | |
| exit 1 | |
| fi | |
| # Find next available Profile N | |
| N=1 | |
| while [[ -d "$VIVALDI_DIR/Profile $N" ]]; do | |
| N=$((N + 1)) | |
| done | |
| NEW_DIR="Profile $N" | |
| NEW_PATH="$VIVALDI_DIR/$NEW_DIR" | |
| echo "Copying template to $NEW_DIR..." | |
| cp -r "$TEMPLATE_PATH" "$NEW_PATH" | |
| echo "Clearing session and account data..." | |
| # Individual files to remove | |
| for f in \ | |
| "Cookies" "Cookies-journal" \ | |
| "Login Data" "Login Data-journal" \ | |
| "Login Data For Account" "Login Data For Account-journal" \ | |
| "Account Web Data" "Account Web Data-journal" \ | |
| "Affiliation Database" "Affiliation Database-journal" \ | |
| "History" "History-journal" \ | |
| "Web Data" "Web Data-journal" \ | |
| "Favicons" "Favicons-journal" \ | |
| "Top Sites" "Top Sites-journal" \ | |
| "Shortcuts" "Shortcuts-journal" \ | |
| "Network Action Predictor" "Network Action Predictor-journal" \ | |
| "Safe Browsing Cookies" "Safe Browsing Cookies-journal" \ | |
| "Trust Tokens" "Trust Tokens-journal" \ | |
| "heavy_ad_intervention_opt_out.db" "heavy_ad_intervention_opt_out.db-journal" \ | |
| "SCT Auditing Pending Reports" \ | |
| "BookmarkMergedSurfaceOrdering" \ | |
| "trusted_vault.pb" "DIPS" \ | |
| "LOCK" "LOG"; do | |
| rm -f "$NEW_PATH/$f" | |
| done | |
| # Directories to remove | |
| for d in \ | |
| "Sessions" "Session Storage" \ | |
| "GPUCache" "DawnGraphiteCache" "DawnWebGPUCache" \ | |
| "blob_storage" "Service Worker" \ | |
| "Storage" "Local Storage" "WebStorage" \ | |
| "Sync Data" "SyncedFiles" "Sync Extension Settings" \ | |
| "VivaldiThumbnails" "VivaldiDirectMatchIcons" \ | |
| "shared_proto_db" "SharedStorage" "Shared Dictionary" \ | |
| "Site Characteristics Database" "Feature Engagement Tracker" \ | |
| "GCM Store" "discount_infos_db" "discounts_db" \ | |
| "parcel_tracking_db" "commerce_subscription_db" \ | |
| "AutofillStrikeDatabase" "BudgetDatabase" \ | |
| "VideoDecodeStats" "chrome_cart_db"; do | |
| rm -rf "$NEW_PATH/$d" | |
| done | |
| echo "Backing up Local State..." | |
| BACKUP="$LOCAL_STATE.bak.$(date +%Y%m%d-%H%M%S)" | |
| cp -p "$LOCAL_STATE" "$BACKUP" | |
| echo "Registering profile..." | |
| CLIENT_NAME="$CLIENT_NAME" TEMPLATE_DIR="$TEMPLATE_DIR" NEW_DIR="$NEW_DIR" LOCAL_STATE="$LOCAL_STATE" python3 -c " | |
| import json, copy, os, tempfile | |
| local_state = os.environ['LOCAL_STATE'] | |
| template_dir = os.environ['TEMPLATE_DIR'] | |
| new_dir = os.environ['NEW_DIR'] | |
| client_name = os.environ['CLIENT_NAME'] | |
| with open(local_state) as f: | |
| state = json.load(f) | |
| cache = state['profile']['info_cache'] | |
| new_info = copy.deepcopy(cache.get(template_dir, {})) | |
| new_info['name'] = client_name | |
| new_info['is_using_default_name'] = False | |
| new_info['gaia_name'] = '' | |
| new_info['gaia_given_name'] = '' | |
| new_info['gaia_id'] = '' | |
| new_info['user_name'] = '' | |
| new_info['managed_user_id'] = '' | |
| new_info['is_consented_primary_account'] = False | |
| state['profile']['info_cache'][new_dir] = new_info | |
| order = state.get('profile', {}).get('profiles_order') | |
| if order is not None and new_dir not in order: | |
| order.append(new_dir) | |
| # Write to a sibling tempfile and atomically rename so a partial write | |
| # (power loss, full disk, kill) can never leave Local State truncated. | |
| fd, tmp = tempfile.mkstemp( | |
| dir=os.path.dirname(local_state), | |
| prefix='Local State.', | |
| suffix='.tmp', | |
| ) | |
| try: | |
| with os.fdopen(fd, 'w') as f: | |
| json.dump(state, f, indent=3) | |
| f.flush() | |
| os.fsync(f.fileno()) | |
| os.replace(tmp, local_state) | |
| except Exception: | |
| try: | |
| os.unlink(tmp) | |
| except FileNotFoundError: | |
| pass | |
| raise | |
| " | |
| echo "Backup saved to: $BACKUP" | |
| echo "" | |
| echo "Done. Profile '$CLIENT_NAME' created as $NEW_DIR." | |
| echo "Launch Vivaldi and select it from the profile switcher." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment