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 cca0ace..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/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..5d3004c 100644 --- a/PaletteShellExtension/PaletteShellExtension.csproj +++ b/PaletteShellExtension/PaletteShellExtension.csproj @@ -16,7 +16,7 @@ phireForge.PaletteShell phireForge - 0.0.7.0 + 0.0.9.0