Fix Command Palette extension and bundle in installer
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build (win-arm64) (push) Has been cancelled
Build and Test / build (win-x64) (push) Has been cancelled
Build and Test / build-extension (arm64) (push) Has been cancelled
Build and Test / build-extension (x64) (push) Has been cancelled
Build and Test / release (push) Has been cancelled

- Fix exe name mismatch: Moltbot.exe -> Moltbot.CommandPalette.exe in manifest
- Add AppListEntry=none to hide from Start Menu (only show in Command Palette)
- Use OpenUrlCommand from toolkit instead of custom DeepLinkCommand
- Add cmdpal-dev.ps1 script for local iteration (auto-detects arm64/x64)
- Update installer to optionally install Command Palette extension
- Add molty2.png to README
- Update CI to bundle Command Palette in installer
This commit is contained in:
Scott Hanselman 2026-01-28 23:33:32 -08:00
parent 12f7e09a10
commit 83d61eb641
15 changed files with 201 additions and 490 deletions

View File

@ -149,6 +149,18 @@ jobs:
name: moltbot-tray-win-arm64
path: artifacts/tray-win-arm64
- name: Download win-x64 cmdpal artifact
uses: actions/download-artifact@v4
with:
name: moltbot-commandpalette-x64
path: artifacts/cmdpal-x64
- name: Download win-arm64 cmdpal artifact
uses: actions/download-artifact@v4
with:
name: moltbot-commandpalette-arm64
path: artifacts/cmdpal-arm64
# Create ZIP files for Updatum auto-update (needs "win-x64" in filename)
- name: Create Release ZIPs
run: |
@ -163,6 +175,18 @@ jobs:
run: |
mkdir publish
copy artifacts/tray-win-x64/Moltbot.Tray.exe publish/
# Copy Command Palette extension for installer (use x64 for installer)
mkdir publish\cmdpal
$cmdpalPath = Get-ChildItem -Path "artifacts/cmdpal-x64" -Recurse -Directory | Where-Object { $_.Name -like "win-x64" } | Select-Object -First 1
if ($cmdpalPath) {
Copy-Item "$($cmdpalPath.FullName)\*" -Destination publish\cmdpal -Recurse
} else {
# Fallback: copy from the first subfolder that has AppxManifest.xml
$manifestFolder = Get-ChildItem -Path "artifacts/cmdpal-x64" -Recurse -Filter "AppxManifest.xml" | Select-Object -First 1
if ($manifestFolder) {
Copy-Item "$($manifestFolder.DirectoryName)\*" -Destination publish\cmdpal -Recurse
}
}
- name: Build Installer (x64)
run: |

View File

@ -6,6 +6,8 @@ A Windows companion suite for [Moltbot](https://moltbot.com) - the AI-powered pe
![Molty - Windows Tray App](docs/molty1.png)
![Molty - Command Palette](docs/molty2.png)
## Projects
This monorepo contains three projects:

BIN
docs/molty2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -29,10 +29,14 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "startupicon"; Description: "Start Moltbot Tray when Windows starts"; GroupDescription: "Startup:"; Flags: unchecked
Name: "cmdpalette"; Description: "Install PowerToys Command Palette extension"; GroupDescription: "Integrations:"; Flags: unchecked
[Files]
; Tray app
Source: "publish\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "src\Moltbot.Tray\moltbot.ico"; DestDir: "{app}"; Flags: ignoreversion
; Command Palette extension (all files from build output)
Source: "publish\cmdpal\*"; DestDir: "{app}\CommandPalette"; Flags: ignoreversion recursesubdirs; Tasks: cmdpalette
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\moltbot.ico"
@ -42,3 +46,9 @@ Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: st
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
; Register Command Palette extension (silently, only if task selected)
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-AppxPackage -Register '{app}\CommandPalette\AppxManifest.xml' -ForceApplicationShutdown"""; Flags: runhidden; Tasks: cmdpalette
[UninstallRun]
; Unregister Command Palette extension on uninstall
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Get-AppxPackage -Name '*Moltbot*' | Remove-AppxPackage"""; Flags: runhidden

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 B

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 474 B

View File

@ -16,13 +16,13 @@
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
<Content Include="Assets\SplashScreen.scale-200.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\LockScreenLogo.scale-200.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Square150x150Logo.scale-200.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Square44x44Logo.scale-200.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\StoreLogo.png" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>

View File

@ -38,6 +38,7 @@
DisplayName="Moltbot"
Description="Moltbot"
BackgroundColor="transparent"
AppListEntry="none"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
@ -46,7 +47,7 @@
<Extensions>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Moltbot.exe" Arguments="-RegisterProcessAsComServer" DisplayName="Moltbot">
<com:ExeServer Executable="Moltbot.CommandPalette.exe" Arguments="-RegisterProcessAsComServer" DisplayName="Moltbot">
<com:Class Id="b71e1e6d-20f4-4fd8-9d8e-cc5dc94ca8b5" DisplayName="Moltbot" />
</com:ExeServer>
</com:ComServer>

View File

@ -2,13 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moltbot.Shared;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@ -16,498 +9,37 @@ namespace Moltbot;
internal sealed partial class MoltbotPage : ListPage
{
private static string _gatewayUrl = "ws://localhost:18789";
private static string _token = "";
public MoltbotPage()
{
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "Moltbot";
Name = "Open";
// Try to load settings from tray app
LoadSettings();
}
private static void LoadSettings()
{
try
{
var settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MoltbotTray", "settings.json");
if (File.Exists(settingsPath))
{
var json = File.ReadAllText(settingsPath);
var settings = System.Text.Json.JsonDocument.Parse(json);
if (settings.RootElement.TryGetProperty("GatewayUrl", out var url))
_gatewayUrl = url.GetString() ?? _gatewayUrl;
if (settings.RootElement.TryGetProperty("Token", out var token))
_token = token.GetString() ?? "";
}
}
catch { }
}
public override IListItem[] GetItems()
{
var items = new List<IListItem>
{
new ListItem(new OpenDashboardCommand(_gatewayUrl, _token))
return [
new ListItem(new OpenUrlCommand("http://localhost:18789"))
{
Title = "🦞 Open Dashboard",
Subtitle = "Open Moltbot web dashboard in browser"
Subtitle = "Open Moltbot web dashboard"
},
new ListItem(new QuickSendCommand(_gatewayUrl, _token))
new ListItem(new OpenUrlCommand("moltbot://chat"))
{
Title = "💬 Quick Send",
Title = "💬 Web Chat",
Subtitle = "Open the Moltbot chat window"
},
new ListItem(new OpenUrlCommand("moltbot://send"))
{
Title = "📝 Quick Send",
Subtitle = "Send a message to Moltbot"
},
new ListItem(new StatusPage(_gatewayUrl, _token))
new ListItem(new OpenUrlCommand("moltbot://settings"))
{
Title = "📊 Full Status",
Subtitle = "View gateway, sessions, and channels"
},
new ListItem(new SessionsPage(_gatewayUrl, _token))
{
Title = "⚡ Sessions",
Subtitle = "View active agent sessions"
},
new ListItem(new ChannelsPage(_gatewayUrl, _token))
{
Title = "📡 Channels",
Subtitle = "View Telegram, WhatsApp status"
},
new ListItem(new HealthCheckCommand(_gatewayUrl, _token))
{
Title = "🔄 Health Check",
Subtitle = "Run a gateway health check"
Title = "⚙️ Settings",
Subtitle = "Configure Moltbot Tray"
}
};
return items.ToArray();
}
}
/// <summary>
/// Command to open the Moltbot dashboard in the browser.
/// </summary>
internal sealed partial class OpenDashboardCommand : InvokableCommand
{
private readonly string _gatewayUrl;
private readonly string _token;
public OpenDashboardCommand(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
}
public override ICommandResult Invoke()
{
try
{
var url = GetDashboardUrl(_gatewayUrl, _token);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url)
{
UseShellExecute = true
});
}
catch { }
return CommandResult.Hide();
}
internal static string GetDashboardUrl(string gatewayUrl, string token)
{
var url = gatewayUrl
.Replace("ws://", "http://")
.Replace("wss://", "https://");
if (!string.IsNullOrEmpty(token))
{
var separator = url.Contains('?') ? "&" : "?";
url = $"{url}{separator}token={Uri.EscapeDataString(token)}";
}
return url;
}
}
/// <summary>
/// Command to send a quick message - prompts for input then sends.
/// </summary>
internal sealed partial class QuickSendCommand : InvokableCommand
{
private readonly string _gatewayUrl;
private readonly string _token;
public QuickSendCommand(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
Name = "Send message to Moltbot";
}
public override ICommandResult Invoke()
{
// Open a simple input dialog using Windows forms
try
{
// Use the dashboard URL with a message prompt
var url = OpenDashboardCommand.GetDashboardUrl(_gatewayUrl, _token);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url)
{
UseShellExecute = true
});
}
catch { }
return CommandResult.Hide();
}
}
/// <summary>
/// Command to run a health check.
/// </summary>
internal sealed partial class HealthCheckCommand : InvokableCommand
{
private readonly string _gatewayUrl;
private readonly string _token;
public HealthCheckCommand(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
}
public override ICommandResult Invoke()
{
// Just run the health check and show a toast/notification
Task.Run(async () =>
{
try
{
using var client = new MoltbotGatewayClient(_gatewayUrl, _token);
await client.ConnectAsync();
await client.CheckHealthAsync();
await Task.Delay(1000);
await client.DisconnectAsync();
}
catch { }
});
// Keep palette open - user can check status page
return CommandResult.KeepOpen();
}
}
/// <summary>
/// Page showing active sessions.
/// </summary>
internal sealed partial class SessionsPage : ContentPage
{
private readonly string _gatewayUrl;
private readonly string _token;
public SessionsPage(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "Sessions";
Name = "View sessions";
}
public override IContent[] GetContent()
{
var sb = new StringBuilder();
sb.AppendLine("## ⚡ Active Sessions");
sb.AppendLine();
try
{
using var client = new MoltbotGatewayClient(_gatewayUrl, _token);
var task = client.ConnectAsync();
task.Wait(TimeSpan.FromSeconds(3));
if (!task.IsCompletedSuccessfully)
{
sb.AppendLine("❌ Could not connect to gateway");
return [new MarkdownContent { Body = sb.ToString() }];
}
client.RequestSessionsAsync().Wait(TimeSpan.FromSeconds(2));
var sessions = client.GetSessionList();
if (sessions.Length == 0)
{
sb.AppendLine("_No active sessions_");
}
else
{
// Group by main/sub
var mainSessions = new List<SessionInfo>();
var subSessions = new List<SessionInfo>();
foreach (var s in sessions)
{
if (s.IsMain) mainSessions.Add(s);
else subSessions.Add(s);
}
if (mainSessions.Count > 0)
{
sb.AppendLine("### ⚡ Main Sessions");
foreach (var s in mainSessions)
{
sb.AppendLine($"- **{s.ShortKey}**");
if (!string.IsNullOrEmpty(s.Model))
sb.AppendLine($" - Model: `{s.Model}`");
if (!string.IsNullOrEmpty(s.Channel))
sb.AppendLine($" - Channel: {s.Channel}");
if (s.StartedAt.HasValue)
sb.AppendLine($" - Started: {s.StartedAt:g}");
}
sb.AppendLine();
}
if (subSessions.Count > 0)
{
sb.AppendLine($"### 🔹 Sub-Sessions ({subSessions.Count})");
foreach (var s in subSessions)
{
var activity = !string.IsNullOrEmpty(s.CurrentActivity) ? $" - {s.CurrentActivity}" : "";
sb.AppendLine($"- {s.ShortKey}{activity}");
}
}
}
client.DisconnectAsync().Wait(TimeSpan.FromSeconds(1));
}
catch (Exception ex)
{
sb.AppendLine($"❌ Error: {ex.Message}");
}
return [new MarkdownContent { Body = sb.ToString() }];
}
}
/// <summary>
/// Page showing channel health.
/// </summary>
internal sealed partial class ChannelsPage : ContentPage
{
private readonly string _gatewayUrl;
private readonly string _token;
private ChannelHealth[]? _channels;
public ChannelsPage(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "Channels";
Name = "View channels";
}
public override IContent[] GetContent()
{
var sb = new StringBuilder();
sb.AppendLine("## 📡 Channel Status");
sb.AppendLine();
try
{
using var client = new MoltbotGatewayClient(_gatewayUrl, _token);
client.ChannelHealthUpdated += (s, channels) => _channels = channels;
var task = client.ConnectAsync();
task.Wait(TimeSpan.FromSeconds(3));
if (!task.IsCompletedSuccessfully)
{
sb.AppendLine("❌ Could not connect to gateway");
return [new MarkdownContent { Body = sb.ToString() }];
}
// Health check fetches channel status
client.CheckHealthAsync().Wait(TimeSpan.FromSeconds(2));
if (_channels == null || _channels.Length == 0)
{
sb.AppendLine("_No channels configured_");
}
else
{
foreach (var ch in _channels)
{
var statusIcon = ch.Status.ToLowerInvariant() switch
{
"running" or "ok" or "connected" => "🟢",
"ready" => "🟡",
"linked" => "🔵",
"stopped" or "configured" => "⚪",
"error" => "🔴",
_ => "⚫"
};
var name = char.ToUpper(ch.Name[0]) + ch.Name[1..];
sb.AppendLine($"### {statusIcon} {name}");
sb.AppendLine();
sb.AppendLine($"- **Status:** {ch.Status}");
if (ch.IsLinked)
sb.AppendLine("- **Linked:** ✅ Yes");
if (!string.IsNullOrEmpty(ch.AuthAge))
sb.AppendLine($"- **Auth Age:** {ch.AuthAge}");
if (!string.IsNullOrEmpty(ch.Error))
sb.AppendLine($"- **Error:** ⚠️ {ch.Error}");
sb.AppendLine();
}
}
client.DisconnectAsync().Wait(TimeSpan.FromSeconds(1));
}
catch (Exception ex)
{
sb.AppendLine($"❌ Error: {ex.Message}");
}
return [new MarkdownContent { Body = sb.ToString() }];
}
}
/// <summary>
/// Page showing full Moltbot status information.
/// </summary>
internal sealed partial class StatusPage : ContentPage
{
private readonly string _gatewayUrl;
private readonly string _token;
private ChannelHealth[]? _channels;
public StatusPage(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "Moltbot Status";
Name = "View status";
}
public override IContent[] GetContent()
{
var markdown = new MarkdownContent
{
Body = GetStatusMarkdown()
};
return [markdown];
}
private string GetStatusMarkdown()
{
var sb = new StringBuilder();
try
{
using var client = new MoltbotGatewayClient(_gatewayUrl, _token);
client.ChannelHealthUpdated += (s, channels) => _channels = channels;
var task = client.ConnectAsync();
task.Wait(TimeSpan.FromSeconds(3));
if (!task.IsCompletedSuccessfully)
{
return "## ❌ Disconnected\n\nCould not connect to gateway.\n\nMake sure Moltbot gateway is running.";
}
sb.AppendLine("## 🦞 Moltbot Status");
sb.AppendLine();
sb.AppendLine("### Connection");
sb.AppendLine($"- **Gateway:** `{_gatewayUrl}`");
sb.AppendLine("- **Status:** ✅ Connected");
sb.AppendLine();
// Get health and sessions
client.CheckHealthAsync().Wait(TimeSpan.FromSeconds(2));
client.RequestSessionsAsync().Wait(TimeSpan.FromSeconds(1));
var sessions = client.GetSessionList();
// Sessions
sb.AppendLine("### ⚡ Sessions");
if (sessions.Length == 0)
{
sb.AppendLine("_No active sessions_");
}
else
{
var mainCount = 0;
var subCount = 0;
foreach (var s in sessions)
{
if (s.IsMain) mainCount++;
else subCount++;
}
sb.AppendLine($"- Main: **{mainCount}** | Sub: **{subCount}**");
sb.AppendLine();
foreach (var s in sessions.Take(5))
{
var icon = s.IsMain ? "⚡" : "🔹";
sb.AppendLine($"- {icon} {s.DisplayText}");
}
if (sessions.Length > 5)
sb.AppendLine($"- _...and {sessions.Length - 5} more_");
}
sb.AppendLine();
// Channels
sb.AppendLine("### 📡 Channels");
if (_channels == null || _channels.Length == 0)
{
sb.AppendLine("_No channels configured_");
}
else
{
foreach (var ch in _channels)
{
var statusIcon = ch.Status.ToLowerInvariant() switch
{
"running" or "ok" => "🟢",
"ready" => "🟡",
"linked" => "🔵",
"stopped" => "⚪",
"error" => "🔴",
_ => "⚫"
};
var name = char.ToUpper(ch.Name[0]) + ch.Name[1..];
sb.AppendLine($"- {statusIcon} **{name}:** {ch.Status}");
}
}
client.DisconnectAsync().Wait(TimeSpan.FromSeconds(1));
sb.AppendLine();
sb.AppendLine("---");
sb.AppendLine("_Use 🦞 Open Dashboard for the full web interface_");
return sb.ToString();
}
catch (Exception ex)
{
return $"## ❌ Error\n\n{ex.Message}\n\nMake sure the gateway is running and your settings are correct.";
}
];
}
}

81
tools/cmdpal-dev.ps1 Normal file
View File

@ -0,0 +1,81 @@
# Command Palette Development Script
# Usage: .\tools\cmdpal-dev.ps1 [build|deploy|remove|cycle]
param(
[Parameter(Position=0)]
[ValidateSet('build', 'deploy', 'remove', 'cycle', 'status')]
[string]$Action = 'cycle'
)
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$ProjectPath = "$RepoRoot\src\Moltbot.CommandPalette"
# Detect architecture
$Arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' }
$OutputPath = "$ProjectPath\bin\$Arch\Debug\net10.0-windows10.0.26100.0\win-$Arch"
function Write-Step($msg) { Write-Host "`n>> $msg" -ForegroundColor Cyan }
function Get-InstalledPackage {
Get-AppxPackage -Name "*Moltbot*" 2>$null
}
function Remove-ExtensionPackage {
$pkg = Get-InstalledPackage
if ($pkg) {
Write-Step "Removing $($pkg.PackageFullName)..."
Remove-AppxPackage -Package $pkg.PackageFullName
Write-Host "Removed!" -ForegroundColor Green
} else {
Write-Host "Not installed" -ForegroundColor Yellow
}
}
function Build-Extension {
Write-Step "Building Command Palette ($Arch)..."
Push-Location $RepoRoot
dotnet build src/Moltbot.CommandPalette -c Debug -p:Platform=$Arch
if ($LASTEXITCODE -ne 0) { throw "Build failed" }
Pop-Location
Write-Host "Build succeeded!" -ForegroundColor Green
}
function Deploy-Extension {
Write-Step "Deploying extension..."
$manifest = "$OutputPath\AppxManifest.xml"
if (-not (Test-Path $manifest)) {
throw "AppxManifest.xml not found at $manifest - did you build first?"
}
# Register for development (loose files, no MSIX needed)
Add-AppxPackage -Register $manifest -ForceApplicationShutdown
Write-Host "Deployed! Open Command Palette, type 'Reload', then 'Moltbot'" -ForegroundColor Green
}
function Show-Status {
Write-Step "Extension Status (Platform: $Arch)"
$pkg = Get-InstalledPackage
if ($pkg) {
Write-Host "Installed: $($pkg.PackageFullName)" -ForegroundColor Green
Write-Host "Version: $($pkg.Version)"
Write-Host "Location: $($pkg.InstallLocation)"
} else {
Write-Host "Not installed" -ForegroundColor Yellow
}
}
# Main
switch ($Action) {
'build' { Build-Extension }
'deploy' { Deploy-Extension }
'remove' { Remove-ExtensionPackage }
'status' { Show-Status }
'cycle' {
Remove-ExtensionPackage
Build-Extension
Deploy-Extension
Write-Host "`n=== In Command Palette: type 'Reload' then 'Moltbot' ===" -ForegroundColor Magenta
}
}

47
tools/icongen/Program.cs Normal file
View File

@ -0,0 +1,47 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
var sizes = new[] { (24, "Square44x44Logo.targetsize-24_altform-unplated.png"),
(88, "Square44x44Logo.scale-200.png"),
(50, "StoreLogo.png"),
(300, "Square150x150Logo.scale-200.png"),
(48, "LockScreenLogo.scale-200.png") };
var assetsPath = args[0];
foreach (var (size, name) in sizes)
{
using var bmp = CreateLobster(size);
bmp.Save(Path.Combine(assetsPath, name), ImageFormat.Png);
Console.WriteLine("Created " + name);
}
static Bitmap CreateLobster(int size)
{
var bmp = new Bitmap(size, size, PixelFormat.Format32bppArgb);
using var g = Graphics.FromImage(bmp);
g.Clear(Color.Transparent);
float s = size / 16f;
var body = Color.FromArgb(255, 255, 79, 64);
var claw = Color.FromArgb(255, 255, 119, 95);
var outline = Color.FromArgb(255, 58, 10, 13);
var eyeW = Color.FromArgb(255, 245, 251, 255);
var eyeB = Color.FromArgb(255, 8, 16, 22);
void P(int x, int y, Color c) { using var b = new SolidBrush(c); g.FillRectangle(b, (int)(x*s), (int)(y*s), (int)Math.Ceiling(s), (int)Math.Ceiling(s)); }
int[][] bd = {new[]{5,3},new[]{6,3},new[]{7,3},new[]{8,3},new[]{9,3},new[]{10,3},new[]{4,4},new[]{5,4},new[]{7,4},new[]{8,4},new[]{10,4},new[]{11,4},new[]{3,5},new[]{4,5},new[]{5,5},new[]{7,5},new[]{8,5},new[]{10,5},new[]{11,5},new[]{12,5},new[]{3,6},new[]{4,6},new[]{5,6},new[]{6,6},new[]{7,6},new[]{8,6},new[]{9,6},new[]{10,6},new[]{11,6},new[]{12,6},new[]{3,7},new[]{4,7},new[]{5,7},new[]{6,7},new[]{7,7},new[]{8,7},new[]{9,7},new[]{10,7},new[]{11,7},new[]{12,7},new[]{4,8},new[]{5,8},new[]{6,8},new[]{7,8},new[]{8,8},new[]{9,8},new[]{10,8},new[]{11,8},new[]{5,9},new[]{6,9},new[]{7,9},new[]{8,9},new[]{9,9},new[]{10,9},new[]{5,12},new[]{6,12},new[]{7,12},new[]{8,12},new[]{9,12},new[]{10,12},new[]{6,13},new[]{7,13},new[]{8,13},new[]{9,13}};
foreach (var p in bd) P(p[0], p[1], body);
int[][] cl = {new[]{1,6},new[]{2,5},new[]{2,6},new[]{2,7},new[]{13,5},new[]{13,6},new[]{13,7},new[]{14,6}};
foreach (var p in cl) P(p[0], p[1], claw);
int[][] ol = {new[]{1,5},new[]{1,7},new[]{2,4},new[]{2,8},new[]{3,3},new[]{3,9},new[]{4,2},new[]{4,10},new[]{5,2},new[]{6,2},new[]{7,2},new[]{8,2},new[]{9,2},new[]{10,2},new[]{11,2},new[]{12,3},new[]{12,9},new[]{13,4},new[]{13,8},new[]{14,5},new[]{14,7},new[]{5,11},new[]{6,11},new[]{7,11},new[]{8,11},new[]{9,11},new[]{10,11},new[]{4,12},new[]{11,12},new[]{3,13},new[]{12,13},new[]{5,14},new[]{6,14},new[]{7,14},new[]{8,14},new[]{9,14},new[]{10,14}};
foreach (var p in ol) P(p[0], p[1], outline);
P(6,4,eyeW); P(9,4,eyeW); P(6,5,eyeB); P(9,5,eyeB);
return bmp;
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="10.0.2" />
</ItemGroup>
</Project>