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
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ Behaviour:
- **Diff viewer** selects the built-in viewer, a detected external program, or an executable chosen by the user.
- **Keyboard shortcut...** opens the shortcut recorder without changing the active shortcut unless Save succeeds.
- **Start at login** toggles per-user startup registration (section 24).
- **About ClipDiff** opens a reusable native window showing the application icon, the product version and short source commit hash, Stuart Dunkeld, `stuartd.dev`, and the source repository link.
- **About ClipDiff** opens a reusable native window showing the application icon, the last source commit (short hash) and build date/time in UTC (embedded at build time), without a product version, Stuart Dunkeld, `stuartd.dev`, and the source repository link.
- **Quit ClipDiff** removes the tray icon, unregisters native listeners/hotkeys, clears references to captured text, and shuts down.
- Double-clicking the tray icon may invoke **Show Diff** when available.
- The ordinary native right-click context-menu behaviour is sufficient.
Expand Down
7 changes: 7 additions & 0 deletions src/ClipDiff.Windows/ClipDiff.Windows.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets/ClipDiff.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<!-- Override BuildDateUtc for reproducible builds; otherwise stamp this build in UTC. -->
<BuildDateUtc Condition="'$(BuildDateUtc)' == ''">$([System.DateTime]::UtcNow.ToString("O"))</BuildDateUtc>
</PropertyGroup>
<ItemGroup>
<AssemblyMetadata Include="BuildDateUtc" Value="$(BuildDateUtc)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ClipDiff.Core/ClipDiff.Core.csproj" />
</ItemGroup>
Expand Down
29 changes: 29 additions & 0 deletions src/ClipDiff.Windows/Views/AboutBuildInfoFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Globalization;

namespace ClipDiff.Windows.Views;

internal static class AboutBuildInfoFormatter
{
private const int ShortCommitLength = 7;

public static string Format(string? informationalVersion, string? buildDateUtc)
{
var parts = informationalVersion?.Split('+', 2);
var commit = parts is { Length: 2 }
? parts[1].Split('.').LastOrDefault(IsCommitHash)
: null;
var shortCommit = commit is null ? "Unknown" : commit[..ShortCommitLength];
var buildDate = DateTimeOffset.TryParse(buildDateUtc, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal, out var timestamp)
? timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm 'UTC'", CultureInfo.InvariantCulture)
: "Unknown";

return $"Last commit: {shortCommit}\nBuild date: {buildDate}";
}

private static bool IsCommitHash(string value) =>
value.Length >= ShortCommitLength && value.All(IsHexDigit);

private static bool IsHexDigit(char value) =>
value is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F';
}
34 changes: 0 additions & 34 deletions src/ClipDiff.Windows/Views/AboutVersionFormatter.cs

This file was deleted.

5 changes: 3 additions & 2 deletions src/ClipDiff.Windows/Views/AboutWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@

<StackPanel Grid.Row="0" Grid.Column="1">
<TextBlock Text="ClipDiff" FontSize="26" FontWeight="SemiBold" />
<TextBlock x:Name="VersionText"
<TextBlock x:Name="BuildInfoText"
Margin="0,2,0,16"
Opacity="0.72" />
Opacity="0.72"
TextWrapping="Wrap" />
<TextBlock Text="A minimal clipboard comparison tool."
Margin="0,0,0,16"
TextWrapping="Wrap" />
Expand Down
113 changes: 58 additions & 55 deletions src/ClipDiff.Windows/Views/AboutWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,69 +10,72 @@ namespace ClipDiff.Windows.Views;

public partial class AboutWindow : Window
{
public AboutWindow()
{
InitializeComponent();
VersionText.Text = $"Version {GetDisplayVersion()}";
}
public AboutWindow()
{
InitializeComponent();
BuildInfoText.Text = GetBuildInfo();
}

public bool AllowClose { get; set; }
public bool AllowClose { get; set; }

private void OnSourceInitialized(object? sender, EventArgs args)
{
var windowHandle = new WindowInteropHelper(this).Handle;
var enabled = 1;
var result = NativeMethods.DwmSetWindowAttribute(
windowHandle,
NativeMethods.DwmwaUseImmersiveDarkMode,
ref enabled,
sizeof(int));
private void OnSourceInitialized(object? sender, EventArgs args)
{
var windowHandle = new WindowInteropHelper(this).Handle;
var enabled = 1;
var result = NativeMethods.DwmSetWindowAttribute(
windowHandle,
NativeMethods.DwmwaUseImmersiveDarkMode,
ref enabled,
sizeof(int));

if (result != 0)
{
NativeMethods.DwmSetWindowAttribute(
windowHandle,
NativeMethods.DwmwaUseImmersiveDarkModeBefore20H1,
ref enabled,
sizeof(int));
}
}
if (result != 0)
{
NativeMethods.DwmSetWindowAttribute(
windowHandle,
NativeMethods.DwmwaUseImmersiveDarkModeBefore20H1,
ref enabled,
sizeof(int));
}
}

private static string GetDisplayVersion()
{
var assembly = typeof(AboutWindow).Assembly;

var informationalVersion = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
private static string GetBuildInfo()
{
var assembly = typeof(AboutWindow).Assembly;

return AboutVersionFormatter.Format(informationalVersion, assembly.GetName().Version);
}
var informationalVersion = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;

private void OnLinkRequestNavigate(object sender, RequestNavigateEventArgs args)
{
try
{
Process.Start(new ProcessStartInfo(args.Uri.AbsoluteUri) { UseShellExecute = true });
}
catch (Exception exception) when (exception is Win32Exception or InvalidOperationException or NotSupportedException)
{
System.Media.SystemSounds.Beep.Play();
}
var buildDate = assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(attribute => attribute.Key == "BuildDateUtc")?.Value;

args.Handled = true;
}
return AboutBuildInfoFormatter.Format(informationalVersion, buildDate);
}

private void OnCloseClick(object sender, RoutedEventArgs args) => Hide();
private void OnLinkRequestNavigate(object sender, RequestNavigateEventArgs args)
{
try
{
Process.Start(new ProcessStartInfo(args.Uri.AbsoluteUri) { UseShellExecute = true });
}
catch (Exception exception) when (exception is Win32Exception or InvalidOperationException or NotSupportedException)
{
System.Media.SystemSounds.Beep.Play();
}

private void OnClosing(object? sender, CancelEventArgs args)
{
if (AllowClose)
{
return;
}
args.Handled = true;
}

args.Cancel = true;
Hide();
}
private void OnCloseClick(object sender, RoutedEventArgs args) => Hide();

private void OnClosing(object? sender, CancelEventArgs args)
{
if (AllowClose)
{
return;
}

args.Cancel = true;
Hide();
}
}
60 changes: 60 additions & 0 deletions tests/ClipDiff.Windows.Tests/AboutBuildInfoFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using ClipDiff.Windows.Views;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ClipDiff.Windows.Tests;

[TestClass]
public sealed class AboutBuildInfoFormatterTests
{
[TestMethod]
[DataRow("1.0+753aab75f36e959cf8dfa6d72a2175110c9ed608")]
[DataRow("2.3.4-preview.1+local.753aab75f36e959cf8dfa6d72a2175110c9ed608")]
public void ShowsCommitAndBuildDateWithoutVersion(string informationalVersion)
{
var result = AboutBuildInfoFormatter.Format(informationalVersion, "2026-10-19T10:43:44.0000000Z");

Assert.AreEqual("Last commit: 753aab7\nBuild date: 2026-10-19 10:43 UTC", result);
}

[TestMethod]
[DataRow(null)]
[DataRow("1.0")]
[DataRow("1.0+preview")]
public void MissingCommitIsExplicit(string? informationalVersion)
{
var result = AboutBuildInfoFormatter.Format(informationalVersion, "2026-10-19T10:43:44Z");

Assert.AreEqual("Last commit: Unknown\nBuild date: 2026-10-19 10:43 UTC", result);
}

[TestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow("invalid")]
public void MissingOrInvalidBuildDateIsExplicit(string? buildDate)
{
var result = AboutBuildInfoFormatter.Format("1.0+753aab75f36e959cf8dfa6d72a2175110c9ed608", buildDate);

Assert.AreEqual("Last commit: 753aab7\nBuild date: Unknown", result);
}

[TestMethod]
public void NormalizesBuildDateToUtc()
{
var result = AboutBuildInfoFormatter.Format(null, "2026-10-19T11:43:44+01:00");

Assert.AreEqual("Last commit: Unknown\nBuild date: 2026-10-19 10:43 UTC", result);
}

[TestMethod]
public void BuildContainsTimestampMetadata()
{
var metadata = typeof(AboutBuildInfoFormatter).Assembly
.GetCustomAttributes(typeof(System.Reflection.AssemblyMetadataAttribute), false)
.Cast<System.Reflection.AssemblyMetadataAttribute>()
.Single(attribute => attribute.Key == "BuildDateUtc");

Assert.IsTrue(DateTimeOffset.TryParse(metadata.Value, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeUniversal, out _));
}
}
44 changes: 0 additions & 44 deletions tests/ClipDiff.Windows.Tests/AboutVersionFormatterTests.cs

This file was deleted.