From 7aa039c6fe5e33803dc63243e6cbd5662f1c25ae Mon Sep 17 00:00:00 2001 From: kjerk Date: Sat, 14 Feb 2026 04:41:32 -0800 Subject: [PATCH 1/4] Add WIP on cli options plus single instance triggers --- src/TimeToKill.App/App.axaml.cs | 38 ++++++- src/TimeToKill.App/Cli/CliOptions.cs | 19 ++++ src/TimeToKill.App/Cli/CommandHandler.cs | 64 ++++++++++++ src/TimeToKill.App/Cli/IdentifierResolver.cs | 63 ++++++++++++ src/TimeToKill.App/Cli/IpcProtocol.cs | 24 +++++ .../Cli/SingleInstanceManager.cs | 99 +++++++++++++++++++ src/TimeToKill.App/Program.cs | 45 ++++++++- src/TimeToKill.App/TimeToKill.App.csproj | 7 +- src/TimeToKill.Shared/Tools/NativeMethods.cs | 13 +++ .../Tools/ProcessNameHelper.cs | 1 - 10 files changed, 364 insertions(+), 9 deletions(-) create mode 100644 src/TimeToKill.App/Cli/CliOptions.cs create mode 100644 src/TimeToKill.App/Cli/CommandHandler.cs create mode 100644 src/TimeToKill.App/Cli/IdentifierResolver.cs create mode 100644 src/TimeToKill.App/Cli/IpcProtocol.cs create mode 100644 src/TimeToKill.App/Cli/SingleInstanceManager.cs create mode 100644 src/TimeToKill.Shared/Tools/NativeMethods.cs diff --git a/src/TimeToKill.App/App.axaml.cs b/src/TimeToKill.App/App.axaml.cs index d6941e8..2995cad 100644 --- a/src/TimeToKill.App/App.axaml.cs +++ b/src/TimeToKill.App/App.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Media.Imaging; using Avalonia.Threading; +using TimeToKill.App.Cli; using TimeToKill.App.Helpers; using TimeToKill.App.Services; using TimeToKill.App.Themes; @@ -27,7 +28,8 @@ public partial class App : Application private TrayIcon _trayIcon; private Bitmap _currentTrayBitmap; private DispatcherTimer _flashTimer; - + private CommandHandler _commandHandler; + private const string DefaultTheme = "default-dark"; public override void Initialize() @@ -52,13 +54,21 @@ public override void OnFrameworkInitializationCompleted() MainViewModel = new MainWindowViewModel(_presetRepository, _timerManager); MainWindowInstance = new MainWindow { DataContext = MainViewModel }; - + + // IPC: start pipe server and wire up command handling + _commandHandler = new CommandHandler(_presetRepository, MainViewModel); + if (Program.InstanceManager != null) { + Program.InstanceManager.CommandReceived += OnIpcCommandReceived; + Program.InstanceManager.StartPipeServer(); + } + ProcessStartupCommands(); + SetupTrayIcon(); - + // Timer events for tray updates _timerManager.TimerTick += OnTimerTick; _timerManager.TimerCompleted += OnTimerCompleted; - + desktop.MainWindow = MainWindowInstance; desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; } @@ -162,10 +172,30 @@ public static void ShowMainWindow() public static void ExitApplication() { + Program.InstanceManager?.Dispose(); + if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.Shutdown(); } } + + private void OnIpcCommandReceived(object sender, IpcCommand command) + { + _commandHandler.Process(command); + } + + private void ProcessStartupCommands() + { + var opts = Program.StartupOptions; + if (opts == null || !opts.HasCommands) + return; + + Dispatcher.UIThread.Post(() => { + if (opts.StartTimers?.Any() == true) { + _commandHandler.Process(IpcCommand.StartTimer(opts.StartTimers)); + } + }); + } private void DisableAvaloniaDataAnnotationValidation() { diff --git a/src/TimeToKill.App/Cli/CliOptions.cs b/src/TimeToKill.App/Cli/CliOptions.cs new file mode 100644 index 0000000..dab12e7 --- /dev/null +++ b/src/TimeToKill.App/Cli/CliOptions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using CommandLine; + +namespace TimeToKill.App.Cli; + +public class CliOptions +{ + [Option("start-timer", Required = false, HelpText = "Start timer(s) by GUID or unique name. Repeatable.")] + public IEnumerable StartTimers { get; set; } + + // Future: + // [Option("stop-timer", Required = false, HelpText = "Stop timer(s) by GUID or unique name. Repeatable.")] + // public IEnumerable StopTimers { get; set; } + + // [Option("list", Required = false, Default = false, HelpText = "List all configured timer presets.")] + // public bool List { get; set; } + + public bool HasCommands => StartTimers?.GetEnumerator().MoveNext() == true; +} diff --git a/src/TimeToKill.App/Cli/CommandHandler.cs b/src/TimeToKill.App/Cli/CommandHandler.cs new file mode 100644 index 0000000..c93fd88 --- /dev/null +++ b/src/TimeToKill.App/Cli/CommandHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using Avalonia.Threading; +using TimeToKill.App.Services; +using TimeToKill.App.ViewModels; + +namespace TimeToKill.App.Cli; + +public class CommandHandler +{ + private readonly PresetRepository _presetRepository; + private readonly MainWindowViewModel _viewModel; + + public CommandHandler(PresetRepository presetRepository, MainWindowViewModel viewModel) + { + _presetRepository = presetRepository; + _viewModel = viewModel; + } + + public void Process(IpcCommand command) + { + switch (command.CommandType) { + case IpcCommandType.StartTimer: + HandleStartTimer(command); + break; + } + } + + private void HandleStartTimer(IpcCommand command) + { + var presets = _presetRepository.LoadPresets(); + var resolver = new IdentifierResolver(presets); + + foreach (var identifier in command.Arguments) { + var (success, preset, error) = resolver.Resolve(identifier); + + if (!success) { + Notify(error); + continue; + } + + Dispatcher.UIThread.Post(() => { + var vm = _viewModel.Presets.FirstOrDefault(p => p.Id == preset.Id); + if (vm != null) { + vm.StartCommand.Execute(null); + var name = preset.DisplayLabel; + if (string.IsNullOrWhiteSpace(name)) + name = preset.ProcessName; + Notify($"Started timer: {name}"); + } else { + Notify($"Preset found but not loaded: {identifier}"); + } + }); + } + } + + private void Notify(string message) + { + Dispatcher.UIThread.Post(() => { + _viewModel.LastNotification = message; + _viewModel.HasNotification = true; + }); + } +} diff --git a/src/TimeToKill.App/Cli/IdentifierResolver.cs b/src/TimeToKill.App/Cli/IdentifierResolver.cs new file mode 100644 index 0000000..674cd84 --- /dev/null +++ b/src/TimeToKill.App/Cli/IdentifierResolver.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TimeToKill.Models; +using TimeToKill.Tools; + +namespace TimeToKill.App.Cli; + +public class IdentifierResolver +{ + private readonly List _presets; + + public IdentifierResolver(List presets) + { + _presets = presets; + } + + public (bool Success, TimerPreset Preset, string Error) Resolve(string identifier) + { + if (string.IsNullOrWhiteSpace(identifier)) + return (false, null, "Identifier is empty"); + + // 1. Try Guid + if (Guid.TryParse(identifier, out var guid)) { + var preset = _presets.FirstOrDefault(p => p.Id == guid); + return preset != null + ? (true, preset, null) + : (false, null, $"No preset with ID '{identifier}'"); + } + + // 2. DisplayLabel (case-insensitive, skip empty) + var labelMatches = _presets + .Where(p => !string.IsNullOrWhiteSpace(p.DisplayLabel) && p.DisplayLabel.Equals(identifier, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (labelMatches.Count == 1) + return (true, labelMatches[0], null); + if (labelMatches.Count > 1) + return (false, null, $"Ambiguous: {labelMatches.Count} presets match label '{identifier}'"); + + // 3. ProcessName base name without extension (e.g. "discord" matches "discord.exe") + var baseMatches = _presets + .Where(p => ProcessNameHelper.GetBaseNameWithoutExtension(p.ProcessName).Equals(identifier, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (baseMatches.Count == 1) + return (true, baseMatches[0], null); + if (baseMatches.Count > 1) + return (false, null, $"Ambiguous: {baseMatches.Count} presets match process '{identifier}'"); + + // 4. ProcessName exe name (e.g. "discord.exe" matches "discord.exe") + var exeMatches = _presets + .Where(p => ProcessNameHelper.GetExeName(p.ProcessName).Equals(identifier, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (exeMatches.Count == 1) + return (true, exeMatches[0], null); + if (exeMatches.Count > 1) + return (false, null, $"Ambiguous: {exeMatches.Count} presets match exe '{identifier}'"); + + return (false, null, $"No preset found matching '{identifier}'"); + } +} diff --git a/src/TimeToKill.App/Cli/IpcProtocol.cs b/src/TimeToKill.App/Cli/IpcProtocol.cs new file mode 100644 index 0000000..a722d37 --- /dev/null +++ b/src/TimeToKill.App/Cli/IpcProtocol.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TimeToKill.App.Cli; + +public enum IpcCommandType +{ + StartTimer = 1, + // Future: StopTimer = 2, ListTimers = 3 +} + +public class IpcCommand +{ + public IpcCommandType CommandType { get; set; } + public List Arguments { get; set; } = new(); + + public static IpcCommand StartTimer(IEnumerable identifiers) + { + return new IpcCommand { + CommandType = IpcCommandType.StartTimer, + Arguments = identifiers.ToList() + }; + } +} diff --git a/src/TimeToKill.App/Cli/SingleInstanceManager.cs b/src/TimeToKill.App/Cli/SingleInstanceManager.cs new file mode 100644 index 0000000..1b24c75 --- /dev/null +++ b/src/TimeToKill.App/Cli/SingleInstanceManager.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace TimeToKill.App.Cli; + +public class SingleInstanceManager : IDisposable +{ + private const string MutexName = "TimeToKill_SingleInstance"; + private const string PipeName = "TimeToKill_Command"; + private const int PipeConnectionTimeoutMs = 3000; + + private Mutex _mutex; + private bool _isFirstInstance; + private CancellationTokenSource _pipeCancellation; + private Task _pipeServerTask; + + public event EventHandler CommandReceived; + + public bool IsFirstInstance => _isFirstInstance; + + public bool TryAcquireInstance() + { + _mutex = new Mutex(true, MutexName, out _isFirstInstance); + return _isFirstInstance; + } + + public void StartPipeServer() + { + if (!_isFirstInstance) + throw new InvalidOperationException("Cannot start pipe server on secondary instance."); + + _pipeCancellation = new CancellationTokenSource(); + _pipeServerTask = Task.Run(() => PipeServerLoop(_pipeCancellation.Token)); + } + + private async Task PipeServerLoop(CancellationToken cancellation) + { + while (!cancellation.IsCancellationRequested) { + try { + await using var server = new NamedPipeServerStream( + PipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + await server.WaitForConnectionAsync(cancellation); + await HandleClientConnection(server); + } catch (OperationCanceledException) { + break; + } catch { + // Connection-level error — backoff and re-listen + try { await Task.Delay(1000, cancellation); } + catch (OperationCanceledException) { break; } + } + } + } + + private async Task HandleClientConnection(NamedPipeServerStream server) + { + try { + using var reader = new StreamReader(server, Encoding.UTF8); + var json = await reader.ReadToEndAsync(); + var command = JsonSerializer.Deserialize(json); + if (command != null) { + CommandReceived?.Invoke(this, command); + } + } catch { + // Malformed message — ignore + } + } + + public static async Task SendCommandToRunningInstance(IpcCommand command) + { + try { + await using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); + await client.ConnectAsync(PipeConnectionTimeoutMs); + var json = JsonSerializer.Serialize(command); + await using var writer = new StreamWriter(client, Encoding.UTF8); + await writer.WriteAsync(json); + await writer.FlushAsync(); + return true; + } catch { + return false; + } + } + + public void Dispose() + { + _pipeCancellation?.Cancel(); + try { _pipeServerTask?.Wait(TimeSpan.FromSeconds(2)); } catch { } + _pipeCancellation?.Dispose(); + + if (_isFirstInstance) { + try { _mutex?.ReleaseMutex(); } catch { } + } + _mutex?.Dispose(); + } +} diff --git a/src/TimeToKill.App/Program.cs b/src/TimeToKill.App/Program.cs index 7fec3bb..74985a3 100644 --- a/src/TimeToKill.App/Program.cs +++ b/src/TimeToKill.App/Program.cs @@ -1,16 +1,59 @@ using System; +using System.Linq; +using System.Threading.Tasks; using Avalonia; +using CommandLine; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; +using TimeToKill.App.Cli; +using TimeToKill.Tools; namespace TimeToKill.App; public sealed class Program { + public static CliOptions StartupOptions { get; private set; } + public static SingleInstanceManager InstanceManager { get; private set; } + [STAThread] public static void Main(string[] args) { - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + // Attach to parent console so --help output is visible from terminals. + // No-op when double-clicked (no parent console to attach to). + Console.WriteLine("saklfjhnafskjfasn"); + NativeMethods.AttachToParentConsole(); + Console.WriteLine("dfohgjndfkhndfh"); + + var parseResult = Parser.Default.ParseArguments(args); + + parseResult.WithParsed(options => { + InstanceManager = new SingleInstanceManager(); + + if (!InstanceManager.TryAcquireInstance()) { + // Another instance is running — send commands and exit + if (options.HasCommands) { + SendCommandsAndExit(options).GetAwaiter().GetResult(); + } + InstanceManager.Dispose(); + InstanceManager = null; + return; + } + + // First instance — store options and start Avalonia + StartupOptions = options; + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + // Avalonia has exited — clean up + InstanceManager.Dispose(); + InstanceManager = null; + }); + } + + private static async Task SendCommandsAndExit(CliOptions options) + { + if (options.StartTimers?.Any() == true) { + await SingleInstanceManager.SendCommandToRunningInstance(IpcCommand.StartTimer(options.StartTimers)); + } } public static AppBuilder BuildAvaloniaApp() diff --git a/src/TimeToKill.App/TimeToKill.App.csproj b/src/TimeToKill.App/TimeToKill.App.csproj index dc0a2a0..2ac935d 100644 --- a/src/TimeToKill.App/TimeToKill.App.csproj +++ b/src/TimeToKill.App/TimeToKill.App.csproj @@ -10,12 +10,12 @@ - - + + - + @@ -29,6 +29,7 @@ + diff --git a/src/TimeToKill.Shared/Tools/NativeMethods.cs b/src/TimeToKill.Shared/Tools/NativeMethods.cs new file mode 100644 index 0000000..2642d5a --- /dev/null +++ b/src/TimeToKill.Shared/Tools/NativeMethods.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace TimeToKill.Tools; + +public static class NativeMethods +{ + private const int AttachParentProcess = -1; + + [DllImport("kernel32.dll")] + private static extern bool AttachConsole(int dwProcessId); + + public static bool AttachToParentConsole() => AttachConsole(AttachParentProcess); +} diff --git a/src/TimeToKill.Shared/Tools/ProcessNameHelper.cs b/src/TimeToKill.Shared/Tools/ProcessNameHelper.cs index 60cb3f8..37cb03f 100644 --- a/src/TimeToKill.Shared/Tools/ProcessNameHelper.cs +++ b/src/TimeToKill.Shared/Tools/ProcessNameHelper.cs @@ -4,7 +4,6 @@ namespace TimeToKill.Tools; public static class ProcessNameHelper { - // "C:/Apps/discord.exe" -> "discord.exe", "discord.exe" -> "discord.exe" public static string GetExeName(string processName) { From f82256f4e5a4dd378ad70a30e1ef172c0572c419 Mon Sep 17 00:00:00 2001 From: kjerk Date: Sat, 14 Feb 2026 04:57:55 -0800 Subject: [PATCH 2/4] Add WIP on cli --- src/TimeToKill.App/Cli/CliOptions.cs | 3 ++ src/TimeToKill.App/Program.cs | 51 ++++++++++++++-------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/TimeToKill.App/Cli/CliOptions.cs b/src/TimeToKill.App/Cli/CliOptions.cs index dab12e7..a4cf5d6 100644 --- a/src/TimeToKill.App/Cli/CliOptions.cs +++ b/src/TimeToKill.App/Cli/CliOptions.cs @@ -16,4 +16,7 @@ public class CliOptions // public bool List { get; set; } public bool HasCommands => StartTimers?.GetEnumerator().MoveNext() == true; + + [Option("help", Default = false, HelpText = "Display this help text.")] + public bool Help { get; set; } } diff --git a/src/TimeToKill.App/Program.cs b/src/TimeToKill.App/Program.cs index 74985a3..df5e44e 100644 --- a/src/TimeToKill.App/Program.cs +++ b/src/TimeToKill.App/Program.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using Avalonia; using CommandLine; +using CommandLine.Text; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using TimeToKill.App.Cli; @@ -18,35 +19,35 @@ public sealed class Program [STAThread] public static void Main(string[] args) { - // Attach to parent console so --help output is visible from terminals. - // No-op when double-clicked (no parent console to attach to). - Console.WriteLine("saklfjhnafskjfasn"); - NativeMethods.AttachToParentConsole(); - Console.WriteLine("dfohgjndfkhndfh"); - var parseResult = Parser.Default.ParseArguments(args); + if (parseResult.Tag == ParserResultType.NotParsed) + return; + + var options = parseResult.Value; + + if (options.Help) { + NativeMethods.AttachToParentConsole(); + var helpText = HelpText.AutoBuild(parseResult); + Console.WriteLine(helpText); + return; + } + + InstanceManager = new SingleInstanceManager(); - parseResult.WithParsed(options => { - InstanceManager = new SingleInstanceManager(); - - if (!InstanceManager.TryAcquireInstance()) { - // Another instance is running — send commands and exit - if (options.HasCommands) { - SendCommandsAndExit(options).GetAwaiter().GetResult(); - } - InstanceManager.Dispose(); - InstanceManager = null; - return; + if (!InstanceManager.TryAcquireInstance()) { + if (options.HasCommands) { + SendCommandsAndExit(options).GetAwaiter().GetResult(); } - - // First instance — store options and start Avalonia - StartupOptions = options; - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - - // Avalonia has exited — clean up InstanceManager.Dispose(); InstanceManager = null; - }); + return; + } + + StartupOptions = options; + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + InstanceManager.Dispose(); + InstanceManager = null; } private static async Task SendCommandsAndExit(CliOptions options) From f413182b8bccc49e470fee3948691ea95f24cd68 Mon Sep 17 00:00:00 2001 From: kjerk Date: Sat, 14 Feb 2026 05:14:53 -0800 Subject: [PATCH 3/4] Allow for selective attaching of console, get manual print of help to work. --- src/TimeToKill.App/Cli/CliOptions.cs | 27 +++++++++++++++++++++++++-- src/TimeToKill.App/Program.cs | 15 +++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/TimeToKill.App/Cli/CliOptions.cs b/src/TimeToKill.App/Cli/CliOptions.cs index a4cf5d6..47212a1 100644 --- a/src/TimeToKill.App/Cli/CliOptions.cs +++ b/src/TimeToKill.App/Cli/CliOptions.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using CommandLine; +using CommandLine.Text; namespace TimeToKill.App.Cli; @@ -15,8 +17,29 @@ public class CliOptions // [Option("list", Required = false, Default = false, HelpText = "List all configured timer presets.")] // public bool List { get; set; } - public bool HasCommands => StartTimers?.GetEnumerator().MoveNext() == true; - [Option("help", Default = false, HelpText = "Display this help text.")] public bool Help { get; set; } + + public bool HasCommands => StartTimers?.GetEnumerator().MoveNext() == true; + + private static Parser CreateParser() => new Parser(s => { + s.AutoHelp = false; + s.AutoVersion = false; + }); + + public static CliOptions Parse(string[] args) + { + var parseResult = CreateParser().ParseArguments(args); + if (parseResult.Tag == ParserResultType.NotParsed) + return null; + return parseResult.Value; + } + + public static void PrintHelp() + { + var result = CreateParser().ParseArguments(Array.Empty()); + var helpText = new HelpText("TimeToKill"); + helpText.AddOptions(result); + Console.WriteLine(helpText); + } } diff --git a/src/TimeToKill.App/Program.cs b/src/TimeToKill.App/Program.cs index df5e44e..a2618a2 100644 --- a/src/TimeToKill.App/Program.cs +++ b/src/TimeToKill.App/Program.cs @@ -2,8 +2,6 @@ using System.Linq; using System.Threading.Tasks; using Avalonia; -using CommandLine; -using CommandLine.Text; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using TimeToKill.App.Cli; @@ -19,19 +17,16 @@ public sealed class Program [STAThread] public static void Main(string[] args) { - var parseResult = Parser.Default.ParseArguments(args); - if (parseResult.Tag == ParserResultType.NotParsed) + var options = CliOptions.Parse(args); + if (options == null) return; - - var options = parseResult.Value; - + if (options.Help) { NativeMethods.AttachToParentConsole(); - var helpText = HelpText.AutoBuild(parseResult); - Console.WriteLine(helpText); + CliOptions.PrintHelp(); return; } - + InstanceManager = new SingleInstanceManager(); if (!InstanceManager.TryAcquireInstance()) { From 4cf8f0bbf69ba90ea448b6b5777b83c33c06fc9f Mon Sep 17 00:00:00 2001 From: kjerk Date: Sat, 14 Feb 2026 05:30:40 -0800 Subject: [PATCH 4/4] Disable cli attach and help for now. --- src/TimeToKill.App/Cli/CliOptions.cs | 14 +++++++------- src/TimeToKill.App/Program.cs | 24 +++++++++--------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/TimeToKill.App/Cli/CliOptions.cs b/src/TimeToKill.App/Cli/CliOptions.cs index 47212a1..46fbc4d 100644 --- a/src/TimeToKill.App/Cli/CliOptions.cs +++ b/src/TimeToKill.App/Cli/CliOptions.cs @@ -9,24 +9,24 @@ public class CliOptions { [Option("start-timer", Required = false, HelpText = "Start timer(s) by GUID or unique name. Repeatable.")] public IEnumerable StartTimers { get; set; } - + // Future: // [Option("stop-timer", Required = false, HelpText = "Stop timer(s) by GUID or unique name. Repeatable.")] // public IEnumerable StopTimers { get; set; } - + // [Option("list", Required = false, Default = false, HelpText = "List all configured timer presets.")] // public bool List { get; set; } - + [Option("help", Default = false, HelpText = "Display this help text.")] public bool Help { get; set; } - + public bool HasCommands => StartTimers?.GetEnumerator().MoveNext() == true; - + private static Parser CreateParser() => new Parser(s => { s.AutoHelp = false; s.AutoVersion = false; }); - + public static CliOptions Parse(string[] args) { var parseResult = CreateParser().ParseArguments(args); @@ -34,7 +34,7 @@ public static CliOptions Parse(string[] args) return null; return parseResult.Value; } - + public static void PrintHelp() { var result = CreateParser().ParseArguments(Array.Empty()); diff --git a/src/TimeToKill.App/Program.cs b/src/TimeToKill.App/Program.cs index a2618a2..fbb28ac 100644 --- a/src/TimeToKill.App/Program.cs +++ b/src/TimeToKill.App/Program.cs @@ -5,7 +5,6 @@ using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using TimeToKill.App.Cli; -using TimeToKill.Tools; namespace TimeToKill.App; @@ -13,49 +12,44 @@ public sealed class Program { public static CliOptions StartupOptions { get; private set; } public static SingleInstanceManager InstanceManager { get; private set; } - + [STAThread] public static void Main(string[] args) { var options = CliOptions.Parse(args); if (options == null) return; - - if (options.Help) { - NativeMethods.AttachToParentConsole(); - CliOptions.PrintHelp(); - return; - } - + InstanceManager = new SingleInstanceManager(); - + if (!InstanceManager.TryAcquireInstance()) { if (options.HasCommands) { SendCommandsAndExit(options).GetAwaiter().GetResult(); } + InstanceManager.Dispose(); InstanceManager = null; return; } - + StartupOptions = options; BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - + InstanceManager.Dispose(); InstanceManager = null; } - + private static async Task SendCommandsAndExit(CliOptions options) { if (options.StartTimers?.Any() == true) { await SingleInstanceManager.SendCommandToRunningInstance(IpcCommand.StartTimer(options.StartTimers)); } } - + public static AppBuilder BuildAvaloniaApp() { IconProvider.Current.Register(); - + return AppBuilder.Configure() .UsePlatformDetect() .WithInterFont()