Edit (2023-003-12)

This is now a feature that is built into VS Code: https://code.visualstudio.com/docs/editor/profiles


Recently I decided I wanted to try and split up my Visual Studio Code environment between how I want to write code for work and how I want to write code for pleasure. Examples of why this is useful are:

  1. I can enable features that wouldn’t be safe for work (like Github Copilot)
  2. I can have my work configuration synced to a different location than my personal settings
  3. I can experiment and be willing to break my personal code environment without impairing my work environment

What’s the game plan?

There were two options I considered for solving this problem:

  1. Using the VSCode Profile Switcher extension
  2. Using the --extensions-dir and --user-data-dir options on the code binary

Profile switcher extension

I tried option 1 first and found it a bit awkward. The Github user that I was logged in with was common between the profiles which meant I couldn’t have different things synced to different places. I also couldn’t easily read the settings file since everything was just kinda squished together so making manual changes was hard (but necessary). Finally I found it really annoying that I had to reload all the extensions every time I switched profiles. I will say though that I liked how you could inherit settings from other profiles. That made things more convenient to come up with defaults and then re-use them elsewhere.

CLI options

After running into problems with the extension, I decided to go with just changing where the settings and extensions were loaded from. That ended up being pretty easy.

I simply picked a place I wanted to store the profiles (~/.local/share/code) and then I created a directory structure like this ~/.local/share/code/<profile_name>/extensions and ~/.local/share/code/<profile_name>/data. Given that I created a Bash alias which launches VS Code with those as the desired storage locations.

alias code@work="code --extensions-dir '${HOME}/.local/share/code/work/extensions' --user-data-dir '${HOME}/.local/share/code/work/data'"

Once that’s done you can just execute the code@work command to launch VS Code with the work profile.

Main downside that I’ve found with this approach is that when you’re doing something like connecting it up to Github which requires the Oauth flow you can’t just click the URL they spit out to complete the login because that’ll just launch the main VS Code environment which isn’t what I want. I only want it to go to the current profile. You can copy the URL they provide and enter it in manually which isn’t too bad. Just something to be aware of.

Application launcher entry

It’s nice to be able to just click an icon to launch VS Code versus having to run a terminal command. To keep this experience I was able to just create a .desktop file for each profile and include that in my users applications list.

This was incredibly easy. The work entry looks like this:

[Desktop Entry]
Name=code@work
Comment=Work code
GenericName=Text Editor
Exec=/usr/share/code/code --extensions-dir "/home/<username>/.local/share/code/work/extensions" --user-data-dir "/home/<username>/.local/share/code/work/data" --unity-launch %F
Icon=com.visualstudio.code
Type=Application
StartupNotify=false
StartupWMClass=Code
Categories=Utility;TextEditor;Development;IDE;
MimeType=text/plain;inode/directory;application/x-code-workspace;
Actions=new-empty-window;
Keywords=vscode;

X-Desktop-File-Install-Version=0.24

[Desktop Action new-empty-window]
Name=New Empty Window
Exec=/usr/share/code/code --extensions-dir "/home/<username>/.local/share/code/work/extensions" --user-data-dir "/home/<username>/.local/share/code/work/data" --new-window %F
Icon=com.visualstudio.code

How can it be automated?

While none of that is hard to do, I’m a developer, I have to make things complicated and spend a ton of time automating something I do infrequently. So here’s a quick Python script that I wrote to handle doing all of that.

#!/usr/bin/env python3

import argparse
import pathlib
import shutil
import sys


PROFILE_DIR = pathlib.Path.home() / ".local" / "share" / "code"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="VSCode profile management")
    subcommands = parser.add_subparsers(help="Subcommands", required=True)

    create_parser = subcommands.add_parser("create", help="Used to create VSCode profiles")
    create_parser.add_argument("--name", type=str, required=True, help="Name of the profile")
    create_parser.set_defaults(func=create_profile)

    copy_parser = subcommands.add_parser("copy", help="Used to copy VSCode profiles")
    copy_parser.add_argument("--from", dest="from_name", type=str, required=True, help="Name of the profile to copy")
    copy_parser.add_argument("--to", dest="to_name", type=str, required=True, help="Name of the new profile to create")
    copy_parser.set_defaults(func=copy_profile)

    delete_parser = subcommands.add_parser("delete", help="Used to delete VSCode profiles")
    delete_parser.add_argument("--name", type=str, required=True, help="Name of the profile to delete")
    delete_parser.set_defaults(func=delete_profile)

    return parser.parse_args()


def get_profile_dir(name: str) -> pathlib.Path:
    return PROFILE_DIR / name


def get_profile_extension_dir(profile_dir: pathlib.Path) -> pathlib.Path:
    return profile_dir / "extensions"


def get_profile_data_dir(profile_dir: pathlib.Path) -> pathlib.Path:
    return profile_dir / "data"


def get_desktop_file_path(profile_name: str) -> pathlib.Path:
    return pathlib.Path.home() / ".local" / "share" / "applications" / f"code@{profile_name}.desktop"


def generate_desktop_file(profile_name: str, profile_dir: pathlib.Path) -> None:
    extensions_dir = get_profile_extension_dir(profile_dir)
    data_dir = get_profile_data_dir(profile_dir)

    file_contents = f"""[Desktop Entry]
Name=code@{profile_name}
Comment=code@{profile_name} VSCode profile
GenericName=Text Editor
Exec=/usr/share/code/code --extensions-dir "{extensions_dir}" --user-data-dir "{data_dir}" --unity-launch %F
Icon=com.visualstudio.code
Type=Application
StartupNotify=false
StartupWMClass=Code
Categories=Utility;TextEditor;Development;IDE;
MimeType=text/plain;inode/directory;application/x-code-workspace;
Actions=new-empty-window;
Keywords=vscode;

X-Desktop-File-Install-Version=0.24

[Desktop Action new-empty-window]
Name=New Empty Window
Exec=/usr/share/code/code --extensions-dir "{extensions_dir}" --user-data-dir "{data_dir}" --new-window %F
Icon=com.visualstudio.code
"""

    desktop_file_path = get_desktop_file_path(profile_name)
    with desktop_file_path.open("w") as desktop_file:
        desktop_file.write(file_contents)


def create_profile(name: str) -> None:
    profile_dir = get_profile_dir(name)

    if profile_dir.exists():
        print(f"Profile {name} already exists")

        sys.exit(1)

    profile_dir.mkdir()
    get_profile_extension_dir(profile_dir).mkdir()
    get_profile_data_dir(profile_dir).mkdir()

    generate_desktop_file(name, profile_dir)

    print(f"Successfully created VSCode profile {name}")


def copy_profile(from_name: str, to_name: str) -> None:
    from_profile_dir = get_profile_dir(from_name)
    to_profile_dir = get_profile_dir(to_name)

    if not from_profile_dir.exists():
        print(f"Profile {from_name} does not exist")

        sys.exit(1)

    if to_profile_dir.exists():
        print(f"Profile {to_name} already exists")

    shutil.copytree(from_profile_dir, to_profile_dir)
    generate_desktop_file(to_name, to_profile_dir)

    print(f"Copied VSCode profile {from_name} to {to_name}")


def delete_profile(name: str) -> None:
    profile_dir = get_profile_dir(name)
    desktop_file_path = get_desktop_file_path(name)

    if not profile_dir.exists():
        print(f"Profile {name} does not exist")
        sys.exit(1)

    shutil.rmtree(profile_dir)
    desktop_file_path.unlink()

    print(f"Successfully deleted VSCode profile {name}")


def main() -> None:
    args = parse_args()

    command_args = {
        name: value
        for name, value in vars(args).items()
        if name != "func"
    }
    args.func(**command_args)


if __name__ == "__main__":
    main()

Future improvements

Here’s a couple things I want to deal with to make the experience better:

  • Get different colored icons for each profile to make it easier to tell them apart (in VSCode and in the application launcher)