main
  1{lib, ...}: {
  2  flake.modules.homeManager.dev-vscode = {
  3    config,
  4    osConfig,
  5    pkgs,
  6    ...
  7  }: let
  8    bask-pkg = pkgs.vscode;
  9
 10    chromiumCLA = lib.optionals isLinux ([
 11        "--ozone-platform-hint=auto"
 12        "--enable-wayland-ime"
 13        "--wayland-text-input-version=3"
 14      ]
 15      ++ lib.optionals (!isPlasma) [
 16        # Fix https://github.com/microsoft/vscode/issues/187338
 17        "--password-store=gnome-libsecret"
 18      ]);
 19
 20    isLinux = pkgs.stdenv.hostPlatform.isLinux;
 21    isPlasma = isLinux && (osConfig.services.desktopManager.plasma6.enable or false);
 22
 23    pkg = bask-pkg.override {
 24      commandLineArgs =
 25        (lib.lists.concatLists [
 26          ["--extensions-dir" extensionsDir]
 27          ["--user-data-dir" userDataDir]
 28          ["--locale" "zh-cn"]
 29        ])
 30        ++ chromiumCLA;
 31    };
 32
 33    # Below VSCode paths are copied from home-manager's vscode module source
 34    vscodePname = bask-pkg.pname;
 35    vscodeConfigDir =
 36      {
 37        "vscode" = "Code";
 38        "vscode-insiders" = "Code - Insiders";
 39        "vscodium" = "VSCodium";
 40        "openvscode-server" = "OpenVSCode Server";
 41        "windsurf" = "Windsurf";
 42        "cursor" = "Cursor";
 43      }.${
 44        vscodePname
 45      };
 46    vscodeExtensionDir =
 47      {
 48        "vscode" = "vscode";
 49        "vscode-insiders" = "vscode-insiders";
 50        "vscodium" = "vscode-oss";
 51        "openvscode-server" = "openvscode-server";
 52        "windsurf" = "windsurf";
 53        "cursor" = "cursor";
 54      }.${
 55        vscodePname
 56      };
 57    vscodeUserDir =
 58      if pkgs.stdenv.hostPlatform.isDarwin
 59      then "${config.home.homeDirectory}/Library/Application Support/${vscodeConfigDir}/User"
 60      else "${config.xdg.configHome}/${vscodeConfigDir}/User";
 61
 62    homeDir = config.home.homeDirectory;
 63    dataDir = config.xdg.dataHome;
 64    userDataDir = "${dataDir}/${vscodePname}/data";
 65    extensionsDir = "${dataDir}/${vscodePname}/extensions";
 66
 67    vscodeSetupScript = pkgs.writeShellApplication {
 68      name = "vscode-setup";
 69      runtimeInputs = with pkgs; [coreutils gnutar jq];
 70      text = let
 71        userSrc = vscodeUserDir;
 72        userDst = "${userDataDir}/User";
 73        extSrc = ".${vscodeExtensionDir}/extensions";
 74        extDst = extensionsDir;
 75
 76        dirsToPreserve = [
 77          "workspaceStorage"
 78          "History"
 79        ];
 80        backupCmds = builtins.concatStringsSep "\n" (map (dir: ''
 81            if [ -e "${userDst}/${dir}" ]; then
 82              echo "Backing up data/User/${dir}..."
 83              mv "${userDst}/${dir}" "/tmp/vscode-${dir}-$$"
 84            fi
 85          '')
 86          (dirsToPreserve ++ ["globalStorage"]));
 87        restoreCmds = builtins.concatStringsSep "\n" (map (dir: ''
 88            if [ -e "/tmp/vscode-${dir}-$$" ]; then
 89              echo "Restoring data/User/${dir}..."
 90              mv "/tmp/vscode-${dir}-$$" "${userDst}/${dir}"
 91            fi
 92          '')
 93          dirsToPreserve);
 94      in ''
 95        set -eu
 96
 97        ${backupCmds}
 98
 99        echo "Cleaning old directories..."
100        rm -rf "${userDst}"
101        rm -rf "${extDst}"
102
103        mkdir -p "${userDataDir}"
104        mkdir -p "${extDst}"
105
106        echo "Copying user settings from ${userSrc}..."
107        cp -r --dereference --no-preserve=mode,ownership ${userSrc} "${userDst}"
108
109        echo "Copying extensions from ${extSrc}..."
110        tar -h -C "${extSrc}" -cf - . | tar -C "${extDst}" -xf - --no-same-owner --no-same-permissions --mode='u=rX,go=rX'
111        chmod u+w -R "${extDst}" 2>/dev/null || true
112
113        ${restoreCmds}
114
115        echo "Restoring and merging data/User/globalStorage..."
116        if [ -e "/tmp/vscode-globalStorage-$$" ]; then
117          cp -rT "/tmp/vscode-globalStorage-$$" "${userDst}/globalStorage"
118
119          src_storage_json="${userSrc}/globalStorage/storage.json"
120          dst_storage_json="${userDst}/globalStorage/storage.json"
121
122          if [ -f "$src_storage_json" ] && [ -f "$dst_storage_json" ]; then
123            echo "Merging data/globalStorage/storage.json with new data..."
124            merged_json=$(mktemp)
125            jq -s '.[0] * .[1]' "$dst_storage_json" "$src_storage_json" > "$merged_json"
126            mv "$merged_json" "$dst_storage_json"
127            echo "Merge complete."
128          else
129            echo "Skipping storage.json merge: one or both files do not exist."
130          fi
131        else
132          echo "No backed-up globalStorage found to restore."
133        fi
134
135        echo "VSCode setup complete."
136      '';
137    };
138  in {
139    programs.vscode = {
140      enable = true;
141      package = pkg;
142      mutableExtensionsDir = false;
143    };
144
145    systemd.user.services.vscode-setup = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
146      Unit = {
147        Description = "VSCode Setup service";
148        After = ["graphical-session-pre.target"];
149        Wants = ["graphical-session-pre.target"];
150      };
151      Install.WantedBy = ["graphical-session.target"];
152      Service = {
153        Type = "oneshot";
154        UMask = "0022";
155        ExecStart = lib.getExe vscodeSetupScript;
156      };
157    };
158
159    # TODO: Test on real macOS system.
160    # For now, it just a simple mirror of the Linux service.
161    launchd.agents.vscode-setup = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
162      enable = true;
163      config = {
164        Program = [lib.getExe vscodeSetupScript];
165        RunAtLoad = true;
166        StandardOutPath = "${homeDir}/Library/Logs/vscode-setup.log";
167        StandardErrorPath = "${homeDir}/Library/Logs/vscode-setup.log";
168        WorkingDirectory = homeDir;
169      };
170    };
171  };
172}