From 4b1656434bf14803e26d6e4bd29df422bd8e7366 Mon Sep 17 00:00:00 2001
From: phireForge <269562005+phireForge@users.noreply.github.com>
Date: Mon, 13 Jul 2026 07:46:38 -0400
Subject: [PATCH 1/2] clipboard fix
---
PaletteShellExtension/Package.appxmanifest | 2 +-
PaletteShellExtension/PaletteScriptAttributes.psm1 | 4 ++++
PaletteShellExtension/PaletteShellExtension.csproj | 2 +-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/PaletteShellExtension/Package.appxmanifest b/PaletteShellExtension/Package.appxmanifest
index cca0ace..cdab38b 100644
--- a/PaletteShellExtension/Package.appxmanifest
+++ b/PaletteShellExtension/Package.appxmanifest
@@ -11,7 +11,7 @@
+ Version="0.0.8.0" />
PaletteShell
phireForge
diff --git a/PaletteShellExtension/PaletteScriptAttributes.psm1 b/PaletteShellExtension/PaletteScriptAttributes.psm1
index 574b07d..550d8c6 100644
--- a/PaletteShellExtension/PaletteScriptAttributes.psm1
+++ b/PaletteShellExtension/PaletteScriptAttributes.psm1
@@ -130,6 +130,10 @@ function Get-ClipboardText {
# Use built-in Get-Clipboard if available (PowerShell 5+)
if (Get-Command -Name Get-Clipboard -ErrorAction SilentlyContinue) {
+ # PowerShell 7+ (Core) Get-Clipboard is text-only and has no -Format parameter.
+ if ($PSVersionTable.PSVersion.Major -ge 6) {
+ return Get-Clipboard -Raw
+ }
return Get-Clipboard -Format Text -Raw
}
diff --git a/PaletteShellExtension/PaletteShellExtension.csproj b/PaletteShellExtension/PaletteShellExtension.csproj
index 555f187..2191121 100644
--- a/PaletteShellExtension/PaletteShellExtension.csproj
+++ b/PaletteShellExtension/PaletteShellExtension.csproj
@@ -16,7 +16,7 @@
phireForge.PaletteShell
phireForge
- 0.0.7.0
+ 0.0.8.0
From 27dfbd4d1f1eb248198aab7fa8a7a4cd08bc4fbd Mon Sep 17 00:00:00 2001
From: phireForge <269562005+phireForge@users.noreply.github.com>
Date: Mon, 13 Jul 2026 10:42:26 -0400
Subject: [PATCH 2/2] improve community script choice
---
.../Commands/InstallScriptManagerCommand.cs | 47 +++++++++++++++++
.../Commands/LaunchCommunityStoreCommand.cs | 51 ++++++++++++-------
PaletteShellExtension/Package.appxmanifest | 2 +-
.../Pages/CommunityScriptsPage.cs | 41 +++++++++++++++
.../Pages/PaletteShellExtensionPage.cs | 36 ++++++++++---
.../PaletteShellExtension.csproj | 2 +-
6 files changed, 151 insertions(+), 28 deletions(-)
create mode 100644 PaletteShellExtension/Commands/InstallScriptManagerCommand.cs
create mode 100644 PaletteShellExtension/Pages/CommunityScriptsPage.cs
diff --git a/PaletteShellExtension/Commands/InstallScriptManagerCommand.cs b/PaletteShellExtension/Commands/InstallScriptManagerCommand.cs
new file mode 100644
index 0000000..ac0343f
--- /dev/null
+++ b/PaletteShellExtension/Commands/InstallScriptManagerCommand.cs
@@ -0,0 +1,47 @@
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PaletteShellExtension.Classes;
+using System;
+using System.Diagnostics;
+
+namespace PaletteShellExtension.Commands;
+
+///
+/// Takes the user to the "PaletteShell Script Manager" Microsoft Store listing so they can
+/// install it. Prefers the ms-windows-store: deep link (opens the Store app straight to the
+/// listing) and falls back to the apps.microsoft.com URL for machines where the Store app
+/// itself is unavailable. Best-effort, never-throw - matches the rest of this codebase.
+///
+internal sealed partial class InstallScriptManagerCommand : InvokableCommand
+{
+ // Microsoft Store product ID for "PaletteShell Script Manager".
+ private const string StoreProductId = "9P4TBJKKNFZ1";
+ private const string StoreProtocolUrl = "ms-windows-store://pdp/?ProductId=" + StoreProductId;
+ private const string StoreWebUrl = "https://apps.microsoft.com/detail/" + StoreProductId;
+
+ public override string Name => "Install";
+ public override IconInfo Icon => new(""); // Download
+
+ public override CommandResult Invoke()
+ {
+ if (TryOpen(StoreProtocolUrl) || TryOpen(StoreWebUrl))
+ {
+ return CommandResult.Dismiss();
+ }
+
+ return CommandResult.ShowToast("Couldn't open the Microsoft Store listing.");
+ }
+
+ private static bool TryOpen(string url)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.Warn($"Failed to open Store listing '{url}': {ex.Message}");
+ return false;
+ }
+ }
+}
diff --git a/PaletteShellExtension/Commands/LaunchCommunityStoreCommand.cs b/PaletteShellExtension/Commands/LaunchCommunityStoreCommand.cs
index 1b869ec..853ad9b 100644
--- a/PaletteShellExtension/Commands/LaunchCommunityStoreCommand.cs
+++ b/PaletteShellExtension/Commands/LaunchCommunityStoreCommand.cs
@@ -1,21 +1,24 @@
using Microsoft.CommandPalette.Extensions.Toolkit;
+using Microsoft.Win32;
+using PaletteShellExtension.Classes;
using System;
using System.Diagnostics;
namespace PaletteShellExtension.Commands;
///
-/// Opens the companion "PaletteShell Script Store" app (a separate, Store-distributed WinUI 3
+/// Opens the companion "PaletteShell Script Manager" app (a separate, Store-distributed WinUI 3
/// app that owns browsing/installing/updating community scripts) via its registered protocol,
-/// so this extension doesn't need to know where - or whether - it's installed. Falls back to
-/// the community scripts GitHub repo if the protocol has no handler, i.e. the app isn't installed yet - same
-/// best-effort, never-throw philosophy as the rest of this codebase (see
-/// 's error handling).
+/// so this extension doesn't need to know where - or whether - it's installed. This command is
+/// only used when the app is known to be installed (see );
+/// when it isn't, the page routes to so the user can
+/// choose between installing it and browsing the repo, rather than presuming they want the Store.
+/// Same best-effort, never-throw philosophy as the rest of this codebase.
///
internal sealed partial class LaunchCommunityStoreCommand : InvokableCommand
{
- private const string CommunityScriptsRepositoryUrl = "https://github.com/paletteshell/PaletteShellScripts";
- private const string ProtocolUrl = "palette-script-manager:";
+ internal const string ProtocolScheme = "palette-script-manager";
+ private const string ProtocolUrl = ProtocolScheme + ":";
public override string Name => "Open";
public override IconInfo Icon => new("☁");
@@ -27,19 +30,31 @@ public override CommandResult Invoke()
Process.Start(new ProcessStartInfo(ProtocolUrl) { UseShellExecute = true });
return CommandResult.Dismiss();
}
+ catch (Exception ex)
+ {
+ Log.Warn($"Failed to launch Script Manager protocol: {ex.Message}");
+ return CommandResult.ShowToast($"Couldn't open the Script Manager: {ex.Message}");
+ }
+ }
+
+ ///
+ /// True when the Script Manager's URL protocol has a handler, i.e. the app is installed.
+ /// A handler is registered under HKEY_CLASSES_ROOT\<scheme> with a "URL Protocol" value;
+ /// HKCR is the merged HKLM+HKCU view, so this also sees per-user (incl. packaged) handlers.
+ /// ShellExecute doesn't throw for an unregistered protocol - Windows either silently no-ops
+ /// or pops its own "how do you want to open this?" dialog - so a catch can't detect a missing
+ /// handler; the caller checks this up front instead.
+ ///
+ public static bool IsScriptManagerInstalled()
+ {
+ try
+ {
+ using var key = Registry.ClassesRoot.OpenSubKey(ProtocolScheme);
+ return key?.GetValue("URL Protocol") is not null;
+ }
catch (Exception)
{
- // No handler registered for the protocol - the companion app isn't installed.
- // Send the user to the community repo instead of failing silently.
- try
- {
- Process.Start(new ProcessStartInfo(CommunityScriptsRepositoryUrl) { UseShellExecute = true });
- return CommandResult.ShowToast("PaletteShell Script Store isn't installed - opening the community scripts repo.");
- }
- catch (Exception ex)
- {
- return CommandResult.ShowToast($"Couldn't open the Script Store or community scripts repo: {ex.Message}");
- }
+ return false;
}
}
}
diff --git a/PaletteShellExtension/Package.appxmanifest b/PaletteShellExtension/Package.appxmanifest
index cdab38b..49ee6b7 100644
--- a/PaletteShellExtension/Package.appxmanifest
+++ b/PaletteShellExtension/Package.appxmanifest
@@ -11,7 +11,7 @@
+ Version="0.0.9.0" />
PaletteShell
phireForge
diff --git a/PaletteShellExtension/Pages/CommunityScriptsPage.cs b/PaletteShellExtension/Pages/CommunityScriptsPage.cs
new file mode 100644
index 0000000..3629259
--- /dev/null
+++ b/PaletteShellExtension/Pages/CommunityScriptsPage.cs
@@ -0,0 +1,41 @@
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using PaletteShellExtension.Commands;
+
+namespace PaletteShellExtension.Pages;
+
+///
+/// Shown from the main page's "Browse community scripts" entry when the Script Manager app
+/// isn't installed. Rather than presuming the user wants to install it (and opening the Store
+/// unprompted), this offers an explicit choice: install the app from the Microsoft Store, or
+/// just browse the community scripts on GitHub. When the app *is* installed, the main page
+/// launches it directly and this page is never reached.
+///
+internal sealed partial class CommunityScriptsPage : ListPage
+{
+ private const string CommunityScriptsRepositoryUrl = "https://github.com/paletteshell/PaletteShellScripts";
+
+ public CommunityScriptsPage()
+ {
+ Title = "Browse community scripts";
+ Name = "Browse community scripts";
+ Icon = new("☁");
+ Id = "CommunityScripts";
+ }
+
+ public override IListItem[] GetItems() =>
+ [
+ new ListItem(new InstallScriptManagerCommand())
+ {
+ Title = "Install PaletteShell Script Manager",
+ Subtitle = "Open the Microsoft Store to install the app for browsing and installing community scripts",
+ Icon = new IconInfo("☁"),
+ },
+ new ListItem(new OpenLinkCommand("Browse on GitHub", CommunityScriptsRepositoryUrl, ""))
+ {
+ Title = "Browse community scripts on GitHub",
+ Subtitle = CommunityScriptsRepositoryUrl,
+ Icon = new IconInfo(""), // Link
+ },
+ ];
+}
diff --git a/PaletteShellExtension/Pages/PaletteShellExtensionPage.cs b/PaletteShellExtension/Pages/PaletteShellExtensionPage.cs
index a9d78f8..6b301a2 100644
--- a/PaletteShellExtension/Pages/PaletteShellExtensionPage.cs
+++ b/PaletteShellExtension/Pages/PaletteShellExtensionPage.cs
@@ -355,14 +355,7 @@ public override IListItem[] GetItems()
},
new ListItem(new ReloadPageCommand(this)) { Title = "Reload scripts" },
new ListItem(new NewScriptWizardPage(rootDirectory)) { Title = "Create new script", Subtitle = "Add a scaffolded .ps1 with metadata headers" },
- new ListItem(new LaunchCommunityStoreCommand())
- {
- Title = "Browse community scripts",
- Subtitle = "Open the Script Manager, or browse the community repo if it isn't installed",
- MoreCommands = [
- new CommandContextItem(new OpenLinkCommand("View repository on GitHub", "https://github.com/paletteshell/PaletteShellScripts", "")),
- ],
- },
+ BuildCommunityScriptsItem(),
]);
// Script items are sorted below: pinned scripts first, then alphabetically by their
@@ -594,6 +587,33 @@ public override IListItem[] GetItems()
return _cachedItems;
}
+ // The "Browse community scripts" row. If the Script Manager app is installed, Enter launches
+ // it directly; if not, Enter opens a chooser page (install from Store vs. browse on GitHub)
+ // rather than presuming the user wants to install it. GitHub is always one keystroke away via
+ // the context menu regardless.
+ private static readonly CommandContextItem GitHubRepoContextItem =
+ new(new OpenLinkCommand("View repository on GitHub", "https://github.com/paletteshell/PaletteShellScripts", ""));
+
+ private static ListItem BuildCommunityScriptsItem()
+ {
+ if (LaunchCommunityStoreCommand.IsScriptManagerInstalled())
+ {
+ return new ListItem(new LaunchCommunityStoreCommand())
+ {
+ Title = "Browse community scripts",
+ Subtitle = "Open the PaletteShell Script Manager",
+ MoreCommands = [GitHubRepoContextItem],
+ };
+ }
+
+ return new ListItem(new CommunityScriptsPage())
+ {
+ Title = "Browse community scripts",
+ Subtitle = "Install the Script Manager app, or browse the community repo on GitHub",
+ MoreCommands = [GitHubRepoContextItem],
+ };
+ }
+
// Fallback glyph for scripts that don't declare their own [ScriptIcon]. Keeps the list
// scannable instead of showing rows with no icon at all.
private static readonly IconInfo DefaultScriptIcon = new(""); // CommandPrompt
diff --git a/PaletteShellExtension/PaletteShellExtension.csproj b/PaletteShellExtension/PaletteShellExtension.csproj
index 2191121..5d3004c 100644
--- a/PaletteShellExtension/PaletteShellExtension.csproj
+++ b/PaletteShellExtension/PaletteShellExtension.csproj
@@ -16,7 +16,7 @@
phireForge.PaletteShell
phireForge
- 0.0.8.0
+ 0.0.9.0