Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions PaletteShellExtension/Commands/InstallScriptManagerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.CommandPalette.Extensions.Toolkit;
using PaletteShellExtension.Classes;
using System;
using System.Diagnostics;

namespace PaletteShellExtension.Commands;

/// <summary>
/// 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.
/// </summary>
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;
}
}
}
51 changes: 33 additions & 18 deletions PaletteShellExtension/Commands/LaunchCommunityStoreCommand.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Win32;
using PaletteShellExtension.Classes;
using System;
using System.Diagnostics;

namespace PaletteShellExtension.Commands;

/// <summary>
/// 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
/// <see cref="Classes.CommunityScriptCatalog"/>'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 <see cref="IsScriptManagerInstalled"/>);
/// when it isn't, the page routes to <see cref="Pages.CommunityScriptsPage"/> 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.
/// </summary>
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("☁");
Expand All @@ -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}");
}
}

/// <summary>
/// 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\&lt;scheme&gt; 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.
/// </summary>
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;
}
}
}
2 changes: 1 addition & 1 deletion PaletteShellExtension/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Identity
Name="phireForge.PaletteShell"
Publisher="CN=7340D37C-B15E-4DA1-B9DD-9B688A69713D"
Version="0.0.7.0" />
Version="0.0.9.0" />
<Properties>
<DisplayName>PaletteShell</DisplayName>
<PublisherDisplayName>phireForge</PublisherDisplayName>
Expand Down
41 changes: 41 additions & 0 deletions PaletteShellExtension/Pages/CommunityScriptsPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PaletteShellExtension.Commands;

namespace PaletteShellExtension.Pages;

/// <summary>
/// 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.
/// </summary>
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
},
];
}
36 changes: 28 additions & 8 deletions PaletteShellExtension/Pages/PaletteShellExtensionPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions PaletteShellExtension/PaletteScriptAttributes.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion PaletteShellExtension/PaletteShellExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<AppxPackageIdentityName>phireForge.PaletteShell</AppxPackageIdentityName>
<AppxPackagePublisher>phireForge</AppxPackagePublisher>
<AppxPackageVersion>0.0.7.0</AppxPackageVersion>
<AppxPackageVersion>0.0.9.0</AppxPackageVersion>

<!-- Shared self-signed cert for sideload testing (CN matches Publisher). Store re-signs on
submission, so this is dev-only. Cert must live in CurrentUser\My on the build machine. -->
Expand Down
Loading