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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ jobs:
--mainExe Snipdeck.App.exe `
--packTitle "${{ env.PACK_TITLE }}" `
--packAuthors "${{ env.PACK_AUTHORS }}" `
--icon src/Snipdeck.App/Assets/Snipdeck.ico `
--channel $channel `
--outputDir Releases

Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Snipdeck branding.** The app now ships with its own icon — a faceted green
chevron — replacing the default .NET exe icon, the generated identicon in the
system tray, and the blank window icon. The icon appears on the executable,
taskbar, Start menu, Alt-Tab, window title bar (beside the app name), system
tray and the Velopack installer. The README carries the new tile logo, and the
source SVGs live in `designs/`.

### Changed
- The "Next Iteration" eyebrow on the Home hero now uses the primary text
colour (white in Dark theme) instead of the muted secondary grey.

## [0.1.0] - 2026-06-01

### Added
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<p align="center">
<img src="designs/snipdeck-tile.svg" alt="Snipdeck" width="160">
</p>

# Snipdeck

A native Windows desktop app for managing parameterised CLI command snippets
Expand Down
73 changes: 73 additions & 0 deletions designs/snipdeck-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions designs/snipdeck-tile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions src/Snipdeck.App/Assets/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Assets

## App icon

`Snipdeck.ico` is the multi-size app icon (16/20/24/32/48/64/128/256, the 256
entry PNG-compressed), rendered from `designs/snipdeck-icon.svg`. It is embedded
in the exe (`<ApplicationIcon>`), set on the window (`AppWindow.SetIcon` +
`SetTaskbarIcon`), used by the tray icon and passed to `vpk pack --icon` for
the installer.

`TitleBarGlyph.png` is the same glyph at natural aspect, 64 px tall, shown at
16 logical px in the custom title bar (`rsvg-convert -h 64
designs/snipdeck-icon.svg -o TitleBarGlyph.png`).

To regenerate after the SVG changes (the glyph is 430×507, so each render is
height-fitted and centred on a square page):

```sh
for s in 16 20 24 32 48 64 128 256; do
left=$(awk "BEGIN{printf \"%.2f\", ($s-430/507*$s)/2}")
rsvg-convert -h $s --page-width $s --page-height $s --left $left --top 0 \
designs/snipdeck-icon.svg -o glyph-$s.png
done
icotool -c -o src/Snipdeck.App/Assets/Snipdeck.ico \
glyph-16.png glyph-20.png glyph-24.png glyph-32.png glyph-48.png \
glyph-64.png glyph-128.png -r glyph-256.png
```

## Home hero images (theme-specific)

The Home page hero banner uses a theme-specific image:
Expand Down
Binary file added src/Snipdeck.App/Assets/Snipdeck.ico
Binary file not shown.
Binary file added src/Snipdeck.App/Assets/TitleBarGlyph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/Snipdeck.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
ToolTipService.ToolTip="Toggle navigation">
<FontIcon Glyph="&#xE700;" FontSize="16" />
</Button>
<Image Source="ms-appx:///Assets/TitleBarGlyph.png"
Height="16"
VerticalAlignment="Center"
Margin="4,0,0,0" />
<TextBlock x:Name="AppTitleTextBlock"
Text="Snipdeck"
Style="{StaticResource CaptionTextBlockStyle}"
Expand Down
8 changes: 8 additions & 0 deletions src/Snipdeck.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ public MainWindow(AppConfig config, ShellPage shellPage)

InitializeComponent();

// Window icon. The exe resource icon covers shortcuts; AppWindow
// needs the file set explicitly — and SetIcon alone does not
// reliably reach the taskbar button, hence the explicit
// SetTaskbarIcon as well.
var appIconPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Assets", "Snipdeck.ico");
AppWindow.SetIcon(appIconPath);
AppWindow.SetTaskbarIcon(appIconPath);

ExtendsContentIntoTitleBar = true;
// The whole bar is the drag region. WinUI does NOT auto-exclude interactive
// children, so the centred search/switcher group is registered as a
Expand Down
65 changes: 6 additions & 59 deletions src/Snipdeck.App/Services/HNotifyIconTrayService.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,35 @@
using System.Buffers.Binary;

using H.NotifyIcon;

using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;

using Snipdeck.Core.Abstractions;
using Snipdeck.Core.Services;

namespace Snipdeck.App.Services
{
internal sealed partial class HNotifyIconTrayService : ITrayService
{
// Stable seed so the tray icon never changes between runs of Snipdeck.
private static readonly Guid _iconSeed = Guid.Parse("5147DEC0-0000-0000-0000-000000000001");
private const string _trayIconFileName = "tray-icon.ico";
private const int _trayIconPixels = 32;

private readonly IPathProvider _paths;
private TaskbarIcon? _icon;
private bool _disposed;

public HNotifyIconTrayService(IPathProvider paths)
{
ArgumentNullException.ThrowIfNull(paths);
_paths = paths;
}

public event EventHandler? ShowRequested;

public event EventHandler? ExitRequested;

public async Task InitialiseAsync()
public Task InitialiseAsync()
{
ObjectDisposedException.ThrowIf(_disposed, this);

if (_icon is not null)
{
return;
return Task.CompletedTask;
}

// H.NotifyIcon resolves IconSource by reading BitmapImage.UriSource
// and then hands the file bytes to System.Drawing.Icon — which only
// accepts ICO format. Wrap the identicon PNG in a minimal ICO
// container before writing it out.
var iconPath = await WriteTrayIconFileAsync();
// accepts ICO format. The shipped app icon is already a multi-size
// ICO, so point straight at it.
var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Snipdeck.ico");
var image = new BitmapImage(new Uri(iconPath, UriKind.Absolute));

_icon = new TaskbarIcon
Expand All @@ -56,6 +41,7 @@ public async Task InitialiseAsync()
LeftClickCommand = new RelayCommand(RaiseShowRequested),
};
_icon.ForceCreate();
return Task.CompletedTask;
}

public void Dispose()
Expand Down Expand Up @@ -103,45 +89,6 @@ private void RaiseShowRequested()
ShowRequested?.Invoke(this, EventArgs.Empty);
}

private async Task<string> WriteTrayIconFileAsync()
{
var pngBytes = IdenticonService.GeneratePng(_iconSeed, size: _trayIconPixels);
var icoBytes = WrapPngAsIco(pngBytes, _trayIconPixels, _trayIconPixels);
_ = Directory.CreateDirectory(_paths.AppDataDirectory);
var path = Path.Combine(_paths.AppDataDirectory, _trayIconFileName);
await File.WriteAllBytesAsync(path, icoBytes);
return path;
}

// Modern Windows accepts a PNG embedded inside an ICO container —
// just a 6-byte ICONDIR header plus a 16-byte ICONDIRENTRY pointing
// at the PNG bytes. Format ref: ICONDIR / ICONDIRENTRY on MSDN.
private static byte[] WrapPngAsIco(byte[] png, int width, int height)
{
const int headerSize = 6;
const int entrySize = 16;
const int dataOffset = headerSize + entrySize;

var ico = new byte[dataOffset + png.Length];
var span = ico.AsSpan();

// ICONDIR: reserved(0), type(1 = icon), count(1)
BinaryPrimitives.WriteUInt16LittleEndian(span[2..4], 1);
BinaryPrimitives.WriteUInt16LittleEndian(span[4..6], 1);

// ICONDIRENTRY: width/height bytes are 0 for >=256, otherwise the literal size.
ico[6] = width >= 256 ? (byte)0 : (byte)width;
ico[7] = height >= 256 ? (byte)0 : (byte)height;
// ico[8] colorCount = 0 (>= 8bpp), ico[9] reserved = 0 — already zero.
BinaryPrimitives.WriteUInt16LittleEndian(span[10..12], 1); // planes
BinaryPrimitives.WriteUInt16LittleEndian(span[12..14], 32); // bits per pixel
BinaryPrimitives.WriteUInt32LittleEndian(span[14..18], (uint)png.Length);
BinaryPrimitives.WriteUInt32LittleEndian(span[18..22], dataOffset);

Buffer.BlockCopy(png, 0, ico, dataOffset, png.Length);
return ico;
}

private sealed partial class RelayCommand(Action execute) : System.Windows.Input.ICommand
{
#pragma warning disable CS0067 // 'CanExecuteChanged' is never used — relay never changes.
Expand Down
9 changes: 9 additions & 0 deletions src/Snipdeck.App/Snipdeck.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<!-- CsWinRT generators emit unsafe code when generic interface types
(IServiceProvider, ServiceProvider) cross the WinRT ABI. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Multi-size app icon, rendered from designs/snipdeck-icon.svg.
Embedded in the exe (taskbar, Start menu, Explorer) and also
copied to output below so the title bar and tray can load it. -->
<ApplicationIcon>Assets\Snipdeck.ico</ApplicationIcon>
</PropertyGroup>

<ItemGroup>
Expand All @@ -39,6 +43,11 @@
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<Content Include="Assets\Snipdeck.ico" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Assets\TitleBarGlyph.png" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<!-- Theme hero images are optional: packaged only once the user drops them in. -->
<ItemGroup Condition="Exists('Assets\HomeHeroLight.png')">
<Content Include="Assets\HomeHeroLight.png" CopyToOutputDirectory="PreserveNewest" />
Expand Down
2 changes: 1 addition & 1 deletion src/Snipdeck.App/Views/ShellPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<StackPanel VerticalAlignment="Top" Margin="36,32,36,0" Spacing="4">
<TextBlock Text="Next Iteration"
FontSize="18"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
<TextBlock Text="Snipdeck"
Style="{ThemeResource TitleLargeTextBlockStyle}"
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
Expand Down
Loading