From 296820f6be54fe99c7f9f6f918afd813f0652b8a Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Thu, 25 Jun 2026 00:11:28 +0200 Subject: [PATCH] vscode.sh: build a theme's local VS Code extension from source and install it A custom-palette theme can't reference a Marketplace id, so it ships its VS Code theme as a `vscode-extension/` source folder. `code --install-extension local.*` fails (not on the Marketplace), so the theme silently never loads. Build a real `.vsix` from that folder and install it; a prebuilt `*.vsix` in the theme dir is still honoured as a fallback. Marketplace ids are left untouched (omarchy-theme- set-vscode installs those, and also always sets `workbench.colorTheme`). Addresses review: - Require an `extension` id for a local theme (warn + skip) so the hook never reinstalls on every theme switch when the id is missing. - De-dupe via installed-state + a sha marker over the SOURCE files (a zip embeds mtimes, so hashing the built .vsix would differ every run). - Fold the behavioural assertions into tests/run.sh (build/install, idempotency, require-ext) and drop the standalone tests/vscode-vsix.test.sh. --- tests/run.sh | 54 ++++++++++++++++++++++++++++++ theme-set.d/30-vscode.sh | 72 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/tests/run.sh b/tests/run.sh index 5870ec7..df74e73 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -2005,6 +2005,58 @@ test_tmux_plugin_prefers_existing_legacy_config() { assert_file_missing "$xdg_config" "tmux plugin does not create xdg config when legacy config exists" } +test_vscode_plugin_builds_local_extension_from_source() { + local home_dir="$TMP_ROOT/vscode-build-home" + local bin_dir="$TMP_ROOT/vscode-build-bin" + local hook_dir="$home_dir/.config/omarchy/hooks/theme-set.d" + local theme_dir="$home_dir/.config/omarchy/current/theme" + local install_log="$home_dir/code-install.log" + + if ! command -v zip >/dev/null 2>&1; then + pass "vscode local-extension build test skipped (zip not installed)" + return 0 + fi + + write_colors_fixture "$home_dir" + mkdir -p "$hook_dir" "$bin_dir" "$theme_dir/vscode-extension/themes" + cp "$ROOT_DIR/theme-set.d/30-vscode.sh" "$hook_dir/30-vscode.sh" + chmod +x "$hook_dir/30-vscode.sh" + printf '{"name":"Foo","extension":"local.theme-foo"}\n' > "$theme_dir/vscode.json" + printf '{"name":"theme-foo","publisher":"local","contributes":{"themes":[{"label":"Foo","uiTheme":"vs-dark","path":"./themes/foo.json"}]}}\n' > "$theme_dir/vscode-extension/package.json" + printf '{"name":"Foo","colors":{"editor.background":"#101112"}}\n' > "$theme_dir/vscode-extension/themes/foo.json" + make_stub_bin "$bin_dir" code 'case "$1" in --list-extensions) cat "$HOME/code-installed.txt" 2>/dev/null ;; --install-extension) printf "%s\n" "$2" >> "$HOME/code-install.log"; printf "local.theme-foo\n" >> "$HOME/code-installed.txt" ;; esac' + make_stub_bin "$bin_dir" pgrep 'exit 1' + make_stub_bin "$bin_dir" notify-send 'exit 0' + + PATH="$bin_dir:$PATH" run_theme_hooks "$home_dir" >/dev/null + assert_file_exists "$install_log" "vscode plugin builds and installs a local extension from vscode-extension/" + assert_eq "1" "$(wc -l < "$install_log" 2>/dev/null | tr -d ' ')" "local extension installed exactly once" + + PATH="$bin_dir:$PATH" run_theme_hooks "$home_dir" >/dev/null + assert_eq "1" "$(wc -l < "$install_log" 2>/dev/null | tr -d ' ')" "unchanged local theme is not reinstalled on the next theme switch" +} + +test_vscode_plugin_requires_extension_id_for_local_theme() { + local home_dir="$TMP_ROOT/vscode-noext-home" + local bin_dir="$TMP_ROOT/vscode-noext-bin" + local hook_dir="$home_dir/.config/omarchy/hooks/theme-set.d" + local theme_dir="$home_dir/.config/omarchy/current/theme" + + write_colors_fixture "$home_dir" + mkdir -p "$hook_dir" "$bin_dir" "$theme_dir/vscode-extension/themes" + cp "$ROOT_DIR/theme-set.d/30-vscode.sh" "$hook_dir/30-vscode.sh" + chmod +x "$hook_dir/30-vscode.sh" + printf '{"name":"Foo"}\n' > "$theme_dir/vscode.json" + printf '{"name":"theme-foo","publisher":"local","contributes":{"themes":[]}}\n' > "$theme_dir/vscode-extension/package.json" + printf '{"name":"Foo","colors":{}}\n' > "$theme_dir/vscode-extension/themes/foo.json" + make_stub_bin "$bin_dir" code 'case "$1" in --install-extension) printf "%s\n" "$2" >> "$HOME/code-install.log" ;; esac; exit 0' + make_stub_bin "$bin_dir" pgrep 'exit 1' + make_stub_bin "$bin_dir" notify-send 'exit 0' + + PATH="$bin_dir:$PATH" run_theme_hooks "$home_dir" >/dev/null + assert_file_missing "$home_dir/code-install.log" "local theme without an extension id is not installed (no reinstall loop)" +} + test_vscode_plugin_skips_when_theme_provides_vscode_json() { local home_dir="$TMP_ROOT/vscode-skip-home" local bin_dir="$TMP_ROOT/vscode-skip-bin" @@ -2822,6 +2874,8 @@ main() { test_tmux_plugin_prefers_existing_legacy_config test_vscode_plugin_skips_when_theme_provides_vscode_json test_vscode_plugin_patches_extension_manifest_and_installs_theme + test_vscode_plugin_builds_local_extension_from_source + test_vscode_plugin_requires_extension_id_for_local_theme test_cursor_plugin_suppresses_electron_deprecation_warning test_theme_set_extracts_colors_with_leading_whitespace_and_comments test_install_preserves_disabled_plugins_and_installs_files diff --git a/theme-set.d/30-vscode.sh b/theme-set.d/30-vscode.sh index 46949cb..01825c5 100755 --- a/theme-set.d/30-vscode.sh +++ b/theme-set.d/30-vscode.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash source "${THPM_THEME_ENV:-$HOME/.local/share/thpm/lib/theme-env.sh}" -output_file="$HOME/.config/omarchy/current/theme/vscode_colors.json" +theme_dir="$(dirname "$input_file")" # input_file is resolved by theme-env.sh (config-aware) +output_file="$theme_dir/vscode_colors.json" if ! command -v code >/dev/null 2>&1; then skipped "VS Code" @@ -11,7 +12,74 @@ if ! command -v jq >/dev/null 2>&1; then fi # check current theme for vscode.json -if [[ -f "$HOME/.config/omarchy/current/theme/vscode.json" ]]; then +if [[ -f "$theme_dir/vscode.json" ]]; then + # A theme themes VS Code in one of two ways: + # - a MARKETPLACE id in vscode.json (Publisher.name) -> omarchy-theme-set-vscode already + # ran `code --install-extension ` for it; nothing to do here. + # - a LOCAL extension (id "local.*") shipped as a "vscode-extension/" source folder -> + # `code --install-extension local.*` can't reach a Marketplace, so we build a real .vsix + # from that folder and install it (the only reliable way to register a local theme). + # Building from source means no committed binary can go stale; a prebuilt *.vsix in the theme + # dir is still honoured as a fallback. Reinstall only when the extension is missing or the + # source/.vsix changed (sha marker), so it is not rebuilt on every theme switch. + # (code/jq are guaranteed present above via skipped.) + ext="$(jq -r '.extension // empty' "$theme_dir/vscode.json")" + ext_src="$theme_dir/vscode-extension" + shopt -s nullglob ; prebuilt=("$theme_dir"/*.vsix) ; shopt -u nullglob + + # Nothing local to install (a Marketplace id or a built-in colorTheme) -> omarchy-theme-set-vscode + # already handled it; leave it alone. + if [[ ! -d "$ext_src" && ${#prebuilt[@]} -eq 0 ]]; then + exit 0 + fi + # A local extension must declare its id so we can register and de-duplicate against it; without + # one we'd reinstall on every switch, so require it. + if [[ -z "$ext" ]]; then + warning "VS Code: local theme extension present but vscode.json has no 'extension' id" + exit 0 + fi + + state_dir="${THPM_STATE_DIR:-$HOME/.local/share/thpm}" + mkdir -p "$state_dir" 2>/dev/null + ext_safe="${ext//[^a-zA-Z0-9._-]/_}" + marker="$state_dir/vscode-vsix-${ext_safe}.sha" + + vsix="" ; sha="" ; tmp="" + if [[ -d "$ext_src" ]] && command -v zip >/dev/null 2>&1; then + # local theme with source -> build a .vsix. sha over the SOURCE files (a zip embeds + # mtimes, so hashing the built .vsix would differ every run and reinstall every time). + sha="$(find "$ext_src" -type f -exec sha256sum {} + 2>/dev/null | sort | sha256sum | cut -d' ' -f1)" + if ! code --list-extensions 2>/dev/null | grep -Fxq "$ext" \ + || [[ "$(cat "$marker" 2>/dev/null)" != "$sha" ]]; then + pkg_name="$(jq -r '.name // empty' "$ext_src/package.json" 2>/dev/null)" + pkg_pub="$(jq -r '.publisher // "local"' "$ext_src/package.json" 2>/dev/null)" + if [[ -n "$pkg_name" ]]; then + tmp="$(mktemp -d)" + mkdir -p "$tmp/extension" + cp -r "$ext_src"/. "$tmp/extension/" 2>/dev/null + printf '\n%s%sThemes\n' "$pkg_name" "$pkg_pub" "$pkg_name" "$pkg_name" >"$tmp/extension.vsixmanifest" + printf '\n\n' >"$tmp/[Content_Types].xml" + ( cd "$tmp" && zip -rqX theme.vsix extension.vsixmanifest '[Content_Types].xml' extension ) && vsix="$tmp/theme.vsix" + fi + fi + else + # no source folder -> honour a prebuilt *.vsix shipped in the theme dir + sha="$(sha256sum "${prebuilt[0]}" 2>/dev/null | cut -d' ' -f1)" + if ! code --list-extensions 2>/dev/null | grep -Fxq "$ext" \ + || [[ "$(cat "$marker" 2>/dev/null)" != "$sha" ]]; then + vsix="${prebuilt[0]}" + fi + fi + + if [[ -n "$vsix" ]]; then + if code --install-extension "$vsix" --force >/dev/null 2>&1; then + [[ -n "$sha" ]] && printf '%s\n' "$sha" >"$marker" 2>/dev/null + success "VS Code (${ext:-theme})" + else + warning "VS Code extension failed to install (${ext:-theme})" + fi + fi + [[ -n "$tmp" ]] && rm -rf "$tmp" exit 0 fi