diff --git a/src/PersonalToolbox/App.xaml b/src/PersonalToolbox/App.xaml
index b2c7b87..bc0452e 100644
--- a/src/PersonalToolbox/App.xaml
+++ b/src/PersonalToolbox/App.xaml
@@ -1,14 +1,14 @@
-
+ xmlns:views="clr-namespace:PersonalToolbox.Views">
+
@@ -16,23 +16,344 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PersonalToolbox/App.xaml.cs b/src/PersonalToolbox/App.xaml.cs
index a85a807..b971cca 100644
--- a/src/PersonalToolbox/App.xaml.cs
+++ b/src/PersonalToolbox/App.xaml.cs
@@ -1,6 +1,10 @@
using System.Configuration;
using System.Data;
+using System.IO;
+using System.IO.Pipes;
+using System.Text;
using System.Windows;
+using System.Threading;
namespace PersonalToolbox;
@@ -9,5 +13,136 @@ namespace PersonalToolbox;
///
public partial class App : System.Windows.Application
{
+ private const string ActivatePipeName = "PersonalToolbox.Activate";
+
+ private FileStream? _instanceLockFile;
+ private CancellationTokenSource? _activationPipeCancellation;
+ private MainWindow? _mainWindow;
+
+ protected override async void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+
+ ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ if (!TryAcquireInstanceLock())
+ {
+ SignalExistingInstance();
+ Environment.Exit(0);
+ return;
+ }
+
+ var mainWindow = new MainWindow();
+ _mainWindow = mainWindow;
+ MainWindow = mainWindow;
+ StartActivationPipe();
+
+ await mainWindow.InitializeShellAsync();
+ if (!mainWindow.StartHiddenToTray)
+ {
+ mainWindow.ShowMainWindow();
+ }
+ else
+ {
+ mainWindow.RefreshTrayMenu();
+ }
+
+ await mainWindow.RunStartupTasksAsync();
+ }
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ _activationPipeCancellation?.Cancel();
+ _activationPipeCancellation?.Dispose();
+ _instanceLockFile?.Dispose();
+ base.OnExit(e);
+ }
+
+ private bool TryAcquireInstanceLock()
+ {
+ try
+ {
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ var configDirectory = Path.Combine(appData, "PersonalToolbox");
+ Directory.CreateDirectory(configDirectory);
+ var lockPath = Path.Combine(configDirectory, "PersonalToolbox.lock");
+
+ _instanceLockFile = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ _instanceLockFile.SetLength(0);
+ var processId = Encoding.UTF8.GetBytes(Environment.ProcessId.ToString());
+ _instanceLockFile.Write(processId, 0, processId.Length);
+ _instanceLockFile.Flush();
+ return true;
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return false;
+ }
+ }
+
+ private static void SignalExistingInstance()
+ {
+ for (var attempt = 0; attempt < 3; attempt++)
+ {
+ try
+ {
+ using var pipe = new NamedPipeClientStream(".", ActivatePipeName, PipeDirection.Out);
+ pipe.Connect(800);
+ using var writer = new StreamWriter(pipe, Encoding.UTF8)
+ {
+ AutoFlush = true
+ };
+ writer.WriteLine("show");
+ return;
+ }
+ catch (IOException)
+ {
+ }
+ catch (TimeoutException)
+ {
+ }
+
+ Thread.Sleep(150);
+ }
+ }
+
+ private void StartActivationPipe()
+ {
+ _activationPipeCancellation = new CancellationTokenSource();
+ _ = Task.Run(() => ListenForActivationAsync(_activationPipeCancellation.Token));
+ }
+
+ private async Task ListenForActivationAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await using var pipe = new NamedPipeServerStream(
+ ActivatePipeName,
+ PipeDirection.In,
+ 1,
+ PipeTransmissionMode.Byte,
+ PipeOptions.Asynchronous);
+
+ await pipe.WaitForConnectionAsync(cancellationToken);
+ using var reader = new StreamReader(pipe, Encoding.UTF8);
+ _ = await reader.ReadLineAsync(cancellationToken);
+
+ await Dispatcher.BeginInvoke(() => _mainWindow?.ShowMainWindow());
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
}
diff --git a/src/PersonalToolbox/MainWindow.xaml b/src/PersonalToolbox/MainWindow.xaml
index 184e0ab..3c062ad 100644
--- a/src/PersonalToolbox/MainWindow.xaml
+++ b/src/PersonalToolbox/MainWindow.xaml
@@ -1,4 +1,4 @@
-
@@ -20,8 +19,8 @@
-
@@ -78,8 +77,8 @@
-
@@ -93,7 +92,7 @@
+ FontFamily="Segoe MDL2 Assets"
+ FontSize="16"
+ Foreground="{DynamicResource PrimaryBrush}" />
@@ -179,12 +178,23 @@
+
+
+