#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' if ! command -v jq >/dev/null 2>&1; then echo "Error: 'jq' is required to merge VS Code settings." >&2 exit 1 fi case "$(uname -s)" in Linux) SETTINGS_DIR="${VSCODE_SETTINGS_DIR:-$HOME/.config/Code/User}" ;; Darwin) SETTINGS_DIR="${VSCODE_SETTINGS_DIR:-$HOME/Library/Application Support/Code/User}" ;; *) echo "Error: unsupported OS '$(uname -s)'. Use settings.ps1 on Windows." >&2 exit 1 ;; esac mkdir -p "$SETTINGS_DIR" SETTINGS_FILE="$SETTINGS_DIR/settings.json" # Update the target of a dotfiles-managed symlink instead of replacing the link. while [[ -L "$SETTINGS_FILE" ]]; do LINK_TARGET="$(readlink "$SETTINGS_FILE")" if [[ "$LINK_TARGET" == /* ]]; then SETTINGS_FILE="$LINK_TARGET" else SETTINGS_FILE="$(dirname "$SETTINGS_FILE")/$LINK_TARGET" fi done SETTINGS_DIR="$(dirname "$SETTINGS_FILE")" mkdir -p "$SETTINGS_DIR" if [[ -f "$SETTINGS_FILE" ]]; then BACKUP_FILE="$SETTINGS_FILE.backup.$(date +%Y%m%d%H%M%S).$$" cp "$SETTINGS_FILE" "$BACKUP_FILE" echo "Backup created: $BACKUP_FILE" else BACKUP_FILE="" fi INPUT_FILE="$SETTINGS_FILE" if [[ ! -f "$INPUT_FILE" ]]; then INPUT_FILE="/dev/null" fi TEMP_FILE="$(mktemp "$SETTINGS_DIR/settings.json.tmp.XXXXXX")" trap 'rm -f "$TEMP_FILE"' EXIT if ! jq -Rs ' def strip_comments: reduce (explode[]) as $c ( {out: [], state: "normal", escaped: false}; if .state == "normal" then if $c == 34 then .out += [$c] | .state = "string" elif $c == 47 then .state = "slash" else .out += [$c] end elif .state == "slash" then if $c == 47 then .state = "line_comment" elif $c == 42 then .state = "block_comment" else .out += [47, $c] | .state = (if $c == 34 then "string" else "normal" end) end elif .state == "line_comment" then if $c == 10 or $c == 13 then .out += [$c] | .state = "normal" else . end elif .state == "block_comment" then if $c == 42 then .state = "block_star" else . end elif .state == "block_star" then if $c == 47 then .state = "normal" elif $c != 42 then .state = "block_comment" else . end else .out += [$c] | if .escaped then .escaped = false elif $c == 92 then .escaped = true elif $c == 34 then .state = "normal" else . end end ) | if .state == "block_comment" or .state == "block_star" then error("unterminated block comment") else (if .state == "slash" then .out + [47] else .out end) end | implode; def strip_trailing_commas: reduce (explode[]) as $c ( {out: [], state: "normal", escaped: false, comma: false, ws: []}; if .state == "string" then .out += [$c] | if .escaped then .escaped = false elif $c == 92 then .escaped = true elif $c == 34 then .state = "normal" else . end elif .comma and ($c == 9 or $c == 10 or $c == 13 or $c == 32) then .ws += [$c] elif .comma and ($c == 93 or $c == 125) then .out += [$c] | .comma = false | .ws = [] else (if .comma then .out += [44] + .ws | .comma = false | .ws = [] else . end) | if $c == 44 then .comma = true else .out += [$c] | if $c == 34 then .state = "string" else . end end end ) | (if .comma then .out + [44] + .ws else .out end) | implode; (if test("^[[:space:]]*$") then "{}" else . end) | strip_comments | strip_trailing_commas | fromjson | if type != "object" then error("settings.json must contain a JSON object") else . end | . + { "editor.fontFamily": "\u0027Ubuntu Mono\u0027, \u0027IBM Plex Mono\u0027, \u0027JetBrains Mono\u0027, Hack, MonoLisa, monospace", "editor.formatOnSave": true, "editor.lineHeight": 24, "editor.renderWhitespace": "all", "editor.rulers": [80, 100, 120], "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "workbench.colorTheme": "GitHub Dark Default", "workbench.iconTheme": "material-icon-theme" } ' "$INPUT_FILE" >"$TEMP_FILE"; then echo "Error: could not parse and merge $SETTINGS_FILE." >&2 [[ -n "$BACKUP_FILE" ]] && echo "Original preserved at: $BACKUP_FILE" >&2 exit 1 fi mv "$TEMP_FILE" "$SETTINGS_FILE" trap - EXIT echo "VS Code settings updated: $SETTINGS_FILE"