From 13c70ba4b683d458731cb77b8c0c8b6289ab8a67 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Tue, 24 Mar 2026 09:26:01 +0100 Subject: [PATCH 01/58] fix naming error --- ...tainfo.xml => io.github.frequency403.openssh_gui.metainfo.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename appimage/{io.github.frequency403.openssh-gui.metainfo.xml => io.github.frequency403.openssh_gui.metainfo.xml} (100%) diff --git a/appimage/io.github.frequency403.openssh-gui.metainfo.xml b/appimage/io.github.frequency403.openssh_gui.metainfo.xml similarity index 100% rename from appimage/io.github.frequency403.openssh-gui.metainfo.xml rename to appimage/io.github.frequency403.openssh_gui.metainfo.xml From 01162ecd9470411ce888733980a00c0240839d12 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Tue, 24 Mar 2026 09:32:04 +0100 Subject: [PATCH 02/58] fix icon name --- appimage/io.github.frequency403.openssh_gui.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appimage/io.github.frequency403.openssh_gui.metainfo.xml b/appimage/io.github.frequency403.openssh_gui.metainfo.xml index b2f2b9d..bdd59df 100644 --- a/appimage/io.github.frequency403.openssh_gui.metainfo.xml +++ b/appimage/io.github.frequency403.openssh_gui.metainfo.xml @@ -33,7 +33,7 @@ - opensshgui + openssh-gui From fe3ef201b611488e2b42422e7e7639afe08d5b32 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Tue, 24 Mar 2026 10:00:59 +0100 Subject: [PATCH 03/58] Fix startup error when files are not readable. (#18) --- .../Extensions/SshConfigurationExtensions.cs | 23 +++++++++++-------- .../Options/SshConfigParserOptions.cs | 8 +++++++ .../Parsers/SshConfigParser.cs | 17 +++++++++++++- .../Services/SshConfigurationProvider.cs | 2 +- .../Services/SshConfigurationSource.cs | 8 +++++++ OpenSSH_GUI/Program.cs | 9 ++++++-- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs b/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs index 53fa7a1..ca8d72e 100644 --- a/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs +++ b/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs @@ -20,9 +20,9 @@ public static class SshConfigurationExtensions /// . /// /// The . - public IConfigurationBuilder AddSshConfig(string path) + public IConfigurationBuilder AddSshConfig(string path, Action? loggingAction = null) { - return builder.AddSshConfig(null, path, false, false); + return builder.AddSshConfig(null, path, false, false, loggingAction); } /// @@ -34,9 +34,9 @@ public IConfigurationBuilder AddSshConfig(string path) /// /// Whether the file is optional. /// The . - public IConfigurationBuilder AddSshConfig(string path, bool optional) + public IConfigurationBuilder AddSshConfig(string path, bool optional, Action? loggingAction = null) { - return builder.AddSshConfig(null, path, optional, false); + return builder.AddSshConfig(null, path, optional, false, loggingAction); } /// @@ -50,9 +50,9 @@ public IConfigurationBuilder AddSshConfig(string path, bool optional) /// Whether the configuration should be reloaded if the file changes. /// The . public IConfigurationBuilder AddSshConfig(string path, bool optional, - bool reloadOnChange) + bool reloadOnChange, Action? loggingAction = null) { - return builder.AddSshConfig(null, path, optional, reloadOnChange); + return builder.AddSshConfig(null, path, optional, reloadOnChange, loggingAction); } /// @@ -67,7 +67,7 @@ public IConfigurationBuilder AddSshConfig(string path, bool optional, /// Whether the configuration should be reloaded if the file changes. /// The . public IConfigurationBuilder AddSshConfig(IFileProvider? fileProvider, - string path, bool optional, bool reloadOnChange) + string path, bool optional, bool reloadOnChange, Action? loggingAction = null) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(path); @@ -79,7 +79,7 @@ public IConfigurationBuilder AddSshConfig(IFileProvider? fileProvider, s.Optional = optional; s.ReloadOnChange = reloadOnChange; s.ResolveFileProvider(); - }); + }, loggingAction); } /// @@ -87,9 +87,12 @@ public IConfigurationBuilder AddSshConfig(IFileProvider? fileProvider, /// /// Configures the source. /// The . - public IConfigurationBuilder AddSshConfig(Action? configureSource) + public IConfigurationBuilder AddSshConfig(Action? configureSource, Action? loggingAction) { - var source = new SshConfigurationSource(); + var source = new SshConfigurationSource + { + OnSkippedIncludeFile = loggingAction + }; configureSource?.Invoke(source); return builder.Add(source); } diff --git a/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs b/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs index 2108b52..0f739db 100644 --- a/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs +++ b/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs @@ -40,6 +40,14 @@ public sealed record SshConfigParserOptions /// Defaults to . /// public bool ThrowOnUnknownKey { get; init; } + + /// + /// Optional callback invoked when an included file cannot be read due to + /// insufficient permissions or an I/O error. Receives the file path and the + /// causing exception. When , inaccessible files are + /// silently skipped. + /// + public Action? OnSkippedIncludeFile { get; init; } /// Gets the default options instance. public static SshConfigParserOptions Default { get; } = new(); diff --git a/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs b/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs index 5d5101a..0b5fa79 100644 --- a/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs +++ b/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs @@ -411,7 +411,22 @@ private static SshConfigDocument ResolveInclude( foreach (var file in Directory.GetFiles(dir, filePattern).Order(StringComparer.Ordinal)) { - var fileContent = File.ReadAllText(file); + string fileContent; + try + { + fileContent = File.ReadAllText(file); + } + catch (UnauthorizedAccessException ex) + { + options.OnSkippedIncludeFile?.Invoke(file, ex); + continue; + } + catch (IOException ex) + { + options.OnSkippedIncludeFile?.Invoke(file, ex); + continue; + } + var included = ParseDocument(fileContent, file, options, depth + 1); globalItems.AddRange(included.GlobalItems); blocks.AddRange(included.Blocks); diff --git a/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs b/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs index 66a328d..f2b78db 100644 --- a/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs +++ b/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs @@ -32,7 +32,7 @@ public override void Load(Stream stream) // We use the file path from the source if available for better error messages. var filePath = Source.Path; var document = SshConfigParser.Parse(content, - new SshConfigParserOptions { IncludeBasePath = filePath is null ? null : Path.GetDirectoryName(filePath) }); + new SshConfigParserOptions { IncludeBasePath = filePath is null ? null : Path.GetDirectoryName(filePath), OnSkippedIncludeFile = Source is SshConfigurationSource source ? source.OnSkippedIncludeFile : null }); var data = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs b/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs index 56893e2..78a92b2 100644 --- a/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs +++ b/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs @@ -7,6 +7,14 @@ namespace OpenSSH_GUI.SshConfig.Services; /// public sealed class SshConfigurationSource : FileConfigurationSource { + /// + /// Optional callback invoked when an included file cannot be read due to + /// insufficient permissions or an I/O error. Receives the file path and the + /// causing exception. When , inaccessible files are + /// silently skipped. + /// + public Action? OnSkippedIncludeFile { get; init; } + /// /// Builds the for this source. /// diff --git a/OpenSSH_GUI/Program.cs b/OpenSSH_GUI/Program.cs index 86e6ace..76b4bc7 100644 --- a/OpenSSH_GUI/Program.cs +++ b/OpenSSH_GUI/Program.cs @@ -99,10 +99,15 @@ public static async Task Main(string[] args) private static void ConfigureAppConfiguration(HostBuilderContext builderContext, IConfigurationBuilder configurationBuilder) { - configurationBuilder.AddSshConfig(ConfigFile.GetPathOfFile(), true, true); - configurationBuilder.AddSshConfig(SshdConfig.GetPathOfFile(), true, true); + configurationBuilder.AddSshConfig(ConfigFile.GetPathOfFile(), true, true, LoggingAction); + configurationBuilder.AddSshConfig(SshdConfig.GetPathOfFile(), true, true, LoggingAction); configurationBuilder.AddInMemoryCollection([ new KeyValuePair(VersionEnvVar, GetHostVersion()) ]); } + + private static void LoggingAction(string arg1, Exception arg2) + { + Log.Logger.Error(arg2, "Failed to load SSH config file: {Path}", arg1); + } } \ No newline at end of file From b3e8a9cdbba74ff9d381c15bb3426f88b6f344fa Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Tue, 24 Mar 2026 10:04:03 +0100 Subject: [PATCH 04/58] remove warnings as error in csproj --- OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj b/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj index 0042c3b..ed5af58 100644 --- a/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj +++ b/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj @@ -3,7 +3,6 @@ false true - true true OpenSSH_GUI.SshConfig From bbbe8eae1d96a0f079ef01062d92d6d787fcb764 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Tue, 24 Mar 2026 10:04:03 +0100 Subject: [PATCH 05/58] remove warnings as error in csproj --- OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj b/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj index 0042c3b..ed5af58 100644 --- a/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj +++ b/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj @@ -3,7 +3,6 @@ false true - true true OpenSSH_GUI.SshConfig From d99e92f4abf787349f625ca7b4865422ac8a01a2 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 00:06:00 +0200 Subject: [PATCH 06/58] Fix and Optimize Configuration, SSH Key Handling, and UI (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix startup error when files are not readable. * Added Winget Pipeline * Extended FileInfoWindow * Implemented PasswordBox with toggleable password visibility * Update CI * Minor version bump * Added Password in File Info view * Introduced object for BasicSshKeyFileInformation * Reniced SshKeyFileInformation.cs * Fixes some errors in SshKeyFileInformation.cs; Updated file reading logic for ppk files * Removed obsolete comments * Refactoring #1 * fixed reactive chain * 😁 * Optimized Reactive chains * - * Added new icon, changed resolverchain * Refactor icon size handling, improve SSH key collection tracking, and update reactive bindings. * Relocate and optimize asset management: replace SVG icon with ICO format, restructure image paths, and update project references accordingly. * Add light theme assets, theme selection settings, and dynamic icon support. * Refactor theme resources, improve reactive property bindings, update dependencies, and enhance SSH key file handling logic. * Refactor SubmitButtons as reusable control, replace redundant button definitions, and apply dynamic theme resources. * Add SubmitButtons control to EditKnownHostsWindow.axaml * Removed obsolete Interfaces * Minor fixes * Adjust Colors throughout the application * removed logger from SshKeyFilePassword.cs * Changes on FileInfoWindow * Color occurences changed to dynamic resource * Update ReactiveUI * Started to implement password change * Implemented password change ability * Disabled selection of items in SshKeyComboBoxStyle, when they are not initialized * Changed writing and disabled false-positive error in SshKeyItemContainerTheme * Implemented retry abort mechanism * Reformat & Cleanup; Changed to ServerConnection.cs * Changes in ConnectToServerViewModel * Removed redundant interfaces; reformat & cleanup * Implemented headered item * Minor refactoring in server handling * added ideas and refactoring suggestions * Refactoring of SshKeyManager.cs * Cleanup of old Extensions, compute fingerprint by ourselfs - even if key is encrypted * Enabled Logging by SSH.NET * Refactoring progress * Refactoring Progress #2 * SshKeyFileInformation changed to OAPHs * not working sorter * Update package dependencies across all projects to latest stable versions * Remove custom Sorter control, migrate to Avalonia DataGrid for key listing adjustments * Replace ContextMenus with FlyoutButtons, update styling and font sizes, and add Xaml.Behaviors.Avalonia dependency. * Add font size adjustment feature to Application Settings with binding and reset option. * Add dynamic font size management with resource observables, MaterialIcon size adjustments, and NumericUpDown control in Application Settings. * Refactor icon size handling: adjust MaterialIconSize resource, streamline size-related properties, and improve logging logic. * Update DataGrid column properties, add loading spinner, and improve reactive processing state handling. * Update localization bindings for tab headers, improve string resource handling, and streamline German translations. * Refactor SSH key export logic: simplify case handling, improve error messaging, and update dynamic window title formatting. * Refactor reactive properties and commands: streamline syntax, enhance property accessibility, and improve dynamic window title logic. * Refactor ViewModelBase hierarchy: simplify generics, remove redundant logger dependencies, and streamline initialization patterns. * Refactor various components: optimize property and method signatures, streamline placeholders and localization, and remove redundant dependencies. * Refactor KnownHostsFile and related components: optimize initialization, enhance file handling logic, and streamline key export functionality. * Refactor KnownHosts handling: optimize file updates, consolidate constructors, and streamline bindings in KnownHosts GUI rendering. * Optimize AuthorizedKeys handling: add change detection for file updates, prevent unnecessary writes to file and server, and fix BUG comment. * Refactor dependency injection tests: use DryIoc container directly and refine registration logic to improve clarity and accuracy. * Add `Window_OnClosing` handler for dialogs, adjust window properties, and include ReactiveUI references in project. * add todo; * Prevent window closing interruptions: handle `Window_OnClosing` to cancel external close attempts and streamline closing logic. * Refactor ApplicationSettings components: enhance UI alignment properties, add "Open Cache Folder" command, and streamline font size initialization logic. * Refactor SshKeyManager file watcher: integrate Avalonia Dispatcher for UI thread invocation and streamline event handling logic. * Refactor dialog closing logic: introduce `_isInternalClose` flag to prevent external close interruptions and streamline event handling. Optimize reactive properties in `SshKeyFileInformation` for better initialization and UI thread integration. * Refactor `SshKeyFileInformation`: remove reactive properties and disposables to simplify initialization and make properties immutable. * Refactor `FlyoutButton` UI: simplify tooltip and key count display logic, remove spinning icon animation, and streamline related ViewModel properties. * Refactor `ApplicationSettingsWindow`: update UI for cache options, enhance layout alignment, and remove unused reactive properties in ViewModel. * Replace DryIoc with Microsoft.Extensions.DependencyInjection for dependency injection, update related service registrations, and refactor impacted components accordingly. * Adjust DependencyInjectionExtensions * Cleanuip Dialoghost * Fix icon store filling * Reformat & Cleanup * small fixes * Added processing animation * Icon Updates * Added some findings, need more. * Update package dependencies to latest versions and improve DI test robustness * Refactor key file format handling: simplify `Password.Set` logic, update binding properties for key formats, clean up TODOs, and enhance backup file deletion logic. * Refactor and modularize SSH key management: introduce new interfaces and services (`IDirectoryCrawler`, `IKeyFileBackupService`, `IKeyFileWriterService`, and `ISshKeyGenerator`), implement `SshKeyFactory` and `KeyFileBackupService`, and update `SshKeyManager` to use dependency injection and improve maintainability. * Simplify "Provide Password" button in SshKeyFileStyle and fix export format in MainWindowViewModel. * Update ReactiveUI and ReactiveUI.SourceGenerators to latest versions in project files. * Add TODO comment to enable scanning of additional user-scoped paths in DirectoryCrawler. * Added AppConfiguration; Added WriteableConfiguration interface * Added writeableconfig to appsettingsviewmodel * Restrict ComboBoxItem binding in SshKeyFileStyle and remove redundant DataType specification. * Refactor DirectoryCrawler to support async disk enumeration and enable scanning of configurable lookup paths. * Add `PathExtensions` utility class for file/directory management and integrate usage into configuration and logging workflows. * Added ComboBox for path selection in AddKeyWindow and font size binding adjustments * Ensure main window is brought to front on open, then clear topmost flag * Refactor configuration system: replaced `IWritableConfiguration` with `IMutableConfiguration`, updated usages across the app, and improved settings UI layout. * Refactor SSH key file format handling: replace `SshKeyFormatExtension` constants with `PathExtensions`, add missing utility functions, and update references across the codebase. * Add lookup path management in ApplicationSettings: enable adding and removing paths with UI integration and validation logic * Refactor ApplicationSettings: enhance lookup path management, implement new bindings for HeaderedItem, throttle reactive commands, and extract `PathDeletableConverter` to a dedicated file. * Refactor application configuration: update font size type to `double`, introduce `ApplyConfiguration` method for dynamic theme and log level application, and optimize reactive bindings in `ApplicationSettingsViewModel`. * Add `CollectionIndexConverter` for 1-based indexing in lookup paths and update `ApplicationSettingsWindow` bindings to include index display * Update `ApplicationSettingsWindow`: localize lookup paths header text and add German translation * Add `TooltippedIcon` control and integrate it into `ApplicationSettingsWindow` for enhanced UI element descriptions * Add hover-based tooltip functionality to `TooltippedIcon` with extended pointer handling and popup interactions * Update dependencies: bump Avalonia packages to 12.0.2, ReactiveUI to 23.2.27, and other minor version upgrades * clean up indentation, adjust formatting across services and views, and streamline property definitions. * Centralize NuGet package version management and refactor project files to improve readability and reduce redundancy. * Fix SSH key format detection: ensure `.EndsWith` is used for extension comparison to handle edge cases --- .github/workflows/auto-tag.yml | 2 +- .github/workflows/build.yml | 43 +- .github/workflows/staging.yml | 32 +- Directory.Build.props | 3 +- Directory.Build.targets | 10 +- Directory.Packages.props | 52 ++ .../Configuration/ApplicationConfiguration.cs | 52 ++ .../JsonFileConfigurationWriter.cs | 53 ++ .../Configuration/LoggerConfiguration.cs | 24 +- .../Configuration/MutableConfiguration.cs | 156 ++++ OpenSSH_GUI.Core/Enums/AuthType.cs | 22 - OpenSSH_GUI.Core/Enums/OperationResult.cs | 9 + OpenSSH_GUI.Core/Enums/ThemeVariant.cs | 8 + OpenSSH_GUI.Core/ExceptionHandler.cs | 8 +- .../DependencyInjectionExtensions.cs | 214 ++++- .../Extensions/IPrivateKeySourceExtensions.cs | 25 - OpenSSH_GUI.Core/Extensions/PathExtensions.cs | 162 ++++ .../Extensions/PlatformIdExtensions.cs | 27 + .../Extensions/SshConfigExtensions.cs | 7 +- .../Extensions/SshConfigFilesExtension.cs | 17 +- .../Extensions/SshKeyFormatExtension.cs | 19 +- .../Extensions/StringExtensions.cs | 46 +- .../Credentials/IConnectionCredentials.cs | 36 - .../Credentials/IKeyConnectionCredentials.cs | 22 - .../IMultiKeyConnectionCredentials.cs | 16 - .../IPasswordConnectionCredentials.cs | 17 - .../Interfaces/Hosts/IDialogHost.cs | 4 - .../Interfaces/IDirectoryCrawler.cs | 23 + .../Interfaces/IKeyFileBackupService.cs | 81 ++ .../Interfaces/IKeyFileWriterService.cs | 72 ++ .../Interfaces/IMutableConfiguration.cs | 65 ++ OpenSSH_GUI.Core/Interfaces/ISshKeyFactory.cs | 14 + .../Interfaces/ISshKeyGenerator.cs | 16 + .../Interfaces/KnownHosts/IKnownHost.cs | 40 - .../Interfaces/KnownHosts/IKnownHostKey.cs | 29 - .../Interfaces/KnownHosts/IKnownHostsFile.cs | 70 -- .../Lib/AuthorizedKeys/AuthorizedKey.cs | 9 +- .../Lib/AuthorizedKeys/AuthorizedKeysFile.cs | 110 +-- .../Lib/Credentials/ConnectionCredentials.cs | 40 - .../Credentials/KeyConnectionCredentials.cs | 47 -- .../MultiKeyConnectionCredentials.cs | 43 - .../PasswordConnectionCredentials.cs | 35 - .../Lib/Keys/BasicSshKeyFileInformation.cs | 385 +++++++++ OpenSSH_GUI.Core/Lib/Keys/SshKeyFactory.cs | 19 + OpenSSH_GUI.Core/Lib/Keys/SshKeyFile.cs | 548 +++++-------- .../Lib/Keys/SshKeyFileInformation.cs | 195 ++--- .../Lib/Keys/SshKeyFilePassword.cs | 246 ++---- OpenSSH_GUI.Core/Lib/Keys/SshKeyFileSource.cs | 17 +- OpenSSH_GUI.Core/Lib/KnownHosts/KnownHost.cs | 61 +- .../Lib/KnownHosts/KnownHostHost.cs | 26 + .../Lib/KnownHosts/KnownHostKey.cs | 38 +- .../Lib/KnownHosts/KnownHostsFile.cs | 186 ++--- OpenSSH_GUI.Core/Lib/Misc/BackedUpFile.cs | 15 + .../Lib/Misc/ConnectionCredentials.cs | 108 +++ OpenSSH_GUI.Core/Lib/Misc/DirectoryCrawler.cs | 122 ++- .../Lib/Misc/KeyManagerOperationResult.cs | 82 ++ .../Lib/Misc/ReactiveBufferWriter.cs | 118 +++ OpenSSH_GUI.Core/Lib/Misc/ServerConnection.cs | 315 +++---- .../MVVM/IInitializableViewModel.cs | 29 + OpenSSH_GUI.Core/MVVM/ViewModelBase.cs | 217 ++--- OpenSSH_GUI.Core/OpenSSH_GUI.Core.csproj | 60 +- OpenSSH_GUI.Core/Resources/AppIconStore.cs | 26 + .../Converter/CollectionIndexConverter.cs | 21 + .../Converter/PathDeletableConverter.cs | 21 + .../Resources/Wrapper/WindowBase.cs | 55 +- .../Services/Hosted/FileSystemAnalyzer.cs | 6 +- .../Services/KeyFileBackupService.cs | 126 +++ .../Services/KeyFileWriterService.cs | 139 ++++ .../Services/ServerConnectionService.cs | 57 +- OpenSSH_GUI.Core/Services/SshKeyGenerator.cs | 42 + OpenSSH_GUI.Core/Services/SshKeyManager.cs | 768 ++++++++++-------- .../Enums/MessageBoxButtons.cs | 2 + OpenSSH_GUI.Dialogs/Enums/MessageBoxIcon.cs | 2 + OpenSSH_GUI.Dialogs/Enums/MessageBoxResult.cs | 2 + .../Interfaces/IMessageBoxProvider.cs | 11 +- .../Models/MessageBoxParams.cs | 5 - .../Models/SecureInputParams.cs | 7 + .../Models/SecureInputResult.cs | 6 +- .../OpenSSH_GUI.Dialogs.csproj | 33 +- .../Services/MessageBoxProvider.cs | 76 +- .../Views/MessageBoxDialog.axaml | 14 +- .../Views/MessageBoxDialog.axaml.cs | 64 +- .../Views/SecureInputDialog.axaml | 16 +- .../Views/SecureInputDialog.axaml.cs | 34 +- .../Views/ValidatedInputDialog.axaml | 15 +- .../Views/ValidatedInputDialog.axaml.cs | 29 +- .../Extensions/SshConfigurationExtensions.cs | 41 +- .../Extensions/SshHostBlockExtensions.cs | 31 +- OpenSSH_GUI.SshConfig/Models/SshBlock.cs | 30 +- .../Models/SshConfigDocument.cs | 11 +- .../Models/SshHostSettings.cs | 4 +- OpenSSH_GUI.SshConfig/Models/SshKnownKeys.cs | 26 +- OpenSSH_GUI.SshConfig/Models/SshLineItem.cs | 19 +- .../Models/SshMatchCriterion.cs | 15 +- .../OpenSSH_GUI.SshConfig.csproj | 28 +- .../Options/SshConfigParserOptions.cs | 15 +- .../Options/SshSerializerOptions.cs | 5 +- .../Parsers/SshConfigParser.cs | 41 +- .../Serializers/SshConfigSerializer.cs | 9 +- .../Services/SshConfigFileService.cs | 9 +- .../Services/SshConfigurationProvider.cs | 13 +- .../Services/SshConfigurationSource.cs | 10 +- .../ServiceCollectionExtensionsTests.cs | 31 +- .../SshConfigFilesExtensionTests.cs | 29 +- .../Extensions/SshKeyFormatExtensionTests.cs | 21 +- .../Extensions/SshKeyTypeExtensionTests.cs | 5 +- .../Core/Extensions/StringExtensionsTests.cs | 47 +- .../Core/MVVM/ViewModelBaseTests.cs | 16 +- OpenSSH_GUI.Tests/OpenSSH_GUI.Tests.csproj | 70 +- OpenSSH_GUI.Tests/ReactiveUiInitFixture.cs | 26 +- .../SshConfig/SshConfigParserTests.cs | 27 +- .../SshConfig/SshConfigSerializerTests.cs | 33 +- .../SshConfig/SshConfigurationBindingTests.cs | 7 +- .../SshConfig/SshHostSettingsTests.cs | 2 + .../SshConfig/SshKnownKeysTests.cs | 38 +- .../SshConfig/SshWildcardMatcherTests.cs | 76 +- OpenSSH_GUI.sln.DotSettings | 115 +++ OpenSSH_GUI.slnx | 24 +- OpenSSH_GUI/App.axaml | 95 ++- OpenSSH_GUI/App.axaml.cs | 189 ++++- OpenSSH_GUI/Assets/appicon.ico | Bin 24306 -> 0 bytes OpenSSH_GUI/Assets/appicon.png | Bin 24358 -> 0 bytes OpenSSH_GUI/Assets/avalonia-logo.ico | Bin 176111 -> 0 bytes OpenSSH_GUI/Converters/Converter.cs | 22 +- .../Extensions/CallerEnricherExtensions.cs | 9 +- .../DependencyInjectionExtensions.cs | 93 +-- .../Logging/Enricher/CallerEnricher.cs | 36 +- OpenSSH_GUI/OpenSSH_GUI.csproj | 142 ++-- OpenSSH_GUI/Program.cs | 122 +-- .../Resources/Controls/FlyoutButton.cs | 45 + .../Resources/Controls/HeaderedItem.axaml | 28 + .../Resources/Controls/HeaderedItem.axaml.cs | 70 ++ .../Resources/Controls/PasswordDisplay.axaml | 34 + .../Controls/PasswordDisplay.axaml.cs | 88 ++ .../Resources/Controls/SubmitButtons.axaml | 49 ++ .../Resources/Controls/SubmitButtons.axaml.cs | 162 ++++ .../Resources/Controls/TooltippedIcon.axaml | 32 + .../Controls/TooltippedIcon.axaml.cs | 119 +++ .../Resources/StringsAndTexts.Designer.cs | 226 +++++- OpenSSH_GUI/Resources/StringsAndTexts.de.resx | 119 ++- OpenSSH_GUI/Resources/StringsAndTexts.resx | 120 ++- OpenSSH_GUI/Resources/Styles/ColorsDark.axaml | 146 ++++ .../Resources/Styles/ColorsLight.axaml | 127 +++ .../Resources/Styles/FlyoutButtonStyles.axaml | 17 + .../Resources/Styles/SshKeyFileStyle.axaml | 369 +++------ .../Resources/Styles/ThemeResource.axaml | 82 ++ .../ViewModels/AddKeyWindowViewModel.cs | 191 +++-- .../ApplicationSettingsViewModel.cs | 318 ++++++-- .../ViewModels/ConnectToServerViewModel.cs | 254 +++--- .../ViewModels/EditAuthorizedKeysViewModel.cs | 83 +- .../EditKnownHostsWindowViewModel.cs | 38 +- .../ViewModels/ExportWindowViewModel.cs | 30 +- .../ViewModels/FileInfoWindowViewModel.cs | 252 +++++- OpenSSH_GUI/ViewModels/MainWindowViewModel.cs | 408 ++++------ OpenSSH_GUI/Views/AddKeyWindow.axaml | 122 ++- OpenSSH_GUI/Views/AddKeyWindow.axaml.cs | 10 +- .../Views/ApplicationSettingsWindow.axaml | 178 +++- .../Views/ApplicationSettingsWindow.axaml.cs | 9 +- OpenSSH_GUI/Views/ConnectToServerWindow.axaml | 88 +- .../Views/ConnectToServerWindow.axaml.cs | 10 +- .../Views/EditAuthorizedKeysWindow.axaml | 44 +- .../Views/EditAuthorizedKeysWindow.axaml.cs | 10 +- OpenSSH_GUI/Views/EditKnownHostsWindow.axaml | 95 +-- .../Views/EditKnownHostsWindow.axaml.cs | 11 +- OpenSSH_GUI/Views/ExportWindow.axaml | 50 +- OpenSSH_GUI/Views/ExportWindow.axaml.cs | 10 +- OpenSSH_GUI/Views/FileInfoWindow.axaml | 227 +++++- OpenSSH_GUI/Views/FileInfoWindow.axaml.cs | 12 +- OpenSSH_GUI/Views/MainWindow.axaml | 315 ++++--- OpenSSH_GUI/Views/MainWindow.axaml.cs | 30 +- images/openssh-gui-light.svg | 63 ++ images/openssh-gui.ico | Bin 0 -> 72398 bytes images/openssh-gui.svg | 62 ++ openssh-gui-bin/PKGBUILD | 26 +- openssh-gui-git/PKGBUILD | 8 +- openssh-gui-nightly/PKGBUILD | 23 +- update-version.sh | 14 + 177 files changed, 8221 insertions(+), 4329 deletions(-) create mode 100644 Directory.Packages.props create mode 100644 OpenSSH_GUI.Core/Configuration/ApplicationConfiguration.cs create mode 100644 OpenSSH_GUI.Core/Configuration/JsonFileConfigurationWriter.cs create mode 100644 OpenSSH_GUI.Core/Configuration/MutableConfiguration.cs delete mode 100644 OpenSSH_GUI.Core/Enums/AuthType.cs create mode 100644 OpenSSH_GUI.Core/Enums/OperationResult.cs create mode 100644 OpenSSH_GUI.Core/Enums/ThemeVariant.cs delete mode 100644 OpenSSH_GUI.Core/Extensions/IPrivateKeySourceExtensions.cs create mode 100644 OpenSSH_GUI.Core/Extensions/PathExtensions.cs create mode 100644 OpenSSH_GUI.Core/Extensions/PlatformIdExtensions.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/Credentials/IConnectionCredentials.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/Credentials/IKeyConnectionCredentials.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/Credentials/IMultiKeyConnectionCredentials.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/Credentials/IPasswordConnectionCredentials.cs create mode 100644 OpenSSH_GUI.Core/Interfaces/IDirectoryCrawler.cs create mode 100644 OpenSSH_GUI.Core/Interfaces/IKeyFileBackupService.cs create mode 100644 OpenSSH_GUI.Core/Interfaces/IKeyFileWriterService.cs create mode 100644 OpenSSH_GUI.Core/Interfaces/IMutableConfiguration.cs create mode 100644 OpenSSH_GUI.Core/Interfaces/ISshKeyFactory.cs create mode 100644 OpenSSH_GUI.Core/Interfaces/ISshKeyGenerator.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHost.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostKey.cs delete mode 100644 OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostsFile.cs delete mode 100644 OpenSSH_GUI.Core/Lib/Credentials/ConnectionCredentials.cs delete mode 100644 OpenSSH_GUI.Core/Lib/Credentials/KeyConnectionCredentials.cs delete mode 100644 OpenSSH_GUI.Core/Lib/Credentials/MultiKeyConnectionCredentials.cs delete mode 100644 OpenSSH_GUI.Core/Lib/Credentials/PasswordConnectionCredentials.cs create mode 100644 OpenSSH_GUI.Core/Lib/Keys/BasicSshKeyFileInformation.cs create mode 100644 OpenSSH_GUI.Core/Lib/Keys/SshKeyFactory.cs create mode 100644 OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostHost.cs create mode 100644 OpenSSH_GUI.Core/Lib/Misc/BackedUpFile.cs create mode 100644 OpenSSH_GUI.Core/Lib/Misc/ConnectionCredentials.cs create mode 100644 OpenSSH_GUI.Core/Lib/Misc/KeyManagerOperationResult.cs create mode 100644 OpenSSH_GUI.Core/Lib/Misc/ReactiveBufferWriter.cs create mode 100644 OpenSSH_GUI.Core/MVVM/IInitializableViewModel.cs create mode 100644 OpenSSH_GUI.Core/Resources/AppIconStore.cs create mode 100644 OpenSSH_GUI.Core/Resources/Converter/CollectionIndexConverter.cs create mode 100644 OpenSSH_GUI.Core/Resources/Converter/PathDeletableConverter.cs create mode 100644 OpenSSH_GUI.Core/Services/KeyFileBackupService.cs create mode 100644 OpenSSH_GUI.Core/Services/KeyFileWriterService.cs create mode 100644 OpenSSH_GUI.Core/Services/SshKeyGenerator.cs create mode 100644 OpenSSH_GUI.sln.DotSettings delete mode 100644 OpenSSH_GUI/Assets/appicon.ico delete mode 100644 OpenSSH_GUI/Assets/appicon.png delete mode 100644 OpenSSH_GUI/Assets/avalonia-logo.ico create mode 100644 OpenSSH_GUI/Resources/Controls/FlyoutButton.cs create mode 100644 OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml create mode 100644 OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml.cs create mode 100644 OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml create mode 100644 OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml.cs create mode 100644 OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml create mode 100644 OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml.cs create mode 100644 OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml create mode 100644 OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml.cs create mode 100644 OpenSSH_GUI/Resources/Styles/ColorsDark.axaml create mode 100644 OpenSSH_GUI/Resources/Styles/ColorsLight.axaml create mode 100644 OpenSSH_GUI/Resources/Styles/FlyoutButtonStyles.axaml create mode 100644 OpenSSH_GUI/Resources/Styles/ThemeResource.axaml create mode 100644 images/openssh-gui-light.svg create mode 100644 images/openssh-gui.ico create mode 100644 images/openssh-gui.svg create mode 100644 update-version.sh diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 3c4165f..4cb34d0 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -2,7 +2,7 @@ name: Auto-Tag on Release Merge on: pull_request: - types: [closed] + types: [ closed ] branches: - master - main diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9549a69..8cd4857 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,6 +68,7 @@ jobs: - name: Build AppImage if: matrix.target == 'linux-x64' run: | + APPSTREAM_VERSION="${GITHUB_REF_NAME#v}" # Download appimagetool wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool chmod +x appimagetool @@ -147,8 +148,8 @@ jobs: - name: Prepare extra assets run: | - cp OpenSSH_GUI/Assets/appicon.png artifacts/ - cp LICENSE artifacts/ + cp OpenSSH_GUI/Assets/appicon.png artifacts/appicon.png # ← Name explizit + cp LICENSE artifacts/LICENSE # Create a single release and upload all files from the 'artifacts' directory - name: Create Release and Upload Assets @@ -177,7 +178,7 @@ jobs: with: name: OpenSSH-GUI-linux-x64 path: ./ - + - name: Download generated desktop file uses: actions/download-artifact@v4 with: @@ -191,11 +192,9 @@ jobs: SHA_ICON=$(sha256sum OpenSSH_GUI/Assets/appicon.png | cut -d' ' -f1) SHA_DESKTOP=$(sha256sum io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD - sed -i "s/sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD - - # Also update openssh-gui-git pkgver for consistency + sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD - name: Update AUR (openssh-gui-bin) @@ -216,4 +215,32 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ github.ref_name }}" \ No newline at end of file + commit_message: "Update to ${{ github.ref_name }}" + + winget: + name: Update Winget Package + runs-on: ubuntu-latest + needs: release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + + steps: + - name: Extract version from tag + id: version + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Install Komac + run: | + curl -sL \ + "https://github.com/russellbanks/Komac/releases/latest/download/komac-linux-amd64" \ + -o komac + chmod +x komac + + - name: Update Winget manifest + run: | + ./komac update "frequency403.OpenSSHGUI" \ + --version "${{ steps.version.outputs.VERSION }}" \ + --urls "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/OpenSSH-GUI-win-x64.exe" \ + --submit \ + --token "${{ secrets.WINGET_GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 9679a76..da6e13c 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -76,7 +76,7 @@ jobs: -p:PublishSingleFile=true \ -p:PublishReadyToRun=true \ -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:Version="${{ env.VERSION }}" + -p:IsNightly=true - name: Rename artifact run: mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} ./publish/${{ matrix.asset_name }}${{ matrix.asset_extension }} @@ -121,7 +121,7 @@ jobs: with: name: OpenSSH-GUI-nightly-x86_64.AppImage path: OpenSSH-GUI-nightly-x86_64.AppImage - + - name: Upload generated desktop file if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 @@ -140,12 +140,13 @@ jobs: name: Create Nightly Release runs-on: ubuntu-latest needs: build - + steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} - name: Resolve git metadata run: | @@ -156,6 +157,13 @@ jobs: echo "BUILD_DATE=$DATE" >> "$GITHUB_ENV" echo "VERSION=${BASE_VERSION}+${HASH}" >> "$GITHUB_ENV" + - name: Force-update nightly tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -f nightly + git push origin -f nightly + - name: Download all build artifacts uses: actions/download-artifact@v4 with: @@ -163,7 +171,12 @@ jobs: - name: Display structure of downloaded files run: ls -R artifacts - + + - name: Collect extra release assets + run: | + cp OpenSSH_GUI/Assets/appicon.png artifacts/appicon.png + cp LICENSE artifacts/LICENSE + - name: Update nightly release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -171,7 +184,7 @@ jobs: NOTES="**Branch:** \`development\` **Commit:** \`${{ env.GIT_HASH }}\` **Built:** ${{ env.BUILD_DATE }} - + > Automated nightly build — not intended for production use." if gh release view nightly &>/dev/null; then @@ -185,10 +198,11 @@ jobs: find artifacts -type f | xargs gh release upload nightly else - find artifacts -type f | xargs gh release create nightly \ + gh release create nightly \ --title "Nightly (${{ env.BUILD_DATE }}) — ${{ env.VERSION }}" \ --notes "$NOTES" \ - --prerelease + --prerelease \ + $(find artifacts -type f) fi # --- JOB 3: AUR NIGHTLY --- @@ -208,7 +222,7 @@ jobs: with: name: OpenSSH-GUI-nightly-linux-x64 path: ./ - + - name: Download generated desktop file uses: actions/download-artifact@v4 with: @@ -228,7 +242,7 @@ jobs: SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-nightly/PKGBUILD - sed -i "s/sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD - name: Update AUR (openssh-gui-nightly) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 diff --git a/Directory.Build.props b/Directory.Build.props index ca675a4..228496d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,8 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.0.0 + 3.1.0 + true diff --git a/Directory.Build.targets b/Directory.Build.targets index c4676ec..03ab852 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -22,12 +22,16 @@ DependsOnTargets="ResolveGitHash"> 1.0.0 + false false - $(GitCommitHash) - $(BaseVersion) - $(BaseVersion)+$(GitCommitHash) + + $(BaseVersion) + $(BaseVersion) + + $(BaseVersion)-$(GitCommitHash) + $(BaseVersion)-$(GitCommitHash) diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..253ac32 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,52 @@ + + + true + true + $(NoWarn);NU1507 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Configuration/ApplicationConfiguration.cs b/OpenSSH_GUI.Core/Configuration/ApplicationConfiguration.cs new file mode 100644 index 0000000..d682586 --- /dev/null +++ b/OpenSSH_GUI.Core/Configuration/ApplicationConfiguration.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; +using OpenSSH_GUI.Core.Enums; +using OpenSSH_GUI.Core.Extensions; +using Serilog.Events; + +namespace OpenSSH_GUI.Core.Configuration; + +public class ApplicationConfiguration +{ + [JsonIgnore] + public static readonly ApplicationConfiguration Default = new() + { + LookupPaths = [SshConfigFilesExtension.GetBaseSshPath()], + PreferredTheme = ThemeVariant.Default, + LogLevel = LogEventLevel.Warning, + FontSize = 14, + LoggerConfiguration = LoggerConfiguration.Default + }; + + [JsonIgnore] + public static string ApplicationConfigurationPath { get; } = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppDomain.CurrentDomain.FriendlyName); + + [JsonIgnore] + public static string ApplicationConfigurationName { get; } = Path.WithJsonExtension(AppDomain.CurrentDomain.FriendlyName.ToLower()); + + [JsonIgnore] + public static string DefaultApplicationConfigurationFileFullPath { get; } = Path.Combine(ApplicationConfigurationPath, ApplicationConfigurationName); + + [Required] + public required string[] LookupPaths { get; set; } + + [Required] + public required ThemeVariant PreferredTheme { get; set; } + + [Required] + public required LogEventLevel LogLevel { get; set; } + + [Required, Range(12, 48, ErrorMessage = "Font size must be between 12 and 48")] + public required double FontSize { get; set; } + + [Required, ValidateObjectMembers] + public required LoggerConfiguration LoggerConfiguration { get; set; } +} + +[JsonSourceGenerationOptions(WriteIndented = true, UseStringEnumConverter = true), JsonSerializable(typeof(ApplicationConfiguration)), JsonSerializable(typeof(LoggerConfiguration))] +public partial class SourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Configuration/JsonFileConfigurationWriter.cs b/OpenSSH_GUI.Core/Configuration/JsonFileConfigurationWriter.cs new file mode 100644 index 0000000..93a8973 --- /dev/null +++ b/OpenSSH_GUI.Core/Configuration/JsonFileConfigurationWriter.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace OpenSSH_GUI.Core.Configuration; + +public sealed class JsonFileConfigurationWriter(string filePath, JsonTypeInfo typeInfo) +{ + private readonly SemaphoreSlim _lock = new(1, 1); + + /// + /// Reads and deserializes the configuration file into . + /// Returns a default instance if the file does not exist. + /// + public async Task ReadAsync(CancellationToken ct) + { + if (!File.Exists(filePath)) + return default; + + await using var stream = File.OpenRead(filePath); + return await JsonSerializer.DeserializeAsync(stream, typeInfo, ct); + } + + /// + /// Atomically writes to the configuration file via a temp-file swap. + /// + public async Task WriteAsync(T value, CancellationToken ct) + { + var tempFile = Path.GetTempFileName(); + await using (var stream = File.Open(tempFile, FileMode.Truncate)) + { + await JsonSerializer.SerializeAsync(stream, value, typeInfo, ct); + } + File.Move(tempFile, filePath, true); + } + + /// + /// Reads the current configuration, applies , then writes the result back atomically. + /// + public async Task UpdateAsync(Func> update, CancellationToken ct) + { + await _lock.WaitAsync(ct); + try + { + var current = await ReadAsync(ct); + var updated = await update(current); + await WriteAsync(updated, ct); + } + finally + { + _lock.Release(); + } + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Configuration/LoggerConfiguration.cs b/OpenSSH_GUI.Core/Configuration/LoggerConfiguration.cs index 864a017..72d17f6 100644 --- a/OpenSSH_GUI.Core/Configuration/LoggerConfiguration.cs +++ b/OpenSSH_GUI.Core/Configuration/LoggerConfiguration.cs @@ -1,9 +1,11 @@ -namespace OpenSSH_GUI.Core.Configuration; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using OpenSSH_GUI.Core.Extensions; + +namespace OpenSSH_GUI.Core.Configuration; public record LoggerConfiguration { - private const string LogFileFolderAndExtension = "log"; - #if DEBUG private const string LogTemplate = "[{Timestamp:yyyy/MM/dd HH:mm:ss}] [{Level:u3}] ({FileName}:{LineNumber}): {Message:lj}{NewLine}{Exception}"; @@ -12,16 +14,20 @@ public record LoggerConfiguration "[{Timestamp:yyyy/MM/dd HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"; #endif - public string LogFileName { get; set; } = - Path.ChangeExtension(AppDomain.CurrentDomain.FriendlyName, LogFileFolderAndExtension); + [Required(AllowEmptyStrings = false)] + public string LogFileName { get; set; } = Path.WithLogExtension(AppDomain.CurrentDomain.FriendlyName); + [Required(AllowEmptyStrings = false)] public string LogFilePath { get; set; } = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - AppDomain.CurrentDomain.FriendlyName, LogFileFolderAndExtension); + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppDomain.CurrentDomain.FriendlyName, PathExtensions.LogExtension); - public string LogFileFullPath => Path.Combine(LogFilePath, LogFileName); + public string LogOutputTemplate { get; set; } = LogTemplate; - public string LogOutputTemplate => LogTemplate; + [JsonIgnore] + public string LogFileFullPath => Path.Combine(LogFilePath, LogFileName); + [JsonIgnore] public static LoggerConfiguration Default { get; } = new(); } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Configuration/MutableConfiguration.cs b/OpenSSH_GUI.Core/Configuration/MutableConfiguration.cs new file mode 100644 index 0000000..af7e0d2 --- /dev/null +++ b/OpenSSH_GUI.Core/Configuration/MutableConfiguration.cs @@ -0,0 +1,156 @@ +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenSSH_GUI.Core.Interfaces; + +namespace OpenSSH_GUI.Core.Configuration; + +/// +public sealed class MutableConfiguration : IMutableConfiguration + where T : class +{ + private readonly ILogger> _logger; + private readonly IOptionsMonitor _options; + private readonly IDisposable? _optionsMonitor; + private readonly JsonFileConfigurationWriter _writer; + + + public MutableConfiguration(ILogger> logger, + JsonFileConfigurationWriter writer, + IOptionsMonitor options) + { + _logger = logger; + _writer = writer; + _options = options; + _optionsMonitor = _options.OnChange(conf => + { + _logger.LogDebug("Configuration changed triggered"); + ConfigurationChanged?.Invoke(this, conf); + }); + } + + /// + public T Current => _options.CurrentValue; + + /// + public Task ExecuteConfigurationUpdateAsync(Action update, CancellationToken ct = default) => + _writer.UpdateAsync( + current => + { + try + { + var config = current ?? throw new InvalidOperationException("Configuration could not be read."); + update(config); + return Task.FromResult(config); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while updating configuration."); + throw; + } + }, ct); + + /// + public Task SetPropertyValueAsync(Expression> property, TValue value, CancellationToken ct = default) => + _writer.UpdateAsync( + current => + { + try + { + var config = current ?? throw new InvalidOperationException("Configuration could not be read."); + + // Unwrap expression (handle Convert) + var memberExpression = property.Body switch + { + MemberExpression m => m, + UnaryExpression { NodeType: ExpressionType.Convert, Operand: MemberExpression m } => m, + _ => throw new InvalidOperationException( + $"Expression '{property}' does not refer to a property.") + }; + + if (memberExpression.Member is not PropertyInfo { CanWrite: true } propertyInfo) + throw new InvalidOperationException( + $"Expression '{property}' does not refer to a writable property."); + + // Convert value if necessary + var targetType = propertyInfo.PropertyType; + object? convertedValue = value; + + if (value is not null && !targetType.IsAssignableFrom(typeof(TValue))) + { + convertedValue = ConvertValue(value, targetType); + } + + SetPropertyValue(propertyInfo, config, convertedValue); + return Task.FromResult(config); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while updating configuration."); + throw; + } + }, ct); + + /// + public Task SetPropertyValueAsync(string key, TValue value, CancellationToken ct = default) => + _writer.UpdateAsync( + current => + { + try + { + var config = current ?? throw new InvalidOperationException("Configuration could not be read."); + SetPropertyValue( + typeof(T).GetProperty( + key, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) + ?? throw new InvalidOperationException($"Property '{key}' was not found on type '{typeof(T).Name}'."), config, value); + return Task.FromResult(config); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while updating configuration."); + throw; + } + }, ct); + + /// + public event EventHandler? ConfigurationChanged; + + /// + public void Dispose() { _optionsMonitor?.Dispose(); } + + + private void SetPropertyValue(PropertyInfo propertyInfo, T config, TValue value) + { + if (!propertyInfo.CanWrite) + throw new InvalidOperationException($"Property '{propertyInfo.Name}' on type '{typeof(T).Name}' is not writable."); + var initialValue = propertyInfo.GetValue(config); + propertyInfo.SetValue(config, value); + _logger.LogDebug( + "Updated property {PropertyName} of configuration object '{ConfigurationType}' from {InitialValue} to {CurrentValue}", propertyInfo.Name, typeof(T).Name, initialValue, + value); + } + + private static object ConvertValue(object value, Type targetType) + { + var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + try + { + if (underlyingType.IsEnum) + return Enum.ToObject(underlyingType, value); + + return Convert.ChangeType(value, underlyingType); + } + catch + { + // fallback: try direct cast + if (targetType.IsInstanceOfType(value)) + return value; + + throw new InvalidCastException( + $"Cannot convert value '{value}' to type '{targetType}'."); + } + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Enums/AuthType.cs b/OpenSSH_GUI.Core/Enums/AuthType.cs deleted file mode 100644 index 50c875a..0000000 --- a/OpenSSH_GUI.Core/Enums/AuthType.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace OpenSSH_GUI.Core.Enums; - -/// -/// Represents the types of authentication supported for SSH connections. -/// -public enum AuthType -{ - /// - /// Represents connection credentials using password authentication. - /// - Password, - - /// - /// Represents the authentication type of connection credentials using SSH key. - /// - Key, - - /// - /// Represents a multi-key authentication type for SSH connections. - /// - MultiKey -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Enums/OperationResult.cs b/OpenSSH_GUI.Core/Enums/OperationResult.cs new file mode 100644 index 0000000..89b9993 --- /dev/null +++ b/OpenSSH_GUI.Core/Enums/OperationResult.cs @@ -0,0 +1,9 @@ +namespace OpenSSH_GUI.Core.Enums; + +public enum OperationResult +{ + Success, + Failure, + Conflict, + Cancelled +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Enums/ThemeVariant.cs b/OpenSSH_GUI.Core/Enums/ThemeVariant.cs new file mode 100644 index 0000000..63f294a --- /dev/null +++ b/OpenSSH_GUI.Core/Enums/ThemeVariant.cs @@ -0,0 +1,8 @@ +namespace OpenSSH_GUI.Core.Enums; + +public enum ThemeVariant +{ + Default, + Dark, + Light +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/ExceptionHandler.cs b/OpenSSH_GUI.Core/ExceptionHandler.cs index 217c2ff..748332e 100644 --- a/OpenSSH_GUI.Core/ExceptionHandler.cs +++ b/OpenSSH_GUI.Core/ExceptionHandler.cs @@ -7,12 +7,14 @@ namespace OpenSSH_GUI.Core; public class ExceptionHandler(ILogger logger) : IObserver { + /// public void OnCompleted() { if (Debugger.IsAttached) Debugger.Break(); } + /// public void OnError(Exception error) { if (Debugger.IsAttached) @@ -21,6 +23,7 @@ public void OnError(Exception error) AvaloniaScheduler.Instance.Schedule(error, HandleException); } + /// public void OnNext(Exception value) { if (Debugger.IsAttached) @@ -29,8 +32,5 @@ public void OnNext(Exception value) AvaloniaScheduler.Instance.Schedule(value, HandleException); } - private static IDisposable HandleException(IScheduler arg1, Exception arg2) - { - throw arg2; - } + private static IDisposable HandleException(IScheduler arg1, Exception arg2) => throw arg2; } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Extensions/DependencyInjectionExtensions.cs b/OpenSSH_GUI.Core/Extensions/DependencyInjectionExtensions.cs index 8213db9..002e7cd 100644 --- a/OpenSSH_GUI.Core/Extensions/DependencyInjectionExtensions.cs +++ b/OpenSSH_GUI.Core/Extensions/DependencyInjectionExtensions.cs @@ -1,5 +1,13 @@ -using Avalonia.Controls; -using DryIoc; +using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization.Metadata; +using Avalonia.Controls; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenSSH_GUI.Core.Configuration; +using OpenSSH_GUI.Core.Interfaces; using OpenSSH_GUI.Core.MVVM; using OpenSSH_GUI.Core.Resources.Wrapper; @@ -7,60 +15,220 @@ namespace OpenSSH_GUI.Core.Extensions; public static class DependencyInjectionExtensions { + private static readonly ConcurrentDictionary RequiredPropertiesCache = new(); + + /// + /// Returns all publicly writable properties of that are + /// annotated with , using a per-type cache + /// to avoid repeated reflection overhead. + /// + /// The type to inspect. + /// An array of representing the required properties. + private static PropertyInfo[] GetRequiredProperties(Type type) + { + return RequiredPropertiesCache.GetOrAdd( + type, static t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite && p.GetCustomAttribute() is not null) + .ToArray()); + } + + /// + /// Validates that and follow the + /// prescribed View/ViewModel naming convention. + /// + /// The View type. + /// The ViewModel type. + /// + /// if the naming convention is satisfied; otherwise . + /// private static bool ValidateNamingConvention() { var t1Name = typeof(T1).Name; var t2Name = typeof(T2).Name; if (t1Name == t2Name) return false; - var option1 = string.Equals(t1Name.Replace("Window", ""), t2Name.Replace("ViewModel", ""), + var option1 = string.Equals( + t1Name.Replace("Window", string.Empty), t2Name.Replace("ViewModel", string.Empty), StringComparison.Ordinal); var option2 = t2Name.StartsWith(t1Name) && t2Name.EndsWith("ViewModel"); return option1 || option2; } - extension(IResolver resolver) + extension(IServiceProvider serviceProvider) { + /// + /// Resolves a from a dedicated , + /// initializes it with the provided , and disposes + /// the scope automatically when the window closes. + /// + /// The window type to resolve. + /// The ViewModel type associated with the view. + /// The type of the initializer parameter passed to the ViewModel. + /// + /// The parameter passed to + /// . + /// + /// The startup location of the window. + /// A to observe during initialization. + /// The fully initialized instance. + /// + /// Thrown if the ViewModel is or was not properly initialized. + /// public async ValueTask ResolveViewAsync( TViewModelInitializerParameter initializerParameters, WindowStartupLocation windowStartupLocation = WindowStartupLocation.CenterScreen, CancellationToken token = default) where TView : WindowBase - where TViewModel : ViewModelBase - where TViewModelInitializerParameter : class, IInitializerParameters + where TViewModel : ViewModelBase { - var viewName = typeof(TView).Name; - var resolvedView = resolver.Resolve(serviceKey: viewName); - await resolvedView.InitializeAsync(initializerParameters, windowStartupLocation, token); - ArgumentNullException.ThrowIfNull(resolvedView.ViewModel); - return !resolvedView.ViewModel.IsInitialized ? throw new InvalidOperationException("ViewModel not properly initialized") : resolvedView; + var scope = serviceProvider.CreateScope(); + var scopeOwnership = scope; + try + { + var resolvedView = scope.ServiceProvider.GetRequiredKeyedService(typeof(TView).Name); + await resolvedView.InitializeAsync(initializerParameters, windowStartupLocation, token); + ArgumentNullException.ThrowIfNull(resolvedView.ViewModel); + + if (!resolvedView.ViewModel.IsInitialized) + throw new InvalidOperationException("ViewModel not properly initialized"); + + resolvedView.Closed += async (_, _) => + { + if (scope is IAsyncDisposable asyncScope) + await asyncScope.DisposeAsync(); + else + scope.Dispose(); + }; + + scopeOwnership = null; + return resolvedView; + } + finally + { + scopeOwnership?.Dispose(); + } } + /// + /// Resolves a from a dedicated , + /// initializes it, and disposes the scope automatically when the window closes. + /// + /// The window type to resolve. + /// The ViewModel type associated with the view. + /// The startup location of the window. + /// A to observe during initialization. + /// The fully initialized instance. + /// + /// Thrown if the ViewModel is or was not properly initialized. + /// public async ValueTask ResolveViewAsync( WindowStartupLocation windowStartupLocation = WindowStartupLocation.CenterScreen, CancellationToken token = default) where TView : WindowBase - where TViewModel : ViewModelBase + where TViewModel : ViewModelBase { - var viewName = typeof(TView).Name; - var resolvedView = resolver.Resolve(serviceKey: viewName); - await resolvedView.InitializeAsync(windowStartupLocation, token); - ArgumentNullException.ThrowIfNull(resolvedView.ViewModel); - return !resolvedView.ViewModel.IsInitialized ? throw new InvalidOperationException("ViewModel not properly initialized") : resolvedView; + var scope = serviceProvider.CreateScope(); + var scopeOwnership = scope; + try + { + var resolvedView = scope.ServiceProvider.GetRequiredKeyedService(typeof(TView).Name); + await resolvedView.InitializeAsync(windowStartupLocation, token); + ArgumentNullException.ThrowIfNull(resolvedView.ViewModel); + + if (!resolvedView.ViewModel.IsInitialized) + throw new InvalidOperationException("ViewModel not properly initialized"); + + resolvedView.Closed += async (_, _) => + { + if (scope is IAsyncDisposable asyncScope) + await asyncScope.DisposeAsync(); + else + scope.Dispose(); + }; + + scopeOwnership = null; + return resolvedView; + } + finally + { + scopeOwnership?.Dispose(); + } } } - extension(IContainer container) + extension(IHostBuilder builder) + { + /// + /// Adds mutable configuration support for the specified type using the provided JSON file path and type metadata. + /// + /// The type of the configuration object. + /// The path to the JSON file containing the configuration data. + /// The JSON type information used for deserialization of the configuration data. + /// The optional parameter, which indicates if the file is optional + /// + /// The optional section name within the JSON file to bind to the configuration object. If not + /// specified, the entire file is considered. + /// + /// The updated instance with the mutable configuration added. + public IHostBuilder AddMutableConfiguration(string filePath, JsonTypeInfo typeInfo, bool optionalFile = true, string? sectionName = null) where T : class + => builder.ConfigureServices((hostBuilderContext, serviceCollection) => + { + if (!Path.IsJson(filePath)) + throw new ArgumentException("File must be of the json file type", nameof(filePath)); + + serviceCollection.AddOptionsWithValidateOnStart() + .Bind(sectionName is null ? hostBuilderContext.Configuration : hostBuilderContext.Configuration.GetRequiredSection(sectionName)); + serviceCollection.AddSingleton(new JsonFileConfigurationWriter(filePath, typeInfo)); + serviceCollection.AddSingleton, MutableConfiguration>(); + }).ConfigureAppConfiguration(configurationBuilder => + { + configurationBuilder.AddJsonFile(filePath, optionalFile, true); + }); + } + + extension(IServiceCollection services) { - public void RegisterViewWithViewModel() - where TViewModel : ViewModelBase + /// + /// Registers and as a + /// keyed pair in the service collection, enforcing the View/ViewModel naming convention. + /// Required properties on the view are resolved and injected via + /// at activation time. + /// + /// The window type to register. + /// The ViewModel type to register. + /// The for both registrations. + /// + /// Thrown if the naming convention between and + /// is not satisfied. + /// + public void RegisterViewWithViewModel(ServiceLifetime lifetime = ServiceLifetime.Transient) + where TViewModel : ViewModelBase where TView : Window { + var viewType = typeof(TView); + var viewModelType = typeof(TViewModel); if (!ValidateNamingConvention()) throw new InvalidOperationException( - $"Viewmodels must follow the following convention: $NameOfView + $ViewModel -> in that case your Viewmodel must be renamed to \"{typeof(TView).Name + "ViewModel"}\""); + $"Viewmodels must follow the following convention: $NameOfView + $ViewModel -> in that case your Viewmodel must be renamed to \"{viewType.Name + "ViewModel"}\""); + + var serviceDescriptorView = ServiceDescriptor.DescribeKeyed( + viewType, viewType.Name, (provider, _) => + { + if (ActivatorUtilities.CreateInstance(provider, viewType) is not TView view) + throw new InvalidOperationException(); + foreach (var requiredProperty in GetRequiredProperties(viewType)) + { + if (provider.GetService(requiredProperty.PropertyType) is { } service) + requiredProperty.SetValue(view, service); + } + return view; + }, lifetime); + + var serviceDescriptorViewModel = + ServiceDescriptor.DescribeKeyed(viewModelType, viewModelType.Name, viewModelType, lifetime); - container.Register(serviceKey: typeof(TView).Name, reuse: Reuse.Transient, made: Made.Of(propertiesAndFields: PropertiesAndFields.Auto)); - container.Register(serviceKey: typeof(TViewModel).Name, reuse: Reuse.Transient); + services.Add(serviceDescriptorView); + services.Add(serviceDescriptorViewModel); } } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Extensions/IPrivateKeySourceExtensions.cs b/OpenSSH_GUI.Core/Extensions/IPrivateKeySourceExtensions.cs deleted file mode 100644 index 3913825..0000000 --- a/OpenSSH_GUI.Core/Extensions/IPrivateKeySourceExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Renci.SshNet; -using SshNet.Keygen.Extensions; - -namespace OpenSSH_GUI.Core.Extensions; - -/// -/// Provides extension methods for the IPrivateKeySource interface. -/// -public static class PrivateKeySourceExtensions -{ - /// - /// Retrieves the fingerprint hash of the private key source. - /// - /// The private key source. - /// The fingerprint hash of the private key source. - public static string FingerprintHash(this IPrivateKeySource privateKeySource) - { - return privateKeySource - .Fingerprint() - .Split(' ') - .First(e => e.StartsWith("SHA")) - .Split(':') - .Last(); - } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Extensions/PathExtensions.cs b/OpenSSH_GUI.Core/Extensions/PathExtensions.cs new file mode 100644 index 0000000..9ff9eec --- /dev/null +++ b/OpenSSH_GUI.Core/Extensions/PathExtensions.cs @@ -0,0 +1,162 @@ +using System.Diagnostics.CodeAnalysis; + +namespace OpenSSH_GUI.Core.Extensions; + +public static class PathExtensions +{ + /// + /// Represents the file extension for JSON files. + /// + public const string JsonExtension = "json"; + + /// + /// Represents the file extension used for log files. + /// + public const string LogExtension = "log"; + + /// + /// Represents the file extension for OpenSSH Public Key format. + /// + public const string OpenSshPublicKeyFileExtension = "pub"; + + /// + /// Represents the file extension used for PuTTY private key files. + /// + public const string PuttyKeyFileExtension = "ppk"; + + extension(Path) + { + /// + /// Appends the ".json" extension to the specified base file name. + /// + /// The base file name to which the ".json" extension will be added. + /// A string representing the file name with the ".json" extension appended. + public static string WithJsonExtension(string baseName) + => Path.ChangeExtension(baseName, JsonExtension); + + /// + /// Appends the ".log" file extension to the specified base file name. + /// + /// The base file name for which the ".log" extension should be added. + /// A string representing the base file name combined with the ".log" extension. + public static string WithLogExtension(string baseName) + => Path.ChangeExtension(baseName, LogExtension); + + /// + /// Appends the OpenSSH Public Key file extension to the specified base file name. + /// + /// The base name of the file to which the extension will be appended. + /// The modified file name with the OpenSSH Public Key file extension. + public static string WithOpenSshPublicKeyExtension(string baseName) + => Path.ChangeExtension(baseName, OpenSshPublicKeyFileExtension); + + /// + /// Changes the file extension of the given base name to the PuTTY private key extension (.ppk). + /// + /// The base name of the file whose extension will be changed. + /// The file name with the PuTTY private key file extension (.ppk). + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static string WithPuTTYKeyExtension(string baseName) + => Path.ChangeExtension(baseName, PuttyKeyFileExtension); + + /// + /// Determines whether the provided file path has a JSON file extension. + /// The comparison is performed in a case-insensitive manner. + /// + /// The file path to evaluate. + /// true if the file path ends with the ".json" extension; otherwise false. + public static bool IsJson(string path) + => path.EndsWith(JsonExtension, StringComparison.OrdinalIgnoreCase); + + /// + /// Determines whether the given path has the ".log" file extension. + /// Comparison is performed case-insensitively. + /// + /// The file path to check. + /// true if the path ends with ".log"; otherwise false. + public static bool IsLog(string path) + => path.EndsWith(LogExtension, StringComparison.OrdinalIgnoreCase); + /// + /// Determines whether the given path represents a PuTTY private key file (.ppk). + /// Comparison is performed case-insensitively. + /// + /// The file path to check. + /// true if the path ends with the PuTTY key file extension; otherwise false. + [SuppressMessage("ReSharper", "InconsistentNaming")] + public static bool IsPuTTYKey(string path) + => path.EndsWith(PuttyKeyFileExtension, StringComparison.OrdinalIgnoreCase); + /// + /// Determines whether the given path represents an OpenSSH public key file. + /// Comparison is performed case-insensitively. + /// + /// The file path to check. + /// true if the path ends with the OpenSSH public key file extension; otherwise false. + public static bool IsOpenSshPublicKey(string path) + => path.EndsWith(OpenSshPublicKeyFileExtension, StringComparison.OrdinalIgnoreCase); + + /// + /// Determines whether the given path has the specified file extension. + /// Comparison is performed case-insensitively. + /// + /// The file path to check. + /// The extension to compare (e.g. ".json"). + /// true if the path ends with the given extension; otherwise false. + public static bool HasExtension(string path, string extension) + => path.EndsWith(extension, StringComparison.OrdinalIgnoreCase); + + /// + /// Gets the file extension of the specified path in normalized form (always lower-case). + /// + /// The file path. + /// The normalized file extension including the leading dot, or an empty string. + public static string GetNormalizedExtension(string path) + => Path.GetExtension(path).ToLowerInvariant(); + } + + extension(Directory) + { + /// + /// Creates a directory at the specified path if it does not already exist. + /// Automatically ensures that the directory structure exists. + /// + /// The full path of the directory to create. + public static void CreateIfNotExists(string? path) + { + if (!Directory.Exists(path) && !string.IsNullOrWhiteSpace(path)) + Directory.CreateDirectory(path); + } + } + + extension(File) + { + /// + /// Creates a file at the specified path if it does not already exist. + /// Automatically ensures that the directory structure exists. + /// + /// The full path of the file to create. + /// The content to write to the file, defaults to + public static void CreateIfNotExists(string? path, string? content = null) + { + ArgumentNullException.ThrowIfNull(path); + if (File.Exists(path)) + return; + Directory.CreateIfNotExists(Path.GetDirectoryName(path)); + + using var createdFile = File.Create(path); + if (content is null) + return; + using var writer = new StreamWriter(createdFile); + writer.Write(content); + } + + /// + /// Deletes the file at the specified path if it exists. + /// + /// The full path of the file to delete. + public static void RemoveIfExists(string path) + { + if (File.Exists(path)) + File.Delete(path); + } + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Extensions/PlatformIdExtensions.cs b/OpenSSH_GUI.Core/Extensions/PlatformIdExtensions.cs new file mode 100644 index 0000000..f6b4ba3 --- /dev/null +++ b/OpenSSH_GUI.Core/Extensions/PlatformIdExtensions.cs @@ -0,0 +1,27 @@ +namespace OpenSSH_GUI.Core.Extensions; + +/// +/// Provides extension methods for . +/// +public static class PlatformIdExtensions +{ + private const string UnixLineSeparator = "\n"; + private const string WindowsLineSeparator = "\r\n"; + + /// + /// Returns the line separator string used by the given platform. + /// + /// The target platform identifier. + /// \n for Unix-like platforms, \r\n for all Windows variants. + public static string GetLineSeparator(this PlatformID platformId) + { + return platformId switch + { + PlatformID.Win32NT or + PlatformID.Win32Windows or + PlatformID.Win32S or + PlatformID.WinCE => WindowsLineSeparator, + _ => UnixLineSeparator + }; + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Extensions/SshConfigExtensions.cs b/OpenSSH_GUI.Core/Extensions/SshConfigExtensions.cs index 0f406d6..9082b20 100644 --- a/OpenSSH_GUI.Core/Extensions/SshConfigExtensions.cs +++ b/OpenSSH_GUI.Core/Extensions/SshConfigExtensions.cs @@ -1,5 +1,4 @@ -using OpenSSH_GUI.Core.Interfaces.Credentials; -using OpenSSH_GUI.Core.Lib.Credentials; +using OpenSSH_GUI.Core.Lib.Misc; using OpenSSH_GUI.SshConfig.Models; namespace OpenSSH_GUI.Core.Extensions; @@ -15,10 +14,10 @@ public static class SshConfigExtensions /// from which the connection credentials will be extracted. /// /// - /// An enumerable collection of objects that + /// An enumerable collection of objects that /// represent the normalized connection details, such as hostname, username, and authentication method. /// - public static IEnumerable GetConnectionEntriesFromConfig(this SshConfigDocument document) + public static IEnumerable GetConnectionEntriesFromConfig(this SshConfigDocument document) { var globalUser = document.GetGlobalEntries("User").FirstOrDefault()?.Value; var globalPort = document.GetGlobalEntries("Port").FirstOrDefault()?.Value; diff --git a/OpenSSH_GUI.Core/Extensions/SshConfigFilesExtension.cs b/OpenSSH_GUI.Core/Extensions/SshConfigFilesExtension.cs index 96159b9..1e87942 100644 --- a/OpenSSH_GUI.Core/Extensions/SshConfigFilesExtension.cs +++ b/OpenSSH_GUI.Core/Extensions/SshConfigFilesExtension.cs @@ -91,14 +91,15 @@ public static string GetBaseSshPath(bool resolve = true, PlatformID? platformId /// The file path as a . public static string GetPathOfFile(this SshConfigFiles files, bool resolve = true, PlatformID? platform = null) { - var path = Path.Combine(files switch - { - SshConfigFiles.Authorized_Keys or - SshConfigFiles.Known_Hosts or - SshConfigFiles.Config => GetBaseSshPath(resolve, platform), - SshConfigFiles.Sshd_Config => GetRootSshPath(resolve, platform), - _ => throw new ArgumentException("Invalid value for \"files\"") - }, Enum.GetName(files)!.ToLower()); + var path = Path.Combine( + files switch + { + SshConfigFiles.Authorized_Keys or + SshConfigFiles.Known_Hosts or + SshConfigFiles.Config => GetBaseSshPath(resolve, platform), + SshConfigFiles.Sshd_Config => GetRootSshPath(resolve, platform), + _ => throw new ArgumentException("Invalid value for \"files\"") + }, Enum.GetName(files)!.ToLower()); platform ??= Environment.OSVersion.Platform; path = platform is PlatformID.Unix ? path.Replace('\\', '/') : path.Replace('/', '\\'); return path; diff --git a/OpenSSH_GUI.Core/Extensions/SshKeyFormatExtension.cs b/OpenSSH_GUI.Core/Extensions/SshKeyFormatExtension.cs index ae691a7..aab129b 100644 --- a/OpenSSH_GUI.Core/Extensions/SshKeyFormatExtension.cs +++ b/OpenSSH_GUI.Core/Extensions/SshKeyFormatExtension.cs @@ -7,16 +7,6 @@ namespace OpenSSH_GUI.Core.Extensions; /// public static class SshKeyFormatExtension { - /// - /// Represents the file extension for OpenSSH Public Key format. - /// - public const string OpenSshPublicKeyFileExtension = ".pub"; - - /// - /// Represents the file extension used for PuTTY private key files. - /// - public const string PuttyKeyFileExtension = ".ppk"; - /// The SSH key format. extension(SshKeyFormat format) { @@ -32,9 +22,9 @@ public static class SshKeyFormatExtension { return format switch { - SshKeyFormat.OpenSSH when usePublicFormat => OpenSshPublicKeyFileExtension, + SshKeyFormat.OpenSSH when usePublicFormat => PathExtensions.OpenSshPublicKeyFileExtension, SshKeyFormat.OpenSSH => null, - SshKeyFormat.PuTTYv2 or SshKeyFormat.PuTTYv3 => PuttyKeyFileExtension, + SshKeyFormat.PuTTYv2 or SshKeyFormat.PuTTYv3 => PathExtensions.PuttyKeyFileExtension, _ => null }; } @@ -45,9 +35,6 @@ public static class SshKeyFormatExtension /// The path to the file. /// Indicates whether the key is public. Default is true. /// The modified file path with the updated extension. - public string ChangeExtension(string path, bool usePublicFormat = true) - { - return Path.ChangeExtension(path, format.GetExtension(usePublicFormat)); - } + public string ChangeExtension(string path, bool usePublicFormat = true) => Path.ChangeExtension(path, format.GetExtension(usePublicFormat)); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Extensions/StringExtensions.cs b/OpenSSH_GUI.Core/Extensions/StringExtensions.cs index 76ad50d..28151f4 100644 --- a/OpenSSH_GUI.Core/Extensions/StringExtensions.cs +++ b/OpenSSH_GUI.Core/Extensions/StringExtensions.cs @@ -15,7 +15,8 @@ public static partial class StringExtensions extension(string input) { /// - /// Resolves a absolute path from a relative path which can contain ~ or ~user or %AppData% or %UserProfile% etc. + /// Resolves a absolute path from a relative path which can contain ~ or ~user or %AppData% or + /// %UserProfile% etc. /// public string ResolvePath() { @@ -26,7 +27,7 @@ public string ResolvePath() path = path.Length == 1 ? home : Path.Combine(home, path[2..]); return Path.GetFullPath(path); } - + /// /// Wraps the input string to the specified maximum length, optionally enclosing each chunk in a specified character. /// @@ -50,10 +51,7 @@ public string ResolvePath() /// // pping. /// /// - public string Wrap(int maxLength, char? wrapper = null) - { - return input.Wrap(maxLength, wrapper is null ? null : wrapper.ToString()); - } + public string Wrap(int maxLength, char? wrapper = null) => input.Wrap(maxLength, wrapper is null ? null : wrapper.ToString()); /// /// Wraps the input string to the specified maximum length, optionally enclosing each chunk in a specified string. @@ -71,11 +69,9 @@ public string Wrap(int maxLength, char? wrapper = null) /// // This is a | long stri | ng that n | eeds wra | pping. /// /// - public string Wrap(int maxLength, string? wrapper = null) - { - return string.Join(wrapper ?? Environment.NewLine, - EcapeRegex().Replace(input, "").SplitToChunks(maxLength)); - } + public string Wrap(int maxLength, string? wrapper = null) => string.Join( + wrapper ?? Environment.NewLine, + EcapeRegex().Replace(input, string.Empty).SplitToChunks(maxLength)); /// /// Splits the input string into chunks of the specified size. @@ -119,10 +115,7 @@ public IEnumerable SplitToChunks(int chunkSize) /// // pascal_case_string /// /// - public string ToSnakeCase() - { - return Regex.Replace(input, "(? Regex.Replace(input, "(? /// Converts the given string to camelCase. @@ -158,10 +151,7 @@ public string ToCamelCase() /// // pascal-case-string /// /// - public string ToKebabCase() - { - return Regex.Replace(input, "(? Regex.Replace(input, "(? /// Converts the given string to PascalCase. @@ -177,10 +167,7 @@ public string ToKebabCase() /// // SnakeCaseString /// /// - public string ToPascalCase() - { - return Regex.Replace(input, @"(^\w)|(\s\w)", m => m.Value.ToUpper()).Replace(" ", ""); - } + public string ToPascalCase() { return Regex.Replace(input, @"(^\w)|(\s\w)", m => m.Value.ToUpper()).Replace(" ", string.Empty); } /// /// Converts the given string to Title Case. @@ -196,10 +183,7 @@ public string ToPascalCase() /// // This Is A Title Case String /// /// - public string ToTitleCase() - { - return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLower()); - } + public string ToTitleCase() => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLower()); /// /// Converts the given string to Sentence case. @@ -238,7 +222,8 @@ public string ToSentenceCase() public string ToStudlyCaps() { var random = new Random(); - return input.Aggregate("", + return input.Aggregate( + string.Empty, (current, t) => current + (random.Next(2) == 0 ? char.ToUpper(t) : char.ToLower(t))); } @@ -256,9 +241,6 @@ public string ToStudlyCaps() /// // 133t Sp34k 15 c00l! /// /// - public string ToLeetSpeak() - { - return input.Replace('e', '3').Replace('a', '4').Replace('o', '0').Replace('i', '1').Replace('s', '5'); - } + public string ToLeetSpeak() => input.Replace('e', '3').Replace('a', '4').Replace('o', '0').Replace('i', '1').Replace('s', '5'); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/Credentials/IConnectionCredentials.cs b/OpenSSH_GUI.Core/Interfaces/Credentials/IConnectionCredentials.cs deleted file mode 100644 index 75c38f5..0000000 --- a/OpenSSH_GUI.Core/Interfaces/Credentials/IConnectionCredentials.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json.Serialization; -using OpenSSH_GUI.Core.Enums; -using Renci.SshNet; - -namespace OpenSSH_GUI.Core.Interfaces.Credentials; - -/// -/// Represents the interface for connection credentials. -/// -public interface IConnectionCredentials -{ - /// - /// Represents the host name for a connection. - /// - /// - /// The host name is an essential property for establishing a connection to a remote server. - /// It identifies the target server that the client wants to connect to. - /// - string Hostname { get; set; } - - /// - /// Represents the base class for connection credentials. - /// - int Port { get; } - - /// - /// Represents the username used for the connection credentials. - /// - string Username { get; set; } - - /// - /// Retrieves the connection information based on the provided credentials. - /// - /// The object representing the SSH connection information. - ConnectionInfo GetConnectionInfo(); -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/Credentials/IKeyConnectionCredentials.cs b/OpenSSH_GUI.Core/Interfaces/Credentials/IKeyConnectionCredentials.cs deleted file mode 100644 index 810976c..0000000 --- a/OpenSSH_GUI.Core/Interfaces/Credentials/IKeyConnectionCredentials.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; -using OpenSSH_GUI.Core.Lib.Keys; - -namespace OpenSSH_GUI.Core.Interfaces.Credentials; - -/// -/// Represents connection credentials for SSH using key-based authentication. -/// -public interface IKeyConnectionCredentials : IConnectionCredentials -{ - /// - /// Represents a connection credential that includes an SSH key. - /// - [JsonIgnore] - SshKeyFile? Key { get; set; } - - /// - /// Renews the SSH key used for authentication. - /// - /// The password for the key file (optional). - void RenewKey(string? password = null); -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/Credentials/IMultiKeyConnectionCredentials.cs b/OpenSSH_GUI.Core/Interfaces/Credentials/IMultiKeyConnectionCredentials.cs deleted file mode 100644 index 300156a..0000000 --- a/OpenSSH_GUI.Core/Interfaces/Credentials/IMultiKeyConnectionCredentials.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; -using OpenSSH_GUI.Core.Lib.Keys; - -namespace OpenSSH_GUI.Core.Interfaces.Credentials; - -/// -/// Represents the interface for multi-key connection credentials. -/// -public interface IMultiKeyConnectionCredentials : IConnectionCredentials -{ - /// - /// Represents the credentials for a multi-key connection. - /// - [JsonIgnore] - IEnumerable? Keys { get; set; } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/Credentials/IPasswordConnectionCredentials.cs b/OpenSSH_GUI.Core/Interfaces/Credentials/IPasswordConnectionCredentials.cs deleted file mode 100644 index e987cf3..0000000 --- a/OpenSSH_GUI.Core/Interfaces/Credentials/IPasswordConnectionCredentials.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace OpenSSH_GUI.Core.Interfaces.Credentials; - -/// -/// Represents the interface for password-based connection credentials. -/// -public interface IPasswordConnectionCredentials : IConnectionCredentials -{ - /// - /// Represents the password connection credentials. - /// - string Password { get; set; } - - /// - /// Gets or sets a value indicating whether the password is encrypted. - /// - bool EncryptedPassword { get; set; } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/Hosts/IDialogHost.cs b/OpenSSH_GUI.Core/Interfaces/Hosts/IDialogHost.cs index 2dbd3f5..604e559 100644 --- a/OpenSSH_GUI.Core/Interfaces/Hosts/IDialogHost.cs +++ b/OpenSSH_GUI.Core/Interfaces/Hosts/IDialogHost.cs @@ -1,12 +1,8 @@ using Avalonia.Controls; -using OpenSSH_GUI.Core.MVVM; namespace OpenSSH_GUI.Core.Interfaces.Hosts; public interface IDialogHost { public Task ShowDialog(TWindow dialogWindow) where TWindow : Window; - - public Task ShowDialog(TWindow dialogWindow) - where TWindow : Window where TResult : ViewModelBase; } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/IDirectoryCrawler.cs b/OpenSSH_GUI.Core/Interfaces/IDirectoryCrawler.cs new file mode 100644 index 0000000..b2c4714 --- /dev/null +++ b/OpenSSH_GUI.Core/Interfaces/IDirectoryCrawler.cs @@ -0,0 +1,23 @@ +using OpenSSH_GUI.Core.Lib.Keys; + +namespace OpenSSH_GUI.Core.Interfaces; + +/// +/// Defines the contract for discovering SSH key file sources on disk. +/// +public interface IDirectoryCrawler +{ + /// + /// Gets a value indicating whether a key file search is currently in progress. + /// + bool IsSearching { get; } + + /// + /// Asynchronously enumerates possible SSH key file sources from both + /// the SSH configuration and the base SSH directory on disk. + /// + /// Token to cancel the enumeration. + /// An async stream of discovered instances. + IAsyncEnumerable GetPossibleKeyFilesOnDiskAsyncEnumerable( + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/IKeyFileBackupService.cs b/OpenSSH_GUI.Core/Interfaces/IKeyFileBackupService.cs new file mode 100644 index 0000000..f6b6f91 --- /dev/null +++ b/OpenSSH_GUI.Core/Interfaces/IKeyFileBackupService.cs @@ -0,0 +1,81 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Lib.Misc; + +namespace OpenSSH_GUI.Core.Interfaces; + +/// +/// Provides file backup, restore, and deletion capabilities for SSH key operations, +/// as well as operation-scoped file logging to capture diagnostic output during +/// potentially destructive file system changes. +/// +public interface IKeyFileBackupService +{ + /// + /// Creates backup copies of the specified files in the backup directory. + /// Each backup is named after the original file with the backup extension appended. + /// + /// The files to back up. + /// + /// A sequence of instances representing the + /// original file and its corresponding backup location. + /// + IEnumerable BackupFiles(params FileInfo[] files); + + /// + /// Restores the specified backed-up files to their original locations, + /// overwriting any existing files at those paths. + /// + /// The backed-up files to restore. + void RestoreBackupFiles(params BackedUpFile[] files); + + /// + /// Deletes the backup copies of the specified files from the backup directory. + /// Should only be called after a successful operation. + /// + /// The backed-up files whose backup copies should be deleted. + void DeleteBackupFiles(params BackedUpFile[] files); + + /// + /// Begins an operation-scoped file log session. + /// Creates the backup directory if it does not exist and initializes a + /// Serilog file sink writing to operation_log.log within that directory. + /// Subsequent calls while a session is already active are no-ops. + /// + void BeginOperationLog(); + + /// + /// Ends the current operation-scoped file log session and releases all associated resources. + /// If is , the entire backup directory + /// is deleted on the assumption that no recovery artifacts need to be retained. + /// + /// + /// to retain the backup directory and log file for post-mortem inspection; + /// to delete the backup directory after the session ends. + /// + void EndOperationLog(bool errorsOccurred = false); + + /// + /// Writes a structured log message at the specified level exclusively to the + /// active operation-scoped file log. Does not write to the application logger — + /// the caller is responsible for that channel separately. + /// If no operation log session is currently active, the call is a no-op. + /// + /// The severity level of the log entry. + /// The structured message template. + /// Arguments to substitute into the message template. + void WriteToOperationLog(LogLevel level, [StructuredMessageTemplate] string? message, params object?[] args); + + /// + /// Writes a structured log message with an associated exception at the specified level + /// exclusively to the active operation-scoped file log. Does not write to the application + /// logger — the caller is responsible for that channel separately. + /// If no operation log session is currently active, the call is a no-op. + /// + /// The severity level of the log entry. + /// The exception to associate with the log entry. + /// The structured message template. + /// Arguments to substitute into the message template. + void WriteToOperationLog(LogLevel level, Exception? exception, [StructuredMessageTemplate] string? message, + params object?[] args); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/IKeyFileWriterService.cs b/OpenSSH_GUI.Core/Interfaces/IKeyFileWriterService.cs new file mode 100644 index 0000000..1163243 --- /dev/null +++ b/OpenSSH_GUI.Core/Interfaces/IKeyFileWriterService.cs @@ -0,0 +1,72 @@ +using System.Text; +using Renci.SshNet; +using SshNet.Keygen; +using SshNet.Keygen.SshKeyEncryption; + +namespace OpenSSH_GUI.Core.Interfaces; + +public interface IKeyFileWriterService +{ + /// + /// Writes the specified content to a file at the given file path, with optional encoding and overwrite behavior. + /// + /// The path of the file to write the content to. + /// The content to write to the file. + /// A flag indicating whether to overwrite the file if it already exists. Defaults to false. + /// The encoding to use when writing the file. Defaults to UTF-8 if not specified. + /// A task representing the asynchronous write operation. + /// Thrown if the file already exists and overwrite is set to false. + /// Thrown when an error occurs during the file writing operation. + ValueTask WriteToFile(string filePath, string content, + bool overwrite = false, Encoding? encoding = null); + /// + /// Writes a private key and its corresponding public key (if applicable) to files in a specific SSH key format. + /// + /// + /// The SSH key format to use for writing the files (e.g., OpenSSH, PuTTYv2, PuTTYv3). + /// + /// + /// The encryption strategy to apply to the private key. + /// + /// + /// The source of the private key to be written to the file. + /// + /// + /// The base file path where the SSH key files will be written. Extensions will be added based on the key format. + /// + /// + /// A boolean value indicating whether to overwrite existing files. Default value is false. + /// + /// + /// A task that represents the asynchronous operation. The task result contains an enumerable collection of file paths + /// to the written SSH key files. + /// + ValueTask> WriteToFileInSpecificFormat( + SshKeyFormat format, + ISshKeyEncryption encryption, + IPrivateKeySource privateKeySource, string filePath, bool overwrite = false); + + /// + /// Writes a private key and its corresponding public key (if applicable) to files in a specific SSH key format. + /// This overload extracts the format and encryption settings from the object. + /// + /// + /// The SSH key generation information containing the key format and encryption settings. + /// + /// + /// The generated private key to be written to the file. + /// + /// + /// The base file path where the SSH key files will be written. Extensions will be added based on the key format. + /// + /// + /// A boolean value indicating whether to overwrite existing files. Default value is false. + /// + /// + /// A task that represents the asynchronous operation. The task result contains an enumerable collection of file paths + /// to the written SSH key files. + /// + ValueTask> WriteToFileInSpecificFormat( + SshKeyGenerateInfo generateInfo, + GeneratedPrivateKey createdKey, string filePath, bool overwrite = false); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/IMutableConfiguration.cs b/OpenSSH_GUI.Core/Interfaces/IMutableConfiguration.cs new file mode 100644 index 0000000..dc00ead --- /dev/null +++ b/OpenSSH_GUI.Core/Interfaces/IMutableConfiguration.cs @@ -0,0 +1,65 @@ +using System.Linq.Expressions; + +namespace OpenSSH_GUI.Core.Interfaces; + +/// +/// Represents a writable configuration that allows dynamic updates and overrides of configuration values at runtime. +/// +/// The type of the configuration class. +public interface IMutableConfiguration : IDisposable where T : class +{ + /// + /// Gets the current instance of the configuration object of type . + /// + /// + /// This property provides access to the current state of the configuration as managed by the underlying options + /// mechanism. + /// It reflects the current configuration values without the need to manually reload or retrieve them. + /// + T Current { get; } + + /// + /// Asynchronously updates the configuration object by applying the specified update action. + /// + /// + /// An action that performs updates on the configuration object. + /// The current configuration is passed to this action. + /// + /// + /// A that can be used to cancel the operation. Defaults to + /// . + /// + /// + /// A that represents the asynchronous operation. + /// + Task ExecuteConfigurationUpdateAsync(Action update, CancellationToken ct = default); + + /// + /// Sets the value of a specific property in the writable configuration using an expression to target the property. + /// + /// The type of the value being set. + /// An expression representing the property to set. + /// The new value to assign to the specified property. + /// A cancellation token to observe while waiting for the operation to complete. + /// A task that represents the asynchronous operation. + Task SetPropertyValueAsync(Expression> property, TValue value, CancellationToken ct = default); + + /// Asynchronously updates the configuration by setting a specific key to the provided value. + /// The key in the configuration to set the value for. + /// The value to assign to the specified key. + /// The optional cancellation token to cancel the operation. + /// The type of the value being set. + /// A task representing the asynchronous operation. + Task SetPropertyValueAsync(string key, TValue value, CancellationToken ct = default); + + + /// + /// Occurs when the configuration is updated, signaling that changes have been applied to the configuration values. + /// + /// + /// Subscribing to this event allows components to react to configuration changes dynamically at runtime. + /// This can be particularly useful for scenarios where live updates to settings or parameters require immediate + /// processing. + /// + event EventHandler ConfigurationChanged; +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/ISshKeyFactory.cs b/OpenSSH_GUI.Core/Interfaces/ISshKeyFactory.cs new file mode 100644 index 0000000..979031b --- /dev/null +++ b/OpenSSH_GUI.Core/Interfaces/ISshKeyFactory.cs @@ -0,0 +1,14 @@ +using OpenSSH_GUI.Core.Lib.Keys; + +namespace OpenSSH_GUI.Core.Interfaces; + +/// +/// Factory for creating new instances. +/// +public interface ISshKeyFactory +{ + /// + /// Creates a new, uninitialized instance. + /// + SshKeyFile Create(); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/ISshKeyGenerator.cs b/OpenSSH_GUI.Core/Interfaces/ISshKeyGenerator.cs new file mode 100644 index 0000000..1e861d4 --- /dev/null +++ b/OpenSSH_GUI.Core/Interfaces/ISshKeyGenerator.cs @@ -0,0 +1,16 @@ +using OpenSSH_GUI.Core.Lib.Keys; +using SshNet.Keygen; + +namespace OpenSSH_GUI.Core.Interfaces; + +public interface ISshKeyGenerator +{ + /// + /// Generates a new SSH key. + /// + /// The full path where the new key should be stored. + /// Parameters for key generation. + /// Whether to overwrite existing file if it exists. + /// A value task representing the asynchronous operation. + ValueTask Generate(string fullFilePath, SshKeyGenerateInfo generateParamsInfo, bool overwrite = false); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHost.cs b/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHost.cs deleted file mode 100644 index 487dc4c..0000000 --- a/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHost.cs +++ /dev/null @@ -1,40 +0,0 @@ -using ReactiveUI; - -namespace OpenSSH_GUI.Core.Interfaces.KnownHosts; - -/// -/// Represents a known host in the OpenSSH GUI. -/// -public interface IKnownHost : IReactiveObject -{ - /// - /// Represents a known host. - /// - string Host { get; } - - /// - /// Represents a known host in the OpenSSH GUI. - /// - bool DeleteWholeHost { get; } - - /// - /// Represents a known host in the OpenSSH GUI. - /// - List Keys { get; set; } - - /// - /// Toggles the marked for deletion flag of each within the list. - /// If the property is true, it sets the flag to false for all keys. Otherwise, it sets - /// the flag to true for all keys. - /// - void KeysDeletionSwitch(); - - /// - /// Retrieves all entries for a known host in the known hosts file. - /// - /// - /// Returns a string containing all the entries for the known host. - /// If the entire host is marked for deletion, returns the line ending character. - /// - string GetAllEntries(); -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostKey.cs b/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostKey.cs deleted file mode 100644 index f2e59d7..0000000 --- a/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostKey.cs +++ /dev/null @@ -1,29 +0,0 @@ -using ReactiveUI; -using SshNet.Keygen; - -namespace OpenSSH_GUI.Core.Interfaces.KnownHosts; - -/// Represents a known host key in the OpenSSH GUI. -/// / -public interface IKnownHostKey : IReactiveObject -{ - /// - /// Represents the type of a known host key. - /// - SshKeyType KeyType { get; } - - /// - /// Represents a known host key in the OpenSSH GUI. - /// - string Fingerprint { get; } - - /// - /// Represents a known host key in the OpenSSH GUI. - /// - string EntryWithoutHost { get; } - - /// - /// Gets or sets whether the KnownHostKey is marked for deletion. - /// - bool MarkedForDeletion { get; set; } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostsFile.cs b/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostsFile.cs deleted file mode 100644 index da5f332..0000000 --- a/OpenSSH_GUI.Core/Interfaces/KnownHosts/IKnownHostsFile.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.ObjectModel; -using ReactiveUI; - -namespace OpenSSH_GUI.Core.Interfaces.KnownHosts; - -/// -/// Represents a known hosts file. -/// -public interface IKnownHostsFile : IReactiveObject -{ - /// - /// Represents the line ending character used in the known_hosts file. - /// - static string LineEnding { get; set; } = string.Empty; - - /// - /// Represents a file that contains known host entries. - /// - ObservableCollection KnownHosts { get; } - - /// - /// Asynchronously reads the contents of the known hosts file. - /// - /// - /// The file stream to read from. If null, the method reads from the file specified in the - /// constructor. - /// - /// A representing the asynchronous operation. - ValueTask ReadContentAsync(FileStream? stream = null); - - /// - /// Synchronizes the known hosts with the given list of new known hosts. - /// - /// The new known hosts to synchronize. - void SyncKnownHosts(IEnumerable newKnownHosts); - - /// - /// Updates the content of the known hosts file asynchronously. - /// - /// A representing the update operation. - ValueTask UpdateFileAsync(); - - /// - /// Initializes the known hosts file asynchronously. - /// - /// The path to the known hosts file or its content. - /// Indicates whether the content is from a server. - /// A cancellation token. - /// A representing the initialized object. - ValueTask InitializeAsync(string knownHostsPathOrContent, bool fromServer = false, - CancellationToken token = default); - - /// - /// Retrieves the updated contents of the known hosts file. - /// - /// The platform ID of the server. - /// The updated contents of the known hosts file as a string. - /// - /// This method retrieves the updated contents of the known hosts file. - /// It takes the platform ID of the server as a parameter and returns the - /// updated contents of the known hosts file as a string. The method - /// checks if the instance of the KnownHostsFile class is created from - /// the server or not. If it is not created from the server, it returns - /// an empty string. It sets the LineEnding property based on the platform - /// ID provided. It then aggregates the known hosts entries excluding those - /// which are flagged for deletion and returns the updated contents as a string. - /// - /// The platform ID of the server. - string GetUpdatedContents(PlatformID platformId); -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKey.cs b/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKey.cs index 16c2f4a..3f14166 100644 --- a/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKey.cs +++ b/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKey.cs @@ -19,7 +19,7 @@ private AuthorizedKey(string keyEntry) KeyTypeDeclarationInFile = split[0]; KeyType = Enum.Parse( KeyTypeDeclarationInFile.StartsWith("ssh-") - ? KeyTypeDeclarationInFile.Replace("ssh-", "") + ? KeyTypeDeclarationInFile.Replace("ssh-", string.Empty) : KeyTypeDeclarationInFile.Split('-')[0], true); Fingerprint = split[1]; Comment = split[2]; @@ -61,10 +61,7 @@ private AuthorizedKey(string keyEntry) /// The full key entry string consists of the key type, fingerprint, and comment separated by spaces. /// /// The full key entry string. - public string GetFullKeyEntry => $"{KeyTypeDeclarationInFile} {Fingerprint} {Comment}"; + public override string ToString() => $"{KeyTypeDeclarationInFile} {Fingerprint} {Comment}"; - internal static AuthorizedKey Parse(string keyEntry) - { - return new AuthorizedKey(keyEntry); - } + internal static AuthorizedKey Parse(string keyEntry) => new(keyEntry); } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKeysFile.cs b/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKeysFile.cs index 6168e2d..a545978 100644 --- a/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKeysFile.cs +++ b/OpenSSH_GUI.Core/Lib/AuthorizedKeys/AuthorizedKeysFile.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Text; using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; using OpenSSH_GUI.Core.Lib.Keys; @@ -11,6 +12,8 @@ namespace OpenSSH_GUI.Core.Lib.AuthorizedKeys; /// public class AuthorizedKeysFile : ReactiveObject { + private AuthorizedKey[] _authorizedKeys = []; + /// /// The contents of the authorized keys file or the path to the file. /// @@ -19,10 +22,8 @@ public class AuthorizedKeysFile : ReactiveObject /// /// Represents an authorized keys file. /// - private AuthorizedKeysFile() - { - } - + private AuthorizedKeysFile() { } + /// /// Gets a value indicating whether the file is from a server. /// @@ -37,6 +38,10 @@ public ObservableCollection AuthorizedKeys set => this.RaiseAndSetIfChanged(ref field, value); } = []; + public bool ChangesMade => !_authorizedKeys.SequenceEqual(AuthorizedKeys); + + public static AuthorizedKeysFile Empty { get; } = new(); + public bool CanAddKey(SshKeyFile key) { try @@ -48,7 +53,7 @@ public bool CanAddKey(SshKeyFile key) return false; } } - + /// /// Adds an authorized key to the authorized keys file. /// @@ -61,28 +66,21 @@ public bool AddAuthorizedKey(SshKeyFile key) return true; } - /// - /// Applies the changes to the authorized keys file. - /// - /// The collection of keys to be applied as changes. - /// True if any changes were made to the authorized keys file; otherwise, false. - public bool ApplyChanges(IEnumerable keys) - { - var countBefore = AuthorizedKeys.Count; - AuthorizedKeys = new ObservableCollection(keys.Where(e => !e.MarkedForDeletion)); - return countBefore != AuthorizedKeys.Count; - } - /// /// Persists the changes made to the authorized keys file. /// - /// The modified object. + /// The modified object. public async ValueTask PersistChangesInFileAsync(CancellationToken token = default) { + if (!ChangesMade) return this; if (IsFileFromServer) return this; - await using var file = new FileStream(_fileContentsOrPath, FileMode.Truncate); - await using var streamWriter = new StreamWriter(file); - await streamWriter.WriteAsync(ExportFileContent()); + await using (var file = new FileStream(_fileContentsOrPath, FileMode.Truncate)) + await using (var streamWriter = new StreamWriter(file)) + { + ReadOnlyMemory content = ExportFileContent().ToCharArray(); + await streamWriter.WriteAsync(content, token); + } + await ReadAndLoadFileContents(_fileContentsOrPath, token); return this; } @@ -94,48 +92,24 @@ public async ValueTask PersistChangesInFileAsync(Cancellatio /// /// A indicating whether the key was added successfully. /// - public ValueTask AddAuthorizedKeyAsync(SshKeyFile key) - { - return ValueTask.FromResult(AddAuthorizedKey(key)); - } - - /// - /// Removes the specified SSH key from the authorized keys list. - /// - /// The SSH key to remove. - /// - /// Returns true if the key is successfully removed; - /// otherwise, false. - /// - public bool RemoveAuthorizedKey(SshKeyFile key) - { - if (AuthorizedKeys.All(e => e.Fingerprint != key.Fingerprint)) return false; - { - AuthorizedKeys.Remove(AuthorizedKeys.First(e => e.Fingerprint == key.Fingerprint)); - return true; - } - } + public ValueTask AddAuthorizedKeyAsync(SshKeyFile key) => ValueTask.FromResult(AddAuthorizedKey(key)); /// /// Exports the content of the authorized keys file. /// - /// Indicates whether to export for the local machine or remote server. Default is true (local). /// - /// The platform ID of the server. If null, the current OS platform will be used. Only applicable if - /// 'local' is set to false. + /// The platform ID of the server. If null, the current OS platform will be used /// /// The content of the authorized keys file as a string. - public string ExportFileContent(bool local = true, PlatformID? platform = null) + public string ExportFileContent(PlatformID? platform = null) { - return local - ? AuthorizedKeys.Where(e => !e.MarkedForDeletion) - .Aggregate("", (s, key) => s += $"{key.GetFullKeyEntry}\r\n") - : AuthorizedKeys.Where(e => !e.MarkedForDeletion).Aggregate("", - (s, key) => s += - $"{key.GetFullKeyEntry}{((platform ??= Environment.OSVersion.Platform) != PlatformID.Unix ? "`r`n" : "\r\n")}"); + var builder = new StringBuilder(); + foreach (var authorizedKey in AuthorizedKeys.Where(e => !e.MarkedForDeletion)) + { + builder.Append($"{authorizedKey}{(platform ?? Environment.OSVersion.Platform).GetLineSeparator()}"); + } + return builder.ToString(); } - - public static AuthorizedKeysFile Empty { get; } = new(); public static async ValueTask OpenAsync(string? filePath = null, CancellationToken cancellationToken = default) @@ -163,9 +137,15 @@ public static async ValueTask ParseAsync(Stream stream, /// A token to monitor for cancellation requests. private async ValueTask LoadFromStreamAsync(Stream stream, CancellationToken cancellationToken = default) { + AuthorizedKeys.Clear(); using var streamReader = new StreamReader(stream, detectEncodingFromByteOrderMarks: true, leaveOpen: true); - if (await streamReader.ReadToEndAsync(cancellationToken) is { } fileContents && - !string.IsNullOrWhiteSpace(fileContents)) LoadFileContents(fileContents); + while (await streamReader.ReadLineAsync(cancellationToken) is { } line) + { + var trimmed = line.Trim(); + if (trimmed.Length == 0 || trimmed[0] == '#') + continue; + AuthorizedKeys.Add(AuthorizedKey.Parse(trimmed)); + } if (stream is FileStream fileStream) { @@ -176,29 +156,17 @@ private async ValueTask LoadFromStreamAsync(Stream stream, CancellationToken can { IsFileFromServer = true; } - } - - /// - /// Loads the contents of a file and parses them into a collection of authorized keys. - /// - /// The contents of the file. - private void LoadFileContents(string fileContents) - { - var splittedContents = fileContents - .Split("\r\n", StringSplitOptions.RemoveEmptyEntries) - .Where(e => !string.IsNullOrWhiteSpace(e.Trim())); - AuthorizedKeys = - new ObservableCollection(splittedContents.Select(e => AuthorizedKey.Parse(e.Trim()))); + _authorizedKeys = AuthorizedKeys.ToArray(); } /// /// Reads and loads the contents of a file. /// /// The path to the file to be read and loaded. + /// A cancellation token. private async ValueTask ReadAndLoadFileContents(string pathToFile, CancellationToken cancellationToken = default) { await using var fileStream = File.Open(pathToFile, FileMode.OpenOrCreate); - using var streamReader = new StreamReader(fileStream); - LoadFileContents(await streamReader.ReadToEndAsync(cancellationToken)); + await LoadFromStreamAsync(fileStream, cancellationToken); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Credentials/ConnectionCredentials.cs b/OpenSSH_GUI.Core/Lib/Credentials/ConnectionCredentials.cs deleted file mode 100644 index 1998230..0000000 --- a/OpenSSH_GUI.Core/Lib/Credentials/ConnectionCredentials.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json.Serialization; -using OpenSSH_GUI.Core.Enums; -using OpenSSH_GUI.Core.Interfaces.Credentials; -using Renci.SshNet; - -namespace OpenSSH_GUI.Core.Lib.Credentials; - -/// -/// Represents the base class for connection credentials. -/// -public class ConnectionCredentials(string hostname, string username) - : IConnectionCredentials -{ - /// - /// Represents the hostname of a server. - /// This property is used in classes related to connection credentials and server settings. - /// - public string Hostname { get; set; } = hostname; - - /// - /// Represents the port number used for establishing an SSH connection. - /// - public int Port => Hostname.Contains(':') ? int.Parse(Hostname.Split(':')[1]) : 22; - - /// - /// Represents the username property of a connection credentials. - /// - public string Username { get; set; } = username; - - /// - /// Retrieves the connection information based on the provided credentials. - /// - /// - /// The object representing the SSH connection information. - /// - public virtual ConnectionInfo GetConnectionInfo() - { - return new ConnectionInfo(Hostname, Username); - } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Credentials/KeyConnectionCredentials.cs b/OpenSSH_GUI.Core/Lib/Credentials/KeyConnectionCredentials.cs deleted file mode 100644 index cd58f14..0000000 --- a/OpenSSH_GUI.Core/Lib/Credentials/KeyConnectionCredentials.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json.Serialization; -using OpenSSH_GUI.Core.Enums; -using OpenSSH_GUI.Core.Interfaces.Credentials; -using OpenSSH_GUI.Core.Lib.Keys; -using Renci.SshNet; - -namespace OpenSSH_GUI.Core.Lib.Credentials; - -/// -/// Represents the credentials for a key-based connection to a server. -/// -public class KeyConnectionCredentials : ConnectionCredentials, IKeyConnectionCredentials -{ - /// - /// Represents connection credentials using SSH key authentication. - /// - public KeyConnectionCredentials(string hostname, string username, SshKeyFile? key) : base(hostname, username) - { - Key = key; - } - - /// - /// Represents connection credentials that include an SSH key for authentication. - /// - [JsonIgnore] - public SshKeyFile? Key { get; set; } - - - /// - /// Renews the SSH key used for authentication. - /// - /// The password for the key file (optional). - public void RenewKey(string? password = null) - { - } - - /// - /// Retrieves the connection information based on the provided credentials. - /// - /// - /// The object representing the SSH connection information. - /// - public override ConnectionInfo GetConnectionInfo() - { - return new PrivateKeyConnectionInfo(Hostname, Username, ProxyTypes.None, "", 0, Key?.PrivateKeySource); - } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Credentials/MultiKeyConnectionCredentials.cs b/OpenSSH_GUI.Core/Lib/Credentials/MultiKeyConnectionCredentials.cs deleted file mode 100644 index 1ed6ad7..0000000 --- a/OpenSSH_GUI.Core/Lib/Credentials/MultiKeyConnectionCredentials.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; -using OpenSSH_GUI.Core.Enums; -using OpenSSH_GUI.Core.Interfaces.Credentials; -using OpenSSH_GUI.Core.Lib.Keys; -using Renci.SshNet; - -namespace OpenSSH_GUI.Core.Lib.Credentials; - -/// *MultiKeyConnectionCredentials(string hostname, string username, -/// -/// ? keys)** -public class MultiKeyConnectionCredentials : ConnectionCredentials, IMultiKeyConnectionCredentials -{ - /// - /// Represents a set of connection credentials for a multi-key authentication. - /// - public MultiKeyConnectionCredentials(string hostname, string username, IEnumerable? keys) : base( - hostname, - username) - { - Keys = keys; - } - - /// - /// Represents the credentials for a multi-key SSH connection. - /// - [JsonIgnore] - public IEnumerable? Keys { get; set; } - - - /// - /// Retrieves the connection information for establishing an SSH connection. - /// - /// - /// The object representing the SSH connection information. - /// - public override ConnectionInfo GetConnectionInfo() - { - if (Keys is not { } keys) return new ConnectionInfo(Hostname, Port, Username); - var sources = keys.Select(e => e.PrivateKeySource).ToArray(); - return sources.All(s => s is not null) ? new PrivateKeyConnectionInfo(Hostname, Port, Username, sources) : new ConnectionInfo(Hostname, Port, Username); - } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Credentials/PasswordConnectionCredentials.cs b/OpenSSH_GUI.Core/Lib/Credentials/PasswordConnectionCredentials.cs deleted file mode 100644 index d26b6f2..0000000 --- a/OpenSSH_GUI.Core/Lib/Credentials/PasswordConnectionCredentials.cs +++ /dev/null @@ -1,35 +0,0 @@ -using OpenSSH_GUI.Core.Enums; -using OpenSSH_GUI.Core.Interfaces.Credentials; -using Renci.SshNet; - -namespace OpenSSH_GUI.Core.Lib.Credentials; - -public class PasswordConnectionCredentials( - string hostname, - string username, - string password, - bool encryptedPassword = false) - : ConnectionCredentials(hostname, username), IPasswordConnectionCredentials -{ - /// - /// Represents connection credentials using password authentication. - /// - public string Password { get; set; } = password; - - /// - /// Gets or sets a value indicating whether the password is encrypted. - /// - /// - /// true if the password is encrypted; otherwise, false. - /// - public bool EncryptedPassword { get; set; } = encryptedPassword; - - /// - /// Retrieves the connection information based on the provided credentials. - /// - /// The object representing the SSH connection information. - public override ConnectionInfo GetConnectionInfo() - { - return new PasswordConnectionInfo(Hostname, Username, Password); - } -} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Keys/BasicSshKeyFileInformation.cs b/OpenSSH_GUI.Core/Lib/Keys/BasicSshKeyFileInformation.cs new file mode 100644 index 0000000..471d551 --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/Keys/BasicSshKeyFileInformation.cs @@ -0,0 +1,385 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Text; +using OpenSSH_GUI.Core.Extensions; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using SshNet.Keygen; +using SshNet.Keygen.SshKeyEncryption; + +namespace OpenSSH_GUI.Core.Lib.Keys; + +[DebuggerDisplay("{ToString()}")] +public readonly record struct BasicSshKeyFileInformation() +{ + private const string OpensshPrivateHeader = "-----BEGIN OPENSSH PRIVATE KEY-----"; + private const string OpensshPrivateFooter = "-----END OPENSSH PRIVATE KEY-----"; + private const string PuttyFileStart = "PuTTY-User-Key-File-"; + private const string OutputFormat = "{0} {1}:{2} {3} ({4})"; + private static readonly ReadOnlyMemory OpensshMagic = "openssh-key-v1\0"u8.ToArray(); + internal ReadOnlyMemory KeyBlob { get; init; } + + + /// The hash algorithm used to compute the fingerprint. Always SHA256 for parsed keys. + public SshKeyHashAlgorithmName HashAlgorithmName { get; private init; } = SshKeyHashAlgorithmName.SHA256; + + /// Base64-encoded SHA256 fingerprint of the public key blob (without trailing padding). + public string FingerPrint { get; private init; } = string.Empty; + + /// Key comment as stored in the key file. + public string Comment { get; private init; } = string.Empty; + + /// Logical SSH key algorithm type. + public SshKeyType KeyType { get; private init; } = SshKeyType.RSA; + + /// Effective bit length of the key (e.g. 256, 384, 521, 2048, 4096). + public int BitLength { get; private init; } = 0; + + /// Storage format of the key, independent of whether it is split across one or two files. + public SshKeyFormat Format { get; private init; } = SshKeyFormat.OpenSSH; + + private bool IsEmpty => FingerPrint.Length == 0; + + private static BasicSshKeyFileInformation Empty { get; } = new(); + + /// + /// Extracts metadata from any supported SSH key file without requiring a passphrase. + /// Supports OpenSSH public keys (.pub), OpenSSH private keys, and PuTTY keys (PPK v1/v2/v3). + /// The comment will be empty for passphrase-protected OpenSSH private keys + /// when no corresponding .pub file is present. + /// + /// Descriptor of the key file(s) on disk. + /// Parsed metadata, or an empty instance if the key cannot be read. + public static BasicSshKeyFileInformation FromKeyFileInfo(SshKeyFileInformation keyFileInformation) + { + if (keyFileInformation is { Exists: false }) + return Empty; + + // .pub file is always preferred — richest source, comment always present + if (keyFileInformation.PublicKeyFileName is { } pubPath) + return TryParseOrEmpty(() => ParseOpenSshPublicKey(File.ReadAllText(pubPath).Trim())); + + var files = keyFileInformation.Files.ToList(); + + // PPK — comment lives in the unencrypted plaintext header regardless of encryption + if (files.FirstOrDefault(f => + f.Extension.Equals(PathExtensions.PuttyKeyFileExtension, StringComparison.OrdinalIgnoreCase)) is + { } ppkFile) + return TryParseOrEmpty(() => ParsePpkFile(File.ReadAllText(ppkFile.FullName))); + + // OpenSSH private key — public key blob is always stored unencrypted + if (files.FirstOrDefault(LooksLikeOpensshPrivateKey) is { } privateFile) + return TryParseOrEmpty(() => ParseOpensshPrivateKey(File.ReadAllText(privateFile.FullName))); + + return Empty; + } + + /// + /// Parses a single-line OpenSSH public key in the format: + /// <keytype> <base64blob> [comment] + /// + private static BasicSshKeyFileInformation ParseOpenSshPublicKey(string content) + { + var parts = content.Split(' ', 3); + if (parts.Length < 2) + throw new FormatException("Not a valid OpenSSH public key line."); + + var keyTypeRaw = parts[0]; + var keyBlob = Convert.FromBase64String(parts[1]); + var comment = parts.Length == 3 ? parts[2] : string.Empty; + + return new BasicSshKeyFileInformation + { + Format = SshKeyFormat.OpenSSH, + HashAlgorithmName = SshKeyHashAlgorithmName.SHA256, + FingerPrint = ComputeFingerprint(keyBlob), + Comment = comment, + KeyType = MapKeyType(keyTypeRaw), + BitLength = ComputeBitLength(keyTypeRaw, keyBlob) + }; + } + + /// + /// Parses the unencrypted public-key section of an OpenSSH private key file. + /// The public key blob is stored in plaintext even when the private key is passphrase-protected. + /// The comment field will be empty because it resides in the encrypted section. + /// + private static BasicSshKeyFileInformation ParseOpensshPrivateKey(string pem) + { + var base64 = pem + .Replace(OpensshPrivateHeader, string.Empty) + .Replace(OpensshPrivateFooter, string.Empty) + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Trim(); + + ReadOnlyMemory blob = Convert.FromBase64String(base64); + + if (!blob.Span[..OpensshMagic.Length].SequenceEqual(OpensshMagic.Span)) + throw new FormatException("Invalid OpenSSH private key magic bytes."); + + var reader = new BlobReader(blob, OpensshMagic.Length); + reader.ReadString(); // ciphername + reader.ReadString(); // kdfname + reader.ReadString(); // kdfoptions + + if (reader.ReadUInt32() == 0) + throw new FormatException("No keys found in OpenSSH private key file."); + + var pubKeyBlob = reader.ReadBlob(); + var inner = new BlobReader(pubKeyBlob, 0); + var keyTypeRaw = inner.ReadString(); + + return new BasicSshKeyFileInformation + { + KeyBlob = pubKeyBlob, + Format = SshKeyFormat.OpenSSH, + HashAlgorithmName = SshKeyHashAlgorithmName.SHA256, + FingerPrint = ComputeFingerprint(pubKeyBlob.Span), + Comment = string.Empty, + KeyType = MapKeyType(keyTypeRaw), + BitLength = ComputeBitLength(keyTypeRaw, pubKeyBlob.Span) + }; + } + + /// + /// Parses a PuTTY private key file (PPK v1, v2, or v3). + /// All versions store the public key blob and comment in unencrypted plaintext headers. + /// PPK v1 is mapped to as no dedicated enum value exists. + /// + private static BasicSshKeyFileInformation ParsePpkFile(string content) + { + var firstLine = content.Split('\n', 2)[0].Trim(); + + var format = int.TryParse(firstLine.Replace(PuttyFileStart, string.Empty)[0].ToString(), out var version) + ? version is 3 ? SshKeyFormat.PuTTYv3 : SshKeyFormat.PuTTYv2 + : SshKeyFormat.PuTTYv2; + + var keyTypeRaw = string.Empty; + var comment = string.Empty; + var publicBase64 = string.Empty; + + using var sr = new StringReader(content); + while (sr.ReadLine() is { } line) + if (line.StartsWith(PuttyFileStart)) + { + keyTypeRaw = SplitPpkField(line); + } + else if (line.StartsWith("Comment:")) + { + comment = SplitPpkField(line); + } + else if (line.StartsWith("Public-Lines:") && + int.TryParse(SplitPpkField(line), out var pubLineCount)) + { + for (var i = 0; i < pubLineCount; i++) + if (sr.ReadLine() is { } pubLine) + publicBase64 += pubLine.Trim(); + + break; // everything we need has been read + } + + if (publicBase64.Length == 0) + throw new FormatException("PPK file contains no public key data."); + + var keyBlob = Convert.FromBase64String(publicBase64); + + return new BasicSshKeyFileInformation + { + KeyBlob = keyBlob, + Format = format, + HashAlgorithmName = SshKeyHashAlgorithmName.SHA256, + FingerPrint = ComputeFingerprint(keyBlob), + Comment = comment, + KeyType = MapKeyType(keyTypeRaw), + BitLength = ComputeBitLength(keyTypeRaw, keyBlob) + }; + } + + /// + /// Maps an OpenSSH wire-format key type string to the enum. + /// DSA and unrecognized types fall back to . + /// + private static SshKeyType MapKeyType(string keyTypeRaw) + { + return keyTypeRaw switch + { + "ssh-ed25519" + or "ssh-ed448" + or "sk-ssh-ed25519@openssh.com" => SshKeyType.ED25519, + "ecdsa-sha2-nistp256" + or "ecdsa-sha2-nistp384" + or "ecdsa-sha2-nistp521" + or "sk-ecdsa-sha2-nistp256@openssh.com" => SshKeyType.ECDSA, + _ => SshKeyType.RSA + }; + } + + /// + /// Returns the effective bit length of the key. + /// For RSA and DSA the modulus size is read directly from the key blob. + /// + private static int ComputeBitLength(string keyTypeRaw, ReadOnlySpan keyBlob) + { + return keyTypeRaw switch + { + "ssh-ed25519" + or "sk-ssh-ed25519@openssh.com" => 256, + "ssh-ed448" => 448, + "ecdsa-sha2-nistp256" + or "sk-ecdsa-sha2-nistp256@openssh.com" => 256, + "ecdsa-sha2-nistp384" => 384, + "ecdsa-sha2-nistp521" => 521, + "ssh-rsa" => GetRsaBitLength(keyBlob), + "ssh-dss" => GetDsaBitLength(keyBlob), + _ => 0 + }; + } + + /// + /// Reads the RSA modulus from an SSH wire-format blob to determine the key's bit length. + /// Layout: [keytype][exponent e][modulus n] — all uint32-length-prefixed. + /// + private static int GetRsaBitLength(ReadOnlySpan span) + { + var typeLen = BinaryPrimitives.ReadInt32BigEndian(span); + span = span[(4 + typeLen)..]; + + var expLen = BinaryPrimitives.ReadInt32BigEndian(span); + span = span[(4 + expLen)..]; + + var modLen = BinaryPrimitives.ReadInt32BigEndian(span); + span = span[4..]; + + if (span[0] == 0x00) + { + span = span[1..]; + modLen--; + } + + return (modLen - 1) * 8 + (int)Math.Floor(Math.Log2(span[0]) + 1); + } + + /// + /// Reads the DSA prime p from an SSH wire-format blob to determine the key's bit length. + /// Layout: [keytype][p][q][g][y] — all uint32-length-prefixed. + /// + private static int GetDsaBitLength(ReadOnlySpan span) + { + var typeLen = BinaryPrimitives.ReadInt32BigEndian(span); + span = span[(4 + typeLen)..]; + + var pLen = BinaryPrimitives.ReadInt32BigEndian(span); + span = span[4..]; + + if (span[0] == 0x00) pLen--; + + return pLen * 8; + } + + /// Computes a SHA256 fingerprint and returns it as unpadded Base64. + private static string ComputeFingerprint(ReadOnlySpan keyBlob, + SshKeyHashAlgorithmName hashAlgorithmName = SshKeyHashAlgorithmName.SHA256) + { + IDigest digest = hashAlgorithmName switch + { + SshKeyHashAlgorithmName.SHA256 => new Sha256Digest(), + SshKeyHashAlgorithmName.SHA512 => new Sha512Digest(), + SshKeyHashAlgorithmName.SHA384 => new Sha384Digest(), + SshKeyHashAlgorithmName.SHA1 => new Sha1Digest(), + SshKeyHashAlgorithmName.MD5 => new MD5Digest(), + _ => new Sha256Digest() + }; + digest.BlockUpdate(keyBlob); + byte[]? rented = null; + var buffer = digest.GetDigestSize() <= 256 + ? stackalloc byte[digest.GetDigestSize()] + : rented = ArrayPool.Shared.Rent(digest.GetDigestSize()); + try + { + var digested = digest.DoFinal(buffer); + return Convert.ToBase64String(buffer[..digested]).TrimEnd('='); + } + finally + { + if (rented is not null) ArrayPool.Shared.Return(rented, true); + } + } + + /// Peeks at the first line of a file to check for the OpenSSH private key header. + private static bool LooksLikeOpensshPrivateKey(FileInfo file) + { + try + { + using var fs = file.OpenText(); + return fs.ReadLine()?.TrimStart().StartsWith(OpensshPrivateHeader) == true; + } + catch + { + return false; + } + } + + /// Splits a PPK header line of the form "Key: Value" and returns the trimmed value. + private static string SplitPpkField(string line) => line.Split(": ", 2) is { Length: 2 } parts ? parts[1].Trim() : string.Empty; + + /// + /// Wraps a parse call and returns on any exception, + /// so that a malformed or unsupported key file never crashes the caller. + /// + private static BasicSshKeyFileInformation TryParseOrEmpty(Func parse) + { + try + { + return parse(); + } + catch + { + return Empty; + } + } + + public string ToString(SshKeyHashAlgorithmName hashAlgorithmName, string outputFormat = OutputFormat) => IsEmpty + ? hashAlgorithmName == HashAlgorithmName + ? string.Format(outputFormat, BitLength, HashAlgorithmName, FingerPrint, Comment, KeyType) + : string.Format( + outputFormat, BitLength, hashAlgorithmName, ComputeFingerprint([], hashAlgorithmName), + Comment, KeyType) + : string.Empty; + + /// + /// Returns a human-readable string matching the output format of ssh-keygen -lf: + /// {bits} SHA256:{fingerprint} {comment} ({keyType}) + /// + public override string ToString() => ToString(HashAlgorithmName); +} + +/// +/// Reads SSH binary protocol fields encoded as big-endian uint32-length-prefixed byte arrays. +/// +file sealed class BlobReader(ReadOnlyMemory data, int offset) +{ + private int _position = offset; + + public uint ReadUInt32() + { + var value = (uint)( + data.Span[_position] << 24 | + data.Span[_position + 1] << 16 | + data.Span[_position + 2] << 8 | + data.Span[_position + 3]); + _position += 4; + return value; + } + + public ReadOnlyMemory ReadBlob() + { + var length = (int)ReadUInt32(); + var result = data[_position..(_position + length)]; + _position += length; + return result; + } + + public string ReadString(Encoding? encoding = null) => (encoding ?? Encoding.ASCII).GetString(ReadBlob().Span); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFactory.cs b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFactory.cs new file mode 100644 index 0000000..7a61eca --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFactory.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Interfaces; + +namespace OpenSSH_GUI.Core.Lib.Keys; + +/// +/// Default implementation of . +/// Creates instances with a shared logger, +/// eliminating the need for a service locator at the call site. +/// +public sealed class SshKeyFactory(ILogger logger, ILoggerFactory loggerFactory) : ISshKeyFactory +{ + /// + public SshKeyFile Create() + { + logger.LogDebug("Creating new SshKeyFile instance"); + return new SshKeyFile(loggerFactory.CreateLogger()); + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFile.cs b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFile.cs index 23b6bff..7dd15dd 100644 --- a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFile.cs +++ b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFile.cs @@ -1,12 +1,10 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; using System.Reactive.Linq; using Microsoft.Extensions.Logging; -using OpenSSH_GUI.Core.Extensions; using OpenSSH_GUI.Core.Lib.AuthorizedKeys; -using OpenSSH_GUI.Core.Services; using ReactiveUI; +using ReactiveUI.Avalonia; using ReactiveUI.SourceGenerators; using Renci.SshNet; using Renci.SshNet.Common; @@ -21,168 +19,217 @@ namespace OpenSSH_GUI.Core.Lib.Keys; /// Represents an SSH key file used in the OpenSSH GUI application, encapsulating properties /// and functionality for managing SSH keys. /// -public sealed partial class SshKeyFile : ReactiveObject, IDisposable, IAsyncDisposable +public sealed partial record SshKeyFile : ReactiveRecord, IDisposable, IAsyncDisposable { + private readonly CompositeDisposable _disposables = new(); + /// /// A logger instance used for logging events and diagnostic information /// related to the operations and state within the class. /// private readonly ILogger _logger; - [ObservableAsProperty] private string _comment = string.Empty; + /// + /// Gets the absolute file path of the SSH key file, projected from . + /// + [ObservableAsProperty(ReadOnly = true)] + private string? _absoluteFilePath; /// - /// Stores the comment associated with the SSH key file. - /// This field is primarily used internally for extracting or storing - /// metadata related to the SSH key during file operations or processing. - /// Default value is an empty string. + /// The basic key file information extracted by ssh-keygen or the file itself in case of a PuTTy key. /// - private string _commentField = string.Empty; + [Reactive] private BasicSshKeyFileInformation _basicSshKeyFileInformation; - [ObservableAsProperty] private string _fingerprint = string.Empty; + /// + /// Holds the comment embedded in the SSH key, derived from either the loaded + /// or the . + /// + [ObservableAsProperty(ReadOnly = true)] + private string _comment = SshKeyGenerateInfo.DefaultSshKeyComment; + + [ObservableAsProperty(ReadOnly = true)] + private bool _fileChangesAllowed; /// - /// Stores the fingerprint information of the SSH key. + /// Gets the file name (without directory) of the SSH key file, projected from . /// - private string _fingerPrintField = string.Empty; + [ObservableAsProperty(ReadOnly = true)] + private string? _fileName; - [ObservableAsProperty] private string _fingerprintString = string.Empty; + /// + /// Holds the raw fingerprint hash of the SSH key, derived from either the loaded + /// or the . + /// + [ObservableAsProperty(ReadOnly = true)] + private string _fingerprint = string.Empty; - [ObservableAsProperty] private SshKeyHashAlgorithmName _hashAlgorithmName = SshKeyHashAlgorithmName.SHA256; + /// + /// Gets the current on-disk format of the SSH key file (e.g. OpenSSH or PuTTY), + /// projected from . + /// + [ObservableAsProperty(ReadOnly = true)] + private SshKeyFormat? _format; /// - /// Represents the default hash algorithm name used for calculating SSH key fingerprints. - /// This field is initialized to the SHA256 algorithm by default and can be updated - /// during key information extraction or other relevant operations. It is used as a fallback - /// in cases where the private key's host key algorithm name cannot be determined. + /// Indicates the hash algorithm (e.g. SHA256, MD5) used for the key's fingerprint, + /// resolved from the host key algorithms of the loaded + /// or from . /// - private SshKeyHashAlgorithmName _hashAlgorithmNameField = SshKeyHashAlgorithmName.SHA256; + [ObservableAsProperty(ReadOnly = true)] + private SshKeyHashAlgorithmName _hashAlgorithmName = SshKeyHashAlgorithmName.SHA256; - [ObservableAsProperty] private bool _isInitialized; + /// + /// Evaluates to when both a valid + /// is loaded and the associated exists on disk. + /// + [ObservableAsProperty(ReadOnly = true)] + private bool _isInitialized; - [ObservableAsProperty] private bool _isPuttyKey; + /// + /// Evaluates to when the current key file format is not + /// , indicating a PuTTY-compatible key format. + /// + [ObservableAsProperty(ReadOnly = true)] + private bool _isPuttyKey; /// /// Holds metadata information about the associated SSH key file, such as file path, name, /// format, and available formats for conversion. Provides access to details about the /// primary key file and related files, such as public key files. /// - /// - /// The source generator creates a public property named KeyFileInfo from this field. - /// The name avoids collision with . - /// [Reactive] private SshKeyFileInformation? _keyFileInfo; - [ObservableAsProperty] private SshKeyType _keyType = SshKeyType.RSA; + /// + /// Provides access to the collection of associated key files (e.g. private and public) + /// for the current SSH key, projected from . + /// Returns an empty array when no files are associated. + /// + [ObservableAsProperty(ReadOnly = true)] + private FileInfo[] _keyFiles = []; + + /// + /// Represents the cryptographic algorithm of the key (e.g. RSA, ECDSA, ED25519), + /// resolved from the loaded or . + /// + [ObservableAsProperty(ReadOnly = true)] + private SshKeyType _keyType = SshKeyType.RSA; + + /// + /// Indicates whether the associated SSH key file requires a password to access. + /// + [ObservableAsProperty(ReadOnly = true)] + private bool _needsPassword; /// - /// Represents the internal field used to store the key type in string representation. - /// This field is primarily utilized for parsing and determining the appropriate - /// when the associated SSH key metadata is loaded or updated. + /// Represents a password container for an SSH key file, encapsulating related + /// password properties and operations while supporting password validation. /// - private string _keyTypeField = string.Empty; + [Reactive(SetModifier = AccessModifier.Private)] + private SshKeyFilePassword _password = new(); /// /// Represents the underlying private key file used to interact with SSH-related /// operations, including authentication and cryptographic functions. /// - /// - /// The source generator creates a public property named PrivateKeyFile from this field. - /// [Reactive] private PrivateKeyFile? _privateKeyFile; - // --- ObservableAsProperty backing fields --- - // The source generator creates public read-only properties and - // corresponding _xxxHelper fields from each of these. - - [ObservableAsProperty] private PrivateKeyFile? _privateKeySource; - - internal void AttachChangeFormatHandler(Func handler) - { - changeFormatHandler = handler; - } - - private Func? changeFormatHandler; - - private Task ChangeFormatOnDisk(SshKeyFormat newFormat, CancellationToken token) - { - return changeFormatHandler is not null ? changeFormatHandler(this, newFormat, token) : Task.CompletedTask; - } - /// - /// Represents a file-based SSH key with fully encapsulated functionalities for managing, - /// manipulating, and interacting with the key. This class provides support for operations - /// such as key format conversion, password management, and key metadata retrieval. - /// Implements for reactive binding capabilities and - /// both and for lifecycle management. + /// Initializes a new instance of , wires up all reactive + /// observable property pipelines. /// + /// + /// An used for diagnostic output throughout the + /// lifetime of this instance. + /// public SshKeyFile(ILogger logger) { _logger = logger; - ChangeFormatOfKeyFile = ReactiveCommand.CreateFromTask(ChangeFormatOnDisk); - - // Wire up all computed ObservableAsPropertyHelper properties. - - _privateKeySourceHelper = this.WhenAnyValue(x => x.PrivateKeyFile) - .ToProperty(this, nameof(PrivateKeySource)); - - _fingerprintHelper = this.WhenAnyValue(x => x.PrivateKeyFile) - .Select(pk => pk?.FingerprintHash() ?? _fingerPrintField) - .ToProperty(this, nameof(Fingerprint)); - - _fingerprintStringHelper = this.WhenAnyValue(x => x.PrivateKeyFile) - .Select(pk => pk?.Fingerprint(SshKeyHashAlgorithmName.SHA256) - .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Skip(1).FirstOrDefault() - ?.Split(':').Skip(1).FirstOrDefault() ?? _fingerPrintField) - .ToProperty(this, nameof(FingerprintString)); - - _commentHelper = this.WhenAnyValue(x => x.PrivateKeyFile) - .Select(pk => pk?.Key.Comment ?? _commentField) - .ToProperty(this, nameof(Comment)); - - _keyTypeHelper = this.WhenAnyValue(x => x.PrivateKeyFile) - .Select(pk => + + var privateKeyFileAndBasicFileInfoObservable = this + .WhenAnyValue(x => x.PrivateKeyFile, x => x.BasicSshKeyFileInformation) + .ObserveOn(AvaloniaScheduler.Instance); + + var privateKeyFileAndFileInfoObservable = this.WhenAnyValue(vm => vm.PrivateKeyFile, vm => vm.KeyFileInfo) + .ObserveOn(AvaloniaScheduler.Instance); + + _needsPasswordHelper = privateKeyFileAndBasicFileInfoObservable.Select(tuple => tuple.Item1 == null) + .ToProperty(this, x => x.NeedsPassword).DisposeWith(_disposables); + + _fingerprintHelper = privateKeyFileAndBasicFileInfoObservable.Select(tuple => tuple.Item2.FingerPrint) + .ToProperty(this, x => x.Fingerprint).DisposeWith(_disposables); + + _commentHelper = privateKeyFileAndBasicFileInfoObservable + .Select(tuple => tuple.Item1?.Key.Comment ?? tuple.Item2.Comment) + .ToProperty(this, x => x.Comment).DisposeWith(_disposables); + + _keyTypeHelper = privateKeyFileAndBasicFileInfoObservable.Select(tuple => + tuple.Item1?.Key switch { - if (pk is not null) - return pk.Key switch - { - EcdsaKey => SshKeyType.ECDSA, - ED25519Key => SshKeyType.ED25519, - _ => SshKeyType.RSA - }; - return Enum.TryParse(_keyTypeField, true, out var enumValue) - ? enumValue - : SshKeyType.RSA; + EcdsaKey => SshKeyType.ECDSA, + ED25519Key => SshKeyType.ED25519, + RsaKey => SshKeyType.RSA, + _ => tuple.Item2.KeyType + }).ToProperty(this, x => x.KeyType).DisposeWith(_disposables); + + _hashAlgorithmNameHelper = privateKeyFileAndBasicFileInfoObservable.Select(tuple => + Enum.TryParse( + tuple.Item1?.HostKeyAlgorithms.FirstOrDefault()?.Name ?? string.Empty, + out var enumValue) + ? enumValue + : tuple.Item2.HashAlgorithmName + ).ToProperty(this, x => x.HashAlgorithmName).DisposeWith(_disposables); + + _absoluteFilePathHelper = privateKeyFileAndFileInfoObservable.Select(tuple => tuple.Item2?.FullFileName) + .ToProperty(this, obj => obj.AbsoluteFilePath); + + _isInitializedHelper = privateKeyFileAndFileInfoObservable + .Select(tuple => tuple.Item1 is not null && tuple.Item2 is { Exists: true }) + .ToProperty(this, x => x.IsInitialized).DisposeWith(_disposables); + + _isPuttyKeyHelper = privateKeyFileAndFileInfoObservable + .Select(tuple => tuple.Item2?.CurrentFormat is not SshKeyFormat.OpenSSH) + .ToProperty(this, x => x.IsPuttyKey).DisposeWith(_disposables); + + _keyFilesHelper = privateKeyFileAndFileInfoObservable + .Select(tuple => tuple.Item2 is not null ? tuple.Item2.Files : []) + .ToProperty(this, x => x.KeyFiles).DisposeWith(_disposables); + + _fileNameHelper = privateKeyFileAndFileInfoObservable.Select(tuple => tuple.Item2?.FileName) + .ToProperty(this, x => x.FileName).DisposeWith(_disposables); + + _formatHelper = privateKeyFileAndFileInfoObservable.Select(tuple => tuple.Item2?.CurrentFormat) + .ToProperty(this, x => x.Format).DisposeWith(_disposables); + + _fileChangesAllowedHelper = this.WhenAnyValue( + vm => vm.NeedsPassword, + vm => vm.Password, + vm => vm.KeyFileInfo, + (needsPassword, password, keyFileInfo) => + keyFileInfo is { KeyFileSource.ProvidedByConfig: false } && + (!needsPassword || password.IsValid)) + .ObserveOn(AvaloniaScheduler.Instance) + .ToProperty(this, x => x.FileChangesAllowed).DisposeWith(_disposables); + + this.WhenAnyValue(vm => vm.KeyFileInfo) + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(keyFileInfo => + { + try + { + if (keyFileInfo is not null) + BasicSshKeyFileInformation = BasicSshKeyFileInformation.FromKeyFileInfo(keyFileInfo); + } + catch (FileNotFoundException) + { + } + catch (Exception e) + { + logger.LogInformation(e, "Failed to extract key information"); + } }) - .ToProperty(this, nameof(KeyType)); - - _hashAlgorithmNameHelper = this.WhenAnyValue(x => x.PrivateKeyFile) - .Select(pk => - Enum.TryParse(pk?.HostKeyAlgorithms.FirstOrDefault()?.Name, out var enumValue) - ? enumValue - : _hashAlgorithmNameField) - .ToProperty(this, nameof(HashAlgorithmName)); - - _isInitializedHelper = this.WhenAnyValue(x => x.PrivateKeyFile, x => x.KeyFileInfo) - .Select(t => t.Item1 is not null && t.Item2 is { Exists: true }) - .ToProperty(this, nameof(IsInitialized)); - - _isPuttyKeyHelper = this.WhenAnyValue(x => x.KeyFileInfo) - .Select(fi => fi?.CurrentFormat is not SshKeyFormat.OpenSSH) - .ToProperty(this, nameof(IsPuttyKey)); + .DisposeWith(_disposables); } - /// - /// Provides access to the collection of associated key files for the current SSH key. - /// The key files typically include the private and public key files that are associated - /// with the key being managed. This property relies on the underlying - /// instance to determine and fetch the file information. - /// Returns an enumeration of objects, representing the files associated - /// with the SSH key. If no files are linked to the key, an empty enumeration is returned. - /// - internal IEnumerable KeyFiles => KeyFileInfo?.Files ?? []; - /// /// Represents the authorized key associated with an SSH key file. /// @@ -193,180 +240,88 @@ public AuthorizedKey AuthorizedKey { get { - if(PrivateKeyFile is { } privateKeyFile) + if (PrivateKeyFile is { } privateKeyFile) return AuthorizedKey.Parse(privateKeyFile.ToOpenSshPublicFormat()); throw new InvalidOperationException("SshKeyFile not initialized."); } } - /// - /// Indicates whether the associated SSH key file requires a password to access. - /// - public bool NeedsPassword - { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } - - /// - /// Represents a password container for an SSH key file, encapsulating related - /// password properties and operations while supporting password validation. - /// - public SshKeyFilePassword Password { get; } = new(); - - /// - /// Gets the absolute file path of the SSH key file. - /// - public string? AbsoluteFilePath => KeyFileInfo?.FullName; - - /// - /// Gets the name of the SSH key file. - /// - public string? FileName => KeyFileInfo?.Name; - - /// - /// Gets the current format of the SSH key file. - /// - public SshKeyFormat? Format => KeyFileInfo?.CurrentFormat; - - /// - /// Gets the list of available SSH key formats to which the current key can be converted. - /// - public IEnumerable? AvailableFormatsForConversion => KeyFileInfo?.AvailableFormatsForConversion; - - /// - /// Gets the default format to which the key file can be converted. - /// - public SshKeyFormat? DefaultConversionFormat => KeyFileInfo?.DefaultConversionFormat; - - /// - /// A reactive command that allows changing the format of an SSH key file on disk. - /// - public ReactiveCommand ChangeFormatOfKeyFile { get; } - - /// - /// Event triggered when the SSH key file is successfully deleted. - /// - public EventHandler? GotDeleted { get; set; } = delegate { }; - /// /// Asynchronously releases the unmanaged resources used by the SshKeyFile instance /// and optionally releases the managed resources. /// - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { - if (PrivateKeyFile is IAsyncDisposable privateKeyFileAsyncDisposable) - await privateKeyFileAsyncDisposable.DisposeAsync(); - else - PrivateKeyFile?.Dispose(); + _disposables.Dispose(); + return ValueTask.CompletedTask; } /// /// Releases the unmanaged resources used by the instance /// and optionally releases the managed resources. /// - public void Dispose() - { - PrivateKeyFile?.Dispose(); - } + public void Dispose() { _disposables.Dispose(); } /// /// Implicit conversion to the underlying . /// - public static implicit operator PrivateKeyFile?(SshKeyFile sshKeyFile) - { - return sshKeyFile.PrivateKeyFile; - } + public static implicit operator PrivateKeyFile?(SshKeyFile sshKeyFile) => sshKeyFile.PrivateKeyFile; - private Process? BuildInformationProcess() - { - if (KeyFileInfo is not { Exists: true }) - throw new FileNotFoundException(); - var processInformation = new ProcessStartInfo - { - FileName = "ssh-keygen", - Arguments = $"-lf {KeyFileInfo.FullName}", - CreateNoWindow = true, - WorkingDirectory = KeyFileInfo.DirectoryName, - UseShellExecute = false, - RedirectStandardOutput = true - }; - return Process.Start(processInformation); - } - - private string? GetPublicKeyInfo() - { - string? publicKeyInfo = null; - if (BuildInformationProcess() is not { } process) return publicKeyInfo; - publicKeyInfo = process.StandardOutput.ReadToEnd(); - return publicKeyInfo; - } - - private async ValueTask GetPublicKeyInfoAsync() - { - string? publicKeyInfo = null; - if (BuildInformationProcess() is not { } process) return publicKeyInfo; - publicKeyInfo = await process.StandardOutput.ReadToEndAsync(); - return publicKeyInfo; - } - /// - /// Extracts detailed information about the SSH key file, such as its fingerprint, - /// hash algorithm, comment, and key type, using the ssh-keygen command-line tool. + /// Resets the state of the current SSH key file instance, clearing any previously set password, + /// and reinitializing the associated private key file to its initial state. + /// If the associated key file requires a password to decrypt but no password is set, + /// the method updates the state to indicate that a password is needed and attempts to + /// extract key metadata for further operations. Logs errors and rethrows exceptions + /// in case of unexpected failures during the reset process. /// - /// - /// Thrown if the SSH key file does not exist. - /// - private async ValueTask ExtractKeyInformation() + public void Reset() { - if (KeyFileInfo is not { Exists: true }) - throw new FileNotFoundException(); - - if (await GetPublicKeyInfoAsync() is { } publicKeyInfo) + try { - var splitted = publicKeyInfo.TrimEnd('\r', '\n').Split(' ', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var pingerprintSplit = splitted[1].Split(':'); - - _hashAlgorithmNameField = Enum.Parse(pingerprintSplit[0]); - _fingerPrintField = pingerprintSplit[1]; - _commentField = splitted[2]; - _keyTypeField = splitted[3]; - - _logger.LogInformation("Extracted Key Information from {filePath}: \"{joinedString}\"", - KeyFileInfo.FullName, string.Join(" ", splitted)); + PrivateKeyFile?.Dispose(); + PrivateKeyFile = null; + Password.Clear(); + PrivateKeyFile = new PrivateKeyFile(KeyFileInfo!.FullFileName); } + catch (SshPassPhraseNullOrEmptyException) + { + if (KeyFileInfo is not null) + BasicSshKeyFileInformation = BasicSshKeyFileInformation.FromKeyFileInfo(KeyFileInfo); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to Initialize {className}", nameof(SshKeyFile)); + throw; + } + + _logger.LogInformation("Reset {className} successfully", KeyFileInfo?.FileName ?? string.Empty); } - /// - /// Resets the state of the current SSH key file instance, clearing any previously set password, - /// and reinitializing the associated private key file to its initial state. - /// If the associated key file requires a password to decrypt but no password is set, - /// the method updates the state to indicate that a password is needed and attempts to - /// extract key metadata for further operations. Logs errors and rethrows exceptions - /// in case of unexpected failures during the reset process. - /// - /// A task representing the asynchronous operation of resetting the SSH key file. - public async ValueTask Reset() + public void Load(SshKeyFileSource source) { try { - Password.Clear(); - PrivateKeyFile = new PrivateKeyFile(KeyFileInfo!.FullName); + KeyFileInfo = new SshKeyFileInformation(source); + PrivateKeyFile = Password.IsValid + ? new PrivateKeyFile(KeyFileInfo.FullFileName, Password.GetPasswordString()) + : new PrivateKeyFile(KeyFileInfo.FullFileName); } - catch (SshPassPhraseNullOrEmptyException) + catch (SshPassPhraseNullOrEmptyException passPhraseNullOrEmptyException) { - NeedsPassword = true; - await ExtractKeyInformation(); + _logger.LogInformation( + passPhraseNullOrEmptyException, "Missing Password for keyfile {filePath}", + source.AbsolutePath); + if (KeyFileInfo is not null) + BasicSshKeyFileInformation = BasicSshKeyFileInformation.FromKeyFileInfo(KeyFileInfo); } catch (Exception e) { _logger.LogError(e, "Failed to Initialize {className}", nameof(SshKeyFile)); throw; } - _logger.LogInformation("Reset {className} successfully", KeyFileInfo?.Name ?? string.Empty); } - + /// /// Loads an SSH key file from the specified file path and initializes it, /// optionally using the provided passphrase for decryption. @@ -375,22 +330,12 @@ public async ValueTask Reset() /// /// An optional passphrase for the key file, used to unlock encrypted private keys. /// - public async ValueTask Load(SshKeyFileSource keyFileSource, ReadOnlyMemory? passPhrase = null) + public void Load(SshKeyFileSource keyFileSource, ReadOnlySpan passPhrase) { try { - KeyFileInfo = new SshKeyFileInformation(keyFileSource); - if (passPhrase is { Length: > 0 } pass) - Password.Set(pass); - PrivateKeyFile = Password.IsValid - ? new PrivateKeyFile(KeyFileInfo.FullName, Password.GetPasswordString()) - : new PrivateKeyFile(KeyFileInfo.FullName); - } - catch (SshPassPhraseNullOrEmptyException passPhraseNullOrEmptyException) - { - _logger.LogInformation(passPhraseNullOrEmptyException, "Missing Password for keyfile {filePath}", keyFileSource.AbsolutePath); - NeedsPassword = true; - await ExtractKeyInformation(); + Password.Set(passPhrase); + Load(keyFileSource); } catch (Exception e) { @@ -406,20 +351,18 @@ public async ValueTask Load(SshKeyFileSource keyFileSource, ReadOnlyMemory /// /// A boolean value indicating whether the password was successfully set. /// - public async ValueTask SetPassword(ReadOnlyMemory password) + public bool SetPassword(ReadOnlySpan password) { try { if (KeyFileInfo is not { Exists: true }) - throw new FileNotFoundException("SshKeyFile not found", KeyFileInfo?.Name); - await Load(KeyFileInfo.KeyFileSource, password); - NeedsPassword = false; + throw new FileNotFoundException("SshKeyFile not found", KeyFileInfo?.FileName); + Load(KeyFileInfo.KeyFileSource, password); return true; } catch (SshPassPhraseNullOrEmptyException) { - NeedsPassword = true; - _logger.LogWarning("Missing Password for keyfile {filePath}", KeyFileInfo?.FullName); + _logger.LogWarning("Missing Password for keyfile {filePath}", KeyFileInfo?.FullFileName); } catch (Exception e) { @@ -428,65 +371,4 @@ public async ValueTask SetPassword(ReadOnlyMemory password) return false; } - - /// - /// Deletes all files associated with this SSH key. If all deletions complete successfully, - /// the event will be triggered. - /// - /// - /// A boolean indicating whether all files were successfully deleted. - /// - /// - /// Thrown if the SSH key file is not initialized before calling this method. - /// - public bool Delete([NotNullWhen(false)] out Exception? error) - { - error = null; - if (!IsInitialized) - throw new InvalidOperationException("Not initialized."); - - var allSucceeded = true; - foreach (var file in KeyFileInfo!.Files) - try - { - file.Delete(); - } - catch (Exception e) - { - _logger.LogError(e, "Failed to delete {FilePath}", file.FullName); - error = e; - allSucceeded = false; - } - - if (allSucceeded && GotDeleted is not null) - GotDeleted(this, EventArgs.Empty); - return allSucceeded; - } - - /// - /// Changes the filename of the SSH key file on disk to the specified new filename. - /// - /// The new filename to assign to the SSH key file. - public void ChangeFilenameOnDisk(string newFilename) - { - try - { - foreach (var file in KeyFileInfo?.Files ?? []) - { - var newFileNameWithMatchingExtension = Path.ChangeExtension(newFilename, - string.IsNullOrEmpty(file.Extension) ? null : file.Extension); - var destination = Path.Combine( - file.DirectoryName ?? SshConfigFilesExtension.GetBaseSshPath(), - newFileNameWithMatchingExtension); - if (File.Exists(destination)) - throw new InvalidOperationException($"File {destination} already exists"); - file.MoveTo(destination); - } - } - catch (Exception e) - { - _logger.LogError(e, "Failed to change filename of {className}", nameof(SshKeyFile)); - throw; - } - } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs index 29b5a5a..6463ea4 100644 --- a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs +++ b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs @@ -1,138 +1,113 @@ -using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using OpenSSH_GUI.Core.Extensions; using SshNet.Keygen; namespace OpenSSH_GUI.Core.Lib.Keys; /// /// Represents metadata and operations related to a specific SSH key file. -/// Provides access to the associated private and potential public key file information, -/// as well as details about key format and available conversion options. +/// All properties are computed eagerly at construction time and are immutable thereafter. /// -public class SshKeyFileInformation(SshKeyFileSource keyFileSource) +public sealed record SshKeyFileInformation { + private static readonly SshKeyFormat[] AvailableFormats = Enum.GetValues(); + /// - /// Represents the internal object associated with the SSH key file. - /// This variable is used to perform various file operations and retrieve metadata of the specified SSH key file path. + /// Initializes a new instance of + /// and eagerly computes all metadata from the provided . /// - private readonly FileInfo _fileInfo = new(keyFileSource.AbsolutePath); + /// The source descriptor for the SSH key file. + public SshKeyFileInformation(SshKeyFileSource keyFileSource) + { + KeyFileSource = keyFileSource; + CanChangeFileName = keyFileSource is { ProvidedByConfig: false }; - public SshKeyFileSource KeyFileSource => keyFileSource; - public bool CanChangeFileName => !keyFileSource.ProvidedByConfig; + FileInfo = !string.IsNullOrWhiteSpace(keyFileSource.AbsolutePath) + ? new FileInfo(keyFileSource.AbsolutePath) + : new FileInfo(Assembly.GetExecutingAssembly().Location); - /// - /// Indicates whether the SSH key file associated with the instance conforms to the OpenSSH format. - /// - /// - /// This property evaluates the format of the SSH key file based on its current extension or metadata. - /// If the key format matches , the property returns true; - /// otherwise, it returns false. - /// - [MemberNotNullWhen(true, nameof(PublicKeyFileName))] - private bool IsOpenSshKey => CurrentFormat is SshKeyFormat.OpenSSH; + FileName = FileInfo.Name; + FullFileName = FileInfo.FullName; + DirectoryName = FileInfo.DirectoryName; + Exists = FileInfo.Exists; - /// - /// Gets the name of the file represented by the current instance of - /// . - /// - /// - /// This property provides the file name, including its extension, as a string. - /// It is derived from the FileInfo instance initialized with the file path. - /// - public string Name => _fileInfo.Name; + CurrentFormat = FileInfo.Extension.EndsWith(PathExtensions.PuttyKeyFileExtension) + ? SshKeyFormat.PuTTYv3 + : SshKeyFormat.OpenSSH; - /// - /// Gets the file name of the public key associated with the current SSH key file, - /// if the key format is OpenSSH. If the current key format is not OpenSSH, this property returns null. - /// - /// - /// The public key file name is constructed by changing the extension of the current file name - /// to ".pub" if the key format is OpenSSH. For other formats, there is no associated public key file. - /// - /// - /// A string representing the file name of the public key for OpenSSH keys, or null - /// if the key is in a format other than OpenSSH. - /// - public string? PublicKeyFileName => IsOpenSshKey ? Path.ChangeExtension(_fileInfo.FullName, "pub") : null; + IsOpenSshKey = CurrentFormat == SshKeyFormat.OpenSSH; - /// - /// Gets the full path of the SSH key file, including the file name and extension. - /// - /// - /// This property provides the complete path to the file as a string, based on the - /// property. It represents the file location on - /// the filesystem. - /// - public string FullName => _fileInfo.FullName; + PublicKeyFileName = IsOpenSshKey + ? Path.ChangeExtension(FullFileName, PathExtensions.OpenSshPublicKeyFileExtension) + : null; - /// - /// Indicates whether the associated SSH key file exists in the file system. - /// - /// - /// This property checks the existence of the file represented by this instance - /// by verifying its status in the file system. It returns true if the file - /// is found, and false otherwise. The property is useful for validation - /// and ensures that operations on the file are only performed when it is available. - /// - public bool Exists => _fileInfo.Exists; + AvailableFormatsForConversion = AvailableFormats + .Where(f => f != CurrentFormat) + .ToArray(); - /// - /// Gets the name of the directory where the SSH key file is located. - /// - /// - /// This property retrieves the full path of the directory containing the SSH key - /// file associated with this instance. If the file is not associated with a valid directory, - /// the property may return null. - /// - public string? DirectoryName => _fileInfo.DirectoryName; + DefaultConversionFormat = AvailableFormatsForConversion.Contains(SshKeyFormat.OpenSSH) + ? SshKeyFormat.OpenSSH + : AvailableFormatsForConversion.FirstOrDefault(); - /// - /// Represents a collection of key-related files associated with an SSH key. - /// - public IEnumerable Files => new[] { FullName, PublicKeyFileName }.Where(e => !string.IsNullOrEmpty(e)) - .Select(e => new FileInfo(e!)); + Files = new[] + { + FullFileName, PublicKeyFileName + } + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => new FileInfo(p!)) + .ToArray(); + } + + /// + public SshKeyFileSource KeyFileSource { get; } + + /// Gets the for the private key file. + public FileInfo FileInfo { get; } + + /// Gets the file name including extension. + public string FileName { get; } + + /// Gets the full absolute file path. + public string FullFileName { get; } + + /// Gets the directory containing the key file, or null if unavailable. + public string? DirectoryName { get; } + + /// Gets whether the key file exists on disk. + public bool Exists { get; } + + /// Gets the detected format of the key file. + public SshKeyFormat CurrentFormat { get; } + + /// Gets whether the key is in OpenSSH format. + public bool IsOpenSshKey { get; } /// - /// Gets the current format of the SSH key file. + /// Gets the absolute path of the associated public key file, + /// or null if the key is not in OpenSSH format. /// - /// - /// The CurrentFormat property determines the format of the SSH key file - /// based on its file extension. It returns SshKeyFormat.PuTTYv3 if the - /// file extension is ".ppk", otherwise it defaults to SshKeyFormat.OpenSSH. - /// This property is used to identify the key format for further operations. - /// - public SshKeyFormat CurrentFormat => _fileInfo.Extension switch - { - ".ppk" => SshKeyFormat.PuTTYv3, - _ => SshKeyFormat.OpenSSH - }; + public string? PublicKeyFileName { get; } + + /// Gets all formats this key can be converted to, excluding its current format. + public SshKeyFormat[] AvailableFormatsForConversion { get; } /// - /// Gets the default format to which the current SSH key can be converted. + /// Gets the recommended default conversion target. + /// Prefers OpenSSH; falls back to the first available format. /// - /// - /// This property evaluates the list of available formats for conversion, and selects the default based on the - /// following criteria: - /// If the OpenSSH format is available for conversion, it will be chosen as the default. - /// Otherwise, the highest-ranking format in the list of available formats (in descending order) is selected. - /// - /// - /// The representing the default conversion format for the SSH key. - /// - /// - public SshKeyFormat DefaultConversionFormat => AvailableFormatsForConversion.Contains(SshKeyFormat.OpenSSH) - ? SshKeyFormat.OpenSSH - : AvailableFormatsForConversion.OrderDescending().First(); + public SshKeyFormat DefaultConversionFormat { get; } /// - /// Gets the collection of SSH key formats that the current key can be converted to, - /// excluding its current format. + /// Gets all files associated with this key (private + public if applicable). /// - /// - /// This property provides a dynamic list of possible target formats for conversion - /// based on the current format of the key. It ensures that the current format is - /// excluded from the list of available options. Examples of SSH key formats include - /// OpenSSH and PuTTYv3. - /// - public IEnumerable AvailableFormatsForConversion => - Enum.GetValues().Where(e => e != CurrentFormat); + public FileInfo[] Files { get; } + + /// Gets whether the file name can be changed by the user. + public bool CanChangeFileName { get; } + + /// + public bool Equals(SshKeyFileInformation? other) => other is not null && KeyFileSource == other.KeyFileSource; + + /// + public override int GetHashCode() => KeyFileSource.GetHashCode(); } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFilePassword.cs b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFilePassword.cs index 85d5e4f..56a5cc5 100644 --- a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFilePassword.cs +++ b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFilePassword.cs @@ -1,225 +1,127 @@ -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; +using System.Buffers; +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; using System.Text; +using OpenSSH_GUI.Core.Lib.Misc; +using ReactiveUI; +using ReactiveUI.Avalonia; +using ReactiveUI.SourceGenerators; +using SshNet.Keygen; +using SshNet.Keygen.SshKeyEncryption; namespace OpenSSH_GUI.Core.Lib.Keys; /// -/// Provides secure, pinned-memory storage for an SSH key file passphrase. -/// All sensitive data is kept in a single pinned buffer and wiped on or . +/// A reactive, disposable container for an SSH key passphrase stored as raw bytes. +/// Acts as a two-state machine: EmptyHasPassword. +/// All observable properties ( fire +/// +/// on every state transition triggered by or . /// -/// -/// -/// Security contract: This class never allocates the passphrase on the managed heap -/// (beyond what callers pass in as ). Callers who need the passphrase as -/// text should use with a stack-allocated Span<char> and -/// wipe it immediately after use. -/// -/// This class is not thread-safe. -/// -public sealed class SshKeyFilePassword : INotifyPropertyChanged, IDisposable +public sealed partial record SshKeyFilePassword : ReactiveRecord, IDisposable { - /// - /// Maximum passphrase size in bytes. Sufficient for any reasonable SSH passphrase. - /// - public const int MaxPasswordBytes = 1024; - - /// - /// Pinned buffer that holds the passphrase bytes. Pinning prevents the GC from - /// relocating the data, so - /// can reliably wipe the only copy. - /// - private readonly byte[] _buffer = GC.AllocateArray(MaxPasswordBytes, true); - - private bool _disposed; + private readonly ReactiveBufferWriter _bufferWriter = new(ushort.MaxValue); + private readonly CompositeDisposable _disposables = new(); private Encoding _encoding = Encoding.UTF8; - private int _writtenCount; - /// - /// Creates an empty instance. Use to populate. + /// Gets a value indicating whether the buffer contains at least one byte. /// - internal SshKeyFilePassword() - { - } - - // ── Public API ────────────────────────────────────────────────────── + [Reactive(SetModifier = AccessModifier.Private)] + private bool _isValid; /// - /// Gets a value indicating whether the buffer contains a passphrase. + /// Initialises the state machine and wires all derived properties + /// to the internal mutation subject. /// - [MemberNotNullWhen(true, nameof(WrittenSpan))] - public bool IsValid + public SshKeyFilePassword() { - get - { - ThrowIfDisposed(); - return _writtenCount > 0; - } + _bufferWriter.WhenAnyValue(vm => vm.WrittenCount) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(e => e != 0) + .Subscribe(eval => + { + this.RaisePropertyChanging(nameof(WrittenSpan)); + IsValid = eval; + this.RaisePropertyChanged(nameof(WrittenSpan)); + }) + .DisposeWith(_disposables); } - /// - /// Gets the number of passphrase bytes currently stored. - /// - public int Length - { - get - { - ThrowIfDisposed(); - return _writtenCount; - } - } + // ── Span accessor ──────────────────────────────────────────────────── /// /// Gets a read-only span over the stored passphrase bytes. - /// This is the primary way to consume the passphrase without heap allocation. + /// This is the primary way to consume the passphrase without heap allocation. + /// + /// + /// is raised for this member on every or call. + /// /// - /// - public ReadOnlySpan WrittenSpan - { - get - { - ThrowIfDisposed(); - return _buffer.AsSpan(0, _writtenCount); - } - } + public ReadOnlySpan WrittenSpan => _bufferWriter.WrittenSpan; - // ── IDisposable ───────────────────────────────────────────────────── + // ── IDisposable ────────────────────────────────────────────────────── /// - /// Securely wipes the internal buffer and marks the instance as disposed. + /// Securely wipes the internal buffer, completes the mutation subject, + /// and disposes all reactive subscriptions. /// Subsequent calls are no-ops. /// public void Dispose() { - if (_disposed) return; - _disposed = true; - SecureClearBuffer(); + _bufferWriter.Clear(); + _disposables.Dispose(); // completes bufferMutated and all ToProperty helpers } - // ── INotifyPropertyChanged ────────────────────────────────────────── - - /// - public event PropertyChangedEventHandler? PropertyChanged; + // ── State transitions ──────────────────────────────────────────────── /// - /// Decodes the stored passphrase into the caller-provided character buffer. - /// The caller should wipe after use. - /// - /// Target buffer (ideally stackalloc). - /// The number of characters written. - /// - /// Thrown when is too small. - public int GetChars(Span destination) - { - ThrowIfDisposed(); - if (_writtenCount == 0) return 0; - return _encoding.GetChars(_buffer.AsSpan(0, _writtenCount), destination); - } - - /// - /// Returns the maximum number of characters that could write - /// for the currently stored passphrase. Useful for sizing a stackalloc buffer. - /// - public int GetMaxCharCount() - { - ThrowIfDisposed(); - return _encoding.GetMaxCharCount(_writtenCount); - } - - /// - /// Replaces the stored passphrase with the UTF-8 encoding of . - /// - /// - /// - public void Set(string password, Encoding? encoding = null) - { - ThrowIfDisposed(); - var enc = encoding ?? _encoding; - var byteCount = enc.GetByteCount(password); - ArgumentOutOfRangeException.ThrowIfGreaterThan(byteCount, MaxPasswordBytes, nameof(password)); - - Span temp = stackalloc byte[byteCount]; - enc.GetBytes(password, temp); - Set(temp, enc); - CryptographicOperations.ZeroMemory(temp); - } - - /// - /// Replaces the stored passphrase with the given raw bytes. + /// Replaces the stored passphrase with the given raw bytes and transitions + /// the instance to the HasPassword state (or stays there on overwrite). + /// Notifies all reactive observers after the write is complete. /// + /// Raw passphrase bytes to store. + /// + /// Optional encoding override used by . + /// Defaults to the previously configured encoding. + /// /// /// public void Set(ReadOnlySpan password, Encoding? encoding = null) { - ThrowIfDisposed(); - SecureClearBuffer(); + _bufferWriter.Clear(); _encoding = encoding ?? _encoding; - WriteToBuffer(password); - } - - /// - public void Set(ReadOnlyMemory password, Encoding? encoding = null) - { - Set(password.Span, encoding); + _bufferWriter.Write(password); } /// - /// Securely wipes the passphrase buffer without disposing the instance, - /// allowing it to be reused with . + /// Securely wipes the passphrase buffer and transitions the instance + /// to the Empty state without disposing it, allowing reuse. + /// Notifies all reactive observers after the wipe. /// /// - public void Clear() - { - ThrowIfDisposed(); - SecureClearBuffer(); - OnPropertyChanged(nameof(IsValid)); - OnPropertyChanged(nameof(Length)); - } - - // ── Private helpers ───────────────────────────────────────────────── - - private void WriteToBuffer(ReadOnlySpan data) - { - ThrowIfDisposed(); - ArgumentOutOfRangeException.ThrowIfGreaterThan( - _writtenCount + data.Length, MaxPasswordBytes, nameof(data)); - - data.CopyTo(_buffer.AsSpan(_writtenCount)); - _writtenCount += data.Length; - - OnPropertyChanged(nameof(IsValid)); - OnPropertyChanged(nameof(Length)); - } - - private void SecureClearBuffer() - { - CryptographicOperations.ZeroMemory(_buffer); - _writtenCount = 0; - } - - private void ThrowIfDisposed() - { - ObjectDisposedException.ThrowIf(_disposed, this); - } - - private void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + public void Clear() { _bufferWriter.Clear(); } /// - /// Converts the stored password bytes to a string on the heap. - /// The resulting string cannot be wiped and will live until GC collection. + /// Decodes the stored passphrase to a managed on the heap. + /// The resulting string cannot be securely wiped and will persist until GC collection. /// public string GetPasswordString() { - Span chars = stackalloc char[GetMaxCharCount()]; - var written = GetChars(chars); + if (!IsValid) + return string.Empty; + + Span chars = stackalloc char[_encoding.GetMaxCharCount(_bufferWriter.WrittenCount)]; + var written = _encoding.GetChars(_bufferWriter.WrittenSpan, chars); var result = new string(chars[..written]); chars.Clear(); return result; } + + public ISshKeyEncryption ToSshKeyEncryption(SshKeyFormat? format = null) => this is { IsValid: true } keyPassword + ? new SshKeyEncryptionAes256( + keyPassword.GetPasswordString(), + format is SshKeyFormat.PuTTYv3 ? new PuttyV3Encryption() : null) + : SshKeyGenerateInfo.DefaultSshKeyEncryption; } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileSource.cs b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileSource.cs index fa4b70e..b95bb10 100644 --- a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileSource.cs +++ b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileSource.cs @@ -3,11 +3,18 @@ namespace OpenSSH_GUI.Core.Lib.Keys; public record SshKeyFileSource { public string AbsolutePath { get; init; } = string.Empty; - public bool ProvidedByConfig { get; init; } = false; + public bool ProvidedByConfig { get; init; } - public static SshKeyFileSource FromDisk(string absolutePath) => - new() { AbsolutePath = absolutePath }; + public static SshKeyFileSource FromDisk(string absolutePath) => new() + { + AbsolutePath = absolutePath + }; - public static SshKeyFileSource FromConfig(string absolutePath) => - new() { AbsolutePath = absolutePath, ProvidedByConfig = true }; + public static SshKeyFileSource FromConfig(string absolutePath) => new() + { + AbsolutePath = absolutePath, + ProvidedByConfig = true + }; + + public override string ToString() => $"{AbsolutePath} | Referenced by Config: {ProvidedByConfig}"; } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHost.cs b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHost.cs index 1f11b47..340a734 100644 --- a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHost.cs +++ b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHost.cs @@ -1,21 +1,23 @@ -using OpenSSH_GUI.Core.Interfaces.KnownHosts; +using System.Collections.ObjectModel; +using System.Text; +using DynamicData; +using OpenSSH_GUI.Core.Extensions; using ReactiveUI; +using ReactiveUI.SourceGenerators; namespace OpenSSH_GUI.Core.Lib.KnownHosts; /// /// Represents a known host in the OpenSSH GUI. /// -public class KnownHost : ReactiveObject, IKnownHost +public partial record KnownHost : ReactiveRecord { + private readonly KnownHostKey[] _keysCopy; + /// - /// Represents a known host entry in the known_hosts file. + /// Represents a known host in the OpenSSH_GUI. /// - public KnownHost(IGrouping knownHosts) - { - Host = knownHosts.Key; - Keys = knownHosts.Select(e => new KnownHostKey(e.Replace($"{Host}", "").Trim()) as IKnownHostKey).ToList(); - } + [ReactiveCollection] private ObservableCollection _keys = []; /// /// Gets or sets the toggled state of the switch. @@ -25,12 +27,23 @@ public KnownHost(IGrouping knownHosts) /// - If it was previously off, all known host keys are marked for deletion. /// - If it was previously on, all known host keys are unmarked for deletion. /// - private bool SwitchToggled { get; set; } + [Reactive] private bool _switchToggled; + + public KnownHost(KeyValuePair knownHosts) + { + HostUri = knownHosts.Key; + _keysCopy = knownHosts.Value; + Keys.AddRange(_keysCopy); + } + + public KnownHostHost HostUri { get; } + + public bool ChangesMade => !_keysCopy.SequenceEqual(Keys); /// /// Represents a known host in the SSH known hosts file. /// - public string Host { get; } + public string Host => HostUri.ToString(); /// /// Represents a known host that can be deleted in its entirety. @@ -38,16 +51,7 @@ public KnownHost(IGrouping knownHosts) public bool DeleteWholeHost => Keys.All(e => e.MarkedForDeletion); /// - /// Represents a known host in the OpenSSH_GUI. - /// - public List Keys - { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } = []; - - /// - /// Toggles the marked for deletion flag of each within the list. + /// Toggles the marked for deletion flag of each within the list. /// If the property is true, it sets the flag to false for all keys. Otherwise, it sets /// the flag to true for all keys. /// @@ -74,14 +78,15 @@ public void KeysDeletionSwitch() /// Returns a string containing all the entries for the known host. /// If the entire host is marked for deletion, returns the line ending character. /// - public string GetAllEntries() + public string Export(PlatformID? platformId = null) { - return DeleteWholeHost - ? IKnownHostsFile.LineEnding - : Keys - .Where(e => !e.MarkedForDeletion) - .Aggregate("", - (current, knownHostsKey) => - current + $"{Host} {knownHostsKey.EntryWithoutHost}{IKnownHostsFile.LineEnding}"); + platformId ??= Environment.OSVersion.Platform; + if (DeleteWholeHost) return platformId.Value.GetLineSeparator(); + var stringBuilder = new StringBuilder(); + foreach (var knownHostKey in Keys.Where(e => !e.MarkedForDeletion)) + { + stringBuilder.Append($"{Host} {knownHostKey}{platformId.Value.GetLineSeparator()}"); + } + return stringBuilder.ToString(); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostHost.cs b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostHost.cs new file mode 100644 index 0000000..f95a14b --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostHost.cs @@ -0,0 +1,26 @@ +namespace OpenSSH_GUI.Core.Lib.KnownHosts; + +public readonly record struct KnownHostHost +{ + private readonly string _originalHostEntry; + + public KnownHostHost(string host) + { + _originalHostEntry = host; + if (host.Split(':') is not { Length: 2 } split) + { + Host = host; + } + else + { + Port = int.Parse(split[1]); + Host = split[0]; + } + Host = Host.Trim('[', ']'); + } + + public int Port { get; } = 22; + public string Host { get; } = string.Empty; + + public override string ToString() => _originalHostEntry; +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostKey.cs b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostKey.cs index 36f9317..6e61b8e 100644 --- a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostKey.cs +++ b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostKey.cs @@ -1,5 +1,5 @@ -using OpenSSH_GUI.Core.Interfaces.KnownHosts; -using ReactiveUI; +using ReactiveUI; +using ReactiveUI.SourceGenerators; using SshNet.Keygen; namespace OpenSSH_GUI.Core.Lib.KnownHosts; @@ -7,21 +7,27 @@ namespace OpenSSH_GUI.Core.Lib.KnownHosts; /// /// Represents a known host key in the OpenSSH GUI. /// -public class KnownHostKey : ReactiveObject, IKnownHostKey +public partial record KnownHostKey : ReactiveRecord { + private readonly string _entryWithoutHost; + + /// + /// Gets or sets a value indicating whether the known host key is marked for deletion. + /// + [Reactive] private bool _markedForDeletion; + /// /// Represents a known host key in the OpenSSH GUI. /// - public KnownHostKey(string entry) + public KnownHostKey(string[] keyParts) { - EntryWithoutHost = entry; - var splitted = EntryWithoutHost.Split(' '); - TypeDeclarationInFile = splitted[0]; + _entryWithoutHost = string.Join(" ", keyParts); + TypeDeclarationInFile = keyParts[0]; KeyType = Enum.Parse( TypeDeclarationInFile.StartsWith("ssh-") - ? TypeDeclarationInFile.Replace("ssh-", "") + ? TypeDeclarationInFile.Replace("ssh-", string.Empty) : TypeDeclarationInFile.Split('-')[0], true); - Fingerprint = splitted[1].Replace("\n", "").Replace("\r", ""); + Fingerprint = keyParts[1]; } /// @@ -39,17 +45,5 @@ public KnownHostKey(string entry) /// public string Fingerprint { get; } - /// - /// Represents a known host key without the host entry in the OpenSSH GUI. - /// - public string EntryWithoutHost { get; } - - /// - /// Gets or sets a value indicating whether the known host key is marked for deletion. - /// - public bool MarkedForDeletion - { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } + public override string ToString() => _entryWithoutHost; } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostsFile.cs b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostsFile.cs index eb58b44..b8156d0 100644 --- a/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostsFile.cs +++ b/OpenSSH_GUI.Core/Lib/KnownHosts/KnownHostsFile.cs @@ -1,81 +1,60 @@ using System.Collections.ObjectModel; -using OpenSSH_GUI.Core.Interfaces.KnownHosts; +using System.Text; +using OpenSSH_GUI.Core.Enums; +using OpenSSH_GUI.Core.Extensions; using ReactiveUI; +using ReactiveUI.SourceGenerators; namespace OpenSSH_GUI.Core.Lib.KnownHosts; /// Represents a known hosts file. -/// / -public class KnownHostsFile : ReactiveObject, IKnownHostsFile +public sealed partial record KnownHostsFile : ReactiveRecord { - /// Represents the path to the known hosts file. - /// / - private string _fileKnownHostsPath = ""; - /// - /// Gets or sets a boolean value indicating whether the `KnownHostsFile` object is created from a server or not. + /// Represents a known hosts file. /// - private bool _isFromServer; + [ReactiveCollection] private ObservableCollection _knownHosts = []; - /// - /// Initializes a new instance of the class. - /// - public KnownHostsFile() - { - } + /// Represents a known hosts file. + public KnownHostsFile(bool IsFromServer = false) => this.IsFromServer = IsFromServer; - /// - /// Represents a known hosts file that stores information about trusted hosts. - /// - /// The path to the file or its content. - /// Indicates whether the content is from a server. - public KnownHostsFile(string knownHostsPathOrContent, bool fromServer = false) - { - _isFromServer = fromServer; - if (_isFromServer) - SetKnownHosts(knownHostsPathOrContent); - else - _fileKnownHostsPath = knownHostsPathOrContent; - // Synchronous reading is deprecated. Use InitializeAsync. - } + public static KnownHostsFile Empty { get; } = new(); + public bool IsFromServer { get; init; } - /// - /// Initializes the known hosts file asynchronously. - /// - /// The path to the file or its content. - /// Indicates whether the content is from a server. - /// A cancellation token. - /// A representing the initialized object. - public async ValueTask InitializeAsync(string knownHostsPathOrContent, bool fromServer = false, - CancellationToken token = default) + private static FileStreamOptions CreateOptions() { - _isFromServer = fromServer; - if (_isFromServer) + var options = new FileStreamOptions { - SetKnownHosts(knownHostsPathOrContent); - } - else + BufferSize = 0, + Access = FileAccess.ReadWrite, + Mode = FileMode.OpenOrCreate, + Share = FileShare.ReadWrite + }; + + if (!OperatingSystem.IsWindows()) { - _fileKnownHostsPath = knownHostsPathOrContent; - await ReadContentAsync(); + options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; } - return this; + return options; } - /// - /// Represents a file that contains known SSH hosts and their keys. - /// - public static string LineEnding { get; set; } = "\r\n"; + public static ValueTask InitializeAsync(FileInfo fileInfo, bool fromServer = false, + CancellationToken token = default) => fileInfo is null + ? throw new ArgumentNullException(nameof(fileInfo)) + : InitializeAsync(new FileStream(fileInfo.FullName, CreateOptions()), fromServer, true, token); - /// - /// Represents a known hosts file. - /// - public ObservableCollection KnownHosts + public static async ValueTask InitializeAsync(Stream knownHostsContent, bool fromServer = false, + bool disposeStream = true, CancellationToken token = default) { - get; - private set => this.RaiseAndSetIfChanged(ref field, value); - } = []; + var knownHostsFile = new KnownHostsFile(fromServer); + if (fromServer) + await knownHostsFile.SetKnownHostsAsync(knownHostsContent, disposeStream, token); + else + await knownHostsFile.ReadContentAsync(token: token); + + return knownHostsFile; + } /// /// Asynchronously reads the contents of the known hosts file. @@ -84,48 +63,38 @@ public ObservableCollection KnownHosts /// The file stream to read from. If null, the method reads from the file specified in the /// constructor. /// + /// A cancellation token. /// A representing the asynchronous operation. - public async ValueTask ReadContentAsync(FileStream? stream = null) + public async ValueTask ReadContentAsync(FileStream? stream = null, CancellationToken token = default) { - if (_isFromServer) return; + if (IsFromServer) return; if (stream is null) { - if (string.IsNullOrEmpty(_fileKnownHostsPath)) return; - await using var file = new FileStream(_fileKnownHostsPath, FileMode.OpenOrCreate); + await using var file = new FileStream(SshConfigFiles.Known_Hosts.GetPathOfFile(), CreateOptions()); using var streamReader = new StreamReader(file, leaveOpen: true); - SetKnownHosts(await streamReader.ReadToEndAsync()); + await SetKnownHostsAsync(file, false, token); } else { using var streamReader = new StreamReader(stream); - SetKnownHosts(await streamReader.ReadToEndAsync()); + await SetKnownHostsAsync(stream, token: token); } } - /// - /// Synchronizes the known hosts with the given list of new known hosts. - /// - /// The new known hosts to synchronize. - public void SyncKnownHosts(IEnumerable newKnownHosts) - { - KnownHosts = new ObservableCollection(newKnownHosts); - } - /// /// Updates the content of the known hosts file asynchronously. /// /// A representing the update operation. public async ValueTask UpdateFileAsync() { - if (_isFromServer) return; - if (string.IsNullOrEmpty(_fileKnownHostsPath)) return; - await using var file = new FileStream(_fileKnownHostsPath, FileMode.Truncate); + if (!KnownHosts.Any(e => e.ChangesMade)) return; + if (IsFromServer) return; + await using var file = new FileStream(SshConfigFiles.Known_Hosts.GetPathOfFile(), FileMode.Truncate); await using var streamWriter = new StreamWriter(file); - var newContent = KnownHosts - .Where(e => !e.DeleteWholeHost) - .Aggregate("", (current, host) => current + host.GetAllEntries()); + var newContent = Export(); await streamWriter.WriteAsync(newContent); - SetKnownHosts(newContent); + file.Seek(0, SeekOrigin.Begin); + await SetKnownHostsAsync(file, false); } /// @@ -133,27 +102,50 @@ public async ValueTask UpdateFileAsync() /// /// The platform ID of the server. /// The updated contents of the known hosts file as a string. - public string GetUpdatedContents(PlatformID platformId) + public async ValueTask GetUpdatedContentsAsync(PlatformID platformId) { - if (!_isFromServer) return ""; - LineEnding = platformId == PlatformID.Unix ? LineEnding : "`r`n"; - var newContent = KnownHosts - .Where(e => !e.DeleteWholeHost) - .Aggregate("", (current, host) => current + host.GetAllEntries()); - SetKnownHosts(newContent); - return newContent; + if (!IsFromServer) return string.Empty; + var content = Export(platformId); + + using var memoryStream = new MemoryStream(); + Memory newContent = Encoding.UTF8.GetBytes(content); + await memoryStream.WriteAsync(newContent); + memoryStream.Seek(0, SeekOrigin.Begin); + await SetKnownHostsAsync(memoryStream, false); + return content; } - /// - /// Sets the known hosts for the file. - /// - /// The contents of the known hosts file. - private void SetKnownHosts(string fileContent) + private async ValueTask SetKnownHostsAsync(Stream contentStream, bool disposeStream = true, + CancellationToken token = default) { - KnownHosts = new ObservableCollection(fileContent - .Split(LineEnding) - .Where(e => !string.IsNullOrEmpty(e)) - .GroupBy(e => e.Split(' ')[0]) - .Select(e => new KnownHost(e))); + KnownHosts.Clear(); + using var streamReader = new StreamReader(contentStream, leaveOpen: !disposeStream); + var dicc = new Dictionary(); + while (await streamReader.ReadLineAsync(token) is { } line) + { + if (line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) is not { Length: >= 2 } splitted) + continue; + foreach (var host in splitted[0].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + var uri = new KnownHostHost(host); + var key = new KnownHostKey(splitted[1..]); + dicc.Add(uri, dicc.Remove(uri, out var keys) ? keys.Append(key).ToArray() : [key]); + } + } + foreach (var dictionaryEntry in dicc) + { + KnownHosts.Add(new KnownHost(dictionaryEntry)); + } + } + + private string Export(PlatformID? platformId = null) + { + platformId ??= Environment.OSVersion.Platform; + var stringBuilder = new StringBuilder(); + foreach (var knownHost in KnownHosts) + { + stringBuilder.Append(knownHost.Export(platformId)); + } + return stringBuilder.ToString(); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Misc/BackedUpFile.cs b/OpenSSH_GUI.Core/Lib/Misc/BackedUpFile.cs new file mode 100644 index 0000000..26c5959 --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/Misc/BackedUpFile.cs @@ -0,0 +1,15 @@ +namespace OpenSSH_GUI.Core.Lib.Misc; + +public record BackedUpFile +{ + public required FileInfo InitialFile { get; init; } + public required FileInfo BackupFile { get; init; } + + public void Backup() { InitialFile.CopyTo(BackupFile.FullName); } + + public void Restore() { BackupFile.MoveTo(InitialFile.FullName, true); } + + public void Delete() { BackupFile.Delete(); } + + public override string ToString() => $"{InitialFile.FullName} -> {BackupFile.FullName}"; +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Misc/ConnectionCredentials.cs b/OpenSSH_GUI.Core/Lib/Misc/ConnectionCredentials.cs new file mode 100644 index 0000000..1f568f0 --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/Misc/ConnectionCredentials.cs @@ -0,0 +1,108 @@ +using OpenSSH_GUI.Core.Lib.Keys; +using Renci.SshNet; + +namespace OpenSSH_GUI.Core.Lib.Misc; + +/// +/// Represents the base class for connection credentials. +/// +public abstract class ConnectionCredentials +{ + private const string Placeholder = "123"; + + /// + /// Represents the base class for connection credentials. + /// + protected ConnectionCredentials(string hostname, string username) + { + if (hostname.Contains(':')) + { + var split = hostname.Split(':'); + Hostname = split[0]; + Port = int.Parse(split[1]); + } + else + { + Hostname = hostname; + Port = 22; + } + + Username = username; + } + + internal static ConnectionCredentials Empty { get; } = + new PasswordConnectionCredentials(Placeholder, Placeholder, Placeholder); + + /// + /// Represents the hostname of a server. + /// This property is used in classes related to connection credentials and server settings. + /// + public string Hostname { get; } + + /// + /// Represents the port number used for establishing an SSH connection. + /// + public int Port { get; } + + /// + /// Represents the username property of a connection credentials. + /// + public string Username { get; } + + /// + /// Retrieves the connection information based on the provided credentials. + /// + /// + /// The object representing the SSH connection information. + /// + public virtual ConnectionInfo GetConnectionInfo() => AddAuthenticationMethods(new NoneAuthenticationMethod(Username)); + + protected ConnectionInfo AddAuthenticationMethods(params AuthenticationMethod[] methods) => new(Hostname, Port, Username, methods); +} + +/// +/// Represents the credentials for a key-based connection to a server. +/// +public class KeyConnectionCredentials(string hostname, string username, SshKeyFile? key) + : ConnectionCredentials(hostname, username) +{ + /// + /// Retrieves the connection information based on the provided credentials. + /// + /// + /// The object representing the SSH connection information. + /// + public override ConnectionInfo GetConnectionInfo() => AddAuthenticationMethods(new PrivateKeyAuthenticationMethod(Username, key?.PrivateKeyFile)); +} + +public class MultiKeyConnectionCredentials(string hostname, string username, IEnumerable? keys) + : ConnectionCredentials(hostname, username) +{ + /// + /// Retrieves the connection information for establishing an SSH connection. + /// + /// + /// The object representing the SSH connection information. + /// + public override ConnectionInfo GetConnectionInfo() + { + return keys is null + ? base.GetConnectionInfo() + : AddAuthenticationMethods( + keys.Select(e => new PrivateKeyAuthenticationMethod(Username, e.PrivateKeyFile) + ).ToArray()); + } +} + +public class PasswordConnectionCredentials( + string hostname, + string username, + string password) + : ConnectionCredentials(hostname, username) +{ + /// + /// Retrieves the connection information based on the provided credentials. + /// + /// The object representing the SSH connection information. + public override ConnectionInfo GetConnectionInfo() => AddAuthenticationMethods(new PasswordAuthenticationMethod(Username, password)); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Misc/DirectoryCrawler.cs b/OpenSSH_GUI.Core/Lib/Misc/DirectoryCrawler.cs index a95e170..e6a7b46 100644 --- a/OpenSSH_GUI.Core/Lib/Misc/DirectoryCrawler.cs +++ b/OpenSSH_GUI.Core/Lib/Misc/DirectoryCrawler.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.Configuration; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Configuration; using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; using OpenSSH_GUI.Core.Lib.Keys; using OpenSSH_GUI.SshConfig.Models; @@ -10,61 +13,98 @@ namespace OpenSSH_GUI.Core.Lib.Misc; /// /// Represents a directory crawler for searching and managing SSH keys. /// -public class DirectoryCrawler( - ILogger logger, - IConfiguration configuration) +public sealed class DirectoryCrawler(ILogger logger, IConfiguration configuration, IMutableConfiguration mutableConfiguration) + : IDirectoryCrawler { private static readonly string[] ImportantFileNames = Enum.GetNames(); - + private readonly List _keyFileSources = []; + + public bool IsSearching { get; private set; } + /// - /// Asynchronously retrieves a collection of new SSH keys from the disk. + /// Asynchronously enumerates possible SSH key file sources from both + /// the SSH configuration and the base SSH directory on disk. /// - /// A cancellation token that can be used to cancel the asynchronous operation. - /// An asynchronous enumerable containing the file paths of the discovered SSH keys. - public ValueTask> GetPossibleKeyFilesOnDisk(CancellationToken token = default) + /// Token to cancel the enumeration. + /// An async stream of discovered instances. + public async IAsyncEnumerable GetPossibleKeyFilesOnDiskAsyncEnumerable( + [EnumeratorCancellation] CancellationToken cancellationToken = default) { + IsSearching = true; + try { - var possibleKeyFiles = new List(); - - try - { - if (configuration.GetSection("SshConfig").Get() is { } sshConfig) + if (configuration.GetSection("SshConfig").Get() is { } sshConfig) + foreach (var hostSetting in sshConfig.Hosts.Concat(sshConfig.Blocks).Append(sshConfig.Global)) { - foreach (var hostSetting in sshConfig.Hosts.Concat(sshConfig.Blocks).Append(sshConfig.Global)) + cancellationToken.ThrowIfCancellationRequested(); + + if (hostSetting.IdentityFiles is not { Length: > 0 } hostIdentityFiles) + continue; + + foreach (var resolvedPath in hostIdentityFiles.Select(p => p.ResolvePath())) { - if (hostSetting.IdentityFiles is not { Length: > 0 } hostIdentityFiles) continue; - foreach (var hostIdentityFile in hostIdentityFiles.Select(path => path.ResolvePath())) - { - if(!possibleKeyFiles.Any(e => e.AbsolutePath.Equals(hostIdentityFile, StringComparison.OrdinalIgnoreCase)) && File.Exists(hostIdentityFile)) - possibleKeyFiles.Add(SshKeyFileSource.FromConfig(hostIdentityFile)); - } + cancellationToken.ThrowIfCancellationRequested(); + + var alreadyTracked = _keyFileSources.Any(e => + e.AbsolutePath.Equals(resolvedPath, StringComparison.OrdinalIgnoreCase)); + + var exists = await Task.Run(() => File.Exists(resolvedPath), cancellationToken); + + if (alreadyTracked || !exists) + continue; + + var source = SshKeyFileSource.FromConfig(resolvedPath); + logger.LogDebug("Adding key file source {Source}", source); + _keyFileSources.Add(source); + yield return source; } } - } - catch (Exception e) + + await foreach (var keyFileSource in EnumerateDiskSources(cancellationToken)) { - logger.LogDebug(e, "Config not readable"); + cancellationToken.ThrowIfCancellationRequested(); + logger.LogDebug("Adding keyfile {KeyFile}", keyFileSource); + _keyFileSources.Add(keyFileSource); + yield return keyFileSource; } - - possibleKeyFiles = possibleKeyFiles.Concat( - Directory.EnumerateFiles(SshConfigFilesExtension.GetBaseSshPath(), "*", new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = false - }).Select(e => new FileInfo(e)) - .Where(e => !ImportantFileNames.Any(ifn => ifn.Equals(e.Name, StringComparison.OrdinalIgnoreCase))) - .Where(e => !possibleKeyFiles.Any(k => k.AbsolutePath.Equals(e.FullName, StringComparison.OrdinalIgnoreCase))) - .Where(e => string.IsNullOrWhiteSpace(e.Extension) || e.Extension.Equals(".ppk", StringComparison.OrdinalIgnoreCase)) - .DistinctBy(e => e.FullName, StringComparer.OrdinalIgnoreCase).Select(e => SshKeyFileSource.FromDisk(e.FullName)) - ).ToList(); - - logger.LogInformation("Found {count} keys", possibleKeyFiles.Count); - return ValueTask.FromResult>(possibleKeyFiles); } - catch (Exception exception) + finally + { + _keyFileSources.Clear(); + IsSearching = false; + } + } + + /// + /// Enumerates SSH key file sources from the base SSH directory, + /// excluding already tracked and config-reserved files. + /// + /// A list of found on disk. + private async IAsyncEnumerable EnumerateDiskSources([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var directoryInfo in mutableConfiguration.Current.LookupPaths.Select(e => new DirectoryInfo(e)) ?? []) { - return ValueTask.FromException>(exception); + logger.LogDebug("Processing directory {Directory}", directoryInfo); + if (cancellationToken.IsCancellationRequested) + yield break; + foreach (var keyFile in directoryInfo.EnumerateFiles( + "*", new EnumerationOptions + { + IgnoreInaccessible = true, + RecurseSubdirectories = false + }).Where(e => !ImportantFileNames.Any(ifn => + ifn.Equals(e.Name, StringComparison.OrdinalIgnoreCase))) + .Where(e => !_keyFileSources.Any(k => + k.AbsolutePath.Equals(e.FullName, StringComparison.OrdinalIgnoreCase))) + .Where(e => string.IsNullOrWhiteSpace(e.Extension) || Path.IsPuTTYKey(e.Name)) + .DistinctBy(e => e.FullName, StringComparer.OrdinalIgnoreCase)) + { + logger.LogDebug("Found key file {KeyFile}", keyFile); + yield return SshKeyFileSource.FromDisk(keyFile.FullName); + if (cancellationToken.IsCancellationRequested) + yield break; + } } } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Misc/KeyManagerOperationResult.cs b/OpenSSH_GUI.Core/Lib/Misc/KeyManagerOperationResult.cs new file mode 100644 index 0000000..dfb2f2c --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/Misc/KeyManagerOperationResult.cs @@ -0,0 +1,82 @@ +using System.Diagnostics.CodeAnalysis; +using OpenSSH_GUI.Core.Enums; + +namespace OpenSSH_GUI.Core.Lib.Misc; + +public record KeyManagerOperationResult +{ + [MemberNotNullWhen(false, nameof(Exception))] + public virtual bool IsSuccess => Result == OperationResult.Success; + + [MemberNotNullWhen(true, nameof(Exception))] + public bool IsConflict => Result == OperationResult.Conflict; + + [MemberNotNullWhen(true, nameof(Exception))] + public bool IsCancelled => Result == OperationResult.Cancelled; + + [MemberNotNullWhen(true, nameof(Exception))] + public bool IsFailure => Result == OperationResult.Failure; + + public OperationResult Result { get; protected init; } + public Exception? Exception { get; protected init; } + + public static KeyManagerOperationResult Success() => new() + { + Result = OperationResult.Success + }; + + public static KeyManagerOperationResult Success(T value) => KeyManagerOperationResult.Success(value); + + public static KeyManagerOperationResult FromException(Exception exception) => exception is OperationCanceledException ? Cancelled(exception) : Failure(exception); + + public static KeyManagerOperationResult Failure(Exception exception) => new() + { + Result = OperationResult.Failure, + Exception = exception + }; + + public static KeyManagerOperationResult Conflict(Exception exception) => new() + { + Result = OperationResult.Conflict, + Exception = exception + }; + + internal static KeyManagerOperationResult Cancelled(Exception exception) => new() + { + Result = OperationResult.Cancelled, + Exception = exception + }; + + /// Throws the associated exception if the result represents a failure. + /// Whether to also throw if the result was cancelled. + public void ThrowIfFailure(bool throwOnCancelled = true) + { + if (IsFailure || throwOnCancelled && IsCancelled) + throw Exception; + } + + public KeyManagerOperationResult WithValue(T value) => KeyManagerOperationResult.SetValue(value, this); +} + +public sealed record KeyManagerOperationResult : KeyManagerOperationResult +{ +#pragma warning disable CS8776 + [MemberNotNullWhen(true, nameof(ResultValue)), MemberNotNullWhen(false, nameof(Exception))] + public override bool IsSuccess => Result == OperationResult.Success && ResultValue is not null; +#pragma warning restore CS8776 + + public T? ResultValue { get; private init; } + + internal static KeyManagerOperationResult SetValue(T value, KeyManagerOperationResult operationResult) => new() + { + Exception = operationResult.Exception, + Result = operationResult.Result, + ResultValue = value + }; + + public static KeyManagerOperationResult Success(T value) => new() + { + Result = OperationResult.Success, + ResultValue = value + }; +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Misc/ReactiveBufferWriter.cs b/OpenSSH_GUI.Core/Lib/Misc/ReactiveBufferWriter.cs new file mode 100644 index 0000000..3d6b5e9 --- /dev/null +++ b/OpenSSH_GUI.Core/Lib/Misc/ReactiveBufferWriter.cs @@ -0,0 +1,118 @@ +using System.Buffers; +using System.ComponentModel; +using ReactiveUI; + +namespace OpenSSH_GUI.Core.Lib.Misc; + +/// +/// A thread-safe, reactive wrapper around +/// implementing for use with ReactiveUI bindings. +/// +/// The element type of the buffer. +public sealed class ReactiveBufferWriter : IReactiveObject, IBufferWriter +{ + private readonly ArrayBufferWriter _inner; + private readonly Lock _lockObject = new(); + private readonly string[] _propertyNames = [nameof(WrittenCount), nameof(WrittenMemory)]; + + /// + /// Initializes a new instance with an optional initial capacity. + /// + /// Initial buffer capacity. Defaults to 256. + public ReactiveBufferWriter(int initialCapacity = 256) => _inner = new ArrayBufferWriter(initialCapacity); + + /// Gets the portion of the buffer that has been written to. + public ReadOnlyMemory WrittenMemory + { + get + { + lock (_lockObject) + { + return _inner.WrittenMemory; + } + } + } + + /// Gets the written data as a span. + public ReadOnlySpan WrittenSpan + { + get + { + lock (_lockObject) + { + return _inner.WrittenSpan; + } + } + } + + /// Gets the number of committed elements. + public int WrittenCount + { + get + { + lock (_lockObject) + { + return _inner.WrittenCount; + } + } + } + + /// + public void Advance(int count) + { + RaiseAllChanging(); + lock (_lockObject) + { + _inner.Advance(count); + } + + RaiseAllChanged(); + } + + /// + public Memory GetMemory(int sizeHint = 0) + { + lock (_lockObject) + { + return _inner.GetMemory(sizeHint); + } + } + + /// + public Span GetSpan(int sizeHint = 0) + { + lock (_lockObject) + { + return _inner.GetSpan(sizeHint); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + public event PropertyChangingEventHandler? PropertyChanging; + + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) { PropertyChanging?.Invoke(this, args); } + + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) { PropertyChanged?.Invoke(this, args); } + + /// Resets the writer and notifies subscribers. + public void Clear() + { + RaiseAllChanging(); + lock (_lockObject) + { + _inner.Clear(); + } + + RaiseAllChanged(); + } + + private void RaiseAllChanging() + { + foreach (var publicPropertyName in _propertyNames) this.RaisePropertyChanging(publicPropertyName); + } + + private void RaiseAllChanged() + { + foreach (var publicPropertyName in _propertyNames) this.RaisePropertyChanged(publicPropertyName); + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Lib/Misc/ServerConnection.cs b/OpenSSH_GUI.Core/Lib/Misc/ServerConnection.cs index 641f333..3af05d0 100644 --- a/OpenSSH_GUI.Core/Lib/Misc/ServerConnection.cs +++ b/OpenSSH_GUI.Core/Lib/Misc/ServerConnection.cs @@ -1,64 +1,91 @@ -using OpenSSH_GUI.Core.Enums; +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; +using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; -using OpenSSH_GUI.Core.Interfaces.Credentials; -using OpenSSH_GUI.Core.Interfaces.KnownHosts; using OpenSSH_GUI.Core.Lib.AuthorizedKeys; -using OpenSSH_GUI.Core.Lib.Credentials; using OpenSSH_GUI.Core.Lib.KnownHosts; using ReactiveUI; +using ReactiveUI.SourceGenerators; using Renci.SshNet; namespace OpenSSH_GUI.Core.Lib.Misc; -public class ServerConnection : ReactiveObject, IDisposable +public sealed partial class ServerConnection : ReactiveObject, IDisposable { - private SshClient _sshClient; + private readonly CompositeDisposable _disposables = new(); - public ServerConnection(IConnectionCredentials? credentials = null) - { - credentials ??= new PasswordConnectionCredentials("123", "123", "123"); - ConnectionCredentials = credentials; - _sshClient = new SshClient(credentials.GetConnectionInfo()) { KeepAliveInterval = TimeSpan.FromSeconds(10) }; - ConnectionTime = DateTime.Now; - } + [ObservableAsProperty(ReadOnly = true)] + private string _connectionString = string.Empty; - private SshClient ClientConnection + [Reactive(SetModifier = AccessModifier.Private)] + private DateTime _connectionTime = DateTime.Now; + + [ObservableAsProperty(ReadOnly = true)] + private string _createEmptyFileCommand = string.Empty; + + [Reactive(SetModifier = AccessModifier.Private)] + private bool _isConnected; + + [ObservableAsProperty(ReadOnly = true)] + private string _lineSeparator = string.Empty; + + [ObservableAsProperty(ReadOnly = true)] + private string _readContentsCommand = string.Empty; + + [Reactive(SetModifier = AccessModifier.Private)] + private PlatformID _serverOs = PlatformID.Other; + + private ServerConnection(ConnectionCredentials? credentials = null) { - get => _sshClient; - set => this.RaiseAndSetIfChanged(ref _sshClient, value); + ConnectionCredentials = credentials ?? ConnectionCredentials.Empty; + ClientConnection = new SshClient(ConnectionCredentials.GetConnectionInfo()) + { + KeepAliveInterval = TimeSpan.FromSeconds(10) + }; + + _connectionStringHelper = this.WhenAnyValue(obj => obj.IsConnected) + .Select(c => c ? $"{ConnectionCredentials.Username}@{ConnectionCredentials.Hostname}" : string.Empty) + .ToProperty(this, obj => obj.ConnectionString) + .DisposeWith(_disposables); + + _readContentsCommandHelper = this.WhenAnyValue(obj => obj.ServerOs) + .Select(c => c == PlatformID.Win32NT ? "type" : "cat") + .ToProperty(this, obj => obj.ReadContentsCommand) + .DisposeWith(_disposables); + + _createEmptyFileCommandHelper = this.WhenAnyValue(obj => obj.ServerOs) + .Select(c => c == PlatformID.Win32NT ? "echo. >" : "touch") + .ToProperty(this, obj => obj.CreateEmptyFileCommand) + .DisposeWith(_disposables); + + _lineSeparatorHelper = this.WhenAnyValue(obj => obj.ServerOs) + .Select(e => e.GetLineSeparator()) + .ToProperty(this, obj => obj.LineSeparator) + .DisposeWith(_disposables); } - private string ReadContentsCommand => ServerOs == PlatformID.Win32NT ? "type" : "cat"; - private string CreateEmptyFileCommand => ServerOs == PlatformID.Win32NT ? "echo. >" : "touch"; - public IConnectionCredentials ConnectionCredentials { get; } - + public static ServerConnection Empty { get; } = new(); - public DateTime ConnectionTime + private ConnectionCredentials ConnectionCredentials { get; - set => this.RaiseAndSetIfChanged(ref field, value); - } = DateTime.Now; + init => this.RaiseAndSetIfChanged(ref field, value); + } - public bool IsConnected + private SshClient ClientConnection { get; - set => this.RaiseAndSetIfChanged(ref field, value); + init => this.RaiseAndSetIfChanged(ref field, value); } - public string ConnectionString => - IsConnected ? $"{ConnectionCredentials.Username}@{ConnectionCredentials.Hostname}" : ""; + /// + public void Dispose() { _disposables.Dispose(); } - public PlatformID ServerOs { get; set; } = PlatformID.Other; - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - void IDisposable.Dispose() - { - GC.SuppressFinalize(this); - } + public static ServerConnection WithCredentials(ConnectionCredentials credentials) => new(credentials); public async ValueTask ConnectToServerAsync(CancellationToken token = default) { - if (ConnectionCredentials is IMultiKeyConnectionCredentials mkcc) return await TestMultiAsync(mkcc, token); await ClientConnection.ConnectAsync(token); IsConnected = ClientConnection.IsConnected; if (!IsConnected) return ServerOs != PlatformID.Other && IsConnected; @@ -68,57 +95,43 @@ public async ValueTask ConnectToServerAsync(CancellationToken token = defa return ServerOs != PlatformID.Other && IsConnected; } - public async ValueTask DisconnectFromServerAsync(CancellationToken token = default) - { - try - { - await Task.Run(() => ClientConnection.Disconnect(), token); - IsConnected = false; - return true; - } - catch (Exception) - { - return false; - } - } - - public async ValueTask CloseConnectionAsync(CancellationToken token = default) + public ValueTask DisconnectFromServerAsync(CancellationToken token = default) { try { - await Task.Run(() => ClientConnection.Disconnect(), token); - IsConnected = false; - return true; + ClientConnection.Disconnect(); + IsConnected = ClientConnection.IsConnected; + return ValueTask.FromResult(true); } catch (Exception) { - return false; + return ValueTask.FromResult(false); } } - public async ValueTask GetKnownHostsFromServerAsync(CancellationToken token = default) + public async ValueTask GetKnownHostsFromServerAsync(CancellationToken token = default) { - if (!IsConnected) return new KnownHostsFile("", true); + if (!IsConnected) throw new InvalidOperationException("No connection to get known hosts from"); - var path = await ResolveRemoteEnvVariablesAsync(SshConfigFiles.Known_Hosts.GetPathOfFile(false, ServerOs), + var path = await ResolveRemoteEnvVariablesAsync( + SshConfigFiles.Known_Hosts.GetPathOfFile(false, ServerOs), token); - var command = ClientConnection.CreateCommand($"{ReadContentsCommand} {path}"); - var result = await Task.Run(() => command.Execute(), token); - - return new KnownHostsFile(result, true); + using var command = ClientConnection.CreateCommand($"{ReadContentsCommand} {path}"); + await command.ExecuteAsync(token); + return await KnownHostsFile.InitializeAsync(command.OutputStream, true, false, token); } - public async ValueTask WriteKnownHostsToServerAsync(IKnownHostsFile knownHostsFile, + public async ValueTask WriteKnownHostsToServerAsync(KnownHostsFile knownHostsFile, CancellationToken token = default) { + if (!knownHostsFile.KnownHosts.Any(e => e.ChangesMade)) return false; if (!IsConnected) return false; - var path = await ResolveRemoteEnvVariablesAsync(SshConfigFiles.Known_Hosts.GetPathOfFile(false, ServerOs), - token); - var command = - ClientConnection.CreateCommand($"echo \"{knownHostsFile.GetUpdatedContents(ServerOs)}\" > {path}"); - var result = await Task.Run(() => command.Execute(), token); - + var path = await ResolveRemoteEnvVariablesAsync( + SshConfigFiles.Known_Hosts.GetPathOfFile(false, ServerOs), token); + var content = await knownHostsFile.GetUpdatedContentsAsync(ServerOs); + using var command = ClientConnection.CreateCommand(BuildRemoteWriteCommand(ServerOs, content, path)); + await command.ExecuteAsync(token); return command.ExitStatus == 0; } @@ -127,9 +140,9 @@ public async ValueTask GetAuthorizedKeysFromServerAsync(Canc if (!IsConnected) throw new InvalidOperationException("No connection to get authorized keys from"); - var path = await ResolveRemoteEnvVariablesAsync(SshConfigFiles.Authorized_Keys.GetPathOfFile(false, ServerOs), - token); - var command = ClientConnection.CreateCommand($"{ReadContentsCommand} {path}"); + var path = await ResolveRemoteEnvVariablesAsync( + SshConfigFiles.Authorized_Keys.GetPathOfFile(false, ServerOs), token); + using var command = ClientConnection.CreateCommand($"{ReadContentsCommand} {path}"); await command.ExecuteAsync(token); return await AuthorizedKeysFile.ParseAsync(command.OutputStream, token); } @@ -137,76 +150,23 @@ public async ValueTask GetAuthorizedKeysFromServerAsync(Canc public async ValueTask WriteAuthorizedKeysChangesToServerAsync(AuthorizedKeysFile authorizedKeysFile, CancellationToken token = default) { + if (!authorizedKeysFile.ChangesMade) return false; if (!IsConnected) return false; - var path = await ResolveRemoteEnvVariablesAsync(SshConfigFiles.Authorized_Keys.GetPathOfFile(false, ServerOs), - token); - var command = - ClientConnection.CreateCommand( - $"echo \"{authorizedKeysFile.ExportFileContent(false, ServerOs)}\" > {path}"); - await Task.Run(() => command.Execute(), token); - + var path = await ResolveRemoteEnvVariablesAsync( + SshConfigFiles.Authorized_Keys.GetPathOfFile(false, ServerOs), token); + var content = authorizedKeysFile.ExportFileContent(ServerOs); + using var command = ClientConnection.CreateCommand(BuildRemoteWriteCommand(ServerOs, content, path)); + await command.ExecuteAsync(token); return command.ExitStatus == 0; } - public async ValueTask TestAndOpenConnectionAsync(CancellationToken token = default) - { - if (ConnectionCredentials is IMultiKeyConnectionCredentials mkcc) return await TestMultiAsync(mkcc, token); - try - { - await ClientConnection.ConnectAsync(token); - IsConnected = ClientConnection.IsConnected; - if (IsConnected) - { - ServerOs = await GetServerOsAsync(token); - await CheckForFilesAndCreateThemIfTheyNotExistAsync(token); - ConnectionTime = DateTime.Now; - } - - if (ServerOs != PlatformID.Other) return IsConnected; - return false; - } - catch (Exception) - { - return false; - } - } - - private async ValueTask TestMultiAsync(IMultiKeyConnectionCredentials mkcc, CancellationToken token = default) - { - //var workingKeys = new List(); - foreach (var key in mkcc.Keys!) - try - { - // using var connection = new SshClient(mkcc.Hostname, mkcc.Username, key.GetSshNetKeyType()); - // await connection.ConnectAsync(token); - // if (connection.IsConnected) workingKeys.Add(key); - } - catch (Exception) - { - // - } - - //mkcc.Keys = workingKeys; - if (mkcc.Keys.Any()) - { - await ClientConnection.ConnectAsync(token); - IsConnected = ClientConnection.IsConnected; - } - - if (!IsConnected) return IsConnected; - ServerOs = await GetServerOsAsync(token); - await CheckForFilesAndCreateThemIfTheyNotExistAsync(token); - ConnectionTime = DateTime.Now; - return ServerOs != PlatformID.Other && IsConnected; - } - private async ValueTask ResolveRemoteEnvVariablesAsync(string originalPath, CancellationToken token = default) { if (!IsConnected) return originalPath; var parts = originalPath.Split('%', StringSplitOptions.RemoveEmptyEntries); - var result = ""; + var result = string.Empty; foreach (var part in parts) if (part.Contains('\\') || part.Contains('/')) { @@ -214,10 +174,12 @@ private async ValueTask ResolveRemoteEnvVariablesAsync(string originalPa } else { - var cmdText = ServerOs is PlatformID.Unix or PlatformID.MacOSX ? $"echo ${part}" : $"echo %{part}%"; - var command = ClientConnection.CreateCommand(cmdText); - var output = await Task.Run(() => command.Execute(), token); - result += output.Trim(); + var cmdText = ServerOs is PlatformID.Unix or PlatformID.MacOSX + ? $"echo ${part}" + : $"echo %{part}%"; + using var command = ClientConnection.CreateCommand(cmdText); + await command.ExecuteAsync(token); + result += command.Result.Trim(); } return result; @@ -230,38 +192,89 @@ private async ValueTask CheckForFilesAndCreateThemIfTheyNotExistAsync(Cancellati var authKeyPath = SshConfigFiles.Authorized_Keys.GetPathOfFile(false); var knownHostPath = SshConfigFiles.Known_Hosts.GetPathOfFile(false); - var authorizedKeysFileCheck = ClientConnection.CreateCommand($"{ReadContentsCommand} {authKeyPath}"); - await Task.Run(() => authorizedKeysFileCheck.Execute(), token); + using var authorizedKeysFileCheck = ClientConnection.CreateCommand($"{ReadContentsCommand} {authKeyPath}"); + await authorizedKeysFileCheck.ExecuteAsync(token); - var knownHostsFileCheck = ClientConnection.CreateCommand($"{ReadContentsCommand} {knownHostPath}"); - await Task.Run(() => knownHostsFileCheck.Execute(), token); + using var knownHostsFileCheck = ClientConnection.CreateCommand($"{ReadContentsCommand} {knownHostPath}"); + await knownHostsFileCheck.ExecuteAsync(token); if (authorizedKeysFileCheck.ExitStatus != 0) { - var createAuthCmd = ClientConnection.CreateCommand($"{CreateEmptyFileCommand} {authKeyPath}"); - await Task.Run(() => createAuthCmd.Execute(), token); + using var createAuthCmd = ClientConnection.CreateCommand($"{CreateEmptyFileCommand} {authKeyPath}"); + await createAuthCmd.ExecuteAsync(token); } if (knownHostsFileCheck.ExitStatus != 0) { - var createKnownCmd = ClientConnection.CreateCommand($"{CreateEmptyFileCommand} {knownHostPath}"); - await Task.Run(() => createKnownCmd.Execute(), token); + using var createKnownCmd = ClientConnection.CreateCommand($"{CreateEmptyFileCommand} {knownHostPath}"); + await createKnownCmd.ExecuteAsync(token); } } private async ValueTask GetServerOsAsync(CancellationToken token = default) { - var linuxCommand = ClientConnection.CreateCommand("uname -s"); - var windowsCommand = ClientConnection.CreateCommand("ver"); + using var unixCommand = ClientConnection.CreateCommand("uname -s"); + await unixCommand.ExecuteAsync(token); - await Task.Run(() => linuxCommand.Execute(), token); - await Task.Run(() => windowsCommand.Execute(), token); + if (unixCommand.ExitStatus == 0) + return PlatformID.Unix; - var isWindows = windowsCommand.ExitStatus == 0; - var isLinux = linuxCommand.ExitStatus == 0; + using var windowsCommand = ClientConnection.CreateCommand("ver"); + await windowsCommand.ExecuteAsync(token); + + if (windowsCommand.ExitStatus == 0 && + windowsCommand.Result.Contains("Windows", StringComparison.OrdinalIgnoreCase)) + return PlatformID.Win32NT; - if (isWindows && !isLinux) return PlatformID.Win32NT; - if (isLinux && !isWindows) return PlatformID.Unix; return PlatformID.Other; } + + /// + /// Builds a platform-appropriate shell command to write the given content to a file on the remote host. + /// + /// The of the remote host. + /// The content to write into the file. + /// The full remote path of the target file. + /// If true, appends to the file instead of overwriting it. + /// A shell command string ready to be executed on the remote host. + /// + /// Thrown when no write command can be constructed for the given . + /// + private static string BuildRemoteWriteCommand(PlatformID platformId, string content, string filePath, + bool append = false) + { + var redirectOperator = append ? ">>" : ">"; + + return platformId is PlatformID.Unix or PlatformID.MacOSX + ? BuildUnixCommand(content, filePath, redirectOperator) + : BuildWindowsCommand(content, filePath, redirectOperator); + } + + /// + /// Builds a Unix shell write command using printf for reliable, escape-safe output. + /// + /// The content to write. + /// The target file path on the remote host. + /// Shell redirect operator (> or >>). + /// A Unix shell command string. + private static string BuildUnixCommand(string content, string filePath, string redirectOperator) + { + var escaped = content.Replace("'", "'\\''"); + return $"printf '%s' '{escaped}' {redirectOperator} '{filePath}'"; + } + + /// + /// Builds a Windows shell write command using PowerShell's Set-Content or Add-Content + /// for reliable Unicode-safe file writing. + /// + /// The content to write. + /// The target file path on the remote host. + /// Shell redirect operator (> or >>), used to determine append mode. + /// A PowerShell command string. + private static string BuildWindowsCommand(string content, string filePath, string redirectOperator) + { + var escaped = content.Replace("'", "''"); + var cmdlet = redirectOperator == ">>" ? "Add-Content" : "Set-Content"; + return $"powershell -Command \"{cmdlet} -Path '{filePath}' -Value '{escaped}' -NoNewline -Encoding UTF8\""; + } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/MVVM/IInitializableViewModel.cs b/OpenSSH_GUI.Core/MVVM/IInitializableViewModel.cs new file mode 100644 index 0000000..a04676f --- /dev/null +++ b/OpenSSH_GUI.Core/MVVM/IInitializableViewModel.cs @@ -0,0 +1,29 @@ +namespace OpenSSH_GUI.Core.MVVM; + +public interface IInitializableViewModel +{ + /// + /// Asynchronously initializes the view model, performing necessary setup operations. + /// + /// A token to monitor for cancellation requests. + /// A representing the asynchronous initialization operation. + public ValueTask InitializeAsync(CancellationToken cancellationToken = default); +} + +public interface IInitializableViewModel +{ + /// Asynchronously initializes the ViewModel with the specified parameters and optional cancellation token. + /// Sets the state of the ViewModel as initialized upon completion. + /// + /// The parameters used to initialize the ViewModel. + /// + /// + /// An optional token for observing cancellation requests. + /// + /// + /// A ValueTask representing the asynchronous initialization operation. + /// + public ValueTask InitializeAsync( + TParam parameters, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/MVVM/ViewModelBase.cs b/OpenSSH_GUI.Core/MVVM/ViewModelBase.cs index 2931d9c..120d3c7 100644 --- a/OpenSSH_GUI.Core/MVVM/ViewModelBase.cs +++ b/OpenSSH_GUI.Core/MVVM/ViewModelBase.cs @@ -1,191 +1,140 @@ -using System.Reactive; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; using ReactiveUI; using ReactiveUI.SourceGenerators; namespace OpenSSH_GUI.Core.MVVM; /// -/// Serves as a base class for all view models in the MVVM pattern within the application. -/// Provides core properties, methods, and initialization logic. +/// Serves as a base class for all view models in the MVVM pattern within the application. +/// Provides core properties, methods, and initialization logic. /// -public abstract class ViewModelBase(ILogger? logger = null) - : ViewModelBase(logger) - where TViewModel : ViewModelBase - where TParameters : class, IInitializerParameters +public abstract class ViewModelBase : ViewModelBase, IInitializableViewModel { - /// Asynchronously initializes the ViewModel with the specified parameters and optional cancellation token. - /// Sets the state of the ViewModel as initialized upon completion. - /// - /// The parameters used to initialize the ViewModel. - /// - /// - /// An optional token for observing cancellation requests. - /// - /// - /// A ValueTask representing the asynchronous initialization operation. - /// + /// public virtual ValueTask InitializeAsync( - TParameters parameters, + TParameters? parameters, CancellationToken cancellationToken = default) { IsInitialized = true; + Activator.Activate().DisposeWith(Disposables); return ValueTask.CompletedTask; } -} -/// -/// Represents a base class for all ViewModel implementations in the MVVM architecture. -/// This class provides core functionality such as exception handling, initialization, -/// and common command execution logic required by derived ViewModels. -/// -/// -/// Inherits from ReactiveUI.ReactiveObject to facilitate reactive programming. -/// Integrates with ILogger for logging purposes and supports exception handling via a reactive subscription. -/// Defines commands and methods that assist in the management of ViewModel-specific operations. -/// -public abstract class ViewModelBase(ILogger? logger = null) : ViewModelBase(logger) - where TViewModel : ViewModelBase -{ - /// - /// Asynchronously initializes the view model, performing necessary setup operations. - /// - /// A token to monitor for cancellation requests. - /// A representing the asynchronous initialization operation. - public virtual ValueTask InitializeAsync(CancellationToken cancellationToken = default) - { - IsInitialized = true; - return ValueTask.CompletedTask; - } + /// + public sealed override ValueTask InitializeAsync(CancellationToken cancellationToken = default) => InitializeAsync(default, cancellationToken); } /// -/// Serves as an abstract base class for view models, providing shared properties, commands, -/// and behaviors for managing the interaction between the view and the application logic. +/// Serves as an abstract base class for view models, providing shared properties, commands, +/// and behaviors for managing the interaction between the view and the application logic. /// -public abstract partial class ViewModelBase : ReactiveObject, IDisposable, IAsyncDisposable +public abstract partial class ViewModelBase : ReactiveObject, IDisposable, IAsyncDisposable, IActivatableViewModel, + IInitializableViewModel { - private readonly IDisposable _booleanSubmitSubscription; - private readonly IDisposable _thrownExceptionsSubscription; - + protected readonly CompositeDisposable Disposables; + /// - /// Represents an event handler used to signal close requests for the view model. - /// This private field can be invoked internally to notify subscribers of the close event. + /// Represents an event handler used to signal close requests for the view model. + /// This private field can be invoked internally to notify subscribers of the close event. /// - [Reactive] - private EventHandler _close = delegate { }; + [Reactive] private EventHandler _close = delegate { }; /// - /// Indicates whether the ViewModel has been initialized successfully. - /// This flag is used to track the internal state of the ViewModel and ensure that - /// initialization processes are not repeated or called prematurely. + /// Indicates whether the ViewModel has been initialized successfully. + /// This flag is used to track the internal state of the ViewModel and ensure that + /// initialization processes are not repeated or called prematurely. /// - [Reactive] - private bool _isInitialized; + [Reactive] private bool _isInitialized; /// - /// Serves as a base class for view models, providing initialization - /// support, exception logging, and reactive command functionality. + /// Serves as a base class for view models, providing initialization + /// support, exception logging, and reactive command functionality. /// - protected ViewModelBase(ILogger? logger) + protected ViewModelBase() { - Logger = logger ?? NullLogger.Instance; - _thrownExceptionsSubscription = ThrownExceptions.Subscribe(exception => Logger.LogError(exception, "Viewmodel threw an exception")); - BooleanSubmit = ReactiveCommand.CreateFromTask(OnBooleanSubmitAsync); - _booleanSubmitSubscription = BooleanSubmit.Subscribe(_ => + Disposables = new CompositeDisposable(); + BooleanSubmitCommand.Subscribe(_ => { if (CloseOnBooleanSubmit) RequestClose(); - }); + }).DisposeWith(Disposables); } /// - /// Provides an instance of the logger. + /// Gets or sets a value indicating whether the view model should automatically + /// request to close when the command is executed. /// /// - /// The property gives access to the logging functionality - /// provided by the Microsoft.Extensions.Logging framework. It is used to log - /// messages, exceptions, and other runtime information throughout the lifecycle - /// of the ViewModel. This property is primarily intended for internal use by - /// the ViewModel to handle various events and errors gracefully. + /// When set to true, the view model invokes the method + /// after the execution of the command. This behavior + /// enables automatic closure of the view upon certain operations. /// - protected ILogger Logger { get; } + protected bool CloseOnBooleanSubmit { get; set; } = true; /// - /// Gets or sets a value indicating whether the view model should automatically - /// request to close when the command is executed. + /// Provides the activator for the view model, enabling activation and deactivation + /// of reactive components tied to the lifecycle of the view model. This property + /// supports managing subscriptions and other reactive resources. /// - /// - /// When set to true, the view model invokes the method - /// after the execution of the command. This behavior - /// enables automatic closure of the view upon certain operations. - /// - private protected bool CloseOnBooleanSubmit { get; set; } = true; + public ViewModelActivator Activator { get; } = new(); - /// - /// Gets a reactive command that represents an asynchronous operation - /// triggered with a boolean parameter. Typically used to handle - /// user interactions requiring confirmation, such as "Ok" or "Cancel" actions. - /// - /// - /// When executed, the command invokes the OnBooleanSubmitAsync method - /// with the provided boolean parameter. By default, this may also trigger a - /// window close action if CloseOnBooleanSubmit is set to true. - /// - public ReactiveCommand BooleanSubmit { get; } + /// + public ValueTask DisposeAsync() + { + Dispose(); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + + /// + public void Dispose() + { + Disposables.Dispose(); + GC.SuppressFinalize(this); + } + + /// + public virtual ValueTask InitializeAsync(CancellationToken cancellationToken = default) + { + IsInitialized = true; + Activator.Activate().DisposeWith(Disposables); + return ValueTask.CompletedTask; + } /// - /// Triggers a request to close the associated view or component. + /// Triggers a request to close the associated view or component. /// /// - /// This method raises the internal Close event, signaling that - /// the ViewModel intends to close. It is primarily used in scenarios - /// where the ViewModel is responsible for managing its own lifecycle transitions. + /// This method raises the internal Close event, signaling that + /// the ViewModel intends to close. It is primarily used in scenarios + /// where the ViewModel is responsible for managing its own lifecycle transitions. /// - protected void RequestClose() - { - Close.Invoke(this, EventArgs.Empty); - } + protected void RequestClose() { Close.Invoke(this, EventArgs.Empty); } /// - /// Handles the submission of a boolean input asynchronously. - /// This method can contain custom logic to process the submitted boolean - /// and perform required asynchronous operations. + /// Handles the submission of a boolean input asynchronously. + /// This method can contain custom logic to process the submitted boolean + /// and perform required asynchronous operations. /// /// The boolean input parameter supplied during submission. /// A token to observe while waiting for the task to complete. /// A task representing the asynchronous operation. - protected virtual Task OnBooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } - - /// - public virtual void Dispose() - { - _booleanSubmitSubscription.Dispose(); - _thrownExceptionsSubscription.Dispose(); - GC.SuppressFinalize(this); - } + [ReactiveCommand] + protected virtual Task BooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) => Task.CompletedTask; - /// - public virtual ValueTask DisposeAsync() + /// + /// Displays the attached for a specified control. + /// + /// + /// The control for which the flyout will be displayed. This parameter must be of type . + /// + [ReactiveCommand] + private void OpenFlyout(object? parameter) { - Dispose(); - GC.SuppressFinalize(this); - return ValueTask.CompletedTask; + if (parameter is Control control) + FlyoutBase.ShowAttachedFlyout(control); } -} - -/// -/// Represents a set of initialization parameters for a specific ViewModel type. -/// This interface is used to define the structure of the data required to initialize a ViewModel. -/// -/// -/// The type of the ViewModel that utilizes this initializer parameters implementation. -/// Must inherit from . -/// -public interface IInitializerParameters where TViewModel : ViewModelBase -{ } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/OpenSSH_GUI.Core.csproj b/OpenSSH_GUI.Core/OpenSSH_GUI.Core.csproj index 1a0a604..c667015 100644 --- a/OpenSSH_GUI.Core/OpenSSH_GUI.Core.csproj +++ b/OpenSSH_GUI.Core/OpenSSH_GUI.Core.csproj @@ -1,31 +1,31 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Resources/AppIconStore.cs b/OpenSSH_GUI.Core/Resources/AppIconStore.cs new file mode 100644 index 0000000..b5bb543 --- /dev/null +++ b/OpenSSH_GUI.Core/Resources/AppIconStore.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Media.Imaging; + +namespace OpenSSH_GUI.Core.Resources; + +/// +/// Holds pre-rendered app icons and window icons, keyed by a canonical string key. +/// Populated during Avalonia framework initialization before the main window is shown. +/// +public sealed class AppIconStore +{ + private readonly Dictionary _bitmaps = new(); + private readonly Dictionary _windowIcons = new(); + + /// Stores a rendered under the given key. + public void AddBitmap(string key, Bitmap bitmap) { _bitmaps[key] = bitmap; } + + /// Stores a under the given key. + public void AddWindowIcon(string key, WindowIcon icon) { _windowIcons[key] = icon; } + + /// Retrieves a by key, or if not found. + public Bitmap? GetBitmap(string key) => _bitmaps.GetValueOrDefault(key); + + /// Retrieves a by key, or if not found. + public WindowIcon? GetWindowIcon(string key) => _windowIcons.GetValueOrDefault(key); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Resources/Converter/CollectionIndexConverter.cs b/OpenSSH_GUI.Core/Resources/Converter/CollectionIndexConverter.cs new file mode 100644 index 0000000..f0083b5 --- /dev/null +++ b/OpenSSH_GUI.Core/Resources/Converter/CollectionIndexConverter.cs @@ -0,0 +1,21 @@ +using System.Collections; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace OpenSSH_GUI.Core.Resources.Converter; + +/// +/// Converts an item and its parent collection into a 1-based index string. +/// +public class CollectionIndexConverter : IMultiValueConverter +{ + /// + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count < 2 || values[0] is null || values[1] is not IList collection) + return string.Empty; + + var index = collection.IndexOf(values[0]); + return index >= 0 ? (index + 1).ToString() : string.Empty; + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Resources/Converter/PathDeletableConverter.cs b/OpenSSH_GUI.Core/Resources/Converter/PathDeletableConverter.cs new file mode 100644 index 0000000..84cf395 --- /dev/null +++ b/OpenSSH_GUI.Core/Resources/Converter/PathDeletableConverter.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; +using OpenSSH_GUI.Core.Extensions; + +namespace OpenSSH_GUI.Core.Resources.Converter; + +/// +/// Converts a path string to a boolean indicating whether it can be deleted. +/// Returns if the path equals the protected default path. +/// +public sealed class PathDeletableConverter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is string path && path != SshConfigFilesExtension.GetBaseSshPath(); + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => new BindingNotification(new NotSupportedException(), BindingErrorType.Error); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Resources/Wrapper/WindowBase.cs b/OpenSSH_GUI.Core/Resources/Wrapper/WindowBase.cs index 428c06f..7b62e46 100644 --- a/OpenSSH_GUI.Core/Resources/Wrapper/WindowBase.cs +++ b/OpenSSH_GUI.Core/Resources/Wrapper/WindowBase.cs @@ -1,36 +1,50 @@ +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; using Avalonia.Controls; -using Avalonia.Media.Imaging; -using DryIoc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.MVVM; using ReactiveUI.Avalonia; -using Serilog; namespace OpenSSH_GUI.Core.Resources.Wrapper; public abstract class WindowBase : WindowBase - where TViewModel : ViewModelBase - where TViewModelInitializer : class, IInitializerParameters + where TViewModel : ViewModelBase { - public ValueTask InitializeAsync(TViewModelInitializer initializer, WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen, CancellationToken cancellationToken = default) + public async ValueTask InitializeAsync(TViewModelInitializer initializer, + WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen, + CancellationToken cancellationToken = default) { WindowInitialize(startupLocation); ArgumentNullException.ThrowIfNull(ViewModel); - return ViewModel.InitializeAsync(initializer,cancellationToken); + await ViewModel.InitializeAsync(initializer, cancellationToken); } } -public abstract class WindowBase : ReactiveWindow where TViewModel : ViewModelBase +public abstract class WindowBase : ReactiveWindow, IDisposable + where TViewModel : ViewModelBase { + private CompositeDisposable Disposables { get; } = new(); public required ILogger> Logger { get; set; } - public required IResolver Resolver { get; set; } + public required IServiceProvider Services { get; set; } + public required AppIconStore AppIconStore { get; set; } + + public void Dispose() { Disposables.Dispose(); } protected void WindowInitialize(WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen) { EnsureInitialized(); + Observable.FromEventPattern( + h => ActualThemeVariantChanged += h, + h => ActualThemeVariantChanged -= h) + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(_ => SetIcon()) + .DisposeWith(Disposables); try { - ViewModel = Resolver.Resolve(serviceKey: typeof(TViewModel).Name); + ViewModel = Services.GetRequiredKeyedService(typeof(TViewModel).Name); } catch (Exception e) { @@ -38,24 +52,33 @@ protected void WindowInitialize(WindowStartupLocation startupLocation = WindowSt throw; } + SetIcon(); + WindowStartupLocation = startupLocation; + ViewModel.Close += RequestClose; + } + + private void SetIcon() + { try { - Icon = new WindowIcon(Resolver.Resolve(serviceKey: "AppIcon")); + if (Enum.TryParse(ActualThemeVariant.Key.ToString(), true, out var themeVariant)) + Icon = AppIconStore.GetWindowIcon(string.Join("_", nameof(WindowIcon), 32, themeVariant).ToLower()); + else + Logger.LogWarning("Could not resolve theme variant {themeVariant}", ActualThemeVariant); } catch (Exception e) { Logger.LogError(e, "Failed to resolve AppIcon"); throw; } - WindowStartupLocation = startupLocation; - ViewModel.Close += RequestClose; } - - public ValueTask InitializeAsync(WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen, CancellationToken cancellationToken = default) + + public async ValueTask InitializeAsync(WindowStartupLocation startupLocation = WindowStartupLocation.CenterScreen, + CancellationToken cancellationToken = default) { WindowInitialize(startupLocation); ArgumentNullException.ThrowIfNull(ViewModel); - return ViewModel!.InitializeAsync(cancellationToken); + await ViewModel!.InitializeAsync(cancellationToken); } private void RequestClose(object? sender, EventArgs e) diff --git a/OpenSSH_GUI.Core/Services/Hosted/FileSystemAnalyzer.cs b/OpenSSH_GUI.Core/Services/Hosted/FileSystemAnalyzer.cs index bcb9a2a..16b3346 100644 --- a/OpenSSH_GUI.Core/Services/Hosted/FileSystemAnalyzer.cs +++ b/OpenSSH_GUI.Core/Services/Hosted/FileSystemAnalyzer.cs @@ -41,7 +41,8 @@ private async Task DoWork(CancellationToken cancellationToken) #pragma warning disable CA1416 if (!Directory.Exists(rootSshPath)) if (unixPlatform) - Directory.CreateDirectory(rootSshPath, + Directory.CreateDirectory( + rootSshPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | @@ -52,7 +53,8 @@ private async Task DoWork(CancellationToken cancellationToken) cancellationToken.ThrowIfCancellationRequested(); if (!Directory.Exists(baseSshPath)) if (unixPlatform) - Directory.CreateDirectory(baseSshPath, + Directory.CreateDirectory( + baseSshPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); // 700 diff --git a/OpenSSH_GUI.Core/Services/KeyFileBackupService.cs b/OpenSSH_GUI.Core/Services/KeyFileBackupService.cs new file mode 100644 index 0000000..d1485af --- /dev/null +++ b/OpenSSH_GUI.Core/Services/KeyFileBackupService.cs @@ -0,0 +1,126 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; +using OpenSSH_GUI.Core.Lib.Misc; +using Serilog; +using Serilog.Extensions.Logging; + +namespace OpenSSH_GUI.Core.Services; + +/// +/// Default implementation of . +/// Manages per-operation backup directories and a scoped Serilog file logger +/// that captures diagnostic output for potentially destructive SSH key file operations. +/// The application logger is intentionally not held by this service — callers are +/// responsible for their own application-level logging channel. +/// +public sealed class KeyFileBackupService : IKeyFileBackupService, IDisposable +{ + private const string BackupFileExtension = "bak"; + + private static readonly string BackupDirectory = + Path.Combine(SshConfigFilesExtension.GetBaseSshPath(), AppDomain.CurrentDomain.FriendlyName); + + private SerilogLoggerFactory? _loggerFactory; + + private ILogger? _operationLogger; + + /// + public void Dispose() { _loggerFactory?.Dispose(); } + + /// + public IEnumerable BackupFiles(params FileInfo[] files) + { + foreach (var file in files) + { + var destination = Path.Combine(BackupDirectory, string.Join(".", file.Name, BackupFileExtension)); + WriteToOperationLog(LogLevel.Debug, "Backing up file {file} to {destination}", file.FullName, destination); + var backup = new BackedUpFile + { + InitialFile = file, + BackupFile = new FileInfo(destination) + }; + backup.Backup(); + WriteToOperationLog(LogLevel.Debug, "Successfully backed up file {file}", file.FullName); + yield return backup; + } + } + + /// + public void RestoreBackupFiles(params BackedUpFile[] files) + { + foreach (var file in files) + { + WriteToOperationLog( + LogLevel.Debug, "Restoring backup file {file} to {destination}", + file.BackupFile.FullName, file.InitialFile.FullName); + file.Restore(); + WriteToOperationLog(LogLevel.Debug, "Successfully restored backup file {file}", file.BackupFile.FullName); + } + } + + /// + public void DeleteBackupFiles(params BackedUpFile[] files) + { + foreach (var file in files) + { + WriteToOperationLog(LogLevel.Debug, "Deleting backup file {file}", file.BackupFile.FullName); + file.Delete(); + WriteToOperationLog(LogLevel.Debug, "Successfully deleted backup file {file}", file.BackupFile.FullName); + } + } + + /// + public void BeginOperationLog() + { + if (_operationLogger is not null) return; + + if (!Directory.Exists(BackupDirectory)) + Directory.CreateDirectory(BackupDirectory); + + var operationLogFile = Path.Combine(BackupDirectory, Path.ChangeExtension("operation_log", "log")); + _loggerFactory = new SerilogLoggerFactory( + new LoggerConfiguration() + .WriteTo.File(operationLogFile) + .MinimumLevel.Verbose() + .CreateLogger(), true); + _operationLogger = _loggerFactory.CreateLogger(); + } + + /// + public void EndOperationLog(bool errorsOccurred = false) + { + if (_operationLogger is null) return; + _operationLogger = null; + _loggerFactory?.Dispose(); + _loggerFactory = null; + if (errorsOccurred) return; + try + { + Directory.Delete(BackupDirectory, true); + } + catch (Exception e) + { + // Intentionally swallowed — backup directory cleanup is best-effort. + // The caller's application logger should have already captured context. + _ = e; + } + } + +#pragma warning disable CA2254 + /// + public void WriteToOperationLog(LogLevel level, [StructuredMessageTemplate] string? message, + params object?[] args) + { + _operationLogger?.Log(level, message, args); + } + + /// + public void WriteToOperationLog(LogLevel level, Exception? exception, + [StructuredMessageTemplate] string? message, params object?[] args) + { + _operationLogger?.Log(level, exception, message, args); + } +#pragma warning restore CA2254 +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Services/KeyFileWriterService.cs b/OpenSSH_GUI.Core/Services/KeyFileWriterService.cs new file mode 100644 index 0000000..cd088e4 --- /dev/null +++ b/OpenSSH_GUI.Core/Services/KeyFileWriterService.cs @@ -0,0 +1,139 @@ +using System.Buffers; +using System.Text; +using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; +using Renci.SshNet; +using SshNet.Keygen; +using SshNet.Keygen.Extensions; +using SshNet.Keygen.SshKeyEncryption; + +namespace OpenSSH_GUI.Core.Services; + +/// +/// Provides functionality to write content or SSH key files to the filesystem. +/// +public class KeyFileWriterService(ILogger logger) : IKeyFileWriterService +{ + /// + public async ValueTask WriteToFile(string filePath, string content, + bool overwrite = false, Encoding? encoding = null) + { + if (encoding is null) + { + encoding ??= Encoding.UTF8; + logger.LogDebug("Using default encoding: {encoding}", encoding.EncodingName); + } + else + { + logger.LogDebug("Using encoding: {encoding}", encoding.EncodingName); + } + + var fileInfo = new FileInfo(filePath); + if (fileInfo.Exists && !overwrite) + { + logger.LogWarning("File {filePath} already exists. Skipping write operation.", filePath); + throw new IOException("File already exists"); + } + + var options = new FileStreamOptions + { + BufferSize = 0, + Access = FileAccess.ReadWrite, + Mode = FileMode.OpenOrCreate, + Share = FileShare.ReadWrite + }; + + if (!OperatingSystem.IsWindows()) + { + options.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + } + + await using var fileStream = fileInfo.Open(options); + logger.LogDebug("Opened file {filePath}", filePath); + + byte[]? rented = null; + var maxByteCount = encoding.GetMaxByteCount(content.Length); + var buffer = maxByteCount <= 256 + ? stackalloc byte[256] + : rented = ArrayPool.Shared.Rent(maxByteCount); + logger.LogDebug("Allocated {byteCount} bytes", buffer.Length); + try + { + var writtenBytes = encoding.GetBytes(content, buffer); + logger.LogDebug("Writing {byteCount} bytes into file {filePath}", writtenBytes, filePath); + fileStream.Write(buffer[..writtenBytes]); + logger.LogDebug("Successfully wrote file {filePath}", filePath); + } + catch (Exception e) + { + logger.LogError(e, "Error while writing file {filePath}", filePath); + throw; + } + finally + { + if (rented is not null) + { + ArrayPool.Shared.Return(rented, true); + logger.LogDebug("Freeing memory"); + } + } + } + + + /// + public async ValueTask> WriteToFileInSpecificFormat( + SshKeyFormat format, + ISshKeyEncryption encryption, + IPrivateKeySource privateKeySource, string filePath, bool overwrite = false) + { + var privateKeyFileContent = format is SshKeyFormat.OpenSSH + ? privateKeySource.ToOpenSshFormat(encryption) + : privateKeySource.ToPuttyFormat(encryption, format); + var writtenFiles = new List(); + switch (format) + { + case SshKeyFormat.PuTTYv2: + case SshKeyFormat.PuTTYv3: + break; + case SshKeyFormat.OpenSSH: + default: + { + var pubKeyFormat = format.ChangeExtension(filePath); + try + { + await WriteToFile(pubKeyFormat, privateKeySource.ToOpenSshPublicFormat(), overwrite); + } + catch (Exception e) + { + logger.LogError(e, "Failed to write public key file {filePath}", pubKeyFormat); + throw; + } + + writtenFiles.Add(pubKeyFormat); + break; + } + } + + var privateFilePath = format.ChangeExtension(filePath, false); + try + { + await WriteToFile(privateFilePath, privateKeyFileContent, overwrite); + } + catch (Exception e) + { + logger.LogError(e, "Failed to write private key file {filePath}", privateFilePath); + throw; + } + writtenFiles.Add(privateFilePath); + return writtenFiles; + } + + + /// + public ValueTask> WriteToFileInSpecificFormat( + SshKeyGenerateInfo generateInfo, + GeneratedPrivateKey createdKey, string filePath, bool overwrite = false) => WriteToFileInSpecificFormat( + generateInfo.KeyFormat, generateInfo.Encryption, createdKey, filePath, + overwrite); +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Services/ServerConnectionService.cs b/OpenSSH_GUI.Core/Services/ServerConnectionService.cs index 4d98eef..ad73a81 100644 --- a/OpenSSH_GUI.Core/Services/ServerConnectionService.cs +++ b/OpenSSH_GUI.Core/Services/ServerConnectionService.cs @@ -1,13 +1,19 @@ -using System.Diagnostics.CodeAnalysis; +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; using Microsoft.Extensions.Logging; -using OpenSSH_GUI.Core.Interfaces.Credentials; using OpenSSH_GUI.Core.Lib.Misc; using ReactiveUI; +using ReactiveUI.SourceGenerators; namespace OpenSSH_GUI.Core.Services; -public class ServerConnectionService(ILogger logger) : ReactiveObject +public sealed partial class ServerConnectionService : ReactiveObject, IDisposable { + private readonly CompositeDisposable _disposables = new(); + + private readonly ILogger _logger; + /// /// Indicates whether the current server connection is active. /// @@ -16,12 +22,8 @@ public class ServerConnectionService(ILogger logger) : /// and is currently active. If the connection is not established or has /// been terminated, it returns false. /// - [MemberNotNullWhen(true, nameof(ServerConnection))] - public bool IsConnected - { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } + [ObservableAsProperty(ReadOnly = true)] + private bool _isConnected; /// /// Gets or sets the server connection instance associated with the service. @@ -31,14 +33,24 @@ public bool IsConnected /// to retrieve or update the instance of the server connection. Setting this property /// raises an internal change notification. /// - public ServerConnection? ServerConnection + [Reactive(SetModifier = AccessModifier.Private)] + private ServerConnection _serverConnection = ServerConnection.Empty; + + public ServerConnectionService(ILogger logger) { - get; - set - { - this.RaiseAndSetIfChanged(ref field, value); - IsConnected = value != null; - } + _logger = logger; + + _isConnectedHelper = this.WhenAnyValue(vm => vm.ServerConnection) + .Select(e => e.WhenAnyValue(sc => sc.IsConnected)) + .Switch() + .ToProperty(this, obj => obj.IsConnected) + .DisposeWith(_disposables); + } + + public void Dispose() + { + _disposables.Dispose(); + _serverConnection.Dispose(); } /// @@ -56,23 +68,23 @@ public ServerConnection? ServerConnection /// A representing the result of the connection attempt. /// Returns true if the connection is successfully established; otherwise, false. /// - public async ValueTask EstablishConnection(IConnectionCredentials connectionCredentials, + public async ValueTask EstablishConnection(ConnectionCredentials connectionCredentials, CancellationToken token = default) { try { - ServerConnection = new ServerConnection(connectionCredentials); + ServerConnection = ServerConnection.WithCredentials(connectionCredentials); return await ServerConnection.ConnectToServerAsync(token); } catch (Exception e) { - logger.LogError(e, "Error connecting to server"); + _logger.LogError(e, "Error connecting to server"); throw; } } /// - /// Closes the current connection to the server, if a connection exists. + /// Closes the current connection to the server if a connection exists. /// /// Indicates whether to throw an exception if no connection exists. /// @@ -86,11 +98,12 @@ public async ValueTask EstablishConnection(IConnectionCredentials connecti /// public async ValueTask CloseConnection(bool throwOnNoConnection = true, CancellationToken token = default) { - if (!IsConnected) + if (!IsConnected) return throwOnNoConnection ? throw new InvalidOperationException("No connection to disconnect from") : true; var disconnectResult = await ServerConnection.DisconnectFromServerAsync(token); + ServerConnection.Dispose(); if (disconnectResult) - ServerConnection = null; + ServerConnection = ServerConnection.Empty; return disconnectResult; } } \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Services/SshKeyGenerator.cs b/OpenSSH_GUI.Core/Services/SshKeyGenerator.cs new file mode 100644 index 0000000..d63ecf5 --- /dev/null +++ b/OpenSSH_GUI.Core/Services/SshKeyGenerator.cs @@ -0,0 +1,42 @@ +using System.Text; +using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; +using OpenSSH_GUI.Core.Lib.Keys; +using SshNet.Keygen; + +namespace OpenSSH_GUI.Core.Services; + +public class SshKeyGenerator(ILogger logger, ISshKeyFactory keyFactory, IKeyFileWriterService keyFileWriterService) : ISshKeyGenerator +{ + /// + public async ValueTask Generate(string fullFilePath, SshKeyGenerateInfo generateParamsInfo, bool overwrite = false) + { + GeneratedPrivateKey? createdKey; + try + { + await using var privateStream = new MemoryStream(); + createdKey = SshKey.Generate(privateStream, generateParamsInfo); + if (createdKey is null) + throw new InvalidOperationException("Could not generate new key"); + } + catch (Exception e) + { + logger.LogError(e, "Error while generating key file {filePath}", fullFilePath); + throw; + } + + var filePath = generateParamsInfo.KeyFormat.ChangeExtension(fullFilePath, false); + + await keyFileWriterService.WriteToFileInSpecificFormat(generateParamsInfo, createdKey, filePath, overwrite); + + var keyFileSource = SshKeyFileSource.FromDisk(filePath); + var keyFile = keyFactory.Create(); + if (string.IsNullOrWhiteSpace(generateParamsInfo.Encryption.Passphrase)) + keyFile.Load(keyFileSource); + else + keyFile.Load(keyFileSource, Encoding.UTF8.GetBytes(generateParamsInfo.Encryption.Passphrase)); + + return keyFile; + } +} \ No newline at end of file diff --git a/OpenSSH_GUI.Core/Services/SshKeyManager.cs b/OpenSSH_GUI.Core/Services/SshKeyManager.cs index 4fe146e..abbcfd2 100644 --- a/OpenSSH_GUI.Core/Services/SshKeyManager.cs +++ b/OpenSSH_GUI.Core/Services/SshKeyManager.cs @@ -1,17 +1,16 @@ using System.Collections.ObjectModel; -using System.Collections.Specialized; +using System.Diagnostics; using System.Text; -using DryIoc; -using Microsoft.Extensions.DependencyInjection; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; using OpenSSH_GUI.Core.Lib.Keys; using OpenSSH_GUI.Core.Lib.Misc; using ReactiveUI; +using ReactiveUI.SourceGenerators; using Renci.SshNet; using SshNet.Keygen; -using SshNet.Keygen.Extensions; -using SshKey = SshNet.Keygen.SshKey; namespace OpenSSH_GUI.Core.Services; @@ -19,483 +18,576 @@ namespace OpenSSH_GUI.Core.Services; /// Manager for SSH keys on the local machine. /// Provides functionality for searching, generating, and changing formats of SSH keys. /// -public class SshKeyManager : ReactiveObject, IDisposable +public sealed partial class SshKeyManager : ReactiveObject, IDisposable { - private const string BackupFileExtension = ".bak"; - - private static readonly FileStreamOptions FileStreamOptions = new() - { - BufferSize = 0, - Access = FileAccess.ReadWrite, - Mode = FileMode.OpenOrCreate, - Share = FileShare.ReadWrite - }; - - private readonly DirectoryCrawler _directoryCrawler; + private readonly IKeyFileBackupService _backupService; + private readonly IDirectoryCrawler _directoryCrawler; + private readonly ISshKeyFactory _keyFactory; + private readonly IKeyFileWriterService _keyFileWriterService; + private readonly ISshKeyGenerator _keyGenerator; private readonly ILogger _logger; private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); - private readonly IResolver _resolver; - private readonly FileSystemWatcher _watcher; + private readonly ObservableCollection _sshKeysInternal = []; - private volatile bool _searching; + [Reactive] private bool _processing; public SshKeyManager( ILogger logger, - DirectoryCrawler directoryCrawler, - IResolver resolver) + IDirectoryCrawler directoryCrawler, + ISshKeyFactory keyFactory, + ISshKeyGenerator keyGenerator, + IKeyFileWriterService keyFileWriterService, + IKeyFileBackupService backupService) { _logger = logger; _directoryCrawler = directoryCrawler; - _resolver = resolver; - - if (!OperatingSystem.IsWindows()) - FileStreamOptions.UnixCreateMode = (UnixFileMode)Convert.ToInt32("600", 8); - - _watcher = new FileSystemWatcher - { - Path = SshConfigFilesExtension.GetBaseSshPath(), - EnableRaisingEvents = true - }; - _watcher.Filters.Add("*.pub"); - _watcher.Filters.Add("*.ppk"); - _watcher.Created += async (_, eventArgs) => await WatcherOnCreated(eventArgs); - _watcher.Deleted += WatcherOnDeleted; - _watcher.Renamed += async (_, eventArgs) => await WatcherOnRenamed(eventArgs); - - SshKeysInternal = []; - SshKeysInternal.CollectionChanged += SshKeysOnCollectionChanged; + _keyFactory = keyFactory; + _keyGenerator = keyGenerator; + _keyFileWriterService = keyFileWriterService; + _backupService = backupService; + SshKeys = new ReadOnlyObservableCollection(_sshKeysInternal); } - - /// - /// Performs the initial SSH key search on disk. - /// Must be called after the DI container is fully built. - /// - public Task InitialSearchAsync(CancellationToken token = default) - => SearchForKeysAndUpdateCollection(); - - private ObservableCollection SshKeysInternal { get; } /// /// Gets the collection of detected SSH keys. /// - public IReadOnlyCollection SshKeys => SshKeysInternal; + public ReadOnlyObservableCollection SshKeys { get; } - public int SshKeysCount + /// + public void Dispose() { - get; - set => this.RaiseAndSetIfChanged(ref field, value); + _semaphoreSlim.Dispose(); + foreach (var sshKeyFile in SshKeys) sshKeyFile.Dispose(); } /// - /// Changes the format of an existing SSH key. + /// Performs the initial SSH key search on disk. + /// Must be called after the DI container is fully built. /// - /// The SSH key file to change. - /// The target SSH key format. - /// A cancellation token. - /// A task representing the asynchronous operation. - public async Task ChangeFormatOfKeyAsync( - SshKeyFile key, - SshKeyFormat newFormat, - CancellationToken token = default) + public async ValueTask InitialSearchAsync(CancellationToken token = default) { - if (!key.IsInitialized) - throw new InvalidOperationException("Key file not initialized"); - - PrivateKeyFile? privateKeyFile = key; - ArgumentNullException.ThrowIfNull(privateKeyFile); - ArgumentException.ThrowIfNullOrWhiteSpace(key.AbsoluteFilePath); - - var filePath = newFormat.ChangeExtension(Path.GetFullPath(key.AbsoluteFilePath), false); - - var password = key.Password.IsValid - ? key.Password.GetPasswordString() - : null; + Processing = true; + await SearchForKeysAndUpdateCollectionAsync(token); + Processing = false; + } - var backedUpFiles = new List<(string backup, string original)>(); - var semaphoreAquired = false; + /// + /// Changes the password of an SSH key file, handling both OpenSSH and PuTTY formats transparently. + /// If the key is in PuTTY format, it will be temporarily converted to OpenSSH, the password changed, + /// and then converted back to the original format. + /// + /// The SSH key file whose password should be changed. + /// The new password to set, encoded using . + /// + /// The encoding used to interpret . Defaults to if + /// null. + /// + /// A cancellation token to observe while waiting for the operation to complete. + /// Thrown if the private key file of is null. + /// Thrown if the resolved key file path is null or whitespace. + /// Thrown if the internal semaphore could not be acquired within 5 seconds. + /// + /// Thrown if ssh-keygen exits with a non-zero code, or if intermediate key file operations fail. + /// On failure, all modified files are restored from backup. + /// + public async ValueTask ChangePasswordOfKeyAsync(SshKeyFile key, + ReadOnlyMemory newPassword, + Encoding? encoding = null, CancellationToken token = default) + { + _backupService.BeginOperationLog(); + encoding ??= Encoding.UTF8; + var semaphoreAcquired = false; + var errorsOccured = false; + BackedUpFile[] backupFiles = []; + string[] additionalDeleteFiles = []; + var keyFilePath = string.Empty; try { - foreach (var existingFile in key.KeyFiles.Select(e => e.FullName)) + Processing = true; + keyFilePath = key.AbsoluteFilePath; + var privateKeyFile = key.PrivateKeyFile; + ArgumentNullException.ThrowIfNull(privateKeyFile); + ArgumentException.ThrowIfNullOrWhiteSpace(keyFilePath); + if (!await _semaphoreSlim.WaitAsync(TimeSpan.FromSeconds(5), token)) { - var backup = existingFile + BackupFileExtension; - File.Copy(existingFile, backup, true); - backedUpFiles.Add((backup, existingFile)); + Log(LogLevel.Error, "Failed to acquire semaphore within 5 seconds"); + throw new TimeoutException("Failed to acquire semaphore within 5 seconds"); } - if(!key.Delete(out var exception)) - throw exception; - semaphoreAquired = await _semaphoreSlim.WaitAsync(TimeSpan.FromSeconds(2), token); - if (!semaphoreAquired) - throw new InvalidOperationException("Another key operation is in progress"); + semaphoreAcquired = true; + backupFiles = _backupService.BackupFiles(key.KeyFiles).ToArray(); - switch (newFormat) + if (key.Format is { } and not SshKeyFormat.OpenSSH) { - case SshKeyFormat.OpenSSH: - await using (var privateFileStream = new FileStream(filePath, FileStreamOptions)) - await using (var streamWriter = new StreamWriter(privateFileStream, Encoding.UTF8)) - { - await streamWriter.WriteAsync(key.Password.IsValid - ? privateKeyFile.ToOpenSshFormat(key.Password.GetPasswordString()) - : privateKeyFile.ToOpenSshFormat()); - } - - await using (var publicFileStream = - new FileStream(newFormat.ChangeExtension(filePath), FileStreamOptions)) - await using (var streamWriter = new StreamWriter(publicFileStream, Encoding.UTF8)) - { - await streamWriter.WriteAsync(privateKeyFile.ToOpenSshPublicFormat()); - } + Log(LogLevel.Debug, "Detected PuTTY key {key} - need to change format first", keyFilePath); + additionalDeleteFiles = (await _keyFileWriterService.WriteToFileInSpecificFormat( + SshKeyFormat.OpenSSH, + key.Password.ToSshKeyEncryption(), privateKeyFile, keyFilePath, true)).ToArray(); - break; + keyFilePath = additionalDeleteFiles.First(e => string.IsNullOrWhiteSpace(Path.GetExtension(e))); + Log(LogLevel.Debug, "New file path: {newFilePath}", keyFilePath); + } - case SshKeyFormat.PuTTYv2: - case SshKeyFormat.PuTTYv3: - default: - await using (var privateFileStream = new FileStream(filePath, FileStreamOptions)) - await using (var streamWriter = new StreamWriter(privateFileStream, Encoding.UTF8)) - { - await streamWriter.WriteAsync(password is not null - ? privateKeyFile.ToPuttyFormat(password, newFormat) - : privateKeyFile.ToPuttyFormat(newFormat)); - } + using var process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = "ssh-keygen", + Arguments = + $"-p -f {keyFilePath} -P \"{key.Password.GetPasswordString()}\" -N \"{encoding.GetString(newPassword.Span)}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + if (process.Start()) + { + await process.WaitForExitAsync(token); + if (process.ExitCode != 0) + { + var message = await process.StandardError.ReadToEndAsync(token); + Log( + LogLevel.Error, "ssh-keygen exited with code {exitCode} and message: {message}", + process.ExitCode, message); + throw new Exception($"ssh-keygen exited with code {process.ExitCode}"); + } - break; + var output = await process.StandardOutput.ReadToEndAsync(token); + Log(LogLevel.Debug, "ssh-keygen exited without errors and output: {message}", output); } - foreach (var (backup, _) in backedUpFiles) - TryDeleteFile(backup); + if (key.Format is { } format and not SshKeyFormat.OpenSSH) + { + var keyFile = _keyFactory.Create(); + keyFile.Load(SshKeyFileSource.FromDisk(keyFilePath), newPassword.Span); + Log( + LogLevel.Debug, + "Changes to the password were made in OpenSSH Format - need to change format to Putty again"); + keyFilePath = (await _keyFileWriterService.WriteToFileInSpecificFormat( + format, keyFile.Password.ToSshKeyEncryption(), + keyFile.PrivateKeyFile ?? throw new Exception("Private key file not found"), keyFilePath, + true)).First(); + + Log(LogLevel.Debug, "New file path: {newFilePath}", keyFilePath); + foreach (var deleteFile in additionalDeleteFiles) File.Delete(deleteFile); + } - await AddKeyAsync(SshKeyFileSource.FromDisk(filePath)); + key.Load(SshKeyFileSource.FromDisk(keyFilePath), newPassword.Span); + Log(LogLevel.Debug, "Successfully changed password of key {key}", keyFilePath); + _backupService.DeleteBackupFiles(backupFiles); + return KeyManagerOperationResult.Success(); } catch (Exception e) { - _logger.LogError(e, "Error changing format of key – attempting rollback"); - foreach (var (backup, original) in backedUpFiles) - try - { - File.Copy(backup, original, true); - TryDeleteFile(backup); - } - catch (Exception rollbackEx) - { - _logger.LogError(rollbackEx, - "Rollback failed for '{original}' – manual recovery may be required", - original); - } - - throw; + errorsOccured = true; + Log(LogLevel.Error, e, "Error changing password of key {key}", keyFilePath); + _backupService.RestoreBackupFiles(backupFiles); + return KeyManagerOperationResult.FromException(e); } finally { - if (semaphoreAquired) + if (semaphoreAcquired) _semaphoreSlim.Release(); + _backupService.EndOperationLog(errorsOccured); + Processing = false; } } /// - /// Changes the order of the SSH keys in the collection. + /// Attempts to delete all files associated with the given SSH key. + /// Unlike , this method does not throw on failure — + /// instead, all encountered exceptions are aggregated and returned alongside a success flag. /// - /// Function to reorder the keys. - public void ChangeOrder(Func, IEnumerable> orderFunc) + /// The SSH key file to delete, including all associated key files. + /// A cancellation token to observe while waiting for the semaphore. + /// + /// A indicating success, or containing an + /// if one or more files could not be deleted. + /// + public async ValueTask TryDeleteKeyAsync(SshKeyFile key, + CancellationToken token = default) { - var reordered = orderFunc(SshKeys).ToList(); - for (var i = 0; i < reordered.Count; i++) + _backupService.BeginOperationLog(); + var errorsOccured = false; + Exception? exception = null; + var semaphoreAcquired = false; + try + { + Processing = true; + semaphoreAcquired = await _semaphoreSlim.WaitAsync(TimeSpan.FromSeconds(5), token); + foreach (var keyFile in key.KeyFiles) + try + { + keyFile.Delete(); + } + catch (Exception ex) + { + Log(LogLevel.Debug, ex, "Error while deleting key {key}", key.AbsoluteFilePath); + exception = exception is null ? ex : new AggregateException(exception, ex); + } + + if (exception is not null) + throw exception; + Log(LogLevel.Debug, "Successfully deleted key {key}", key.AbsoluteFilePath); + _sshKeysInternal.Remove(key); + return KeyManagerOperationResult.Success(); + } + catch (Exception e) + { + Log(LogLevel.Error, e, "Error deleting key"); + errorsOccured = true; + exception = exception is null ? e : new AggregateException(exception, e); + return KeyManagerOperationResult.FromException(exception); + } + finally { - var oldIndex = SshKeysInternal.IndexOf(reordered[i]); - if (oldIndex != i) - SshKeysInternal.Move(oldIndex, i); + if (semaphoreAcquired) + _semaphoreSlim.Release(); + _backupService.EndOperationLog(errorsOccured); + Processing = false; } } /// - /// Generates a new SSH key. + /// Renames all files associated with the given to a new base file name, + /// preserving each file's original extension. If any target file already exists and + /// is , a conflict result is returned. + /// On failure, all files are restored from backup. /// - /// The full path where the new key should be stored. - /// Parameters for key generation. - /// A value task representing the asynchronous operation. - public async ValueTask GenerateNewKey(string fullFilePath, SshKeyGenerateInfo generateParamsInfo) + /// + /// The whose associated files are to be renamed. + /// After a successful rename, the key is reloaded from the new primary file. + /// + /// + /// The new base file name (without extension) to assign to all files of the key. + /// Each file retains its original extension. + /// + /// A flag to indicate forceful overwrite of any existent files. + /// + /// A to observe while waiting for the semaphore + /// and during file move operations. + /// + /// + /// Thrown when another key operation is already in progress and the semaphore + /// could not be acquired within the timeout. + /// + /// + /// File moves are performed via wrapped in + /// , + /// since no native async move API exists in .NET. On same-volume moves, this is an atomic + /// metadata operation. Backups are created before any file is moved and deleted only on full success; + /// on any failure the backup is restored. + /// + public async ValueTask RenameKeyAsync(SshKeyFile key, string newFileName, + bool overwrite = false, CancellationToken token = default) { - if (File.Exists(fullFilePath)) - throw new InvalidOperationException("File already exists"); - if (GenerateKeyFile() is not { } keyFile) - throw new InvalidOperationException("Key file not generated"); - if (!await _semaphoreSlim.WaitAsync(100)) - throw new InvalidOperationException("Another key operation is in progress"); + var semaphoreAcquired = false; + _backupService.BeginOperationLog(); + var errorsOccurred = false; + BackedUpFile[] backupFiles = []; try { - await using var privateStream = new MemoryStream(); - var createdKey = SshKey.Generate(privateStream, generateParamsInfo); + Processing = true; + semaphoreAcquired = await _semaphoreSlim.WaitAsync(TimeSpan.FromSeconds(5), token); + if (!semaphoreAcquired) + throw new InvalidOperationException("Another key operation is in progress"); - switch (generateParamsInfo.KeyFormat) + backupFiles = _backupService.BackupFiles(key.KeyFiles).ToArray(); + var filePairs = (key.KeyFileInfo?.Files ?? []).Select(file => { - case SshKeyFormat.PuTTYv2: - case SshKeyFormat.PuTTYv3: - var puttyPath = generateParamsInfo.KeyFormat.ChangeExtension(fullFilePath); - await using (var fs = new FileStream(puttyPath, FileStreamOptions)) - await using (var sw = new StreamWriter(fs)) - { - await sw.WriteAsync(createdKey.ToPuttyFormat( - generateParamsInfo.Encryption, generateParamsInfo.KeyFormat)); - } - - await keyFile.Load(SshKeyFileSource.FromDisk(puttyPath), - Encoding.UTF8.GetBytes(generateParamsInfo.Encryption.Passphrase)); - break; - - case SshKeyFormat.OpenSSH: - default: - var pubPath = generateParamsInfo.KeyFormat.ChangeExtension(fullFilePath); - var privatePath = generateParamsInfo.KeyFormat.ChangeExtension(fullFilePath, false); - await using (var fs = new FileStream(privatePath, FileStreamOptions)) - await using (var sw = new StreamWriter(fs)) - { - await sw.WriteAsync( - createdKey.ToOpenSshFormat(generateParamsInfo.Encryption)); - } + ArgumentNullException.ThrowIfNull(file.Directory); + var newFileNameForFile = Path.ChangeExtension( + newFileName, + string.IsNullOrEmpty(file.Extension) ? null : file.Extension); + var destinationForFile = Path.Combine(file.Directory.FullName, newFileNameForFile); + Log(LogLevel.Debug, "Renaming file {file} to {newFileName}", file.FullName, newFileNameForFile); + Log(LogLevel.Debug, "Destination: {destination}", destinationForFile); + return (Source: file, Target: destinationForFile); + }).ToArray(); + + if (!overwrite && filePairs.Any(p => File.Exists(p.Target))) + { + Log(LogLevel.Debug, "Destination files already exist"); + return KeyManagerOperationResult.Conflict(new Exception("Destination files already exist")); + } - await using (var fs = new FileStream(pubPath, FileStreamOptions)) - await using (var sw = new StreamWriter(fs)) - { - await sw.WriteAsync(createdKey.ToOpenSshPublicFormat()); - } + foreach (var (source, target) in filePairs) + { + await Task.Run(() => source.MoveTo(target, true), token); + Log(LogLevel.Debug, "Successfully renamed file {file} to {newFileName}", source.FullName, source.Name); + } - await keyFile.Load(SshKeyFileSource.FromDisk(privatePath), - Encoding.UTF8.GetBytes(generateParamsInfo.Encryption.Passphrase)); - break; + var expectedExtension = key.Format?.GetExtension(false); + if (filePairs.Select(p => p.Source).FirstOrDefault(file => + string.Equals( + string.IsNullOrEmpty(file.Extension) ? null : file.Extension, + expectedExtension, + StringComparison.OrdinalIgnoreCase) && + file.Exists) is { } keyFileToLoad) + { + Log(LogLevel.Debug, "Loading key file {keyFile}", keyFileToLoad.FullName); + key.Load(SshKeyFileSource.FromDisk(keyFileToLoad.FullName)); + Log(LogLevel.Debug, "Successfully loaded key file {keyFile}", keyFileToLoad.FullName); + _backupService.DeleteBackupFiles(backupFiles); + return KeyManagerOperationResult.Success(); } - SshKeysInternal.Add(keyFile); + Log(LogLevel.Warning, "No valid key file found for key format {format}", key.Format); + throw new Exception("No valid key file found for key format"); } catch (Exception e) { - _logger.LogError(e, "Error generating key"); - throw; + errorsOccurred = true; + Log(LogLevel.Error, e, "Failed to change filename of {className}", nameof(SshKeyFile)); + _backupService.RestoreBackupFiles(backupFiles); + return KeyManagerOperationResult.FromException(e); } finally { - _semaphoreSlim.Release(); + if (semaphoreAcquired) + _semaphoreSlim.Release(); + _backupService.EndOperationLog(errorsOccurred); + Processing = false; } } /// - /// Triggers a re-search for SSH keys on the disk. + /// Changes the format of an existing SSH key. /// + /// The SSH key file to change. + /// The target SSH key format. + /// A cancellation token. /// A task representing the asynchronous operation. - public async Task RerunSearchAsync() + public async ValueTask ChangeFormatOfKeyAsync( + SshKeyFile key, + SshKeyFormat newFormat, + CancellationToken token = default) { - if (!await _semaphoreSlim.WaitAsync(100)) - throw new InvalidOperationException("Another key operation is in progress"); - if (_searching) - throw new InvalidOperationException("Can't rerun search while searching"); + if (!key.IsInitialized) + return KeyManagerOperationResult.Failure(new InvalidOperationException("Key file not initialized")); + + PrivateKeyFile? privateKeyFile = key; try { - SshKeysInternal.Clear(); - await SearchForKeysAndUpdateCollection(); + ArgumentNullException.ThrowIfNull(privateKeyFile); + ArgumentException.ThrowIfNullOrWhiteSpace(key.AbsoluteFilePath); } catch (Exception e) { - _logger.LogError(e, "Unhandled error during key re-search"); + return KeyManagerOperationResult.Failure(e); } - finally - { - _semaphoreSlim.Release(); - } - } - private async Task WatcherOnRenamed(RenamedEventArgs e) - { - if (!await _semaphoreSlim.WaitAsync(100)) - return; + _backupService.BeginOperationLog(); + var filePath = newFormat.ChangeExtension(Path.GetFullPath(key.AbsoluteFilePath), false); + var writtenFiles = new List(); + BackedUpFile[] backupFiles = []; + var semaphoreAcquired = false; + var errorsOccured = false; try { - if (SshKeysInternal.SingleOrDefault(k => k.AbsoluteFilePath == Path.ChangeExtension(e.OldFullPath, null)) is - { } oldKey) - SshKeyGotDeleted(oldKey, EventArgs.Empty); - await AddKeyAsync(SshKeyFileSource.FromDisk(Path.ChangeExtension(e.FullPath, null))); - } - catch (Exception exception) - { - _logger.LogError(exception, "Error handling renamed key"); - } - finally - { - _semaphoreSlim.Release(); - } - } + Processing = true; + backupFiles = _backupService.BackupFiles(key.KeyFiles).ToArray(); - private void WatcherOnDeleted(object? sender, FileSystemEventArgs eventArgs) - { - if (!_semaphoreSlim.Wait(100)) - return; - try - { - var normalizedPath = Path.ChangeExtension( - Path.GetFullPath(eventArgs.FullPath), null); + semaphoreAcquired = await _semaphoreSlim.WaitAsync(TimeSpan.FromSeconds(2), token); + if (!semaphoreAcquired) + throw new InvalidOperationException("Another key operation is in progress"); - var key = SshKeys.SingleOrDefault(k => - string.Equals(k.AbsoluteFilePath, normalizedPath, - StringComparison.OrdinalIgnoreCase)); + writtenFiles.AddRange( + await _keyFileWriterService.WriteToFileInSpecificFormat( + newFormat, + key.Password.ToSshKeyEncryption(), + privateKeyFile, + filePath, true)); - if (key is null) - return; + key.Load(SshKeyFileSource.FromDisk(filePath)); + Log( + LogLevel.Debug, "Successfully changed format of key {key} to {format}", + key.AbsoluteFilePath, newFormat); - _logger.LogDebug("Key {key} deleted", key.AbsoluteFilePath); - SshKeyGotDeleted(key, EventArgs.Empty); + foreach (var backupFile in backupFiles) + { + if (!writtenFiles.Contains(backupFile.InitialFile.FullName, StringComparer.OrdinalIgnoreCase)) + { + backupFile.InitialFile.Delete(); + Log(LogLevel.Debug, "Deleted source key file {file}", backupFile.InitialFile.FullName); + } + } + + _backupService.DeleteBackupFiles(backupFiles); + return KeyManagerOperationResult.Success(); } catch (Exception e) { - _logger.LogError(e, "Error handling deleted key"); + var exc = e; + errorsOccured = true; + Log(LogLevel.Error, e, "Error changing format of key – attempting rollback"); + foreach (var writtenFile in writtenFiles) + try + { + File.Delete(writtenFile); + } + catch (Exception ex) + { + exc = exc switch + { + AggregateException agg => new AggregateException(agg.InnerExceptions.Append(ex)), + not null => new AggregateException(exc, ex), + _ => ex + }; + Log(LogLevel.Warning, ex, "Could not delete created file '{path}'", writtenFile); + } + + try + { + _backupService.RestoreBackupFiles(backupFiles); + } + catch (Exception exception) + { + exc = exc switch + { + AggregateException agg => new AggregateException(agg.InnerExceptions.Append(exception)), + not null => new AggregateException(exc, exception), + _ => exception + }; + Log(LogLevel.Warning, exception, "Could not restore backup files"); + } + + return KeyManagerOperationResult.FromException(exc); } finally { - _semaphoreSlim.Release(); + if (semaphoreAcquired) + _semaphoreSlim.Release(); + _backupService.EndOperationLog(errorsOccured); + Processing = false; } } - private async Task WatcherOnCreated(FileSystemEventArgs e) + /// + /// Generates a new SSH key and adds it to the managed collection. + /// + /// The full path where the new key should be stored. + /// Parameters for key generation. + /// Whether to overwrite an existing file at the target path. + /// A indicating the outcome of the operation. + public async ValueTask GenerateNewKey(string fullFilePath, + SshKeyGenerateInfo generateParamsInfo, bool overwrite = false) { + if (File.Exists(fullFilePath) && !overwrite) + return KeyManagerOperationResult.Failure(new InvalidOperationException("File already exists")); if (!await _semaphoreSlim.WaitAsync(100)) - return; + return KeyManagerOperationResult.FromException( + new InvalidOperationException("Another key operation is in progress")); try { - var keyFilePath = string.Equals( - Path.GetExtension(e.FullPath), - SshKeyFormatExtension.PuttyKeyFileExtension, - StringComparison.OrdinalIgnoreCase) - ? e.FullPath - : Path.ChangeExtension(e.FullPath, null); - - if (SshKeys.Any(key => - string.Equals(key.AbsoluteFilePath, keyFilePath, - StringComparison.OrdinalIgnoreCase))) - return; - - await AddKeyAsync(SshKeyFileSource.FromDisk(keyFilePath)); + Processing = true; + var filePath = generateParamsInfo.KeyFormat.ChangeExtension(fullFilePath, false); + var keyFile = await _keyGenerator.Generate(filePath, generateParamsInfo, overwrite); + _sshKeysInternal.Add(keyFile); + return KeyManagerOperationResult.Success(); } - catch (Exception exception) + catch (Exception e) { - _logger.LogError(exception, "Error adding key"); + Log(LogLevel.Error, e, "Error generating key"); + return KeyManagerOperationResult.FromException(e); } finally { _semaphoreSlim.Release(); + Processing = false; } } - private void SshKeysOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (e.NewItems is { } newItems) - foreach (var key in newItems.OfType()) - try - { - key.GotDeleted += SshKeyGotDeleted; - } - catch (Exception exception) - { - _logger.LogError(exception, "Error adding GotDeleted event handler"); - } - - break; - - case NotifyCollectionChangedAction.Remove: - if (e.OldItems is { } oldItems) - foreach (var key in oldItems.OfType()) - try - { - key.GotDeleted -= SshKeyGotDeleted; - key.Dispose(); - } - catch (Exception exception) - { - _logger.LogError(exception, "Error removing GotDeleted event handler"); - } - - break; - } - - SshKeysCount = SshKeysInternal.Count; - } - - private SshKeyFile? GenerateKeyFile() + /// + /// Triggers a re-search for SSH keys on disk and rebuilds the managed collection. + /// + /// A cancellation token. + /// A indicating the outcome of the operation. + public async ValueTask RerunSearchAsync(CancellationToken token = default) { + if (!await _semaphoreSlim.WaitAsync(100, token)) + return KeyManagerOperationResult.FromException( + new InvalidOperationException("Another key operation is in progress")); try { - if (_resolver.GetService() is { } keyFile) - { - keyFile.AttachChangeFormatHandler(ChangeFormatOfKeyAsync); - return keyFile; - } + Processing = true; + _sshKeysInternal.Clear(); + await SearchForKeysAndUpdateCollectionAsync(token); } catch (Exception e) { - _logger.LogError(e, "Error resolving generic SshKeyFile"); + Log(LogLevel.Error, e, "Unhandled error during key re-search"); + return KeyManagerOperationResult.FromException(e); } - return null; + finally + { + _semaphoreSlim.Release(); + Processing = false; + } + + return KeyManagerOperationResult.Success(); } - private async Task AddKeyAsync(SshKeyFileSource keyFileSource) + private void AddKey(SshKeyFileSource keyFileSource) { - if (SshKeysInternal.Any(k => - string.Equals(k.AbsoluteFilePath, keyFileSource.AbsolutePath, + if (_sshKeysInternal.Any(k => + string.Equals( + k.AbsoluteFilePath, keyFileSource.AbsolutePath, StringComparison.OrdinalIgnoreCase))) return; try { - if (GenerateKeyFile() is not { } keyFileGenerated) - throw new InvalidOperationException("Key file not generated"); - - await keyFileGenerated.Load(keyFileSource); - SshKeysInternal.Add(keyFileGenerated); + var keyFileGenerated = _keyFactory.Create(); + keyFileGenerated.Load(keyFileSource); + _sshKeysInternal.Add(keyFileGenerated); } catch (Exception e) { - _logger.LogError(e, "Error loading keyfile {filePath}", keyFileSource.AbsolutePath); + Log(LogLevel.Error, e, "Error loading keyfile {filePath}", keyFileSource.AbsolutePath); } } - private async Task SearchForKeysAndUpdateCollection() + private async ValueTask SearchForKeysAndUpdateCollectionAsync( + CancellationToken token = default) { - Interlocked.Exchange(ref _searching, true); + if (_directoryCrawler.IsSearching) + return KeyManagerOperationResult.Conflict(new InvalidOperationException("Key search already in progress")); + var semaphoreAcquired = false; + var errorsOccured = false; + _backupService.BeginOperationLog(); try { - foreach (var key in await _directoryCrawler.GetPossibleKeyFilesOnDisk()) - await AddKeyAsync(key); + semaphoreAcquired = await _semaphoreSlim.WaitAsync(TimeSpan.FromSeconds(5), token); + await foreach (var sshKey in _directoryCrawler.GetPossibleKeyFilesOnDiskAsyncEnumerable(token)) + AddKey(sshKey); + return KeyManagerOperationResult.Success(); + } + catch (Exception e) + { + errorsOccured = true; + Log(LogLevel.Error, e, "Error searching for keys"); + return KeyManagerOperationResult.Failure(e); } finally { - Interlocked.Exchange(ref _searching, false); + if (semaphoreAcquired) + _semaphoreSlim.Release(); + _backupService.EndOperationLog(errorsOccured); } } - private void SshKeyGotDeleted(object? sender, EventArgs e) +#pragma warning disable CA2254 + private void Log(LogLevel level, [StructuredMessageTemplate] string? message, params object?[] args) { - if (sender is not SshKeyFile key) return; - SshKeysInternal.Remove(key); + _logger.Log(level, message, args); + _backupService.WriteToOperationLog(level, message, args); } - private void TryDeleteFile(string path) + private void Log(LogLevel level, Exception? exception, [StructuredMessageTemplate] string? message, + params object?[] args) { - try - { - File.Delete(path); - } - catch (Exception e) - { - _logger.LogWarning(e, "Could not delete temporary file '{path}'", path); - } - } - - public void Dispose() - { - _watcher.Dispose(); - _semaphoreSlim.Dispose(); - foreach (var sshKeyFile in SshKeys) - { - sshKeyFile.Dispose(); - } - GC.SuppressFinalize(this); + _logger.Log(level, exception, message, args); + _backupService.WriteToOperationLog(level, exception, message, args); } +#pragma warning restore CA2254 } \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Enums/MessageBoxButtons.cs b/OpenSSH_GUI.Dialogs/Enums/MessageBoxButtons.cs index e16b3a2..4d274b6 100644 --- a/OpenSSH_GUI.Dialogs/Enums/MessageBoxButtons.cs +++ b/OpenSSH_GUI.Dialogs/Enums/MessageBoxButtons.cs @@ -1,3 +1,5 @@ +using OpenSSH_GUI.Dialogs.Views; + namespace OpenSSH_GUI.Dialogs.Enums; /// diff --git a/OpenSSH_GUI.Dialogs/Enums/MessageBoxIcon.cs b/OpenSSH_GUI.Dialogs/Enums/MessageBoxIcon.cs index 9231bc5..aca0424 100644 --- a/OpenSSH_GUI.Dialogs/Enums/MessageBoxIcon.cs +++ b/OpenSSH_GUI.Dialogs/Enums/MessageBoxIcon.cs @@ -1,3 +1,5 @@ +using OpenSSH_GUI.Dialogs.Views; + namespace OpenSSH_GUI.Dialogs.Enums; /// diff --git a/OpenSSH_GUI.Dialogs/Enums/MessageBoxResult.cs b/OpenSSH_GUI.Dialogs/Enums/MessageBoxResult.cs index 603d908..747efab 100644 --- a/OpenSSH_GUI.Dialogs/Enums/MessageBoxResult.cs +++ b/OpenSSH_GUI.Dialogs/Enums/MessageBoxResult.cs @@ -1,3 +1,5 @@ +using OpenSSH_GUI.Dialogs.Views; + namespace OpenSSH_GUI.Dialogs.Enums; /// diff --git a/OpenSSH_GUI.Dialogs/Interfaces/IMessageBoxProvider.cs b/OpenSSH_GUI.Dialogs/Interfaces/IMessageBoxProvider.cs index 98f9ab7..9cd18dc 100644 --- a/OpenSSH_GUI.Dialogs/Interfaces/IMessageBoxProvider.cs +++ b/OpenSSH_GUI.Dialogs/Interfaces/IMessageBoxProvider.cs @@ -1,3 +1,4 @@ +using Material.Icons; using OpenSSH_GUI.Dialogs.Enums; using OpenSSH_GUI.Dialogs.Models; @@ -21,7 +22,7 @@ Task ShowMessageBoxAsync( string title, string message, MessageBoxButtons buttons = MessageBoxButtons.Ok, - MessageBoxIcon icon = MessageBoxIcon.None); + MaterialIconKind icon = MaterialIconKind.ErrorOutline); /// /// Shows a modal message box and returns the button the user clicked. @@ -34,6 +35,11 @@ Task ShowMessageBoxAsync( /// Task ShowMessageBoxAsync(MessageBoxParams @params); + public Task ShowErrorMessageBoxAsync(Exception? e = null, string? customMessage = null); + + public Task ShowRetryMessageBoxAsync(Func> tryActionAsync, string title, string message, + MaterialIconKind icon = MaterialIconKind.ErrorOutline, int retries = 3, bool showTryCountInTitle = true); + /// /// Shows a modal secure-input (password) prompt and returns the result. /// @@ -63,7 +69,8 @@ Task ShowMessageBoxAsync( /// The parameters for the secure-input prompt. /// /// A whose - /// contains the UTF-8 encoded password, or null when the user cancels. + /// contains the by the encoded password, or null when the user + /// cancels. /// The caller is responsible for disposing the result to zero the buffer. /// Task ShowSecureInputAsync(SecureInputParams @params); diff --git a/OpenSSH_GUI.Dialogs/Models/MessageBoxParams.cs b/OpenSSH_GUI.Dialogs/Models/MessageBoxParams.cs index 396162b..df95c3b 100644 --- a/OpenSSH_GUI.Dialogs/Models/MessageBoxParams.cs +++ b/OpenSSH_GUI.Dialogs/Models/MessageBoxParams.cs @@ -27,9 +27,4 @@ public class MessageBoxParams /// Gets or sets the icon shown beside the message. /// public MaterialIconKind? Icon { get; set; } - - /// - /// Gets or sets the legacy icon shown beside the message. - /// - public MessageBoxIcon LegacyIcon { get; set; } = MessageBoxIcon.None; } \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Models/SecureInputParams.cs b/OpenSSH_GUI.Dialogs/Models/SecureInputParams.cs index 828a2e9..18c16d5 100644 --- a/OpenSSH_GUI.Dialogs/Models/SecureInputParams.cs +++ b/OpenSSH_GUI.Dialogs/Models/SecureInputParams.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace OpenSSH_GUI.Dialogs.Models; /// @@ -21,4 +23,9 @@ public class SecureInputParams : MessageBoxParams /// Defaults to 0 (unlimited). /// public int MaxLength { get; set; } = 0; + + /// + /// Gets or sets the character encoding used for input. + /// + public Encoding Encoding { get; set; } = Encoding.UTF8; } \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Models/SecureInputResult.cs b/OpenSSH_GUI.Dialogs/Models/SecureInputResult.cs index 56839b8..63ea54e 100644 --- a/OpenSSH_GUI.Dialogs/Models/SecureInputResult.cs +++ b/OpenSSH_GUI.Dialogs/Models/SecureInputResult.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using OpenSSH_GUI.Dialogs.Views; namespace OpenSSH_GUI.Dialogs.Models; @@ -20,10 +21,7 @@ public sealed class SecureInputResult : IDisposable /// Ownership of is transferred to this instance. /// /// The UTF-8 encoded password bytes. Must not be null. - internal SecureInputResult(byte[] buffer) - { - _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); - } + internal SecureInputResult(byte[] buffer) => _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); /// /// Gets the UTF-8 encoded password bytes. diff --git a/OpenSSH_GUI.Dialogs/OpenSSH_GUI.Dialogs.csproj b/OpenSSH_GUI.Dialogs/OpenSSH_GUI.Dialogs.csproj index ab6394a..be14827 100644 --- a/OpenSSH_GUI.Dialogs/OpenSSH_GUI.Dialogs.csproj +++ b/OpenSSH_GUI.Dialogs/OpenSSH_GUI.Dialogs.csproj @@ -1,14 +1,21 @@  - - - net10.0 - enable - enable - - - - - - - - + + enable + enable + + + + + + + + + + + ..\..\..\home\olli\.nuget\packages\reactiveui\23.2.1\lib\net10.0\ReactiveUI.dll + + + ..\..\..\home\olli\.nuget\packages\reactiveui.avalonia\11.4.12\lib\net10.0\ReactiveUI.Avalonia.dll + + + \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Services/MessageBoxProvider.cs b/OpenSSH_GUI.Dialogs/Services/MessageBoxProvider.cs index 7c8c8ca..ac15b89 100644 --- a/OpenSSH_GUI.Dialogs/Services/MessageBoxProvider.cs +++ b/OpenSSH_GUI.Dialogs/Services/MessageBoxProvider.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Material.Icons; using OpenSSH_GUI.Dialogs.Enums; using OpenSSH_GUI.Dialogs.Interfaces; using OpenSSH_GUI.Dialogs.Models; @@ -13,60 +14,92 @@ namespace OpenSSH_GUI.Dialogs.Services; public class MessageBoxProvider(Window owner) : IMessageBoxProvider { /// - public async Task ShowMessageBoxAsync( + public Task ShowMessageBoxAsync( string title, string message, MessageBoxButtons buttons = MessageBoxButtons.Ok, - MessageBoxIcon icon = MessageBoxIcon.None) - { - return await ShowMessageBoxAsync(new MessageBoxParams + MaterialIconKind icon = MaterialIconKind.ErrorOutline) => ShowMessageBoxAsync( + new MessageBoxParams { Title = title, Message = message, Buttons = buttons, - LegacyIcon = icon + Icon = icon }); - } /// - public async Task ShowMessageBoxAsync(MessageBoxParams @params) + public Task ShowMessageBoxAsync(MessageBoxParams @params) { var dialog = new MessageBoxDialog(@params); - return await dialog.ShowDialog(owner); + return dialog.ShowDialog(owner); + } + + public Task ShowErrorMessageBoxAsync(Exception? e = null, string? customMessage = null) + { + return ShowMessageBoxAsync( + new MessageBoxParams + { + Title = e?.GetType().Name ?? "Error", + Message = e switch + { + not null when !string.IsNullOrWhiteSpace(customMessage) => string.Join(" ", customMessage, e.Message), + null when !string.IsNullOrWhiteSpace(customMessage) => customMessage, + not null => e.ToString(), + _ => string.Empty + }, + Buttons = MessageBoxButtons.Ok, + Icon = MaterialIconKind.ErrorOutline + }); + } + + public async Task ShowRetryMessageBoxAsync(Func> tryActionAsync, string title, string message, + MaterialIconKind icon = MaterialIconKind.ErrorOutline, int retries = 3, bool showTryCountInTitle = true) + { + var tryCount = 1; + + while (tryCount <= retries) + { + if (showTryCountInTitle) + title = string.Join(" ", title, string.Join("/", tryCount, retries)); + if (await tryActionAsync() is null or true) + return true; + if (await ShowMessageBoxAsync(title, message, MessageBoxButtons.OkCancel, icon) is MessageBoxResult.Cancel) + return true; + tryCount++; + } + + return tryCount <= retries; } /// - public async Task ShowSecureInputAsync( + public Task ShowSecureInputAsync( string title, string prompt, int minLength = 1, - int maxLength = 0) - { - return await ShowSecureInputAsync(new SecureInputParams + int maxLength = 0) => ShowSecureInputAsync( + new SecureInputParams { Title = title, Prompt = prompt, MinLength = minLength, MaxLength = maxLength }); - } /// - public async Task ShowSecureInputAsync(SecureInputParams @params) + public Task ShowSecureInputAsync(SecureInputParams @params) { var dialog = new SecureInputDialog(@params); - return await dialog.ShowDialog(owner); + return dialog.ShowDialog(owner); } /// - public async Task ShowValidatedInputAsync( + public Task ShowValidatedInputAsync( string title, string prompt, Func validator, string initialValue = "", - string watermark = "Enter value…") - { - return await ShowValidatedInputAsync(new ValidatedInputParams + string watermark = "Enter value…") => ShowValidatedInputAsync( + new ValidatedInputParams { Title = title, Prompt = prompt, @@ -74,12 +107,11 @@ public async Task ShowMessageBoxAsync(MessageBoxParams @params InitialValue = initialValue, Watermark = watermark }); - } /// - public async Task ShowValidatedInputAsync(ValidatedInputParams @params) + public Task ShowValidatedInputAsync(ValidatedInputParams @params) { var dialog = new ValidatedInputDialog(@params); - return await dialog.ShowDialog(owner); + return dialog.ShowDialog(owner); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Views/MessageBoxDialog.axaml b/OpenSSH_GUI.Dialogs/Views/MessageBoxDialog.axaml index 79438b8..6a5c6b7 100644 --- a/OpenSSH_GUI.Dialogs/Views/MessageBoxDialog.axaml +++ b/OpenSSH_GUI.Dialogs/Views/MessageBoxDialog.axaml @@ -3,15 +3,16 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="OpenSSH_GUI.Dialogs.Views.MessageBoxDialog" Title="Message" - Width="440" - MinWidth="300" - SizeToContent="Height" + MinWidth="440" + SizeToContent="WidthAndHeight" CanResize="False" + CanMaximize="False" + CanMinimize="False" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" - ExtendClientAreaChromeHints="NoChrome" - ExtendClientAreaToDecorationsHint="False"> - + ExtendClientAreaToDecorationsHint="False" + Closing="Window_OnClosing"> + @@ -22,6 +23,7 @@ VerticalAlignment="Top"> + public partial class MessageBoxDialog : Window { - /// - /// Initialises a new with the provided content and configuration. - /// - /// The window title bar text. - /// The message body shown to the user. - /// Which button set to display. Defaults to . - /// Optional icon shown to the left of the message. Defaults to . - public MessageBoxDialog( - string title, - string message, - MessageBoxButtons buttons = MessageBoxButtons.Ok, - MessageBoxIcon icon = MessageBoxIcon.None) - { - InitializeComponent(); - - Title = title; - PART_Message.Text = message; - - ApplyButtons(buttons); - ApplyIcon(icon); - } - + private bool _isInternalClose; /// /// Initialises a new with the provided . /// @@ -49,10 +28,7 @@ public MessageBoxDialog(MessageBoxParams @params) ApplyButtons(@params.Buttons); - if (@params.Icon.HasValue) - ApplyIcon(@params.Icon); - else - ApplyIcon(@params.LegacyIcon); + ApplyIcon(@params.Icon); } // ------------------------------------------------------------------------- @@ -88,26 +64,6 @@ private void ApplyButtons(MessageBoxButtons buttons) } } - /// - /// Applies the icon glyph and colour that correspond to the requested type. - /// Uses Unicode symbols so no external icon library is required. - /// - private void ApplyIcon(MessageBoxIcon icon) - { - if (icon == MessageBoxIcon.None) return; - - PART_Icon.IsVisible = true; - - (PART_Icon.Text, PART_Icon.Foreground) = icon switch - { - MessageBoxIcon.Information => ("ℹ", Brushes.DodgerBlue), - MessageBoxIcon.Warning => ("⚠", Brushes.Orange), - MessageBoxIcon.Error => ("✖", Brushes.Crimson), - MessageBoxIcon.Question => ("?", Brushes.MediumSlateBlue), - _ => (string.Empty, Brushes.Transparent) - }; - } - /// /// Applies the to the dialog. /// @@ -126,21 +82,35 @@ private void ApplyIcon(MaterialIconKind? icon) private void OnYesClick(object? sender, RoutedEventArgs e) { + _isInternalClose = true; Close(MessageBoxResult.Yes); } private void OnNoClick(object? sender, RoutedEventArgs e) { + _isInternalClose = true; Close(MessageBoxResult.No); } private void OnOkClick(object? sender, RoutedEventArgs e) { + _isInternalClose = true; Close(MessageBoxResult.Ok); } private void OnCancelClick(object? sender, RoutedEventArgs e) { + _isInternalClose = true; + Close(MessageBoxResult.Cancel); + } + private void Window_OnClosing(object? sender, WindowClosingEventArgs e) + { + if (_isInternalClose) + return; + + e.Cancel = true; + + _isInternalClose = true; Close(MessageBoxResult.Cancel); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml b/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml index d48cb0f..a640091 100644 --- a/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml +++ b/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml @@ -3,15 +3,17 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="OpenSSH_GUI.Dialogs.Views.SecureInputDialog" Title="Secure Input" - Width="380" - SizeToContent="Height" + MinWidth="380" + SizeToContent="WidthAndHeight" CanResize="False" + CanMaximize="False" + CanMinimize="False" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" - ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaToDecorationsHint="False" - Opened="OnOpened"> - + Opened="OnOpened" + Closing="Window_OnClosing"> + @@ -35,13 +37,13 @@ diff --git a/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml.cs b/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml.cs index 7841943..cf602d0 100644 --- a/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml.cs +++ b/OpenSSH_GUI.Dialogs/Views/SecureInputDialog.axaml.cs @@ -42,8 +42,8 @@ namespace OpenSSH_GUI.Dialogs.Views; /// public partial class SecureInputDialog : Window { + private readonly Encoding _encoding = Encoding.UTF8; private readonly int _maxLength; - private readonly int _minLength; // Each entry represents the UTF-8 encoding of one logical character typed @@ -51,6 +51,8 @@ public partial class SecureInputDialog : Window // character correctly even for multi-byte code points. private readonly List _segments = new(); + private bool _isInternalClose; + /// /// Initialises a new . /// @@ -93,6 +95,7 @@ public SecureInputDialog(SecureInputParams @params) { InitializeComponent(); + _encoding = @params.Encoding; Title = @params.Title; PART_Prompt.Text = @params.Prompt; PART_Prompt.IsVisible = !string.IsNullOrWhiteSpace(@params.Prompt); @@ -121,10 +124,7 @@ public SecureInputDialog(SecureInputParams @params) /// /// Moves keyboard focus to the password field once the window is shown. /// - private void OnOpened(object? sender, EventArgs e) - { - PART_Input.Focus(); - } + private void OnOpened(object? sender, EventArgs e) { PART_Input.Focus(); } // ------------------------------------------------------------------------- // Secure input interception @@ -144,11 +144,7 @@ private void OnInputTextInput(object? sender, TextInputEventArgs e) // Encode each character individually so Backspace can remove exactly // one logical character at a time. - foreach (var ch in e.Text) - { - var encoded = Encoding.UTF8.GetBytes(new[] { ch }); - _segments.Add(encoded); - } + foreach (var encoded in e.Text.Select(ch => _encoding.GetBytes([ch]))) _segments.Add(encoded); SyncDisplay(); HideError(); @@ -181,14 +177,12 @@ private void OnInputKeyDown(object? sender, KeyEventArgs e) // Button handlers // ------------------------------------------------------------------------- - private void OnOkClick(object? sender, RoutedEventArgs e) - { - TryConfirm(); - } + private void OnOkClick(object? sender, RoutedEventArgs e) { TryConfirm(); } private void OnCancelClick(object? sender, RoutedEventArgs e) { ZeroSegments(); + _isInternalClose = true; Close(null); } @@ -211,7 +205,7 @@ private void TryConfirm() var buffer = ConsolidateBuffer(); ZeroSegments(); - + _isInternalClose = true; Close(new SecureInputResult(buffer)); } @@ -274,4 +268,14 @@ private void HideError() PART_Error.IsVisible = false; PART_Error.Text = string.Empty; } + private void Window_OnClosing(object? sender, WindowClosingEventArgs e) + { + if (_isInternalClose) + return; + + e.Cancel = true; + _isInternalClose = true; + Closing -= Window_OnClosing; + Close(null); + } } \ No newline at end of file diff --git a/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml b/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml index 35c2dae..65b2a7c 100644 --- a/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml +++ b/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml @@ -3,15 +3,16 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" x:Class="OpenSSH_GUI.Dialogs.Views.ValidatedInputDialog" Title="Input" - Width="420" - SizeToContent="Height" + MinWidth="420" + SizeToContent="WidthAndHeight" CanResize="False" + CanMaximize="False" + CanMinimize="False" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" - ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaToDecorationsHint="False" - Opened="OnOpened"> - + Opened="OnOpened" Closing="Window_OnClosing"> + @@ -31,12 +32,12 @@ diff --git a/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml.cs b/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml.cs index ffc0e56..3542cd6 100644 --- a/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml.cs +++ b/OpenSSH_GUI.Dialogs/Views/ValidatedInputDialog.axaml.cs @@ -25,6 +25,8 @@ public partial class ValidatedInputDialog : Window { private readonly Func _validator; + private bool _isInternalClose; + /// /// Initialises a new . /// @@ -54,7 +56,7 @@ public ValidatedInputDialog( Title = title; PART_Prompt.Text = prompt; PART_Prompt.IsVisible = !string.IsNullOrWhiteSpace(prompt); - PART_Input.Watermark = watermark; + PART_Input.PlaceholderText = watermark; PART_Input.Text = initialValue; // Subscribe to live text changes for real-time validation. @@ -84,7 +86,7 @@ public ValidatedInputDialog(ValidatedInputParams @params) PART_MaterialIcon.Kind = @params.Icon.Value; } - PART_Input.Watermark = @params.Watermark; + PART_Input.PlaceholderText = @params.Watermark; PART_Input.Text = @params.InitialValue; // Subscribe to live text changes for real-time validation. @@ -108,10 +110,7 @@ private void OnOpened(object? sender, EventArgs e) // Validation // ------------------------------------------------------------------------- - private void OnInputTextChanged(object? sender, TextChangedEventArgs e) - { - Validate(); - } + private void OnInputTextChanged(object? sender, TextChangedEventArgs e) { Validate(); } /// /// Runs the external validator against the current input and updates the @@ -155,13 +154,11 @@ private void OnInputKeyDown(object? sender, KeyEventArgs e) } } - private void OnOkClick(object? sender, RoutedEventArgs e) - { - TryConfirm(); - } + private void OnOkClick(object? sender, RoutedEventArgs e) { TryConfirm(); } private void OnCancelClick(object? sender, RoutedEventArgs e) { + _isInternalClose = true; Close(new ValidatedInputResult(null)); } @@ -180,6 +177,7 @@ private void TryConfirm() return; } + _isInternalClose = true; Close(new ValidatedInputResult(text)); } @@ -194,4 +192,15 @@ private void HideError() PART_Error.IsVisible = false; PART_Error.Text = string.Empty; } + private void Window_OnClosing(object? sender, WindowClosingEventArgs e) + { + if (_isInternalClose) + return; + + e.Cancel = true; + + _isInternalClose = true; + Closing -= Window_OnClosing; + Close(new ValidatedInputResult(null)); + } } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs b/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs index ca8d72e..b704e5c 100644 --- a/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs +++ b/OpenSSH_GUI.SshConfig/Extensions/SshConfigurationExtensions.cs @@ -19,11 +19,9 @@ public static class SshConfigurationExtensions /// Path relative to the base path stored in of /// . /// + /// /// The . - public IConfigurationBuilder AddSshConfig(string path, Action? loggingAction = null) - { - return builder.AddSshConfig(null, path, false, false, loggingAction); - } + public IConfigurationBuilder AddSshConfig(string path, Action? loggingAction = null) => builder.AddSshConfig(null, path, false, false, loggingAction); /// /// Adds the SSH configuration file at to the . @@ -33,11 +31,10 @@ public IConfigurationBuilder AddSshConfig(string path, Action /// . /// /// Whether the file is optional. + /// /// The . - public IConfigurationBuilder AddSshConfig(string path, bool optional, Action? loggingAction = null) - { - return builder.AddSshConfig(null, path, optional, false, loggingAction); - } + public IConfigurationBuilder AddSshConfig(string path, bool optional, + Action? loggingAction = null) => builder.AddSshConfig(null, path, optional, false, loggingAction); /// /// Adds the SSH configuration file at to the . @@ -48,12 +45,10 @@ public IConfigurationBuilder AddSshConfig(string path, bool optional, Action /// Whether the file is optional. /// Whether the configuration should be reloaded if the file changes. + /// /// The . public IConfigurationBuilder AddSshConfig(string path, bool optional, - bool reloadOnChange, Action? loggingAction = null) - { - return builder.AddSshConfig(null, path, optional, reloadOnChange, loggingAction); - } + bool reloadOnChange, Action? loggingAction = null) => builder.AddSshConfig(null, path, optional, reloadOnChange, loggingAction); /// /// Adds the SSH configuration file at to the . @@ -65,6 +60,7 @@ public IConfigurationBuilder AddSshConfig(string path, bool optional, /// /// Whether the file is optional. /// Whether the configuration should be reloaded if the file changes. + /// /// The . public IConfigurationBuilder AddSshConfig(IFileProvider? fileProvider, string path, bool optional, bool reloadOnChange, Action? loggingAction = null) @@ -72,22 +68,25 @@ public IConfigurationBuilder AddSshConfig(IFileProvider? fileProvider, ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(path); - return builder.AddSshConfig(s => - { - s.FileProvider = fileProvider; - s.Path = path; - s.Optional = optional; - s.ReloadOnChange = reloadOnChange; - s.ResolveFileProvider(); - }, loggingAction); + return builder.AddSshConfig( + s => + { + s.FileProvider = fileProvider; + s.Path = path; + s.Optional = optional; + s.ReloadOnChange = reloadOnChange; + s.ResolveFileProvider(); + }, loggingAction); } /// /// Adds an SSH configuration source to the . /// /// Configures the source. + /// /// The . - public IConfigurationBuilder AddSshConfig(Action? configureSource, Action? loggingAction) + public IConfigurationBuilder AddSshConfig(Action? configureSource, + Action? loggingAction) { var source = new SshConfigurationSource { diff --git a/OpenSSH_GUI.SshConfig/Extensions/SshHostBlockExtensions.cs b/OpenSSH_GUI.SshConfig/Extensions/SshHostBlockExtensions.cs index 4c69d15..05c4bbe 100644 --- a/OpenSSH_GUI.SshConfig/Extensions/SshHostBlockExtensions.cs +++ b/OpenSSH_GUI.SshConfig/Extensions/SshHostBlockExtensions.cs @@ -13,11 +13,9 @@ public static class SshHostBlockExtensions /// /// The block to convert. /// A type-safe representation of the block. - public static SshHostSettings GetSettings(this SshBlock block) - { - return GetSettingsFromEntries(block.GetEntries(), - block is SshHostBlock hostBlock ? hostBlock.Patterns.ToArray() : null); - } + public static SshHostSettings GetSettings(this SshBlock block) => GetSettingsFromEntries( + block.GetEntries(), + block is SshHostBlock hostBlock ? hostBlock.Patterns.ToArray() : null); /// /// Extracts from a collection of . @@ -94,7 +92,12 @@ public static SshHostBlock WithSettings(this SshHostBlock block, SshHostSettings var handledKeys = new HashSet(StringComparer.OrdinalIgnoreCase) { - "HostName", "User", "Port", "IdentityFile", "ProxyJump", "LocalForward" + "HostName", + "User", + "Port", + "IdentityFile", + "ProxyJump", + "LocalForward" }; var addedHostName = settings.HostName == null; @@ -159,8 +162,8 @@ public static SshHostBlock WithSettings(this SshHostBlock block, SshHostSettings break; } - else if ((item is SshConfigEntry otherEntry && settings.OtherEntries is { Length: > 0 } && - settings.OtherEntries.Contains(otherEntry)) || item is not SshConfigEntry) + else if (item is SshConfigEntry otherEntry && settings.OtherEntries is { Length: > 0 } && + settings.OtherEntries.Contains(otherEntry) || item is not SshConfigEntry) newItems.Add(item); // Add any settings that weren't in the original block @@ -177,11 +180,19 @@ public static SshHostBlock WithSettings(this SshHostBlock block, SshHostSettings // Add new other entries that weren't there if (settings.OtherEntries is not { Length: > 0 }) - return block with { Items = newItems.ToImmutable(), RawHeaderText = string.Empty }; + return block with + { + Items = newItems.ToImmutable(), + RawHeaderText = string.Empty + }; foreach (var oe in settings.OtherEntries) if (!newItems.Contains(oe)) newItems.Add(oe); - return block with { Items = newItems.ToImmutable(), RawHeaderText = string.Empty }; + return block with + { + Items = newItems.ToImmutable(), + RawHeaderText = string.Empty + }; } } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Models/SshBlock.cs b/OpenSSH_GUI.SshConfig/Models/SshBlock.cs index fd16024..52c55dc 100644 --- a/OpenSSH_GUI.SshConfig/Models/SshBlock.cs +++ b/OpenSSH_GUI.SshConfig/Models/SshBlock.cs @@ -93,10 +93,7 @@ public SshHostBlock( int lineNumber, string rawHeaderText, string? headerComment) - : base(items, lineNumber, rawHeaderText, headerComment) - { - Patterns = patterns; - } + : base(items, lineNumber, rawHeaderText, headerComment) => Patterns = patterns; /// /// Gets the hostname patterns declared on the Host header line. @@ -114,16 +111,10 @@ public SshHostBlock( /// /// One or more hostname patterns. /// Optional initial block contents. - public static SshHostBlock Create(IEnumerable patterns, IEnumerable? items = null) - { - return new SshHostBlock([..patterns], [..items ?? []], 0, string.Empty, null); - } + public static SshHostBlock Create(IEnumerable patterns, IEnumerable? items = null) => new([..patterns], [..items ?? []], 0, string.Empty, null); /// - public override string ToString() - { - return $"Host {string.Join(' ', Patterns)}"; - } + public override string ToString() => $"Host {string.Join(' ', Patterns)}"; } /// @@ -142,10 +133,7 @@ public SshMatchBlock( int lineNumber, string rawHeaderText, string? headerComment) - : base(items, lineNumber, rawHeaderText, headerComment) - { - Criteria = criteria; - } + : base(items, lineNumber, rawHeaderText, headerComment) => Criteria = criteria; /// /// Gets the criteria that must all be satisfied simultaneously for this block to apply. @@ -159,14 +147,8 @@ public SshMatchBlock( /// /// One or more match criteria. /// Optional initial block contents. - public static SshMatchBlock Create(IEnumerable criteria, IEnumerable? items = null) - { - return new SshMatchBlock([..criteria], [..items ?? []], 0, string.Empty, null); - } + public static SshMatchBlock Create(IEnumerable criteria, IEnumerable? items = null) => new([..criteria], [..items ?? []], 0, string.Empty, null); /// - public override string ToString() - { - return $"Match {string.Join(' ', Criteria.Select(static c => c.ToString()))}"; - } + public override string ToString() { return $"Match {string.Join(' ', Criteria.Select(static c => c.ToString()))}"; } } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Models/SshConfigDocument.cs b/OpenSSH_GUI.SshConfig/Models/SshConfigDocument.cs index d118be9..2e43bb4 100644 --- a/OpenSSH_GUI.SshConfig/Models/SshConfigDocument.cs +++ b/OpenSSH_GUI.SshConfig/Models/SshConfigDocument.cs @@ -47,12 +47,10 @@ public SshConfigDocument(ImmutableArray globalItems, ImmutableArray public static SshConfigDocument Empty { get; } = new([], []); /// Returns all instances in document order. - public IEnumerable HostBlocks => - Blocks.OfType(); + public IEnumerable HostBlocks => Blocks.OfType(); /// Returns all instances in document order. - public IEnumerable MatchBlocks => - Blocks.OfType(); + public IEnumerable MatchBlocks => Blocks.OfType(); /// /// Returns all items at global scope, @@ -72,8 +70,5 @@ public IEnumerable GetGlobalEntries(string? key = null) /// . /// /// The target hostname to test. - public IEnumerable GetMatchingHostBlocks(string hostname) - { - return HostBlocks.Where(b => SshWildcardMatcher.Matches(hostname.AsSpan(), b.Patterns)); - } + public IEnumerable GetMatchingHostBlocks(string hostname) { return HostBlocks.Where(b => SshWildcardMatcher.Matches(hostname.AsSpan(), b.Patterns)); } } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Models/SshHostSettings.cs b/OpenSSH_GUI.SshConfig/Models/SshHostSettings.cs index 3468b08..d25fc78 100644 --- a/OpenSSH_GUI.SshConfig/Models/SshHostSettings.cs +++ b/OpenSSH_GUI.SshConfig/Models/SshHostSettings.cs @@ -26,9 +26,7 @@ public sealed record SshHostSettings( /// Initializes a new instance of the class. /// Required for the configuration binder. /// - public SshHostSettings() : this([]) - { - } + public SshHostSettings() : this([]) { } /// /// Gets an empty instance. diff --git a/OpenSSH_GUI.SshConfig/Models/SshKnownKeys.cs b/OpenSSH_GUI.SshConfig/Models/SshKnownKeys.cs index 793fd0b..659496f 100644 --- a/OpenSSH_GUI.SshConfig/Models/SshKnownKeys.cs +++ b/OpenSSH_GUI.SshConfig/Models/SshKnownKeys.cs @@ -157,7 +157,8 @@ public static class SshKnownKeys /// (i.e. later occurrences accumulate rather than override earlier ones). /// private static readonly FrozenSet MultiOccurrenceKeys = - FrozenSet.Create(StringComparer.OrdinalIgnoreCase, + FrozenSet.Create( + StringComparer.OrdinalIgnoreCase, "CertificateFile", "DynamicForward", "IdentityFile", @@ -168,7 +169,8 @@ public static class SshKnownKeys /// Keywords that accept multiple space-separated value tokens on a single directive line. /// private static readonly FrozenSet MultiTokenKeys = - FrozenSet.Create(StringComparer.OrdinalIgnoreCase, + FrozenSet.Create( + StringComparer.OrdinalIgnoreCase, "SendEnv", "SetEnv", "Host", @@ -179,35 +181,23 @@ public static class SshKnownKeys /// or returns unchanged if it is not a recognised keyword. /// /// A configuration keyword in any casing. - public static string Normalize(string key) - { - return CanonicalKeys.GetValueOrDefault(key, key); - } + public static string Normalize(string key) => CanonicalKeys.GetValueOrDefault(key, key); /// /// Returns when supports multiple occurrences /// within the same block with additive (accumulative) semantics. /// - public static bool IsMultiOccurrenceKey(string key) - { - return MultiOccurrenceKeys.Contains(key); - } + public static bool IsMultiOccurrenceKey(string key) => MultiOccurrenceKeys.Contains(key); /// /// Returns when accepts multiple /// space-separated value tokens on a single directive line. /// - public static bool IsMultiTokenKey(string key) - { - return MultiTokenKeys.Contains(key); - } + public static bool IsMultiTokenKey(string key) => MultiTokenKeys.Contains(key); /// /// Returns when is a recognised /// ssh_config(5) client keyword. /// - public static bool IsKnownKey(string key) - { - return CanonicalKeys.ContainsKey(key); - } + public static bool IsKnownKey(string key) => CanonicalKeys.ContainsKey(key); } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Models/SshLineItem.cs b/OpenSSH_GUI.SshConfig/Models/SshLineItem.cs index fc29289..d476cb0 100644 --- a/OpenSSH_GUI.SshConfig/Models/SshLineItem.cs +++ b/OpenSSH_GUI.SshConfig/Models/SshLineItem.cs @@ -36,15 +36,10 @@ private protected SshLineItem(int lineNumber, string rawText) public sealed record SshBlankLine : SshLineItem { /// 1-based source line number. - public SshBlankLine(int lineNumber) : base(lineNumber, string.Empty) - { - } + public SshBlankLine(int lineNumber) : base(lineNumber, string.Empty) { } /// Creates a blank line not associated with any source position. - public static SshBlankLine Create() - { - return new SshBlankLine(0); - } + public static SshBlankLine Create() => new(0); } /// @@ -56,10 +51,7 @@ public sealed record SshCommentLine : SshLineItem /// 1-based source line number. /// Original line text. public SshCommentLine(string comment, int lineNumber, string rawText) - : base(lineNumber, rawText) - { - Comment = comment; - } + : base(lineNumber, rawText) => Comment = comment; /// /// Gets the full comment text, including the leading # character @@ -144,8 +136,5 @@ public SshConfigEntry( /// /// Configuration keyword (case-insensitive). /// One or more value tokens. - public static SshConfigEntry Create(string key, params string[] values) - { - return new SshConfigEntry(SshKnownKeys.Normalize(key), [..values], null, 0, string.Empty); - } + public static SshConfigEntry Create(string key, params string[] values) => new(SshKnownKeys.Normalize(key), [..values], null, 0, string.Empty); } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Models/SshMatchCriterion.cs b/OpenSSH_GUI.SshConfig/Models/SshMatchCriterion.cs index bd1ec0a..e2c7a63 100644 --- a/OpenSSH_GUI.SshConfig/Models/SshMatchCriterion.cs +++ b/OpenSSH_GUI.SshConfig/Models/SshMatchCriterion.cs @@ -80,22 +80,13 @@ public sealed record SshMatchCriterion(SshMatchCriterionKind Kind, string? Patte public static SshMatchCriterion Final { get; } = new(SshMatchCriterionKind.Final, null); /// Creates a host criterion with the given pattern. - public static SshMatchCriterion ForHost(string pattern) - { - return new SshMatchCriterion(SshMatchCriterionKind.Host, pattern); - } + public static SshMatchCriterion ForHost(string pattern) => new(SshMatchCriterionKind.Host, pattern); /// Creates a user criterion with the given pattern. - public static SshMatchCriterion ForUser(string pattern) - { - return new SshMatchCriterion(SshMatchCriterionKind.User, pattern); - } + public static SshMatchCriterion ForUser(string pattern) => new(SshMatchCriterionKind.User, pattern); /// Creates an exec criterion with the given shell command. - public static SshMatchCriterion ForExec(string command) - { - return new SshMatchCriterion(SshMatchCriterionKind.Exec, command); - } + public static SshMatchCriterion ForExec(string command) => new(SshMatchCriterionKind.Exec, command); /// public override string ToString() diff --git a/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj b/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj index ed5af58..e8fd359 100644 --- a/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj +++ b/OpenSSH_GUI.SshConfig/OpenSSH_GUI.SshConfig.csproj @@ -1,15 +1,15 @@ - - - false - true - true - OpenSSH_GUI.SshConfig - - - - - - - - + + + false + true + true + OpenSSH_GUI.SshConfig + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs b/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs index 0f739db..d9af89e 100644 --- a/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs +++ b/OpenSSH_GUI.SshConfig/Options/SshConfigParserOptions.cs @@ -40,12 +40,12 @@ public sealed record SshConfigParserOptions /// Defaults to . /// public bool ThrowOnUnknownKey { get; init; } - + /// - /// Optional callback invoked when an included file cannot be read due to - /// insufficient permissions or an I/O error. Receives the file path and the - /// causing exception. When , inaccessible files are - /// silently skipped. + /// Optional callback invoked when an included file cannot be read due to + /// insufficient permissions or an I/O error. Receives the file path and the + /// causing exception. When , inaccessible files are + /// silently skipped. /// public Action? OnSkippedIncludeFile { get; init; } @@ -55,5 +55,8 @@ public sealed record SshConfigParserOptions /// /// Gets a strict options instance that throws on any unrecognised keyword. /// - public static SshConfigParserOptions Strict { get; } = new() { ThrowOnUnknownKey = true }; + public static SshConfigParserOptions Strict { get; } = new() + { + ThrowOnUnknownKey = true + }; } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Options/SshSerializerOptions.cs b/OpenSSH_GUI.SshConfig/Options/SshSerializerOptions.cs index b123bb8..34da542 100644 --- a/OpenSSH_GUI.SshConfig/Options/SshSerializerOptions.cs +++ b/OpenSSH_GUI.SshConfig/Options/SshSerializerOptions.cs @@ -56,5 +56,8 @@ public sealed record SshSerializerOptions public static SshSerializerOptions Default { get; } = new(); /// Gets a round-trip options instance that preserves original formatting verbatim. - public static SshSerializerOptions RoundTripMode { get; } = new() { RoundTrip = true }; + public static SshSerializerOptions RoundTripMode { get; } = new() + { + RoundTrip = true + }; } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs b/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs index 0b5fa79..a5ccd59 100644 --- a/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs +++ b/OpenSSH_GUI.SshConfig/Parsers/SshConfigParser.cs @@ -50,14 +50,16 @@ public static class SshConfigParser // ───────────────────────────────────────────────────────────────────────── private static readonly FrozenSet MatchKeywords = - FrozenSet.Create(StringComparer.OrdinalIgnoreCase, + FrozenSet.Create( + StringComparer.OrdinalIgnoreCase, "all", "canonical", "final", "exec", "host", "originalhost", "user", "localuser", "tagged", "localnetwork", "address", "group", "localaddress", "localport", "port", "rdomain"); private static readonly FrozenSet NoArgMatchKeywords = - FrozenSet.Create(StringComparer.OrdinalIgnoreCase, + FrozenSet.Create( + StringComparer.OrdinalIgnoreCase, "all", "canonical", "final"); // ───────────────────────────────────────────────────────────────────────── // Public API @@ -101,20 +103,15 @@ public static async Task LoadAsync( /// /// Raw configuration text. /// Parser options, or to use . - public static SshConfigDocument Parse(string content, SshConfigParserOptions? options = null) - { - return ParseDocument(content, null, options ?? SshConfigParserOptions.Default, 0); - } + public static SshConfigDocument Parse(string content, SshConfigParserOptions? options = null) => ParseDocument(content, null, options ?? SshConfigParserOptions.Default, 0); /// /// Parses SSH configuration content from a of characters. /// /// Raw configuration characters. /// Parser options, or to use . - public static SshConfigDocument Parse(ReadOnlySpan content, SshConfigParserOptions? options = null) - { - return ParseDocument(content.ToString(), null, options ?? SshConfigParserOptions.Default, 0); - } + public static SshConfigDocument Parse(ReadOnlySpan content, SshConfigParserOptions? options = null) => + ParseDocument(content.ToString(), null, options ?? SshConfigParserOptions.Default, 0); // ───────────────────────────────────────────────────────────────────────── // Core parse loop @@ -324,13 +321,14 @@ private static ImmutableArray ParseMatchCriteria( if (NoArgMatchKeywords.Contains(keyword)) { - criteria.Add(keyword.ToLowerInvariant() switch - { - "all" => SshMatchCriterion.All, - "canonical" => SshMatchCriterion.Canonical, - "final" => SshMatchCriterion.Final, - _ => throw new UnreachableException() - }); + criteria.Add( + keyword.ToLowerInvariant() switch + { + "all" => SshMatchCriterion.All, + "canonical" => SshMatchCriterion.Canonical, + "final" => SshMatchCriterion.Final, + _ => throw new UnreachableException() + }); i++; } else @@ -499,11 +497,8 @@ private sealed class BlockBuilder public string? HeaderComment { get; init; } public ImmutableArray.Builder Items { get; } = ImmutableArray.CreateBuilder(); - public SshBlock Build() - { - return IsHost - ? new SshHostBlock(HostPatterns, Items.ToImmutable(), LineNumber, RawHeaderText, HeaderComment) - : new SshMatchBlock(MatchCriteria, Items.ToImmutable(), LineNumber, RawHeaderText, HeaderComment); - } + public SshBlock Build() => IsHost + ? new SshHostBlock(HostPatterns, Items.ToImmutable(), LineNumber, RawHeaderText, HeaderComment) + : new SshMatchBlock(MatchCriteria, Items.ToImmutable(), LineNumber, RawHeaderText, HeaderComment); } } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Serializers/SshConfigSerializer.cs b/OpenSSH_GUI.SshConfig/Serializers/SshConfigSerializer.cs index 188cc4b..a0736a0 100644 --- a/OpenSSH_GUI.SshConfig/Serializers/SshConfigSerializer.cs +++ b/OpenSSH_GUI.SshConfig/Serializers/SshConfigSerializer.cs @@ -104,7 +104,7 @@ private static void WriteBlock(StringBuilder sb, SshBlock block, SshSerializerOp if (opts.RoundTrip && block.RawHeaderText.Length > 0) sb.Append(block.RawHeaderText); else - sb.Append(BuildBlockHeader(block, opts)); + sb.Append(BuildBlockHeader(block)); sb.Append(opts.NewLine); @@ -112,7 +112,7 @@ private static void WriteBlock(StringBuilder sb, SshBlock block, SshSerializerOp WriteItem(sb, item, opts.Indent, opts); } - private static string BuildBlockHeader(SshBlock block, SshSerializerOptions opts) + private static string BuildBlockHeader(SshBlock block) { var header = block switch { @@ -172,8 +172,5 @@ private static string BuildEntryLine(SshConfigEntry entry, string indent, SshSer /// Wraps in double quotes when it contains whitespace, /// preserving unquoted values that are already safe. /// - private static string QuoteIfNeeded(string value) - { - return value.AsSpan().ContainsAny(' ', '\t') ? $"\"{value}\"" : value; - } + private static string QuoteIfNeeded(string value) => value.AsSpan().ContainsAny(' ', '\t') ? $"\"{value}\"" : value; } \ No newline at end of file diff --git a/OpenSSH_GUI.SshConfig/Services/SshConfigFileService.cs b/OpenSSH_GUI.SshConfig/Services/SshConfigFileService.cs index 58c3cd8..1ab00e9 100644 --- a/OpenSSH_GUI.SshConfig/Services/SshConfigFileService.cs +++ b/OpenSSH_GUI.SshConfig/Services/SshConfigFileService.cs @@ -22,10 +22,11 @@ public static SshConfiguration LoadFromFile(string filePath) return new SshConfiguration(); var content = File.ReadAllText(filePath); - var document = SshConfigParser.Parse(content, new SshConfigParserOptions - { - IncludeBasePath = Path.GetDirectoryName(filePath) - }); + var document = SshConfigParser.Parse( + content, new SshConfigParserOptions + { + IncludeBasePath = Path.GetDirectoryName(filePath) + }); return MapDocumentToConfiguration(document); } diff --git a/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs b/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs index f2b78db..360d111 100644 --- a/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs +++ b/OpenSSH_GUI.SshConfig/Services/SshConfigurationProvider.cs @@ -15,9 +15,7 @@ public sealed class SshConfigurationProvider : FileConfigurationProvider /// Initializes a new instance of . /// /// The source settings. - public SshConfigurationProvider(SshConfigurationSource source) : base(source) - { - } + public SshConfigurationProvider(SshConfigurationSource source) : base(source) { } /// /// Loads the SSH configuration data from the stream. @@ -31,8 +29,13 @@ public override void Load(Stream stream) // Use the existing parser to parse the content. // We use the file path from the source if available for better error messages. var filePath = Source.Path; - var document = SshConfigParser.Parse(content, - new SshConfigParserOptions { IncludeBasePath = filePath is null ? null : Path.GetDirectoryName(filePath), OnSkippedIncludeFile = Source is SshConfigurationSource source ? source.OnSkippedIncludeFile : null }); + var document = SshConfigParser.Parse( + content, + new SshConfigParserOptions + { + IncludeBasePath = filePath is null ? null : Path.GetDirectoryName(filePath), + OnSkippedIncludeFile = Source is SshConfigurationSource source ? source.OnSkippedIncludeFile : null + }); var data = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs b/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs index 78a92b2..5f62349 100644 --- a/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs +++ b/OpenSSH_GUI.SshConfig/Services/SshConfigurationSource.cs @@ -8,13 +8,13 @@ namespace OpenSSH_GUI.SshConfig.Services; public sealed class SshConfigurationSource : FileConfigurationSource { /// - /// Optional callback invoked when an included file cannot be read due to - /// insufficient permissions or an I/O error. Receives the file path and the - /// causing exception. When , inaccessible files are - /// silently skipped. + /// Optional callback invoked when an included file cannot be read due to + /// insufficient permissions or an I/O error. Receives the file path and the + /// causing exception. When , inaccessible files are + /// silently skipped. /// public Action? OnSkippedIncludeFile { get; init; } - + /// /// Builds the for this source. /// diff --git a/OpenSSH_GUI.Tests/Core/Extensions/ServiceCollectionExtensionsTests.cs b/OpenSSH_GUI.Tests/Core/Extensions/ServiceCollectionExtensionsTests.cs index 8a01f05..9afac9c 100644 --- a/OpenSSH_GUI.Tests/Core/Extensions/ServiceCollectionExtensionsTests.cs +++ b/OpenSSH_GUI.Tests/Core/Extensions/ServiceCollectionExtensionsTests.cs @@ -1,9 +1,6 @@ using Avalonia.Controls; using Avalonia.Threading; -using DryIoc; -using DryIoc.Microsoft.DependencyInjection; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using OpenSSH_GUI.Core.Extensions; using OpenSSH_GUI.Core.MVVM; using Xunit; @@ -16,17 +13,18 @@ public class DependencyInjectionExtensionsTests public void RegisterViewWithViewModel_ValidNaming_ShouldRegister() { // Arrange - var services = new Container(); + var serviceCollection = new ServiceCollection(); // Act - services.RegisterViewWithViewModel(); - var provider = services.BuildServiceProvider(); + serviceCollection.RegisterViewWithViewModel(); + + var services = serviceCollection.BuildServiceProvider(); // Assert Dispatcher.UIThread.Invoke(() => { - Assert.NotNull(provider.GetKeyedService("MockWindow")); - Assert.NotNull(provider.GetKeyedService("MockWindowViewModel")); + Assert.NotNull(services.GetKeyedService(nameof(MockWindow))); + Assert.NotNull(services.GetKeyedService(nameof(MockWindowViewModel))); }); } @@ -34,22 +32,15 @@ public void RegisterViewWithViewModel_ValidNaming_ShouldRegister() public void RegisterViewWithViewModel_InvalidNaming_ShouldThrow() { // Arrange - var services = new Container(); + var services = new ServiceCollection(); // Act & Assert - Assert.Throws(() => - services.RegisterViewWithViewModel()); + Assert.Throws(() => services.RegisterViewWithViewModel()); } - private class MockWindow : Window - { - } + private class MockWindow : Window; - private class MockWindowViewModel() : ViewModelBase(NullLogger.Instance) - { - } + private class MockWindowViewModel : ViewModelBase; - private class InvalidVm() : ViewModelBase(NullLogger.Instance) - { - } + private class InvalidVm : ViewModelBase; } \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/Core/Extensions/SshConfigFilesExtensionTests.cs b/OpenSSH_GUI.Tests/Core/Extensions/SshConfigFilesExtensionTests.cs index 6aea5b5..f0d8f8b 100644 --- a/OpenSSH_GUI.Tests/Core/Extensions/SshConfigFilesExtensionTests.cs +++ b/OpenSSH_GUI.Tests/Core/Extensions/SshConfigFilesExtensionTests.cs @@ -7,28 +7,13 @@ namespace OpenSSH_GUI.Tests.Core.Extensions; public class SshConfigFilesExtensionTests { - [Theory] - [InlineData(PlatformID.Win32NT, false, "%PROGRAMDATA%\\ssh")] - [InlineData(PlatformID.Unix, false, "/etc/ssh")] - public void GetRootSshPath_Tests(PlatformID platform, bool resolve, string expected) - { - SshConfigFilesExtension.GetRootSshPath(resolve, platform).ShouldBe(expected); - } + [Theory, InlineData(PlatformID.Win32NT, false, "%PROGRAMDATA%\\ssh"), InlineData(PlatformID.Unix, false, "/etc/ssh")] + public void GetRootSshPath_Tests(PlatformID platform, bool resolve, string expected) { SshConfigFilesExtension.GetRootSshPath(resolve, platform).ShouldBe(expected); } - [Theory] - [InlineData(PlatformID.Win32NT, false, "%USERPROFILE%\\.ssh")] - [InlineData(PlatformID.Unix, false, "%HOME%/.ssh")] - public void GetBaseSshPath_Tests(PlatformID platform, bool resolve, string expected) - { - SshConfigFilesExtension.GetBaseSshPath(resolve, platform).ShouldBe(expected); - } + [Theory, InlineData(PlatformID.Win32NT, false, "%USERPROFILE%\\.ssh"), InlineData(PlatformID.Unix, false, "%HOME%/.ssh")] + public void GetBaseSshPath_Tests(PlatformID platform, bool resolve, string expected) { SshConfigFilesExtension.GetBaseSshPath(resolve, platform).ShouldBe(expected); } - [Theory] - [InlineData(SshConfigFiles.Config, PlatformID.Win32NT, false, "%USERPROFILE%\\.ssh\\config")] - [InlineData(SshConfigFiles.Config, PlatformID.Unix, false, "%HOME%/.ssh/config")] - [InlineData(SshConfigFiles.Sshd_Config, PlatformID.Unix, false, "/etc/ssh/sshd_config")] - public void GetPathOfFile_Tests(SshConfigFiles file, PlatformID platform, bool resolve, string expected) - { - file.GetPathOfFile(resolve, platform).ShouldBe(expected); - } + [Theory, InlineData(SshConfigFiles.Config, PlatformID.Win32NT, false, "%USERPROFILE%\\.ssh\\config"), + InlineData(SshConfigFiles.Config, PlatformID.Unix, false, "%HOME%/.ssh/config"), InlineData(SshConfigFiles.Sshd_Config, PlatformID.Unix, false, "/etc/ssh/sshd_config")] + public void GetPathOfFile_Tests(SshConfigFiles file, PlatformID platform, bool resolve, string expected) { file.GetPathOfFile(resolve, platform).ShouldBe(expected); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs b/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs index 3f9986c..1ca2bcb 100644 --- a/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs +++ b/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs @@ -7,21 +7,10 @@ namespace OpenSSH_GUI.Tests.Core.Extensions; public class SshKeyFormatExtensionTests { - [Theory] - [InlineData(SshKeyFormat.OpenSSH, true, ".pub")] - [InlineData(SshKeyFormat.OpenSSH, false, null)] - [InlineData(SshKeyFormat.PuTTYv2, false, ".ppk")] - [InlineData(SshKeyFormat.PuTTYv3, true, ".ppk")] - public void GetExtension_Tests(SshKeyFormat format, bool isPublic, string? expected) - { - format.GetExtension(isPublic).ShouldBe(expected); - } + [Theory, InlineData(SshKeyFormat.OpenSSH, true, ".pub"), InlineData(SshKeyFormat.OpenSSH, false, null), InlineData(SshKeyFormat.PuTTYv2, false, ".ppk"), + InlineData(SshKeyFormat.PuTTYv3, true, ".ppk")] + public void GetExtension_Tests(SshKeyFormat format, bool isPublic, string? expected) { format.GetExtension(isPublic).ShouldBe(expected); } - [Theory] - [InlineData(SshKeyFormat.OpenSSH, "test.key", true, "test.pub")] - [InlineData(SshKeyFormat.PuTTYv3, "test.key", false, "test.ppk")] - public void ChangeExtension_Tests(SshKeyFormat format, string path, bool isPublic, string expected) - { - format.ChangeExtension(path, isPublic).ShouldBe(expected); - } + [Theory, InlineData(SshKeyFormat.OpenSSH, "test.key", true, "test.pub"), InlineData(SshKeyFormat.PuTTYv3, "test.key", false, "test.ppk")] + public void ChangeExtension_Tests(SshKeyFormat format, string path, bool isPublic, string expected) { format.ChangeExtension(path, isPublic).ShouldBe(expected); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/Core/Extensions/SshKeyTypeExtensionTests.cs b/OpenSSH_GUI.Tests/Core/Extensions/SshKeyTypeExtensionTests.cs index c154ecf..2e762d7 100644 --- a/OpenSSH_GUI.Tests/Core/Extensions/SshKeyTypeExtensionTests.cs +++ b/OpenSSH_GUI.Tests/Core/Extensions/SshKeyTypeExtensionTests.cs @@ -6,10 +6,7 @@ namespace OpenSSH_GUI.Tests.Core.Extensions; public class SshKeyTypeExtensionTests { - [Theory] - [InlineData(SshKeyType.RSA)] - [InlineData(SshKeyType.ECDSA)] - [InlineData(SshKeyType.ED25519)] + [Theory, InlineData(SshKeyType.RSA), InlineData(SshKeyType.ECDSA), InlineData(SshKeyType.ED25519)] public static void SshKeyType_Tests(SshKeyType sshKeyType) { var bitValues = sshKeyType.SupportedKeySizes; diff --git a/OpenSSH_GUI.Tests/Core/Extensions/StringExtensionsTests.cs b/OpenSSH_GUI.Tests/Core/Extensions/StringExtensionsTests.cs index 31e6834..a99e794 100644 --- a/OpenSSH_GUI.Tests/Core/Extensions/StringExtensionsTests.cs +++ b/OpenSSH_GUI.Tests/Core/Extensions/StringExtensionsTests.cs @@ -6,33 +6,17 @@ namespace OpenSSH_GUI.Tests.Core.Extensions; public class StringExtensionsTests { - [Theory] - [InlineData("HelloWorld", "hello_world")] - public void ToSnakeCase_Tests(string input, string expected) - { - input.ToSnakeCase().ShouldBe(expected); - } + [Theory, InlineData("HelloWorld", "hello_world")] + public void ToSnakeCase_Tests(string input, string expected) { input.ToSnakeCase().ShouldBe(expected); } - [Theory] - [InlineData("HelloWorld", "helloWorld")] - public void ToCamelCase_Tests(string input, string expected) - { - input.ToCamelCase().ShouldBe(expected); - } + [Theory, InlineData("HelloWorld", "helloWorld")] + public void ToCamelCase_Tests(string input, string expected) { input.ToCamelCase().ShouldBe(expected); } - [Theory] - [InlineData("HelloWorld", "hello-world")] - public void ToKebabCase_Tests(string input, string expected) - { - input.ToKebabCase().ShouldBe(expected); - } + [Theory, InlineData("HelloWorld", "hello-world")] + public void ToKebabCase_Tests(string input, string expected) { input.ToKebabCase().ShouldBe(expected); } - [Theory] - [InlineData("hello world", "HelloWorld")] - public void ToPascalCase_Tests(string input, string expected) - { - input.ToPascalCase().ShouldBe(expected); - } + [Theory, InlineData("hello world", "HelloWorld")] + public void ToPascalCase_Tests(string input, string expected) { input.ToPascalCase().ShouldBe(expected); } [Fact] public void SplitToChunks_Tests() @@ -49,22 +33,13 @@ public void Wrap_Tests() } [Fact] - public void ToTitleCase_Tests() - { - "this is a title".ToTitleCase().ShouldBe("This Is A Title"); - } + public void ToTitleCase_Tests() { "this is a title".ToTitleCase().ShouldBe("This Is A Title"); } [Fact] - public void ToSentenceCase_Tests() - { - "THIS IS A SENTENCE.".ToSentenceCase().ShouldBe("This is a sentence."); - } + public void ToSentenceCase_Tests() { "THIS IS A SENTENCE.".ToSentenceCase().ShouldBe("This is a sentence."); } [Fact] - public void ToLeetSpeak_Tests() - { - "leetspeak".ToLeetSpeak().ShouldBe("l33t5p34k"); - } + public void ToLeetSpeak_Tests() { "leetspeak".ToLeetSpeak().ShouldBe("l33t5p34k"); } [Fact] public void ToStudlyCaps_Tests() diff --git a/OpenSSH_GUI.Tests/Core/MVVM/ViewModelBaseTests.cs b/OpenSSH_GUI.Tests/Core/MVVM/ViewModelBaseTests.cs index ff08f95..273217a 100644 --- a/OpenSSH_GUI.Tests/Core/MVVM/ViewModelBaseTests.cs +++ b/OpenSSH_GUI.Tests/Core/MVVM/ViewModelBaseTests.cs @@ -1,5 +1,4 @@ using System.Reactive.Linq; -using Microsoft.Extensions.Logging.Abstractions; using OpenSSH_GUI.Core.MVVM; using Xunit; @@ -14,7 +13,7 @@ public async Task InitializeAsync_ShouldSetIsInitialized() var vm = new TestViewModel(); // Act - await vm.InitializeAsync(cancellationToken: TestContext.Current.CancellationToken); + await vm.InitializeAsync(TestContext.Current.CancellationToken); // Assert Assert.True(vm.IsInitialized); @@ -27,7 +26,7 @@ public async Task BooleanSubmit_ShouldCallOnBooleanSubmitAsync() var vm = new TestViewModel(); // Act - await vm.BooleanSubmit.Execute(true).FirstAsync(); + await vm.BooleanSubmitCommand.Execute(true).FirstAsync(); // Assert Assert.True(vm.OnBooleanSubmitCalled); @@ -41,7 +40,7 @@ public void RequestClose_ShouldInvokeCloseEvent() // Arrange var vm = new TestViewModel(); var closeInvoked = false; - vm.Close += (s, e) => closeInvoked = true; + vm.Close += (_, _) => closeInvoked = true; // Act vm.TriggerClose(); @@ -51,21 +50,18 @@ public void RequestClose_ShouldInvokeCloseEvent() Assert.False(vm.IsInitialized); } - private class TestViewModel() : ViewModelBase(NullLogger.Instance) + private class TestViewModel : ViewModelBase { public bool OnBooleanSubmitCalled { get; private set; } public bool InputParam { get; private set; } - protected override Task OnBooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) + protected override Task BooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) { OnBooleanSubmitCalled = true; InputParam = inputParameter; return Task.CompletedTask; } - public void TriggerClose() - { - RequestClose(); - } + public void TriggerClose() { RequestClose(); } } } \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/OpenSSH_GUI.Tests.csproj b/OpenSSH_GUI.Tests/OpenSSH_GUI.Tests.csproj index 85c4d73..746a06f 100644 --- a/OpenSSH_GUI.Tests/OpenSSH_GUI.Tests.csproj +++ b/OpenSSH_GUI.Tests/OpenSSH_GUI.Tests.csproj @@ -1,38 +1,34 @@ - - false - true - Exe - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - + + false + true + Exe + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/ReactiveUiInitFixture.cs b/OpenSSH_GUI.Tests/ReactiveUiInitFixture.cs index cee200e..87cc1d2 100644 --- a/OpenSSH_GUI.Tests/ReactiveUiInitFixture.cs +++ b/OpenSSH_GUI.Tests/ReactiveUiInitFixture.cs @@ -6,20 +6,20 @@ namespace OpenSSH_GUI.Tests; /// -/// Assembly-wide fixture that initializes ReactiveUI core services -/// before any test runs. Required because -/// and related types throw if ReactiveUI has not been bootstrapped. +/// Assembly-wide fixture that initializes ReactiveUI core services +/// before any test runs. Required because +/// and related types throw if ReactiveUI has not been bootstrapped. /// /// -/// Assembly-wide fixture that runs a dedicated Avalonia UI thread with a -/// live dispatcher loop. Required because -/// enforces UI-thread access, and deadlocks -/// without a running message loop. +/// Assembly-wide fixture that runs a dedicated Avalonia UI thread with a +/// live dispatcher loop. Required because +/// enforces UI-thread access, deadlocks +/// without a running message loop. /// public sealed class ReactiveUiInitFixture : IDisposable { - private readonly CancellationTokenSource cts = new(); - private readonly ManualResetEventSlim initialized = new(); + private readonly CancellationTokenSource _cts = new(); + private readonly ManualResetEventSlim _initialized = new(); public ReactiveUiInitFixture() { @@ -33,17 +33,17 @@ public ReactiveUiInitFixture() .WithCoreServices() .BuildApp(); - initialized.Set(); + _initialized.Set(); - Dispatcher.UIThread.MainLoop(cts.Token); + Dispatcher.UIThread.MainLoop(_cts.Token); }); uiThread.IsBackground = true; uiThread.Start(); - initialized.Wait(); + _initialized.Wait(); } /// - public void Dispose() => cts.Cancel(); + public void Dispose() { _cts.Cancel(); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/SshConfig/SshConfigParserTests.cs b/OpenSSH_GUI.Tests/SshConfig/SshConfigParserTests.cs index 3a5f516..99a716f 100644 --- a/OpenSSH_GUI.Tests/SshConfig/SshConfigParserTests.cs +++ b/OpenSSH_GUI.Tests/SshConfig/SshConfigParserTests.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.FileProviders; -using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; -using OpenSSH_GUI.Core.Lib.Credentials; +using OpenSSH_GUI.Core.Lib.Misc; using OpenSSH_GUI.SshConfig.Exceptions; using OpenSSH_GUI.SshConfig.Extensions; using OpenSSH_GUI.SshConfig.Models; @@ -15,11 +14,8 @@ namespace OpenSSH_GUI.Tests.SshConfig; public class SshConfigParserTests { - private IFileProvider GetEmbeddedFileProvider() - { - return new EmbeddedFileProvider(typeof(SshConfigParserTests).Assembly, "OpenSSH_GUI.Tests.Assets.Testfiles"); - } - + private IFileProvider GetEmbeddedFileProvider() => new EmbeddedFileProvider(typeof(SshConfigParserTests).Assembly, "OpenSSH_GUI.Tests.Assets.Testfiles"); + private string GetEmbeddedResource(string fileName) { var assembly = typeof(SshConfigParserTests).Assembly; @@ -39,12 +35,10 @@ public void Parse_GlobalConfig_ShouldParseEmbeddedFile() var doc = SshConfigParser.Parse(content); doc.Blocks.Length.ShouldBeGreaterThan(0); - // "Host *" sollte vorhanden sein var allHosts = doc.Blocks.OfType().ToList(); allHosts.ShouldContain(b => b.Patterns.Contains("*")); - // Suche nach "ConnectTimeout 20" im globalen Kontext oder im Host * Block - var globalEntries = doc.GetGlobalEntries().ToArray(); + _ = doc.GetGlobalEntries().ToArray(); var hostStar = allHosts.FirstOrDefault(b => b.Patterns.Contains("*")); hostStar.ShouldNotBeNull(); hostStar.GetEntries().ShouldContain(e => e.Key == "ConnectTimeout" && e.Value == "20"); @@ -60,10 +54,10 @@ public void Parse_PersonalConfig_Into_Config_DependencyInjection() var ss = configurationRoot.GetSection("SshConfig").Get(); Assert.NotNull(ss); - + var ifsCount = ss.Hosts.Where(host => host.IdentityFiles is not null).Sum(host => host.IdentityFiles?.Length); ifsCount.ShouldNotBe(null); - if(ifsCount is { } count) + if (ifsCount is { } count) count.ShouldBeGreaterThan(0); } @@ -105,7 +99,7 @@ public void Parse_SshdServerConfig_ShouldParseEmbeddedFile() [Fact] public void Parse_EmptyContent_ShouldReturnEmptyDocument() { - var doc = SshConfigParser.Parse(""); + var doc = SshConfigParser.Parse(string.Empty); doc.GlobalItems.ShouldAllBe(i => i is SshBlankLine); doc.Blocks.ShouldBeEmpty(); } @@ -243,7 +237,10 @@ public void Parse_IncludeRecursion_ShouldThrow() // Arrange var content = "Include recursive.conf"; var options = new SshConfigParserOptions - { MaxIncludeDepth = 1, IncludeBasePath = Directory.GetCurrentDirectory() }; + { + MaxIncludeDepth = 1, + IncludeBasePath = Directory.GetCurrentDirectory() + }; var recursiveFile = Path.Combine(Directory.GetCurrentDirectory(), "recursive.conf"); File.WriteAllText(recursiveFile, "Include recursive.conf"); @@ -283,6 +280,7 @@ public void Parse_InvalidPort_ShouldBeHandledInSettings() // Assert Assert.Null(settings.Port); // Note: In SshHostBlockExtensions.GetSettings, unparseable "Port" is added to otherEntries + Assert.NotNull(settings.OtherEntries); Assert.Single(settings.OtherEntries); Assert.Equal("Port", settings.OtherEntries[0].Key); } @@ -301,6 +299,7 @@ public void Parse_QuotedValues_ShouldStripDoubleQuotes() // Assert Assert.Equal("quoted server", block.Patterns[0]); Assert.Equal("alice", settings.User); + Assert.NotNull(settings.IdentityFiles); Assert.Contains("~/.ssh/id rsa", settings.IdentityFiles); } diff --git a/OpenSSH_GUI.Tests/SshConfig/SshConfigSerializerTests.cs b/OpenSSH_GUI.Tests/SshConfig/SshConfigSerializerTests.cs index 5bcaaec..2aa1a0d 100644 --- a/OpenSSH_GUI.Tests/SshConfig/SshConfigSerializerTests.cs +++ b/OpenSSH_GUI.Tests/SshConfig/SshConfigSerializerTests.cs @@ -17,7 +17,11 @@ public void Serialize_SimpleDocument_ShouldProduceCorrectOutput() [SshHostBlock.Create(["example"], [SshConfigEntry.Create("User", "alice")])] ); - var output = SshConfigSerializer.Serialize(doc, new SshSerializerOptions { Indent = " " }); + var output = SshConfigSerializer.Serialize( + doc, new SshSerializerOptions + { + Indent = " " + }); output.ShouldContain("VisualHostKey yes"); output.ShouldContain("Host example"); @@ -38,7 +42,10 @@ public void Serialize_RoundTrip_ShouldPreserveFormatting() [Fact] public void Serialize_MatchBlock_ShouldProduceCorrectOutput() { - var criteria = new[] { SshMatchCriterion.ForHost("example.com"), SshMatchCriterion.ForUser("root") }; + var criteria = new[] + { + SshMatchCriterion.ForHost("example.com"), SshMatchCriterion.ForUser("root") + }; var doc = new SshConfigDocument( [], [SshMatchBlock.Create(criteria, [SshConfigEntry.Create("Port", "22")])] @@ -81,11 +88,25 @@ public void Serialize_RoundTrip_WithModifications_ShouldRegenerate() var entry = block.GetEntries("User").First(); // Modify entry and clear RawText to force regeneration - var modifiedEntry = entry with { Values = ["bob"], RawText = string.Empty }; - var modifiedBlock = block with { Items = [modifiedEntry], RawHeaderText = string.Empty }; - var modifiedDoc = doc with { Blocks = [modifiedBlock] }; + var modifiedEntry = entry with + { + Values = ["bob"], + RawText = string.Empty + }; + var modifiedBlock = block with + { + Items = [modifiedEntry], + RawHeaderText = string.Empty + }; + var modifiedDoc = doc with + { + Blocks = [modifiedBlock] + }; - var options = new SshSerializerOptions { RoundTrip = true }; + var options = new SshSerializerOptions + { + RoundTrip = true + }; // Act var output = SshConfigSerializer.Serialize(modifiedDoc, options); diff --git a/OpenSSH_GUI.Tests/SshConfig/SshConfigurationBindingTests.cs b/OpenSSH_GUI.Tests/SshConfig/SshConfigurationBindingTests.cs index 33c4f3c..2c22f27 100644 --- a/OpenSSH_GUI.Tests/SshConfig/SshConfigurationBindingTests.cs +++ b/OpenSSH_GUI.Tests/SshConfig/SshConfigurationBindingTests.cs @@ -9,10 +9,7 @@ namespace OpenSSH_GUI.Tests.SshConfig; public class SshConfigurationBindingTests { - private static IFileProvider GetEmbeddedFileProvider() - { - return new EmbeddedFileProvider(typeof(SshConfigParserTests).Assembly, "OpenSSH_GUI.Tests.Assets.Testfiles"); - } + private static IFileProvider GetEmbeddedFileProvider() => new EmbeddedFileProvider(typeof(SshConfigParserTests).Assembly, "OpenSSH_GUI.Tests.Assets.Testfiles"); [Fact] public void AddSshConfig_ShouldBeBindableToObjects() @@ -40,10 +37,8 @@ public void AddSshConfig_ShouldBeBindableToObjects() path = path.Length == 1 ? home : Path.Combine(home, path[2..]); return Path.GetFullPath(path); })) - { if (!possibleKeyFiles.Any(e => e.Equals(hostIdentityFile, StringComparison.OrdinalIgnoreCase))) possibleKeyFiles.Add(hostIdentityFile); - } } possibleKeyFiles.ShouldNotBeEmpty(); diff --git a/OpenSSH_GUI.Tests/SshConfig/SshHostSettingsTests.cs b/OpenSSH_GUI.Tests/SshConfig/SshHostSettingsTests.cs index d46e911..79f946a 100644 --- a/OpenSSH_GUI.Tests/SshConfig/SshHostSettingsTests.cs +++ b/OpenSSH_GUI.Tests/SshConfig/SshHostSettingsTests.cs @@ -62,8 +62,10 @@ public void WithSettings_ShouldUpdateBlock() Assert.Equal("new.example.com", reserializedSettings.HostName); Assert.Equal("bob", reserializedSettings.User); Assert.Equal(22, reserializedSettings.Port); + Assert.NotNull(reserializedSettings.IdentityFiles); Assert.Single(reserializedSettings.IdentityFiles); Assert.Equal("~/.ssh/id_new", reserializedSettings.IdentityFiles[0]); + Assert.NotNull(reserializedSettings.LocalForwards); Assert.Single(reserializedSettings.LocalForwards); Assert.Equal("9000 localhost:90", reserializedSettings.LocalForwards[0]); } diff --git a/OpenSSH_GUI.Tests/SshConfig/SshKnownKeysTests.cs b/OpenSSH_GUI.Tests/SshConfig/SshKnownKeysTests.cs index 9baab90..86fb65f 100644 --- a/OpenSSH_GUI.Tests/SshConfig/SshKnownKeysTests.cs +++ b/OpenSSH_GUI.Tests/SshConfig/SshKnownKeysTests.cs @@ -6,37 +6,15 @@ namespace OpenSSH_GUI.Tests.SshConfig; public class SshKnownKeysTests { - [Theory] - [InlineData("hostname", "HostName")] - [InlineData("USER", "User")] - [InlineData("identityfile", "IdentityFile")] - [InlineData("UNKNOWN", "UNKNOWN")] - public void Normalize_ShouldCanonicalizeCasing(string input, string expected) - { - SshKnownKeys.Normalize(input).ShouldBe(expected); - } + [Theory, InlineData("hostname", "HostName"), InlineData("USER", "User"), InlineData("identityfile", "IdentityFile"), InlineData("UNKNOWN", "UNKNOWN")] + public void Normalize_ShouldCanonicalizeCasing(string input, string expected) { SshKnownKeys.Normalize(input).ShouldBe(expected); } - [Theory] - [InlineData("IdentityFile", true)] - [InlineData("HostName", false)] - public void IsMultiOccurrenceKey_Tests(string key, bool expected) - { - SshKnownKeys.IsMultiOccurrenceKey(key).ShouldBe(expected); - } + [Theory, InlineData("IdentityFile", true), InlineData("HostName", false)] + public void IsMultiOccurrenceKey_Tests(string key, bool expected) { SshKnownKeys.IsMultiOccurrenceKey(key).ShouldBe(expected); } - [Theory] - [InlineData("SendEnv", true)] - [InlineData("HostName", false)] - public void IsMultiTokenKey_Tests(string key, bool expected) - { - SshKnownKeys.IsMultiTokenKey(key).ShouldBe(expected); - } + [Theory, InlineData("SendEnv", true), InlineData("HostName", false)] + public void IsMultiTokenKey_Tests(string key, bool expected) { SshKnownKeys.IsMultiTokenKey(key).ShouldBe(expected); } - [Theory] - [InlineData("HostName", true)] - [InlineData("SomethingRandom", false)] - public void IsKnownKey_Tests(string key, bool expected) - { - SshKnownKeys.IsKnownKey(key).ShouldBe(expected); - } + [Theory, InlineData("HostName", true), InlineData("SomethingRandom", false)] + public void IsKnownKey_Tests(string key, bool expected) { SshKnownKeys.IsKnownKey(key).ShouldBe(expected); } } \ No newline at end of file diff --git a/OpenSSH_GUI.Tests/SshConfig/SshWildcardMatcherTests.cs b/OpenSSH_GUI.Tests/SshConfig/SshWildcardMatcherTests.cs index 3d6df75..b52c5e0 100644 --- a/OpenSSH_GUI.Tests/SshConfig/SshWildcardMatcherTests.cs +++ b/OpenSSH_GUI.Tests/SshConfig/SshWildcardMatcherTests.cs @@ -6,41 +6,45 @@ namespace OpenSSH_GUI.Tests.SshConfig; public class SshWildcardMatcherTests { - [Theory] - [InlineData("example.com", "example.com", true)] - [InlineData("example.com", "*.com", true)] - [InlineData("example.com", "example.*", true)] - [InlineData("example.com", "*example*", true)] - [InlineData("example.com", "ex?mple.com", true)] - [InlineData("example.com", "other.com", false)] - [InlineData("abc", "a?c", true)] - [InlineData("abc", "a*", true)] - [InlineData("abc", "*c", true)] - [InlineData("abc", "*", true)] - [InlineData("abc", "abcd", false)] - [InlineData("abc", "ab", false)] - [InlineData("", "*", true)] - [InlineData("a", "", false)] - [InlineData("", "", true)] - [InlineData("abc", "***", true)] - [InlineData("abc", "*b*", true)] - [InlineData("abc", "a**c", true)] - public void MatchesGlob_Tests(string input, string pattern, bool expected) - { - SshWildcardMatcher.MatchesGlob(input.AsSpan(), pattern.AsSpan()).ShouldBe(expected); - } + [Theory, InlineData("example.com", "example.com", true), InlineData("example.com", "*.com", true), InlineData("example.com", "example.*", true), + InlineData("example.com", "*example*", true), InlineData("example.com", "ex?mple.com", true), InlineData("example.com", "other.com", false), InlineData("abc", "a?c", true), + InlineData("abc", "a*", true), InlineData("abc", "*c", true), InlineData("abc", "*", true), InlineData("abc", "abcd", false), InlineData("abc", "ab", false), + InlineData("", "*", true), InlineData("a", "", false), InlineData("", "", true), InlineData("abc", "***", true), InlineData("abc", "*b*", true), InlineData("abc", "a**c", true)] + public void MatchesGlob_Tests(string input, string pattern, bool expected) { SshWildcardMatcher.MatchesGlob(input.AsSpan(), pattern.AsSpan()).ShouldBe(expected); } - [Theory] - [InlineData("host1", new[] { "host1", "host2" }, true)] - [InlineData("host2", new[] { "host1", "host2" }, true)] - [InlineData("host3", new[] { "host1", "host2" }, false)] - [InlineData("host1", new[] { "!host1", "host*" }, false)] - [InlineData("host2", new[] { "!host1", "host*" }, true)] - [InlineData("host1", new[] { "host*", "!host1" }, false)] - [InlineData("host1", new string[] { }, false)] - [InlineData("host1", new[] { "" }, false)] - public void Matches_Tests(string hostname, string[] patterns, bool expected) - { - SshWildcardMatcher.Matches(hostname.AsSpan(), patterns).ShouldBe(expected); - } + [Theory, InlineData( + "host1", new[] + { + "host1", "host2" + }, true), InlineData( + "host2", new[] + { + "host1", "host2" + }, true), InlineData( + "host3", new[] + { + "host1", "host2" + }, false), + InlineData( + "host1", new[] + { + "!host1", "host*" + }, false), InlineData( + "host2", new[] + { + "!host1", "host*" + }, true), InlineData( + "host1", new[] + { + "host*", "!host1" + }, false), + InlineData( + "host1", new string[] + { + }, false), InlineData( + "host1", new[] + { + "" + }, false)] + public void Matches_Tests(string hostname, string[] patterns, bool expected) { SshWildcardMatcher.Matches(hostname.AsSpan(), patterns).ShouldBe(expected); } } \ No newline at end of file diff --git a/OpenSSH_GUI.sln.DotSettings b/OpenSSH_GUI.sln.DotSettings new file mode 100644 index 0000000..e5e3410 --- /dev/null +++ b/OpenSSH_GUI.sln.DotSettings @@ -0,0 +1,115 @@ + + HINT + SUGGESTION + <?xml version="1.0" encoding="utf-16"?><Profile name="Full Cleanup Custom"><CppReformatCode>True</CppReformatCode><FSharpReformatCode>True</FSharpReformatCode><ShaderLabReformatCode>True</ShaderLabReformatCode><XMLReformatCode>True</XMLReformatCode><VBReformatCode>True</VBReformatCode><CSReformatCode>True</CSReformatCode><CSharpReformatComments>True</CSharpReformatComments><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" ArrangeAccessors="True" ArrangeArgumentsStyle="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeEmptyString="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" /><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CppCodeStyleCleanupDescriptor ArrangeBraces="True" ArrangeAuto="True" ArrangeFunctionDeclarations="True" ArrangeNestedNamespaces="True" ArrangeTypeAliases="True" ArrangeCVQualifiers="True" ArrangeSlashesInIncludeDirectives="True" ArrangeOverridingFunctions="True" SortDefinitions="True" SortIncludeDirectives="True" SortMemberInitializers="True" /><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CSReformatInactiveBranches>True</CSReformatInactiveBranches><CSharpFormatDocComments>True</CSharpFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSReorderTypeMembers>True</CSReorderTypeMembers><CSShortenReferences>True</CSShortenReferences><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><Xaml.RemoveRedundantNamespaceAlias>True</Xaml.RemoveRedundantNamespaceAlias><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="Full Cleanup Custom" /&gt; + &lt;inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="JSRemoveUnnecessaryParentheses" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="UnterminatedStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /&gt; + &lt;inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="true" level="WARNING" enabled_by_default="true" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id=""&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="CMake"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="CSS"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Razor"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="VueExpr"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CppAddTypenameTemplateKeywords>True</CppAddTypenameTemplateKeywords><CppCStyleToStaticCastDescriptor>True</CppCStyleToStaticCastDescriptor><CppRedundantDereferences>True</CppRedundantDereferences><CppDeleteRedundantAccessSpecifier>True</CppDeleteRedundantAccessSpecifier><CppRemoveCastDescriptor>True</CppRemoveCastDescriptor><CppRemoveElseKeyword>True</CppRemoveElseKeyword><CppShortenQualifiedName>True</CppShortenQualifiedName><CppDeleteRedundantSpecifier>True</CppDeleteRedundantSpecifier><CppRemoveStatement>True</CppRemoveStatement><CppDeleteRedundantTypenameTemplateKeywords>True</CppDeleteRedundantTypenameTemplateKeywords><CppReplaceExpressionWithBooleanConst>True</CppReplaceExpressionWithBooleanConst><CppMakeIfConstexpr>True</CppMakeIfConstexpr><CppMakePostfixOperatorPrefix>True</CppMakePostfixOperatorPrefix><CppMakeVariableConstexpr>True</CppMakeVariableConstexpr><CppChangeSmartPointerToMakeFunction>True</CppChangeSmartPointerToMakeFunction><CppReplaceThrowWithRethrowFix>True</CppReplaceThrowWithRethrowFix><CppTypeTraitAliasDescriptor>True</CppTypeTraitAliasDescriptor><CppRemoveRedundantConditionalExpressionDescriptor>True</CppRemoveRedundantConditionalExpressionDescriptor><CppSimplifyConditionalExpressionDescriptor>True</CppSimplifyConditionalExpressionDescriptor><CppReplaceExpressionWithNullptr>True</CppReplaceExpressionWithNullptr><CppReplaceTieWithStructuredBindingDescriptor>True</CppReplaceTieWithStructuredBindingDescriptor><CppUseAssociativeContainsDescriptor>True</CppUseAssociativeContainsDescriptor><CppUseEraseAlgorithmDescriptor>True</CppUseEraseAlgorithmDescriptor><CppJoinDeclarationAndAssignmentDescriptor>True</CppJoinDeclarationAndAssignmentDescriptor><CppMakeClassFinal>True</CppMakeClassFinal><CppMakeLocalVarConstDescriptor>True</CppMakeLocalVarConstDescriptor><CppMakeMethodConst>True</CppMakeMethodConst><CppMakeMethodStatic>True</CppMakeMethodStatic><CppMakePtrOrRefParameterConst>True</CppMakePtrOrRefParameterConst><CppMakeParameterConst>True</CppMakeParameterConst><CppPassValueParameterByConstReference>True</CppPassValueParameterByConstReference><CppRemoveElaboratedTypeSpecifierDescriptor>True</CppRemoveElaboratedTypeSpecifierDescriptor><CppRemoveRedundantLambdaParameterListDescriptor>True</CppRemoveRedundantLambdaParameterListDescriptor><CppRemoveRedundantMemberInitializerDescriptor>True</CppRemoveRedundantMemberInitializerDescriptor><CppRemoveRedundantParentheses>True</CppRemoveRedundantParentheses><CppRemoveTemplateArgumentsDescriptor>True</CppRemoveTemplateArgumentsDescriptor><CppRemoveUnreachableCode>True</CppRemoveUnreachableCode><CppRemoveUnusedIncludes>True</CppRemoveUnusedIncludes><CppRemoveUnusedLambdaCaptures>True</CppRemoveUnusedLambdaCaptures><CppReplaceIfWithIfConsteval>True</CppReplaceIfWithIfConsteval><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><VBMakeFieldReadonly>True</VBMakeFieldReadonly><Xaml.RedundantFreezeAttribute>True</Xaml.RedundantFreezeAttribute><Xaml.RemoveRedundantModifiersAttribute>True</Xaml.RemoveRedundantModifiersAttribute><Xaml.RemoveRedundantNameAttribute>True</Xaml.RemoveRedundantNameAttribute><Xaml.RemoveRedundantResource>True</Xaml.RemoveRedundantResource><Xaml.RemoveRedundantCollectionProperty>True</Xaml.RemoveRedundantCollectionProperty><Xaml.RemoveRedundantAttachedPropertySetter>True</Xaml.RemoveRedundantAttachedPropertySetter><Xaml.RemoveRedundantStyledValue>True</Xaml.RemoveRedundantStyledValue><Xaml.RemoveForbiddenResourceName>True</Xaml.RemoveForbiddenResourceName><Xaml.RemoveRedundantGridDefinitionsAttribute>True</Xaml.RemoveRedundantGridDefinitionsAttribute><Xaml.RemoveRedundantUpdateSourceTriggerAttribute>True</Xaml.RemoveRedundantUpdateSourceTriggerAttribute><Xaml.RemoveRedundantBindingModeAttribute>True</Xaml.RemoveRedundantBindingModeAttribute><Xaml.RemoveRedundantGridSpanAttribut>True</Xaml.RemoveRedundantGridSpanAttribut></Profile> + ExpressionBody + NotRequired + False + ExpressionBody + StringEmpty + Join + ExpressionBody + ExpressionBody + public file private required internal new protected static abstract sealed override async extern unsafe volatile virtual readonly + Remove + 0 + 0 + 0 + 0 + 1 + True + True + False + NEVER + ALWAYS + ALWAYS + NEVER + False + False + True + False + False + True + True + True + 182 + CHOP_IF_LONG + CHOP_ALWAYS + + True + True + True + True \ No newline at end of file diff --git a/OpenSSH_GUI.slnx b/OpenSSH_GUI.slnx index 17e92e4..205903e 100644 --- a/OpenSSH_GUI.slnx +++ b/OpenSSH_GUI.slnx @@ -15,30 +15,10 @@ - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/OpenSSH_GUI/App.axaml b/OpenSSH_GUI/App.axaml index 1e39dfa..f58e492 100644 --- a/OpenSSH_GUI/App.axaml +++ b/OpenSSH_GUI/App.axaml @@ -3,17 +3,108 @@ x:Class="OpenSSH_GUI.App" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" RequestedThemeVariant="Default"> - + + + + + + + + + + + + + + + 14 + 14 + 18 - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/App.axaml.cs b/OpenSSH_GUI/App.axaml.cs index b93c6c6..2fc7291 100644 --- a/OpenSSH_GUI/App.axaml.cs +++ b/OpenSSH_GUI/App.axaml.cs @@ -1,21 +1,71 @@ +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using DryIoc; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Configuration; +using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; +using OpenSSH_GUI.Core.Resources; using OpenSSH_GUI.Core.Services; using OpenSSH_GUI.ViewModels; using OpenSSH_GUI.Views; +using Renci.SshNet; +using Serilog.Core; +using SkiaSharp; +using Svg.Skia; namespace OpenSSH_GUI; -public class App(ILogger logger, IResolver resolver) : Application +internal class DoubleToleranceComparer(double epsilon) : IEqualityComparer { + public bool Equals(double x, double y) => Math.Abs(x - y) < epsilon; + + public int GetHashCode(double obj) => 0; +} + +[UsedImplicitly] +public class App( + ILogger logger, + IServiceProvider serviceProvider, + AppIconStore iconStore, + IHostApplicationLifetime hostApplicationLifetime) : Application +{ + private const string RessourceUri = "avares://OpenSSH_GUI/Assets/openssh-gui{0}.svg"; + private const string Underline = "_"; + internal const string SystemFontSize = "SystemFontSize"; + private const string BaseFontSize = "BaseFontSize"; + private const string MaterialIconSize = "MaterialIconSize"; + private static readonly CompositeDisposable Disposables = new(); + + private static readonly Dictionary IconSizes = new() + { + { 16, 16 }, + { 32, 32 }, + { 48, 48 }, + { 64, 64 }, + { 128, 128 }, + { 256, 256 }, + { 512, 512 } + }; + public override void Initialize() { AvaloniaXamlLoader.Load(this); + +#if DEBUG + this.AttachDeveloperTools(); +#endif } public override async void OnFrameworkInitializationCompleted() @@ -23,31 +73,148 @@ public override async void OnFrameworkInitializationCompleted() try { base.OnFrameworkInitializationCompleted(); - if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; - desktop.MainWindow = await resolver.ResolveViewAsync(); - logger.LogInformation("MainWindow created"); - desktop.MainWindow.Opened += OnMainWindowOpened; + SshNetLoggingConfiguration.InitializeLogging(serviceProvider.GetRequiredService()); + try + { + foreach (var variant in new[] + { + ThemeVariant.Light, ThemeVariant.Dark + }) + { + foreach (var (width, height) in IconSizes) + { + await using var svgStream = AssetLoader.Open( + new Uri( + string.Format(RessourceUri, variant is ThemeVariant.Light ? "-light" : string.Empty))); + var memoryStream = new MemoryStream(); + using var svg = new SKSvg(); + svg.Load(svgStream); + var bitmap = new SKBitmap(width, height, true); + using (var canvas = new SKCanvas(bitmap)) + { + if (Math.Min( + width / (svg.Picture?.CullRect.Width ?? width), + height / (svg.Picture?.CullRect.Height ?? height)) is var scale and > 0) + canvas.Scale(scale); + canvas.Clear(SKColors.Transparent); + canvas.DrawPicture(svg.Picture); + } + + using (var data = SKImage.FromBitmap(bitmap)) + { + if (data != null) + { + using var dataToEncode = data.Encode(SKEncodedImageFormat.Png, 100); + if (dataToEncode is null) continue; + memoryStream.Write(dataToEncode.AsSpan()); + memoryStream.Seek(0, SeekOrigin.Begin); + } + } + + var bm = new Bitmap(memoryStream); + var bitmapKey = string.Join(Underline, nameof(Bitmap), width, variant).ToLower(); + iconStore.AddBitmap(bitmapKey, bm); + } + + var iconKey = string.Join(Underline, nameof(WindowIcon), 32, variant).ToLower(); + var bitmapRef = iconStore.GetBitmap(string.Join(Underline, nameof(Bitmap), 32, variant).ToLower()); + if (bitmapRef is not null) + iconStore.AddWindowIcon(iconKey, new WindowIcon(bitmapRef)); + } + } + catch (Exception e) + { + logger.LogError(e, "Error creating app icons"); + throw; + } + + try + { + ApplyConfiguration(); + if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; + desktop.MainWindow = await serviceProvider.ResolveViewAsync(); + + if (Current is not null) + { + Current.Resources + .GetResourceObservable(SystemFontSize) + .Select(fs => fs as double?) + .Where(fs => fs.HasValue) + .Select(fs => fs!.Value) + .DistinctUntilChanged(new DoubleToleranceComparer(0.1)) + .Subscribe(FontSizeChanged) + .DisposeWith(Disposables); + if (Current.TryFindResource(BaseFontSize, out var fontSize) && + fontSize is double fontSizeValueDouble) + { + var fontSizeValue = fontSizeValueDouble * desktop.MainWindow.RenderScaling; + Current.Resources[SystemFontSize] = fontSizeValue; + } + } + + logger.LogInformation("MainWindow created"); + desktop.MainWindow.Opened += OnMainWindowOpened; + } + catch (Exception e) + { + logger.LogError(e, "Error during application initialization"); + } } catch (Exception e) { - logger.LogError(e, "Error during application initialization"); + logger.LogError(e, "Unhandled error during application initialization"); } } - + + private void ApplyConfiguration() + { + var configuration = serviceProvider.GetRequiredService>().Current; + Resources[SystemFontSize] = configuration.FontSize; + RequestedThemeVariant = configuration.PreferredTheme switch + { + ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light, + ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark, + _ => Avalonia.Styling.ThemeVariant.Default + }; + + var levelSwitch = serviceProvider.GetRequiredService(); + levelSwitch.MinimumLevel = configuration.LogLevel; + } + + private static void FontSizeChanged(double fontSize) + { + if (Current is null) return; + var materialIconSize = fontSize + 4; + Current.Resources[MaterialIconSize] = materialIconSize; + } + /// - /// Triggers the initial SSH key search after the main window has been presented, - /// ensuring the UI is fully ready before background work begins. + /// Triggers the initial SSH key search after the main window has been presented, + /// ensuring the UI is fully ready before background work begins. /// private async void OnMainWindowOpened(object? sender, EventArgs e) { try { if (sender is Window window) + { window.Opened -= OnMainWindowOpened; + window.Topmost = true; + logger.LogDebug("Trying to bring {WindowName} to front", sender.GetType().Name ?? "null"); + window.Activate(); + + Dispatcher.Post( + () => + { + logger.LogDebug("Window is not set as topmost anymore"); + window.Topmost = false; + }, DispatcherPriority.Background); + } try { - await resolver.Resolve().InitialSearchAsync(); + await serviceProvider.GetRequiredService().InitialSearchAsync(hostApplicationLifetime.ApplicationStopping); + logger.LogInformation("Initial key search completed"); } catch (Exception ex) { diff --git a/OpenSSH_GUI/Assets/appicon.ico b/OpenSSH_GUI/Assets/appicon.ico deleted file mode 100644 index d7a2067a8efb9cb7f1524f6944412805bb4f7be3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24306 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 diff --git a/OpenSSH_GUI/Assets/avalonia-logo.ico b/OpenSSH_GUI/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49ff9b94e52778f5324a1b87dd443a698b57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176111 zcmeDk2S5|a7VMr~?`&u9?L6D5r)O_~sMvcwdp~TLayYQR=&jt)Ayzj1|ar_qzjj>}3?t6{b(C9x>L&LzJ@V=g=#=Jw20UVfL=lL2M z{~gmTy4MTV(6|w$snH9bK-Q3=ARS!FJP09mMIup;tn{o!d3kv!@^W%);OYRUBb<*& zUfu&ZAL5yllVg^Vkuh1CsZc0vmX(z?KQA};38YPiymH{ogEL>rnVXlJ_Y}Vu#0Z*Z zXJ>DO??NCgJkKTk#1sX_t1C|tlgWFD>4bgc>I_5jV7WPYGW!B~zVr%7^rTZC!#6?c{Pd1_ zIeFIbAU9KzPF`JjK#u*jl^h+ik(i9#LoR9kM=p*!K(3EAAonMnB2VL3`nrudy<`&NuX{dHBmskjyxgulTMQtE3Ohgoyr@s&2vplOB)Vp ze6g71$V6hl<|45ib%@-XX-qtiKP3U?F2rNcJ@R0RF?IT1cn$exVcoK!f1QW^)&ly{ z3AmSFJ)>R+k*BM#kUJAklKT@+kp}>?{lwGck?uL-bKHT5m?`)zmK~l0c(=2&s|j@& z40U)+@FF@h54?V(MG?laiB_b6A`x{u%op_95uY zW1-*KL%t#^|D0TsDM}~l+*GQ*ST{KEPYl%i81@@|ef=8vJsu$;A$77+Q~Lrw4nRI0 zkd6gsDx7I>3SrDdK|i|(V{0j!&2A<8Z9xti8u*N)q%=z9^M8XrJqz;M2Ito zsTq@?%nl@y)Rm@J$CU}0xWj1xrz(d5Byxx8h6%MGM1z`VI>EECaN>OQtsQ_HOm0cSY;4uk#> zo|l~y0eFvtdp3OIm6MrmoGM5ilg>+Tr>sqgfkBP<`1ty1DQRu8g=s@zE?JSAlluzF zpgK9!tx^Z%g3O-fKBfwplZ21Pz=E=#)4O4lkeR48$b^**d-mC0@{*Wv!9}3Zo ziHWHPYh@S2HI&U)Rxr-H(LQ0s{fZ-bcFdMI9JkdK4TP>??!5IIGk1zkwgp1W|T+_51`MJ_tvk-iShpsgTd>mb@2Efo5{(cTgmBR z{}7YmJITfI2Z`&y_o#Up=Vs~o;l#5NS;82hVfpYvGaUMxAXzXlJ1g6!L_&BVWb=vn z!XxC+pfyU%HvMxqF&nX$T!ZykTCVh3M)|dQ3A}dcsp)e8c4{Gzt%H~=B&W1?t5o*I zkq5|)F^5$uAMhVi2!BI~Kr$dVJJ(jWT>K4bh{cNIDwl0B>R)0t_NYqbOWR+KFG?15 z%ScOG1!W^$SnRkkSHA?lEoK}cyqM24jP!#vu9!SsV?k`j9WPP7&oM`7vZ5=LAC2uV zWTgzs$;vua^a6e$y(eIC$+f@F6zk__{@g*>Vezs_i~Ytr*lB<6_tO67vFl#3ba(^f zkB#Mv`QpEFv$CzE3DN|q#Ac5kef1?@Ouk+=4?S`1yyT@$Gr}xip#5tH0^%66L>Gezin; zXnz5gpPC{V2Xr3NXw>oHkw;PaNVf(&dQZ(QIKJPTm7GVU-$}30j-N`D|H;fn`nu=} z{XY`R7jyU{VcxkeeUUDbkau@p5r;E2gcFrWZq7F%(z(Tc^+jnirB|P04kgNuxa(6Q zJo{S$m#jldZ~}3hcdI46`{ib5Un-f95SJtOsd-Jd>^tL65W5LR zC18~;7k|5LvnBbkUdtc(Ik^r<*J1hau9hTO(mG9)HWkKXiHR*2*7Rqat`)(pYS}NA zS&~cvvKS?fv<#7CNd}+ap|E^S!eaddbWh)$?Ci58Qp1B>T>FndA*z=BX7@dk1w4|X zBR4nqep-rfFpo}ejOF72>1v9_;uc7g0&V(1(RcWap?n&G>|mIOkp}~psiRi zHUVd?aeP91i~~A#KCBn|Fn?c$b<-Z!?gzk2T?JWyA0Xs0TftV%!Ss)N}l7JjS#1jpNwR;TWh{xfL5WqSHeYXqDr!BFM zQM|JXFk@Thf`}j!P9dC3INjkiC_JGe*lra*F(3DWvnEqRqb`)u1j_0NWsU(c1wnZz zh*&jN!1*o8DWKX_d1&Hz#r})3SmcvF2B5|c&LgQnU!)|a^aMJ4WGYuM3*ejr@Xt zFpBf@bK!S3Ji~WNPfQ0V9+_~az?|crCKRucBnt+J6B1e=3<~O}^bs*2HDcUi>Lt;W ze!;dD@`RJ2&Ie(fzk~dX1l&-ksyxzREj}hlP9A`GS6W%Q7dY3@VO>X>66t!Vw*j0id=OdKwG7!3B;>J@yXrfs#)R|YM}{dZB_*9XF&qzcc71!0v+NB&q@++RafN_ zIRlU2!e=G_RieT&58xwBx)Z%_a!hh-n1}yNOHDHX*m)%~`w9=B9$XmXdNS25_LHhR zon9B|F_8a^&d$iTfM+G-0AHc%RFP2sd_If2q*$e8Zg6aK7@St(Wd5k!tXyQWziNL` z&`!BHzsgj(=qIGD3G-ufkZTXkOiwq1`&DqZ*kQfne@!;WM3OBYLa!#&Q=WgdV|LUZ*e z_x4(l3DZ%q80wmfel$b9%O3CB&2dz_D_cMjE*mHmGBIif!Avg6( z%EMHye;(AI!(S}h{!q6XLRgzoZUS_ulcKuHJ_9)y=r8Y*g9BHWyY48@H3zweY<=Z_ zm)8DJ4~Zm2v`Du8us+qrH38`8&F~)Accn*GdM3HK>1-wHzMowB>tN;T&zBU{A9*9B zi>Ub~C-oz;hDp_}wIUQ14{RzyMNij*CBm(hDs9&jV?|kvG8tVQp=$!vk zTm6%1w1y}zM%h_uZJ*3wkwZh)mao5$+)K&Y%tvCMDUkJ{zWmx~eYMmd>Z^$~rGzJ( z1Z`ice8YN&d6{*;F!2EKyz+vi&{>px2=!7T7M}#$dlB1t#+0rf>yC0W`7tYdU)uPE zdZqy{OU*yN7QVFw*mp#d#Q=-azQc)5EVJ(SHkgxikh3d0aBX{U>_FAsYRr+!)IUSQ z6;bp9&1^OUW+aL1D0{Vbj zf1{(Ln{e6OVZevnm*y|M0=Goz9`nF90%?LPOO8`|6Zv)3WaKWwk1ch*mu5*_QSSI? z{)JN8Kg`yv*f(-FIbyDuqJLNs5kI560_gg;vT15$Es9Agwf-MZl}ZBS0bRcm>yP{i$c(1s=j0Vr z9?;zVi`5S154zENu35f2>ySh*S( zzs;0n?&h*sD5BON8bmWJEUX3CqK$vTOrU3wCZKev>#q}< zwI^k_iux@2LqGC%-+l66Qh{xyY+sT8t;nW9rV7}v_+o*0dP-bMTdY4GEML}7{CM_n zKm(z?LFs|=gBw!~DSfwWyCW?ot-I~`@4rSJ2OsF3 zg4zQPKo7!==l+_?6V3+sO0^G*^Nu7}$LLe^yL`J>rtSz!m`$lP4^}@9+K_lKCUv?IjMtB3|xN4sO)(LSOqX)yHfPpM#+g7R3XUoo8>@{pA5 zgrB+qa3CzL{`fBRz7M%Q-jM3=m2G#NZ;-|ei)YLfdm-Y|a-XC3TYR_tJVx zuaLe_*UsrG;fof-cgh!WY37Ajq{m`k(2RrP>6WI?~# zo66?*L;WdySE{}k-q%2VIv>)5z1nuTFJZf=O4)hYxlm6b5y$Z;S*I%BC`gl=nVES` z#N`e}It|{Js^gczLrs)tfu3n#mLy|8v_XYnP*9)pJjwwc;S$I>N3wy(D!0B4#sbRG z1&PT6!FFsrz@R#VTb^1fNDF191C4N6%n^@3Jp>Kp%8;zoej{yr*((9P9pZtqyB0|n z$@2&bimvn{;A7)5Q`5I1O`HmHsfyNJ3I|jO?AB8napE{#VP2Y)ot}9C+DEA!bwvSy zJhMOs;t2X}Jzi2$pV*)RJvG#$-0d!{yYvcms)1_;^2%rr4RhH%u#>ZcG6fZ_Z_#)8 z`58ddIHPVF`pciZL|%KeeJjfzM_M;kuTUOkFM_yWMYB2pt?>uYfit0>o(2BKAKt4x z#sTh3)E}dLCg@}r;TT056Vyppw!f4G55j?S0m6YaEa>9u#p2ipSkQdhky zk`MAgXcL8NJL>;%?1@>dpK;#C6Xo0SwD{&oU!e^J!mX}4Lq2c-4*5m7v4*+28H+XSA1MF(@3#Vg;)9VrT6Yo4Gmc3 zrB^221E(RqQg8z2M8Vy$usz^Pwa*yjXW`I?s{w!mH^d#X!z+B)1h3G5lz|p80NacL zf3mUg2_y&bJHg){$B!1M+7>{4E6#gp`-t-(i^1xMHU}IgX9U>4O$HiZrija50yd`q zr1C@tU`Kuh^vVz5yq6(Pznzhqb~!yY%`81F{XDFHre&U~nWpfqsYH}&n#vcQPvr~D zAWw@lN!m4uP<%X-0N|~Axn=%+>SyKB>HMatcH&O#_icsgnqbIZj+Oj{$oyG~1 zh4a8J>}XDA)-zb|B4BMsJEPJWVw^VBcR-POEbwc-%1`2Jr$ndpi1v+c0@b@2`d}cB#nZw%mj+*H?+|wE>j@z5 z;Ke5O5hd|;V9cHexW5=5SDD5EfAC9SKR2i}7?r()a&dn9DLx|pS6%{pcp5)-BeZEy zW$N>#zXiL4IDS&HjxrdPJx9I=`#YP-?u;~gra0XM`bjls8|L@WUXuxrM9de%o84(539=gDy^nN!7{ zfUJq6#3T`>ZzQ3=3n3hO0!ia5pGqVwA*Fvp9aL#&VLX&FD+NC4M)L5=-D@IUgL0*m z_>{3gzuA?UX(f1jAho4620L1t)u!abZP#LU zKVF9+W(??p$~rl|s=2@c{3qq$Eq04BEl^efbj@J!T!n1R*??E;?H7ptkad)e z7REtP20PjjV_XE|;RQAzXTg5OdWkWKat|izhCf{>K2Z!{nHxZ(ChA?2qvN}y&kev{ zZr=cmzwki+I{9z#XPd_I!j3-FXpf9~b$h+DW#S(DhN}2a7pIp7e=U@agB{NVnCpw# zj+D~Hi(W;(4<<)PZz2E6*e_QGcJq<@6vik}G!|5bU!oX(#63mhM6-X(5KH#KeYyJm zJBasjXz&`f!jAsjHlVv#1h4!vRpHM_R{}riW>T2UHpnXiT}vxMstP}x&f0%ffgf>?a$B@Hg;)Y;vy-m^*i;gX`%zV}V+;dZ@Sj%%ul%#hz>jnu z%7a0sJppvusCQ85U=;9$s^JG%DH{uJT+$!e8ClkaC&+9@05{ErE$&3GN z$ioeniREN{Ds}~qcPZWxcC?AVJKO(*_G6m&ys=%Kvl#pX%wiemWtFp#j znSPiAKJp|Ot3>`lyMj2c2=Zj(6{^omVW;fpsu+Hn9jy-fnTXi@ML_QqcSe)1XyLu< z<)`I>{UyXd!gyP%91)IwW68} z<{79=)4sc0s?E2;B9j7Q$gPQnR2-9gRSZAMgh9_a6m;h$d=(T`PQ>A>4EvKk*U`C3 zQ8r~hi*WEGx5pY1b;F-7krd;9P**|mdD%JW!>sUtaZ&6!J2HV>TX|X`A1CEyOh@k_ zVs<4=5unKDU~_5*@lqA7ck<6v?f;+~IVHpLXvENBT8lWmDImk87Xwn}CMhzGJUVfU zSnn|-9#&2S{V34i#A=O6F&Qh!C zj1{a1UioLSFN?XFD9sl7|5aJ|(bfd?le3}!mk>g67>UL3E`=Sh7grW67q3s-mylhY zAGwE$7p$}r<#?eePMAFGcpqu+t5U8Izwnkk{21#4=C~5}!QvEwQuuFdHKEFTe)LW; zxs6nIf$^5rakyi=G8N=syl{oXw?q|E1tL>f_)#B*dP^Ap3hi2D{e5KdAM6a`U|1Kf z%{fl_{$QV%!j5tqL7c+uO4O&U2N;`775HW1d6$|cKbgB%7XG;KxV9qDYXJUB1Z%`~ zPXe-8^W{g1`T@>`u2-K@WrV*f@OzSn9pyH`4=WuGi?X#<1$K<%2jjO?xTPcZx zVYa3FLrUAmX%U73Df<9eGC)@i(e6JVXak0EQ$WV=Tv`s;4%o)Xzqpz_BBrDED1}|h z$01Ks(IZQoL7wWB?$0WP-}g-EzD?3Pz!+!YTK^e(4UO=3;A_TY4a&~Sx-Lyu+BG{p zi<}?5w@lbkc40f~3`t8Vv8}(e^Kq zk=Rqj6a1r6CXndyu4~2SI%%Jm;vHd^@~}_-zFe+0fI1TN9g*U;tm~tx=U~$T)kL)r zAI}bX9a;F%?y+zUz%{T04Wy_|qTGUu@m};xl!YJdcoM)@u8;>(ZPJGRd4IJT_{|l}b&BvVg&kuoBOh1b zLwAFqk2mgpkinNwelFrzE{Syp9}$DcN@GjUVkQ&ud=qDBGxcCam;h4ij0{P0^7 zZPqyPoc`czcd{sb89x#O7)oVUieR^eSj#BOLwOd)L&bd=l)MRVPkf*toS;j2jRA}m1LF!<~g-4vkp&@ZzD{4fS$z?UK( z^q!#mS`MF-6jEYF3J!51pV&+@Dw5a9j`ynQ^SFOXoJ;w5Yw-Cp1%37)v~|b%P9A=| zN4+=7g1}#L6vQ}JeNu%s!M#%MOg~M@>!fpCRltrueyZ|$QdFVM7uv7jyoa)0MX*!w zgB}2FKG5Dp#G!QG=I3n_F&D8?m(JGkh9#1zViSLz)sHEV^U-MDloy<%gh`zkI zSBNtBsWt#z0L}y4c=j-`6)e^7TD~B>M;ny)M<1(wo`1dO2JG2Wb{qitIsr}Z#ba@% zAdd%n4#d6G`$1tdfa?Hd--&k2s0RjJpr3r6s@$hQ91GWNHkDrEo-MpgVqU-=PAc+t zvULMmjt{Z3R-^!Ji@IH9<6gcYD7$7iT0`{v0l%{4Fn3m%k;gaz-bbDi?7OP2={Uxb z2EACi z!kzMINBD3LEX1$dRvY4}|A+)!a3)OHlMCa8SMmVo;4E9DXPKeQHfYOLR==0;1DL+R zmm#VpFM;zX_$QozI;o@=u4LUS{W;jHy+Au}Mku4BC(P%NVX0$Y0qoQx{0?osQ=kn& z&OIs<{4$^)s7I(*X($zEfd2SkuQ-(x%jtq+9#WM$-z$S%`W)LJ24cD3{F%&39tFN7 z$Ds{Wri~QWvPz!99ub*OMag^}PF!49^l?DFwiJ%aTyZ``83FbK4vqz(WIxDJs*Sxr z;3E^(>Z?MK;h}xHI$@W#8}%(P`7y>mgm!oUeUejII2C*^0ejRp0QYtwhdUAMHTpya zMzzGfLDV(R6#=K>52Tf`Y~-60!V+3wJ0Puqx>S&n9|1hQ0ooDULN&#N9MFJk5%{fr zg7{9CL@A<$;8QpTWp^9~qZO`g>h#xE5oCqQpxOoP0OJqZqABv3Xh$iCO9uac!3^+| zS`R*mmtfD0XJ}gp{UaK9Qrt@*nL6|GS?~<^f((ZD&JZ)rN+Oo*Nd=!ev_r=Emd#{# z#x_RTO?81=L1Snle~Due=V7F~(Y63>$&+Ie2B04-z%yQy%+mrLgsy-sn1LtiBhV*{ zo5-Dr<0vJTHJBU2>cxY0#F$QKlZ-Sh_BCv41?5(|M_5m63&a(!n>663VuOO3CHd2T zNsftWoe~$<7Uxeqv5k;UXXAK=HbY-4q+2nDE#y<dM++sV6e(A&4oBHVm`4)zXi75>h@ zZLmjh`@okzo&8>Tb_;X<)FaR}uxI$Yz@CAwAA5#+1azml`E`qY8`LG-Bd~LrS6HVo zuYgVr|Im(jhQ6=r(;v!!)5X7IfSXq*tY?t($1c83@4E(i_;jYd^X-5z1ipOVGRX05 zGlUUkxk!%}%1FKmKBAHx5M?zKZ;HGGz+Ug&lXs0gUwAf093y^XKSp+m@r~%k@C)xB z8x%D-A&mKFTqt97EG>FMOkk82!;d~K+Anez<5T#&n81hy@N7X`aMb)*8e=XqBzksS zXpCMQjXonbj4>@CELJxuGGS^$B$GOmqT_Ycen!OW#78i7;zOf#q5~qQM*BuirGE&S z7Vb@(5#<}97ZVVn#|WfPkEb!U5r){1sF8^@=0HYZcu&xMxASrKY2oYO`xCau7m$@z z5`7i=oEqb91_rfodwU`jYFgiOkD+{!<9PIGhM)rh&o}9HfQymna)t2b7p#mGwfUH4DqU6z>mR2B1m;7#|Sh1Xie} ztI}DHBlvLo zf-EW)(3$>ifR-Z9@A%YaQiB>&6VF4@wAUj!&Y;&Phq&tOeP$DU3;1*l#oja9yo+ zm{r-60QOXfnfI6z&03ro#vBn75Y`FXtx(qX&GZ4pJJN8tmeD+E&1vsw9pVC``o+=W zMp1KmEN5++NOA+lcNmQ7|66=3>q{^nFkm0zt?^+oW03~(ef_!#wkPT|R33a^J|VTX z<9vm9$2APsbl87qAgpbZQka~@Vjlk##Noree^Zsg{^NN;3&6U^bf--vK zjlMiu%PtXWtciQlpk4sSdl<}HNXxLoDFMApwix;Kk)y#1<>aW;y!F* z2GRdSelZ4kr0T>M;CzIA(#grGZonhArcob)+sDu%2Otdtc>f#dZfoer6}Hd&+!Fu4 zzd+|2o){IkAUY`ex7fEq&5)jg5&6~E0qloJm(W0Vf&3fYpVkLxx^cj(EeBfmKIqof z<6!*%i~1tSVV_VdTtiWAg*M<{c@Ch)yxR@8ddSBy1H(JVgvJa99(E4I-9HKAJ+7$Y zKYpmC1)xm@z!$G%gfM;&!Z`re+Ok(=^``(}sMyLB5AQ~6jWj)r9ycX9p1lS5w|DS9 zPb~od$fQIIzgf$Lz5W^bCHh&dcMkS z>q<1p|JeiBH-^TFjGr9_GBcE$mX0m8z6Dz$QUm9Elut(oM0dx2$YZ6fE*$gUt6Z*n z^)Rrb=EShp(Y- zWB5gk3U>Bxr5t5ydsCpB16fY!8{al4$6-as&w}~>Cd~Jl-+yaYKL|m$Kb5s{W1Deq;9}-uTE-3 zc=60ASsrDR;2qd5o)$BV!(c4|xvlG00{cg?g)IO&WN*5E_;j=-Uwo3q+PI4T370MsKKIA`YfGr^A zi=85ULZ|vad*4xQBfc;r$i4>37DIhQ+r)oj3{8qjSPtVp=ts*}pB4c7r$-T9LE6DD zKd6=dL)i}6-=Wh0LktVLj}q%_`c^=Xm+ubMz?z=vTzAyWdKyxXa3{6hW}iz zQh8!~MnL2wFO~kF zb);bbhVtVc<6cW+U!N)5zsh{lLGtRj9Z7)rfEXWG_Neyw7o^%Vf*FASh)Uxh*L;-g zqFo6yIBG;PGifu}o0LC@m23l6@HZ3U|LNj1`^3=LN{@e>_tB>cb$RT_SY61s zQhO+tci4-P1?2v}S7BeS)dhPLeMQ{kK7JP<9z4c`x0-ykdgDJe-5%|LDl`8Bt~Ak( zkdo_zJvQJ1_fz{KYd+HOxG&Y=ksGTW?#&=p@-^7gN)@_J)imm+|G=)UviR3TJ94z! ziVtS=XEUjJKeD{zw<7659SqecfZ9qg?rp11NR3| z1+S{6sV?}(SZ^rji-)n#iDK!Y51xV{tF}i^PuhHQxJUfsUNEZSR+V(s1pkz*Ckobm z)aeUYyd7Y|Rb{cVoi9E9CUKAZ8h?-YM>#Lj{OEVjrYBAZn{79>4RpDT{GPu5W^sSz zJH!d_^)2N9H~rKD%(N+UfH;p?uGe1;lE(+_pFcp+3e^9UEulLDvo8v zU!siX^0H&!1@5nb?Em(6H2zWEhrYUu;PCz!lL2Hhe8pI-_*1_p@4h(hujn2oPXF1E z4>w&%#H#?p^o}5LA0i3kEsfBgejxA7oyg-YSBS;fLzGNcm2r=_zdqUk_Qv~u=6{^~ zk>|&`U(6Gpt~izze~B_alj#S(i2mLT_VIQ<_k?i5RVQC?e|!4tK;pRLMhQ9}X+7zj zFU38|{;bCtelP34Ci-iKLaf^)h)D|jFTGNX#fm@unJO|5RKqh{+Wf8aFykka|H` zTU7M9!*S~>v(@}?%cY{#Qu(_~Q95y0ccp0DBkpluY}@Z+{1>eK5Pv=?Dqb7o>Z;r@ zDkOyc*U9m*+euZ}XurGkOobY#CkgB~PaZ8X1H2dD9(jM<6I@l5P zbR+oWZ6M|G$5Z5!MRW31cNJC74gc z<^KN^?GO7bVBGLDaoY8AHH2K^jMOv|@ji(7K7C7Q?*0V!FPBRJF)3g!xG?SCGW~EB z;U4|*XwN>D$n$GFc(uu@+K>M?Ig?K^l9Z zG~AyB{0Ba$ULj^27hO~<{yp{8YiKNi+f^Cs>^T}U4`)fRx17X@)peh;O7UrA6)-GFV1DuM9AS2u4JcInHySBoyzkg^8QD);ve%<=ON|_9z=YkY5O|7Q_BC#(Eqa`rc-cv%C}g1 zvRwE-I$;aR$;>V)!tLxMf@8k4aWBO^#@k7ut2aJkQAH~F!~fhXwc?-Qq~4HPuutyI zX#a=_{;&MoDx?1j`2WZ*xX&v1<@lASDL}SZE*=2o!qNlujKos!sLHrU`}~L({?gB@ z#no+_dilS2kI(WEbpR-W{lXdkk)uo5{{#2us)zfovLh**|99mr7wN#ggI1I|4>+3K zDVBBcQ}1%&9^>t}o%~EY6wB-@+@QViLoE}vj(-82qgF^nDS{(0;KPlvdXz8wL z`iUyD^D8gh2_6w@#l8Kc(**mJIuBk#%0IDTQG-j{{|WVf7{fg&JYgKf(5)~7f;!n~ z-*Dn=`Gh<%x=oPIM;)N-dXKQ>X6KMQYcG@=_ZV*neKX>`BGlPL70&DZp@(Y4|Feac zD_j>vAA${UdNPx}3gfoXA$FsZ@vnh){}LM#HB!js8!5_5UC+`55@NT}yu!Fg ze>}&3Zm6p|70yQ-$0H9WpHVCR-yM8V;rb~05bU87F>=zAwe=A@f7s=*R+20Y)0mO2qVYz9&()@7mFS|hUAU5dN zIFbWm39i+u%5+psCx}$(zBRi(;G(`12PnbYDcYRCQ4nHSW)O&;#Mm_&~s8~P@+4bphZ#y>o#;`<^G zm;kYzVIP;`h8jv+L$w#M2Nf|LwYOY!zM?rFb3hoaDn&x5BK6j-j9HPS1I_{z}W8CPm;po$EFD-U|6r-1MPNHRdMm3Sw|wv|^EvKVCAdfYz*14uX#uI1><@B8|76ZG#a1OGK~ zujaUr=s$P~?CtQqTAk^lLC!DLezqQJ9rs1Jm+{AYvg9I3^oc5^WmJ2Wot8y{uf9>c zd{=hde&1z~ zweUHytfk;{-;eI?-56u}mWFrfJB$Hv6kdxFQETD`e4hBds*D0Z{}U_&$v6`B)JE6`e>_fH{leyKk?KT8Qb#s zmcOrxbsu{Q?8$eM6%qRv4dOYJ!S_p1FTGMR->Ln4!(zsyrijj#uji?jI@&F`+;O(P zH{3fdvQWFO4_hDTb~YF0{%DZpVk|eD)1}B&<%;QXZ%>AQ#P8f#hyjblmPgI~uy>a#c$cPuyeNMVnsTi<kyt zWnOkU?ZV3gAk>{a|Hn#Ue7$d-$D?>YuoZ|=vt74*`=;_!&nJX4$D_O#7R&*2t9rlhZ0G|owp)ES>pjl-(GH)aXsW7f zeySkV6vqBI9Q%N?`cP1%#=f*aU_Nx1147^Uwn5uaej;}-OaYly1qkMgdO{C_2j8?@ z563;qcH`aE>&v02-C@iOu+9RE)K6O}2xwJzi+l`>EkrJejuVh5u=Im#Fn^+k0*V+SzF!#So!x}54R&&P59{@;frOPrzZrcjt4?Ct)94SF8* z-3^B^ieptCf9kkLIReHA34I^hF(D#$0{9e~LWQb~7L)}xgC`+x4IV)PPk@hLEu<~cJ_u~eXRF&rR2Juo zesjP+&S|A(wbbKzA9+eL`RcdfP}C0i4CehDNwV++(tH@#4aa6hWqqpl1t=D1L3-U_ z@8DKwBgkg3R>L|FudI$$@jMseh)04R-(mj6YN5k@yWgI0LlUb3)Kc?IPfdG-`?4|u z!+WBR567mec&to1Twf?Z0q`e?2RmVq3;tKt{D7i{KzR|vF_64ie)Ws%@sX$VGI&h* zYWCGo1gD~BD2GE{A8mAyCd1gBkWMZ9o(g?K6Zs45bGR=!>IWqv2?k{RLaScM7C}6q z4VA-evnuTiXah>KdQZ~WYITh&2~a6da6h)>c=ndWFy@Fj|M0e+o}Trqdfu1s6Hq;h z9|!|makMc&eG}{#QSO)lrGQzXSGYEyeHYqnx^A|vv~MQ*ajkG*O& z;Czc~KGNa71-kt&HSf!J0Sy3@;t3->KmE!KV*Z)JWUU6<`>HW&sj61}HuBAfm<;#O z9uxQH2=no2@rBp?61XpXfa^d_H}ET`y`y!AqcKKt6S4%2Ih$GZUc zV?;ZgLR-#ig?njd1AxG0(4scoo8FiSv?=^^?oU{12_qxGaXF6&xoGf)%tQk1?3Y*ORFCq>BS?+2Pdy@4*iJ#>GrF)B|a z8Lk+oBQ7Ft6x}ztYmj45Ga8LnryB8iWuaTydrbhe2J)*kPg-;IDMh*v?MHyG!S$d@ z?8!ejZuR~JvV0Njvwa@@D^P|S4DnZyc0zwmsCl(t>y;s0{yFwzU*7_{FzQ28dxRBb zS>U630(iu@>W!r;sa+n#*!Jau9)}gin2h!m!Op?0aIVC46ZvWRHvHD_DH#Fk4FSd| zplc%if_tOwLQ+h^a^Q9BK-m6&K^Nras4&W1gVLALR*94gB#TluRJHQJqV}g$cGZ^Br0&j;j zGRjT9r}2t^1R?{dJy)X^oBI$3))g8({$xCh5jr` zm!v!JSSf2@&C^1j9{YVX^nV=l-x_bH5TO-&s41ljP>+qWZQOq#O(8l>&&?P`$%|`#IRH5inQcTivR(QM?%05sh5&`oZ z(7-?8T>l;LbrnUo((jNy#JLdr0m=?hW`J=E(atnJJVuMdU@ZY#!^1EqxWZa0RB;%7 ziDdc6!+<^JL+q^y;C{+sK4C48=0%>a5bxhJeWw(^s=lD+gSmD!X<*Av z(MB+C(B}Z-n8nhf|H7D7AS+oe<_nELKlTD-Nzq?=je!0q$j;0T$Vg4ML75#pI&mR~ z&YB!gV+_T)D;(_d&^`xcw9@Vgabbs`Bf*3d2 zxL+l#vFlQ~qH_?Z<|WL(!4OX%3HpROd=w$8JTdy#g0I5|h^J}CWpn>USsf>eH8UY1 zVFKcu+FJp19Z1U}Era|G&Sfz9{21%SP+GAYYHffy0q0N$(1TLEBghbIoD4UT;oUdW8 z%%|z85^G@!{}eYqc?&l#X+?4jdp`O-qTMCxv#|f+ehR4DK%ArP3(7cujP;7)w>)3r zn6jh#f<o(B6%4}cg*!?0hl!xP5izE)^E$z~)@!#(bB z{9<20IVAW|%)oU8_esEbxfuVPvabd?Wjxq7qrD}{X%OELZV^9Y|HCyMZSate|1rOp z2ZL%&ORW*o{y@oB{NsssLa`y(s@AGAD@q5|q@m@B2yqBphRUT9Bd-pQ#4dmX- z-a`Jxsss1Ms-xh(SoPq2vFa(fXUdi5UdFwF+;7i zgR4=uy!XYMN26|e@0oJ&RrS5QTzTKxeO0$lIq}y-E`1ZZ{!`X{N4fMJ<@%@m{TR9T zW90h3{Jp;1_>sRaHaSAqkh=#{yJ8(g{vPJ%VhDlzVhticy}@)_3}E^Dj&jrG7_jb! zS`{5|Uko69xqG;ko$b+5P!<4cIbjy%2D1wsG8LxoWh#iPgY1Mk2JdAmq>uM96{2oG z7f2N^(?W%-Sy6#h_A&)@Ecm{t0R4h{DMW?Y6=hhMT~U)3W>-{0>F0$_Q1p4>2Sv%D z6l{{h!l(_6A?yl9)=%k@N zaon7JSGg|x_q7Y#&B|juxcD%6>#o1+)<1u1qE3x&3mg2l@bqsBFY>&nHeaW^e|htf zKHp7TRkEX>*5L>^pzveE1V>uh)=%(!z^tHeJ#h+r0a!kz;Gr zYCHCK+Ow#GNlmtEwrbkxQ{M${N#n-2+)TM{_b?~)-p+tgtxg&#Jr=G!H~H75v*%9F z8I^W*%8CX~S9AZ`dvV*1bVl@x#W!-_w7GL?be{3$(5@{FLgU|Uv$a|`a`)Esv`?Qj zGz~mND{E+U{q>;FZjH&Qw`(1(={>z&5BKiwbvpgIKjBP_>p5QNzS9Gjq~#9hw@n%~GWGBn z7c??xYW5@cow_}`f86zINN}5han1T)7}`}gwbrv2J+AzEky+!&(T!HM_2>s z*@g$ht;}ZXSOgh18{g}WVW086W}a`}v__2@o}J9Qof*`qx8_d|tp1?sYPa8@bFblq z2fYFZHGJ0GaH3}5d7U=e_pBSPskPPc=@QeX^^9yym)GxlYkk7O{YLeg{X226j#k^m z#K6GY8#}hE-SEDZl_pfXU4!vQjcVU?Z92B=Ua{-74gz72+2+V~&JRZU%z6_WV%VwO zkpaKm?Q(o&oAIre7;LO*`0EmPjlkn8j%zqtwfV)Y`8ccEt^YP_KHZUJ*wmw;>CabR z=(et7W^Fd}sNNg%IxEc<&FXuj#!sDej?bKQc%#jllUl9b=>77h7gnyvh#;LQI`_1C zw{LDUzVkX}LkL_FQT)lsPIo>Dau!*zYi4pxKcAMofXV)$>?S*Yk zd)8hVI>9@UAKLS()#WQK8jR&x@FXvPTfKh0LBqy-&TF2TZSC&s=wo4?*xPFV`khBN zt?g#GMsM7-4@TNA1{~A8&|+)zpBh}fVmA1~%#E{dOwf4x$z=4w&s4$I*TEDJLp;b zJGs$~2+PDdACt5jU6xBwQ1Y7(~SpNM?EHiS>{JLR6 z>#den#~iO(Z`tu-^OnbkJ{ykgFl~CId%fcMkf2Uc4adS`BUXgWmyQuE*I$HTAX+vfoFt*%q z!O~~CW=jjs#GoO=pReQ?uDM*_)8cKt-A39=roY@2zV^AL(<8^tJG-{9zcDL$=z%*M zkIj44d{Us*2>CCKso7F$j|%k9ID zENVQ=V$>bM;vH#BMM^iTc29b2?wZ?m$#^3&1v&*$4Y4<9@?%1CS4 z@ma0LkLfpgoL>q0^wSpL%aNz;DE z8i=hI_n5fJc+1``x3hGww;i^1^};{8cH4XAp_BcFmv4I1Fj;Z2t8TaYYkGHHf2zUt z9rWgx{_LHU_9o%dOq~|S3{%?)U4J^V{p@_<_1@FwUl+aAS(deNa-Tn2)PAa$?sC4* zEo*HXuJ)t2X$QysW8ZpJo#qqHy%q61_P1!@9@qKh68_J_Vt6}hHw@E_2pM|Kddp95 ze`z_Rxrw8P@y~zki<}m5)M!pznr-j?x6h0)W*A)@_qItcleXiDSa;=9;b!*z-uK>Y zXul%z^sOU(rc4@hwf80QyS|y%1CFj@8eexFb@JGXf8N!2JfP=@(RDny%PbeSnR>3* zh9_s7+6V8^eXBKVs&&7&T7$>Y4LuGHZhvoBxXCjk|5q6UKaTCYRNOb#|DKV>l)jV5 zvjGk7IGWA2>gboba)I9A{s)3*Gjxy5d8hT~z6i6xgoGB&p&7OMX{7F)>~*$b*!rC- zx(6+!JDu#?sH-`%%d|f~4miCgs7=Q1t&8J&yd5&M2Gf{v_?MH7bGP4^FaB@nPDi1` z%`QE=_D=0{$~L#@;AK{Nb6n^5H)+-PU8nqyvmxt={WR9?xE0@|$&sbQ?!>1WT54IJ z?>|gCl6h;|<$izH3%${2UFI?GzUH>4A}!4l-4ART@lGQ_xV>H1;nx;uux?uUEVX1ZpxaRLs1ONQ~Pi^wdyGf7SvC}4-udw6pul@U(Q`y_C&i|fxGot=K@`GDDs+kFB9wLa8;bj$Y54x6Sef3o@a@Vp*PoSXCCzN)t`;fP6- zCGItD`mp@y?Dl)@?;L9V%hpSq>h)><=AWfrPQkvn7WSWb;@W_Nks~gQS@$~K&n)Og z{PTK?QoJ+X-0A7n!%blRHg9G3zuHdrnD~-8E$97RX6>09>}LrdgxOALIBqM$Y2nh7 z3y-zBrO|F@#<)iT%L006I_%sXsnhy+%G&0=k2>@lY~1+cfRVx1_`l9-lX1-~Q1`a6 z$-=gaHn}Ykb{KVFxc+kXloN*nM%qmnG_%ja;AO2_-U@QOG-mk0#RDRT_4$3sHs+D$ z8jYqoZDCA4QP(4MCdBEu5EGa)X?c+;83qQ zb$iq13|yX{AF?n1$>Zk6bN?ml^-lHZ``_^Px-%|$rrurQ+}GXxT$FRx8|~j*^=dKS z&DH-cRLr0Hs!>;qXDw{C7TE@_?P(l0?s~p^?N;rz*Y?iVna9<2zT9HsvM1vB+g)44r1yzkKS$)1J8Q_m zQSGO{wd`Y(Jji zuJo8SLjTF(ZA*7H+4`C{baRZWOW@EjL-Rfw4*f~j6>Ho!uMXO9|F+A=X|IjWaC*Et zW4OS4yM2B-gZp|=^p=kCN4(nn+p3erdgZ@JF7?pYJv ze*-e!1nM;HU^*B&b8FpJE_Zh&&{`cm6y7TOP)79mbLO3E^kuaiyJ?!!x!?K)nm3Jh zrt9psSwf$-uAdiw&*q)x=7Yj^KjLR<^+|EH{FFHVgYm-sO~+r}VAN=9pMU=O*=J^r z^&iqUuh!BQu3q$fl2Zpa{UytH(I@ro(*JSuS_TO_{ya6s{i<2l$WZ%dE!y@xf)Q+MaEdmo3m zt%|GhpudI}KjWZ<#Sr5;eayA?I@+A)Ogs^LY8icK&fw^KzmCj~)HnX8Wh>Ung+mOR z_KjT>W?}1*tF!%Qv*tQOG;A7One~*N?Emz3VEu+onhcAK>K^=I{@WHFuDjC>S1#Vu zCAP<`J&T`STj=QJ=sWk?{m7VWT0I!kwKiQ0G1<`g-{1GGbGqumo z*9i`uVlsKnm_OtG{xxCgZzCKY&%Gad&d+jKux7)@{&!bsUEibYrDM5e?TN?x9%)_K zbYTAO|Dqg?@3-BuUN`0MNYA-0DX;!pw|RB$X`42`nKgdVwDHG@qklWle}9^>@cF96 zj{7aQzHD}6o{XiDnPe;qn;5Pp0%kX&>6nxk?DZnISc6`?Ug5Y8xI|_ zc8!(whqK1I`Ah$Kl{;Rqag#}jE4b_b>GQ~WmWU}F7t?0@8^68JLIAJZPMQ>anI!8lP|3?nmc{yz;GA4ybJ$y_P@Ny$}HX1^G^PTjhnOYxISuf zU>QeiLBDAx9Ym*QkA44-=VF~&i?lRYd-ClqbS6Kv&{^A}{+*AG>|`esrxb@#TRQeM zduQ|f?~SKINAyXUN=6JEA-Xx=q;Z|@WP59~9y*D$Rtn~9G8%jOoyPP%`5k62Si9(@ z_Atw4{RIPcokj$kyABMdr9atdZ}@8qho**2uYY*8cc9KSk!kMUdZuP(?VHy!v)}Z0 z_{iFu>92kZ+53mS?#*A0Yx;!x9qsur*Jrw^?wu(6#MUlr-z0An_(w&CTI8?)%e1Gh zf77?ke?8{Ib8j$ZNZ_KuHoIDDx9m-u_k=boEb8cq=VR=jub6s!%e6~uGpJ^tvFpFV z56*MUET?s|?=W~}+Vj0O0e7E1_+JEe$ub4fp)5boU z-ibRT^P+V8tT(&>;==lx)0{WFq0Q&%FJ341+`N|+Z`d?&{m8#p`5)V0H1&l?7q{l^ zteSNjsI_3I*{<=Ok4nrp2HU#YHal$F3>L`_FL}*a*8P9EH`m9IvuKOY>RTUoouS=3 zvvc>sZLF|f-RIrroiSQi>)in3sr5S#KHO2amrfT}*CV}eAGtm+^jwG~!$o@=ECaD3 z1KaI3XXlF-zD}RV*rfGw`+^TT3wgF1ByCNeS-Je4W3+GX_0&6mC3e++Y!GC9_Ga_W z_7m&5)H5HyFK+(=(-n1&t=!qVzGiT2(@^e>*zMuHTWoP@JZq`Z#Sg!38DePQw7XkO ztJ*WK>$V)9SZh$1`Vq?q(jKPNFb>XuHHcULHd7_COvK1=NHXx-FPbAVzkj=O%?`2ufJpZgES92UKcc16YTO?WN zu#R6Pi)(2oHRDu*tQ$nww*L?tj2a@+eu^RG`5}docs6x zJTLa;IlD7EbM5S0Gy9cXUtyXerU$mrq5}I|fxOr6yc3H7#R^$?Ca%>zjZn6RE79)R z9rNwNTUIGqA$@2@g#H)TN0TK^N+r7_40s_o(ZvS?dt;Sx2?1>;Rp?yu08UP@a}o`I z!_O7UP1u@GmucqfuC=e7c5CaV-b#8u4(}4w`X_h(xkeX3wwkz-$BSG;NXnv%d10&e z=U$=8X?SCWVky_EnGdCe(;&e3eD~jS%+W<|=GO%5(U9mQtBzGr*4|Efrvf)6>0CLg zWUIrr>=sYQ=4G4xq>ZiImuXUPY}IUzspa|L`GHIEFGN7Ec^RJ|t8Xs!?Pebhjr_hy zGFzPsp5mBj?8kMDt@DfCD@C;hyvPy>7niXA?0nLAB$UXe9=%lYiyxavyQdLBB9+xp z7F2xH_D}tnst9L&(@zJogb3TOg;7|TfEU#x|toHuf8a{6E)6!%)%Kd~UGf#F9XzDV!@ zI9}6v-7OLT>6vEt*U#Igiy+?Soc<49iZAeC-qK zo^WS0H2*!F%_mDGQ!_6}e$~DP-lK2^$kA~!rXQo-zHF~9q$FpmPK}HLCn-p+k`g{l z*CEBSsv_!XeG7u;;$`npPkci9Win#!SV6#A5cOGj(y6s8@#55a!ji4PNLKA9v{yqFT%fC2h%|%wC*018&)Am1k z{HtMaP=WS$GQnpb2U74p-155bC{L2!h;OYy2_qQGaVW1(2Mnln!rUPrv+>183+FM& z*z7YM-^Sd~!-|oAQ&u3q|BS?OoA8N4{3~dwCWf`EY4fzcDLue#kkn}3luJ-VyOri6 z5`^Qq(+h<2z2+hNM9urStfF=4ydS4^H6jTFu5J*P1rkCLZ#rXxo+~1+SA_?FAXZbWi zW6g6T;pLUE%}9&VXdf={1v}s#jfET$ThZ|T$neTk$(Y`%#Q8eB_&?xUHdsNSYH+C~ z3PT^Pk9QkJGF;Gokyp!CBc;<#8mutUm}L0Qg#$j#JnT1z;c8FWa#|HvWTQL;tRJa2 zheTUDFQ^zR8fS+-3H%O3-qAY)xe<*OJrg7fDyQPo*zR`&+-l`0nkMX<`834fw~;Cm z)fxJKEmTrMo!rHsGI~gmR#B>BX%(bHio)`sslE~F@kZx~Q;u__o8JFwUE67Dy~C2P z&m(L#(DK*>5w-?AVob$l9C_?%L)(b|lk0lEhEPYOSTLa zhN8MaZJU({y}AQWxr&^e03V-xiX*Q;}5U>^)$G2}I_HD^-cC7m5>wa+DWBvY7){{t8cc9ch zGAkS_6fCJi5Bt1s84xF=L~7kkKRHrAyrigiD@!BZX>!-RVZFBYd46NkUug(YdHJ6J zS-E4kJ(DZiit948DT*1^u}cQF>pLvS^9uxlR4gRJA|KmObc$YO25CydGyj{QfHFLz zjVx%xr1&klRWiHD44y(9zpGQw!22s=OBsgu@1l_Jj=uLbwwD!^c*++hypTbL9fzul z`afvx7-dkBS{5I57rE=IT=d+{pHd?0A4G)W>M{4Gu(z?U0Kd9ru@qRdhrLgaXh|Qc z;inWpLN|UMz%oC~G87h6YOa85OC_p%O8vJyIYRrHWH$K*?p$|n2@}?e@7&DEML7?< za8%umS@Z)M4*-k0&y_o_j84rCs%&niWqa)(?oX8oXioiFT1_=RE>2#=MxRqq@Qg0O zRk!1#YnBbPb99e_?$G0?L|a7c@!aO)@RP@16*KbY=gh-4LuKB-`!-e@igK9(paJ%J zgMW@N^_u9+wKYc+*6|3^t=9T>R`UdV?#W1bHS=r>-|kWpMTxPj=m`ISX7h(@rO>$6 zlc@)JFDBhexe0PC+a=T>rOndNyQ#X7HDX}Yr=h@MuNwf0spYF}SxR&>aMhrcTGO-k z^ckJU%dus>f|dhFy?M9sRK$C-H`L;xqIHmxNc$=92z{Ce>mec>v!vzTZ1+vo;yQ2N z?SLLdLlPeC+dK<&_;D6PJiPiO6giX_)X=5DtXg_&*mUwh<2t6bWw%KzW7S_t19~Mb zr`g_4v_;br)2m}Xyt`E_E1V+H{j>ZJgb@!VuJu&ve!epq&p$lyGhkHsVm;8%^q~W> zDI0uH6`&y)1YGo&5ok-cci`V!&22TUvAMGEl*9j_!L?paYeGFB0(v9cy{^InDE4-Y znj8CU^4-i>gK;pb*WR2?{h9( z`GK!>0yq1KkAPn_@~%1B(7O4>zLwW>M|rl*1ag#_&W^x=v0n&DMWwm$Is^E9fawF5 zc$^@82P8I!%`szW8jfhwY{6lLc?SW_$>|4?`vu-h&>3w)H~(_x4-9RU^R7U$h|8T7 zD~=hWLF^3O+V;F`&2=~ANWJw&U;F=?uv5{TDOy|XPIJtiu-ZAFy zGn1JAMJ%waqH4LNChZZ-VbTQp&D5%oD3farRL>)U{?IRCqe?MNZq?e*n#kb2^87ru zg!f;wQky{nyD7VTlNi3B1jOj{uLx(LWcsANbrhUvLOd zB{R+o=%n{@XpYLY>;bX?zS=I~GPfTMRN1l*0nWK>T^i2*O5#>+1>&b4q&8F=@3KRS z0u$u(eCoqaN|`5Q7IrCfC;qfC`DZ^^-p(OkehdI<4M!=R_UeJ#-K!sq^OD)lhT65> zLcnGPr}Sq1!ApDK4*(QrWC;vZ1{0ut6ZgkGL3I-gRn#1ULzWDUcBjcry6pSQrIBC+ z-A_vouz+9TNnhVxncW%5k>?%Dsvq>p%~vTgS~4w2y@i{-t$*nzQUCv1fd0M`L;wLk zA-SDDnHahbEFLNzXPTXop%Q&d#-Hg9j{7lNuNPWhrLF{t-2u}>X#yp`gMFV zI}DwE;E%0}$%EzF%dRnPKW4xNHA2wH`D&ml;ecD%Fv=o^{%_0#_Y2k575D+!u&cmN z+A=p(a&YQ9iMHpKodyjXyro5KSchq;^W1W*c=~IS>*#CIButTaXs};p0IkHXAgn1? zWWCkRIT?or2a~84xiv#7uWPbA@c=jOQ?l5;@c1}vQk%jc@{%FHw8a2{1FZ3c9bkGCe`HfFG1){hEA|@YkKZreSDhbz4U8ap`FGT)P2Yzy93U z2e~ugFTYx3?r(PzD$ZegS9z&lAHsSI_I=)3&w65*XCcmqpsE9Cl^KiYUh=f|hM__kQtm7#Rkjse8aSeX*PaKipFk{l$`X=IH#8^*b=)rZ=L~bNv zP8+~(+@8X=A-zRFp10JVtsS74P#tzTxM|r>VbRUc%Bo<(#iI?pfW{ z@Oo9}JkMOfr<~o6lgl)24QE0Ze>l`^yy+E(R6S_z#&j~fPS9dx0>2}Wtk!2#0(^da zqoYnp@qqhqar&9vYX672_^3$XetsGbp*7eziPEVD9BzU6%Y#=$AU6-^BEm#?wu793 z7`%1B8uay`Zyt1kWmRPjIfk@5|DXLDyuFyVTCuk*^`!*7LTDi(j^njo{byI}Q43}; zET7ka2-4Nan-xt-=FfJ)o23kA@MSc(@i1RAS9-lJJ8L)5Ih6X>0ii|56x?X!;GK@l zb(c`kUcaJ?f88{+2@MwKHv(q`J$;X!4QptrN5vn{4ML-aHOLspU38&5T~{v1Jte}l zH7PCiC*4H<4JWH374SR;%o*~oUrQxry{z9Lkh@A=NOTAH@zj2PIp8!|onm)a*=>a| za$Ip~aHp7&1GtPOfq8i`3m65D>^LHX868#l9uFB`Wy^r)z&yddo@7%))v6fLw=7Nv z(2{NaP3e+GIZ_j+ronvhlm2}0JRa>$aqmVn)NCTp$WMm5ra*6kjGgVk{U=*Zx{^$B zhIz8EPY76x8)!V+d6~9QToOq;vw|8UnzWkB4A4MlT9GAzQV+1a6g0^ zvzD)(bMb6)5cG^~_S^CnHKRf`H(lOFuz2^$%~WZgvuC%<@+KOP~EW5_OJ9t>8{s8gN>TX!X|f(nvd^YA%B>5gu0fO`H|Wdvr);OAE60%f%I-_~ z4}4#D#K?#Q?)5mBy^tp&8aWlU)pS!ro!D!ES9Y@oKGIlRaJkJ8kN<>%lQpXveto+H zd)(p-wVn}ep05*Lk@CJ-B($ysG_uq#6zGE4>yEb8I(qEh8Fr@?m+bRBJL%Hip`Ra5 z4DHgf83E}#JVv^$LqXOT{PJNt8d=-KVF=v*eW4)YIad`l92zVd9(kB#Cw)txw|((9 zLI1039sYmgM@_R?O3*oR;fWtFvwQn1Pt08@3es=u*uwXCj*?ojwTn-;kD*bz#-#-p zt9V2qYeSs!HoPGX!=n(m8{UaqpwM<$oEWTFSBvb{KVIIYD|y9mrS*YU*p^p6Y4f3` zsUc4c@p|811m>G4I{%Q^t6~yh{kP@cK^h8Iba}x>hz)O+(;`FTC=Kb#UyxxL`=gHw zl2Oo5wN{|KSntSTW?2WAD9Tw0EL%+oKje5ig$4b0SX22wE4jq`tedWrMEr>wt12_* z7NzB-b~8SoE$&fTdDc_!EdIbaK=GDU!CRsh4p9zpT7} zsH2OqYKQd|<~%UHZ;cdkT~DDI4xHq+XG-+B@e1FUIf{8aOdP+DV$clweqPgt()dcnFy)54A%n+ z<}H|_?Dg|DxxmLxhge5d<}>d%E(_Rp!AT9ln?%IjsLK%t!0(tJe~ z4LT~XGe@Hh_BP13eA~-t#Z*;2d`URqUsibWeio;l`;+ek!AOWf5t5}WV#JtiCN8L~ zgRD9c=nMDshW;6D`&+#?H1GW@BGFIpmlx8l0t&Q{{u%E3>z;-k%_MftKHL^g2Lj7n zV0AR~!dUR@Calfkb8sFEc-W`gDJ6%qL*iivF|U5#td{#?SBd~lwa!Z1RHFHbK{LvH z)lHcHGBN5r=?hM|lzsBX;{vI>W%Cp$%(OD(8}Q`ex8K_f7pE*q(66Bh%btDeTD!>pVed?R5F3^$~`RZ%!6ubeokffb!oKf*>RaBxV$NNLMVU@6O4A$m8b1nY6 z&(8@i?{!b{+{XhA@nwNN-!;^9%e>0zK!M14QJ_x36Vy(U=2jMK1iCXCF#0_0EfDk< z+)~R|r8%EcH3INyW?cOR@jmz>rQvldLj~SB56{*NH|=b1fvr{M)3{-tCI@T6N>z=r zyi^}Q=cSfQ0Q-;s=M_`sk5IC8b%t>|wHzBtcxrbA^~4bPC;yYtKTmm5%D)(L??8tB zCS?DOo|iVXABz`SyR|z$qkQ27#Ui0-?}48CR{>WT>B|?voDiJzx=YpoTS{V>lD%1)KK)SvVKwQQXwblm!EpR#r~3 z!Z-}maF(JEs#Ef2?dgNB^>g@XcI`z;iZPy+Y%>`2ybTH_^E(+W`@NYrbHHN|mc8Sv zuoVH%((BF4)aHJ>cQ$;`#^dSH9)n$qmO*4c!e;_It6JHPnM~07jl-Xy@qjob1UI$u z3;>;^L|?2f=KR$v1YI>FY8olAJ5J<%)yTwbb~U*T1Dt`nFWVUb_4pF~&JmGxI;z`J zZxaljbTDj7HPE@ye~&oKhhy`uZ^y&Hft9mS;8Tl(N&2=`-FdG9Kp|hupdjdvnr4!v zGZ(M#_yZbL>WUEo<@)aWe$!}uI&RA!*l~PW@5&20$oc~W@&L;HM|NLr^%&xd9KZ>0 zsw8WN+440l44SYneAM#JrGew&h;guO&<5=_D+PlF<$@B)FeY$rmyM_*AJFvham*9Bo;!`SHG=AjbkugKXcLnm}(B$)`M_ z8ns`aqc8VS4S(|AGaLrkE~Bi>=FgG2DVc{ zw%8X6hHT~RYPos~y5Hy(tSa~xW1K_PSZ?X+y+1)lR-uYEIm(LB-;lq93r&-=wtxGB z@}z9qy#ak{Xct4P!A(k6=Noh3f35rbY+^)5v!)s^jKLbZm&V{TMYD#&*%$b zu%17k_Tt}0X8Bfkr`BD0`{bdI&{2e85lV>TrOTb(Q*=4YSqNwhEYQZh`>!%UdDBIx*QS<+)yt@9yr7yZ*9fhLG0i z%rAQ?dQBUZZR>PWwM^tzipNpR=1?ZVi29DOHU#n8qA)YmuJ1k1l*4JiD?SJy3a;qF zJG|4u-AqYR>Eld2%(4Y!Sz&69{^e-D&vI{BzbCNkdMxr5A=(fiimMKXSC5&_m0my= zH*wuz;PNJaN@Z*|*E#CS*Hs3v(dKa6{*XPBt{|^G!5_;d>%=KF*pP_adAZp*R2U-&u;VE*ULLOqhmX6M zWm$YpQ)t55wDM?P(4~hdZ9z>K&f&;36f(j#?my`Zw`)tEpQ^qh`)*xh`-)`FujVz| z>u)>V+WVSJ+I11yHud&`wX+l5babO71ubc)>FOYLm7EgFn(^>I{G!$ z6-mCg0 z=f}@w;QLnuoa{1U)D!}0=*7GSyv zheBP3;G0unLtnunWE%jw*q_K9Tb!R#m7(OfD*v4B3U$e}i~RfLQ2@*pDA)5VAFJ0x z_SaD||A#i`HTS!fJ+x4GqZgK^yyL$uxWPwcS%D0<;Zw02Pf%8$>bn*Z=+eqo3N4!Y zwoC;8B&Cb3Pk8X-wbf={(vle{Wy57pC{0^xkll#CBW&L^=s5!gYWusN1Vf~H@9sk$ zAQ7nXyG)?AUvBi+yapp+Hl9~NHvrUbevi|AGFP$(KcwKPn7(nY%fvvJ^FXeG@lA6H zi+r+mu84W9#G(5!+c5p^s01Asp6I`+Hh;q(XDrCyl+VENp zo$B5qGOf)&5cVXoRfeC}T6ukmy~27V(paiI7+$YEcsI2>UZ(D-Gl<43rf_>1Ht9cH zAPTw4m$R5>;=vBu3bCqBFmFb4CNH6yON&_%=kePd_l~^{UtDbQ>9^gNi*D$5s=zpg3T#%vwN7OuWq9h69tq zo}8q~k33jj(xlT2d->5RbyFhpI@5q@=6cEhpi_D~<6o^RUjU9om3mu>2|E{W&wVf9 zQP1&58$z+0Sa&4Kr_?Q@Qo{!6`{0OC;FlFgO>7r%~Roh_b6nvkvcRmn%?p8Jm z6;Gxf@PZ?I@6DF*M64x9-;Qzthr~c_=yYU?ChI=TAPQuZoHyO>dT?*eKvk{;?2H3weQm_yxg|5H;dncj(OpQ>~{mZW3e?lMPkq&U6A?Fiwe<>T?x6BvAvT|Wx^_)23 zLPge;h3L6^k-90|%;gfHnI4&KMKQJW{9G-}_SH8{AEa-Bz>}`lS4aM@1FAGemKo#w zr0DoqMv~wgq_0BA0lwG%8%qJN0sqUPwbng2*Ob={lIXgB&b!;_$F;mQhDiJbA~i{w z&D?vS3|=+fIvn?J^fVe4J*aK4iS~zse_KRvEOuQ ze~(Bx>7D)xSwetjG~-Vu+bV8luCoc!QZl(ulgV+eRul+elLOv|kRQG8NPK9auCg_V ziOtjfGyzaE8jd^qgs2pY-N@biXp0fu8h9NhtJf?Qnm_oz4oOXX);BbiMJBfF|0_V_FF zVVk&v-_a64@oIaT8S*LMhE4EuIxUT?{_2J5c|HAt*1>s(8j|h7v;ndpk{_=5Hc=Mm zl=WWBh|vJmzr8JeL?hJJp_f?mhsXX8#^}D4&wmi_*ZFz+Q(w7->V&vLSOO1&`D{<8 z*EY1F2`7q#g8Jy{7WJ z3l!FGqh7S_zkBM9Vfl!JLr)dUan{F(6)o0j25iQ6J_z$N>@%nr-r`ub^Ilvm%JO{3 z;yHR9as671)suuS2qv@lhkbhQ_uOk)4WX1}5WMD50xSHTYD{|nABA=t{LtT638X$d zkBxd&i1@BKwB|U$8Z)+-q7Sa-ItG28E&k@Zi2@CY-SCUfYfG{>OJc8v3N;o*Rw2!S z9nj&^4KUDqZ)#WtmjbLHU>7bQ5kBFZlvJ7dze!PviP+y?H733-uC!66?p~0!AN;FW z4xwaaV-KJGc-3+S?SPuCEzxaI1m8Xkp3jV4_)*VunEiZg)J1TEEr5Q|qG)KSw&2by zh@@}ShzzolCAaeJ)B{UAp#2uBGx^qLcyW_3GQS(z=M=P0)5Q{0vTdR0-nV#1AQ9*R zBh~U?~2Gp{kVb^(Tw_fu+ivIszhDEh)fip*wgQr2H>jL zh>Hz@5+5z5_PpQw2K)Kg)|((S%fn4^cl=5Jen#dvY7}^Tw%P{tYQF=%wG(4RRlvpm zLh-_c4B7Nj9FHN8=mBCyrlu~0&m|g~BNSU(!jc}fJ{cyyeeOxoDf-Uwa^Kl`5RxnA zl6l>}oHRjXo8wYI^iPUx#AXDx;^7%}6Snz#VSV)-SRUwrhQ>B(`?PwZU)j9HIzl{(~P004c;M?Xn_kwzif5BDKwT1t8}sNQ;LBLE|G z@wFAE!h+lUt|k4>>c|G@NgBf?0KXI{0xsIcoH!8l?^@-YFE&_4PbWK^_s=wb?C)~s z1zu|iK7|_m;^h@8|XC zS;%4S&-r?ih$&PYy!Q+E9wPqmbwaN2FY^Np6>pXTh#fXy6cU-)fEuGHbM?|T5V;7a z#L_VPBBz2rEgtaPXhPItwBeiiB^h`7F-h;brq}!UzV;8L`0n%FzB<9I$UUOr^ByAr z@Ex9gOUA8soP2F>Kt|6vXTXE``2fJKN;msVcQA>+BNG39Jk`*=wRS-gm~&CqQTo4W z^#A~4D=Yn{Z4)WHwC^If<}&LxrP|kdF=lez$02 z^Zpa`z6a!jOK`V#xqUh3aJePO9M$}*4OZTWDy5{4(?^>PRoB`D0?&E3Iw1(I;aag^f@I7rK_xr`qZ2I(DMtOD%UPx*h74FwDQ{i1+l!;RoX zzehX^qan(A(QGjL`z4(k01XXpbJVykF61Hc`i6}iqO3SAk4pwSr`mgnBJ#w)d4IPu z)e|LQ9L#Nsp-s=^c^E}XO1$jrahYUJ@XS6pOb1^o$FW_@`tz;whY88coMixM_nCm@ z|INXL4FN_=b0w0=0~eph4_Q8rOE~`WR4A65B$Hn(iWvdwC{Sn7)%L;4RoqeSI}%2glWi^!%#%14TRly^0j`32G-ng!S}fq)i9LRP|;@6Xu@OBityyQz<7 z{qo0eDQc!_8)*wKHZ5vO@D$I-mVp>r+g%NcluoUv zzK2gUUU3rS7SZZWK{?wU?oAT{FIGNX=Td1dDzNNiFfP@cYU_r>9;Xv_rMqwqpA(c6%Pd>8I|spT1>bnxINc8?<)5O zKUQ>#vV5EY6B>MxDt5W;V>IEdnWUc0PFE#iR&a!=iv|F2=l**a04EaMpoQ2mb0U2Vh_QFE%|lAxZD%@ zT#wS0T*bxAR#0>9Y2&zC=|@>ExBH?Ztx9RMB_%IOCzcXfv`u-1o*sg!9vit%44vwi zn~pyJyp_DB&fZUFh~!fEq-JsbAl`hDzDeKN?$svVBVW%aLGD>ZqO-2!V;lP z;iFadb_2(nwjK+#T_GrkQen;Eqr6wf=h5C57(rfNVo?IfHY~`J?k3ul@hfC_k6Cvl z`=t9<8J<~tj>Oos8Qr5vz-3l#UkhoDP(t86Q6N$N3tYZv32rKdaztNG6DDZ(Ql~`7tcZ9KOpry6UPb z0haC}UI+K4p=-qAcC${JZay1NLeRhS4z`i}fR|X_ zhlWv4nq_9O-7IPgMbhEfkETRmKUwxSYsYhQK#-^x>MS2;_Dj~1OxCF)W_IcF@Wi3+ z8&9t5iD9}W{$=11r!a^`IJ7L_?P?di9Q-isNMUGFzbDSm*WULppO4$?@-qt8(wTw!35Dw;_B(*a>Z1u*4#O5`0{_;F=B(lOv7@usFO5HCAJ0D$5_ zurB<|>%x>Z^hC0bwGWyms1JX0{W)u;&Ua_2q&1LTwE3>*OgNK3Tno(()o{%)9ir4= z0uh$O-qRaDtpV}#Txti*td;N)i}1PAR^BU{Zf-Amh`|Sa(<%s#$Q~(+nY7MPuBr?} z_A41E@bR{SZa4cAD5mt?}333Dztpp6a|98 zDOWNDim-Ll2h?KfKX{|d_4X=pb%JJs&USU_9b*W zZFzB2QU1f9Vrb~+1dlo`X?B0~sKvEb;0^6f_mSa1ThFIZ*ZqYnor|mScTxC(XwT{0 zzoTS>iGlZD7^wD0>e?^?7TQRgr%4YcFaW@{8j7)rF#B*y(&b8s@EE;PJsAevr|yx6 zlA@Ada8wsXx^uap(Oz_-Py(&+BX5rTpXXab!`DZ?uIKlv=fx}iu<_|0#9*q?QRiDB zTBqA?@DM^v75G4B1srWM=j(7x0RX|j;D8Xr6_2<&aSsb~YRHfL+0Uj2RpIkgRN$yu zZ=;a<_n!s!=jWpUNdEDg3D2*)aDmT+Gu=)IXLXJgmtR(^(67bNMoDvqKAIN2?*Br0 z{rme-o~ZTItSHOV7vzo$3p>YSIDgx_C0(GH%rnRD38 zoLV62H}$u<8Xm;}fDH;(5m4O6i;UIL=-)P3Y3koBVY;1vUi3!E1i3F^w#QBI?h7zg zjolg}=Ev9TV^uo__4mdO({Cwzt<}uyl%Rb$y6Rc4ibSHOO3X{%8WM^ETGJjI~Lj6D1sG(K>&YEPP5jK{qa|0>~AMcu;~rumSivUpTL9nx-jrjHX|mmuFkHrYO6! zBwkw{;rPeFup;GfZSQPaNM{f!E;nz=RZoIU27PQ>9;TTV^&~XiZZ!qk>-{K1I(;4^ zGT_fAar+V^i~FlMs<=sn@*OR#lRf_wznw%V-c8Ez1Xu(259_o|wIU5>RIPi_W3Vw0 zO0W@RQf(50)H=WRI_d!gaPjbNhOm^J@O5ibuELy`5lvu{%qP9mvFp@N(^6DpK70?Q zE1IQN8@BY%gfq<7(&tMDy@~VVjTqB2@X!pAjEQ&%Q49?XT8C1U%EDGN4hoF z$2%>kE@(4S=sp)G>uNvrau0V1Jif`56#jq*e4(HS<5Q-E+(N#JL#WOX(i8t!`n|Ju z{>XnLxxSd;o}RBK(EFLcZ}`fDI&KgyTf##=m)05(a5szwepzWHOw_+6PwIm%NoHOK z=tq@MJSmfGU}b0{yyz%l)NkNZb)$Mh^9vt!>O8gc1aGg047w{RS1gRSIK@Gsu00F6 z+QbAptiet`@9+$H8@aOIH1Z_?vSculREuFN{Fh45 zPdy;As3xTcudm0-0h%57B`6HG(2D^MB;x8Nz6XZd4Ou&!9=34iYIpD2*g*GLf2n|0 zRYOI6e&`}aw{#NA-(ePmrM(C3nMnk&4uYg!1yF$1o~k)(52QOJSXT_ZCpe~)a;}F; z7%Diu?tq?`GpSNq%~N2D62^S6(UGb(Pxx0&7+#$4MHst6D9eme3r`XxUaQa`9&vKv zZl`r-)3xeMpw7JwHV==$#g~bdCZ=9|sn?eHv}LXAj1AB_ekb?&ZMv){{27yoBL!x$ zxMc8DqJypgPutY$!PsgrG4$s2ZgoFFfYc4@3Uk)-*wpK9L?moGiA(wI_>}v%;HyNh zo({CmmRQ*ms4<GlUgOb6rh-nL_QJ&?g)n{kwM-%D8eyd?Du;r=- zC;l$p;0Upu=xtD5+_>K0myz*Q@WJ4M3 z0&VM(V``1oC#iA$mW~2Q?NG@^y0AyUZZsjBOGW z*EA6XamL1I2X=#;wBxXJ=%f787OXn|Iw{AO@kzhrR54CL!voL`M z6j)&Skqv@~$Q?Lg70SZHVYHYAOW9dm(%U_+tS0D(4VUiYxpLLca@^O1pRmROPB5P;o@Re*l+{4QGFTY*60 zoyUS;mZf2#R9R*8RpwaLSl+n3p43Ung!IAMO~rg7T-vVs8YLY{HkX6BYste-96fh3 z=b4o)kTcO`YJgjj$KfOo|3EVCTifo*aM??~Qun}<;B!ZbgNfOgM3P0jc~a7}8fDr) ziERa+lUT_O>Vt}`+kYyl?%ITq7R%F1HK3p3Ct!@%lPm5@yVpPcha$=KT8&wl#w zBtFFLL!&@9g>Z1F?Z=YCsufykO*@fQNlaQ($^5-8u_U`X`B7wm--P2{>>5zR&Jket zFEq{P8858BWube)a4(>$ckb%9oV5k_f_z((Cas|-dmVE>H?G6Q1{vLK_znseAxc(m zE;W;wMGKpwOX+E1M4Iwaxh4U^S@O^?BZSd37FtN9Z4Pd($KX@x0~z|^Bon+IH_uq` zy#gH8oMkd8u$Wo*EL;xNq&e_(fSpF<@AFR*`D$0&iNmw6|Cxk|34|+I7SO|a+`P`s z{;(1T+hqdNK>Igq&M!)(>ZXt8j9x;|6vqa(IwWeSl7C+&UYWb67Xn^4UuDnQ?&OWw zqYYvOI<~g{vKg#Sf#3tpM7o~$HeS0LVVrEs=dI8IY_U2jnYHB|Mo%hY+9*{xY?#Ym zUZ~Q1Sid6zdu~VGD?RPpT>kkl`8*%}#vYC6hAatXNl4x?^7VK9M!PmF$X>&MUSajkAKnBOagJ<8aEwD(gy~n6Z|H2Tiw6 z$i{WMre7jLR}N)6fi>)aZNeX{inIB+uC7ME$H&zZY%3%aUJc!+o(8kPPIXdXCzv?C(;KdG9Tk14q zHoMa<>D`nZ$aQe*`N|21D8u$`gaagtoh>>wuIsP0-Ws`KG)S}zwV&Q&8VijoB4gAg zV2T)obZ{oByj7RN+}tbvN&9zw&eZRVR6@Fcr{@wYLJkv)ay57_XxaX#b58B~Ri@J< z9XriAttah1YnSyK@9FkK_T{<{vWl`&R{=BZO4R7xvFsK%hnMnD#+65N!9_WAZ9eOOZrtI!Ys`S~L? zs&de9ZQ;V?Z~*(VNbvz_%L3$`dvwOl$nV{M!IaqP{ljN{eQao}*}o#U&B!KuDN|#^ ziqpZjIgrO3a86=V@XZRK9ro+DV0 z0PtUauW#4GKK=YcfsYzwjQeG^|C=Z)0&A4EDC;hOLOZJ8)WX>uNG#~7%*PAI zYq?h)3TEs9Gx#om99QV0DEKd7Uo7D1qXFn)LFmSmE??3#l94e8r1Sx)DGlqa=5~D3 z?&-R%GKKW>GRcjKY7=yFcFCXuQ-!LBuHi=*+doijda!-h7_83;-GuOy&{Hv3QN!~m z#YGOM19Vgu;tHxGm8wk(b#XE@WrCW?2xNYi3=39i7Elm zBRgDHe*sN3L3MOPJ#76<@-#C903*p&5xM6HF6i4<1GMi%llV;XEpk5rBx1@2rRNwm zU+xJ1Z2KHPSNqDccSNwf#ZZ=LVY`F*OkYPV{u?t5D1>h76qdbHqVWo6l8 z-07!&peKW?*r%?POK)n7G{P^&32DhEWL?t9l1)1MjkXg4Hwo^P~qsAm5!$gpQ zOD>z`5a;9a7w(%7zu;}=0vl=Mgr_~OnSgN03Z$Vy%wm83!HN2z7Giovul$*nwj|6o zg2ttj)@m6aNJheEWxPtAP_>MKS=4#|6z81DvLACC^D1_ICo}~RAgPN&n8P7&%P_O2 zSRU-NPc*1o-fl|cakGjv&fuw9VcmKPIF}1R=Su3WliM~*0b}H-u(PfdlZy-ZocnD_ zkoqp7m;y%V)Hw#2E10K6iG{vZ=(%^LFEtUnUa1hCH+Drxeu1`c zSIry2J!!ol{6O=R$&(M;mlAARw zRdRBuS?#PR=i9vo7^D5hh20`OG=QF;nO%lyVU|NH_3EeK^tYxSvgbQ5A9I1>JS*VR zXXzuV8Zkoq8Z&Db-)s7^dspFQXAf#=$=bO6vZ_2^6f3b%sTAA|N#_6mvJCwc%#GW) z6xNfI1{XX4Y4M+8RU!rf|9692AoyP(;C~wA-n-4e{+~l};0C!HD+#6}-o%ev@oU^i zS?!CJm7X4HMB{*kWvJL3De{73zGR!QJWQAI*gQdXHWcT-3hQH((x4kZ$8-oY<@oqX#r-zvoqbz6h#W@TOlb35@{k7!9i=)%eT#x zZP}|+=TGtH-4X1rC&UjGeU9Kz3U{GGKrBVX6Gycu_X|j#b~mHO{pu*&Q+~+VSHJaX zBf|cMFyj3n0dUh45-TDSPcMW3kiPA0ojUumeY=MFl#u76G@cz;Sy>JTus?YwUqY!S z1Z~Jedf^#(KfR7Dt!5#i1GYuwH{P6GQCu%ypC*X}yS#4W47odsNkH2nk}>&{WdI$1 zBK)3hNo(Ns4*)=eX-WQbGHGx4#_jIdc-VjEBA^d@C&Ga}=S={ahJa$^jRM+_!u_iA z#QED3f5cYMj}G1S<|f80%*Xt^PfHEo+mci}`9IIO#Q+x+St!6Uip!0G*7hn-PcKD3 z7J%!V3kqOgFB&w1A$Jr4{*8lT$dj?X{k-*84KMfUi$pi7LQ6E@yzlZRB)p6qz^hU6_QGr$0^@d(aQptenyySNh-%_RYVD&$3|u9t~~8hhwV zWcVjUxGQGQ*Z|@94p|xi#Yns3lJ#*3W#>zz&s}Ri+e>9-gxkrp@e<0C{nN;q8#=qkJc7-=FaS{D${LKzMLd8{xAVN$ zg}iHvw$!wnp`&&5ty+6!4c0=3&UrzKrMz%dHth%P3DLNzmfV4XD@p&Yg3q<6A6U9g z0yK_ceBgD`+n>OEgIE!;RQUf%C4ZH*6ydk-sOI|?`RS6|?c#>KGnf76VdKnA1>9~1 z#icg&tK0MhbGPm`ThztRA45b~uEg3(e`d@WXbNLh!)$HB(wN2M2ZF?c27=Zx$i85Z zVxTt#NoApxijxg2Hd{nOT=C->=R928xFO@*73G7es(YLbv$N~~8F5QIK&6-wCIlcB z=bJ_-ZXERCm)8)lVc2)9+ zie%bqvEDSUjqEW4SK5$+-1Bv=EdJ-2@0v_Y2MFtF-&>xOFcISSH~yPQo16URPp;W3 z-7giNt2^Iyd>sgzsYD4X&kbCs2aU807AI;7lWn^GFwC1Ie>bZoB_^bqu*f7O)}B@T z;tHp#vfoIBg#|rWkVmB^M+#FnX$0rbg^??c6bFYMXGB6F#vU4q$3jBpA;#zP1)bCl zst_P51c1)je!lR8ulIumT$A(xk${_l%wvQk7~-K^M4el3u+dSW%!6*2C|sm3Z-C`F11 z?A>64X*Nf^M1hvrD?Cal_dO<1~Oc@h$&Kpc7M1eN>R)-laXPN%M{>{lLk``NeLSXA+QYK7)QcM+QI6JLtz6a z@}C6A6cj3By429wP;3{5+N_Mkf2b%EexKY+w5>Q=61O|y5=@QI~mvn*dA)Qds6v^clW&gb9HUQGQeK;Y zH}9+BqP>qJ!*^8KM#lRZN-y!dC^e6nj*uIHt^6O-c;5g;oOoy>DH&ySfSedT25wj? z99$Rh^Y5+5tm=Kgu3mueL=szKVT^R^1IvXD74+kIt z!eQ*r@CM;P3z7K2$ppFGOG(~AC`#ptg_2d*l&iJHCB}YI0Dq}6|@cmSfY;?qN&5Ld} zM`*Gm8ZD_OZ%#qo+?w9;XMXD*$UbUaZh8IB&n ziq1nuX2OmE9rup}u<<5GkkG!BsFf#$2~Rx>755d&%fdK{Bnq`F9Kt-;^2qkfp}=b^ zR9GmEkP+qp2}E8flrTKTw@?E9JcC%AfB_u9%o{Z+41DOZaVP}oE8y680}6288?p_} zZwNz!0OqpZgC*{A(H(q?l#XrqrhFSJvomW-r*2!{Xasm!mb$WX8 z{v___;2_F(dg+<$JC*o3XWinXbj`_ZFD`0TN{|e8py>!5g`t2x4mO)<=6Grn1sU^- zN@|Eg%y8gfxG*N2(m;g><`22>(D@m1T69IJ5;&<`5eea7IC_fkxU@9HH5O6Z`)Q5u zBm*dP3lIS!G?TG4LIX(XTV6!n)MScf)U4RV=EA!aFlpk%RJLnG4bID!r)KSytKMO| zfkA$udLnSZW0vrkfX!hvw45kKFQl1aG=7nLQZdxLRX}e(>L#RzO*D}=FDly15<35a zZLD8cJdZ*}p9=H1oeGN?&iyC*WnkFp>aUK?)D~T$(c_`$p2Zoj;X^lNM!gPd`PE?WQR>{#sl$BEVCdY-pUjvH_ z9ZM6748Ew1aU=sAJLv$&^D?XO7cGmOqKE`tiNXZ|;O-(l*2#_P>^w56$hL6$Wz85Foy>=GT1sau%9IHrCA(c>4 zk?qx;Td0F)ju3crVk>t;+w8SS17Y$TDQBcCH0mXwAP4@SBUx5L3^K=}!{mp4M@t=F znZKagtSNjXTAA`HH)PD|ZK#Y{Iu*>OwF1vOO$HuTDt zYg?Lk-PWQDM%|I{;uLHm)`spZHu4&7XLb3rv&HfiC;_C1zCO{s4qLy~pE zDhupy&d$vH3}co#H@ci-(##{7Ok4hllwiYQU!^t*3{pea<}d;Dvc^NhluO|zoEXkb zOyZeO;#vQ!FFJ5C18s+~OnU9rHEo0OB1UfL!K7P0GBH2W8?$ZsALD^Z6 z$O)6U8~lfH$7tKn@ttE&Uq9BYU#(rSytObGTMdw9l(8|-4-2wAPy5ncliHK?h@hWzpX5k<(H zdO336+daW@*_;RwrxoF6Mt)!;m=XTm@{Rr29+xHO<=MHci}Q?Z3HHpgo_qk z2G%czD^vOQ^{geeOi{7Uo;~@mB!Ao#)Gs3Lq#u?-coT>{h$&}=X{Ab{`~0fC@U5Uq zSKBJ{Q+SLj%G1~(jJ{B+;-4-zp{*(eZrP!Vc65F5y6k@-&VZXsA=e3Nscj`yk1>ay z1*A9RK5RG#?VjIYf@tToOfu*e*a!=Q*tKVER%)95 z{HUpj{2uugi`(b`GgovlI*d(iU*q=*VbYWd;a#NjqgVQ({3Rust#)&@O8sHT+{enH zmy&H`uR4Y)ihzoM$bq)LI1`pgMLLPIO-5U~^d2dPP6%aeLqcci)1CKLNp#FZWx`=E z=M;k1jROL3#0c!E^rM-5zAv(Fp!yG@iT*A;0{NRW9ZOS(N`|y?g3J@dIJRVZnv2*O z%Lx>CHj437>bXC6lQ|vJCx_V$t?n@3*!pFiqP2RvAHQDup>MM?u-m5mHR{;#(l`~C zm^O^nPPD{`upCY)xU96Ca*-nU@rRAsM1}2PZe^}*ecBW663z4b&`pI9`p)h`OKX}3 zbo`(U1j$Rz&IjXG;LXkIXU60sgM}^YB3_cBsTq4+Bey1uLqX)68uxB~Ky@gaN^I?J z9xpZ1e&Ji{*hNSfwQg2Q2mWq`eSW&?)<&oEb!J;xGesZ$Gjj^Hqh()5lv1O!Hgp&k zM{U_^8OjeEW_x~1Gh1HphC_s%3SSmh3s#c)P4Mc_!&*I1zm=K>j)J%V?0BdMU9F{8 z7U>{OKF{AWe1=7AlGbmuXPJL5v{4pu&4cpl(`p0$;+7N8jEB;g?;>FxR#0Cd^Agjx zWyY+^`KE@}i4)u3N?~jT;oq&iw}-6NcbAwgRHF5dzcHt9jIWX09q;i;{e|69d7xjc z48ki{m`c}_75@IV_C>$`Y0&4)khm|3lL@sz7Cr%03Y==bon-y`oH)AF>3dStPEgOX zxnhqYtT{zWVzSyuh*oQ56|reFwRSYdl97<%wRM)jaWqP*--!&nby#klJ~8VyaC~A% zFUrgIiZxnmsGdYE>t5dwcy6lx5DqmwTw{(AHnY9H+ukEsV@4gW2ioqpRhlDDt(u{F zuWd$l!21E5Gs-5L|yQjCWY9+?KUon4WvO^*Jz0+|+ zkul;ILhYy-8e>9o^9q_d3Da~INi}Y|(I~dS-j=u03a4zNB*rD#ukvE)9*bUA_O7o{ zBb)by+x_Fi%n_C2dC!{K!|oOt?iR*arwZT0%j}u$@NOnH-*Z$$nru;iy__ZBB=eAV zbhz7P3xOT`jT9W~I-QOpK|i9rm+ zzaLaEt7{#Eb4m8)wa>fPoxA$Xm>rUbmVEUrk#_LyLe{SDe`$W)Tn3q~J#TF9jW_#z z?aUSrtDoMjc>DqFoZ8&0UT?N(kmKQNK1*A=RvGq$tNyvzsC|>;%A?cXL^pMbvb%r@ z75}6GwwovP7TR%MACo{{GIB<|h|QCMl#jg%Hg|F672+arpYSsJkW< z#|qK!QT2G{I>h5t3{7P>354CM!X;0n1ozunB-cez!AfXSK8)bJ>}rws>nHFr>4cF+ zdr`|=7kP9!+8D9oFWY%6xA{bCEiF}%2+P-hI`>1JsIG5=Oe^Dj&;ZM+kNw=QUhC;Z zlUi*ZioyV|Tb9+ioXN@j(PpC}oG7TS$xtCd!q^KWQB zpGdqOR;$i0QH-?kq&6$T6Dxa38v(ST{NakwJsaJKPFOjDP)^8e%dPBGd`%|~&FrG_ z+q_{NU(xQdS26pzRLX2D<2p$H$7yz!Qlwc)jv9A z{?JiXY#SAR=F+sa*>?GXq@{n={q+|Uasf%~|E$IcmcM=dTwOGgJbGaW?*3vsjP+H+ zlqz`3zBe49vPNMPbF^&7p!EqcQ|}Yk=#=Rdkg)$jNmiTneQRq=;Jv>;a3tp2zXIES z_QeZ!snl0LWx?_z@Pu*MRCBhlzJEdm0`^*3Ta(hE6S6hIR+m4^YQOEp%Ha*(svA{n zoo{?BT;S&2<2fw2Hcw_VP^A`yrlU<%3pToJ>8nB->eL_Mxm1?2!1i>LuEXiNSwej* z0{OUFU-8x^iC%qBa9prA{62;Jxpr)Bt4av%zlZCenekS6d)Ks^g*s=wV@gfd14dK# zhKcKgF)Dn7LbTY9|55gS7K0}z2dBMjjjSuSk9ysxg0(uHaZ+tb1KWx=f^P_>b0mj& zaf<$N)Tn+_>x$4oLsvEh0L`n#*^T*QXfrBM@Gff0E-+h+m@3z{xC?%+>9Kx|4LEDm z$jNO}qdKu@GHkB%!i>CgFtPRq*I{bj3-yMMaRbqK^t zdcfZpQ*U$%_+F8z$Rk?p;H-GH>x97>6kYqeo`*^=O{BsyFtTs(_@K?{ z`bnYo`^p8aq}&BzhZ>HZVLbh%0;8`it!bk2>Ec|_^M-yRN5NgbJfL#2;rlX{wpsA^*2!4aDore#7G^D1A!*CT6U@%{ zkzR+VD;#ryhGR(C_z-cV->fUy-XDumnbnfSPiu?vMjJo9kiO^>!$SCJ@;|Ba^Hvx~ zIeWLv5j8bE+~MbLR`nw`Mp>fz?_U%EV%IOa&fdW|G>GBoI#Q}+>Y?%2;1uY*%9C-1 zw4k`t{0GsctzYUJE^YhAt&VG{IBe2m80^8ntinD>gP#rQK z8YvN*mS`Ao6L68axpldW5lpBX7r)`Fy0pEVfWw2Ut|1c;!0_4=zpsetwa&es@wxLD zl_?lw%V*IkcyG&f@0{A;{N2`z7qM}?tf|Tj);X%u%B`WoD!aTyrM8>@{L_u@ zDnX2xk)i)n9g%Nn7NckHq@|tsw_}I@r(0XMZ&Nh^7olhH1uDWA^nR2*#2ZsltQjT0aDiRBp`-#Bj2{yLZm#2Urzi@U)i&-sn zT1v?AV<|c^7*ePqOLb<2Xyaubt*;s|E%(9Iy@hls%nXQpdb6CbHx3SqzOJsmPfjVe zSL{!=<@KNOlxH@Z&__;G#)0C8D!CP>XpZXmy77IN_UmtMcJm)^6>c@hfim&KnV|z# zi(cku3m049dP&V=e4-$1x)|4OmwV30>!1YSlGyD6~g1l;aN%} z(GtaPp&uimzckmnPD8h}C8a|Vlb>X~9ebW>f(r8~yEjjB9S*`zEzB|V#@ZHZ8(Y7( zP5*RlI8b77T~S)XdKq=Iy?(0e^8wEC_{Q^12cT-VbE5iO85~^)H{HPIM!`DI5ezl) zM^?&F+VVz(Ja}nMl%M?CAb530R9#$LV}jORdtPA2zt)gq1H?2X)PF|Zz*M0wj#St& z4?=pIX`Xy;H`KO{hVzY0Ff*~{w-+(buzZ*jsUF zCuBPf-G5)a-yYTVc>~cO{!?~*HZmPiV&G)4t=X-|>N*)vLTpe-4iEQxT%BBym{@2q zYi#}SieB&m+>U}Ea`*i{Zp-t#c#FdUDqvfd28vHa3E+0W?zrgdUGvj?3AtLlFl0>T z305?}J$Q@|tI4q;O$}R&mK9MpuKth9v5EK8 zOAhMM8^2sCPm_jH1qnklRr*VuIhawfob@M*-~pz?WgXxC1W`Xwu;mu^x-cItHlT-` z1lmt>?Bq{=diTi|TCFR4i7=Yvkx!;hjrLnm?Ht=Mr~ z1sh#{y=3E>GYWacQH_uXhTi67`1T#ROmlojQHzL`#HLA;vXHTm)2K-Wm9X`sJJN^X zrq=zzetLawm;Z#z>h@q^fafEd<-DG;SI^(dY=2ytacBHf@efA1ZT)bO`@rTGqAs(N zW#lRCux`Hdl!=-1%l$ENOP`O;3~T~;hmJ1Dt>$g$vwH)KE}7HM-djN*i%YN<9sCvP zTsMl~wVbS})3U7fvh+@m#Y5(YiAG*mTsh#0g$2A0Sc)GlB8PaDv@hbE zx^uepk3U9`HJ7nZ-bc=P+qzq1;%Zt;^f&gJt7qYS6g8dB=b=(c$3GFHuXcUrO_CH&pFN@d>s<~D}Mg>w8zaBNxKGm@3$42*K#4?`ns)o#?C#B zW71Z^g;rT15SLMN?==nIWJ3pQe#Buxb?E*_73@%1T)m6ReVwEA(GaL%Zp`=F_m^)Y zdy8=OzPt z`cEuILAM9IQ9cte!1BC0?g~NG=(2{B!JVak0k#Ubo+y!yYqgcmof`XQL&!Pyq+0$$|mIAT=Zrnng!26-pX2kcClR zWS%-=CbBaGtFr|hkL+8E$9(r@VMQoQFhH)^fj_ z&EvZW);j!p=QeiII+KszXF!YbfUH}8EZ6jggo2Mt)qwL4{g*%%423e_emsCzQn2^U zpT=u%gzbdZKLlJYqJ|_qd@>0(i;s=rv(uw$iWiPu;@V2Ko-trg;_4o+Cso<&9@*&g z65(1)44oXH{ImV~f4BgRY-6C3?)8rlvE#4a%+g?hRM7S=BfumEXCvVP93uY){)OYB z16DAAfJKN{ZwN_QApbF;PH=uZqMnQ_5Y-xk!;K;#A?4S5Af#QV*U9os>Ot3U+*Gg}LjAxY@Ei4QT z4Sj|dGLL+q)!%ExpMm@@x2r@`Rfp+b&OSsUuqW`M<=OYdq%%8tlA4$Lp(SRqb^M_7 z*RT4He724Hv{T-_KVaA7u(CV|E9~aix&ZOeoie-%fnn#FgluhGhq~=$rOm2J)DMnA z&JPm}j+zV-LQ4n*jsxl7pp&0o67KGu?HWu7-q^s7N?n_x|Tf7hXlo zL&;oiQHE(+y?EBjtl7Gw`{I%s@{eB62Lf@xNFEwEdY;! zy1FPI8=4v#iz{u(K29Q&6NdHmudp%wxB;TTPO+;lts6ASgouEG(r-8B%!((^>Svq! zUT=J?sF0$D#jm0%nw5y!`-QA+c8iRQkJwO*PBtIBE4OgSj%@L0D|&hgu->a(DRSO-r1SF4Ca)gMEGC%}xV`ovRJ zr_?kLps$ocrI`jm#H4B-M&m7N1!?em{VEl|&2FO+-pJb!rou z4@m*8AM9|ZS@#5T2ZFl0li|RIO^GhC?UE_uRQ$G>Fkl0TT|C8vm%B2d(9%s%B|9jqxs@wwb$gq zBYA{CD1|f;u~=eC=WfEd%EXyV318?*pj> zr_q54_gS?B!18 z&TR%Zj$*_Z&R=&&VAo+TTmG>63z-DkEcoD64lIxzJ(hvJFhHWH$23U(x!!J*om$fm z{F1Ylm8)Yjf&L3pJITVao9X?`Su@Clqe(0^#U&RxeQsX%pHYbdZ3A{Kv!E$77VUds zFyY0GO)dNMK7#5@^G*v2o+7d*fEogx0S!(l(xCTW3mpb`--=UkPhTORhRY3?aqrME zgA0dG);t6GGCQp7X!-tCbnPnHeUp20oyyfaAuX4+!z4pw6J~}83qJgIc3uz*q z3mwaj!t<&ti7d;LJ^F8VZ%<~hHGccfz{Y|tkw8M$HZEYxsyt5h@SOq*vo{AW7E{Q8 zB$C8p;7=}fjQ!!dP?HkeGnjpJ-OEk$slG!yH;e0RrC?=Pm-#J)J0CG6qiO_9&f}f_ z8P%XBD{GyvucJl$u1qH5dLtXZpKDa%Lw03iU4Qb91%*K-Mgo8alDr-xKzy54fL>}g zw|7GI=k~f#LVzF+0*@WX?d8^V9fueTba{fTcln(MKgM>&f84C9hH$Qt@~nLwrGkb0 zY<%|ii!8{{cl|>Wyh`Q`028$YQBCJtj=##Q-o2Vaz(a-lLG=JQXv0_n%)UXwIy9*F zT;Rihq_ns@D=WddfeQL=Kl|6T-}HjZJAQivX510l0xC}8+1lvk;*y4bPiYZZREDgU z6fV^7`KG$o>+9WbCx9)%V7zdmEMN7G&8gq@&`mIUFHR>V6-Nu@q+-CEnXeS7V|h*o za672p-W}Y6pX$G((2ooCgDYjZ=1(yLZ0dt$JJ{2HiFV&h8D5X%@mPi!Yn=?|JHyFu zcQ1HveUc_Z%p*3Ri1N1F#|pX2x?$@!}E}=iXL8C zUkk1*0>o7|CScQ(%JrU?=Ch_8=2y;`-joA`yC!Dvq)s!nE^#GZ>v|M;6NU@?8wyS4j}1|ts9`D=3_so~O}g!!=mFS-rLz7^*)Di4 z*PB|c8Xe7}mdkC|YVE4?$VtL$?GyNY`aXK3c{7?foCWq+A5pRI8YhC1a%N`_nSzEG zLQqim%Xoe+t2q}Bzmd?JwgHR2Hhf4~!C-P6k&=9mCl$2c)8a@2C?mqRdTnI-6h(XC zyViqk5v*2u9Nk9igCkwGxp=-fssK2I1O|l86F+^zv!rEI+kkR~htLQDZWVw4KUhEb z$XrBw6oZydGG1(7NhRa$|9LP<*G3xXV%H9FVS;t7IXOh@OmU_WTWzw^D=W?8e=P_! zaMk#P6xyy_asl;72&n-8D6IPALa@;yfS#{%1}(@Zr+!Zgrx0#-1h%(qn?2>&{+w;# z`mE^uq2j`Y)z-O~T4l+s$*vOjbUkuZ_-1}|u5S<9{4wU{QJr95V8@KM3pQM z6ZDb?n+>)g35%NGjzb2x4V|9|COXC-MSz1tSk6QS{uWm| z)ZMe#ZosoDPT~1D?>k*dn)+gBJ`*Sp&d*|T%_ptO7i<K3(Y4j*V*M&T{@ zAxEjo4(9N6yO_N+hn2SfuK-9DY)1`;T!Iz95d6^c(c*OD~j@O(n* z2hjV(U}O@MEE;i74P<<^a6Zwwlq>#Muf)CP3QmJWWvl*WY48euGN}FKcmV_O%StZY zc?iI8cJjj|h&qTYfCNYo+$nY)&=gr-OQnul)i<>GN2kLBy{ackJm$kc1=u5hBSOd73dq@1dYrjd(FVN~}vb+=$+b0Bm*y-z6bo=8u@=6eJKRS0g|x(|-f z<5}4Xgistw9@6l#2l_XRBVoL>&}Ka#?8{Jsxxw8jDC-jc`b$A3z_-rx*CpHga_pP= zYq+;{Te+@v$6wk%Qf1de^lnhzz6=a#Xe7?c=UKBde zgupVzX1H52M9aY;m6;@RN9g|5WzHMY2LT+&soIa4x<0%rsF%zd=A9Q$`|xzzbRHt> zA*~<&X>>ZgmM$>i6lZJP_kd#u5mu;_Ce@og+)jG+9;pl(M7Rh6sTll#;&57t$cSvU zgoe+nV0^XitGZ>3pKC0zhnII{*R_0kx6!l^=2yj7m$O7UI1e2*)0F3BtC5HSC)#J& zYT2%>-exn9|9R*<3E5HRdB5u1J9UGSI2;f?Pb9oI1V=MMa2}Wzy7n1Yzz^I6Zw1(0 zWm$h%o+5|AB3Zh?ufdUGY&! zJu#d?ARM|M8MzoR96~!cJ0pseLjy};QXT)o^4KvD8q zAb?3tk&;k7<}JXq4}_+-k^^qhNZQT=h6S(J8lLEIF=YFA`; zexW+iWmVO`Zf7IX;&D@^)8(Z}@&Nf4j7yc5>>s^bMLUmw)g`}FCjUvkjeTk}-I1AN zqr^{*Oo?3F+gaVVi&T5-4hyxE_~yQ~F!vSZ*Q5dH74y*!{55o6Fg^8zpL(&xENGFF z`Ih}Y=mS##S69XTITdh*_wVg14>@dSBhZ76`(6$4II`|Oe5A}i{6EbYCY#xH7dls6KM79JC215$YTlB1@`7ty=v4{igdfl39H9@{p1-$cbV-j z0pS|OIAdxm$c7+WX(O+RXOaIW)90wqzVJzh_H)FO2F5>A9d&0>_O0e2-+S5p0S!FZ z?d>6?GU@tMmKV6WOoSrd1>R5YMW~H~eCBCwI)f80?4cu1W`QlpHR*b|5zk?)$NaHk zNzl!NdNZgp2TJ4%&XTFA*9=%K2`_gvw~D)&By*!sMThReGlE6!Ii{> zSZ|dnkA`ohrqS2s@2jr3cs)nX{=K}(CbU=iyZa1=SFRX9`6UzLHq$EG)H-69FKb;$- zzzhoYBH-}iwUIRkUwkce;QGhP*`OA4_p{06bR-G%7rC0jV|%3Ev`)A8ETeh*0lR7E z@X2-O^~r@)pkVN`GZvM;f}br&nV7J*G=f~i+YGM4a;9H~2ux=ErMvdB#H;3QJkb5!jD)Cov^g-`;tAVWZ9wU?jB2XgeYiCWt- zx$-M(mA~0M{4+C?=Iy*42ySFr9`~{UPhcXiH%{~GJ;8Q}YA;VjJ>Q+La!p(A2p_+T z>HAOTAh|wnzHxyN>1rP1(9w$ZRY!OuSkyT6Heaz}+Y_!{-0K%O9ISbG*YE0E^sk8I zj_P0DquMio%7WlS0QT~8#ArR5jVz3b+2+5T$}(8iSm}N+F|8|T>#<5OWqUY05Fc%v z3u;`qB*3a50hhYlk(oYEklkLGHvKXLmv!%BiL6#>xb8++be=P;?LXPd6{73KQEA+T z2GLygKYAZPwdsr1i_Y~6<7F3R@f)uO9$c9Va~__>9v77CgHB7n&aP-o0*9m|4}hMG z<`>gHcyip6m1dK=LxkF?RWogNk$6bzu-T*SE7kNK<0FBlP3rxqpU=*2H}X~ezllOFM&tohSgn9+jv4C${>|Nw+hCS~d~Ao6|^OP%2FM(NMR~MF=Oz z6lq4%+6$(V$_c_>g#SuxVGVtb?Uw$$Po4O#sAss-@gpnq^VOVlo_UE9Khh?yo$PFN z*0jjHg3u~oVlcOUPy)>R4}K#9sG6T1KEHVV+NdE+^l)ZoZ*$VT)RDBlY+Xl68Wp%FGrbHbb~EpdvBhyx}nNfzb&PCR%o5COmNymB{X&r{XO@^{@PwsPlWT zqg<{1TPx1}FV7vce{2ZK#t++=-4~tH@Tjf-fS2*Wi6%%-(X6}SSeS~q0<570&=b~k zHz-3eMDSzD7n#$yCZ*_!7~}2(Pwz8=bDQZi2HWsV@@-oN@}toRJkbS&LQC#gc?H3{ zt?EYiQW$v(Xz();sOL1h4Uq?R;mz-udhWOQ_7A5;d_P@Lya*tBW*$EGkaZZFTEJUt ze_B}x3kk7{j*)JaG#8qOkdu^UKMFUgVD~NsB?g8MLw0{ZjW%!k3BF@9^<05meIW$j zU%#hbEJP3~-8zN~8@wGrw!AK=Z~v#j0XSwJW#HNHjr!JJNGea2ec;LGc?dOu7hD`V zBm|5K4uGXhGxojm0}vBDe4%PzcL_Q-$g~KXH~0ux6t(`5cHmxoo;rRXIU{OnS*rZi zTz!(>J34eWV3(Zn6>bZ(x3u&0xn|eL`PqEc`#1(`sS849TPxow1jEqQtDt_+3216F zV@T3L_h@5^`-rjeKHwKUq`{y1c(#2cnpp~Q=>qC#j)i(&M8t4)UE8r0;sm!EbK^z}t868nFG5ah%>Frzl=xRHoEm=V>Tt~bVO=JCl_ZK1q;6rkpqFZA?YW6>g1;L##LSI#;VOs)}Wz{J;v6@ zGNadvAb$|{6>sFJiLv_R9lfuP8Zps-gSwejrb0^rb~6fRh=>sk^cIb3IFtOSFXN*4 z7i$)qsGOooXpX7i!d%goMYPvsr;n=)sU8kN0viGnU^}q0{YtwqS2?EpA_PC!$~;-V z>;#odgKx|evGH=o(}rj#nE|>{Crcp5S_j<}-y8_O5}^n<6IfR21!lizdx*0+m;l?m zT8TZ01j1V|b2=!uY^1Yb`sIYt(xOLZN{{Bo3-@(o>}jO=i49)&3+P}oaS77~=0%n1 zjI6-tD8biQHYAi37l-iPWtJ(8TCYLk2l0eE8Zw3l2C%l7L|!(;o#;1Ky}6j(vh;Eo zSrr7l34F45y@#M0fSzE_R^E>uUN)iJhGOJ*((DL(x+G&3QrIpZ*kD)T3m0h7e#ptI z$C%ld8dGRWScYri1A^#RVsIwyU(^^;3H19wY|WPb~U_LHChwWo6# zEGv0zb9mo4D?F;3tbWvsd*b!qYha=JoZ@=zpb`&!4o8#z*wEbIbN{lPmQ>qIgXCpQ z{K)Tt1SZa4s=d$#UWlNjKPZ2ho1&6tm1dMxK2ma_lvYW0h+<7IF@4E;4^PEgzJxI_ z3)_NdbD3zYPy_c&V0j@y=o1lW@}H2by`0t>9WASx?7C%WjXr*u_ev(6UY#qbTveGY5@Xb~~m$cxjkgoJhiYC|az zM(G5r`0zMP6(+3pPGAvCQ!9jhgZB}Cu_y=vn0a+v(@Gtg_jmF{$Td|cDQY3}H_PgB zXWLz;*gm$D)$d0E56tX0)UClePz$Id8nhn0kpY+*WNq+8it+u;StL=e(OEgSgl_R$ zOnL??opQJcMd1&{M6AN$Y$C;v_>)Mwzotdx_}N_Ri=q%0?+itm4_H|(f0<5 zbcR(n!J^@^=S9ixm};MAQY1ng-g5Hd`cHm*J0Ra(F^8&tmfMu5jZO{};%~+;lUk9;sBC5e%8a)7erv^Nj^`&WCBtrmN24i#&j>HQ-be6xdN^o(Tol zaCuxqSxsgOZc15}*H~mzaYFBmU3GHI_zw4;*qyu|{D!(a*X_*&HpTh+LPsb3H9hMd zRng6UCK!{9@KQR<#!wL(NCq%r;%I(G|KtasilEucM(R#A(z|A^9Q8r6`_Su|K zWOiy_3iwjzu8$P?EY*Rdj;d9#2{7#MUlQC-&)%kIu=5>lb5|VrQbIY}mP}8!=lAd7 z{V+zUrCC#CBrGL(zD3YytEZi4URA>`7<1l;XCwd zZRA2KtEsiUE>(MkAWtNL9iSD8;Ci3)al0OT&O37D3v z1l@QQBaFIdvJJT;1x`y)+U1uRRM1%F^F$Q(9Y$yM#bBUwYBwD1a{V^I^aZtV4fXqi z2;!}H%u_CZ_!Vm+b+bnocXgpE(g;lzMVpP?=uiw`TI2_m)}CyjXznVN`NtJ#GGH-S zek(T_z)InKIq!`^H8NNd(nf-ANPyrfUIa-bmA2$tIQ&Li^+GG3{wiY4)1a0~Ij{R+ z?fDD)n}FUD8Q;qT|MU2j7k+=p(A3J+4?pwPI2Gj%`z=y}HU0o+!D&ds=d{5}zuGpz z%5Shm#pP6CvEZ9F6_Y8HrAMRdtiBc$FCw&g6sRy);<0S(}Bt|;VLtdDE!`E_!bMGHmjENpJ01YSN#{Nuo7JX&$YUvIP&K; zeb84FL0+isB%Xc87_K)$u+1m(I5Kxnz!-n8foxrU1`z?8q&L^#QqwZ@sHodAWgMx~ z0`3N}QBX)_mS^d4xV_8S`k9Soab8TrXZ?KVRlQVS`Khd-ijc_CN}>S$C74j_>?fAY z(dk3EkCx@a0yxsPN|(`Gz6fp&FMZxUF=`YoWdP*q9Kd0|<9b6>!`m5qS83XYJ1eDE z!+YgkQhQJ2e$@=|aeQK=fA4wU?wvfV^{7YFlc`X&>-t&w%I(XxC1nB? z9nSNR0Hz0-lN~*WcCwP ztC8S_c4=|3HjiqNje7IQQJ(_%E)+$L8i}Mx)l5Z!whB+@7jLosqa%9dM}8kn-#7d% zp3GxTb!@N8WUzS`No5JMK$oPEs6UK9qRM;DyE(_u6=#Lzz|Dt8b17uM{3%~3vs4DgZB=rM zyYDW>2{^Q?%)b)YgQ#HtKc>Dis;Vw(_nbq6beEJM-QC^Y-Q6h-2c=VyZb3jABm_z6 zPU&ut2I)9=`+oPkd{SrZ+;jXv=oyI51qdzohlBo`7nt{&Nzy{l@tEc zzLI6JD+R6isVVWa8?y)jCVryo^F4Cn`Iqm-`F>uNZ~4ARYX0M&-KL;7T{DL9=&Ba; zd^c9JdpNP}9YlwoAEFyjRQLsjZy-IhoSTMOlv8g|bm(ScJq_$Qr>5~CMzbKSjmcof z3pO1_eXYs9ME2+&_BoA=8oej2T*b2LmurzTD+{sx&`gh@b>x?a^v*CiHS!(-lTzR` z@FlA>^J%g0UOhD3^~tA`E`3{D@;>P^O4)_D@V`$+~=PNWyq(!q_2uY^;Q;!7Mg>i8aLJ^ps^42fr{oi)F!!6)8_JQtjn| zk3*2(f6f#@853r@z_I@Ua>;;`erOM4XYcvtdu8*Va&rB7o|zTJ?mUX>GhdyCzkN;C z`EE7vojKLwcB3Du&S}0njRehE2F^WT$xLm9hWx0h{M%?hRzyUiu``^}q;9%$!HDe9 znvPTJnv!zJ+8C$h{A%DuCg$3QVQZH(U%;PLDr1Ooh5uTmS@8A}lWq(N&~2qu6x=xI z1$+sIr?yH?MKSSgd5qGEHVaF$qo5|a$GwREyyFpk!&h)YW@+f(%Fks2EUGMe^ zDRG`lc#>x+nLNSJKOW}8P)E;!VJD~2)al*i&C;He;A7eB%C@l8OCuOvcb+8 zYLaiJZcl6>H6zB${SvNZ!ZTu=kl|Pz+3S(4sxl=o1Mxi}aj()O`!@SZJa}n6vFbN2 z=5YUus!i)pm1=t_qXv&}AKu4!582NW<99cjv=`@t8dvO`mA?PpWP;sG1nqCD<*o7g zMj5aFv*om$0>;0%03YTve|uLbF#>OinyRt*`|fYYs7+&K+N@?eow1jKtbyU!`@b7g zFNTVc(n7HN^K0O}d!sxr*m} zoj2&h55@cAi(uXg{U*4 zds⪻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

r@R8DskCg$QY%d=x6zD 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

!LxQh!NmF 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 FormatToStringConverter { get; } = new(EnumToString); - public static FuncValueConverter KeyTypeToStringConverter { get; } = new(EnumToString); + + public static FuncValueConverter FormatChangeTooltipConverter { get; } = + new(format => string.Format(StringsAndTexts.FileInfoWindowChangeFormatTo, EnumToString(format))); + public static FuncValueConverter PlatformIdToStringConverter { get; } = new(ConvertPlatformId); + public static FuncValueConverter NullToColumnSpanConverter { get; } = new(o => o is null ? 2 : 1); - private static string? EnumToString(TEnum value) where TEnum : struct, Enum - => Enum.GetName(value); + private static string? EnumToString(TEnum value) where TEnum : struct, Enum => Enum.GetName(value); - private static string? ConvertPlatformId(PlatformID arg) => - EnumToString(arg) is { } platformId - ? platformId.StartsWith(WindowsShort, StringComparison.CurrentCultureIgnoreCase) - ? Windows - : platformId - : null; + private static string? ConvertPlatformId(PlatformID arg) => EnumToString(arg) is { } platformId + ? platformId.StartsWith(WindowsShort, StringComparison.CurrentCultureIgnoreCase) + ? Windows + : platformId + : null; } \ No newline at end of file diff --git a/OpenSSH_GUI/Extensions/CallerEnricherExtensions.cs b/OpenSSH_GUI/Extensions/CallerEnricherExtensions.cs index a26abf5..094ecf3 100644 --- a/OpenSSH_GUI/Extensions/CallerEnricherExtensions.cs +++ b/OpenSSH_GUI/Extensions/CallerEnricherExtensions.cs @@ -5,15 +5,14 @@ namespace OpenSSH_GUI.Extensions; /// -/// Provides extension methods for enriching Serilog loggers with caller information. +/// Provides extension methods for enriching Serilog loggers with caller information. /// public static class CallerEnricherExtensions { /// - /// Enriches log events with the caller's class name, method name, and line number. + /// Enriches log events with the caller's class name, method name, and line number. /// /// The Serilog enrichment configuration. - /// The updated . - public static LoggerConfiguration WithCaller(this LoggerEnrichmentConfiguration enrichmentConfiguration) - => enrichmentConfiguration.With(); + /// The updated . + public static LoggerConfiguration WithCaller(this LoggerEnrichmentConfiguration enrichmentConfiguration) => enrichmentConfiguration.With(); } \ No newline at end of file diff --git a/OpenSSH_GUI/Extensions/DependencyInjectionExtensions.cs b/OpenSSH_GUI/Extensions/DependencyInjectionExtensions.cs index 3d8c335..61e462d 100644 --- a/OpenSSH_GUI/Extensions/DependencyInjectionExtensions.cs +++ b/OpenSSH_GUI/Extensions/DependencyInjectionExtensions.cs @@ -1,15 +1,16 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Input.Platform; -using Avalonia.Media.Imaging; -using Avalonia.Platform; using Avalonia.Platform.Storage; -using DryIoc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using OpenSSH_GUI.Core; using OpenSSH_GUI.Core.Extensions; +using OpenSSH_GUI.Core.Interfaces; using OpenSSH_GUI.Core.Interfaces.Hosts; using OpenSSH_GUI.Core.Lib.Keys; using OpenSSH_GUI.Core.Lib.Misc; +using OpenSSH_GUI.Core.Resources; using OpenSSH_GUI.Core.Services; using OpenSSH_GUI.Core.Services.Hosted; using OpenSSH_GUI.Dialogs.Interfaces; @@ -17,67 +18,51 @@ using OpenSSH_GUI.ViewModels; using OpenSSH_GUI.Views; using Serilog.Core; -#if DEBUG -using Serilog.Events; -#endif namespace OpenSSH_GUI.Extensions; public static class DependencyInjectionExtensions { - private const string IconUri = "avares://OpenSSH_GUI/Assets/appicon.ico"; - - extension(IContainer container) + extension(IHostBuilder builder) { - internal void ConfigureServicesInternal() + internal IHostBuilder RegisterOpenSshGuiServices() { - container.Register(); - container.Register(); - container.RegisterInstance( - new LoggingLevelSwitch( -#if DEBUG - LogEventLevel.Verbose -#endif - )); - - container.Register(); - container.Register(); - container.Register(); - container.Register(serviceKey: nameof(MainWindow), made: Made.Of(propertiesAndFields: PropertiesAndFields.Auto)); - container.Register(serviceKey: nameof(MainWindowViewModel)); + builder.ConfigureServices((_, services) => + { + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - container.RegisterDelegate(resolver => - resolver.Resolve(serviceKey: nameof(MainWindow))); - container.RegisterDelegate(resolver => - resolver.Resolve(serviceKey: nameof(MainWindow))); - container.RegisterDelegate(resolver => - resolver.Resolve(serviceKey: nameof(MainWindow))!.Clipboard!); - container.RegisterDelegate(resolver => - resolver.Resolve(serviceKey: nameof(MainWindow)).StorageProvider); - container.RegisterDelegate(resolver => - resolver.Resolve(serviceKey: nameof(MainWindow)).Launcher); - container.RegisterDelegate(_ => new Bitmap(AssetLoader.Open(new Uri(IconUri))), - serviceKey: Program.IconServiceKey); + services.AddSingleton(sp => sp.GetRequiredKeyedService(nameof(MainWindow))); + services.AddSingleton(sp => sp.GetRequiredKeyedService(nameof(MainWindow))); + services.AddSingleton(sp => sp.GetRequiredKeyedService(nameof(MainWindow)).Clipboard!); + services.AddSingleton(sp => sp.GetRequiredKeyedService(nameof(MainWindow)).StorageProvider); + services.AddSingleton(sp => sp.GetRequiredKeyedService(nameof(MainWindow)).Launcher); - container.RegisterViewWithViewModel(); - container.RegisterViewWithViewModel(); - container.RegisterViewWithViewModel(); - container.RegisterViewWithViewModel(); - container.RegisterViewWithViewModel(); - container.RegisterViewWithViewModel(); - container.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(ServiceLifetime.Singleton); + services.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(); + services.RegisterViewWithViewModel(); - container.Register(Reuse.Transient); - container.Register(Reuse.Transient); - } - } - - extension(IServiceCollection collection) - { - internal IServiceCollection RegisterOpenSshGuiServices() - { - collection.AddHostedService(); - return collection; + services.AddTransient(); + services.AddTransient(); + services.AddHostedService(); + }); + return builder; } } } \ No newline at end of file diff --git a/OpenSSH_GUI/Logging/Enricher/CallerEnricher.cs b/OpenSSH_GUI/Logging/Enricher/CallerEnricher.cs index 1df30c8..d90386f 100644 --- a/OpenSSH_GUI/Logging/Enricher/CallerEnricher.cs +++ b/OpenSSH_GUI/Logging/Enricher/CallerEnricher.cs @@ -5,17 +5,17 @@ namespace OpenSSH_GUI.Logging.Enricher; /// -/// Enriches log events with caller information: -/// the class name, method name, and line number -/// derived from the current stack frame. +/// Enriches log events with caller information: +/// the class name, method name, and line number +/// derived from the current stack frame. /// public sealed class CallerEnricher : ILogEventEnricher { private const string LineNumberProperty = "LineNumber"; - private const string FileNameProperty = "FileName"; + private const string FileNameProperty = "FileName"; // Serilog-internal namespaces to skip when walking the stack - private static readonly string[] serilogNamespaces = + private static readonly string[] SerilogNamespaces = [ "Serilog.", "System.", @@ -23,37 +23,37 @@ public sealed class CallerEnricher : ILogEventEnricher ]; /// - /// Enriches the given log event with caller class, method, file name, and line number. + /// Enriches the given log event with caller class, method, file name, and line number. /// public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var frame = FindCallerFrame(); - var lineNumber = frame?.GetFileLineNumber() ?? 0; - var fileName = Path.GetFileName(frame?.GetFileName()) ?? ""; + var lineNumber = frame?.GetFileLineNumber() ?? 0; + var fileName = Path.GetFileName(frame?.GetFileName()) ?? ""; logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(LineNumberProperty, lineNumber)); - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(FileNameProperty, fileName)); + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(FileNameProperty, fileName)); } /// - /// Walks the stack to find the first frame outside of Serilog, system namespaces, - /// and the enricher itself. + /// Walks the stack to find the first frame outside of Serilog, system namespaces, + /// and the enricher itself. /// - /// The first relevant , or null if not found. + /// The first relevant , or null if not found. private static StackFrame? FindCallerFrame() { - var stack = new StackTrace(fNeedFileInfo: true); + var stack = new StackTrace(true); foreach (var frame in stack.GetFrames()) { var declaringType = frame.GetMethod()?.DeclaringType; - if (declaringType == null) continue; - if (declaringType == typeof(CallerEnricher)) continue; - if (typeof(ILogEventEnricher).IsAssignableFrom(declaringType)) continue; - + if (declaringType == null) continue; + if (declaringType == typeof(CallerEnricher)) continue; + if (typeof(ILogEventEnricher).IsAssignableFrom(declaringType)) continue; + var ns = declaringType.Namespace ?? string.Empty; - if (Array.Exists(serilogNamespaces, ns.StartsWith)) continue; + if (Array.Exists(SerilogNamespaces, ns.StartsWith)) continue; return frame; } diff --git a/OpenSSH_GUI/OpenSSH_GUI.csproj b/OpenSSH_GUI/OpenSSH_GUI.csproj index a21773e..3c0d78e 100644 --- a/OpenSSH_GUI/OpenSSH_GUI.csproj +++ b/OpenSSH_GUI/OpenSSH_GUI.csproj @@ -1,68 +1,76 @@  - - - - - Exe - true - app.manifest - true - true - true - true - Assets\appicon.ico - false - frequency403 - en - OpenSSH-GUI.snk - false - - - - - - - - - - - - - - - - - - - - - - - - - - PublicResXFileCodeGenerator - StringsAndTexts.Designer.cs - - - - - - True - True - StringsAndTexts.resx - - - - - OpenSSH GUI - OpenSSH GUI - frequency403.opensshgui - 1.0.0 - APPL - OpenSSHGui - OpenSSHGui.icns - NSApplication - true - - + + + Exe + true + app.manifest + true + true + true + true + ../images/openssh-gui.ico + false + frequency403 + en + OpenSSH-GUI.snk + false + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + Assets/%(Filename)%(Extension) + + + + + + PublicResXFileCodeGenerator + StringsAndTexts.Designer.cs + + + + + True + True + StringsAndTexts.resx + + + SubmitButtons.axaml + Code + + + + + OpenSSH GUI + + OpenSSH GUI + frequency403.opensshgui + 1.0.0 + APPL + OpenSSHGui + OpenSSHGui.icns + + NSApplication + true + + \ No newline at end of file diff --git a/OpenSSH_GUI/Program.cs b/OpenSSH_GUI/Program.cs index 76b4bc7..8beaaee 100644 --- a/OpenSSH_GUI/Program.cs +++ b/OpenSSH_GUI/Program.cs @@ -1,12 +1,13 @@ using System.Reflection; +using System.Text.Json; using Avalonia; -using DryIoc; -using DryIoc.Microsoft.DependencyInjection; using JetBrains.Annotations; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using OpenSSH_GUI.Core; +using OpenSSH_GUI.Core.Configuration; using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; using OpenSSH_GUI.Extensions; @@ -14,100 +15,125 @@ using ReactiveUI.Avalonia; using Serilog; using Serilog.Core; +using Serilog.Sinks.SystemConsole.Themes; +using LoggerConfiguration = Serilog.LoggerConfiguration; namespace OpenSSH_GUI; +// REFACTOR: Change Readme.MD accordingly to new Project functionality; [UsedImplicitly] internal sealed class Program { - public const SshConfigFiles ConfigFile = SshConfigFiles.Config; - public const SshConfigFiles SshdConfig = SshConfigFiles.Sshd_Config; + private const SshConfigFiles ConfigFile = SshConfigFiles.Config; + private const SshConfigFiles SshdConfig = SshConfigFiles.Sshd_Config; public const string AppName = "OpenSSH GUI"; public const string VersionEnvVar = "RUNNING_VERSION"; - public const string IconServiceKey = "AppIcon"; - private static string GetHostVersion() - { - return Assembly.GetEntryAssembly() - ?.GetCustomAttribute() - ?.InformationalVersion - ?? Assembly.GetEntryAssembly()?.GetName().Version?.ToString() - ?? "0.0.0"; - } + private static string GetHostVersion() => Assembly.GetEntryAssembly() + ?.GetCustomAttribute() + ?.InformationalVersion + ?? Assembly.GetEntryAssembly()?.GetName().Version?.ToString() + ?? "0.0.0"; - private static Logger? CreateLogger(IContainer container) + private static void ConfigureOpenSshGuiLogger( + ApplicationConfiguration bootstrapConfig, + LoggingLevelSwitch levelSwitch, + LoggerConfiguration loggerConfiguration) { - var logConfiguration = Core.Configuration.LoggerConfiguration.Default; - if (!Directory.Exists(logConfiguration.LogFilePath)) - Directory.CreateDirectory(logConfiguration.LogFilePath); + var loggerConfig = bootstrapConfig.LoggerConfiguration; + Directory.CreateIfNotExists(loggerConfig.LogFilePath); - return new LoggerConfiguration() + loggerConfiguration .Enrich.FromLogContext() .Enrich.WithCaller() - .MinimumLevel.ControlledBy(container.Resolve()) + .MinimumLevel.ControlledBy(levelSwitch) #if DEBUG - .WriteTo.ColoredConsole(outputTemplate: logConfiguration.LogOutputTemplate) + .WriteTo.Console( + outputTemplate: loggerConfig.LogOutputTemplate, + theme: AnsiConsoleTheme.Code) #endif .WriteTo.File( - logConfiguration.LogFileFullPath, - outputTemplate: logConfiguration.LogOutputTemplate, - rollingInterval: RollingInterval.Day) - .CreateLogger(); + loggerConfig.LogFileFullPath, + outputTemplate: loggerConfig.LogOutputTemplate, + rollingInterval: RollingInterval.Day); } #pragma warning disable CA1416 [STAThread] public static async Task Main(string[] args) { - using var container = new Container(rules => - { - var newRules = rules; - if (!rules.HasMicrosoftDependencyInjectionRules()) - newRules = rules.WithMicrosoftDependencyInjectionRules(); - - return newRules.WithDefaultReuse(Reuse.Singleton) - .WithTrackingDisposableTransients() - .WithoutThrowOnRegisteringDisposableTransient() - .WithDefaultIfAlreadyRegistered(IfAlreadyRegistered.Replace); - }); - container.ConfigureServicesInternal(); - var factory = new DryIocServiceProviderFactory(container); using var mainCancellationTokenSource = new CancellationTokenSource(); + + File.CreateIfNotExists( + ApplicationConfiguration.DefaultApplicationConfigurationFileFullPath, + JsonSerializer.Serialize(ApplicationConfiguration.Default, SourceGenerationContext.Default.ApplicationConfiguration)); + + var bootstrapConfig = ReadBootstrapConfiguration(); + var levelSwitch = new LoggingLevelSwitch(bootstrapConfig.LogLevel); + var host = Host.CreateDefaultBuilder(args) - .UseServiceProviderFactory(factory) - .ConfigureServices(services => services.RegisterOpenSshGuiServices()) - .UseSerilog(logger: CreateLogger(container), dispose: true) + .AddMutableConfiguration(ApplicationConfiguration.DefaultApplicationConfigurationFileFullPath, SourceGenerationContext.Default.ApplicationConfiguration, false) .ConfigureAppConfiguration(ConfigureAppConfiguration) + .ConfigureServices(services => services.AddSingleton(levelSwitch)) + .RegisterOpenSshGuiServices() + .UseSerilog((_, _, loggerConfig) => + ConfigureOpenSshGuiLogger(bootstrapConfig, levelSwitch, loggerConfig)) .Build(); var appBuilder = AppBuilder.Configure(() => host.Services.GetRequiredService()) + .UseSkia() .UsePlatformDetect() .WithInterFont() .UseReactiveUI(configure => { - configure.WithPlatformServices(); - configure.WithAvalonia(); - configure.WithExceptionHandler(host.Services.GetRequiredService()); + configure + .WithPlatformServices() + .WithAvalonia() + .WithExceptionHandler(host.Services.GetRequiredService()); }); - + await host.StartAsync(mainCancellationTokenSource.Token); appBuilder.StartWithClassicDesktopLifetime(args); await host.StopAsync(mainCancellationTokenSource.Token); } #pragma warning restore CA1416 - private static void ConfigureAppConfiguration(HostBuilderContext builderContext, + private static void ConfigureAppConfiguration(HostBuilderContext hostBuilderContext, IConfigurationBuilder configurationBuilder) { configurationBuilder.AddSshConfig(ConfigFile.GetPathOfFile(), true, true, LoggingAction); configurationBuilder.AddSshConfig(SshdConfig.GetPathOfFile(), true, true, LoggingAction); - configurationBuilder.AddInMemoryCollection([ + + configurationBuilder.AddJsonFile( + new PhysicalFileProvider(ApplicationConfiguration.ApplicationConfigurationPath), ApplicationConfiguration.ApplicationConfigurationName, false, true); + + configurationBuilder.AddInMemoryCollection( + [ new KeyValuePair(VersionEnvVar, GetHostVersion()) ]); } - private static void LoggingAction(string arg1, Exception arg2) + private static void LoggingAction(string arg1, Exception arg2) { Log.Logger.Error(arg2, "Failed to load SSH config file: {Path}", arg1); } + + /// + /// Reads the application configuration directly from disk without using DI. + /// Used during host bootstrap to avoid circular dependency with Serilog setup. + /// Returns on any failure. + /// + private static ApplicationConfiguration ReadBootstrapConfiguration() { - Log.Logger.Error(arg2, "Failed to load SSH config file: {Path}", arg1); + try + { + var json = File.ReadAllText( + ApplicationConfiguration.DefaultApplicationConfigurationFileFullPath); + return JsonSerializer.Deserialize( + json, + SourceGenerationContext.Default.ApplicationConfiguration) + ?? ApplicationConfiguration.Default; + } + catch + { + return ApplicationConfiguration.Default; + } } } \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/FlyoutButton.cs b/OpenSSH_GUI/Resources/Controls/FlyoutButton.cs new file mode 100644 index 0000000..751b478 --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/FlyoutButton.cs @@ -0,0 +1,45 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace OpenSSH_GUI.Resources.Controls; + +public class FlyoutButton : ContentControl +{ + public static readonly StyledProperty FlyoutProperty = + AvaloniaProperty.Register(nameof(Flyout)); + + public FlyoutButton() + { + AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + AddHandler(PointerReleasedEvent, OnPointerReleased, RoutingStrategies.Tunnel); + AddHandler(PointerEnteredEvent, (_, _) => PseudoClasses.Set(":pointerover", true)); + AddHandler( + PointerExitedEvent, (_, _) => + { + PseudoClasses.Set(":pointerover", false); + PseudoClasses.Set(":pressed", false); + }); + } + + public FlyoutBase? Flyout + { + get => GetValue(FlyoutProperty); + set => SetValue(FlyoutProperty, value); + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + PseudoClasses.Set(":pressed", true); + + if (Flyout is { } flyout) + { + flyout.ShowAt(this); + e.Handled = true; + } + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { PseudoClasses.Set(":pressed", false); } +} \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml b/OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml new file mode 100644 index 0000000..60b3fd2 --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml.cs b/OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml.cs new file mode 100644 index 0000000..c018493 --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/HeaderedItem.axaml.cs @@ -0,0 +1,70 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace OpenSSH_GUI.Resources.Controls; + +public partial class HeaderedItem : ContentControl +{ + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); + + public static readonly StyledProperty SideHeaderProperty = + AvaloniaProperty.Register(nameof(SideHeader)); + + // Header alignment + public static readonly StyledProperty HeaderHorizontalAlignmentProperty = + AvaloniaProperty.Register( + nameof(HeaderHorizontalAlignment), HorizontalAlignment.Left); + + public static readonly StyledProperty HeaderVerticalAlignmentProperty = + AvaloniaProperty.Register( + nameof(HeaderVerticalAlignment), VerticalAlignment.Center); + + // SideHeader alignment + public static readonly StyledProperty SideHeaderHorizontalAlignmentProperty = + AvaloniaProperty.Register( + nameof(SideHeaderHorizontalAlignment), HorizontalAlignment.Right); + + public static readonly StyledProperty SideHeaderVerticalAlignmentProperty = + AvaloniaProperty.Register( + nameof(SideHeaderVerticalAlignment), VerticalAlignment.Center); + + public HeaderedItem() { InitializeComponent(); } + + public object? Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + public object? SideHeader + { + get => GetValue(SideHeaderProperty); + set => SetValue(SideHeaderProperty, value); + } + + public HorizontalAlignment HeaderHorizontalAlignment + { + get => GetValue(HeaderHorizontalAlignmentProperty); + set => SetValue(HeaderHorizontalAlignmentProperty, value); + } + + public VerticalAlignment HeaderVerticalAlignment + { + get => GetValue(HeaderVerticalAlignmentProperty); + set => SetValue(HeaderVerticalAlignmentProperty, value); + } + + public HorizontalAlignment SideHeaderHorizontalAlignment + { + get => GetValue(SideHeaderHorizontalAlignmentProperty); + set => SetValue(SideHeaderHorizontalAlignmentProperty, value); + } + + public VerticalAlignment SideHeaderVerticalAlignment + { + get => GetValue(SideHeaderVerticalAlignmentProperty); + set => SetValue(SideHeaderVerticalAlignmentProperty, value); + } +} \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml b/OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml new file mode 100644 index 0000000..2d45cd4 --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml.cs b/OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml.cs new file mode 100644 index 0000000..6c0b6e0 --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/PasswordDisplay.axaml.cs @@ -0,0 +1,88 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using OpenSSH_GUI.Core.Lib.Keys; + +namespace OpenSSH_GUI.Resources.Controls; + +/// +/// A password input control with a toggleable visibility button (eye icon). +/// When hidden, input is masked with the configured ; +/// when revealed, plain text is shown. Supports read-only mode and external +/// observation of the current visibility state via . +/// +public partial class PasswordDisplay : UserControl +{ + private const char DefaultMaskChar = '●'; + private const string DefaultWatermark = "Password"; + + // --- Avalonia Styled Properties --- + + /// Bindable property for the placeholder text. + public static readonly StyledProperty WatermarkProperty = + AvaloniaProperty.Register(nameof(Watermark), DefaultWatermark); + + /// Bindable property for the character used to mask the password input. + public static readonly StyledProperty MaskCharacterProperty = + AvaloniaProperty.Register(nameof(MaskCharacter), DefaultMaskChar); + + /// + /// Bindable property indicating whether the password is currently visible. + /// Can be observed or driven externally (e.g., from a ViewModel) to react + /// to visibility changes — for instance, to show or enable a copy button. + /// + public static readonly StyledProperty PasswordVisibleProperty = + AvaloniaProperty.Register(nameof(PasswordVisible)); + + + public static readonly StyledProperty SecurePasswordProperty = + AvaloniaProperty.Register(nameof(SecurePassword)); + + // --- Parts --- + private TextBox _textBox = new(); + private ToggleButton _toggle = new(); + + public PasswordDisplay() { InitializeComponent(); } + + public SshKeyFilePassword? SecurePassword + { + get => GetValue(SecurePasswordProperty); + set => SetValue(SecurePasswordProperty, value); + } + + public string? Watermark + { + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); + } + + public char MaskCharacter + { + get => GetValue(MaskCharacterProperty); + set => SetValue(MaskCharacterProperty, value); + } + + public bool PasswordVisible + { + get => GetValue(PasswordVisibleProperty); + set => SetValue(PasswordVisibleProperty, value); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == SecurePasswordProperty && GetValue(SecurePasswordProperty) is { } property) + _textBox.Text = property.GetPasswordString(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + _textBox = this.FindControl("PART_Password")!; + _toggle = this.FindControl("PART_Toggle")!; + _textBox.TextChanged += (_, _) => { _toggle.IsEnabled = _textBox.Text?.Length > 0; }; + } +} \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml b/OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml new file mode 100644 index 0000000..61665ce --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml @@ -0,0 +1,49 @@ + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml.cs b/OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml.cs new file mode 100644 index 0000000..857b75a --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/SubmitButtons.axaml.cs @@ -0,0 +1,162 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Disposables.Fluent; +using Avalonia; +using Avalonia.Controls; +using Material.Icons; +using ReactiveUI; + +namespace OpenSSH_GUI.Resources.Controls; + +public partial class SubmitButtons : UserControl +{ + public static readonly DirectProperty> BooleanSubmitProperty = + AvaloniaProperty.RegisterDirect>( + nameof(BooleanSubmit), + c => c.BooleanSubmit, + (c, v) => c.BooleanSubmit = v); + + public static readonly DirectProperty AbortButtonEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(AbortButtonEnabled), + c => c.AbortButtonEnabled, (c, v) => c.AbortButtonEnabled = v); + + public static readonly DirectProperty SubmitButtonEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(SubmitButtonEnabled), + c => c.SubmitButtonEnabled, (c, v) => c.SubmitButtonEnabled = v); + + public static readonly DirectProperty AbortButtonTooltipProperty = + AvaloniaProperty.RegisterDirect( + nameof(AbortButtonTooltip), + c => c.AbortButtonTooltip, (c, v) => c.AbortButtonTooltip = v); + + public static readonly DirectProperty SubmitButtonTooltipProperty = + AvaloniaProperty.RegisterDirect( + nameof(SubmitButtonTooltip), + c => c.SubmitButtonTooltip, (c, v) => c.SubmitButtonTooltip = v); + + public static readonly DirectProperty AbortButtonIconKindProperty = + AvaloniaProperty.RegisterDirect( + nameof(AbortButtonIconKind), + c => c.AbortButtonIconKind, (c, v) => c.AbortButtonIconKind = v); + + public static readonly DirectProperty SubmitButtonIconKindProperty = + AvaloniaProperty.RegisterDirect( + nameof(SubmitButtonIconKind), + c => c.SubmitButtonIconKind, (c, v) => c.SubmitButtonIconKind = v); + + public static readonly DirectProperty AbortButtonContentProperty = + AvaloniaProperty.RegisterDirect( + nameof(AbortButtonContent), + c => c.AbortButtonContent, (c, v) => c.AbortButtonContent = v); + + public static readonly DirectProperty AbortButtonContentEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(AbortButtonContentEnabled), + c => c.AbortButtonContentEnabled, (c, v) => c.AbortButtonContentEnabled = v); + + public static readonly DirectProperty SubmitButtonContentProperty = + AvaloniaProperty.RegisterDirect( + nameof(SubmitButtonContent), + c => c.SubmitButtonContent, (c, v) => c.SubmitButtonContent = v); + + public static readonly DirectProperty SubmitButtonContentEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(SubmitButtonContentEnabled), + c => c.SubmitButtonContentEnabled, (c, v) => c.SubmitButtonContentEnabled = v); + + private readonly CompositeDisposable _disposables = new(); + + public SubmitButtons() + { + this.WhenAnyValue(x => x.AbortButtonContent) + .Subscribe(x => AbortButtonContentEnabled = x is not null) + .DisposeWith(_disposables); + + this.WhenAnyValue(x => x.SubmitButtonContent) + .Subscribe(x => SubmitButtonContentEnabled = x is not null) + .DisposeWith(_disposables); + + InitializeComponent(); + } + + public bool AbortButtonContentEnabled + { + get; + set => SetAndRaise(AbortButtonContentEnabledProperty, ref field, value); + } = false; + + public Control? AbortButtonContent + { + get; + set => SetAndRaise(AbortButtonContentProperty, ref field, value); + } = null; + + public bool SubmitButtonContentEnabled + { + get; + set => SetAndRaise(SubmitButtonContentEnabledProperty, ref field, value); + } = false; + + public Control? SubmitButtonContent + { + get; + set => SetAndRaise(SubmitButtonContentProperty, ref field, value); + } = null; + + + public ReactiveCommand BooleanSubmit + { + get; + set => SetAndRaise(BooleanSubmitProperty, ref field, value); + } = ReactiveCommand.Create(_ => new Unit()); + + + public bool AbortButtonEnabled + { + get; + set => SetAndRaise(AbortButtonEnabledProperty, ref field, value); + } = true; + + + public bool SubmitButtonEnabled + { + get; + set => SetAndRaise(SubmitButtonEnabledProperty, ref field, value); + } = true; + + + public string AbortButtonTooltip + { + get; + set => SetAndRaise(AbortButtonTooltipProperty, ref field, value); + } = StringsAndTexts.CancelAndClose; + + + public string SubmitButtonTooltip + { + get; + set => SetAndRaise(SubmitButtonTooltipProperty, ref field, value); + } = StringsAndTexts.SaveAndClose; + + + public MaterialIconKind AbortButtonIconKind + { + get; + set => SetAndRaise(AbortButtonIconKindProperty, ref field, value); + } = MaterialIconKind.CancelOutline; + + + public MaterialIconKind SubmitButtonIconKind + { + get; + set => SetAndRaise(SubmitButtonIconKindProperty, ref field, value); + } = MaterialIconKind.CheckOutline; + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _disposables.Dispose(); + } +} \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml b/OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml new file mode 100644 index 0000000..8c16cd0 --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml.cs b/OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml.cs new file mode 100644 index 0000000..83263ab --- /dev/null +++ b/OpenSSH_GUI/Resources/Controls/TooltippedIcon.axaml.cs @@ -0,0 +1,119 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; +using Material.Icons; + +namespace OpenSSH_GUI.Resources.Controls; + +public partial class TooltippedIcon : UserControl +{ + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon), MaterialIconKind.Info); + + public static readonly StyledProperty ToolTipContentProperty = + AvaloniaProperty.Register(nameof(ToolTipContent)); + + public static readonly StyledProperty ToolTipPlacementProperty = + AvaloniaProperty.Register(nameof(ToolTipPlacement), PlacementMode.Bottom); + + private DispatcherTimer? _hoverTimer; + private bool _isPinned; + + public TooltippedIcon() + { + InitializeComponent(); + InitHoverTimer(); + } + + public MaterialIconKind Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public object? ToolTipContent + { + get => GetValue(ToolTipContentProperty); + set => SetValue(ToolTipContentProperty, value); + } + + public PlacementMode ToolTipPlacement + { + get => GetValue(ToolTipPlacementProperty); + set => SetValue(ToolTipPlacementProperty, value); + } + + /// + /// Initializes the hover delay timer used to open the popup on prolonged pointer hover. + /// + private void InitHoverTimer() + { + _hoverTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(600) + }; + _hoverTimer.Tick += OnHoverTimerTick; + } + + /// + /// Opens the popup transiently when the hover delay elapses, unless already pinned. + /// + private void OnHoverTimerTick(object? sender, EventArgs e) + { + _hoverTimer?.Stop(); + Popup.IsOpen = true; + } + + /// + /// Starts the hover timer when the pointer enters the hit area. + /// + private void OnPointerEntered(object? sender, PointerEventArgs e) + { + if (!_isPinned) + _hoverTimer?.Start(); + } + + /// + /// Closes the popup on pointer exit, unless it has been pinned via click. + /// + private void OnPointerExited(object? sender, PointerEventArgs e) + { + _hoverTimer?.Stop(); + if (_isPinned) return; + + var pos = e.GetPosition(Popup.Child); + if (Popup.IsOpen && Popup.Child is not null) + { + var bounds = Popup.Child.Bounds; + if (bounds.Contains(pos)) return; + } + + Popup.IsOpen = false; + } + + /// + /// Pins the popup open on click, or unpins and closes it if already pinned. + /// Light dismiss will also unpin via . + /// + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + _hoverTimer?.Stop(); + _isPinned = !_isPinned; + Popup.IsOpen = _isPinned; + } + + /// + /// Resets the pinned state when the popup is closed externally via light dismiss. + /// + private void OnPopupClosed(object? sender, EventArgs e) { _isPinned = false; } + + /// + /// Closes the popup when the pointer leaves the popup content area, unless pinned. + /// + private void OnPopupContentExited(object? sender, PointerEventArgs e) + { + if (!_isPinned) + Popup.IsOpen = false; + } +} \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/StringsAndTexts.Designer.cs b/OpenSSH_GUI/Resources/StringsAndTexts.Designer.cs index 0800c85..42a90b8 100644 --- a/OpenSSH_GUI/Resources/StringsAndTexts.Designer.cs +++ b/OpenSSH_GUI/Resources/StringsAndTexts.Designer.cs @@ -201,15 +201,9 @@ public static string MainWindowEditKnownHostsFileToolTip { } } - public static string MainWindowFoundKeyPairsCountLabelPart1 { + public static string MainWindowFoundKeyPairsCountLabel { get { - return ResourceManager.GetString("MainWindowFoundKeyPairsCountLabelPart1", resourceCulture); - } - } - - public static string MainWindowFoundKeyPairsCountLabelPart2 { - get { - return ResourceManager.GetString("MainWindowFoundKeyPairsCountLabelPart2", resourceCulture); + return ResourceManager.GetString("MainWindowFoundKeyPairsCountLabel", resourceCulture); } } @@ -573,18 +567,6 @@ public static string Days { } } - public static string ApplicationSettingsCleanup { - get { - return ResourceManager.GetString("ApplicationSettingsCleanup", resourceCulture); - } - } - - public static string ApplicationSettingsFiles { - get { - return ResourceManager.GetString("ApplicationSettingsFiles", resourceCulture); - } - } - public static string ApplicationSettingsClearWholeCache { get { return ResourceManager.GetString("ApplicationSettingsClearWholeCache", resourceCulture); @@ -620,5 +602,209 @@ public static string ApplicationSettingsViewModelConfirmationError { return ResourceManager.GetString("ApplicationSettingsViewModelConfirmationError", resourceCulture); } } + + public static string MainWindowReloadingKeys { + get { + return ResourceManager.GetString("MainWindowReloadingKeys", resourceCulture); + } + } + + public static string MainWindowKeyNotPasswordProtected { + get { + return ResourceManager.GetString("MainWindowKeyNotPasswordProtected", resourceCulture); + } + } + + public static string MainWindowKeyPasswordProtectedLocked { + get { + return ResourceManager.GetString("MainWindowKeyPasswordProtectedLocked", resourceCulture); + } + } + + public static string MainWindowKeyPasswordProtectedUnlocked { + get { + return ResourceManager.GetString("MainWindowKeyPasswordProtectedUnlocked", resourceCulture); + } + } + + public static string MainWindowProvidePassword { + get { + return ResourceManager.GetString("MainWindowProvidePassword", resourceCulture); + } + } + + public static string MainWindowOpenFileInfoWindow { + get { + return ResourceManager.GetString("MainWindowOpenFileInfoWindow", resourceCulture); + } + } + + public static string FileInfoWindowFoundAssociatedFiles { + get { + return ResourceManager.GetString("FileInfoWindowFoundAssociatedFiles", resourceCulture); + } + } + + public static string FileInfoWindowChangePasswordTooltip { + get { + return ResourceManager.GetString("FileInfoWindowChangePasswordTooltip", resourceCulture); + } + } + + public static string FileInfoWindowKeyFormat { + get { + return ResourceManager.GetString("FileInfoWindowKeyFormat", resourceCulture); + } + } + + public static string FileInfoWindowCurrent { + get { + return ResourceManager.GetString("FileInfoWindowCurrent", resourceCulture); + } + } + + public static string FileInfoWindowChangeFormatTo { + get { + return ResourceManager.GetString("FileInfoWindowChangeFormatTo", resourceCulture); + } + } + + public static string FileInfoWindowPassword { + get { + return ResourceManager.GetString("FileInfoWindowPassword", resourceCulture); + } + } + + public static string FileInfoWindowChangePassword { + get { + return ResourceManager.GetString("FileInfoWindowChangePassword", resourceCulture); + } + } + + public static string FileInfoWindowEnterNewPassword { + get { + return ResourceManager.GetString("FileInfoWindowEnterNewPassword", resourceCulture); + } + } + + public static string FileInfoWindowConfirmFileOverwrite { + get { + return ResourceManager.GetString("FileInfoWindowConfirmFileOverwrite", resourceCulture); + } + } + + public static string FileInfoWindowFileAlreadyExists { + get { + return ResourceManager.GetString("FileInfoWindowFileAlreadyExists", resourceCulture); + } + } + + public static string FileInfoWindowChangeMessage { + get { + return ResourceManager.GetString("FileInfoWindowChangeMessage", resourceCulture); + } + } + + public static string FileInfoWindowEnterNewFilename { + get { + return ResourceManager.GetString("FileInfoWindowEnterNewFilename", resourceCulture); + } + } + + public static string FileInfoWindowFilenameCannotBeEmpty { + get { + return ResourceManager.GetString("FileInfoWindowFilenameCannotBeEmpty", resourceCulture); + } + } + + public static string FileInfoWindowPasswordCopied { + get { + return ResourceManager.GetString("FileInfoWindowPasswordCopied", resourceCulture); + } + } + + public static string ApplicationSettingsLogLevel { + get { + return ResourceManager.GetString("ApplicationSettingsLogLevel", resourceCulture); + } + } + + public static string ApplicationSettingsTheme { + get { + return ResourceManager.GetString("ApplicationSettingsTheme", resourceCulture); + } + } + + public static string ApplicationSettingsFontSize { + get { + return ResourceManager.GetString("ApplicationSettingsFontSize", resourceCulture); + } + } + + public static string ApplicationSettingsCleanupFiles { + get { + return ResourceManager.GetString("ApplicationSettingsCleanupFiles", resourceCulture); + } + } + + public static string ConnectToServerPreconfiguredConnections { + get { + return ResourceManager.GetString("ConnectToServerPreconfiguredConnections", resourceCulture); + } + } + + public static string ConnectToServerHostname { + get { + return ResourceManager.GetString("ConnectToServerHostname", resourceCulture); + } + } + + public static string ConnectToServerUsername { + get { + return ResourceManager.GetString("ConnectToServerUsername", resourceCulture); + } + } + + public static string ConnectToServerPassword { + get { + return ResourceManager.GetString("ConnectToServerPassword", resourceCulture); + } + } + + public static string ConnectToServerConnectionFailed { + get { + return ResourceManager.GetString("ConnectToServerConnectionFailed", resourceCulture); + } + } + + public static string EditKnownHostsLocal { + get { + return ResourceManager.GetString("EditKnownHostsLocal", resourceCulture); + } + } + + public static string EditKnownHostsRemote { + get { + return ResourceManager.GetString("EditKnownHostsRemote", resourceCulture); + } + } + + public static string EditAuthorizedKeysLocal { + get { + return ResourceManager.GetString("EditAuthorizedKeysLocal", resourceCulture); + } + } + + public static string EditAuthorizedKeysServer { + get { + return ResourceManager.GetString("EditAuthorizedKeysServer", resourceCulture); + } + } + + public static string ApplicationSettingsLookupPaths { + get { + return ResourceManager.GetString("ApplicationSettingsLookupPaths", resourceCulture); + } + } } } diff --git a/OpenSSH_GUI/Resources/StringsAndTexts.de.resx b/OpenSSH_GUI/Resources/StringsAndTexts.de.resx index a99b372..239021b 100644 --- a/OpenSSH_GUI/Resources/StringsAndTexts.de.resx +++ b/OpenSSH_GUI/Resources/StringsAndTexts.de.resx @@ -138,8 +138,8 @@ "known_hosts"-Datei bearbeiten - - Schlüsselpaare im SSH-Verzeichnis gefunden + + {0} Schlüsselpaare im SSH-Verzeichnis gefunden Mit einem Server verbinden, um die Datei auf dem Server zu bearbeiten! @@ -165,9 +165,6 @@ Änderungen speichern und schließen - - Es wurden - Keine Verbindung zum schließen! @@ -253,13 +250,13 @@ Passwort eingeben: - Passwürt für {0} Schlüsseldatei bereitstellen + Passwort für Schlüsseldatei eingeben Falsches Passwort! - Sie haben ein falsches passwort eingegeben. Dies ist Versuch {0} von {1} möglichen Versuchen. Möchten sie es erneut versuchen? + Sie haben ein falsches passwort eingegeben. Möchten sie es erneut versuchen? Dateinamen ändern @@ -276,12 +273,6 @@ Tage - - Räume - - - Dateien auf - Gesamtes cache aufräumen @@ -300,4 +291,106 @@ Bitte den richtigen Bestätigungswert eingeben + + Schlüssel werden neu geladen... + + + Schlüssel ist nicht passwortgeschützt + + + Schlüssel ist passwortgeschützt und kann erst nach Eingabe des Passworts verwendet werden + + + Schlüssel ist passwortgeschützt und mit korrektem Passwort geöffnet + + + Passwort eingeben + + + Dateiinfo-Fenster öffnen + + + {0} zugehörige Dateien gefunden + + + Passwort dieses Schlüssels ändern + + + Schlüsselformat + + + Aktuell: + + + Format zu {0} ändern + + + Passwort + + + Passwort ändern + + + Neues Passwort für Schlüssel {0} eingeben + + + Dateiüberschreibung bestätigen + + + Die Schlüsseldatei {0} existiert bereits. Möchten Sie sie überschreiben? + + + Ändern + + + Neuen Dateinamen eingeben + + + Dateiname darf nicht leer sein + + + Passwort in Zwischenablage kopiert + + + Log-Stufe + + + Design + + + Schriftgröße + + + {0} Dateien aufräumen + + + Vorkonfigurierte Verbindungen + + + Hostname + + + Benutzername + + + Passwort + + + Verbindung fehlgeschlagen + + + Lokal + + + Entfernt + + + Lokal + + + Server + + + Suchpfade + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/StringsAndTexts.resx b/OpenSSH_GUI/Resources/StringsAndTexts.resx index 94e236e..75f14d8 100644 --- a/OpenSSH_GUI/Resources/StringsAndTexts.resx +++ b/OpenSSH_GUI/Resources/StringsAndTexts.resx @@ -102,11 +102,8 @@ Edit "known_hosts" file - - Found - - - keypairs in SSH directory + + Found {0} keypairs in SSH directory Delete {0}? @@ -198,6 +195,7 @@ Filename does already exist! + Status: {0} @@ -265,13 +263,13 @@ Enter the password: - Provide password for key {0} + Provide password for key Password incorrect! - You provided a wrong password. Try {0} of {1}. Do you want to try again? + You provided a wrong password. Do you want to try again? Change Filename @@ -288,12 +286,6 @@ Days - - Cleanup - - - files - Clear whole cache @@ -312,4 +304,106 @@ Provide the correct phrase to continue + + Reloading keys... + + + Key is not password protected + + + Key is password protected and cannot be used until a password is provided + + + Key is password protected and opened with correct password + + + Provide Password + + + Open Fileinfo Window + + + Found {0} associated Files + + + Change password of this key + + + KeyFormat + + + Current: + + + Change format to the {0} format + + + Password + + + Change password + + + Enter a new password for key {0} + + + Confirm File Overwrite + + + The keyfile {0} already exist. Do you want to overwrite it? + + + ChangeMe + + + Enter new filename + + + Filename cannot be empty + + + Password copied to clipboard + + + LogLevel + + + Theme + + + Font Size + + + Cleanup {0} files + + + Preconfigured Connections + + + Hostname + + + Username + + + Password + + + Connection failed + + + Local + + + Remote + + + Local + + + Server + + + Lookup Paths + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Styles/ColorsDark.axaml b/OpenSSH_GUI/Resources/Styles/ColorsDark.axaml new file mode 100644 index 0000000..a4dba5d --- /dev/null +++ b/OpenSSH_GUI/Resources/Styles/ColorsDark.axaml @@ -0,0 +1,146 @@ + + + + #FF0D1117 + + + #FF161B22 + + + #FF1C2128 + + + + #800D1117 + + + + #FF21262D + + + + #FF30363D + + + + #264EC9B0 + + + + #334EC9B0 + + + + #FFE6EDF3 + + + #FF8B949E + + + + #FFC8A87E + + + + #FF4EC9B0 + + + #FF6DD4BE + + + #FF0D1117 + + + + #FFF0A500 + + + #FFFFB929 + + + #FF0D1117 + + + + + + #FF3FB950 + + + + #FFDB8C00 + + + + #FF6E7681 + + + + #FF2EA472 + + + + #FFCF4949 + + + + #FFFF7A1A + + + + + + #FF79B8FF + + + + #FFFF6B4A + + + + #FFBC8CDF + + + + #FF8BC34A + + + + #FF45D0D0 + + + + #FFB5C77A + + + + #FF8B7EC8 + + + + #FFFF5F57 + + + #FFFFBD2E + + + #FF28C840 + + + #FF1A7A7A + #FF1D8E8E + #FF155F5F + #FF0F4040 + #FF155F5F + #FFE6EDF3 + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Styles/ColorsLight.axaml b/OpenSSH_GUI/Resources/Styles/ColorsLight.axaml new file mode 100644 index 0000000..f744fd9 --- /dev/null +++ b/OpenSSH_GUI/Resources/Styles/ColorsLight.axaml @@ -0,0 +1,127 @@ + + + + #FFF0F4F8 + + + #FFE2E8EF + + + #FFFFFFFF + + + #80F0F4F8 + + + + #FF2D333B + + + + #FFB8C5D0 + + + #401A8A75 + + + #401A8A75 + + + + #FF1A2332 + + + #FF5A6A7A + + + #FF7A6442 + + + + #FF1A8A75 + + + #FF157A67 + + + #FFFFFFFF + + + + #FFC07800 + + + #FFA06400 + + + #FFFFFFFF + + + + #FF1A7A2F + + + #FFB86800 + + + #FF7A8A97 + + + #FF1E6B47 + + + #FF9B2020 + + + #FFC05A00 + + + + #FF3B6FD4 + + + #FFD4350A + + + #FF6B3A9B + + + #FF5A8A1A + + + #FF007A7A + + + #FF6B7A2A + + + #FF4E3FAA + + + + #FFD93025 + + + #FFC07800 + + + #FF1E8C34 + + + #FF007A7A + #FF006868 + #FF005555 + #FFB0CECE + #FF005555 + #FFFFFFFF + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Styles/FlyoutButtonStyles.axaml b/OpenSSH_GUI/Resources/Styles/FlyoutButtonStyles.axaml new file mode 100644 index 0000000..e02a069 --- /dev/null +++ b/OpenSSH_GUI/Resources/Styles/FlyoutButtonStyles.axaml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Styles/SshKeyFileStyle.axaml b/OpenSSH_GUI/Resources/Styles/SshKeyFileStyle.axaml index 40df699..5a105bd 100644 --- a/OpenSSH_GUI/Resources/Styles/SshKeyFileStyle.axaml +++ b/OpenSSH_GUI/Resources/Styles/SshKeyFileStyle.axaml @@ -4,66 +4,29 @@ xmlns:openSshGui="clr-namespace:OpenSSH_GUI.Resources" xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" xmlns:converters="clr-namespace:OpenSSH_GUI.Converters" - xmlns:keys="clr-namespace:OpenSSH_GUI.Core.Lib.Keys;assembly=OpenSSH_GUI.Core" - xmlns:keygen="clr-namespace:SshNet.Keygen;assembly=SshNet.Keygen"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:keys="clr-namespace:OpenSSH_GUI.Core.Lib.Keys;assembly=OpenSSH_GUI.Core"> + + + + + + - - - - - - - - + + + + - - - - - - - - - + + + @@ -74,215 +37,111 @@ + Text="{Binding Format, Converter={x:Static converters:Converter.FormatToStringConverter}}" /> - - - - - - - - - + + + + + + + + + + + - - - - - - - - - DarkCyan - #00838F - #006064 - #004F4F - - #006064 - - White - White - White - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Resources/Styles/ThemeResource.axaml b/OpenSSH_GUI/Resources/Styles/ThemeResource.axaml new file mode 100644 index 0000000..73064b1 --- /dev/null +++ b/OpenSSH_GUI/Resources/Styles/ThemeResource.axaml @@ -0,0 +1,82 @@ + + + #FF4EC9B0 + #FF0D1117 + #FFFF5F57 + + + #FF000000 + #CC000000 + #99000000 + #66000000 + #33000000 + + + #FFE6EDF3 + #FFBFC9D3 + #FF8B949E + #FF6A747E + #FF30363D + + + #FFE6EDF3 + #FFFFFFFF + #FF6E7681 + #FF30363D + #FF21262D + #FF1C2128 + #FF161B22 + #FF6E7681 + #FF3D444D + + + #FF000000 + #CC000000 + #99000000 + #33000000 + + + #1A4EC9B0 + #334EC9B0 + + #FF1A8A75 + #FFF0F4F8 + #FFD93025 + + + #FFFFFFFF + #CCFFFFFF + #99FFFFFF + #66FFFFFF + #33FFFFFF + + + #FF1A2332 + #FF2D3A4A + #FF5A6A7A + #FF7A8A97 + #FFB8C5D0 + + + #FF1A2332 + #FFFFFFFF + #FF7A8A97 + #FFB8C5D0 + #FFF5F7FA + #FFEAEFF4 + #FFE2E8EF + #FFB8C5D0 + #FFD5DCE4 + + + #FF000000 + #CC000000 + #99000000 + #33000000 + + + #1A1A8A75 + #331A8A75 + + \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/AddKeyWindowViewModel.cs b/OpenSSH_GUI/ViewModels/AddKeyWindowViewModel.cs index dfc0611..a33f9d7 100644 --- a/OpenSSH_GUI/ViewModels/AddKeyWindowViewModel.cs +++ b/OpenSSH_GUI/ViewModels/AddKeyWindowViewModel.cs @@ -1,12 +1,18 @@ -using JetBrains.Annotations; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Controls; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenSSH_GUI.Core.Configuration; using OpenSSH_GUI.Core.Extensions; using OpenSSH_GUI.Core.MVVM; using OpenSSH_GUI.Core.Services; -using OpenSSH_GUI.Dialogs.Enums; using OpenSSH_GUI.Dialogs.Interfaces; using OpenSSH_GUI.Resources; using ReactiveUI; +using ReactiveUI.Avalonia; using ReactiveUI.SourceGenerators; using ReactiveUI.Validation.Abstractions; using ReactiveUI.Validation.Contexts; @@ -18,77 +24,139 @@ namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public sealed partial class AddKeyWindowViewModel : ViewModelBase, IValidatableViewModel +public sealed partial class AddKeyWindowViewModel : ViewModelBase, IValidatableViewModel { - private readonly SshKeyManager _sshKeyManager; + private const string KeyPrefix = "id"; + private readonly IOptionsMonitor _applicationConfigurationMonitor; + private readonly ILogger _logger; private readonly IMessageBoxProvider _messageBoxProvider; + private readonly SshKeyManager _sshKeyManager; + + [Reactive] private ApplicationConfiguration _applicationConfiguration = ApplicationConfiguration.Default; + + [ObservableAsProperty(ReadOnly = true)] + private int[] _availableKeySizes = []; + + [ObservableAsProperty(ReadOnly = true)] + private bool _canChangeKeySize; + + [Reactive] private string _chosenPath = string.Empty; + + [ObservableAsProperty(ReadOnly = true)] + private int _comboBoxFontSize; + + [Reactive] private string _comment = SshKeyGenerateInfo.DefaultSshKeyComment; + + [Reactive] private SshKeyFormat _keyFormat = SshKeyGenerateInfo.DefaultSshKeyFormat; + + [Reactive] private string _keyName = string.Empty; + + [Reactive] private string _password = string.Empty; + + [Reactive] private int _selectedKeySize; + + [Reactive] private SshKeyType _selectedKeyType = SshKeyGenerateInfo.DefaultSshKeyType; public AddKeyWindowViewModel(ILogger logger, SshKeyManager sshKeyManager, - IMessageBoxProvider messageBoxProvider) : base(logger) + IOptionsMonitor applicationConfigurationMonitor, + Application application, + IMessageBoxProvider messageBoxProvider) { + _logger = logger; _sshKeyManager = sshKeyManager; + _applicationConfigurationMonitor = applicationConfigurationMonitor; _messageBoxProvider = messageBoxProvider; - - _keyTypeSubscription = this.WhenAnyValue(x => x.SelectedKeyType) - .Subscribe(type => + _comboBoxFontSize = int.Parse(application.Resources[App.SystemFontSize]?.ToString() ?? "14") - 2; + _applicationConfigurationMonitor.OnChange(conf => + { + ApplicationConfiguration = conf; + })?.DisposeWith(Disposables); + + this.WhenAnyValue(vm => vm.ApplicationConfiguration) + .ObserveOn(AvaloniaScheduler.Instance) + .StartWith(ApplicationConfiguration.Default) + .Subscribe(config => { - try - { - KeyName = $"id_{Enum.GetName(type)!.ToLower()}"; - - var ordered = type.SupportedKeySizes.OrderDescending().ToList(); - AvaliableKeySizes = ordered; - SelectedKeySize = ordered.First(); - CanChangeKeySize = ordered.Count > 1; - } - catch (Exception e) + ChosenPath = config.LookupPaths.FirstOrDefault() ?? string.Empty; + }).DisposeWith(Disposables); + + _comboBoxFontSizeHelper = application.GetResourceObservable(App.SystemFontSize) + .StartWith(application.Resources[App.SystemFontSize]) + .WhereNotNull() + .OfType() + .Select(e => e - 2) + .ToProperty(this, vm => vm.ComboBoxFontSize); + + var selectedKeyTypeChanged = this.WhenAnyValue(vm => vm.SelectedKeyType) + .ObserveOn(AvaloniaScheduler.Instance); + + _availableKeySizesHelper = selectedKeyTypeChanged + .Select(e => e.SupportedKeySizes.OrderDescending().ToArray()) + .ToProperty( + this, vm => vm.AvailableKeySizes, + SshKeyGenerateInfo.DefaultSshKeyType.SupportedKeySizes.OrderDescending().ToArray()) + .DisposeWith(Disposables); + + selectedKeyTypeChanged + .Subscribe(e => + { + if (DefaultKeyNames.Values.Any(keyName => + string.IsNullOrWhiteSpace(KeyName) || + string.Equals(keyName, KeyName, StringComparison.OrdinalIgnoreCase))) + if (DefaultKeyNames.TryGetValue(e, out var defaultKeyName)) + KeyName = defaultKeyName; + + SelectedKeySize = e switch { - Logger.LogError(e, "Error reacting to key type change"); - } - }); + SshKeyType.ECDSA => SshKeyGenerateInfo.DefaultEcdsaSshKeyLength, + SshKeyType.ED25519 => SshKeyGenerateInfo.DefaultEd25519SshKeyLength, + SshKeyType.RSA => SshKeyGenerateInfo.DefaultRsaSshKeyLength, + _ => SshKeyGenerateInfo.DefaultSshKeyType.SupportedKeySizes.Max() + }; + }) + .DisposeWith(Disposables); + + _canChangeKeySizeHelper = this.WhenAnyValue(vm => vm.AvailableKeySizes) + .Select(e => e.Length > 1) + .ToProperty( + this, vm => vm.CanChangeKeySize, + initialValue: SshKeyGenerateInfo.DefaultSshKeyType.SupportedKeySizes.Any()) + .DisposeWith(Disposables); + + this.WhenAnyValue(vm => vm.KeyName) + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(name => + { + if (string.IsNullOrWhiteSpace(name) && DefaultKeyNames.TryGetValue(SelectedKeyType, out var value)) + KeyName = value; + }).DisposeWith(Disposables); - KeyNameValidationHelper = this.ValidationRule(e => e.KeyName, IsPropertyValid, StringsAndTexts.AddKeyWindowFilenameError); - SelectedKeyType = SshKeyTypes.First(); + KeyNameValidationHelper = + this.ValidationRule(e => e.KeyName, IsPropertyValid, StringsAndTexts.AddKeyWindowFilenameError) + .DisposeWith(Disposables); } - private static bool IsPropertyValid(string? arg) - { - if(string.IsNullOrWhiteSpace(arg)) return false; - return !File.Exists(Path.Combine(SshConfigFilesExtension.GetBaseSshPath(), arg)); - } + public static IDictionary DefaultKeyNames { get; } = Enum.GetValues() + .Select(type => + new KeyValuePair(type, string.Join("_", KeyPrefix, Enum.GetName(type)!.ToLower()))) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); public static SshKeyType[] SshKeyTypes { get; } = Enum.GetValues(); public static SshKeyFormat[] SshKeyFormats { get; } = Enum.GetValues(); - private readonly IDisposable _keyTypeSubscription; - - [Reactive] - private SshKeyType _selectedKeyType; - - [Reactive] - private IEnumerable _avaliableKeySizes = []; - - [Reactive] - private int _selectedKeySize; - - [Reactive] - private SshKeyFormat _keyFormat = SshKeyFormat.OpenSSH; - - [Reactive] - private string _keyName = "id_rsa"; - - [Reactive] - private bool _canChangeKeySize; - - public string Comment { get; set; } = $"{Environment.UserName}@{Environment.MachineName}"; - public string Password { get; set; } = ""; public ValidationHelper KeyNameValidationHelper { get; } public IValidationContext ValidationContext { get; } = new ValidationContext(); - + + private bool IsPropertyValid(string? arg) + { + if (string.IsNullOrWhiteSpace(arg)) return false; + return !File.Exists(Path.Combine(ChosenPath, arg)); + } + /// - protected override async Task OnBooleanSubmitAsync( + protected override async Task BooleanSubmitAsync( bool inputParameter, CancellationToken cancellationToken = default) { @@ -114,22 +182,15 @@ protected override async Task OnBooleanSubmitAsync( if (!string.IsNullOrWhiteSpace(Comment)) genParm.Comment = Comment; - await _sshKeyManager.GenerateNewKey(fullNewFilePath, genParm); + var genResult = await _sshKeyManager.GenerateNewKey(fullNewFilePath, genParm, true); + genResult.ThrowIfFailure(); + CloseOnBooleanSubmit = true; } catch (Exception e) { - Logger.LogError(e, "Error creating key"); - await _messageBoxProvider.ShowMessageBoxAsync( - StringsAndTexts.Error, - e.Message, - MessageBoxButtons.Ok, - MessageBoxIcon.Error); + _logger.LogError(e, "Error creating key"); + await _messageBoxProvider.ShowErrorMessageBoxAsync(e, StringsAndTexts.Error); + CloseOnBooleanSubmit = false; } } - - public override void Dispose() - { - _keyTypeSubscription.Dispose(); - base.Dispose(); - } } \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/ApplicationSettingsViewModel.cs b/OpenSSH_GUI/ViewModels/ApplicationSettingsViewModel.cs index 57a6a6f..79f4d8d 100644 --- a/OpenSSH_GUI/ViewModels/ApplicationSettingsViewModel.cs +++ b/OpenSSH_GUI/ViewModels/ApplicationSettingsViewModel.cs @@ -1,84 +1,242 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Reactive; +using System.Reactive.Disposables.Fluent; using System.Reactive.Linq; +using Avalonia; +using Avalonia.Platform.Storage; using JetBrains.Annotations; +using Material.Icons; using Microsoft.Extensions.Logging; +using OpenSSH_GUI.Core.Configuration; +using OpenSSH_GUI.Core.Enums; +using OpenSSH_GUI.Core.Interfaces; using OpenSSH_GUI.Core.MVVM; +using OpenSSH_GUI.Dialogs.Enums; using OpenSSH_GUI.Dialogs.Interfaces; +using OpenSSH_GUI.Dialogs.Models; using OpenSSH_GUI.Resources; using ReactiveUI; +using ReactiveUI.Avalonia; using ReactiveUI.SourceGenerators; -using Serilog; using Serilog.Core; using Serilog.Events; namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public partial class ApplicationSettingsViewModel : ViewModelBase +public partial class ApplicationSettingsViewModel : ViewModelBase { + private readonly Application _application; + private readonly ILauncher _launcher; + private readonly LoggingLevelSwitch _levelSwitch; private readonly ILogger _logger; private readonly IMessageBoxProvider _messageBoxProvider; - private readonly LoggingLevelSwitch _levelSwitch; - public static LogEventLevel[] AvailableLogLevels { get; }= Enum.GetValues(); - public static int[] DaysToDelete { get; } = Enumerable.Range(1, 4).Select(i => i * 7).ToArray(); - private readonly IDisposable _levelSwitchSubscription; - private readonly IDisposable _daysToDeleteSubscription; - - [Reactive] - private LogEventLevel _currentLogLevel; - - [Reactive] - private int _daysToDeleteSelected; - - public ObservableCollection LogFiles { get; } = []; - - [ObservableAsProperty] - private bool _canDeleteOldLogFiles; - - public ApplicationSettingsViewModel(ILogger logger, IMessageBoxProvider messageBoxProvider, LoggingLevelSwitch levelSwitch) + private readonly IMutableConfiguration _mutableConfiguration; + private readonly IStorageProvider _storageProvider; + + [ObservableAsProperty(ReadOnly = true)] + private ApplicationConfiguration _applicationConfiguration = ApplicationConfiguration.Default; + + [Reactive] private bool _canDeleteOldLogFiles; + + [Reactive] private LogEventLevel _currentLogLevel; + + [Reactive] private ThemeVariant _currentThemeVariant; + + [Reactive] private int _daysToDeleteSelected; + + [Reactive] private double _fontSize; + + public ApplicationSettingsViewModel(ILogger logger, + IMutableConfiguration mutableConfiguration, + ILauncher launcher, + IStorageProvider storageProvider, + IMessageBoxProvider messageBoxProvider, + Application application, + LoggingLevelSwitch levelSwitch) { _logger = logger; + _mutableConfiguration = mutableConfiguration; + _launcher = launcher; + _storageProvider = storageProvider; _messageBoxProvider = messageBoxProvider; _levelSwitch = levelSwitch; - CurrentLogLevel = levelSwitch.MinimumLevel; - _levelSwitchSubscription = this.WhenAnyValue(model => model.CurrentLogLevel) + _application = application; + _fontSize = _mutableConfiguration.Current.FontSize; + _currentLogLevel = _mutableConfiguration.Current.LogLevel; + _currentThemeVariant = _mutableConfiguration.Current.PreferredTheme; + _daysToDeleteSelected = DaysToDelete[0]; + + _applicationConfigurationHelper = Observable.FromEventPattern( + handler => mutableConfiguration.ConfigurationChanged += handler, + handler => mutableConfiguration.ConfigurationChanged -= handler) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(pattern => pattern.EventArgs) + .StartWith(mutableConfiguration.Current) + .ToProperty(this, vm => vm.ApplicationConfiguration) + .DisposeWith(Disposables); + + Observable + .FromEventPattern( + handler => levelSwitch.MinimumLevelChanged += handler, + handler => levelSwitch.MinimumLevelChanged -= handler + ).ObserveOn(AvaloniaScheduler.Instance) + .Select(pattern => Observable.FromAsync(async () => + { + await OnNextLevel(pattern.EventArgs); + return Unit.Default; + })) + .Switch() + .Subscribe( + _ => { }, + ex => logger.LogError(ex, "Error while changing loglevel") + ) + .DisposeWith(Disposables); + + this.WhenAnyValue(model => model.CurrentLogLevel) + .Skip(1) .DistinctUntilChanged() - .Subscribe(OnNext); - - _daysToDeleteSubscription = this.WhenAnyValue(model => model.DaysToDeleteSelected) - .Subscribe(OnNext); + .Throttle(TimeSpan.FromMilliseconds(300)) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(x => Observable.FromAsync(async () => + { + await OnNextLevel(new LoggingLevelSwitchChangedEventArgs(_levelSwitch.MinimumLevel, x)); + return Unit.Default; + })) + .Switch() + .Subscribe( + _ => { }, + ex => logger.LogError(ex, "Error while changing loglevel") + ) + .DisposeWith(Disposables); - _canDeleteOldLogFilesHelper = this.WhenAnyValue(model => model.LogFiles.Count) + this.WhenAnyValue(model => model.DaysToDeleteSelected) + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(OnNextDaysToDelete) + .DisposeWith(Disposables); + + this.WhenAnyValue(vm => vm.LogFiles.Count) + .ObserveOn(AvaloniaScheduler.Instance) + .DistinctUntilChanged() + .Subscribe(count => { CanDeleteOldLogFiles = count > 0; }) + .DisposeWith(Disposables); + + this.WhenAnyValue(vm => vm.CurrentThemeVariant) + .Skip(1) + .ObserveOn(AvaloniaScheduler.Instance) + .DistinctUntilChanged() + .Subscribe(OnNextTheme) + .DisposeWith(Disposables); + + this.WhenAnyValue(vm => vm.FontSize) + .Skip(1) .DistinctUntilChanged() - .Select(e => e > 0) - .ToProperty(this, model => model.CanDeleteOldLogFiles); - - DeleteOldLogFilesCommand = ReactiveCommand.Create(DeleteOldLogFiles); - ClearWholeCacheCommand = ReactiveCommand.CreateFromTask(ClearWholeCache); - DaysToDeleteSelected = DaysToDelete[0]; + .Throttle(TimeSpan.FromMilliseconds(300)) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(x => Observable.FromAsync(async () => + { + await OnNextFontSize(x); + return Unit.Default; + })) + .Switch() + .Subscribe( + _ => { }, + ex => logger.LogError(ex, "Error while changing font size") + ) + .DisposeWith(Disposables); + } + + public static LogEventLevel[] AvailableLogLevels { get; } = Enum.GetValues(); + public static ThemeVariant[] ThemeVariants { get; } = Enum.GetValues(); + public static int[] DaysToDelete { get; } = Enumerable.Range(1, 4).Select(i => i * 7).ToArray(); + + public ObservableCollection LogFiles { get; } = []; + + + [ReactiveCommand] + private Task DeleteLookupPathAsync(string path, CancellationToken cancellationToken = default) => + _mutableConfiguration.SetPropertyValueAsync(conf => conf.LookupPaths, _mutableConfiguration.Current.LookupPaths.Where(p => p != path).ToArray(), cancellationToken); + + [ReactiveCommand] + private async Task AddLookupPathAsync(CancellationToken cancellationToken = default) + { + if (await _storageProvider.OpenFolderPickerAsync( + new FolderPickerOpenOptions + { + AllowMultiple = false + }) is { Count: > 0 } folders) + { + foreach (var folder in folders) + { + var localPathNullable = folder.TryGetLocalPath(); + _logger.LogDebug("Checking folder: {Path}", localPathNullable); + if (localPathNullable is null) + { + _logger.LogWarning("Folder {Path} is not accessible", folder.Path); + await _messageBoxProvider.ShowMessageBoxAsync( + new MessageBoxParams + { + Buttons = MessageBoxButtons.Ok, + Icon = MaterialIconKind.FolderRemoveOutline, + Message = "This folder is not accessible.", + Title = "Folder not accessible" + }); + continue; + } + if (_mutableConfiguration.Current.LookupPaths.Contains(localPathNullable)) + { + _logger.LogWarning("Folder {Path} is already in the lookup paths", localPathNullable); + await _messageBoxProvider.ShowMessageBoxAsync( + new MessageBoxParams + { + Buttons = MessageBoxButtons.Ok, + Icon = MaterialIconKind.FolderRemoveOutline, + Message = "This folder is already in the lookup paths.", + Title = "Folder already in lookup paths" + }); + continue; + } + _logger.LogDebug("Adding lookup path: {Path}", folder); + await _mutableConfiguration.SetPropertyValueAsync( + conf => conf.LookupPaths, _mutableConfiguration.Current.LookupPaths.Append(localPathNullable).ToArray(), cancellationToken); + } + } + } + + [ReactiveCommand] + private async Task OnNextFontSize(double obj) + { + _application.Resources[App.SystemFontSize] = obj; + await _mutableConfiguration.SetPropertyValueAsync(conf => conf.FontSize, obj); } - - public ReactiveCommand DeleteOldLogFilesCommand { get; } - public ReactiveCommand ClearWholeCacheCommand { get; } + + [ReactiveCommand] private async Task ClearWholeCache(CancellationToken cancellationToken = default) { - var loggerConfiguration = Core.Configuration.LoggerConfiguration.Default; - var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDomain.CurrentDomain.FriendlyName); - if ((await _messageBoxProvider.ShowValidatedInputAsync(StringsAndTexts.ApplicationSettingsViewModelAreYouSure, - string.Format(StringsAndTexts.ApplicationSettingsViewModelConfirmMessageBoxContent.Replace("\\n", Environment.NewLine), cachePath, StringsAndTexts.ApplicationSettingsViewModelConfirmDialogConfirmValue), + var loggerConfiguration = LoggerConfiguration.Default; + var cachePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppDomain.CurrentDomain.FriendlyName); + if (await _messageBoxProvider.ShowValidatedInputAsync( + StringsAndTexts.ApplicationSettingsViewModelAreYouSure, + string.Format( + StringsAndTexts.ApplicationSettingsViewModelConfirmMessageBoxContent.Replace( + "\\n", + Environment.NewLine), cachePath, + StringsAndTexts.ApplicationSettingsViewModelConfirmDialogConfirmValue), inputToValidate => { ArgumentException.ThrowIfNullOrWhiteSpace(inputToValidate); - return string.Equals(inputToValidate, StringsAndTexts.ApplicationSettingsViewModelConfirmDialogConfirmValue, StringComparison.Ordinal) + return string.Equals( + inputToValidate, + StringsAndTexts.ApplicationSettingsViewModelConfirmDialogConfirmValue, StringComparison.Ordinal) ? null : StringsAndTexts.ApplicationSettingsViewModelConfirmationError; - })) is { IsConfirmed: false }) return; + }) is { IsConfirmed: false }) return; var stopWatch = Stopwatch.StartNew(); foreach (var file in Directory.EnumerateFiles(cachePath, "*", SearchOption.AllDirectories)) - { try { File.Delete(file); @@ -88,12 +246,11 @@ private async Task ClearWholeCache(CancellationToken cancellationToken = default { _logger.LogError(e, "Error deleting file: {File}", file); } - } + foreach (var directory in Directory.EnumerateDirectories(cachePath, "*", SearchOption.AllDirectories)) - { try { - if(directory == loggerConfiguration.LogFilePath) continue; + if (directory == loggerConfiguration.LogFilePath) continue; Directory.Delete(directory, true); _logger.LogInformation("Deleted directory: {Directory}", directory); } @@ -101,15 +258,15 @@ private async Task ClearWholeCache(CancellationToken cancellationToken = default { _logger.LogError(e, "Error deleting directory: {Directory}", directory); } - } + stopWatch.Stop(); _logger.LogInformation("Cache cleared in {ElapsedTime} ms", stopWatch.Elapsed.Milliseconds); } - + + [ReactiveCommand] private void DeleteOldLogFiles() { foreach (var logFile in LogFiles) - { try { if (!File.Exists(logFile)) continue; @@ -120,34 +277,63 @@ private void DeleteOldLogFiles() { _logger.LogError(e, "Failed to delete log file: {LogFile}", logFile); } - } + LogFiles.Clear(); } - private void OnNext(int obj) + [ReactiveCommand] + private Task OpenCacheFolder(CancellationToken token = default) => _launcher.LaunchDirectoryInfoAsync( + new DirectoryInfo( + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppDomain.CurrentDomain.FriendlyName))); + + private async void OnNextTheme(ThemeVariant variant) { - _logger.LogDebug("Days to delete selected: {Days}", obj); - var logConfiguration = Core.Configuration.LoggerConfiguration.Default; - LogFiles.Clear(); - foreach (var logFile in Directory.EnumerateFiles(logConfiguration.LogFilePath, "*.log", SearchOption.TopDirectoryOnly)) + try { - var extractedDate = Path.GetFileName(logFile).Replace(AppDomain.CurrentDomain.FriendlyName, string.Empty)[..8]; - if(DateOnly.TryParseExact(extractedDate, "yyyyMMdd", out var dateTime) && DateTime.Now.Subtract(dateTime.ToDateTime(TimeOnly.MinValue)) > TimeSpan.FromDays(obj)) - LogFiles.Add(logFile); + var themeVariant = variant switch + { + ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark, + ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light, + _ => Avalonia.Styling.ThemeVariant.Default + }; + if (_application.ActualThemeVariant == themeVariant) return; + _logger.LogDebug( + "Changing Theme Variant from {OldThemeVariant} to {ThemeVariant}", + _application.ActualThemeVariant.Key.ToString(), themeVariant.Key); + _application.RequestedThemeVariant = themeVariant; + await _mutableConfiguration.SetPropertyValueAsync(conf => conf.PreferredTheme, variant); + } + catch (Exception e) + { + _logger.LogError(e, "Error while changing Theme Variant from {OldThemeVariant} to {ThemeVariant}", _application.ActualThemeVariant.Key.ToString(), variant.ToString()); } } - private void OnNext(LogEventLevel obj) + private void OnNextDaysToDelete(int obj) { - _levelSwitch.MinimumLevel = obj; - _logger.LogCritical("Log level changed to {LogLevel}", obj); + var logConfiguration = LoggerConfiguration.Default; + LogFiles.Clear(); + foreach (var logFile in Directory.EnumerateFiles( + logConfiguration.LogFilePath, "*.log", + SearchOption.TopDirectoryOnly)) + { + var fileName = Path.GetFileName(logFile).Replace(AppDomain.CurrentDomain.FriendlyName, string.Empty); + if (fileName.Length < 8) continue; + var extractedDate = fileName[..8]; + if (DateOnly.TryParseExact(extractedDate, "yyyyMMdd", out var dateTime) && + DateTime.Now.Subtract(dateTime.ToDateTime(TimeOnly.MinValue)) > TimeSpan.FromDays(obj)) + LogFiles.Add(logFile); + } } - public override void Dispose() + private async Task OnNextLevel(LoggingLevelSwitchChangedEventArgs obj) { - _levelSwitchSubscription.Dispose(); - _daysToDeleteSubscription.Dispose(); - GC.SuppressFinalize(this); - base.Dispose(); + if (_levelSwitch.MinimumLevel == obj.NewLevel) + return; + _levelSwitch.MinimumLevel = obj.NewLevel; + _logger.LogCritical("Log level changed from {OldLogLevel} to {NewLogLevel}", obj.OldLevel, obj.NewLevel); + await _mutableConfiguration.SetPropertyValueAsync(conf => conf.LogLevel, obj.NewLevel); } } \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/ConnectToServerViewModel.cs b/OpenSSH_GUI/ViewModels/ConnectToServerViewModel.cs index a8dca63..84d0083 100644 --- a/OpenSSH_GUI/ViewModels/ConnectToServerViewModel.cs +++ b/OpenSSH_GUI/ViewModels/ConnectToServerViewModel.cs @@ -1,15 +1,19 @@ -using System.Reactive; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Reactive; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; +using Avalonia; using Avalonia.Media; using JetBrains.Annotations; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using OpenSSH_GUI.Core.Extensions; -using OpenSSH_GUI.Core.Interfaces.Credentials; -using OpenSSH_GUI.Core.Lib.Credentials; using OpenSSH_GUI.Core.Lib.Keys; +using OpenSSH_GUI.Core.Lib.Misc; using OpenSSH_GUI.Core.MVVM; using OpenSSH_GUI.Core.Services; -using OpenSSH_GUI.Dialogs.Enums; using OpenSSH_GUI.Dialogs.Interfaces; using OpenSSH_GUI.Resources; using OpenSSH_GUI.SshConfig.Models; @@ -19,30 +23,68 @@ namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public sealed partial class ConnectToServerViewModel : ViewModelBase +public sealed partial class ConnectToServerViewModel : ViewModelBase { - private readonly ServerConnectionService _serverConnectionService; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly IMessageBoxProvider _messageBoxProvider; - private readonly IDisposable _hostSettingsSubscription; - private readonly IDisposable _keyComboBoxEnabledSubscription; - private readonly IDisposable _connectionCredentialsSubscription; + private readonly ServerConnectionService _serverConnectionService; + + [Reactive] private bool _authWithAllKeys; + + [Reactive] private bool _authWithPublicKey; + + [Reactive] private bool _canConnectToServer; + + [Reactive] private ConnectionCredentials? _connectionCredentials; + + [Reactive(SetModifier = AccessModifier.Private)] + private bool _enablePreConfiguredHosts; + + [Reactive] private string _hostName = string.Empty; + + [Reactive] private bool _keyComboBoxEnabled; + + [Reactive] private string _password = string.Empty; + [Reactive] private SshHostSettings? _selectedHostSettings; + + [Reactive] private SshKeyFile? _selectedPublicKey; + + [ReactiveCollection] private ObservableCollection _sshHostSettings = []; + + [Reactive] private SshKeyManager _sshKeyManager; + + [Reactive] private IBrush _statusButtonBackground = + Application.Current?.Resources["OverlayBrush"] as IBrush ?? Brushes.Gray; + + [Reactive] private string _statusButtonText = string.Format( + StringsAndTexts.ConnectToServerStatusBase, + StringsAndTexts.ConnectToServerStatusUnknown); + + [Reactive] private string _statusButtonToolTip = string.Format( + StringsAndTexts.ConnectToServerStatusBase, + StringsAndTexts.ConnectToServerStatusUntested); + + [Reactive] private bool _tryingToConnect; + + [Reactive] private string _username = string.Empty; public ConnectToServerViewModel(ILogger logger, ServerConnectionService serverConnectionService, IMessageBoxProvider messageBoxProvider, IConfiguration configuration, - SshKeyManager sshKeyManager) : base(logger) + SshKeyManager sshKeyManager) { + _logger = logger; _messageBoxProvider = messageBoxProvider; + _configuration = configuration; _serverConnectionService = serverConnectionService; SshKeyManager = sshKeyManager; SelectedPublicKey = SshKeyManager.SshKeys.FirstOrDefault(); - TestConnection = ReactiveCommand.CreateFromTask(TestConnectionAsync); - ResetCommand = ReactiveCommand.Create(Reset); - _hostSettingsSubscription = this + this .WhenAnyValue(viewModel => viewModel.SelectedHostSettings) - .Subscribe(async settings => + .SelectMany(async settings => { try { @@ -53,112 +95,90 @@ public ConnectToServerViewModel(ILogger logger, { logger.LogError(e, "Error testing connection"); } - }); - - _keyComboBoxEnabledSubscription = this - .WhenAnyValue(viewModel => viewModel.AuthWithPublicKey, model => model.AuthWithAllKeys, model => model._serverConnectionService.IsConnected) - .Subscribe((tuple) => - { - KeyComboBoxEnabled = tuple is { Item3: false, Item1: true, Item2: false }; - }); - _connectionCredentialsSubscription = this.WhenAnyValue(viewModel => viewModel.ConnectionCredentials) - .Subscribe(credentials => - { - CanConnectToServer = credentials is not null; - }); - + return Unit.Default; + }) + .Subscribe() + .DisposeWith(Disposables); + + this + .WhenAnyValue( + viewModel => viewModel.AuthWithPublicKey, model => model.AuthWithAllKeys, + model => model._serverConnectionService.IsConnected) + .Subscribe(tuple => { KeyComboBoxEnabled = tuple is { Item3: false, Item1: true, Item2: false }; }) + .DisposeWith(Disposables); + + this.WhenAnyValue(viewModel => viewModel.ConnectionCredentials) + .Subscribe(credentials => { CanConnectToServer = credentials is not null; }).DisposeWith(Disposables); + + Observable + .FromEventPattern( + h => ((INotifyCollectionChanged)SshHostSettings).CollectionChanged += h, + h => ((INotifyCollectionChanged)SshHostSettings).CollectionChanged -= h) + .Select(_ => SshHostSettings.Count) + .StartWith(SshHostSettings.Count) + .Subscribe(count => { EnablePreConfiguredHosts = count > 0; }) + .DisposeWith(Disposables); + } + + public override ValueTask InitializeAsync(CancellationToken cancellationToken = default) + { try { - var config = configuration.GetSection("SshConfig").Get(); - SshHostSettings = config?.Hosts.Distinct() ?? []; + SshHostSettings.Clear(); + foreach (var hostSettings in _configuration.GetSection("SshConfig").Get()?.Hosts + .Distinct() ?? []) + { + _logger.LogDebug("Found host {host}", hostSettings.HostName); + SshHostSettings.Add(hostSettings); + } } catch (Exception e) { - SshHostSettings = []; - logger.LogDebug(e, "Config not readable"); + _logger.LogDebug(e, "Config not readable"); } - } - [Reactive] - private IConnectionCredentials? _connectionCredentials; - - [Reactive] private bool _canConnectToServer; - - public ReactiveCommand TestConnection { get; } - public ReactiveCommand ResetCommand { get; } - public SshKeyManager SshKeyManager { get; } - public bool EnablePreConfiguredHosts => SshHostSettings.Any(); - [Reactive] private SshHostSettings? _selectedHostSettings; - - public IEnumerable SshHostSettings { get; } - - [Reactive] private bool _authWithPublicKey; - - [Reactive] private bool _authWithAllKeys; - - [Reactive] private SshKeyFile? _selectedPublicKey; - - [Reactive] private string _hostName = string.Empty; - - [Reactive] private string _username = string.Empty; - - [Reactive] private string _password = string.Empty; - - [Reactive] private bool _tryingToConnect; - - [Reactive] private string _statusButtonToolTip = string.Format(StringsAndTexts.ConnectToServerStatusBase, - StringsAndTexts.ConnectToServerStatusUntested); - - [Reactive] private string _statusButtonText = string.Format(StringsAndTexts.ConnectToServerStatusBase, - StringsAndTexts.ConnectToServerStatusUnknown); - - [Reactive] private IBrush _statusButtonBackground = Brushes.Gray; - - [Reactive] private bool _keyComboBoxEnabled; + return base.InitializeAsync(cancellationToken); + } - private async Task TestConnectionAsyncBase(CancellationToken cancellationToken = default) + private void TestConnectionAsyncBase() { if (ConnectionCredentials is not null) { - StatusButtonText = string.Format(StringsAndTexts.ConnectToServerStatusBase, + StatusButtonText = string.Format( + StringsAndTexts.ConnectToServerStatusBase, StringsAndTexts.ConnectToServerStatusSuccess); StatusButtonToolTip = string.Format(StringsAndTexts.ConnectToServerSshConnectionString, Username, HostName); - StatusButtonBackground = Brushes.Green; + StatusButtonBackground = Application.Current?.Resources["SuccessBrush"] as IBrush ?? Brushes.Green; } else { - StatusButtonText = string.Format(StringsAndTexts.ConnectToServerStatusBase, + StatusButtonText = string.Format( + StringsAndTexts.ConnectToServerStatusBase, StringsAndTexts.ConnectToServerStatusFailed); - StatusButtonBackground = Brushes.Red; + StatusButtonBackground = Application.Current?.Resources["ErrorBrush"] as IBrush ?? Brushes.Red; } TryingToConnect = false; - - if (_serverConnectionService.IsConnected) - { - await _serverConnectionService.CloseConnection(false, cancellationToken); - return; - } - - await _messageBoxProvider!.ShowMessageBoxAsync(StringsAndTexts.Error, StatusButtonToolTip, MessageBoxButtons.Ok, - MessageBoxIcon.Error); } - private async Task TestConnectionAsync(SshHostSettings? hostSettings = null, CancellationToken cancellationToken = default) + private async ValueTask TestConnectionAsync(SshHostSettings? hostSettings = null, + CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(hostSettings); if (hostSettings.IdentityFiles is null) { - StatusButtonText = string.Format(StringsAndTexts.ConnectToServerStatusBase, + StatusButtonText = string.Format( + StringsAndTexts.ConnectToServerStatusBase, StringsAndTexts.ConnectToServerStatusFailed); - StatusButtonBackground = Brushes.Red; + StatusButtonBackground = Application.Current?.Resources["ErrorBrush"] as IBrush ?? Brushes.Red; return; } using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - linkedTokenSource.CancelAfter(TimeSpan.FromSeconds(5)); + if (!Debugger.IsAttached) + linkedTokenSource.CancelAfter(TimeSpan.FromSeconds(5)); try { @@ -171,16 +191,16 @@ private async Task TestConnectionAsync(SshHostSettings? hostSettings = null, Can .Select(f => f.ResolvePath()) .ToHashSet(StringComparer.Ordinal); - var keys = (SshKeyManager.SshKeys ?? []) - .Where(e => e.KeyFileInfo?.KeyFileSource?.AbsolutePath is { } path + var keys = SshKeyManager.SshKeys + .Where(e => e.KeyFileInfo?.KeyFileSource.AbsolutePath is { } path && resolvedPaths.Contains(path)); var connectionCredentials = new MultiKeyConnectionCredentials( hostSettings.HostName ?? string.Empty, hostSettings.User ?? string.Empty, keys); - - if(await _serverConnectionService.EstablishConnection(connectionCredentials, linkedTokenSource.Token)) + + if (await _serverConnectionService.EstablishConnection(connectionCredentials, linkedTokenSource.Token)) ConnectionCredentials = connectionCredentials; } catch (Exception exception) @@ -188,31 +208,28 @@ private async Task TestConnectionAsync(SshHostSettings? hostSettings = null, Can StatusButtonToolTip = exception.Message; } - await TestConnectionAsyncBase(cancellationToken); + TestConnectionAsyncBase(); } - private async Task TestConnectionAsync(CancellationToken cancellationToken = default) + [ReactiveCommand] + private async ValueTask TestConnectionAsync(CancellationToken cancellationToken = default) { using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); linkedTokenSource.CancelAfter(TimeSpan.FromSeconds(5)); try { - if(string.IsNullOrWhiteSpace(HostName) || string.IsNullOrWhiteSpace(Username) || (SelectedPublicKey is null && string.IsNullOrWhiteSpace(Password))) + if (string.IsNullOrWhiteSpace(HostName) || string.IsNullOrWhiteSpace(Username) || + SelectedPublicKey is null && string.IsNullOrWhiteSpace(Password)) throw new ArgumentException(StringsAndTexts.ConnectToServerValidationError); TryingToConnect = true; - IConnectionCredentials? connectionCredentials = null; + ConnectionCredentials? connectionCredentials; if (AuthWithPublicKey) - { connectionCredentials = new KeyConnectionCredentials(HostName, Username, SelectedPublicKey); - } else if (AuthWithAllKeys) - { + else if (AuthWithAllKeys) connectionCredentials = new MultiKeyConnectionCredentials(HostName, Username, SshKeyManager.SshKeys); - } else - { connectionCredentials = new PasswordConnectionCredentials(HostName, Username, Password); - } - if(await _serverConnectionService.EstablishConnection(connectionCredentials, linkedTokenSource.Token)) + if (await _serverConnectionService.EstablishConnection(connectionCredentials, linkedTokenSource.Token)) ConnectionCredentials = connectionCredentials; } catch (Exception exception) @@ -220,49 +237,44 @@ private async Task TestConnectionAsync(CancellationToken cancellationToken = def StatusButtonToolTip = exception.Message; } - await TestConnectionAsyncBase(cancellationToken); + TestConnectionAsyncBase(); } - protected override async Task OnBooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) + protected override async Task BooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) { if (!inputParameter) return; if (!CanConnectToServer) return; if (ConnectionCredentials is null) return; try { - if(!(await _serverConnectionService.EstablishConnection(ConnectionCredentials, cancellationToken))) - { - await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.Error, "Connection failed", MessageBoxButtons.Ok, MessageBoxIcon.Error); - } + if (!await _serverConnectionService.EstablishConnection(ConnectionCredentials, cancellationToken)) + await _messageBoxProvider.ShowMessageBoxAsync( + StringsAndTexts.Error, + StringsAndTexts.ConnectToServerConnectionFailed); } catch (Exception e) { - Logger.LogError(e, "Unhandled error during connection"); - await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.Error, e.Message, MessageBoxButtons.Ok, MessageBoxIcon.Error); + _logger.LogError(e, "Unhandled error during connection"); + await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.Error, e.Message); } } + [ReactiveCommand] private void Reset() { HostName = string.Empty; Username = string.Empty; Password = string.Empty; - StatusButtonText = string.Format(StringsAndTexts.ConnectToServerStatusBase, + StatusButtonText = string.Format( + StringsAndTexts.ConnectToServerStatusBase, StringsAndTexts.ConnectToServerStatusUnknown); - StatusButtonToolTip = string.Format(StringsAndTexts.ConnectToServerStatusBase, + StatusButtonToolTip = string.Format( + StringsAndTexts.ConnectToServerStatusBase, StringsAndTexts.ConnectToServerStatusUntested); - StatusButtonBackground = Brushes.Gray; + StatusButtonBackground = Application.Current?.Resources["OverlayBrush"] as IBrush ?? Brushes.Gray; SelectedHostSettings = null; AuthWithAllKeys = false; AuthWithPublicKey = false; ConnectionCredentials = null; } - - public override void Dispose() - { - _hostSettingsSubscription.Dispose(); - _keyComboBoxEnabledSubscription.Dispose(); - _connectionCredentialsSubscription.Dispose(); - base.Dispose(); - } } \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/EditAuthorizedKeysViewModel.cs b/OpenSSH_GUI/ViewModels/EditAuthorizedKeysViewModel.cs index 0e31ca4..ff25f3b 100644 --- a/OpenSSH_GUI/ViewModels/EditAuthorizedKeysViewModel.cs +++ b/OpenSSH_GUI/ViewModels/EditAuthorizedKeysViewModel.cs @@ -1,4 +1,4 @@ -using System.Reactive; +using System.Reactive.Disposables.Fluent; using System.Reactive.Linq; using JetBrains.Annotations; using Microsoft.Extensions.Logging; @@ -9,55 +9,64 @@ using OpenSSH_GUI.Core.MVVM; using OpenSSH_GUI.Core.Services; using ReactiveUI; +using ReactiveUI.Avalonia; using ReactiveUI.SourceGenerators; namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public partial class EditAuthorizedKeysViewModel : ViewModelBase +public partial class EditAuthorizedKeysViewModel : ViewModelBase { - [ObservableAsProperty] - private bool _addButtonEnabled; - - [ObservableAsProperty] - private bool _keyAddPossible; - - [Reactive] - private SshKeyFile? _selectedKey; - - [Reactive] - private AuthorizedKeysFile _authorizedKeysFileRemote = AuthorizedKeysFile.Empty; - - [Reactive] - private AuthorizedKeysFile _authorizedKeysFileLocal = AuthorizedKeysFile.Empty; - - public EditAuthorizedKeysViewModel(ILogger logger, + private readonly ILogger _logger; + + [ObservableAsProperty] private bool _addButtonEnabled; + + [Reactive] private AuthorizedKeysFile _authorizedKeysFileLocal = AuthorizedKeysFile.Empty; + + [Reactive] private AuthorizedKeysFile _authorizedKeysFileRemote = AuthorizedKeysFile.Empty; + + [ObservableAsProperty] private bool _keyAddPossible; + + [Reactive] private SshKeyFile? _selectedKey; + + public EditAuthorizedKeysViewModel( + ILogger logger, SshKeyManager sshKeyManager, - ServerConnectionService serverConnectionService) : base(logger) + ServerConnectionService serverConnectionService) { + _logger = logger; SshKeyManager = sshKeyManager; ServerConnectionService = serverConnectionService; SelectedKey = SshKeyManager.SshKeys.FirstOrDefault(); - AddKey = ReactiveCommand.CreateFromTask(OnAddKey); - - _addButtonEnabledHelper = this.WhenAnyValue(vm => vm.SelectedKey, vm => vm.AuthorizedKeysFileRemote, vm => vm.KeyAddPossible) + + _addButtonEnabledHelper = this.WhenAnyValue( + vm => vm.SelectedKey, vm => vm.AuthorizedKeysFileRemote, + vm => vm.KeyAddPossible) .DistinctUntilChanged() .Select(props => - { - if (props is { Item2: { AuthorizedKeys: { Count: > 0 } } col, Item1: { } keyFile , Item3: true}) - return col.CanAddKey(keyFile); - return false; - }).ToProperty(this, vm => vm.AddButtonEnabled); - - _keyAddPossibleHelper = this.WhenAnyValue(vm => vm.SshKeyManager.SshKeysCount) - .Select(props => props > 0).ToProperty(this, vm => vm.KeyAddPossible); + props is + { + Item1: { } keyFile, + Item2: + { + AuthorizedKeys.Count: > 0 + } col, + Item3: true + } && col.CanAddKey(keyFile)) + .ToProperty(this, vm => vm.AddButtonEnabled) + .DisposeWith(Disposables); + + _keyAddPossibleHelper = this.WhenAnyValue(vm => vm.SshKeyManager.SshKeys) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(keys => keys.Count > 0) + .ToProperty(this, vm => vm.KeyAddPossible) + .DisposeWith(Disposables); } - + public SshKeyManager SshKeyManager { get; } public ServerConnectionService ServerConnectionService { get; } - public ReactiveCommand AddKey { get; } - protected override async Task OnBooleanSubmitAsync(bool inputParameter, + protected override async Task BooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) { try @@ -71,7 +80,7 @@ await ServerConnectionService.ServerConnection.WriteAuthorizedKeysChangesToServe } catch (Exception e) { - Logger.LogError(e, "Error while editing authorized keys"); + _logger.LogError(e, "Error while editing authorized keys"); } } @@ -85,8 +94,6 @@ public override async ValueTask InitializeAsync(CancellationToken cancellationTo await base.InitializeAsync(cancellationToken); } - private async Task OnAddKey(SshKeyFile key) - { - await AuthorizedKeysFileRemote.AddAuthorizedKeyAsync(key); - } + [ReactiveCommand] + private async Task AddKey(SshKeyFile key, CancellationToken cancellationToken = default) { await AuthorizedKeysFileRemote.AddAuthorizedKeyAsync(key); } } \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/EditKnownHostsWindowViewModel.cs b/OpenSSH_GUI/ViewModels/EditKnownHostsWindowViewModel.cs index 8bace48..913fa85 100644 --- a/OpenSSH_GUI/ViewModels/EditKnownHostsWindowViewModel.cs +++ b/OpenSSH_GUI/ViewModels/EditKnownHostsWindowViewModel.cs @@ -1,58 +1,52 @@ using System.Collections.ObjectModel; using JetBrains.Annotations; -using Microsoft.Extensions.Logging; using OpenSSH_GUI.Core.Enums; using OpenSSH_GUI.Core.Extensions; -using OpenSSH_GUI.Core.Interfaces.KnownHosts; using OpenSSH_GUI.Core.Lib.KnownHosts; using OpenSSH_GUI.Core.MVVM; using OpenSSH_GUI.Core.Services; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public partial class EditKnownHostsWindowViewModel( - ILogger logger, - ServerConnectionService serverConnectionService) : ViewModelBase(logger) +public partial class EditKnownHostsWindowViewModel(ServerConnectionService serverConnectionService) : ViewModelBase { + [Reactive] private ObservableCollection _knownHostsLocal = []; + + [Reactive] private ObservableCollection _knownHostsRemote = []; + public ServerConnectionService ServerConnectionService => serverConnectionService; - private IKnownHostsFile? KnownHostsFileLocal { get; set; } - private IKnownHostsFile? KnownHostsFileRemote { get; set; } - [Reactive] - private ObservableCollection _knownHostsRemote = []; - - [Reactive] - private ObservableCollection _knownHostsLocal = []; + private KnownHostsFile? KnownHostsFileLocal { get; set; } + private KnownHostsFile? KnownHostsFileRemote { get; set; } - protected override async Task OnBooleanSubmitAsync(bool inputParameter, + protected override async Task BooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) { if (!inputParameter) return; ArgumentNullException.ThrowIfNull(KnownHostsFileLocal); - - KnownHostsFileLocal.SyncKnownHosts(KnownHostsLocal); - if (serverConnectionService.IsConnected) - KnownHostsFileRemote?.SyncKnownHosts(KnownHostsRemote); await KnownHostsFileLocal.UpdateFileAsync(); if (!serverConnectionService.IsConnected) return; ArgumentNullException.ThrowIfNull(KnownHostsFileRemote); - await serverConnectionService.ServerConnection.WriteKnownHostsToServerAsync(KnownHostsFileRemote, + await serverConnectionService.ServerConnection.WriteKnownHostsToServerAsync( + KnownHostsFileRemote, cancellationToken); } public override async ValueTask InitializeAsync(CancellationToken cancellationToken = default) { KnownHostsFileLocal = - await new KnownHostsFile().InitializeAsync(SshConfigFiles.Known_Hosts.GetPathOfFile(), + await KnownHostsFile.InitializeAsync( + new FileInfo(SshConfigFiles.Known_Hosts.GetPathOfFile()), token: cancellationToken); if (serverConnectionService.IsConnected) KnownHostsFileRemote = await serverConnectionService.ServerConnection.GetKnownHostsFromServerAsync(cancellationToken); - KnownHostsLocal = new ObservableCollection(KnownHostsFileLocal.KnownHosts.OrderBy(e => e.Host)); - KnownHostsRemote = serverConnectionService.IsConnected ? new ObservableCollection(KnownHostsFileRemote!.KnownHosts.OrderBy(e => e.Host)) : []; + KnownHostsLocal = new ObservableCollection(KnownHostsFileLocal.KnownHosts.OrderBy(e => e.Host)); + KnownHostsRemote = serverConnectionService.IsConnected + ? new ObservableCollection(KnownHostsFileRemote!.KnownHosts.OrderBy(e => e.Host)) + : []; await base.InitializeAsync(cancellationToken); } } \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/ExportWindowViewModel.cs b/OpenSSH_GUI/ViewModels/ExportWindowViewModel.cs index 3cf222c..8f5ad30 100644 --- a/OpenSSH_GUI/ViewModels/ExportWindowViewModel.cs +++ b/OpenSSH_GUI/ViewModels/ExportWindowViewModel.cs @@ -7,15 +7,14 @@ namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public partial class ExportWindowViewModel(ILogger logger, IClipboard clipboard) : ViewModelBase(logger) +public partial class ExportWindowViewModel(ILogger logger, IClipboard clipboard) + : ViewModelBase<(string WindowTitle, string Export)> { - [Reactive] - private string _windowTitle = ""; - - [Reactive] - private string _export = ""; - - public override ValueTask InitializeAsync(ExportWindowViewModelInitializerParameters parameters, + [Reactive] private string _export = string.Empty; + + [Reactive] private string _windowTitle = string.Empty; + + public override ValueTask InitializeAsync((string WindowTitle, string Export) parameters, CancellationToken cancellationToken = default) { WindowTitle = parameters.WindowTitle; @@ -23,15 +22,22 @@ public override ValueTask InitializeAsync(ExportWindowViewModelInitializerParame return base.InitializeAsync(parameters, cancellationToken); } - protected override async Task OnBooleanSubmitAsync(bool inputParameter, + protected override async Task BooleanSubmitAsync(bool inputParameter, CancellationToken cancellationToken = default) { - if (inputParameter) - await clipboard.SetTextAsync(Export); + try + { + if (inputParameter) + await clipboard.SetTextAsync(Export); + } + catch (Exception e) + { + logger.LogError(e, "Error submitting export to clipboard"); + } } } -public record ExportWindowViewModelInitializerParameters : IInitializerParameters +public record ExportWindowViewModelInitializerParameters { public string WindowTitle { get; init; } = string.Empty; public string Export { get; init; } = string.Empty; diff --git a/OpenSSH_GUI/ViewModels/FileInfoWindowViewModel.cs b/OpenSSH_GUI/ViewModels/FileInfoWindowViewModel.cs index 1d8fae6..0f5296c 100644 --- a/OpenSSH_GUI/ViewModels/FileInfoWindowViewModel.cs +++ b/OpenSSH_GUI/ViewModels/FileInfoWindowViewModel.cs @@ -1,24 +1,256 @@ +using System.Collections.ObjectModel; +using System.Reactive.Disposables.Fluent; +using System.Reactive.Linq; +using Avalonia.Input.Platform; +using DynamicData; using JetBrains.Annotations; +using Material.Icons; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using OpenSSH_GUI.Core.Lib.Keys; using OpenSSH_GUI.Core.MVVM; +using OpenSSH_GUI.Core.Services; +using OpenSSH_GUI.Dialogs.Enums; +using OpenSSH_GUI.Dialogs.Interfaces; +using OpenSSH_GUI.Dialogs.Models; +using OpenSSH_GUI.Resources; +using ReactiveUI; +using ReactiveUI.Avalonia; using ReactiveUI.SourceGenerators; +using SshNet.Keygen; namespace OpenSSH_GUI.ViewModels; + [UsedImplicitly] -public partial class FileInfoWindowViewModel : ViewModelBase +public partial class FileInfoWindowViewModel : ViewModelBase { - [Reactive] + private readonly IClipboard _clipboard; + private readonly SshKeyManager _keyManager; + private readonly ILogger _logger; + private readonly IMessageBoxProvider _messageBoxProvider; + private readonly IServiceProvider _serviceProvider; + + [ObservableAsProperty(ReadOnly = true)] + private string _associatedFilesHeader = string.Empty; + + [ObservableAsProperty(ReadOnly = true)] + private SshKeyFormat _defaultKeyFormat; + + [Reactive(SetModifier = AccessModifier.Private)] private SshKeyFile _keyFile; - - public override ValueTask InitializeAsync(FileInfoViewModelInitializer parameters, CancellationToken cancellationToken = default) + + [ReactiveCollection] + private ObservableCollection _keyFormats = []; + + [ObservableAsProperty(ReadOnly = true)] + private string _password = string.Empty; + + [ObservableAsProperty(ReadOnly = true)] + private string _windowTitle = "Key info"; + + public FileInfoWindowViewModel(ILogger logger, IMessageBoxProvider messageBoxProvider, + IServiceProvider serviceProvider, IClipboard clipboard, SshKeyManager keyManager) + { + _logger = logger; + _messageBoxProvider = messageBoxProvider; + _serviceProvider = serviceProvider; + _clipboard = clipboard; + _keyManager = keyManager; + _keyFile = _serviceProvider.GetRequiredService(); + + _passwordHelper = this.WhenAnyValue(vm => vm.KeyFile.Password.IsValid) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(_ => KeyFile.Password.IsValid + ? KeyFile.Password.GetPasswordString() + : string.Empty + ).ToProperty(this, vm => vm.Password) + .DisposeWith(Disposables); + + _windowTitleHelper = + this.WhenAnyValue(vm => vm.KeyFile.FileName, vm => vm.KeyFile.Format, vm => vm.KeyFile.Comment) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(e => string.Join(" ", e.Item1, e.Item2, e.Item3)) + .ToProperty(this, vm => vm.WindowTitle) + .DisposeWith(Disposables); + + _associatedFilesHeaderHelper = this.WhenAnyValue(vm => vm.KeyFile.KeyFiles) + .ObserveOn(AvaloniaScheduler.Instance) + .Select(e => string.Format(StringsAndTexts.FileInfoWindowFoundAssociatedFiles, e.Length)) + .ToProperty(this, vm => vm.AssociatedFilesHeader) + .DisposeWith(Disposables); + + _defaultKeyFormatHelper = this.WhenAnyValue(vm => vm.KeyFile.KeyFileInfo) + .ObserveOn(AvaloniaScheduler.Instance) + .WhereNotNull() + .Select(e => e.DefaultConversionFormat) + .ToProperty(this, vm => vm.DefaultKeyFormat) + .DisposeWith(Disposables); + + this.WhenAnyValue(vm => vm.KeyFile.KeyFileInfo) + .ObserveOn(AvaloniaScheduler.Instance) + .WhereNotNull() + .Subscribe(OnNext) + .DisposeWith(Disposables); + } + + private void OnNext(SshKeyFileInformation obj) + { + KeyFormats.Clear(); + KeyFormats.AddRange(obj.AvailableFormatsForConversion.Order().ToArray()); + } + + private void SetKeyOrDefault(SshKeyFileSource? source = null) + { + KeyFile = (source is not null + ? _keyManager.SshKeys.SingleOrDefault(x => x.KeyFileInfo?.KeyFileSource == source) + : null) + ?? _serviceProvider.GetRequiredService(); + } + + public override ValueTask InitializeAsync(SshKeyFileSource? parameters, + CancellationToken cancellationToken = default) { - KeyFile = parameters.Key; - + SetKeyOrDefault(parameters); return base.InitializeAsync(parameters, cancellationToken); } -} -public class FileInfoViewModelInitializer : IInitializerParameters -{ - public required SshKeyFile Key { get; init; } + [ReactiveCommand] + private async Task ChangePasswordOfKeyFileAsync(CancellationToken cancellationToken = default) + { + try + { + using var si = await _messageBoxProvider.ShowSecureInputAsync( + new SecureInputParams + { + Buttons = MessageBoxButtons.OkCancel, + Icon = MaterialIconKind.KeyOutline, + MinLength = 0, + Prompt = string.Format(StringsAndTexts.FileInfoWindowEnterNewPassword, KeyFile.FileName), + Title = StringsAndTexts.FileInfoWindowChangePassword + }); + if (si is null) + { + _logger.LogInformation("User canceled password change"); + return; + } + + (await _keyManager.ChangePasswordOfKeyAsync(KeyFile, si.Value, token: cancellationToken)).ThrowIfFailure(); + _logger.LogInformation("Key file password changed"); + SetKeyOrDefault(KeyFile.KeyFileInfo?.KeyFileSource); + } + catch (Exception e) + { + _logger.LogError(e, "Error changing password of key file"); + await _messageBoxProvider.ShowErrorMessageBoxAsync(e); + } + } + + [ReactiveCommand] + private async Task ChangeFormatOfKeyFileAsync(SshKeyFormat format, CancellationToken cancellationToken = default) + { + try + { + (await _keyManager.ChangeFormatOfKeyAsync(KeyFile, format, cancellationToken)).ThrowIfFailure(); + _logger.LogInformation("Key file format changed"); + SetKeyOrDefault(KeyFile.KeyFileInfo?.KeyFileSource); + } + catch (Exception e) + { + _logger.LogError(e, "Error changing format of key file"); + await _messageBoxProvider.ShowErrorMessageBoxAsync(e); + } + } + + [ReactiveCommand] + private async Task ChangeFileNameAsync(SshKeyFile keyFile, CancellationToken cancellationToken = default) + { + var validatedInputResult = await _messageBoxProvider.ShowValidatedInputAsync( + new ValidatedInputParams + { + Buttons = MessageBoxButtons.OkCancel, + Icon = MaterialIconKind.FileEditOutline, + InitialValue = Path.GetFileNameWithoutExtension(keyFile.FileName) ?? string.Empty, + Message = StringsAndTexts.FileInfoWindowChangeMessage, + Prompt = StringsAndTexts.FileInfoWindowEnterNewFilename, + Watermark = StringsAndTexts.FileInfoWindowEnterNewFilename, + Validator = argument => string.IsNullOrWhiteSpace(argument) + ? StringsAndTexts.FileInfoWindowFilenameCannotBeEmpty + : null + }); + if (validatedInputResult is { IsConfirmed: true, Value: { Length: > 0 } filename }) + try + { + var result = await _keyManager.RenameKeyAsync(keyFile, filename, token: cancellationToken); + while (result is { IsSuccess: false }) + { + result.ThrowIfFailure(); + + if (await _messageBoxProvider.ShowMessageBoxAsync( + new MessageBoxParams + { + Title = StringsAndTexts.FileInfoWindowConfirmFileOverwrite, + Message = + string.Format(StringsAndTexts.FileInfoWindowFileAlreadyExists, filename), + Buttons = MessageBoxButtons.YesNo, + Icon = MaterialIconKind.QuestionMarkCircleOutline + }) is not MessageBoxResult.Yes) + throw new OperationCanceledException("User canceled operation"); + _logger.LogInformation("User confirmed overwrite of key file"); + result = await _keyManager.RenameKeyAsync(keyFile, filename, true, cancellationToken); + } + + _logger.LogInformation("Key file renamed"); + SetKeyOrDefault(KeyFile.KeyFileInfo?.KeyFileSource); + } + catch (Exception e) + { + _logger.LogError(e, "Error renaming key file"); + await _messageBoxProvider.ShowErrorMessageBoxAsync(e); + } + else + _logger.LogInformation("User canceled key file rename"); + } + + [ReactiveCommand] + private async Task DeleteKeyAsync(SshKeyFile keyFile, CancellationToken cancellationToken = default) + { + if (await _messageBoxProvider.ShowMessageBoxAsync( + string.Format(StringsAndTexts.MainWindowViewModelDeleteKeyTitleText, keyFile.FileName), + StringsAndTexts.MainWindowViewModelDeleteKeyQuestionTextPair, MessageBoxButtons.YesNo, + MaterialIconKind.QuestionMarkCircleOutline) is MessageBoxResult.Yes) + try + { + (await _keyManager.TryDeleteKeyAsync(keyFile, cancellationToken)).ThrowIfFailure(); + _logger.LogInformation("Key file deleted"); + RequestClose(); + } + catch (Exception e) + { + _logger.LogError(e, "Error deleting key file"); + await _messageBoxProvider.ShowErrorMessageBoxAsync( + e, + string.Format(StringsAndTexts.MainWindowViewModelDeleteKeyTitleText, keyFile.FileName)); + } + else + _logger.LogInformation("User canceled key file deletion"); + } + + [ReactiveCommand] + private async Task CopyPasswordIntoClipboardAsync(SshKeyFilePassword password, CancellationToken token = default) + { + try + { + await _clipboard.SetTextAsync(password.GetPasswordString()); + await _clipboard.FlushAsync(); + await _messageBoxProvider.ShowMessageBoxAsync( + StringsAndTexts.FileInfoWindowPasswordCopied, + StringsAndTexts.FileInfoWindowPasswordCopied, MessageBoxButtons.Ok, + MaterialIconKind.InformationOutline); + } + catch (Exception e) + { + _logger.LogError(e, "Error copying password to clipboard"); + await _messageBoxProvider.ShowErrorMessageBoxAsync(e); + } + } } \ No newline at end of file diff --git a/OpenSSH_GUI/ViewModels/MainWindowViewModel.cs b/OpenSSH_GUI/ViewModels/MainWindowViewModel.cs index 2ae7cf5..b7e5af8 100644 --- a/OpenSSH_GUI/ViewModels/MainWindowViewModel.cs +++ b/OpenSSH_GUI/ViewModels/MainWindowViewModel.cs @@ -1,13 +1,10 @@ -using System.Reactive; +using System.Collections.Specialized; +using System.Reactive.Disposables.Fluent; using System.Reactive.Linq; using System.Reflection; -using System.Text.Encodings.Web; using Avalonia.Platform.Storage; -using Avalonia.Threading; -using DryIoc; using JetBrains.Annotations; using Material.Icons; -using Material.Icons.Avalonia; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using OpenSSH_GUI.Core.Extensions; @@ -22,6 +19,7 @@ using OpenSSH_GUI.Resources; using OpenSSH_GUI.Views; using ReactiveUI; +using ReactiveUI.Avalonia; using ReactiveUI.SourceGenerators; using Renci.SshNet; using SshNet.Keygen.Extensions; @@ -29,188 +27,125 @@ namespace OpenSSH_GUI.ViewModels; [UsedImplicitly] -public partial class MainWindowViewModel : ViewModelBase +public partial class MainWindowViewModel : ViewModelBase { private static readonly string? ProjectUrl = Assembly.GetExecutingAssembly() .GetCustomAttributes() .FirstOrDefault(a => a.Key == "ProjectUrl")?.Value; private readonly IDialogHost _dialogHost; - private readonly IMessageBoxProvider _messageBoxProvider; private readonly ILauncher _launcher; - private readonly IResolver _serviceProvider; - private readonly IDisposable[] _subscriptions = []; + private readonly ILogger _logger; + private readonly IMessageBoxProvider _messageBoxProvider; + private readonly IServiceProvider _serviceProvider; + + [ObservableAsProperty(ReadOnly = true)] + private bool _isProvidePasswordExecuting; + + [ObservableAsProperty(ReadOnly = true)] + private string _itemsCountTooltip = string.Empty; + + [Reactive(SetModifier = AccessModifier.Private)] + private string _version; + + [ObservableAsProperty(ReadOnly = true)] + private string _windowTitle = string.Empty; public MainWindowViewModel( ILogger logger, SshKeyManager sshKeyManager, ServerConnectionService serverConnectionService, - IResolver serviceProvider, + IServiceProvider serviceProvider, IConfiguration configuration, IMessageBoxProvider messageBoxProvider, ILauncher launcher, - IDialogHost dialogHost) : base(logger) + IDialogHost dialogHost) { SshKeyManager = sshKeyManager; ServerConnectionService = serverConnectionService; + _logger = logger; _serviceProvider = serviceProvider; _messageBoxProvider = messageBoxProvider; _launcher = launcher; _dialogHost = dialogHost; - - DisconnectServer = ReactiveCommand.CreateFromTask(DisconnectFromServerAsync); - ProvidePassword = ReactiveCommand.CreateFromTask(ProvidePasswordAsync); - NotImplementedMessage = ReactiveCommand.CreateFromTask(ShowNotImplementedMessageBoxAsync); - OpenBrowser = ReactiveCommand.CreateFromTask(OpenBrowserAsync); - OpenExportKeyWindowPublic = ReactiveCommand.CreateFromTask(ShowPublicKeyExportWindow); - OpenExportKeyWindowPrivate = ReactiveCommand.CreateFromTask(ShowPrivateKeyExportWindow); - OpenConnectToServerWindow = - ReactiveCommand.CreateFromTask(OpenWindow); - OpenEditKnownHostsWindow = - ReactiveCommand.CreateFromTask(OpenWindow); - OpenEditAuthorizedKeysWindow = - ReactiveCommand.CreateFromTask(OpenWindow); - OpenCreateKeyWindow = ReactiveCommand.CreateFromTask(OpenWindow); - DeleteKey = ReactiveCommand.CreateFromTask(DeleteKeyAsync); - ReloadKeys = ReactiveCommand.CreateFromTask(SshKeyManager.RerunSearchAsync); - ShowPassword = ReactiveCommand.CreateFromTask(ShowPasswordExportWindow); - ResetKey = ReactiveCommand.CreateFromTask(ResetKeyAsync); - ChangeFilename = ReactiveCommand.CreateFromTask(ChangeFilenameAsync); - OpenApplicationSettingsWindow = ReactiveCommand.CreateFromTask(OpenWindow); - Version = configuration[Program.VersionEnvVar] ?? "VERSION ERROR"; - - _itemsCountIconHelper = this.WhenAnyValue(vm => vm.SshKeyManager.SshKeys.Count) - .Select(GetMaterialNumericIcon) - .ToProperty(this, vm => vm.ItemsCountIcon); - + _windowTitleHelper = this.WhenAnyValue(vm => vm.Version) - .Select(ver => string.Join("-", Program.AppName, ver)) - .ToProperty(this, vm => vm.WindowTitle); - - _keyTypeSortDirectionIconHelper = - this.WhenAnyValue(vm => vm.KeyTypeSort) - .Select(EvaluateSortIconKind) - .ToProperty(this, vm => vm.KeyTypeSortDirectionIcon); - - _commentSortDirectionIconHelper = this.WhenAnyValue(vm => vm.CommentSort) - .Select(EvaluateSortIconKind) - .ToProperty(this, vm => vm.CommentSortDirectionIcon); - - _fingerPrintSortDirectionIconHelper = this.WhenAnyValue(vm => vm.FingerPrintSort) - .Select(EvaluateSortIconKind) - .ToProperty(this, vm => vm.FingerPrintSortDirectionIcon); - - _subscriptions = _subscriptions.Concat([ - this.WhenAnyValue(vm => vm.KeyTypeSort) - .Subscribe(sort => SshKeyManager.ChangeOrder(sort switch - { - null => key => key.OrderBy(e => e.FileName), - true => key => key.OrderBy(e => e.KeyType), - false => key => key.OrderByDescending(e => e.KeyType) - })), - - this.WhenAnyValue(vm => vm.CommentSort) - .Subscribe(sort => SshKeyManager.ChangeOrder(sort switch - { - null => key => key.OrderBy(e => e.FileName), - true => key => key.OrderBy(e => e.Comment), - false => key => key.OrderByDescending(e => e.Comment) - })), - - this.WhenAnyValue(vm => vm.FingerPrintSort) - .Subscribe(sort => SshKeyManager.ChangeOrder(sort switch - { - null => key => key.OrderBy(e => e.FileName), - true => key => key.OrderBy(e => e.Fingerprint), - false => key => key.OrderByDescending(e => e.Fingerprint) - })) - ]).ToArray(); + .Select(v => string.Join(" v", Program.AppName, v)) + .ToProperty(this, vm => vm.WindowTitle) + .DisposeWith(Disposables); + + var sshKeysCountChanged = Observable + .FromEventPattern( + h => ((INotifyCollectionChanged)SshKeyManager.SshKeys).CollectionChanged += h, + h => ((INotifyCollectionChanged)SshKeyManager.SshKeys).CollectionChanged -= h) + .Select(_ => SshKeyManager.SshKeys.Count) + .StartWith(SshKeyManager.SshKeys.Count) + .ObserveOn(AvaloniaScheduler.Instance); + + _itemsCountTooltipHelper = sshKeysCountChanged + .Select(count => string.Format(StringsAndTexts.MainWindowFoundKeyPairsCountLabel, count)) + .ToProperty(this, vm => vm.ItemsCountTooltip) + .DisposeWith(Disposables); + _isProvidePasswordExecutingHelper = ProvidePasswordCommand.IsExecuting + .ToProperty(this, vm => vm.IsProvidePasswordExecuting) + .DisposeWith(Disposables); } - public ReactiveCommand DisconnectServer { get; } - public ReactiveCommand ProvidePassword { get; } - public ReactiveCommand NotImplementedMessage { get; } - public ReactiveCommand OpenBrowser { get; } - public ReactiveCommand OpenExportKeyWindowPublic { get; } - public ReactiveCommand OpenExportKeyWindowPrivate { get; } - public ReactiveCommand OpenConnectToServerWindow { get; } - public ReactiveCommand OpenEditKnownHostsWindow { get; } - public ReactiveCommand OpenEditAuthorizedKeysWindow { get; } - public ReactiveCommand OpenCreateKeyWindow { get; } - public ReactiveCommand DeleteKey { get; } - public ReactiveCommand ReloadKeys { get; } - public ReactiveCommand ShowPassword { get; } - public ReactiveCommand ResetKey { get; } - public ReactiveCommand ChangeFilename { get; } - public ReactiveCommand OpenApplicationSettingsWindow { get; } public ServerConnectionService ServerConnectionService { get; } public SshKeyManager SshKeyManager { get; } - [Reactive] private string _version; - [Reactive] private bool? _keyTypeSort; - [Reactive] private bool? _commentSort; - [Reactive] private bool? _fingerPrintSort; + [ReactiveCommand] + private Task OpenApplicationSettingsWindowAsync(CancellationToken cancellationToken = default) => + OpenWindow(cancellationToken); - [ObservableAsProperty] private MaterialIcon _itemsCountIcon = new() { Kind = MaterialIconKind.Infinity }; - [ObservableAsProperty] private string _windowTitle = string.Empty; - [ObservableAsProperty] private MaterialIconKind _keyTypeSortDirectionIcon = MaterialIconKind.CircleOutline; - [ObservableAsProperty] private MaterialIconKind _commentSortDirectionIcon = MaterialIconKind.CircleOutline; - [ObservableAsProperty] private MaterialIconKind _fingerPrintSortDirectionIcon = MaterialIconKind.CircleOutline; + [ReactiveCommand] + private Task OpenFileInfoWindowAsync(SshKeyFileSource source, CancellationToken cancellationToken = default) => + OpenWindow( + source, + cancellationToken); - private static MaterialIcon GetMaterialNumericIcon(int count) => new() - { - Kind = count switch - { - 0 => MaterialIconKind.NumericZero, - 1 => MaterialIconKind.NumericOne, - 2 => MaterialIconKind.NumericTwo, - 3 => MaterialIconKind.NumericThree, - 4 => MaterialIconKind.NumericFour, - 5 => MaterialIconKind.NumericFive, - 6 => MaterialIconKind.NumericSix, - 7 => MaterialIconKind.NumericSeven, - 8 => MaterialIconKind.NumericEight, - 9 => MaterialIconKind.NumericNine, - 10 => MaterialIconKind.Numeric10, - _ => MaterialIconKind.Infinity - }, - Width = 20, - Height = 20 - }; - - private async Task ResetKeyAsync(SshKeyFile keyFile, CancellationToken token) + [ReactiveCommand] + private Task OpenCreateKeyWindowAsync(CancellationToken cancellationToken = default) => OpenWindow(cancellationToken); + + [ReactiveCommand] + private Task OpenEditAuthorizedKeysWindowAsync(CancellationToken cancellationToken = default) => + OpenWindow(cancellationToken); + + [ReactiveCommand] + private Task OpenEditKnownHostsWindowAsync(CancellationToken cancellationToken = default) => OpenWindow(cancellationToken); + + [ReactiveCommand] + private Task OpenConnectToServerWindowAsync(CancellationToken cancellationToken = default) => OpenWindow(cancellationToken); + + [ReactiveCommand] + private void ResetKey(SshKeyFile keyFile) { try { - await keyFile.Reset(); + keyFile.Reset(); } catch (Exception e) { - Logger.LogError(e, "Unhandled error during key reset"); + _logger.LogError(e, "Unhandled error during key reset"); } } - private async Task ChangeFilenameAsync(SshKeyFile key, CancellationToken token = default) + [ReactiveCommand] + private async Task ReloadKeysAsync(CancellationToken cancellationToken = default) { - var validatedInputResult = await _messageBoxProvider.ShowValidatedInputAsync(new ValidatedInputParams + switch (await SshKeyManager.RerunSearchAsync(cancellationToken)) { - Buttons = MessageBoxButtons.OkCancel, - Icon = MaterialIconKind.FileEditOutline, - InitialValue = key.FileName ?? string.Empty, - Message = "ChangeMe", - Prompt = "EnterNewFilename", - Watermark = "Enter new filename", - Validator = argument => - { - ArgumentException.ThrowIfNullOrWhiteSpace(argument); - return SshKeyManager.SshKeys.Any(k => k.FileName == argument) ? "Filename already exists" : null; - } - }); - if (validatedInputResult is { IsConfirmed: true, Value: { Length: > 0 } filename }) - key.ChangeFilenameOnDisk(filename); + case { IsSuccess: false } x: + await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.Error, x.Exception.Message); + break; + default: + _logger.LogInformation("Keys reloaded"); + break; + } } + [ReactiveCommand] private Task ShowPrivateKeyExportWindow(SshKeyFile key, CancellationToken token = default) { PrivateKeyFile? keyFile = key; @@ -218,13 +153,12 @@ private Task ShowPrivateKeyExportWindow(SshKeyFile key, CancellationToken token switch (key) { case null or { NeedsPassword: true, Password.IsValid: false }: - Logger.LogError("Keyfile is null"); + _logger.LogError("Keyfile is null"); return Task.CompletedTask; - case { NeedsPassword: false, Password.IsValid: true } passwordProtectedKeyFile: + case { NeedsPassword: false, Password.IsValid: true }: { - keyFile = passwordProtectedKeyFile; if (keyFile is not null) - content = keyFile.ToOpenSshFormat(passwordProtectedKeyFile.Password.GetPasswordString()); + content = keyFile.ToOpenSshFormat(key.Password.GetPasswordString()); break; } default: @@ -238,70 +172,74 @@ private Task ShowPrivateKeyExportWindow(SshKeyFile key, CancellationToken token return ShowExportWindow(key, content, null, token); } + [ReactiveCommand] private Task ShowPublicKeyExportWindow(SshKeyFile key, CancellationToken token = default) { PrivateKeyFile? keyFile = key; - var content = keyFile is not null ? keyFile.ToOpenSshPublicFormat() : string.Empty; - return ShowExportWindow(key, content, null, token); - } - - private async Task ShowPasswordExportWindow(SshKeyFile key, CancellationToken token = default) - { - if (!key.Password.IsValid) return; - if (await _messageBoxProvider.ShowMessageBoxAsync(new MessageBoxParams - { - Title = "Are you shure?", - Message = - "Are you shure you want to export the password?\r\nThe password can be stored in plain text in the clipboard afterwards", - Buttons = MessageBoxButtons.YesNo - }) is MessageBoxResult.Yes) - await ShowExportWindow(key, key.Password.GetPasswordString(), - string.Format(StringsAndTexts.KeysShowPasswordOf, key.AbsoluteFilePath), token); + return keyFile != null + ? ShowExportWindow(key, keyFile.ToOpenSshPublicFormat(), null, token) + : _messageBoxProvider.ShowMessageBoxAsync( + StringsAndTexts.Error, + StringsAndTexts.MainWindowViewModelExportKeyErrorMessage); } private async Task ShowExportWindow(SshKeyFile key, string content, string? windowTitle = null, CancellationToken token = default) { - windowTitle ??= string.Format(StringsAndTexts.MainWindowViewModelDynamicExportWindowTitle, - key.HashAlgorithmName, key.FileName); + windowTitle ??= string.Format( + StringsAndTexts.MainWindowViewModelDynamicExportWindowTitle, + key.KeyType, key.FileName); if (string.IsNullOrWhiteSpace(content)) { - await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.Error, - StringsAndTexts.MainWindowViewModelExportKeyErrorMessage, MessageBoxButtons.Ok, MessageBoxIcon.Error); + await _messageBoxProvider.ShowMessageBoxAsync( + StringsAndTexts.Error, + StringsAndTexts.MainWindowViewModelExportKeyErrorMessage); return; } - var view = await _serviceProvider.ResolveViewAsync( - new ExportWindowViewModelInitializerParameters - { - Export = content, - WindowTitle = windowTitle - }, token: token); + var view = await _serviceProvider + .ResolveViewAsync( + new ValueTuple + { + Item1 = windowTitle, + Item2 = content + }, token: token); await _dialogHost.ShowDialog(view); } + [ReactiveCommand] private async Task ShowNotImplementedMessageBoxAsync(CancellationToken cancellationToken = default) { - await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.NotImplementedBoxTitle, - StringsAndTexts.NotImplementedBoxText, MessageBoxButtons.Ok, MessageBoxIcon.Information); + await _messageBoxProvider.ShowMessageBoxAsync( + StringsAndTexts.NotImplementedBoxTitle, + StringsAndTexts.NotImplementedBoxText, MessageBoxButtons.Ok, MaterialIconKind.InformationOutline); } + [ReactiveCommand] private async Task OpenBrowserAsync(int commandTypeParameter, CancellationToken cancellationToken = default) { - if (commandTypeParameter switch - { - 1 => string.Join("/", ProjectUrl, "issues"), - 2 => string.Join("#", ProjectUrl, "authors"), - _ => ProjectUrl - } is { Length: > 0 } url) - await _launcher.LaunchUriAsync(new Uri(HtmlEncoder.Default.Encode(url))); - } + if (ProjectUrl is null) + return; + var uriBuilder = new UriBuilder(ProjectUrl); + switch (commandTypeParameter) + { + case 1: + uriBuilder.Path += "/issues"; + break; + case 2: + uriBuilder.Query = "tab=readme-ov-file"; + uriBuilder.Fragment = "authors"; + break; + } + await _launcher.LaunchUriAsync(uriBuilder.Uri); + } + [ReactiveCommand] private async Task DisconnectFromServerAsync(CancellationToken cancellationToken) { var messageBoxText = StringsAndTexts.MainWindowDisconnectBoxTextSuccess; - var messageBoxIcon = MessageBoxIcon.Information; + var messageBoxIcon = MaterialIconKind.InformationOutline; if (ServerConnectionService.IsConnected) { try @@ -311,81 +249,83 @@ private async Task DisconnectFromServerAsync(CancellationToken cancellationToken catch (Exception exception) { messageBoxText = exception.Message; - messageBoxIcon = MessageBoxIcon.Error; + messageBoxIcon = MaterialIconKind.ErrorOutline; } } else { messageBoxText = StringsAndTexts.MainWindowDisconnectBoxTextNone; - messageBoxIcon = MessageBoxIcon.Error; + messageBoxIcon = MaterialIconKind.ErrorOutline; } - await _messageBoxProvider.ShowMessageBoxAsync(StringsAndTexts.MainWindowDisconnectBoxTitle, messageBoxText, + await _messageBoxProvider.ShowMessageBoxAsync( + StringsAndTexts.MainWindowDisconnectBoxTitle, messageBoxText, MessageBoxButtons.Ok, messageBoxIcon); } - private async Task OpenWindow(CancellationToken token = default) - where TWindow : WindowBase where TViewModel : ViewModelBase - { - await _dialogHost.ShowDialog( - await _serviceProvider.ResolveViewAsync(token: token)); - } - + [ReactiveCommand] private async Task DeleteKeyAsync(SshKeyFile sshKeyFile, CancellationToken cancellationToken = default) { if (await _messageBoxProvider.ShowMessageBoxAsync( string.Format(StringsAndTexts.MainWindowViewModelDeleteKeyTitleText, sshKeyFile.FileName), StringsAndTexts.MainWindowViewModelDeleteKeyQuestionTextPair, MessageBoxButtons.YesNo, - MessageBoxIcon.Question) is MessageBoxResult.Yes) - if(!sshKeyFile.Delete(out var error)) + MaterialIconKind.QuestionBoxOutline) is MessageBoxResult.Yes) + if (await SshKeyManager.TryDeleteKeyAsync(sshKeyFile, cancellationToken) is + { IsSuccess: false, Exception: { } error }) await _messageBoxProvider.ShowMessageBoxAsync( string.Format(StringsAndTexts.MainWindowViewModelDeleteKeyTitleText, sshKeyFile.FileName) - , error.Message, MessageBoxButtons.Ok, MessageBoxIcon.Error); + , error.Message); } + [ReactiveCommand] private async Task ProvidePasswordAsync(SshKeyFile key, CancellationToken cancellationToken = default) { - var trys = 0; + if (!await _messageBoxProvider.ShowRetryMessageBoxAsync( + async () => + { + using var secureInputResult = await _messageBoxProvider.ShowSecureInputAsync( + new SecureInputParams + { + Title = StringsAndTexts.MainWindowViewModelProvidePasswordPromptHeading, + Prompt = string.Join( + Environment.NewLine, + StringsAndTexts.MainWindowViewModelProvidePasswordPromptBodyHeading, + Path.GetFileName(key.AbsoluteFilePath)) + }); + bool? operationResult = secureInputResult switch + { + null => null, + { Value.Length: <= 0 } => true, + { Value.Length: > 0 } => key.SetPassword(secureInputResult.Value.Span) + }; + if (operationResult is false) + key.Reset(); + return operationResult; + }, StringsAndTexts.MainWindowViewModelProvidePasswordErrorHeading, + StringsAndTexts.MainWindowViewModelProvidePasswordErrorContent, + retries: 3, showTryCountInTitle: true, icon: MaterialIconKind.WarningOutline)) + await _messageBoxProvider.ShowErrorMessageBoxAsync( + customMessage: string.Join( + " ", "Key", key.FileName, + "could not be opened correctly")); + } - while (key.NeedsPassword && trys < 3) - { - using var secureInputResult = await _messageBoxProvider.ShowSecureInputAsync( - StringsAndTexts.MainWindowViewModelProvidePasswordPromptHeading, - string.Format(StringsAndTexts.MainWindowViewModelProvidePasswordPromptBodyHeading, - Path.GetFileName(key.AbsoluteFilePath))); - if (secureInputResult != null && await key.SetPassword(secureInputResult.Value)) - return; - - - if (await _messageBoxProvider.ShowMessageBoxAsync( - StringsAndTexts.MainWindowViewModelProvidePasswordErrorHeading, string.Format( - StringsAndTexts.MainWindowViewModelProvidePasswordErrorContent, - trys + 1, 3), MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning) is MessageBoxResult - .Cancel) - break; - trys++; - } + private async Task OpenWindow(TInitializer param, + CancellationToken token = default) + where TWindow : WindowBase + where TViewModel : ViewModelBase + { + await _dialogHost.ShowDialog( + await _serviceProvider.ResolveViewAsync(param, token: token) + ); } - - private static MaterialIconKind EvaluateSortIconKind(bool? value) => - value switch - { - null => MaterialIconKind.CircleOutline, - true => MaterialIconKind.ChevronDownCircleOutline, - false => MaterialIconKind.ChevronUpCircleOutline - }; - public override void Dispose() + private async Task OpenWindow(CancellationToken token = default) + where TWindow : WindowBase + where TViewModel : ViewModelBase { - _windowTitleHelper.Dispose(); - _itemsCountIconHelper.Dispose(); - _commentSortDirectionIconHelper.Dispose(); - _fingerPrintSortDirectionIconHelper.Dispose(); - _keyTypeSortDirectionIconHelper.Dispose(); - foreach (var subscription in _subscriptions) - subscription.Dispose(); - GC.SuppressFinalize(this); - base.Dispose(); + await _dialogHost.ShowDialog( + await _serviceProvider.ResolveViewAsync(token: token)); } } \ No newline at end of file diff --git a/OpenSSH_GUI/Views/AddKeyWindow.axaml b/OpenSSH_GUI/Views/AddKeyWindow.axaml index bc145f6..9a1043a 100644 --- a/OpenSSH_GUI/Views/AddKeyWindow.axaml +++ b/OpenSSH_GUI/Views/AddKeyWindow.axaml @@ -2,13 +2,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" xmlns:assets="clr-namespace:OpenSSH_GUI.Resources" - xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350" Width="400" - Height="375" + Height="400" x:Class="OpenSSH_GUI.Views.AddKeyWindow" x:DataType="viewModels:AddKeyWindowViewModel" Title="{x:Static assets:StringsAndTexts.AddKeyWindowTitle}" @@ -16,83 +15,58 @@ ShowInTaskbar="True" ShowActivated="True" WindowStartupLocation="CenterOwner"> - - - - - - - - - - - + ItemsSource="{CompiledBinding AvailableKeySizes}" + SelectedItem="{CompiledBinding SelectedKeySize}" /> + + - - - - - + + + - + + + - + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/AddKeyWindow.axaml.cs b/OpenSSH_GUI/Views/AddKeyWindow.axaml.cs index 413e716..83f2595 100644 --- a/OpenSSH_GUI/Views/AddKeyWindow.axaml.cs +++ b/OpenSSH_GUI/Views/AddKeyWindow.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia.Media.Imaging; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using JetBrains.Annotations; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; using ReactiveUI; @@ -15,9 +12,10 @@ public partial class AddKeyWindow : WindowBase public AddKeyWindow() { InitializeComponent(); - this.WhenActivated(d => + this.WhenActivated(_ => { - this.BindValidation(ViewModel, model => model.KeyName, + this.BindValidation( + ViewModel, model => model.KeyName, window => window.KeyFileNameValidation.Text!); }); } diff --git a/OpenSSH_GUI/Views/ApplicationSettingsWindow.axaml b/OpenSSH_GUI/Views/ApplicationSettingsWindow.axaml index db4488f..93a28b7 100644 --- a/OpenSSH_GUI/Views/ApplicationSettingsWindow.axaml +++ b/OpenSSH_GUI/Views/ApplicationSettingsWindow.axaml @@ -5,51 +5,153 @@ xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:resources="clr-namespace:OpenSSH_GUI.Resources" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" + xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:converter="clr-namespace:OpenSSH_GUI.Core.Resources.Converter;assembly=OpenSSH_GUI.Core" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="OpenSSH_GUI.Views.ApplicationSettingsWindow" x:DataType="viewModels:ApplicationSettingsViewModel" - Width="400" + Width="800" Height="375" CanResize="False" ShowInTaskbar="True" ShowActivated="True" Title="{x:Static resources:StringsAndTexts.ApplicationSettingsWindowTitle}" WindowStartupLocation="CenterOwner"> - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/ConnectToServerWindow.axaml.cs b/OpenSSH_GUI/Views/ConnectToServerWindow.axaml.cs index 29bb833..c8d6c59 100644 --- a/OpenSSH_GUI/Views/ConnectToServerWindow.axaml.cs +++ b/OpenSSH_GUI/Views/ConnectToServerWindow.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia.Media.Imaging; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using JetBrains.Annotations; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; @@ -10,8 +7,5 @@ namespace OpenSSH_GUI.Views; [UsedImplicitly] public partial class ConnectToServerWindow : WindowBase { - public ConnectToServerWindow() - { - InitializeComponent(); - } + public ConnectToServerWindow() { InitializeComponent(); } } \ No newline at end of file diff --git a/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml b/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml index 5537851..4eea5e8 100644 --- a/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml +++ b/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml @@ -5,8 +5,8 @@ xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" xmlns:openSshGui="clr-namespace:OpenSSH_GUI.Resources" - xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:authorizedKeys="clr-namespace:OpenSSH_GUI.Core.Lib.AuthorizedKeys;assembly=OpenSSH_GUI.Core" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="OpenSSH_GUI.Views.EditAuthorizedKeysWindow" x:DataType="viewModels:EditAuthorizedKeysViewModel" @@ -18,7 +18,7 @@ WindowStartupLocation="CenterOwner"> - + - + - + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml.cs b/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml.cs index 2fd9639..d15ed79 100644 --- a/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml.cs +++ b/OpenSSH_GUI/Views/EditAuthorizedKeysWindow.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia.Media.Imaging; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using JetBrains.Annotations; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; @@ -10,8 +7,5 @@ namespace OpenSSH_GUI.Views; [UsedImplicitly] public partial class EditAuthorizedKeysWindow : WindowBase { - public EditAuthorizedKeysWindow() - { - InitializeComponent(); - } + public EditAuthorizedKeysWindow() { InitializeComponent(); } } \ No newline at end of file diff --git a/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml b/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml index d987727..fc0260c 100644 --- a/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml +++ b/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml @@ -5,8 +5,8 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" xmlns:openSshGui="clr-namespace:OpenSSH_GUI.Resources" - xmlns:knownHosts="clr-namespace:OpenSSH_GUI.Core.Interfaces.KnownHosts;assembly=OpenSSH_GUI.Core" - xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" + xmlns:knownHosts="clr-namespace:OpenSSH_GUI.Core.Lib.KnownHosts;assembly=OpenSSH_GUI.Core" mc:Ignorable="d" x:Class="OpenSSH_GUI.Views.EditKnownHostsWindow" x:DataType="viewModels:EditKnownHostsWindowViewModel" @@ -20,7 +20,7 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + @@ -102,9 +106,9 @@ - + - + - + - - - - - - - + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml.cs b/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml.cs index 34d14f5..2deb318 100644 --- a/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml.cs +++ b/OpenSSH_GUI/Views/EditKnownHostsWindow.axaml.cs @@ -1,8 +1,4 @@ -using Avalonia.Controls; -using Avalonia.Media.Imaging; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using JetBrains.Annotations; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; @@ -11,8 +7,5 @@ namespace OpenSSH_GUI.Views; [UsedImplicitly] public partial class EditKnownHostsWindow : WindowBase { - public EditKnownHostsWindow() - { - InitializeComponent(); - } + public EditKnownHostsWindow() { InitializeComponent(); } } \ No newline at end of file diff --git a/OpenSSH_GUI/Views/ExportWindow.axaml b/OpenSSH_GUI/Views/ExportWindow.axaml index 32d91f9..f0160e2 100644 --- a/OpenSSH_GUI/Views/ExportWindow.axaml +++ b/OpenSSH_GUI/Views/ExportWindow.axaml @@ -2,49 +2,29 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" xmlns:openSshGui="clr-namespace:OpenSSH_GUI.Resources" - xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" Width="500" Height="300" x:Class="OpenSSH_GUI.Views.ExportWindow" x:DataType="viewModels:ExportWindowViewModel" Title="{Binding WindowTitle}"> - - - + + + - - - - - + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/ExportWindow.axaml.cs b/OpenSSH_GUI/Views/ExportWindow.axaml.cs index d040828..6f4c89a 100644 --- a/OpenSSH_GUI/Views/ExportWindow.axaml.cs +++ b/OpenSSH_GUI/Views/ExportWindow.axaml.cs @@ -1,15 +1,11 @@ -using Avalonia.Controls; -using JetBrains.Annotations; +using JetBrains.Annotations; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; namespace OpenSSH_GUI.Views; [UsedImplicitly] -public partial class ExportWindow : WindowBase +public partial class ExportWindow : WindowBase { - public ExportWindow() - { - InitializeComponent(); - } + public ExportWindow() { InitializeComponent(); } } \ No newline at end of file diff --git a/OpenSSH_GUI/Views/FileInfoWindow.axaml b/OpenSSH_GUI/Views/FileInfoWindow.axaml index c592bfa..0e97c0c 100644 --- a/OpenSSH_GUI/Views/FileInfoWindow.axaml +++ b/OpenSSH_GUI/Views/FileInfoWindow.axaml @@ -3,9 +3,230 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:OpenSSH_GUI.ViewModels" + xmlns:io="clr-namespace:System.IO;assembly=System.Runtime" + xmlns:converters="clr-namespace:OpenSSH_GUI.Converters" + xmlns:openSshGui="clr-namespace:OpenSSH_GUI.Resources" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:keygen="clr-namespace:SshNet.Keygen;assembly=SshNet.Keygen" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="OpenSSH_GUI.Views.FileInfoWindow" x:DataType="viewModels:FileInfoWindowViewModel" - Title="FileInfoWindow"> - Welcome to Avalonia! - + Width="400" + Height="375" + CanResize="False" + ShowActivated="True" + WindowStartupLocation="CenterOwner" + Title="{Binding WindowTitle}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/FileInfoWindow.axaml.cs b/OpenSSH_GUI/Views/FileInfoWindow.axaml.cs index 692d2b9..07d0f0f 100644 --- a/OpenSSH_GUI/Views/FileInfoWindow.axaml.cs +++ b/OpenSSH_GUI/Views/FileInfoWindow.axaml.cs @@ -1,16 +1,12 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; using JetBrains.Annotations; +using OpenSSH_GUI.Core.Lib.Keys; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; namespace OpenSSH_GUI.Views; + [UsedImplicitly] -public partial class FileInfoWindow : WindowBase +public partial class FileInfoWindow : WindowBase { - public FileInfoWindow() - { - InitializeComponent(); - } + public FileInfoWindow() { InitializeComponent(); } } \ No newline at end of file diff --git a/OpenSSH_GUI/Views/MainWindow.axaml b/OpenSSH_GUI/Views/MainWindow.axaml index d8e5075..b40ea3c 100644 --- a/OpenSSH_GUI/Views/MainWindow.axaml +++ b/OpenSSH_GUI/Views/MainWindow.axaml @@ -7,6 +7,8 @@ xmlns:openSshGui="clr-namespace:OpenSSH_GUI.Resources" xmlns:converters="clr-namespace:OpenSSH_GUI.Converters" xmlns:sys="clr-namespace:System;assembly=mscorlib" + xmlns:keys="clr-namespace:OpenSSH_GUI.Core.Lib.Keys;assembly=OpenSSH_GUI.Core" + xmlns:controls="clr-namespace:OpenSSH_GUI.Resources.Controls" mc:Ignorable="d" d:DesignWidth="1150" d:DesignHeight="450" x:Class="OpenSSH_GUI.Views.MainWindow" x:DataType="viewModels:MainWindowViewModel" @@ -14,31 +16,48 @@ Height="450" Title="{Binding WindowTitle}"> - - + + + + @@ -46,65 +65,96 @@ - + + - - - - - - + + + + + + + - - - - - + - - - - - - - + + + + + + + + - - - - - + + + + + + + + + + + + + - - + + + + + + + + + + - - - + - - + - + Background="{DynamicResource DisconnectedBrush}" CornerRadius="5"> + - + - - + + + Command="{Binding OpenConnectToServerWindowCommand}"> + Command="{Binding DisconnectFromServerCommand}"> - - + + - + - + - + + - + - - + + - + + + + + + - + - - + + + Command="{Binding ReloadKeysCommand}"> + Command="{Binding OpenApplicationSettingsWindowCommand}"> + Command="{Binding ShowNotImplementedMessageBoxCommand}"> + Command="{Binding ShowNotImplementedMessageBoxCommand}"> @@ -274,7 +333,7 @@ + Command="{Binding ShowNotImplementedMessageBoxCommand}"> @@ -282,7 +341,7 @@ + Command="{Binding ShowNotImplementedMessageBoxCommand}"> @@ -292,22 +351,22 @@ - - - - - + + + + - + - - + + + Command="{Binding OpenBrowserCommand}"> 0 @@ -317,7 +376,7 @@ + Command="{Binding OpenBrowserCommand}"> 1 @@ -326,7 +385,7 @@ + Command="{Binding OpenBrowserCommand}"> 2 @@ -334,11 +393,11 @@ - - - - - + + + + + \ No newline at end of file diff --git a/OpenSSH_GUI/Views/MainWindow.axaml.cs b/OpenSSH_GUI/Views/MainWindow.axaml.cs index 001513e..2b69ed4 100644 --- a/OpenSSH_GUI/Views/MainWindow.axaml.cs +++ b/OpenSSH_GUI/Views/MainWindow.axaml.cs @@ -1,10 +1,7 @@ using Avalonia.Controls; -using Avalonia.Media.Imaging; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenSSH_GUI.Core.Interfaces.Hosts; -using OpenSSH_GUI.Core.MVVM; using OpenSSH_GUI.Core.Resources.Wrapper; using OpenSSH_GUI.ViewModels; @@ -13,32 +10,17 @@ namespace OpenSSH_GUI.Views; [UsedImplicitly] public partial class MainWindow : WindowBase, IDialogHost { - public MainWindow() + private readonly ILogger _logger; + + public MainWindow(ILogger logger) { + _logger = logger; InitializeComponent(); } - + public Task ShowDialog(TWindow dialogWindow) where TWindow : Window { - Logger.LogDebug("Showing dialog {nameOfWindow}", typeof(TWindow).Name); + _logger.LogDebug("Showing dialog {nameOfWindow}", typeof(TWindow).Name); return dialogWindow.ShowDialog(this); } - - public -#if DEBUG - async -#endif - Task ShowDialog(TWindow dialogWindow) - where TWindow : Window where TResult : ViewModelBase - { - Logger.LogDebug("Showing dialog {nameOfWindow} with expected result {nameOfResult}", typeof(TWindow).Name, - typeof(TResult).Name); -#if !DEBUG - return dialogWindow.ShowDialog(this); -#else - var result = await dialogWindow.ShowDialog(this); - Logger.LogDebug("Result: {nameOfResult}", result?.GetType().Name); - return result; -#endif - } } \ No newline at end of file diff --git a/images/openssh-gui-light.svg b/images/openssh-gui-light.svg new file mode 100644 index 0000000..00d698e --- /dev/null +++ b/images/openssh-gui-light.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/openssh-gui.ico b/images/openssh-gui.ico new file mode 100644 index 0000000000000000000000000000000000000000..2891934440154d97dd37b185660e25804b0f8629 GIT binary patch literal 72398 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 F@c#uY)L8%k literal 0 HcmV?d00001 diff --git a/images/openssh-gui.svg b/images/openssh-gui.svg new file mode 100644 index 0000000..6459ca7 --- /dev/null +++ b/images/openssh-gui.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 9f01b1d..a5e1299 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,5 +1,5 @@ pkgname=openssh-gui-bin -pkgver=2.2.1 +pkgver=3.0.0 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') @@ -9,15 +9,19 @@ depends=('icu' 'openssl' 'zlib' 'krb5' 'libx11') options=('!strip') provides=('openssh-gui') conflicts=('openssh-gui' 'openssh-gui-git' 'openssh-gui-nightly') -source=("${pkgname}-${pkgver}::${url}/releases/download/v${pkgver}/OpenSSH-GUI-linux-x64" - "${pkgname}-icon-${pkgver}.png::${url}/raw/v${pkgver}/OpenSSH_GUI/Assets/appicon.png" - "${pkgname}-desktop-${pkgver}.desktop::${url}/raw/v${pkgver}/io.github.frequency403.openssh_gui.desktop" - "${pkgname}-license-${pkgver}::${url}/raw/v${pkgver}/LICENSE") -sha256sums=('6fb2a77a39be10e0b4d880d24c15563f258f05ffa98d6423e9042e085854f755' 'de5104be112173655a8a5950a4b129e0f28d94e29b80239bf7c82360c524bf9c' '9d73c85e0e47fddf9e8930b42caf0f89b39df7f6088a9ca1a08d0c5d2ea5ff42' '04765b5ced4962532281a4c40754d25380df5e89e49bf3f0ea9054f05a6ee34a') + +_rawurl="https://raw.githubusercontent.com/frequency403/OpenSSH-GUI/v${pkgver}" +_relurl="${url}/releases/download/v${pkgver}" + +source=("${pkgname}-${pkgver}::${_relurl}/OpenSSH-GUI-linux-x64" + "${pkgname}-icon-${pkgver}.png::${_relurl}/appicon.png" + "${pkgname}-desktop-${pkgver}.desktop::${_relurl}/io.github.frequency403.openssh_gui.desktop" + "${pkgname}-license-${pkgver}::${_rawurl}/LICENSE") +sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP') package() { - install -Dm755 "${pkgname}-${pkgver}" "${pkgdir}/usr/bin/openssh-gui" - install -Dm644 "${pkgname}-icon-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" - install -Dm644 "${pkgname}-desktop-${pkgver}.desktop" "${pkgdir}/usr/share/applications/openssh-gui.desktop" - install -Dm644 "${pkgname}-license-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -} + install -Dm755 "${pkgname}-${pkgver}" "${pkgdir}/usr/bin/openssh-gui" + install -Dm644 "${pkgname}-icon-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" + install -Dm644 "${pkgname}-desktop-${pkgver}.desktop" "${pkgdir}/usr/share/applications/io.github.frequency403.openssh_gui.desktop" + install -Dm644 "${pkgname}-license-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} \ No newline at end of file diff --git a/openssh-gui-git/PKGBUILD b/openssh-gui-git/PKGBUILD index f4b881d..c75e44d 100644 --- a/openssh-gui-git/PKGBUILD +++ b/openssh-gui-git/PKGBUILD @@ -14,8 +14,12 @@ source=("git+${url}.git#branch=development") sha256sums=('SKIP') pkgver() { - cd "${_pkgname}" - git describe --long --tags | sed 's/\([^-]*-g\)/r\1/;s/-/./g;s/^v//' + cd "${srcdir}/OpenSSH-GUI" + local base + base=$(grep -oP '(?<=)[^<]+' Directory.Build.props) + local hash + hash=$(git rev-parse --short HEAD) + echo "${base}-${hash}" | tr '-' '.' } build() { diff --git a/openssh-gui-nightly/PKGBUILD b/openssh-gui-nightly/PKGBUILD index cc34966..61cb549 100644 --- a/openssh-gui-nightly/PKGBUILD +++ b/openssh-gui-nightly/PKGBUILD @@ -1,7 +1,7 @@ pkgname=openssh-gui-nightly -pkgver=1.0.0.20260316.abc1234 +pkgver=3.0.0.19700101.unknown pkgrel=1 -pkgdesc="A GUI for OpenSSH configuration and management (Nightly build from develop)" +pkgdesc="A GUI for OpenSSH configuration and management (Nightly build)" arch=('x86_64') url="https://github.com/frequency403/OpenSSH-GUI" license=('MIT') @@ -9,15 +9,18 @@ depends=('icu' 'openssl' 'zlib' 'krb5' 'libx11') options=('!strip') provides=('openssh-gui') conflicts=('openssh-gui' 'openssh-gui-bin' 'openssh-gui-git') -source=("${pkgname}-${pkgver}::${url}/releases/download/nightly/OpenSSH-GUI-nightly-linux-x64" - "${pkgname}-icon-${pkgver}.png::${url}/raw/develop/OpenSSH_GUI/Assets/appicon.png" - "${pkgname}-desktop-${pkgver}.desktop::${url}/raw/develop/io.github.frequency403.openssh_gui.desktop" - "${pkgname}-license-${pkgver}::${url}/raw/develop/LICENSE") + +_relurl="${url}/releases/download/nightly" + +source=("${pkgname}-${pkgver}::${_relurl}/OpenSSH-GUI-nightly-linux-x64" + "${pkgname}-icon-${pkgver}.png::${_relurl}/appicon.png" + "${pkgname}-desktop-${pkgver}.desktop::${_relurl}/io.github.frequency403.openssh_gui.desktop" + "${pkgname}-license-${pkgver}::${_relurl}/LICENSE") sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP') package() { - install -Dm755 "${pkgname}-${pkgver}" "${pkgdir}/usr/bin/openssh-gui" - install -Dm644 "${pkgname}-icon-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" - install -Dm644 "${pkgname}-desktop-${pkgver}.desktop" "${pkgdir}/usr/share/applications/openssh-gui.desktop" - install -Dm644 "${pkgname}-license-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + install -Dm755 "${pkgname}-${pkgver}" "${pkgdir}/usr/bin/openssh-gui" + install -Dm644 "${pkgname}-icon-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" + install -Dm644 "${pkgname}-desktop-${pkgver}.desktop" "${pkgdir}/usr/share/applications/io.github.frequency403.openssh_gui.desktop" + install -Dm644 "${pkgname}-license-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } \ No newline at end of file diff --git a/update-version.sh b/update-version.sh new file mode 100644 index 0000000..6dc234c --- /dev/null +++ b/update-version.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROPS="Directory.Build.props" +VERSION=$(grep -oP '(?<=)[^<]+' "${PROPS}") + +echo "→ Version: ${VERSION}" + +PKGBUILD_BIN="openssh-gui-bin/PKGBUILD" +sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "${PKGBUILD_BIN}" +sed -i "s/^pkgrel=.*/pkgrel=1/" "${PKGBUILD_BIN}" +(cd openssh-gui-bin && updpkgsums) + +echo "✓ Done – ${PKGBUILD_BIN} → ${VERSION}" \ No newline at end of file From 8ee5e98211af4ed0671ad08f2cf346fc7c2da57a Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 00:19:11 +0200 Subject: [PATCH 07/58] Introduce new workflows: auto-tag refinement, unit tests, build-and-package, and optimize release flow. - **Auto-tag Workflow**: Improved conditional checks, logging, and tag push status messages. - Added **Unit Test Workflow** for .NET projects with caching and restoration steps. - Modularized build process with **Build-and-Package Workflow** to handle multi-platform targets and upload artifacts cleanly. - Refined and streamlined **Release Workflows** with reusable job configurations and reduced redundancy. --- .github/workflows/auto-tag.yml | 15 +- .github/workflows/build-and-package.yml | 153 ++++++++++++++++++ .github/workflows/build.yml | 165 ++++---------------- .github/workflows/staging.yml | 196 ++++++------------------ .github/workflows/test.yml | 44 ++++++ 5 files changed, 279 insertions(+), 294 deletions(-) create mode 100644 .github/workflows/build-and-package.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 4cb34d0..aac7269 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -12,10 +12,9 @@ permissions: jobs: create-tag: + name: Create and Push Tag # Only run when a release/* branch was actually merged (not just closed) - if: > - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'release/') + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest steps: @@ -29,18 +28,21 @@ jobs: id: version run: | BRANCH="${{ github.event.pull_request.head.ref }}" + # Strip 'release/' prefix VERSION="${BRANCH#release/}" + # Strip optional 'v' prefix VERSION="${VERSION#v}" + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" echo "TAG=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "Detected version: $VERSION (tag: v${VERSION})" + echo "::notice ::Detected version: $VERSION (tag: v${VERSION})" - name: Check if tag already exists id: check run: | if git rev-parse "refs/tags/${{ steps.version.outputs.TAG }}" >/dev/null 2>&1; then echo "exists=true" >> "$GITHUB_OUTPUT" - echo "Tag ${{ steps.version.outputs.TAG }} already exists, skipping." + echo "::warning ::Tag ${{ steps.version.outputs.TAG }} already exists, skipping." else echo "exists=false" >> "$GITHUB_OUTPUT" fi @@ -51,4 +53,5 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "${{ steps.version.outputs.TAG }}" -m "Release ${{ steps.version.outputs.VERSION }}" - git push origin "${{ steps.version.outputs.TAG }}" \ No newline at end of file + git push origin "${{ steps.version.outputs.TAG }}" + echo "::notice ::Tag ${{ steps.version.outputs.TAG }} pushed successfully." \ No newline at end of file diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml new file mode 100644 index 0000000..ffc3fb1 --- /dev/null +++ b/.github/workflows/build-and-package.yml @@ -0,0 +1,153 @@ +name: Build and Package + +on: + workflow_call: + inputs: + version: + required: true + type: string + is_nightly: + required: false + type: boolean + default: false + asset_name_prefix: + required: false + type: string + default: "OpenSSH-GUI" + +jobs: + build: + name: Build for ${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + matrix: + include: + - target: linux-x64 + asset_extension: '' + - target: win-x64 + asset_extension: '.exe' + - target: osx-x64 + asset_extension: '' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Determine .NET version + id: dotnet-version + run: | + TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) + echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ steps.dotnet-version.outputs.version }} + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} + restore-keys: | + ${{ runner.os }}-dotnet- + + - name: Publish application + run: | + dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ + --configuration Release \ + --runtime ${{ matrix.target }} \ + --output "./publish" \ + -p:PublishSingleFile=true \ + -p:PublishReadyToRun=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:Version="${{ inputs.version }}" \ + ${{ inputs.is_nightly && '-p:IsNightly=true' || '' }} + + - name: Rename artifact + id: rename + run: | + ASSET_NAME="${{ inputs.asset_name_prefix }}-${{ matrix.target }}${{ matrix.asset_extension }}" + mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} "./publish/$ASSET_NAME" + echo "ASSET_NAME=$ASSET_NAME" >> "$GITHUB_OUTPUT" + + # --- AppImage (Linux only) --- + - name: Build AppImage + if: matrix.target == 'linux-x64' + id: appimage + run: | + sudo apt-get update && sudo apt-get install -y librsvg2-bin + + # Download appimagetool + wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool + chmod +x appimagetool + + # Create AppDir structure + mkdir -p AppDir/usr/bin + mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps + mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps + mkdir -p AppDir/usr/share/applications + mkdir -p AppDir/usr/share/metainfo + + cp "./publish/${{ steps.rename.outputs.ASSET_NAME }}" AppDir/usr/bin/openssh-gui + chmod +x AppDir/usr/bin/openssh-gui + + # Convert SVG to PNG for AppImage icon + # Use rsvg-convert to create a high-quality PNG icon + rsvg-convert -w 256 -h 256 images/openssh-gui.svg -o AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png + cp images/openssh-gui.svg AppDir/usr/share/icons/hicolor/scalable/apps/openssh-gui.svg + cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/openssh-gui.png + cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/appicon.png + + cp appimage/io.github.frequency403.openssh_gui.metainfo.xml AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml + + # Use ~ for nightly versions in appstream metadata + APPSTREAM_VERSION="${{ inputs.version }}" + if [[ "${{ inputs.is_nightly }}" == "true" ]]; then + APPSTREAM_VERSION="${APPSTREAM_VERSION//+/~}" + fi + + sed -i "s|||" \ + AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml + + appstreamcli make-desktop-file \ + AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml \ + AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop + + cp AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop \ + AppDir/io.github.frequency403.openssh_gui.desktop + + cp appimage/AppRun AppDir/AppRun + chmod +x AppDir/AppRun + + # Build AppImage + APPIMAGE_NAME="${{ inputs.asset_name_prefix }}-x86_64.AppImage" + ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "$APPIMAGE_NAME" + echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT" + + - name: Upload AppImage artifact + if: matrix.target == 'linux-x64' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.appimage.outputs.APPIMAGE_NAME }} + path: ${{ steps.appimage.outputs.APPIMAGE_NAME }} + + - name: Upload generated desktop file + if: matrix.target == 'linux-x64' + uses: actions/upload-artifact@v4 + with: + name: io.github.frequency403.openssh_gui.desktop + path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop + + - name: Upload appicon artifact + if: matrix.target == 'linux-x64' + uses: actions/upload-artifact@v4 + with: + name: appicon + path: AppDir/appicon.png + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.rename.outputs.ASSET_NAME }} + path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8cd4857..aa2167d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,163 +3,54 @@ on: push: tags: - - 'v[0-9]*.[0-9]*.[0-9]*'# Trigger only on version tags like v1.2.3 + - 'v[0-9]*.[0-9]*.[0-9]*' # Trigger only on version tags like v1.2.3 + +permissions: + contents: write jobs: # --- JOB 1: BUILD --- - # This job runs in a matrix to build for all target platforms in parallel. build: - name: Build for ${{ matrix.target }} - runs-on: ubuntu-latest - strategy: - matrix: - include: - - target: linux-x64 - asset_name: OpenSSH-GUI-linux-x64 - asset_extension: '' - - target: win-x64 - asset_name: OpenSSH-GUI-win-x64 - asset_extension: '.exe' - - target: osx-x64 - asset_name: OpenSSH-GUI-osx-x64 - asset_extension: '' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Determine .NET version from project - id: dotnet-version - run: | - TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) - echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" - echo "Detected TargetFramework: net${TFM} → installing SDK ${TFM}.x" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ steps.dotnet-version.outputs.version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-dotnet- - - # Restore, build, and publish the application for the specific target - - name: Publish application - run: | - dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ - --configuration Release \ - --runtime ${{ matrix.target }} \ - --output "./publish" \ - -p:PublishSingleFile=true \ - -p:PublishReadyToRun=true \ - -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:Version="${GITHUB_REF_NAME#v}" - - # Rename the output file to the desired asset name - - name: Rename artifact - run: mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} ./publish/${{ matrix.asset_name }}${{ matrix.asset_extension }} - - # --- AppImage (Linux only) --- - - name: Build AppImage - if: matrix.target == 'linux-x64' - run: | - APPSTREAM_VERSION="${GITHUB_REF_NAME#v}" - # Download appimagetool - wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool - chmod +x appimagetool - - # Create AppDir structure - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/metainfo - - cp ./publish/${{ matrix.asset_name }} AppDir/usr/bin/openssh-gui - chmod +x AppDir/usr/bin/openssh-gui - - cp OpenSSH_GUI/Assets/appicon.png AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png - cp OpenSSH_GUI/Assets/appicon.png AppDir/openssh-gui.png - cp appimage/io.github.frequency403.openssh_gui.metainfo.xml AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - sed -i "s|||" \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - appstreamcli make-desktop-file \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml \ - AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - - cp AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop \ - AppDir/io.github.frequency403.openssh_gui.desktop - - cp appimage/AppRun AppDir/AppRun - chmod +x AppDir/AppRun - - # Build AppImage - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir OpenSSH-GUI-x86_64.AppImage - - - name: Upload AppImage artifact - if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 - with: - name: OpenSSH-GUI-x86_64.AppImage - path: OpenSSH-GUI-x86_64.AppImage - - - name: Upload generated desktop file - if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 - with: - name: io.github.frequency403.openssh_gui.desktop - path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.asset_name }}${{ matrix.asset_extension }} - path: ./publish/${{ matrix.asset_name }}${{ matrix.asset_extension }} + uses: ./.github/workflows/build-and-package.yml + with: + version: ${{ github.ref_name }} + is_nightly: false + asset_name_prefix: OpenSSH-GUI # --- JOB 2: RELEASE --- - # This job runs only ONCE after all build jobs have successfully completed. release: name: Create GitHub Release runs-on: ubuntu-latest - # The 'needs' keyword ensures that this job waits for the 'build' job to finish needs: build - permissions: - contents: write # Required to create a release and upload assets - # Ensure this job only runs for tag pushes, not for manual dispatches that should only build if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository uses: actions/checkout@v4 - # Download all artifacts (from the matrix builds) into a single directory - name: Download all build artifacts uses: actions/download-artifact@v4 with: path: artifacts/ - - name: Display structure of downloaded files - run: ls -R artifacts - - - name: Prepare extra assets + - name: Flatten and Prepare Assets run: | - cp OpenSSH_GUI/Assets/appicon.png artifacts/appicon.png # ← Name explizit + # Move files from subdirectories to the root of artifacts/ + find artifacts -mindepth 2 -type f -exec mv -t artifacts/ {} + + # Remove empty subdirectories + find artifacts -mindepth 1 -type d -delete + + # Copy LICENSE to artifacts cp LICENSE artifacts/LICENSE + + echo "Release Assets:" + ls -R artifacts - # Create a single release and upload all files from the 'artifacts' directory - name: Create Release and Upload Assets uses: softprops/action-gh-release@v2 with: - # The release will be created from the pushed tag tag_name: ${{ github.ref_name }} - # All files downloaded into the 'artifacts' directory will be uploaded - files: "artifacts/**/*" - # Automatically generate the release body from commits since the last tag + files: "artifacts/*" generate_release_notes: true deploy-aur: @@ -173,23 +64,19 @@ jobs: with: fetch-depth: 0 - - name: Download Linux Artifact - uses: actions/download-artifact@v4 - with: - name: OpenSSH-GUI-linux-x64 - path: ./ - - - name: Download generated desktop file + - name: Download Artifacts uses: actions/download-artifact@v4 with: - name: io.github.frequency403.openssh_gui.desktop path: ./ - - name: Update PKGBUILD for openssh-gui-bin + - name: Prepare for PKGBUILD update run: | + # Move files from subdirectories to current directory + find . -maxdepth 2 -mindepth 2 -type f -exec mv -t . {} + + VERSION=${GITHUB_REF_NAME#v} SHA_BIN=$(sha256sum OpenSSH-GUI-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum OpenSSH_GUI/Assets/appicon.png | cut -d' ' -f1) + SHA_ICON=$(sha256sum appicon.png | cut -d' ' -f1) SHA_DESKTOP=$(sha256sum io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index da6e13c..dc7d36d 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -5,141 +5,48 @@ on: branches: - development -env: - BUILD_CONFIGURATION: Release - permissions: contents: write jobs: - # --- JOB 1: BUILD --- - build: - name: Build for ${{ matrix.target }} + # --- JOB 0: PREPARE --- + prepare: + name: Prepare Metadata runs-on: ubuntu-latest - strategy: - matrix: - include: - - target: linux-x64 - asset_name: OpenSSH-GUI-nightly-linux-x64 - asset_extension: "" - - target: win-x64 - asset_name: OpenSSH-GUI-nightly-win-x64 - asset_extension: ".exe" - - target: osx-x64 - asset_name: OpenSSH-GUI-nightly-osx-x64 - asset_extension: "" - + outputs: + version: ${{ steps.meta.outputs.version }} + base_version: ${{ steps.meta.outputs.base_version }} + git_hash: ${{ steps.meta.outputs.git_hash }} + build_date: ${{ steps.meta.outputs.build_date }} steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine .NET version from project - id: dotnet-version - run: | - TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) - echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" - echo "Detected TargetFramework: net${TFM} → installing SDK ${TFM}.x" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ steps.dotnet-version.outputs.version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-dotnet- - - - name: Resolve git metadata + - name: Resolve metadata id: meta run: | HASH=$(git rev-parse --short HEAD) DATE=$(date +%Y-%m-%d) BASE_VERSION=$(grep -oPm1 '(?<=)[^<]+' Directory.Build.props) VERSION="${BASE_VERSION}+${HASH}" - echo "GIT_HASH=$HASH" >> "$GITHUB_ENV" - echo "BUILD_DATE=$DATE" >> "$GITHUB_ENV" - echo "VERSION=$VERSION" >> "$GITHUB_ENV" - echo "BASE_VERSION=$BASE_VERSION" >> "$GITHUB_ENV" - - - name: Publish application - run: | - dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ - --configuration ${{ env.BUILD_CONFIGURATION }} \ - --runtime ${{ matrix.target }} \ - --output "./publish" \ - -p:PublishSingleFile=true \ - -p:PublishReadyToRun=true \ - -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:IsNightly=true - - - name: Rename artifact - run: mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} ./publish/${{ matrix.asset_name }}${{ matrix.asset_extension }} - - # --- AppImage (Linux only) --- - - name: Build AppImage - if: matrix.target == 'linux-x64' - run: | - wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool - chmod +x appimagetool - - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/metainfo - - cp ./publish/${{ matrix.asset_name }} AppDir/usr/bin/openssh-gui - chmod +x AppDir/usr/bin/openssh-gui - - cp OpenSSH_GUI/Assets/appicon.png AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png - cp OpenSSH_GUI/Assets/appicon.png AppDir/openssh-gui.png - APPSTREAM_VERSION="${VERSION//+/~}" - cp appimage/io.github.frequency403.openssh_gui.metainfo.xml AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - sed -i "s|||" \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - appstreamcli make-desktop-file \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml \ - AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - - cp AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop \ - AppDir/io.github.frequency403.openssh_gui.desktop - - cp appimage/AppRun AppDir/AppRun - chmod +x AppDir/AppRun + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT" + echo "git_hash=$HASH" >> "$GITHUB_OUTPUT" + echo "build_date=$DATE" >> "$GITHUB_OUTPUT" - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir OpenSSH-GUI-nightly-x86_64.AppImage - - - name: Upload AppImage artifact - if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 - with: - name: OpenSSH-GUI-nightly-x86_64.AppImage - path: OpenSSH-GUI-nightly-x86_64.AppImage - - - name: Upload generated desktop file - if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 - with: - name: io.github.frequency403.openssh_gui.desktop - path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.asset_name }}${{ matrix.asset_extension }} - path: ./publish/${{ matrix.asset_name }}${{ matrix.asset_extension }} + # --- JOB 1: BUILD --- + build: + needs: prepare + uses: ./.github/workflows/build-and-package.yml + with: + version: ${{ needs.prepare.outputs.version }} + is_nightly: true + asset_name_prefix: OpenSSH-GUI-nightly # --- JOB 2: NIGHTLY RELEASE --- nightly-release: name: Create Nightly Release runs-on: ubuntu-latest - needs: build + needs: [prepare, build] steps: - name: Checkout @@ -148,15 +55,6 @@ jobs: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} - - name: Resolve git metadata - run: | - HASH=$(git rev-parse --short HEAD) - DATE=$(date +%Y-%m-%d) - BASE_VERSION=$(grep -oPm1 '(?<=)[^<]+' Directory.Build.props) - echo "GIT_HASH=$HASH" >> "$GITHUB_ENV" - echo "BUILD_DATE=$DATE" >> "$GITHUB_ENV" - echo "VERSION=${BASE_VERSION}+${HASH}" >> "$GITHUB_ENV" - - name: Force-update nightly tag run: | git config user.name "github-actions[bot]" @@ -169,27 +67,32 @@ jobs: with: path: artifacts/ - - name: Display structure of downloaded files - run: ls -R artifacts - - - name: Collect extra release assets + - name: Flatten and Prepare Assets run: | - cp OpenSSH_GUI/Assets/appicon.png artifacts/appicon.png + # Move files from subdirectories to the root of artifacts/ + find artifacts -mindepth 2 -type f -exec mv -t artifacts/ {} + + # Remove empty subdirectories + find artifacts -mindepth 1 -type d -delete + + # Copy LICENSE to artifacts cp LICENSE artifacts/LICENSE + + echo "Nightly Assets:" + ls -R artifacts - name: Update nightly release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | NOTES="**Branch:** \`development\` - **Commit:** \`${{ env.GIT_HASH }}\` - **Built:** ${{ env.BUILD_DATE }} + **Commit:** \`${{ needs.prepare.outputs.git_hash }}\` + **Built:** ${{ needs.prepare.outputs.build_date }} > Automated nightly build — not intended for production use." if gh release view nightly &>/dev/null; then gh release edit nightly \ - --title "Nightly (${{ env.BUILD_DATE }}) — ${{ env.VERSION }}" \ + --title "Nightly (${{ needs.prepare.outputs.build_date }}) — ${{ needs.prepare.outputs.version }}" \ --notes "$NOTES" \ --prerelease @@ -199,7 +102,7 @@ jobs: find artifacts -type f | xargs gh release upload nightly else gh release create nightly \ - --title "Nightly (${{ env.BUILD_DATE }}) — ${{ env.VERSION }}" \ + --title "Nightly (${{ needs.prepare.outputs.build_date }}) — ${{ needs.prepare.outputs.version }}" \ --notes "$NOTES" \ --prerelease \ $(find artifacts -type f) @@ -209,7 +112,7 @@ jobs: deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest - needs: nightly-release + needs: [prepare, nightly-release] steps: - name: Checkout Repository @@ -217,27 +120,22 @@ jobs: with: fetch-depth: 0 - - name: Download Linux Artifact - uses: actions/download-artifact@v4 - with: - name: OpenSSH-GUI-nightly-linux-x64 - path: ./ - - - name: Download generated desktop file + - name: Download Artifacts uses: actions/download-artifact@v4 with: - name: io.github.frequency403.openssh_gui.desktop path: ./ - - name: Update PKGBUILD for openssh-gui-nightly + - name: Prepare for PKGBUILD update run: | - HASH=$(git rev-parse --short HEAD) + # Move files from subdirectories to current directory + find . -maxdepth 2 -mindepth 2 -type f -exec mv -t . {} + + DATE=$(date +%Y%m%d) - BASE_VERSION=$(grep -oPm1 '(?<=)[^<]+' Directory.Build.props) - VERSION="${BASE_VERSION}.${DATE}.${HASH}" - echo "VERSION=$VERSION" >> "$GITHUB_ENV" + VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" + echo "AUR_VERSION=$VERSION" >> "$GITHUB_ENV" + SHA_BIN=$(sha256sum OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum OpenSSH_GUI/Assets/appicon.png | cut -d' ' -f1) + SHA_ICON=$(sha256sum appicon.png | cut -d' ' -f1) SHA_DESKTOP=$(sha256sum io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) @@ -252,4 +150,4 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Nightly update ${{ env.VERSION }}" \ No newline at end of file + commit_message: "Nightly update ${{ env.AUR_VERSION }}" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0d80e4a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Unit Tests + +on: + push: + branches: [ main, master, development ] + pull_request: + branches: [ main, master, development ] + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Determine .NET version from project + id: dotnet-version + run: | + TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) + echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ steps.dotnet-version.outputs.version }} + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} + restore-keys: | + ${{ runner.os }}-dotnet- + + - name: Restore dependencies + run: dotnet restore OpenSSH_GUI.slnx + + - name: Build + run: dotnet build OpenSSH_GUI.slnx --configuration Release --no-restore + + - name: Run Tests + run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal From 5fbbd39bec636660d33646b3b2dbaeb370fb4bde Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 00:40:00 +0200 Subject: [PATCH 08/58] Bump version to 3.1.0, improve PKGBUILD formatting, and fix SSH key format extension handling. --- .github/workflows/staging.yml | 9 +---- .../Extensions/SshKeyFormatExtensionTests.cs | 4 +- openssh-gui-bin/PKGBUILD | 32 ++++++++++----- openssh-gui-git/PKGBUILD | 40 +++++++++++++------ 4 files changed, 52 insertions(+), 33 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index dc7d36d..156b342 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -47,7 +47,7 @@ jobs: name: Create Nightly Release runs-on: ubuntu-latest needs: [prepare, build] - + steps: - name: Checkout uses: actions/checkout@v4 @@ -55,13 +55,6 @@ jobs: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} - - name: Force-update nightly tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -f nightly - git push origin -f nightly - - name: Download all build artifacts uses: actions/download-artifact@v4 with: diff --git a/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs b/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs index 1ca2bcb..8960a62 100644 --- a/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs +++ b/OpenSSH_GUI.Tests/Core/Extensions/SshKeyFormatExtensionTests.cs @@ -7,8 +7,8 @@ namespace OpenSSH_GUI.Tests.Core.Extensions; public class SshKeyFormatExtensionTests { - [Theory, InlineData(SshKeyFormat.OpenSSH, true, ".pub"), InlineData(SshKeyFormat.OpenSSH, false, null), InlineData(SshKeyFormat.PuTTYv2, false, ".ppk"), - InlineData(SshKeyFormat.PuTTYv3, true, ".ppk")] + [Theory, InlineData(SshKeyFormat.OpenSSH, true, "pub"), InlineData(SshKeyFormat.OpenSSH, false, null), InlineData(SshKeyFormat.PuTTYv2, false, "ppk"), + InlineData(SshKeyFormat.PuTTYv3, true, "ppk")] public void GetExtension_Tests(SshKeyFormat format, bool isPublic, string? expected) { format.GetExtension(isPublic).ShouldBe(expected); } [Theory, InlineData(SshKeyFormat.OpenSSH, "test.key", true, "test.pub"), InlineData(SshKeyFormat.PuTTYv3, "test.key", false, "test.ppk")] diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index a5e1299..2b7ceae 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,27 +1,39 @@ pkgname=openssh-gui-bin -pkgver=3.0.0 +pkgver=3.1.0 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') url="https://github.com/frequency403/OpenSSH-GUI" license=('MIT') + depends=('icu' 'openssl' 'zlib' 'krb5' 'libx11') options=('!strip') + provides=('openssh-gui') conflicts=('openssh-gui' 'openssh-gui-git' 'openssh-gui-nightly') +_relurl="https://github.com/frequency403/OpenSSH-GUI/releases/download/v${pkgver}" _rawurl="https://raw.githubusercontent.com/frequency403/OpenSSH-GUI/v${pkgver}" -_relurl="${url}/releases/download/v${pkgver}" -source=("${pkgname}-${pkgver}::${_relurl}/OpenSSH-GUI-linux-x64" - "${pkgname}-icon-${pkgver}.png::${_relurl}/appicon.png" - "${pkgname}-desktop-${pkgver}.desktop::${_relurl}/io.github.frequency403.openssh_gui.desktop" - "${pkgname}-license-${pkgver}::${_rawurl}/LICENSE") +source=( + "${pkgname}-${pkgver}::${_relurl}/OpenSSH-GUI-linux-x64" + "${pkgname}-icon-${pkgver}.png::${_relurl}/appicon.png" + "${pkgname}-desktop-${pkgver}.desktop::${_relurl}/io.github.frequency403.openssh_gui.desktop" + "${pkgname}-license-${pkgver}::${_rawurl}/LICENSE" +) + sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP') package() { - install -Dm755 "${pkgname}-${pkgver}" "${pkgdir}/usr/bin/openssh-gui" - install -Dm644 "${pkgname}-icon-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" - install -Dm644 "${pkgname}-desktop-${pkgver}.desktop" "${pkgdir}/usr/share/applications/io.github.frequency403.openssh_gui.desktop" - install -Dm644 "${pkgname}-license-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + install -Dm755 "${srcdir}/${pkgname}-${pkgver}" \ + "${pkgdir}/usr/bin/openssh-gui" + + install -Dm644 "${srcdir}/${pkgname}-icon-${pkgver}.png" \ + "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" + + install -Dm644 "${srcdir}/${pkgname}-desktop-${pkgver}.desktop" \ + "${pkgdir}/usr/share/applications/io.github.frequency403.openssh_gui.desktop" + + install -Dm644 "${srcdir}/${pkgname}-license-${pkgver}" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } \ No newline at end of file diff --git a/openssh-gui-git/PKGBUILD b/openssh-gui-git/PKGBUILD index c75e44d..bb37a2d 100644 --- a/openssh-gui-git/PKGBUILD +++ b/openssh-gui-git/PKGBUILD @@ -2,42 +2,56 @@ pkgname=openssh-gui-git _pkgname=OpenSSH-GUI pkgver=2.2.1.r0.g845610b pkgrel=1 -pkgdesc="A GUI for OpenSSH configuration and management (GIT version, built from develop)" +pkgdesc="A GUI for OpenSSH configuration and management (GIT version, built from development branch)" arch=('x86_64') url="https://github.com/frequency403/OpenSSH-GUI" license=('MIT') + depends=('dotnet-runtime-10.0') makedepends=('git' 'dotnet-sdk-10.0') + provides=('openssh-gui') conflicts=('openssh-gui' 'openssh-gui-bin' 'openssh-gui-nightly') + source=("git+${url}.git#branch=development") sha256sums=('SKIP') pkgver() { - cd "${srcdir}/OpenSSH-GUI" - local base + cd "${srcdir}/${_pkgname}" + + local base count hash base=$(grep -oP '(?<=)[^<]+' Directory.Build.props) - local hash + count=$(git rev-list --count HEAD) hash=$(git rev-parse --short HEAD) - echo "${base}-${hash}" | tr '-' '.' + + printf "%s.r%s.g%s\n" "$base" "$count" "$hash" } build() { - cd "${_pkgname}" + cd "${srcdir}/${_pkgname}" + dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ --configuration Release \ --runtime linux-x64 \ - --output "publish" \ + --output publish \ -p:PublishSingleFile=true \ -p:PublishReadyToRun=true \ -p:IncludeNativeLibrariesForSelfExtract=true \ - --self-contained false + -p:SelfContained=false } package() { - cd "${_pkgname}" - install -Dm755 "publish/OpenSSH_GUI" "${pkgdir}/usr/bin/openssh-gui" - install -Dm644 "OpenSSH_GUI/Assets/appicon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" - install -Dm644 "openssh-gui.desktop" "${pkgdir}/usr/share/applications/openssh-gui.desktop" - install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + cd "${srcdir}/${_pkgname}" + + install -Dm755 "publish/OpenSSH_GUI" \ + "${pkgdir}/usr/bin/openssh-gui" + + install -Dm644 "OpenSSH_GUI/Assets/appicon.png" \ + "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" + + install -Dm644 "openssh-gui.desktop" \ + "${pkgdir}/usr/share/applications/openssh-gui.desktop" + + install -Dm644 "LICENSE" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } \ No newline at end of file From cdff1010306d83ad54abce7e11612c2dd22f266c Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 00:45:09 +0200 Subject: [PATCH 09/58] Add `build-and-package.yml` and `test.yml` workflows; refine asset preparation logic in `staging.yml`. --- .github/workflows/staging.yml | 20 ++++++++++---------- OpenSSH_GUI.slnx | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 156b342..c180b8d 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -62,16 +62,16 @@ jobs: - name: Flatten and Prepare Assets run: | - # Move files from subdirectories to the root of artifacts/ - find artifacts -mindepth 2 -type f -exec mv -t artifacts/ {} + - # Remove empty subdirectories - find artifacts -mindepth 1 -type d -delete - - # Copy LICENSE to artifacts - cp LICENSE artifacts/LICENSE - + rm -rf release-assets + mkdir -p release-assets + + # Copy files from artifact subdirectories into a clean output directory. + find artifacts -type f -exec cp {} release-assets/ \; + + cp LICENSE release-assets/LICENSE + echo "Nightly Assets:" - ls -R artifacts + ls -la release-assets - name: Update nightly release env: @@ -92,7 +92,7 @@ jobs: gh release view nightly --json assets --jq '.assets[].name' \ | xargs -r -I{} gh release delete-asset nightly {} --yes - find artifacts -type f | xargs gh release upload nightly + find release-assets -type f -print0 | xargs -0 gh release upload nightly else gh release create nightly \ --title "Nightly (${{ needs.prepare.outputs.build_date }}) — ${{ needs.prepare.outputs.version }}" \ diff --git a/OpenSSH_GUI.slnx b/OpenSSH_GUI.slnx index 205903e..08d6113 100644 --- a/OpenSSH_GUI.slnx +++ b/OpenSSH_GUI.slnx @@ -14,6 +14,8 @@ + + From 1431a2d490a07a55786eae34eb22245a21e49d10 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 00:55:09 +0200 Subject: [PATCH 10/58] Refactor workflows: streamline asset preparation, reorganize directories, and remove redundant nightly release job. --- .github/workflows/build.yml | 33 ++++++++++--------- .github/workflows/staging.yml | 62 +---------------------------------- 2 files changed, 18 insertions(+), 77 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa2167d..f86afb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,22 +35,21 @@ jobs: - name: Flatten and Prepare Assets run: | - # Move files from subdirectories to the root of artifacts/ - find artifacts -mindepth 2 -type f -exec mv -t artifacts/ {} + - # Remove empty subdirectories - find artifacts -mindepth 1 -type d -delete - - # Copy LICENSE to artifacts - cp LICENSE artifacts/LICENSE - + rm -rf release-assets + mkdir -p release-assets + + find artifacts -type f -exec cp {} release-assets/ \; + + cp LICENSE release-assets/LICENSE + echo "Release Assets:" - ls -R artifacts + ls -la release-assets - name: Create Release and Upload Assets uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} - files: "artifacts/*" + files: "release-assets/*" generate_release_notes: true deploy-aur: @@ -71,15 +70,17 @@ jobs: - name: Prepare for PKGBUILD update run: | - # Move files from subdirectories to current directory - find . -maxdepth 2 -mindepth 2 -type f -exec mv -t . {} + + rm -rf aur-assets + mkdir -p aur-assets + + find . -mindepth 2 -type f -exec cp {} aur-assets/ \; VERSION=${GITHUB_REF_NAME#v} - SHA_BIN=$(sha256sum OpenSSH-GUI-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) + SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) + SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index c180b8d..aa94cab 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -42,70 +42,10 @@ jobs: is_nightly: true asset_name_prefix: OpenSSH-GUI-nightly - # --- JOB 2: NIGHTLY RELEASE --- - nightly-release: - name: Create Nightly Release - runs-on: ubuntu-latest - needs: [prepare, build] - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.PAT_TOKEN }} - - - name: Download all build artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Flatten and Prepare Assets - run: | - rm -rf release-assets - mkdir -p release-assets - - # Copy files from artifact subdirectories into a clean output directory. - find artifacts -type f -exec cp {} release-assets/ \; - - cp LICENSE release-assets/LICENSE - - echo "Nightly Assets:" - ls -la release-assets - - - name: Update nightly release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - NOTES="**Branch:** \`development\` - **Commit:** \`${{ needs.prepare.outputs.git_hash }}\` - **Built:** ${{ needs.prepare.outputs.build_date }} - - > Automated nightly build — not intended for production use." - - if gh release view nightly &>/dev/null; then - gh release edit nightly \ - --title "Nightly (${{ needs.prepare.outputs.build_date }}) — ${{ needs.prepare.outputs.version }}" \ - --notes "$NOTES" \ - --prerelease - - gh release view nightly --json assets --jq '.assets[].name' \ - | xargs -r -I{} gh release delete-asset nightly {} --yes - - find release-assets -type f -print0 | xargs -0 gh release upload nightly - else - gh release create nightly \ - --title "Nightly (${{ needs.prepare.outputs.build_date }}) — ${{ needs.prepare.outputs.version }}" \ - --notes "$NOTES" \ - --prerelease \ - $(find artifacts -type f) - fi - - # --- JOB 3: AUR NIGHTLY --- deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest - needs: [prepare, nightly-release] + needs: prepare steps: - name: Checkout Repository From 638c3b845b34cbc6d9a97f643019e0c512acbcfa Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 00:57:17 +0200 Subject: [PATCH 11/58] Update `staging.yml`: adjust dependencies for `deploy-aur-nightly` and refine artifact handling logic --- .github/workflows/staging.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index aa94cab..167eef9 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -45,7 +45,7 @@ jobs: deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest - needs: prepare + needs: [prepare, build] steps: - name: Checkout Repository @@ -56,12 +56,14 @@ jobs: - name: Download Artifacts uses: actions/download-artifact@v4 with: - path: ./ + path: artifacts/ - name: Prepare for PKGBUILD update run: | - # Move files from subdirectories to current directory - find . -maxdepth 2 -mindepth 2 -type f -exec mv -t . {} + + rm -rf aur-assets + mkdir -p aur-assets + + find artifacts -type f -exec cp {} aur-assets/ \; DATE=$(date +%Y%m%d) VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" From d7f6b4bc3c135d907569902f95e5f8bfb014618f Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 01:04:23 +0200 Subject: [PATCH 12/58] Refactor GitHub Actions workflows: improve artifact handling and enhance AUR metadata validation --- .github/workflows/build.yml | 21 ++++++++++++++++----- .github/workflows/staging.yml | 25 ++++++++++++++++++------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f86afb9..be9449c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,25 +66,36 @@ jobs: - name: Download Artifacts uses: actions/download-artifact@v4 with: - path: ./ + path: artifacts/ - name: Prepare for PKGBUILD update run: | rm -rf aur-assets mkdir -p aur-assets - - find . -mindepth 2 -type f -exec cp {} aur-assets/ \; - + + find artifacts -type f -exec cp {} aur-assets/ \; + + echo "AUR asset files:" + ls -la aur-assets + + test -f aur-assets/OpenSSH-GUI-linux-x64 + test -f aur-assets/appicon.png + test -f aur-assets/io.github.frequency403.openssh_gui.desktop + test -f LICENSE + VERSION=${GITHUB_REF_NAME#v} SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD + echo "Updated openssh-gui-bin PKGBUILD:" + grep -E '^(pkgver=|sha256sums=)' openssh-gui-bin/PKGBUILD + - name: Update AUR (openssh-gui-bin) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 167eef9..734daaa 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -45,7 +45,7 @@ jobs: deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest - needs: [prepare, build] + needs: prepare steps: - name: Checkout Repository @@ -56,7 +56,7 @@ jobs: - name: Download Artifacts uses: actions/download-artifact@v4 with: - path: artifacts/ + path: ./ - name: Prepare for PKGBUILD update run: | @@ -64,19 +64,30 @@ jobs: mkdir -p aur-assets find artifacts -type f -exec cp {} aur-assets/ \; - + + echo "AUR asset files:" + ls -la aur-assets + + test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 + test -f aur-assets/appicon.png + test -f aur-assets/io.github.frequency403.openssh_gui.desktop + test -f LICENSE + DATE=$(date +%Y%m%d) VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" echo "AUR_VERSION=$VERSION" >> "$GITHUB_ENV" - - SHA_BIN=$(sha256sum OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) + + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) + SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) + SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-nightly/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD + echo "Updated openssh-gui-nightly PKGBUILD:" + grep -E '^(pkgver=|sha256sums=)' openssh-gui-nightly/PKGBUILD + - name: Update AUR (openssh-gui-nightly) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: From ec4453544b516ae0e8ca28bb2e355f5b3155b96b Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 01:05:58 +0200 Subject: [PATCH 13/58] Update `staging.yml`: add `build` dependency for `deploy-aur-nightly` and improve artifact preparation logic --- .github/workflows/staging.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 734daaa..8a50dbf 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -45,8 +45,8 @@ jobs: deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest - needs: prepare - + needs: [ prepare, build ] + steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -56,35 +56,37 @@ jobs: - name: Download Artifacts uses: actions/download-artifact@v4 with: - path: ./ + path: artifacts/ - name: Prepare for PKGBUILD update run: | + set -euo pipefail + rm -rf aur-assets mkdir -p aur-assets - + find artifacts -type f -exec cp {} aur-assets/ \; - + echo "AUR asset files:" - ls -la aur-assets - + find aur-assets -maxdepth 1 -type f -printf '%f\n' | sort + test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 test -f aur-assets/appicon.png test -f aur-assets/io.github.frequency403.openssh_gui.desktop test -f LICENSE - + DATE=$(date +%Y%m%d) VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" echo "AUR_VERSION=$VERSION" >> "$GITHUB_ENV" - + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-nightly/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD - + echo "Updated openssh-gui-nightly PKGBUILD:" grep -E '^(pkgver=|sha256sums=)' openssh-gui-nightly/PKGBUILD From 08afa8951e0b95c370158b05be4da9fb81e8609d Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 01:35:53 +0200 Subject: [PATCH 14/58] Revamp README: overhaul content structure, include new features, update screenshots, and replace outdated information. --- README.md | 271 +++++++++++++--------- images/AddKeyWindow.png | Bin 17914 -> 29252 bytes images/AppSettings.png | Bin 29806 -> 0 bytes images/ApplicationSettings.png | Bin 0 -> 29988 bytes images/ConnectToServerQuickConnect.png | Bin 18870 -> 0 bytes images/ConnectToServerWindow.png | Bin 20429 -> 0 bytes images/ConnectToServerWindowEmpty.png | Bin 0 -> 20914 bytes images/ConnectToServerWindowFilled.png | Bin 0 -> 20491 bytes images/ConnectToServerWindowSuccess.png | Bin 20596 -> 0 bytes images/ConnectToServerWindowWithKey.png | Bin 28124 -> 0 bytes images/EditAuthorizedKeysWindow.png | Bin 12377 -> 0 bytes images/EditAuthorizedKeysWindowRemote.png | Bin 26839 -> 0 bytes images/EditKnownHostsWindow.png | Bin 0 -> 25327 bytes images/ExportKeyWindow.png | Bin 15383 -> 0 bytes images/FileInfoWindow.png | Bin 0 -> 22675 bytes images/FileInfoWindowPasswordVisible.png | Bin 0 -> 23015 bytes images/FoundPasswordProtectedKey.png | Bin 46847 -> 0 bytes images/KnownHostsWindow.png | Bin 40635 -> 0 bytes images/MainView.png | Bin 0 -> 40250 bytes images/MainViewPassEntered.png | Bin 0 -> 39235 bytes images/MainWindow.png | Bin 48739 -> 0 bytes images/NewMainUI.png | Bin 52887 -> 0 bytes images/ProvidePasswordPrompt.png | Bin 8571 -> 0 bytes images/SettingsContextMenu.png | Bin 22478 -> 0 bytes images/ShowForgetPws.png | Bin 50856 -> 0 bytes images/Sorted.png | Bin 48378 -> 0 bytes images/tooltip.png | Bin 4786 -> 0 bytes images/tooltipKey.png | Bin 14358 -> 0 bytes images/tooltipServer.png | Bin 11403 -> 0 bytes 29 files changed, 161 insertions(+), 110 deletions(-) delete mode 100644 images/AppSettings.png create mode 100644 images/ApplicationSettings.png delete mode 100644 images/ConnectToServerQuickConnect.png delete mode 100644 images/ConnectToServerWindow.png create mode 100644 images/ConnectToServerWindowEmpty.png create mode 100644 images/ConnectToServerWindowFilled.png delete mode 100644 images/ConnectToServerWindowSuccess.png delete mode 100644 images/ConnectToServerWindowWithKey.png delete mode 100644 images/EditAuthorizedKeysWindow.png delete mode 100644 images/EditAuthorizedKeysWindowRemote.png create mode 100644 images/EditKnownHostsWindow.png delete mode 100644 images/ExportKeyWindow.png create mode 100644 images/FileInfoWindow.png create mode 100644 images/FileInfoWindowPasswordVisible.png delete mode 100644 images/FoundPasswordProtectedKey.png delete mode 100644 images/KnownHostsWindow.png create mode 100644 images/MainView.png create mode 100644 images/MainViewPassEntered.png delete mode 100644 images/MainWindow.png delete mode 100644 images/NewMainUI.png delete mode 100644 images/ProvidePasswordPrompt.png delete mode 100644 images/SettingsContextMenu.png delete mode 100644 images/ShowForgetPws.png delete mode 100644 images/Sorted.png delete mode 100644 images/tooltip.png delete mode 100644 images/tooltipKey.png delete mode 100644 images/tooltipServer.png diff --git a/README.md b/README.md index 26888ca..aac0f87 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,233 @@ -OpenSSH_GUI +# OpenSSH GUI -A GUI for managing your SSH Keys - on Windows, Linux and macOS! +A cross-platform desktop application for managing SSH keys, known hosts, and authorized keys — built with Avalonia UI, ReactiveUI, and .NET 10. -The primary reason for creating this project was to give "end-users" -a modern looking GUI for managing their SSH Keys - and making it easier -to deploy them to a server of their choice. +The goal of this project is to give users a modern, keyboard-friendly GUI for everything that usually requires `ssh-keygen` or hand-editing text files. It runs on **Windows**, **Linux**, and **macOS** and works entirely locally — no cloud, no telemetry. -The program I found -> [PuSSHy](https://github.com/klimenta/pusshy) was, in my opinion -not as user-friendly as it could be. I also wanted to use this program on my different -machines, running on Linux and macOS. So I decided to create my own! +--- -I hope you like it! +## Features -### Installing +- Browse, inspect, and manage all SSH key files in your configured lookup paths +- Generate new SSH keys (RSA, ECDSA, ED25519) with configurable bit size, comment, password, and format +- Convert keys between **OpenSSH** and **PuTTY v2/v3** formats in one click +- Change or clear the passphrase of any key file +- Rename key files safely (both private and public halves move together) +- Display SHA-256 fingerprints without ever unlocking the private key +- Open a **FileInfo** window per key to inspect, rename, delete, convert, or copy the password +- Edit the local `known_hosts` file; mark individual key entries or whole hosts for deletion +- Edit the local `authorized_keys` file +- Connect to a remote SSH server and edit its `known_hosts` and `authorized_keys` in the same UI +- Quick-connect from pre-configured `~/.ssh/config` host blocks +- Export public or private key content to the clipboard +- Application settings: log level, theme (dark/light/system), font size, lookup paths, cache cleanup +- Full dark and light theme with a VS Code–inspired teal/amber/red colour palette -No Installation needed! Just run the OpenSSHA_GUI.exe or .bin +--- -## Usage +## Screenshots -It is free to you, if you connect to a Server or not. -This program can be used on PC's (Local Machines) and Servers! +### Main Window -If you choose to connect to a server - ***beware!*** -This program - nor the author(s) take responsibility for saved messed up files! -***Make a backup if you already have files!*** +![Main Window](images/MainView.png) -If you need help, open an [Issue]() +The main window lists all discovered SSH keys in a table. Each row shows: -#### Main Window +- Lock/key icon indicating whether the key is encrypted and whether the passphrase has been provided +- Key algorithm (RSA, ECDSA, ED25519) and format (OpenSSH / PuTTY) +- SHA-256 fingerprint; password-protected keys show a **Provide Password** button inline +- Comment +- Action buttons: export public key, export private key, open FileInfo window -![](images/MainWindow.png) +### Main Window — Password Entered -##### V2 UI +![Main Window with password unlocked](images/MainViewPassEntered.png) -![](images/NewMainUI.png) -You can now convert the Key to the opposite format. -You can choose to delete or keep the key. -If the key is kept, the program will move it into a newly created sub-folder of your -.ssh directory. +Once a passphrase is provided, the fingerprint column shows the actual hash and a **Forget Password** button appears. -##### Key without provided password +### Add New SSH Key -![](images/FoundPasswordProtectedKey.png) +![Add Key Window](images/AddKeyWindow.png) -##### Password options, when a password was provided +Fields: -![](images/ShowForgetPws.png) +| Field | Notes | +|---|---| +| Key filename | Directory dropdown (from lookup paths) + filename text box | +| Keytype | RSA / ECDSA / ED25519 — default name updates automatically | +| Bitsize | Populated from the cryptographic legal key sizes for the chosen type; hidden for ED25519 | +| Password | Optional; leave blank for an unencrypted key | +| Comment | Defaults to `user@hostname` | +| Key Format | OpenSSH or PuTTY v2/v3 | -##### Provide password prompt +The **Add** button stays disabled until the filename passes validation (non-empty and not already on disk). -![](images/ProvidePasswordPrompt.png) +### FileInfo Window -##### Application Settings +![FileInfo Window](images/FileInfoWindow.png) +![FileInfo Window — password visible](images/FileInfoWindowPasswordVisible.png) -![](images/AppSettings.png) -App settings can be accessed through the settings context menu. -There is also an option, that the program converts all PPK keys in your .ssh directory -to the OpenSSH format. The PPK Keys are not deleted, they will be put into a folder called PPK -![](images/SettingsContextMenu.png) +Shows all files associated with the key (e.g. `id_ed25519` + `id_ed25519.pub`). From here you can: -##### Sorting feature +- **Change password** — prompts for the new passphrase via a secure input dialog +- **Rename** — moves both halves, prompts for overwrite if a conflict is detected +- **Delete** — removes all associated files from disk +- **Convert format** — the SplitButton converts to the default target; the dropdown allows choosing any other available format +- **Password field** — shows masked passphrase with a toggle-visibility eye button and a copy-to-clipboard button -You can sort the keys, if you want to. Just click on the top description category to sort by. -![](images/Sorted.png) +### Application Settings -#### Add SSH Key +![Application Settings](images/ApplicationSettings.png) -![](images/AddKeyWindow.png) +| Section | Options | +|---|---| +| Log Level | Verbose / Debug / Information / Warning / Error / Fatal | +| Theme | Default (system) / Dark / Light | +| Cache Options | Delete log files older than N days; clear whole application cache | +| Font Size | Numeric up/down; reset button restores the default | +| Lookup Paths | Add/remove directories the key crawler searches | -#### Connect to a Server +### Connect to Server -Right-Click on the Connection-status icon and click "Connect" on the showing menu. +![Connect to Server — empty](images/ConnectToServerWindowEmpty.png) +![Connect to Server — connected](images/ConnectToServerWindowFilled.png) -![](images/ConnectToServerWindow.png) +The connection window supports: -- You can also auth with a public key from the recognized keys on your machine! - ![](images/ConnectToServerWindowWithKey.png) +- **Preconfigured connections** — populated automatically from `~/.ssh/config` host blocks that carry an `IdentityFile` directive +- Manual entry of hostname, username, and either a password or a public key from the recognised key list +- **Test connection** button — attempts a connection and shows a colour-coded status badge (unknown / success / failed) +- After a successful test, the **Accept** button becomes active and establishes the session for the rest of the UI -- V2 Feature: Quick Connect - ![](images/ConnectToServerQuickConnect.png) - If you submitted a valid connection earlier, the program will save the connection, - and suggest this connection here for quick access. +### Edit known_hosts +![Edit known_hosts](images/EditKnownHostsWindow.png) -- You need to test the connection before you can submit it, if you do not use the new Quick-Connect feature. - If you get a connection error, an error window shows up. - ![](images/ConnectToServerWindowSuccess.png) +Displays every known host in a collapsible list. Each host shows its individual key entries (algorithm + fingerprint). Toggle buttons mark individual keys or entire hosts for deletion on save. A **Remote** tab appears when a server connection is active, allowing the same edits on the server's `known_hosts`. -#### Edit Authorized Keys +--- -Edit your local (or remote) authorized_keys! +## Architecture Overview -![](images/EditAuthorizedKeysWindow.png) +The project is split into four assemblies: -In the remote Version you can even add a key from the recognized keys! -The key cannot be added, when it's already present on the remote! -![](images/EditAuthorizedKeysWindowRemote.png) +| Assembly | Role | +|---|---| +| `OpenSSH_GUI` | Avalonia application shell — views, view models, DI wiring, app lifecycle | +| `OpenSSH_GUI.Core` | Domain logic — key management, SSH config crawling, server connections, backup service | +| `OpenSSH_GUI.SshConfig` | SSH `~/.ssh/config` parser, serialiser, and `IConfiguration` provider | +| `OpenSSH_GUI.Dialogs` | Reusable modal dialogs (message box, secure password input, validated text input) | -#### Edit Known Hosts Window +### Key Components -![](images/KnownHostsWindow.png) +**`SshKeyManager`** is the central service. It owns the observable collection of `SshKeyFile` instances and exposes async operations for generate, rename, change-password, change-format, delete, and reload. Every destructive operation backs up the affected files first and restores them on failure. -Here you have a list of all "Known Hosts" from your "known_hosts" file. -If you want to remove one key from a Host, toggle the button of the specific Key. -If you want to remove the whole host, just toggle the button on the top label. +**`SshKeyFile`** is a reactive record. It uses `ReactiveUI.SourceGenerators` to expose observable properties for fingerprint, comment, key type, format, password state, and file metadata. The fingerprint is extracted without decrypting the private key by parsing the unencrypted public key blob directly (supports OpenSSH `.pub`, OpenSSH private key header, and PPK v2/v3 headers). -#### Export Key Window +**`DirectoryCrawler`** is an `IAsyncEnumerable`-based crawler that reads `~/.ssh/config` identity files first (marking them as config-provided) and then enumerates the configured lookup directories for any remaining key files. -![](images/ExportKeyWindow.png) +**`SshConfigParser`** is a zero-dependency recursive-descent parser for `ssh_config(5)` syntax. It handles `Host`, `Match`, and `Include` directives, wildcard patterns, quoted values, and inline comments, and exposes the result as an `IConfiguration` source so the rest of the app can bind directly via `IOptions`. -#### Tooltips +**`ServerConnection`** wraps SSH.NET's `SshClient` and adds OS detection, remote `known_hosts`/`authorized_keys` read/write, and environment variable resolution on both Unix and Windows remote shells. -***Tooltip when not connected to a server*** -![](images/tooltip.png) +--- -***Tooltip from Key*** -![](images/tooltipKey.png) +## Installation -***Tooltip from connection*** -![](images/tooltipServer.png) +No installer is required. Download the self-contained binary for your platform and run it directly. -## Further Information +The application creates the following paths on first launch if they do not exist: -- The program will create these at startup without prompting if they don't exist: - .ssh/(**authorized_keys**, **known_hosts**) - (.config/OpenSSH_GUI/ | AppData\Roaming\OpenSSH_GUI\) **OpenSSH_GUI** and a "logs" directory +- `~/.ssh/` (mode 700 on Unix) +- `/etc/ssh/` or `%PROGRAMDATA%\ssh\` (mode 755 on Unix) +- `~/.ssh/known_hosts` and `~/.ssh/authorized_keys` +- `%APPDATA%\OpenSSH_GUI\` — configuration and log files -### Attention: This program will save your Passwords! +--- -You can not disable this feature. The Passwords are stored when: +## Configuration File -- you enter a server connection with a password -- provide a password for a keyfile +Application settings are stored as JSON at: -Your passwords are stored on your local machine inside the SQLite Database, protected with AES-Encryption. -Only the program itself can read any kind of string value inside the database. +- **Linux / macOS:** `~/.config/OpenSSH_GUI/OpenSSH_GUI.json` +- **Windows:** `%APPDATA%\OpenSSH_GUI\OpenSSH_GUI.json` -## Plans for the future +The file is created automatically on first run. You can also edit it by hand — changes are picked up at runtime via `IOptionsMonitor`. -- [ ] Beautify UI -- [ ] Add functionality for editing local and remote SSH (user/root) Settings -- many more not yet known! +```json +{ + "LookupPaths": [ "/home/user/.ssh" ], + "PreferredTheme": "Dark", + "LogLevel": "Warning", + "FontSize": 14, + "LoggerConfiguration": { + "LogFileName": "OpenSSH_GUI.log", + "LogFilePath": "/home/user/.config/OpenSSH_GUI/log" + } +} +``` -## Authors +--- -- **Oliver Schantz** - *Idea and primary development* - - [GitHub](https://github.com/frequency403) +## Building from Source -See also the list of -[contributors](https://github.com/frequency403/OpenSSH-GUI/contributors) -who participated in this project. +Requirements: .NET 10 SDK. -## Used Libraries / Technologies +```bash +git clone https://github.com/frequency403/OpenSSH-GUI +cd OpenSSH-GUI +dotnet build +dotnet run --project OpenSSH_GUI +``` -- [Avalonia UI](https://avaloniaui.net/) - Reactive UI +Tests: -- [ReactiveUI.Validation](https://github.com/reactiveui/ReactiveUI.Validation/) +```bash +dotnet test OpenSSH_GUI.Tests +``` -- [MessageBox.Avalonia](https://github.com/AvaloniaCommunity/MessageBox.Avalonia) +--- -- [Material.Icons](https://github.com/SKProCH/Material.Icons) +## Security Notes -- [SSH.NET](https://github.com/sshnet/SSH.NET) +- Passphrases are handled as raw byte buffers (`SshKeyFilePassword`) backed by a `ReactiveBufferWriter`. The buffer is zeroed via `CryptographicOperations.ZeroMemory` when cleared or disposed. +- The secure password input dialog (`SecureInputDialog`) intercepts `TextInputEvent` at tunnel phase to avoid Avalonia's default string accumulation in the `TextBox` internal buffer. +- Private key files are never read unless the user explicitly provides a passphrase. Fingerprints and metadata are always extracted from the unencrypted public portions of the key file. +- All destructive file operations (rename, convert, change password) create backups before modifying any file and restore them automatically on failure. -- [Serilog](https://serilog.net/) +--- -- [SshNet.Keygen](https://github.com/darinkes/SshNet.Keygen/) +## Known Limitations -- [SshNet.PuttyKeyFile](https://github.com/darinkes/SshNet.PuttyKeyFile) +- SSH config editing (local `~/.ssh/config` and remote `sshd_config`) is not yet implemented (placeholder menu items exist). +- Remote server operations require the connecting user to have read/write access to `~/.ssh/known_hosts` and `~/.ssh/authorized_keys` on the remote machine. -- [EntityFrameworkCore](https://github.com/dotnet/EntityFramework.Docs) +--- -- [SshNet.PuttyKeyFile](https://github.com/darinkes/SshNet.PuttyKeyFile) +## Used Libraries -## License +| Library | Purpose | +|---|---| +| [Avalonia UI](https://avaloniaui.net/) | Cross-platform UI framework | +| [ReactiveUI](https://reactiveui.net/) | MVVM + reactive extensions | +| [ReactiveUI.SourceGenerators](https://github.com/reactiveui/ReactiveUI.SourceGenerators) | Source-generated reactive properties and commands | +| [ReactiveUI.Validation](https://github.com/reactiveui/ReactiveUI.Validation) | Inline form validation | +| [SSH.NET](https://github.com/sshnet/SSH.NET) | SSH client | +| [SshNet.Keygen](https://github.com/darinkes/SshNet.Keygen) | Key generation and format conversion | +| [SshNet.PuttyKeyFile](https://github.com/darinkes/SshNet.PuttyKeyFile) | PuTTY key file support | +| [Material.Icons.Avalonia](https://github.com/SKProCH/Material.Icons) | Icon set | +| [Serilog](https://serilog.net/) | Structured logging | +| [BouncyCastle](https://www.bouncycastle.org/) | SHA-256 fingerprint computation | +| [Microsoft.Extensions.Hosting](https://learn.microsoft.com/dotnet/core/extensions/hosting) | DI, configuration, hosted services | -This project is licensed under the [MIT License](LICENSE) +--- + +## Authors -- see the [LICENSE](LICENSE) file for - details +- **Oliver Schantz** — idea and primary development — [GitHub](https://github.com/frequency403) + +See also the [contributors](https://github.com/frequency403/OpenSSH-GUI/contributors) list. + +## License +This project is licensed under the [MIT License](LICENSE). diff --git a/images/AddKeyWindow.png b/images/AddKeyWindow.png index e9c1ac2876ad2fdc53e355a42c483aa2543cf192..0dad93b31871a2a30d27c9e1aa7c1cc18d6e073d 100644 GIT binary patch literal 29252 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 literal 17914 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 SZvp=YEK*|fkn*=5KmT8Ts{~O1 diff --git a/images/AppSettings.png b/images/AppSettings.png deleted file mode 100644 index efa04f6cbeb200b79bf66d6355ce701a5a781bc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29806 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 P^CV@(n+oM}CinjfXZE4! diff --git a/images/ConnectToServerWindow.png b/images/ConnectToServerWindow.png deleted file mode 100644 index 790dedbb5ddc17cdf03c34961ef1301bd264a7b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20429 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 literal 0 HcmV?d00001 diff --git a/images/ConnectToServerWindowFilled.png b/images/ConnectToServerWindowFilled.png new file mode 100644 index 0000000000000000000000000000000000000000..8f48b563fd38df88edeaf5b2ab5e35cd2167f92f GIT binary patch literal 20491 zcmeIa2UwHK)+ih+3JN<^rKwcun-Y3a>0L-dC;_(gUP3RzR-_vc5RiaD=?P6rfPjD~ zNE49~dhfmWdZSx+`^r7v|KIZ6^PJ&f-Ze9;%~~_-op)y7wC{8b@ROR7suJML835o6 z@dt1^ai&jIUf%S+uC|h@hT?AxO#os#|1$vK=;V&jRlavq-@x!D$=GicU)vs-ySaRQ z{s)Q3cc^E5YE9~X& z;!ec5{}o0+bmfV#1rfe&{U_M$Pq4WQ;w%1BB9649gU44{U(;8OuUI-k!Nlh!;x8k> z4WJ892HgAFe`1-KoYMdR@ofO$+_OLG%#r|r@?Zeq+Q1)mY_9+SiYEX-S^FP#f7rz3 z0ph_ocIS!tSt~05U?Uph=XB0fQr!}axj;jE_knB28q1^CMRnsBJE?_0_jK9Vg+;)K*!Syc zrvrd1=ZO6Ro&y5p07qX3;Xhq+(9x~IYSI>HYJot_kt9v6^Ea>et-H4`+%1}zPwjqk zTsF|sgtfD?XHr)u6)f;$F<>r06sYvfgipZp-nlHh%p0835GMw(e2G-GLynKZ( zrXzi5x9C7^R&XB#6sL*~eOzx$ef3Y4Fj>|Y&03^k>)zg?ws(tP!s!xPrci~PHfbQk z1~aSG#}V6RJTW>HMkZ$}dOHU))L&xJX)CeLq!Jl1950t**7JN=Y4Sbe#6{wPZW@Ww z9sC(nCXA$F8E2O|by9;cn}VK90lb1v zj$bY1c1}bDN7Cbknm=(Dh}UN+;5q1Lts0ok4d?K%iozq)9W*;*I(cKt9|7rtD-EXW z(6*`F*C?lT^!qRcdKir9ox>*{1HHv0?;Gc2+}wao`4CS>Hyv+r;WpvQxESyIe;(hy zR%8XJY5UBKia{)9MMqNvR8*<(C@l* z%#@mSyk1@0ZnW+$)UjtMX=o@dVWFwoR2ltkq6U3)F{E5ruo9CVWWDvGrfmPq; zspz*!8VmUEPRc8e=;vGgY0%+|OkP4w0k@vSkfwXIQd;2w=J1f*i2)yLG!9Y z3YN}i3|D2_km9GaEhGyxM<{GiIx>PEE>q0@7OTFGxb}_6ckX^KSCVur%bIqLW*^17 zBmGKWGB6itUNGg?vM)B#Kr*$t9MgD+^s3Dz-G7=0rFI^-<7VN;W+zT^sJvygmUb|7 zu|X~P9SEnd`>LyCE7wg*>>5R)ehYdSQ)drh5YjVQ`Yq#a&n4T?W$-{Y3KdIgn8V_p z@6M~llCA^K$w?tYu^jtMA?y=RE;=iGjLrZ@YvK5h1v{<{(?l0Fyhv`CcmM}5rG8yh z{UvcY++Y>RNPTccqfb`JR8{uU55r3)~yi(q+?SUyN54Ke7SV@TV_HHb99=oqHYi&fE1h) z=0tECo7G{UubI7`VU;_*;l9sEJ^i<=*tqg4pa0ucv#Z}S1ODNQ2{k5OMHjzid5KOz z>ZSrxeL>$cJ@xXtnSaR1WgmRi+1vVg=jOMJ5WD=v)_!`{@278>U9;z(C4(&fcM9(8 zPz_rK(ULMKu@tqtsCy>cEO6P!qVzuQ^7M#VGjz0dy2XnF!K&VBdcXIM4ES?m@Im`i z8=HwzS#13Rwu(CHVSz&47jvMcfnmSFt6UaQWeIP_Pn!^Ud!#2QHevnAitZQtsSg`@ zQJS7n`%sMk;)nD#S=s2sNV%M}Ke|p>5EG#Fe^9a_ey_yZ-k%{LA*zaR{b+pCb?%5F zI(>SgeL>7|@U>poxScw0iRo*@+ZK#%Ktvkdy}sW;0=v==oja z^O`y%PH_I-Lyw*|Wg5qV2TM z2rFz2ZhenksvsZ~w9c0NFWAjH4l`Bn%2Zi(z;(G*W`jGSLp_F>5qhk&;xi5o z`U69ys3d+Z_#XJz=n8YMZfH+y{6c=bDE>ABj=vSgX1B{4-*jvQ=0Cr8JG?dN&hKOI zmIwCJP^E#Zd)W|!>xX%TehUnY9c0)XhcV)KtQ9s?;XcT~@G*%ak=dUTRP|aBar zONfjK8f{pS#OyPb;w!Q4qZ0SWED5pV@X8le)OB0q;5iCPvLG!%0k~@se zp-5#!R3aC}9nc`Z9mRN}*OT>NZZPlzX!Wg-3>_b9e@=W;DuWm|kMLA;|3mA^>OX=E zB+1xI=VQ6d7r~b%?3JJxRZ)$Ii~Nou97lRA`~KM6X(&{B&5F|_+DKbkD#8 zzn3|;r3N6f{_9`jHu2NFEojPet7hU_(i{ILHHky|*8xoWf5Z7((O0*Q52^wABBo_v z^3>c1o*R4XfOZwXGa;^asEOa$660V`>trWqINEVL^7D%TEnU?8x zwo`x>+ZY_Fi8F5xjIXM3SnWyW8}VA*=km68InoZlnNxrpcHk{@-oNd*9UotrFoY&R zjviZJmp0(Fy)oJjQWdmPZ*?>w`@B7+*%dXzQv5WLROv~07lAqm1PW1ThDmqvlT)@m z@q!HZN)lw%or!}=KM`30;yZjqcV7m1? zgI;GEp&ni>wpPVoB%AeG3>yhOybhY8>4Pj%%6wPpSZ447IqM9M(4bQD;H2IRYPL zrCWG|f}}xMYj5-vFg%h^s0uv=h&4_erC??11&3brR~U-lX5Q6t-Wg(mL@JaH^Op-m zrjpPG>cQDR00KEB2Sd1H2O|V}nr^edlONBD2hopL^i{MDJv zLc_Lw{NsVIBT!79#n>oJff~|~n`0NMs45J9*|4tDIwh8~OI{C?8a^PioTL!#vMo^K z!+>3tx-@x;^*;8d!&FZJmvb`)cPp26w)#xB+DY4xrbf-wIB0G zJXnT_Hzfjtx)qo`OoymkWZyk+(jR`rh}tdV+kU1@58;Uinld8W*);js6gBo}gqy*_ zfmar^LrwuQHm87|qlhemhW82QrZpG-7e*cD;mbcWY1S$oEQElIseNs#jbm|5D||GH zitW!z%w#^Szf3b!!DJVYOk*n8apt&Qw$G2V(_?6;6v3f#EItSs*PaDx@l-EExaR`t zfuAMs7q2_tpRY9tTk5}fs$jqvF%$zG z#kJhe@q)c7KLy-A$nWIh%I)cZLAKFJ%Vrt}UL}fcdh$dD9%gBov8iV0lw{| zedRW5chhK!wSObu%2r8MIXU6YF&v}Q7lLhEH5rp zqf|MP%=L23tsaANoW#LVgw#}a!J4k|y`vG?!gl@1tI%;qRh<`B4Z0oh&!as zh<1MBrrftgVHl0d*n%xfM}G&{;j}*i&c)sN-u~lpM);Tg%MS%ALWtiMw*)eGX)oR( z(F1rowmFg;e;oG1t+**abtFFy&l-Heu-n$?TyMjm9sJ52Ia(-qLnlTW>Zsg>Kk?*FO*Wr3e#JM-x6~>== z7>gZ!Xm^=#PU?5c@UcuLAtmzuN19Hdr=Ets5UX{$WYqQkD$WkJAJN5_VQq%l8( zTsQZAhHPkuLxGhVvK6T#JT5F%7_d5;v7tX|?{!jbp8PThk?)Fi~VGrhw*xit9-11ZZpczlY|)47HA+i zNyyA zR}|506~S%G&5GBt*)vD@GUVn_m-}0)T2xJddW>vnLsWIXEymloI=Wy0sPx$itR{Y!1PlP#~<6?T>K5bg=7OvUPgk;VNB5t%o;OoP`-+SU2V zwev=k5m+_9^E9|?#iDTg6_DPbWpydP8+JAXf~3-9ild)~jGORwJ#7Oi>d+_$Qp8uA zqdw2SKof0+50l(s9}=2%F7K`+cs=}F6M`i*Y_a8w(=E|5pk&jx46IAa*;asRRM5$? znrXJXWF}uU-&GEq7rZdhs!k178j!d(;4U2i8ba!)lxzo6<--Qf`>;<|n z#-q9(eAw!0t zeO;tiT`Cai+s@hGrqjBwgybq=EwZ#$M0`&o3W z^jms9aWc;siv@XIj76kP$0i|n$`hrGRvCM|{5H4HT59B&i?jNojOvPajyr7ZI~npI zVrrfps?%v;r{t6s!z1GJebWRQre%u?rm1Nq8I{UD*{XY2j5nuSwjghtk_X*jQHruBKpAte z!)#i-y}^j_B05~v@rid#jpJ%-`-jMcp!AhwJW`gVT*}|f6Jq?p-w_p|?*9gKpo*%z zY`kV=591{WO@5j^A09qd-TNc%{728r(wdvkO{Z+-j!P{bNV%kYbgb#=bbrb9$^**8 z5*o-}%uU$S&@fN6Pt2lREHV{WynN*x^_^~M|7llpXVX;V>efLT}P9I5Q$7jG&;Qo{4_L{}LSa;aqOLl1& zd0w@?lJ2}`LduG%6z|+(ptWy-V44i|ib<;!ead79nXd8`=d1H0}g81q6?G44rxq zzry6LT<2ZGCS&UDD|#}^F>`(T!<^YviEJ9Zp-Asfuu&(E4s|AtB~N_+r90@;O11pgq04UmNwf8<{dL>7!ku=n!)Gfo16oNXxu|Ztd7O}ajE=a_UDqlYtiBI> zgE^nEup^3ADGG6dNY@cI%~zK?-Urg|&Y)kFE5e@G+H)G)b1~_#bP4XOiZ!nXdWa9z zCRBk-rvp!51;*WofSss!e7;#B$cb zEK}n`rQ;rXQ~3H#x1iagxXcvn@aBj*`bs{284_-*_5%cE!t-DzY=jM$6KO9W+x<_t zuqYF{#`>@tO}^V*$B=?pBwpx>6npW@uG|Gnhj`MZ7)&}^Wdy#d2!&TXi==A!1pvGQ zIM&7?#3#eKzw1 z4c6_5pba`_#wfJFp5!dt`3c7(YM3vN#@kF)8XEfhWEuEqatT5MiUuMWc!Abytojj6!jd*kQCSZ`wwzlz-JDST zdTq{@H{XuMGBXWt9izI2ouAn;-7%CKrywmrJ6oOtEYnYZ!kXod*6uFKE`jOG%Doml znP|LVrTMxO(HCYk?8=5yYj-!CO=G%Oky;_X!{k>=Oa1Y{gyXLAEWr=gLd|TKco-Zd zk=qHiw=<#*GGDI+fMpmHBYF4KoP%N7V#TL`;*L|mC2y$vrk}te-;K3!VJfjy|A*E| zK%4TKRTcQpJ@d)*g=gD~Kqr^WGV#v&Oxp!9t26u|hCTtFK0f4$c<3xe&)vuC7uh|5 zv007#qI%k4VmL)cV9PA1Fw@N~X)dCYw4vEeV=wCRIJB#6sdlL}@I6_CQ*)!HmG+=T z#tAT|pex{Tbmmi6AbM5B~+50uAxrs`PN?LD!W#LOGq%P)c68|8!O?~1`cIww&81_v-USM5;|!ms`1z?vUnYAkho*OCHKU*r-({W zP$EF=y}8?vDLI2>=b(G3(6(9C`@Y;cW1;$48!0uhVg*IL>fHB{vFn9rEYgQ%+d~UP zJ;#oiXlRN@!Mxj7^0B?$utgSqbZ-mNDaf61N(m>V2;Wnq3x=8g?&Ow<$YR&d{y5@L zCK}x;_waD$bMC9b^7};(@D9)W)JX|o!#RPgZ=uQtuBybfUcO$Lh_6!EM-XF9>IJBG z?~~xV!Pk3Icdzm{j!F{0ef&E|C9f}t)Uqc;961Ugr&`0+D$SDr=$U0j+R6&|_dZ8z zx@nBatZhZ9I|uFuLg7b=SA;9$t(ZLq{2msCLyaG1zg1?PZ7br{jv2r?MZZ_J7it%o zm>3^>Uhh}XApM-6C&_r1#==Y7Nv&MtF33VqQp}c3up!Rqbw(JzR&_+7_35l?gi~&V zwx#I_W%QnMmn&#|ls5MBZ@w1R%qc7Ms7VMrR2C32+Z2{L1=JcCtzQ&-uM3f!4RrzM zI1$8duYhQ1JO*~&&q!&SJp$%;n7@3ForZ2OlwT!a%Ei z3i^%Kj?NfgoUMEqB5HzNo1x4p8(P1z-s+KHA5tcYE6ym4x;V#zW8P|mwoDUuxo$1) zDyUV3NacFO0|o6nTIsv7nWE6E$-Esog?p89$&IPq(6`)`KGliG3DJA%V&NU7`EEL&lzpJ^+{81j;c|b3 zAK*4KcVUm1sk|kc_k_`pBMD_Yt;wMjr9BW4q24g##==LdcvhLJL(!?eT;2crf~`GA zh;%Qg!=8Xyx+nhXud)3fEBSW&6FAdqZKr+!++%Nk1u&xiPJQ{eH$=VQ_wT|tLYlwG zEw%-n0>D|s4If)l^|2(@(CdBE@pDc6uGwxWxv?y4$TqDn@87!fHO}q!bRT7fL}hQf zwaW+vL$zi2G@cU#Wx3K2xFuhUe8{Atd_bdTKWcU6VAv^oc5XT$P^uduw64lhkuDW>Z4Zndy<{ZDQ;d%qlw(e6RQ-TeU0t9PaSAAHyEtdmt9`(# z{k7c6dkUyDcrnu6z#kNz0~#FNsFbh`aPm(zaE{)%e&U&Uz(^W&-Pu@fZdiu5w!D?M zJ#sO-E2K< zZgS`AST=%r2bhAfJpon;K6A&mO##mJ*COrmBy<)$3{3}YtkXyZ| z5;z6iI0Y;Q9;>RC%~hQOpvM%sU)r})HroYOj+MN1EK^ram=Z6Jo09DIZXR#eh{zRv z>FDS@*_d6AHe#1GTn%m82I{dcRJb4pwst$P{ju-wISe zY+&^wHC`?{1#o3&7H(K37#1f(ANJ0j0ze%d?S~pClb=rkUIUfUGY2pA5@+YX`TBZt z+gk=?@46xtt==FH#@IejKW8Cj`yrqG%Q*LVAPBJ!j7&UN3xc|h5f8h5jl61H#wevO z;uYc9>YWs2;qGKYs)x1AixX&Xb~6t&PYhCIG9k(5lir33c7YZ4t>T+{1jA8ySU8ZB zbA5Ju<`h7j4c(`Jor+-g_0pKOr>n8`TmH@!-4X?gZ8WsX78!TTzAX;_KC}O4<^Lax z$uoo}Z^g5zbNYQ_743_VLbz*oB({zvx$nk@eB?r=8uo5SjC9^&_)?uo7o>yKfO5?WO~7P_)}{ zNu#-6BzDFpLR68#9GTOD6H<_$Gx)D_{geED{rZlgL~1<{LngfhVxz!*h_~qW8?^^7 z`VJ#<2ckNV@T7e@kyYNYB%L#UXPZ5KqI<(@tZ-q8jP7f>q5F=>yP|)fQq~d~`*Ci{ zPNF~~o?5^1>5sSN@2^c%h1ey)ndVo2H}v;&J$Wioif4(Phmq&lODK-INRCZ6bhC_H zcH4dqB%A`+rQ}mGfVtb(_52z4uB2siU+}tU@Y=xN)itS=Us&#f_r~?K^_(ju<6}Q> zwX)vEq|#g5NF*UW^F-s*oIp1foA#~eUOkVl+0($99yG^o$aeI*a^E0_52GI#=$(9^ zmzHq3-oAJVlX~r%9#-JlzAOR!eD4oM&&mJf-ywG_w{b7aNRXtB>J-p*3Si^5NO7S^)_1j*6PIAlO0jn3ej~ILFlNRMQ6Kqbz=&g7^ zQKaz3PIlb`eE|)?>itAHx2Cc=WSpFP#G3Yva_p3>)Ocl}{)iV%dLrKPs+z zLV276e-#y9cDydFt1_K<7g)@S&?uSGO%ASci92FHypO+Y$}Ur@b3Rs?r8NsXa=w-K ziHqt4Ewxv(wL9Yg(*0M}-6qVdLeQ~_) zzG~Zwsybo2K5kkx8ym;URRurE7TCrbhQ-X)?;(99fI`L`*y)qk^f6yrwh(#Gihv9C zFc~1@ih|CCE1R;kC5n2M2(i#d#C@!R$7S)cEmJ)WR2iM!>GV4iZ<7QlkFB56e2R!o zibUUm?e(YLpeUIjY~0zyAK!41|p>7G;2eo?^} zR94E4}>3BQ5!TkKP zR4wDec05@x!*Y77;bCuo@&3a{zd>otxEmnVxxxXTAAnf_`Llm)m8jkC^!(ZVvdbey z_=s9mF4{gdZs!?bX_m*TuiU4xVr9IHZ^U&)ssi%}(fZF;)qF_kjP<3vd#*=W2D}Ls zYIfT=c@F^a>6q9sF~IksQIK&v9E?9hwydG*vrMBG&CmP>4uiC-;=4>kqW^4FcSgA< z^YVKX(LshtMN* z!&5!VA!r4DwzPxr7CdbgOWt-M&Z7JaVZq0Ehf z)%PZKn+L0EpCx3yOy`pt+5a7ccZ=0L;!_?cNht^&X>$D5RzOwlr8UXfu8xD1Blaco zs?L%q<^K!B2WSw=dqT0i^`_w)|!5 zB&Kt<V1S#)%-*Ell?{J>Yvc=X9J)tGT~dMZxmw+DE*WL{(5FL0#4^Jw{p zaa9I~QP?ibsL;J*r-Z0{D58~LxP$?-iv*S^;z2=^ecDgvdNwQLs(naZptRv~{j{ATcjt4(|b@z78K2}>XpsVRS z&1vHX&#m zOt^Cr_COu7)zbq}*oXJ>qpYGj+S%G=$w55ao^1R1BPbfPu5eigudHDGPvrnW#J|&t zZ&gJrew*E+)5E6#j>O_{{AxQmFwyG><#pWVME&!F7OeWi&uupK&RAv9ta+{8Mn2hy z0L?--cn^Pq9X`#v?tOrErb{}V#*3t~xm`x-qtuA`6!H45Z7avBwb@@5iw&0~pE`Bx(3DrgorDKKLxq%}1#e%~kC?}6*HTjY068xWJgu=~YV zUbVm0%Ms;cG0Q!-Rsg@v{bik%(gwM6l5hGJ|2Z_AN;6+v8Dm_(>%>y_h`zk5y@xK3L$-7!VQnj#vFX2}0ohEJ%-gY0sb2GbW;8ldwW9NL5w+QfUM ziv@Pz+5vPYNlxz4`pHJaOS7-4b;dv4fFv>u!HhBzNJ(uhP;B8Cy*QO=J4IC1h=Nk+aPG2*%e~`O2G*!tBNnV3p}jekgm`q5LRJ@pb(c1rVUa!Y zJ4tnzSa6qC%VJ5^+;g6sTy5kOC4oB4=rt8tLd)!`o^+IrGCA^H(GHG}%^k|u-$cyW z3bR97rMq~e$@ph?>{=UEl01(V@9ZVh;A0xD+NH;dNwmcvAa-m1)Z=Y5;#ZY|kitNR zlQ`SM0B_K75dBX1!*L8EM%b@@XOY*BWwy&EnX+ zF2>PK6mm1UAJ#U?RfTUH5jRQJ53G>+!XG*0c*w6}in=oE_`k?()Fddq+d{B#*1-o- z7|Bi8OsKS0oim~$7+x0JTBOAgeWqlqM_;c*kBh zkae)gNzqnj5qRJFNXLgls7A%R-_-=g92&1{qd<80m%Ce zrvL)>6tMREXtq*r?#5y2PHMVf;I>`u5xbnkO7sEA$?S1*CIR_{wa~YS=erktG;fmac)W4LA4&0#o^^;v`D#+QdWFm zKIxc+2_LWQIjFBHj8gM)@{+{%4BCz1!9odkVqmpC0|p^NIuOs}j?k#kd_G(p%w-&Z zHM93J-=V18?p?{IJrNNg5I=|O_t6nY$p0D?4H+LWHz~}H4t=_rGBiKuFKM-I4BPki z_EwIoyzkVYsg$6~>WC+JTIJn`$fBU`&#)4-oY|D+<5l6r&$XTsPa}(Y=j;eV==|Jm zZZ1Ja8$E*qiR>hH%8sV>G4o%b$n8Kx+ePrarX|^)%9LXs!GhPOtjc@l}xJ{9bX!2*om?T zHn%TDRjRISbwFX->eAg}o;+p*LYWNg>@y8Gcz((Cu7D5`0j26VMVHmyaQFWf8eVE+x%mdsfs<)LJDfajL4h*NaB5*CZWTG>td`O2y74b#D>-o83l5j8G_aIN(c`hD~ScByBHOxr2!bTymaXtTY z$Yixx%E7P+I`JLC5*-D%-R6RmB;-?~*5@0Ok{jb|Mg*kiM6_bGVuZ!AL&?1Mynj8{ zz4I$^){*2Zx9R%w?4Xsu1F;2U|JW`M+X~ZruiRzf1)G~znNb~GF>~F}DJ|Vg%YM%S z!K|kMX6@41%t>faXukTw7W1Rf?0F+zgJeT%MEJxgl{=)mn1XgMIW2Sf+43*@FU^r- z$DtNmj0>~OQC$C~-6KluQNIj{B)%~{!n?eFm`1=MsG){q4&!GbrO!m9_0+!mxxjU9 z0?oHBrl>x6bKN<-lG~)8yoW@8(wiyni(77ruXuK(zh;N-PLs6Jgi}OfnLO@MkN615 z|7oG75+MC}$apEX~^yRWf7?|Z`qeT)>Su~#|(oHy7 zjxkdpz2poq-3yTAEI_M>QvZLX+9EAqjgcgTvP3VfBhd5X0_tu0=xddr(o(>;n)KtuvM zrs1uNIn`cPFttWKAy9lPz=?CMhoWYJ6h<(@^t}-rphw8iN7O*HbO9?>5vKs1$y0zJ z=Rjy*J=Qkz!@iu_eEi%C1~u`K!J$(?LkRbrt&}@6uOLibb@27@qee&Yrt#~}jVROd z%j*v!D(SU`Ng}U6zjS)edHo_vVESil!g^cfpv7+@H(FU^^316~h(byhT80zK9Gt=K zfnMGHISUngbL+VctkqLxR~f`)Yq)RgHJo}rlUO!PgP@_9{$b~+dEx_&yU5E*3fgPA zuQQgvth`IMOR192oMx`|KxpnsM(Hk96A?N&$#cxjgp`e4L8+#iMv#F(&CX1Dh zn4j7Y6j-bhm!zcDv02v18G&O*qiH8xa?8Zsa)%STpUNmYI#eRIW-PCV;%ve68R)wIAoJ+QIW=-J236w@f;yzJ5|$6d7`r% z-JD2kzrMl6klEQQAI1QM>6R!OkXdCa3+v5|n?^K5Z;E&qPmHx);vRhYv8*T_=YgYw zd_*d#?bE=!og13{lbF3oTdIOB;^PUF5QrMSMwJPLML&hx*@s8^?XJqjzupSqhz{2T zL$nsPR?BP23_G-c1#HT``AZi7cEdGx#FzRR;yNE}(^cKfsAtzt+!*@!ZX3ou6}|=` zNIz(&bqs|)uN7zNbv0d(4xC{-yg#n&Hl(F8`Z!j!8oL8giMA6Ub0hwt#a6}Wa3EUl z8Sy0Fe{$n3fP!aj>@VWZn7;!KSXQaDKGo5_Uxd|>mn!`w)|Ay`m6I^lj#L}t)y(W7 zqq2rW?fbhN-97!}d3bWlr}6aIffj6*B|E9W*{%<%jqyWr6ioz0E`8MAVQEa=c`j9_ zEJa5?9+-$3@z169+f6_SG@bo)&$70z7LEy4m95UQxq>@m_jQi zv%!7gK?7!Ybw~kl(d6b|6a{p@J!&^PG11<`l-Zh2MKLh>7gy(*+fG5<64|sLXGZaj zuNK^Rq5b#U_A>D>eH&tzHyIOsei#D=Z82W4UK7Jy36K5LI3ci&5-m# zZkd5;%Lu$isnHn_8O z(aFDuorbZelIHlFABXhchNHncyxX!;FUyD3Kh0_-hI9@v^Xji59dHKclxt`}C(;%* zDGxa#ov3C4J4mf? zoiQs%VE7H`(b);VZ-mHiNOvB&%HX&p+P9UyA>F)FfgX6CGW$Doxb`=$I^S?qe&XjW u`^q)v8&1Y0x_^oOr#Cnz+qFW}W?P3P9fpJ;&yErQIoQ9_zbpVa?f)N5_O}@T literal 0 HcmV?d00001 diff --git a/images/ConnectToServerWindowSuccess.png b/images/ConnectToServerWindowSuccess.png deleted file mode 100644 index 121a1eec594625b69cbe78f1193dd474256df04c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20596 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 G5}E+@5zVRq diff --git a/images/ConnectToServerWindowWithKey.png b/images/ConnectToServerWindowWithKey.png deleted file mode 100644 index a476222167b979e599db41577c125db7b16e6514..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28124 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 diff --git a/images/EditAuthorizedKeysWindow.png b/images/EditAuthorizedKeysWindow.png deleted file mode 100644 index a6fd89b6efc430aa143b329f53076c959367809a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12377 zcmeHt2UL^WwrJ?)f5NN*~2=r6%fnR}|BsDuU@Ut(##_TeP*eSUH6n=KUWN`@u zDvmwOy7LQAK6w9%LjVZG|CRUbYeMGU1%cF7uNq&n4R&6c2uXAdOq=Igp%m3mJ$WE< zy*Kh|^3SrT4(v-lrIPvcug4<~t3JLHc|F+i*QU=+ZCz&Hqk^*n=5iSG?I-OdFFfzK z6plN8$++m?A+b-OJ1swn&p%O+P+#c2DDvClwi)VXm8Bg@-#i?Ru1%ji!-fRrEv6EQ zd8kgko31yqueZ$jS2i#8>4w6BvV((Diom{EOS11MF5YO zDO;{lx7Jt!N}N6TO?=1)WEy?D7`?qZTp8dVB=*obx1iu0xc*WzxzU$W2XRKPln$4A z+4ZnWTH+K%op<<-iXVH-CqS@(ImfJkgBrHdz|@Wjbd<=v{Q~951L9;^UBn|FQ>67` z!v{z#*QPDd?%g7jioU~U=k8$l3081N!B&*iK^1`WI;JI>5#k2qjC7al(KrF4oZhicv8j4h6>jD zU<6*Rm;fVM)N>3t<|-5^1xEqd-qHilF-oPF_1JGVjQP=PkiCs3f*$=9r!l*>_0K-< zm)jm(4PJPcASah;ki7MT*Rz%ya`EkfmQ7s8col0`%cU#5rcUbfR;Er1)IFHhhn?UE z^*xxY-qe5a;(}1Gk83z`Bm@0uqI$btIotS4_1Jw-0$Glt6_zQX%n z*@6A6u2>EB4bgrl)jq7>+VfE4B0D>Bh#q9SLQ;IiSB0mq&6cXV&O~&sPN1g-hRT}0 zN61LeAkkxE@IC$vF(Y?||NaP5Wbg@a|7i7GX{pax>EpMsdtI{;&Va%bR$1*Nuc1|4 zV7ScX0#auyd*-jBd+Y^!Wn|SU@G&8wCa0Q{;Qpv|PC;MaP?7ww+x`1hOXfCSzZF;! z*9=dnAggDtoP9^kXMChVQJF#BL{&0>T7A%h(>3<{X_@`+W&>4JWD1WTpHYW1U!MUp z+BE|57YAME%9SKeiaOWI%E|dwy4~$z);)f^ge-gfuY=r%j-ZeS2Xm%TLoe$ldb8fW ztm@nt4G#x@#(HO})=H-}2Q6Tdxg(lfHmxb+hT>WFJt=Ug{ma_D30mhsA(9fg*dc#P z=A=W%>3&QcJ-rG?$<24ikyXp95Jpz z#g^NjFl54sKC2FX(e^~F=Qx=;H1a*@9vd~fD%c{vF}#M$KH0)|1zS-2%5eAh8rFE; z7fsq?eu-!#4#y`tArUa=cuc5gc4l4L@d5H&4ZCdlesxD$fVDxz{N(Z)HU+hLlfE9% zbvHtw`O8@NEqmH8LoT9+iDcq=R=a4-L4yIaaQ;!F-2JR;&MTd5<9w|DA*$X zVspycB`Q86RZ7`V5A1BPXl`Oifvho1B7926m2JYzI>Fp8VvsvF(dp;U606*tdunWw zU5FvCHSP3*@7u=8BuM!Vd`yl*>|_bJMhJ}F9BRk+2opN^Wo#J4Iwem_=)$v!PN3GS zc!iLz`=-Hulj-L-pv8a>Dsr?f!3?$uga4=%sT(>;Tp_n>k9Bnn2~SR=^aaND!%2Yx z$B!Q`zVkIZcyqwc+|p~XP7tUVY0#fA65yV#d$4m{1%66K)}@LS6+Hjl@VE*o{5CY8 zOxd`kE~saST4uO<4#JK_x~WZ`>bS98lV23|E*2sk=rtJBORhk%^k*&d^%PVqnO_q9 zTQ`;q9@~_=`B;Cu$uI4wsg3G1u?C7Hg@+q83VLt#eQwN#$4W8wu5U*@czD}kZy#AN^qr4q(`BV~=0s55i78Xhf$ z+gG%I8AB?M$9iZfiZ;psh`Up>+48YC?xskX|1>k9` zIq8V?2I@p(<|h2cRQx7)L`7l%vEI$^W)GzP1}srtL*Tjajqi!K0s@Xc zislS-;_e-)uF~?tH(KVW357eb9gSzt7qi0WhP2QrK803{mA(oee|6d-*P+77T32#8 z+*Gr0Zrx1ABj{*%iezholzuElzV<4o_752CAnfdot%cuxD`(Yvp9}rQCne{~S01cc zo77o1LZ+6%rX(o4mOO-sj6d+ge|;n;2OwiVPyJz37$x#-j%MAZ^jiV`w*1nP3}lVo zEG0Mqcy~M|_|BkYqga>5I*eEv+~wiaalNulE}0(^y1p=@9{S}ibA=|-bkn6)F7vGA zdV&zsnAwXB{9#Al20`uT%RjpJ%z>Xyl&mwPtTFoW&EVIy($=3X;=Bdwy)FA1=L7)B@g zJZC06y!!r`v5(0&pv1??hF9~E*C4aoD(Fm3gQ!O%1!d9I5 zJizB#^$_cP?g-H)v6lsj5KtOxC+_ zy9Y^Ic`#BCt@4byGywpreDY7OlZN5t_bbhzC@5`jFxRQ|F&-<-n+^__7}&{| zyj8EcdXPD&LyTUeQxAQ9MrND6f{Mqgp-bjNv3u1G>UXcW_FP>;Tg@YD>cGC$@9tBQ zk@(n7eVtXuV3aJpL@}j{OrQPY&oiSh=v5!}S6uepAA=3;B@A>m%;)7{h*mr=(=x}Y zTgY*2-KEfu1IGv!k0)s-6`L682-|E~PhSLzmSQu5C&1MTUdX#ERVRAA=4%2o5`wJa}0T&<{|5zEeeDu-e%%}9~ zYMG@G*m1HF=dJ66;sKm{Z^ zHk%eSyO%XP9XB;ImmZUlf!3CjQ{5RCl}nz=^1tsYdmV7YH-Hd82Bpfnc_-wxqB485 zhxC!NyYGRa4u#ccUo$_X(uruXGM9FgX(=!1p8EA`snE|xO7&b79-PF=Jb88(cGokiIm zoghQxRu1D$SE{|E;w|2w3z`%}UBpnTHDK^~q(R~3&`LL9ld6FmHVpZrRRdC#HY33o z_h=f%$p<1chhDP}J->G-x=aq?!nmr&by9$p50>wG*rnyvhD(2XldxzK@i-2X-p)DF zT{g>o&=bEr^u{5=(khg`bakmW$=gi#{vKof1s67!;i?b&vKI=nGT}qfwgMlIcpii23)?a&KgYfaDr7P&zfxwmk$2@AkfPPS1!Nx@2|*})KUc>0fBz+l|A@;D|s&sMjc$L`%QzS zQB|2_=O|G=si`*W<_?Ulpm6m037Hfrk|D{h2XD|P zJW=$GbPhjXOx$T1QQyntJ#27&{%$lQ`BjS4Tv%P8Tsw_23SDlcc!I_ju1EN>&_va|A{V)L*@rvFzXb?%aA{V$>}f}&vyV}4B< zLUB-SwO0lhqtrO3b8>k__Q%Y9cdxZeRzjtBWAnums+oBDiVHDH3eg!EB2W-5!O6ZC z$(-y}@LMs7%jBBJlyB0L+#4fQ>y%~?_SoB89rIsug{ZA6t2 z7(;qAy=`W24jX7qH$WTzZGK7Dv$w1!{)oL2IKDQLWL=4NXwL}r7>`Sn3Z$!PaLvX^ zvhZHTxrX?TSzGSdFY2TV@R9JaGpbI*-llO?G-q*gFx;%ZJ_(($$BBU3QnIEhtgyw? zK2Cv6FLGm_TVqUvq(Pt$hE>5hM-fGfXxt2*iq9u8)@Pdv&2@|=p=aGVUnhIoCN;F+ z+a>m_cU7~ZzS*j(X`%L#+KcVXAh#M~{8%2;|H?)eKY;4JLa&Hs!|O4NP$3EDFavN- zvsFeeUdsAJ)*+d!K!@v6!a`~T@x!DZ=zx2wXaZ*LX~pIoeu|z0 z6PzEF?aiTc%X~Q=L6*gZV+gaJi`+(6ly^*n33X3+oe zMLex1Xqivta7)iEk`4n{{vph2BUj==SP;;QY=rBU?eaaxGJo4;L<`)-`52z-6Lfu% zq_oP+F{id?wPgZC$J7T|KuO6O)JN}NBy*q_zSqE%$U_agII$fyFWeMlBX*Wu(Pe@D=@Ui&Va18cbXS#Cl#|VHs)YY948W<^7ogOQm zPQoQ#&>;G3rgY3T{-Px>?+iPR=VEB{0y6d@J?@^VjEq9el3C#R2XC~u3psb~y@R~+ zo>_lUw1NWERap+cdZNk1HAoNy8b&&yW3Ti#yfbu`+{9!zj{o(p?&Amjx)LVZud^9f z<fQP=2XCUTrOv$ z&~Jtk-Oh6_e6Ro2jWMx1fMMftxN6P>F5P;QOQ6M^*Z z4hD6UZ&40@MRL2iZK}i}F4KQIFOmD5I@Qba>8R35CnJJvX+(Sjm#(JHM((P+vs>egZO5dP+f!xhH#T#MIv|{DZk3Lz<+Z-nt7rwXAKR{SPU3xY{XD56MAbR%<5RD z6(wR(qZ5AuA#MoE#|~Ig)6GZ;_lP?y%ebQg6FuwofdJYc_74tbLo@-%z6M0i&nVyr zfi{dIP8lx_6~*jsdXWG=8KfZf!iWU?j<&sV0QQL-!GEw@{9kP?|GCD>T{He8>cCFM z?;6Y7%4}5=@FgcS+@_^N=ie;X5CZG#bM&Zcy)Tf}~+k_7Kl!iHCVe#qm zyM3UC(svu9Pb?VSUH%wbzt9wm+czUr>~6IjlJ8fOi8L`bt||WhzJg1ZA&5qa0?-V_ zP0L=Ge-c2Y_T^<#BqR_Gy@ri4851?A+2?VHS+o~?svBGEHSv{>ug*BMX9{R_E=e6> z+8SVdx^d&|%^q4uOz|?ArbGaESi4*GQ)8k{i+1w6H!TRs>J217pmg~=INOG2e+r?? zi1>F!ZgVrWg7_g{0`8JJD|flZjUXu@9Gfo2dj@VkE(w64jBnb?K-5o5L$sf|&q;l_ zWF2Tr#f(svq@b&yL104-eOR*4moq1gWqw}{=F^2hH%AbN?rc|eZs)s^q?NwqwZhpg z%>w*!bFQF3xo%G_U7N1}tt(xSA(kTrHrS0fGwyHFFyw@c%g~|%3^gEmL1e*gia*jI z&Y{ZsX1f7*JTaI#Yfoo@r(*xYi60YouhH=&iGN3No=o1>VDQdMFHbKsO^Ab0k zZZ&m(j%fNuR*kLl8QIx8zq-Nf=d+5+`?_V!9?h95cFl)xCRYvD1YA5Kt*6vT$PhF_ z86NLKD&0PFI>W$VcU8}zu?}&6d7K$m5Vja;UmEIDQ{f7$5t7IX>whbz6L#@s?Uro; z$BW^b5}hR7mvbZ%Dq&aeWaV>DT=E!Ae@>pBo)ux5F{IO&k-@rQusK(i3v-e2V~!#E z6BiTDmm$4FXWwNq98GH&^k5%&jRO@<$YaInIe| zxLt1*9Qm#2Wo#5;Qr&=Hu*gh2&nz6Saz$t^w>uMG=IOH8rRv$?Mg<(Ulizl4Rj_N@ ztKkw?b8q`S?{kz3=TA#&DVZKMyx<(}8AyT%#mHXx5FX+kHoa9r<+vjzvQ>&*+scBo z7Jf@<%Y)l&uIXk{Qt0&=IwG&yQ*^Vm35Dlk?b0h1)14@q!K@Mb=N2PG*VA{!Yog@X zn-b!*wpSKQQ#MFY%!bGLSy&V~*B~@B70q#{YKVp@bwaXE6MU8oA`z3v5z6x7 zgDiECa$S~^+~A~)z@V1#5izNz)R$=|gf_dHWYW(SI|7_rdQ{9&AvU3a@O~sj9l*KX zN%&A>F519Q`^+S}voUVfj)ir=e=U*|pL$=N5gmiPRlnw^WgVX;RSlnrYJjGTfgU=0 zH9wQJ=IkyNxi^7rzqU5e85u0QadvZ5@Es!~V{U7%eyC?Eb~OjF`paBH`&kvM?Q~si z@Dbe8Vb=8}BxQ&l5};{pa^!@b9#}c3)|%32wyy|(TtY%_mp`_61gGZ)KO!SYZ)_YW z-abNTQHM9zSys7-ES0TB>cWGsw|7SAObz6a5Fk)Dx@K$C%Ua!cyaHLbc6kZ825uzY z8)m`_ut6aP#mmL^)jNe}B+j2#iZ~J@-&ChDsdv||0K5kU4jm8xZ z3|KpPiU$QBkq!#p2q!_MVey9j1q+hTpC(+B4vHUI8+xODr*#Z*Wd~NgJi-b&Db$p% z4>`?`)fhCa)4hfC5Kr}oUzY%%nG9oZib3=0+4quvl~Ovn7~bs*jMvSzin1C@I;7`?TH&nb>S zF4UvAyMZh_2LcHS3Axfjy3qat25T3CY8aA?KP%R15m{a4;n5l6BsqW`}%hy4md4)+v`u5u~d7Hp5)ri>>Gy-a6NunS|=+?&wwbQc%(orXinJWnehTFcA3M-BJ{ z>@;@fqH=A{xDK@3uQxM9a$?&$ef+2Uby2OjTmZ++_QhJNm7#Rkkl7J2h!$tI!Ujoe zYFv@>v;ac}p3IuY0E6&Rp9_t1(a9o?Ri{Y33_+UL&Vz zczGEo_TWpU-)iMtV-A$QD)v0>AdpkC`U}$38kQnr@Wt{GjkqjA?7^Exh>PSXvGc+A zj!0>%p0)r(It!8n<1yRDY!W&4FDq^C;^tHPz0psJ_xtC?w6+ox&%^)NP)5Tfq29p1 zfE6YKOf8~i9EE@_LWJy4QoQB?TVeMXJlb?shxb;9X>F{X2@gORRIj~16C%=_y-6R$ z9aQ)2Pt3*6C5l~mZDo5zOwuSKChA~Lst^sb+C0B~3&Yy^j7GSU27vGuFh}NlhlR_h z-r4pMqcyGhm<5 z&od=N_3Xs9Ndu%2NBDQxzu2^q3I{g^+*XFBSu20v2QyELwBrnHzuK5Yela;C$?V1i3-Bkm&{OL5z z)<~KdFG?%Ybni@kpqLK?FUH0NE@Wq3qU}*jZ9fYu+MKs<`zt(O1vl^-vq zcfS7m<7I}52GDFN`wzGSqCrBNYZEYn{!7UJ7$e5(Xlma5@$I^i(c>SVL;u%=!+$O+ z{6|#Ae_7AOu=Qzxq2R@y8X@b`xo6bWxU-Eh5+nw1^2Q+{mAruY;l{s{V)-wV>u=;u zyxEaf0)T@Dx$7iDcKP4ryy-lFGztRfn%740NXHP=zq~qW=z)PmvHMM{Sw_I^zt`G% zv;KQq&h@|7Zp$P|0oa(gp8>Ku-M{~r9a83n6acUJ-)FWbu!z4mbqClPkbd(2kzD#m zGbe`izPVOP)0Y2K-9J_LzoWGO ziH(0^_!Bu zn2q#UOac7@{O5q;u^memHFVAEmq<=W$@`vu>2q~hB$r=WX$(JVOkwz)I}EWM&bEY_7cj0u4U)BYjq{_y z-NNR z3Sod=#slrBu8T_~^ZniC>?L{b?F|OAmF~B*(MDZgNN8fVPFdNvJBx&pN^V#|w6=2$ z*=NDhc4qZDq{rQ`=`K;d;stz8dT=H^RWJh4V~){~Bew9D&0;KsL99BDR^8418!JM8 zz8EZsWPTRdN>AlHmca>zmX-kUmOS<27K_Dokg+_Od9vBw zyxEX#k+Nla#lgW32b?mj_#p9L1q==N!OI`!fiX4!#(rdA)48Kus2j|zrcD66YKhTB zfjJtdd$bmfS{^QzFGEtKi)y!S^MpNL#JfBa-W;Ww6>W2K4Hyq})x^@6c=?Y<{|$6V Bybb^W diff --git a/images/EditAuthorizedKeysWindowRemote.png b/images/EditAuthorizedKeysWindowRemote.png deleted file mode 100644 index 980683fe317a5ee96eed253b2119f7d61021baf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26839 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 YZp`|~E?+<~-SM-~Iq# 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 literal 0 HcmV?d00001 diff --git a/images/ExportKeyWindow.png b/images/ExportKeyWindow.png deleted file mode 100644 index db168082a09d639a873659d77c826edb2d4dd0a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15383 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 BcNhQw literal 0 HcmV?d00001 diff --git a/images/FileInfoWindowPasswordVisible.png b/images/FileInfoWindowPasswordVisible.png new file mode 100644 index 0000000000000000000000000000000000000000..ff683b72b5891ff6088ca8f8d39fa503f22d2d78 GIT binary patch literal 23015 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 PScQhFu1cYj#hd>Cs!2Px diff --git a/images/KnownHostsWindow.png b/images/KnownHostsWindow.png deleted file mode 100644 index 345ebe833d523cad18de571dd2e1695ab8044e69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40635 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�IkATPh`HwT|DN~ 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 literal 0 HcmV?d00001 diff --git a/images/MainWindow.png b/images/MainWindow.png deleted file mode 100644 index be1a1d2f6cf828ab6a04e87c7388b43c6731ab5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48739 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 AUH||9 diff --git a/images/ProvidePasswordPrompt.png b/images/ProvidePasswordPrompt.png deleted file mode 100644 index f859648d8a21c06ec967c52bf20159381d5cc792..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8571 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 diff --git a/images/ShowForgetPws.png b/images/ShowForgetPws.png deleted file mode 100644 index 9b11e474efbc2d03ff07fac8b3a7ec3f1948ef66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50856 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 diff --git a/images/tooltip.png b/images/tooltip.png deleted file mode 100644 index 1dd8817bc2b24e8896a7de7aed60f08da540fcb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4786 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 diff --git a/images/tooltipKey.png b/images/tooltipKey.png deleted file mode 100644 index 0dc3a9fcedbec3cfeef708727908d62e14257015..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14358 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 wn2CeqLlU diff --git a/images/tooltipServer.png b/images/tooltipServer.png deleted file mode 100644 index 5a66a4d104bd8e97643f669ceb9cd04cc13fc43d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11403 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 From f9f6e489b8309deff3be502b6ddb988ce6d36d2a Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 01:49:16 +0200 Subject: [PATCH 15/58] Bump version to 3.1.1; update PKGBUILD, normalize version logic in workflow, and adjust artifact packaging. --- .github/workflows/build-and-package.yml | 9 ++++++++- Directory.Build.props | 2 +- openssh-gui-bin/PKGBUILD | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index ffc3fb1..f0073a8 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -52,6 +52,13 @@ jobs: restore-keys: | ${{ runner.os }}-dotnet- + - name: Normalize version + id: version + run: | + VERSION="${{ inputs.version }}" + VERSION="${VERSION#v}" + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + - name: Publish application run: | dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ @@ -61,7 +68,7 @@ jobs: -p:PublishSingleFile=true \ -p:PublishReadyToRun=true \ -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:Version="${{ inputs.version }}" \ + -p:Version="${{ steps.version.outputs.VERSION }}" \ # ← normalisiert ${{ inputs.is_nightly && '-p:IsNightly=true' || '' }} - name: Rename artifact diff --git a/Directory.Build.props b/Directory.Build.props index 228496d..6e80f39 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.1.0 + 3.1.1 true diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 2b7ceae..1ec676f 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,5 +1,5 @@ pkgname=openssh-gui-bin -pkgver=3.1.0 +pkgver=3.1.1 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') From 4a91f0a364dfc2e82b99f20de71f17acc1fc5bfc Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 02:03:39 +0200 Subject: [PATCH 16/58] Bump version to 3.1.2; update PKGBUILD, enhance version normalization logic in workflow, and streamline artifact preparation. --- .github/workflows/build-and-package.yml | 32 ++++++++++++------------- Directory.Build.props | 2 +- openssh-gui-bin/PKGBUILD | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index 225cc51..28f9309 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -52,13 +52,19 @@ jobs: restore-keys: | ${{ runner.os }}-dotnet- - - name: Normalize version - id: version + - name: Normalize version and build args + id: build-meta run: | VERSION="${{ inputs.version }}" VERSION="${VERSION#v}" echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + EXTRA_ARGS="" + if [[ "${{ inputs.is_nightly }}" == "true" ]]; then + EXTRA_ARGS="-p:IsNightly=true" + fi + echo "EXTRA_ARGS=$EXTRA_ARGS" >> "$GITHUB_OUTPUT" + - name: Publish application run: | dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ @@ -68,8 +74,8 @@ jobs: -p:PublishSingleFile=true \ -p:PublishReadyToRun=true \ -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:Version="${{ steps.version.outputs.VERSION }}" \ - ${{ inputs.is_nightly && '-p:IsNightly=true' || '' }} + -p:Version="${{ steps.build-meta.outputs.VERSION }}" \ + ${{ steps.build-meta.outputs.EXTRA_ARGS }} - name: Rename artifact id: rename @@ -85,11 +91,9 @@ jobs: run: | sudo apt-get update && sudo apt-get install -y librsvg2-bin - # Download appimagetool wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool chmod +x appimagetool - # Create AppDir structure mkdir -p AppDir/usr/bin mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps @@ -99,35 +103,31 @@ jobs: cp "./publish/${{ steps.rename.outputs.ASSET_NAME }}" AppDir/usr/bin/openssh-gui chmod +x AppDir/usr/bin/openssh-gui - # Convert SVG to PNG for AppImage icon - # Use rsvg-convert to create a high-quality PNG icon rsvg-convert -w 256 -h 256 images/openssh-gui.svg -o AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png cp images/openssh-gui.svg AppDir/usr/share/icons/hicolor/scalable/apps/openssh-gui.svg cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/openssh-gui.png cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/appicon.png cp appimage/io.github.frequency403.openssh_gui.metainfo.xml AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - # Use ~ for nightly versions in appstream metadata - APPSTREAM_VERSION="${{ inputs.version }}" + + APPSTREAM_VERSION="${{ steps.build-meta.outputs.VERSION }}" if [[ "${{ inputs.is_nightly }}" == "true" ]]; then APPSTREAM_VERSION="${APPSTREAM_VERSION//+/~}" fi - + sed -i "s|||" \ AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - + appstreamcli make-desktop-file \ AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml \ AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - + cp AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop \ AppDir/io.github.frequency403.openssh_gui.desktop cp appimage/AppRun AppDir/AppRun chmod +x AppDir/AppRun - # Build AppImage APPIMAGE_NAME="${{ inputs.asset_name_prefix }}-x86_64.AppImage" ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "$APPIMAGE_NAME" echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT" @@ -157,4 +157,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ steps.rename.outputs.ASSET_NAME }} - path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} + path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 6e80f39..aa0b155 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.1.1 + 3.1.2 true diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 1ec676f..8d67b0d 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,5 +1,5 @@ pkgname=openssh-gui-bin -pkgver=3.1.1 +pkgver=3.1.2 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') From 75c82ec2666aa372521a3e87501bdcfb638f4140 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 02:14:05 +0200 Subject: [PATCH 17/58] Enhance Komac installation in workflow: dynamically fetch latest release URL, add validation, and log version. --- .github/workflows/build.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index be9449c..0982762 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -131,10 +131,16 @@ jobs: - name: Install Komac run: | - curl -sL \ - "https://github.com/russellbanks/Komac/releases/latest/download/komac-linux-amd64" \ - -o komac + KOMAC_URL=$(curl -fsSL https://api.github.com/repos/russellbanks/Komac/releases/latest \ + | grep -o '"browser_download_url": "[^"]*linux-amd64[^"]*"' \ + | grep -v '\.sha' \ + | head -1 \ + | cut -d'"' -f4) + + echo "Downloading Komac from: $KOMAC_URL" + curl -fsSL "$KOMAC_URL" -o komac chmod +x komac + ./komac --version - name: Update Winget manifest run: | From 2ae51bfd7b8821220d1fddf980f1c492442a0367 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 09:12:58 +0200 Subject: [PATCH 18/58] Refactor GitHub Actions: modularize workflows, add custom composite actions for .NET setup, AppImage building, and publishing, and enhance release process with concurrency and timeout management. --- .github/actions/build-appimage/action.yml | 127 ++++++++++++++ .github/actions/determine-version/action.yml | 43 +++++ .github/actions/dotnet-publish/action.yml | 40 +++++ .github/actions/dotnet-setup/action.yml | 59 +++++++ .github/actions/install-komac/action.yml | 37 +++++ .github/workflows/auto-tag.yml | 31 ++-- .github/workflows/build-and-package.yml | 129 +++++---------- .github/workflows/build.yml | 165 ++++++++++++------- .github/workflows/staging.yml | 95 ++++++----- .github/workflows/test.yml | 34 ++-- Directory.Build.props | 2 +- openssh-gui-bin/PKGBUILD | 2 +- openssh-gui-git/PKGBUILD | 2 +- update-version.sh | 14 -- 14 files changed, 535 insertions(+), 245 deletions(-) create mode 100644 .github/actions/build-appimage/action.yml create mode 100644 .github/actions/determine-version/action.yml create mode 100644 .github/actions/dotnet-publish/action.yml create mode 100644 .github/actions/dotnet-setup/action.yml create mode 100644 .github/actions/install-komac/action.yml delete mode 100644 update-version.sh diff --git a/.github/actions/build-appimage/action.yml b/.github/actions/build-appimage/action.yml new file mode 100644 index 0000000..339c2a6 --- /dev/null +++ b/.github/actions/build-appimage/action.yml @@ -0,0 +1,127 @@ +name: 'Build AppImage' +description: > + Packages a self-contained .NET binary into an AppImage for linux-x64. + Requires the repository to be checked out and the binary to exist at the given path. + Note: FUSE is not available on GitHub-hosted runners, so appimagetool runs via + --appimage-extract-and-run. + +inputs: + binary-path: + description: 'Path to the compiled binary to package' + required: true + version: + description: 'Application version (no v-prefix; may contain + for nightly builds)' + required: true + is-nightly: + description: 'Set to "true" to replace + with ~ in the AppStream version' + required: false + default: 'false' + asset-prefix: + description: 'Filename prefix for the output AppImage' + required: false + default: 'OpenSSH-GUI' + appimagetool-tag: + description: 'Release tag of appimagetool to download' + required: false + default: 'continuous' + +outputs: + appimage-name: + description: 'Filename of the produced AppImage' + value: ${{ steps.build.outputs.appimage-name }} + +runs: + using: composite + steps: + - name: Install AppImage build dependencies + shell: bash + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends librsvg2-bin appstream + + - name: Download appimagetool + shell: bash + run: | + set -euo pipefail + TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/${{ inputs.appimagetool-tag }}/appimagetool-x86_64.AppImage" + echo "::notice::Downloading appimagetool from $TOOL_URL" + wget --progress=dot:giga "$TOOL_URL" -O appimagetool + chmod +x appimagetool + + - name: Assemble and build AppImage + id: build + shell: bash + run: | + set -euo pipefail + + BINARY_PATH="${{ inputs.binary-path }}" + VERSION="${{ inputs.version }}" + IS_NIGHTLY="${{ inputs.is-nightly }}" + ASSET_PREFIX="${{ inputs.asset-prefix }}" + APP_ID="io.github.frequency403.openssh_gui" + + # Validate all required source files exist before doing any work + for REQUIRED in \ + "$BINARY_PATH" \ + "images/openssh-gui.svg" \ + "appimage/${APP_ID}.metainfo.xml" \ + "appimage/AppRun" + do + if [[ ! -f "$REQUIRED" ]]; then + echo "::error::Required file not found: $REQUIRED" + exit 1 + fi + done + + # Prepare AppDir layout + mkdir -p "AppDir/usr/bin" + mkdir -p "AppDir/usr/share/icons/hicolor/256x256/apps" + mkdir -p "AppDir/usr/share/icons/hicolor/scalable/apps" + mkdir -p "AppDir/usr/share/applications" + mkdir -p "AppDir/usr/share/metainfo" + + cp "$BINARY_PATH" AppDir/usr/bin/openssh-gui + chmod +x AppDir/usr/bin/openssh-gui + + # Icons + rsvg-convert -w 256 -h 256 images/openssh-gui.svg \ + -o AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png + cp images/openssh-gui.svg \ + AppDir/usr/share/icons/hicolor/scalable/apps/openssh-gui.svg + cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/openssh-gui.png + cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/appicon.png + + # Metainfo: patch placeholder version and date + METAINFO_DEST="AppDir/usr/share/metainfo/${APP_ID}.metainfo.xml" + cp "appimage/${APP_ID}.metainfo.xml" "$METAINFO_DEST" + + APPSTREAM_VERSION="$VERSION" + if [[ "$IS_NIGHTLY" == "true" ]]; then + APPSTREAM_VERSION="${APPSTREAM_VERSION//+/~}" + fi + sed -i \ + "s|||" \ + "$METAINFO_DEST" + + # Generate .desktop file from metainfo + appstreamcli make-desktop-file \ + "$METAINFO_DEST" \ + "AppDir/usr/share/applications/${APP_ID}.desktop" + cp "AppDir/usr/share/applications/${APP_ID}.desktop" \ + "AppDir/${APP_ID}.desktop" + + cp appimage/AppRun AppDir/AppRun + chmod +x AppDir/AppRun + + # Assemble AppImage (--appimage-extract-and-run bypasses FUSE requirement) + APPIMAGE_NAME="${ASSET_PREFIX}-x86_64.AppImage" + ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "$APPIMAGE_NAME" + + if [[ ! -f "$APPIMAGE_NAME" ]]; then + echo "::error::appimagetool did not produce $APPIMAGE_NAME" + exit 1 + fi + + echo "appimage-name=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT" + echo "::notice::AppImage built successfully: $APPIMAGE_NAME" \ No newline at end of file diff --git a/.github/actions/determine-version/action.yml b/.github/actions/determine-version/action.yml new file mode 100644 index 0000000..573f13a --- /dev/null +++ b/.github/actions/determine-version/action.yml @@ -0,0 +1,43 @@ +name: Determine Version +description: Extract and validate a semantic version from Directory.Build.props and optionally validate against a branch or tag name + +outputs: + version: + description: Extracted semantic version without leading v + value: ${{ steps.extract.outputs.version }} + tag: + description: Tag name with leading v + value: ${{ steps.extract.outputs.tag }} + +runs: + using: composite + + steps: + - name: Extract BaseVersion + id: extract + shell: bash + run: | + set -euo pipefail + + FILE="Directory.Build.props" + + if [[ ! -f "$FILE" ]]; then + echo "::error::$FILE not found" + exit 1 + fi + + VERSION="$(dotnet msbuild "$FILE" -nologo -getProperty:BaseVersion | tr -d '\r')" + + if [[ -z "$VERSION" ]]; then + echo "::error::BaseVersion not found in $FILE" + exit 1 + fi + + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid BaseVersion: $VERSION" + exit 1 + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "::notice::Detected version: $VERSION (tag: v$VERSION)" \ No newline at end of file diff --git a/.github/actions/dotnet-publish/action.yml b/.github/actions/dotnet-publish/action.yml new file mode 100644 index 0000000..5ee1116 --- /dev/null +++ b/.github/actions/dotnet-publish/action.yml @@ -0,0 +1,40 @@ +name: 'Publish .NET Application' +description: > + Runs dotnet publish for a single-file, ReadyToRun, self-contained Release build. + Assumes the .NET SDK is already set up in the environment. + +inputs: + project-path: + description: 'Relative path to the .csproj file to publish' + required: true + runtime: + description: 'Target RID (e.g. win-x64, linux-x64, osx-x64)' + required: true + version: + description: 'Version string passed to MSBuild /p:Version (no v-prefix)' + required: true + output-dir: + description: 'Output directory relative to the workspace root' + required: false + default: './publish' + extra-msbuild-args: + description: 'Additional MSBuild property flags, e.g. "-p:IsNightly=true"' + required: false + default: '' + +runs: + using: composite + steps: + - name: Publish application + shell: bash + run: | + set -euo pipefail + dotnet publish "${{ inputs.project-path }}" \ + --configuration Release \ + --runtime "${{ inputs.runtime }}" \ + --output "${{ inputs.output-dir }}" \ + -p:PublishSingleFile=true \ + -p:PublishReadyToRun=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:Version="${{ inputs.version }}" \ + ${{ inputs.extra-msbuild-args }} \ No newline at end of file diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml new file mode 100644 index 0000000..ac20840 --- /dev/null +++ b/.github/actions/dotnet-setup/action.yml @@ -0,0 +1,59 @@ +name: 'Setup .NET Environment' +description: > + Checks out the repository, auto-detects the TargetFramework from + Directory.Build.props, sets up the .NET SDK, and restores the NuGet cache. + +inputs: + fetch-depth: + description: 'Number of commits to fetch (0 = full history, 1 = shallow)' + required: false + default: '1' + token: + description: 'GitHub token used for checkout' + required: false + default: ${{ github.token }} + +outputs: + dotnet-version: + description: 'Resolved .NET version string, e.g. "10.0.x"' + value: ${{ steps.detect-tfm.outputs.version }} + +runs: + using: composite + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: ${{ inputs.fetch-depth }} + token: ${{ inputs.token }} + + - name: Detect TargetFramework from Directory.Build.props + id: detect-tfm + shell: bash + run: | + set -euo pipefail + PROPS_FILE="Directory.Build.props" + if [[ ! -f "$PROPS_FILE" ]]; then + echo "::error::$PROPS_FILE not found in workspace root" + exit 1 + fi + TFM=$(grep -oPm1 '(?<=net)[0-9.]+' "$PROPS_FILE" || true) + if [[ -z "$TFM" ]]; then + echo "::error::Could not extract from $PROPS_FILE" + exit 1 + fi + echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" + echo "::notice::Resolved .NET version: ${TFM}.x" + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ steps.detect-tfm.outputs.version }} + + - name: Restore NuGet cache + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} + restore-keys: | + ${{ runner.os }}-nuget- \ No newline at end of file diff --git a/.github/actions/install-komac/action.yml b/.github/actions/install-komac/action.yml new file mode 100644 index 0000000..84cd3a0 --- /dev/null +++ b/.github/actions/install-komac/action.yml @@ -0,0 +1,37 @@ +name: 'Install Komac' +description: > + Resolves and downloads the latest Komac (linux-amd64) release from GitHub + and makes it executable in the current working directory. + +outputs: + komac-path: + description: 'Absolute path to the komac binary' + value: ${{ steps.install.outputs.komac-path }} + +runs: + using: composite + steps: + - name: Resolve and download Komac + id: install + shell: bash + run: | + set -euo pipefail + + KOMAC_URL=$(curl -fsSL \ + https://api.github.com/repos/russellbanks/Komac/releases/latest \ + | grep -o '"browser_download_url": "[^"]*linux-amd64[^"]*"' \ + | grep -v '\.sha' \ + | head -1 \ + | cut -d'"' -f4) + + if [[ -z "$KOMAC_URL" ]]; then + echo "::error::Failed to resolve Komac download URL from GitHub Releases API" + exit 1 + fi + + echo "::notice::Downloading Komac from: $KOMAC_URL" + curl -fsSL "$KOMAC_URL" -o komac + chmod +x komac + ./komac --version + + echo "komac-path=$(pwd)/komac" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index aac7269..2e68ff3 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -10,12 +10,17 @@ on: permissions: contents: write +concurrency: + group: auto-tag-${{ github.ref }} + cancel-in-progress: false + jobs: create-tag: name: Create and Push Tag # Only run when a release/* branch was actually merged (not just closed) if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Checkout repository @@ -24,25 +29,17 @@ jobs: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} - - name: Extract version from branch name + - name: Extract and validate version from branch name id: version - run: | - BRANCH="${{ github.event.pull_request.head.ref }}" - # Strip 'release/' prefix - VERSION="${BRANCH#release/}" - # Strip optional 'v' prefix - VERSION="${VERSION#v}" - - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "TAG=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "::notice ::Detected version: $VERSION (tag: v${VERSION})" + uses: ./.github/actions/determine-version - name: Check if tag already exists id: check run: | + set -euo pipefail if git rev-parse "refs/tags/${{ steps.version.outputs.TAG }}" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "::warning ::Tag ${{ steps.version.outputs.TAG }} already exists, skipping." + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "::warning::Tag ${{ steps.version.outputs.TAG }} already exists, skipping." else echo "exists=false" >> "$GITHUB_OUTPUT" fi @@ -50,8 +47,10 @@ jobs: - name: Create and push tag if: steps.check.outputs.exists == 'false' run: | - git config user.name "github-actions[bot]" + set -euo pipefail + git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.version.outputs.TAG }}" -m "Release ${{ steps.version.outputs.VERSION }}" + git tag -a "${{ steps.version.outputs.TAG }}" \ + -m "Release ${{ steps.version.outputs.VERSION }}" git push origin "${{ steps.version.outputs.TAG }}" - echo "::notice ::Tag ${{ steps.version.outputs.TAG }} pushed successfully." \ No newline at end of file + echo "::notice::Tag ${{ steps.version.outputs.TAG }} pushed successfully." \ No newline at end of file diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index 28f9309..b88b82a 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -13,13 +13,15 @@ on: asset_name_prefix: required: false type: string - default: "OpenSSH-GUI" + default: 'OpenSSH-GUI' jobs: build: name: Build for ${{ matrix.target }} runs-on: ubuntu-latest + timeout-minutes: 30 strategy: + fail-fast: false # One failing target must not kill the others matrix: include: - target: linux-x64 @@ -30,31 +32,14 @@ jobs: asset_extension: '' steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Setup .NET environment + uses: ./.github/actions/dotnet-setup - - name: Determine .NET version - id: dotnet-version - run: | - TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) - echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ steps.dotnet-version.outputs.version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-dotnet- - - - name: Normalize version and build args - id: build-meta + - name: Normalize version and build flags + id: meta + shell: bash run: | + set -euo pipefail VERSION="${{ inputs.version }}" VERSION="${VERSION#v}" echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" @@ -66,95 +51,65 @@ jobs: echo "EXTRA_ARGS=$EXTRA_ARGS" >> "$GITHUB_OUTPUT" - name: Publish application - run: | - dotnet publish OpenSSH_GUI/OpenSSH_GUI.csproj \ - --configuration Release \ - --runtime ${{ matrix.target }} \ - --output "./publish" \ - -p:PublishSingleFile=true \ - -p:PublishReadyToRun=true \ - -p:IncludeNativeLibrariesForSelfExtract=true \ - -p:Version="${{ steps.build-meta.outputs.VERSION }}" \ - ${{ steps.build-meta.outputs.EXTRA_ARGS }} + uses: ./.github/actions/dotnet-publish + with: + project-path: OpenSSH_GUI/OpenSSH_GUI.csproj + runtime: ${{ matrix.target }} + version: ${{ steps.meta.outputs.VERSION }} + output-dir: ./publish + extra-msbuild-args: ${{ steps.meta.outputs.EXTRA_ARGS }} - - name: Rename artifact + - name: Rename artifact for distribution id: rename + shell: bash run: | + set -euo pipefail ASSET_NAME="${{ inputs.asset_name_prefix }}-${{ matrix.target }}${{ matrix.asset_extension }}" - mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} "./publish/$ASSET_NAME" + SOURCE="./publish/OpenSSH_GUI${{ matrix.asset_extension }}" + if [[ ! -f "$SOURCE" ]]; then + echo "::error::Expected build output not found: $SOURCE" + exit 1 + fi + mv "$SOURCE" "./publish/$ASSET_NAME" echo "ASSET_NAME=$ASSET_NAME" >> "$GITHUB_OUTPUT" - # --- AppImage (Linux only) --- - name: Build AppImage if: matrix.target == 'linux-x64' id: appimage - run: | - sudo apt-get update && sudo apt-get install -y librsvg2-bin - - wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool - chmod +x appimagetool - - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/metainfo - - cp "./publish/${{ steps.rename.outputs.ASSET_NAME }}" AppDir/usr/bin/openssh-gui - chmod +x AppDir/usr/bin/openssh-gui - - rsvg-convert -w 256 -h 256 images/openssh-gui.svg -o AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png - cp images/openssh-gui.svg AppDir/usr/share/icons/hicolor/scalable/apps/openssh-gui.svg - cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/openssh-gui.png - cp AppDir/usr/share/icons/hicolor/256x256/apps/openssh-gui.png AppDir/appicon.png - - cp appimage/io.github.frequency403.openssh_gui.metainfo.xml AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - APPSTREAM_VERSION="${{ steps.build-meta.outputs.VERSION }}" - if [[ "${{ inputs.is_nightly }}" == "true" ]]; then - APPSTREAM_VERSION="${APPSTREAM_VERSION//+/~}" - fi - - sed -i "s|||" \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml - - appstreamcli make-desktop-file \ - AppDir/usr/share/metainfo/io.github.frequency403.openssh_gui.metainfo.xml \ - AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop - - cp AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop \ - AppDir/io.github.frequency403.openssh_gui.desktop - - cp appimage/AppRun AppDir/AppRun - chmod +x AppDir/AppRun - - APPIMAGE_NAME="${{ inputs.asset_name_prefix }}-x86_64.AppImage" - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "$APPIMAGE_NAME" - echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> "$GITHUB_OUTPUT" + uses: ./.github/actions/build-appimage + with: + binary-path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} + version: ${{ steps.meta.outputs.VERSION }} + is-nightly: ${{ inputs.is_nightly }} + asset-prefix: ${{ inputs.asset_name_prefix }} - - name: Upload AppImage artifact + - name: Upload AppImage if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 with: - name: ${{ steps.appimage.outputs.APPIMAGE_NAME }} - path: ${{ steps.appimage.outputs.APPIMAGE_NAME }} + name: ${{ steps.appimage.outputs.appimage-name }} + path: ${{ steps.appimage.outputs.appimage-name }} + if-no-files-found: error - - name: Upload generated desktop file + - name: Upload .desktop file if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 with: name: io.github.frequency403.openssh_gui.desktop path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop + if-no-files-found: error - - name: Upload appicon artifact + - name: Upload app icon if: matrix.target == 'linux-x64' uses: actions/upload-artifact@v4 with: name: appicon path: AppDir/appicon.png + if-no-files-found: error - - name: Upload build artifact + - name: Upload platform binary uses: actions/upload-artifact@v4 with: name: ${{ steps.rename.outputs.ASSET_NAME }} - path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} \ No newline at end of file + path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0982762..f049289 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,26 +3,43 @@ on: push: tags: - - 'v[0-9]*.[0-9]*.[0-9]*' # Trigger only on version tags like v1.2.3 + - 'v[0-9]*.[0-9]*.[0-9]*' permissions: contents: write +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false # Never cancel an in-flight release + jobs: - # --- JOB 1: BUILD --- + prepare: + name: Prepare Metadata + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Determine Version + id: version + uses: ./.github/actions/determine-version + build: + needs: prepare uses: ./.github/workflows/build-and-package.yml with: - version: ${{ github.ref_name }} + version: ${{ needs.prepare.outputs.version }} is_nightly: false asset_name_prefix: OpenSSH-GUI - # --- JOB 2: RELEASE --- release: name: Create GitHub Release runs-on: ubuntu-latest - needs: build - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + timeout-minutes: 15 + needs: [ prepare, build ] steps: - name: Checkout repository @@ -33,70 +50,104 @@ jobs: with: path: artifacts/ - - name: Flatten and Prepare Assets + - name: Prepare and validate release assets run: | + set -euo pipefail rm -rf release-assets mkdir -p release-assets - find artifacts -type f -exec cp {} release-assets/ \; + # Copy all artifacts, hard-fail on name collision + find artifacts -type f | while read -r FILE; do + DEST="release-assets/$(basename "$FILE")" + if [[ -f "$DEST" ]]; then + echo "::error::Artifact name collision detected: $(basename "$FILE")" + exit 1 + fi + cp "$FILE" "$DEST" + done cp LICENSE release-assets/LICENSE - echo "Release Assets:" - ls -la release-assets - - - name: Create Release and Upload Assets + # Assert all expected files are present before creating the release + for EXPECTED in \ + "OpenSSH-GUI-linux-x64" \ + "OpenSSH-GUI-win-x64.exe" \ + "OpenSSH-GUI-osx-x64" \ + "OpenSSH-GUI-x86_64.AppImage" + do + if [[ ! -f "release-assets/$EXPECTED" ]]; then + echo "::error::Expected release asset missing: $EXPECTED" + exit 1 + fi + done + + echo "Release assets:" + ls -lah release-assets/ + + - name: Publish GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.ref_name }} - files: "release-assets/*" + tag_name: ${{ needs.prepare.outputs.tag }} + files: release-assets/* generate_release_notes: true deploy-aur: name: Update AUR Packages runs-on: ubuntu-latest - needs: release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + timeout-minutes: 15 + needs: [ prepare, release ] + steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Artifacts + - name: Download build artifacts uses: actions/download-artifact@v4 with: path: artifacts/ - - name: Prepare for PKGBUILD update + - name: Prepare AUR assets and update PKGBUILDs run: | + set -euo pipefail rm -rf aur-assets mkdir -p aur-assets - find artifacts -type f -exec cp {} aur-assets/ \; - - echo "AUR asset files:" - ls -la aur-assets - - test -f aur-assets/OpenSSH-GUI-linux-x64 - test -f aur-assets/appicon.png - test -f aur-assets/io.github.frequency403.openssh_gui.desktop - test -f LICENSE - - VERSION=${GITHUB_REF_NAME#v} - SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) - SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) + find artifacts -type f | while read -r FILE; do + DEST="aur-assets/$(basename "$FILE")" + if [[ -f "$DEST" ]]; then + echo "::error::AUR artifact name collision: $(basename "$FILE")" + exit 1 + fi + cp "$FILE" "$DEST" + done + + # Assert all required AUR files are present + test -f aur-assets/OpenSSH-GUI-linux-x64 \ + || { echo "::error::Missing linux binary"; exit 1; } + test -f aur-assets/appicon.png \ + || { echo "::error::Missing appicon.png"; exit 1; } + test -f "aur-assets/io.github.frequency403.openssh_gui.desktop" \ + || { echo "::error::Missing .desktop file"; exit 1; } + test -f LICENSE \ + || { echo "::error::Missing LICENSE"; exit 1; } + + VERSION="${{ needs.prepare.outputs.version }}" + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) + SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) + SHA_DESKTOP=$(sha256sum "aur-assets/io.github.frequency403.openssh_gui.desktop" | cut -d' ' -f1) + SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ + openssh-gui-bin/PKGBUILD sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD - echo "Updated openssh-gui-bin PKGBUILD:" + echo "Updated PKGBUILDs:" grep -E '^(pkgver=|sha256sums=)' openssh-gui-bin/PKGBUILD - - name: Update AUR (openssh-gui-bin) + - name: Publish AUR (openssh-gui-bin) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-bin @@ -104,9 +155,9 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ github.ref_name }}" + commit_message: "Update to ${{ needs.prepare.outputs.tag }}" - - name: Update AUR (openssh-gui-git) + - name: Publish AUR (openssh-gui-git) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-git @@ -114,38 +165,24 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ github.ref_name }}" - + commit_message: "Update to ${{ needs.prepare.outputs.tag }}" + winget: name: Update Winget Package runs-on: ubuntu-latest - needs: release - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - - steps: - - name: Extract version from tag - id: version - run: | - VERSION="${GITHUB_REF_NAME#v}" - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + timeout-minutes: 10 + needs: [ prepare, release ] + steps: - name: Install Komac - run: | - KOMAC_URL=$(curl -fsSL https://api.github.com/repos/russellbanks/Komac/releases/latest \ - | grep -o '"browser_download_url": "[^"]*linux-amd64[^"]*"' \ - | grep -v '\.sha' \ - | head -1 \ - | cut -d'"' -f4) - - echo "Downloading Komac from: $KOMAC_URL" - curl -fsSL "$KOMAC_URL" -o komac - chmod +x komac - ./komac --version + id: komac + uses: ./.github/actions/install-komac - name: Update Winget manifest run: | - ./komac update "frequency403.OpenSSHGUI" \ - --version "${{ steps.version.outputs.VERSION }}" \ - --urls "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/OpenSSH-GUI-win-x64.exe" \ + set -euo pipefail + "${{ steps.komac.outputs.komac-path }}" update "frequency403.OpenSSHGUI" \ + --version "${{ needs.prepare.outputs.version }}" \ + --urls "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.tag }}/OpenSSH-GUI-win-x64.exe" \ --submit \ --token "${{ secrets.WINGET_GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 8a50dbf..3591675 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -8,32 +8,44 @@ on: permissions: contents: write +concurrency: + group: nightly-${{ github.ref }} + cancel-in-progress: true # Supersede previous nightly on rapid pushes + jobs: - # --- JOB 0: PREPARE --- prepare: name: Prepare Metadata runs-on: ubuntu-latest + timeout-minutes: 5 outputs: version: ${{ steps.meta.outputs.version }} base_version: ${{ steps.meta.outputs.base_version }} git_hash: ${{ steps.meta.outputs.git_hash }} build_date: ${{ steps.meta.outputs.build_date }} + steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 - - name: Resolve metadata + + - name: Determine Version + id: version + uses: ./.github/actions/determine-version + + - name: Resolve build metadata id: meta run: | + set -euo pipefail HASH=$(git rev-parse --short HEAD) DATE=$(date +%Y-%m-%d) - BASE_VERSION=$(grep -oPm1 '(?<=)[^<]+' Directory.Build.props) + BASE_VERSION="${{ steps.version.outputs.version }}" + VERSION="${BASE_VERSION}+${HASH}" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT" - echo "git_hash=$HASH" >> "$GITHUB_OUTPUT" - echo "build_date=$DATE" >> "$GITHUB_OUTPUT" + echo "git_hash=$HASH" >> "$GITHUB_OUTPUT" + echo "build_date=$DATE" >> "$GITHUB_OUTPUT" + echo "::notice::Nightly version: $VERSION" - # --- JOB 1: BUILD --- build: needs: prepare uses: ./.github/workflows/build-and-package.yml @@ -45,52 +57,61 @@ jobs: deploy-aur-nightly: name: Update AUR Nightly Package runs-on: ubuntu-latest + timeout-minutes: 15 needs: [ prepare, build ] - + steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Artifacts + - name: Download build artifacts uses: actions/download-artifact@v4 with: path: artifacts/ - - name: Prepare for PKGBUILD update + - name: Prepare and update nightly PKGBUILD run: | set -euo pipefail - rm -rf aur-assets mkdir -p aur-assets - - find artifacts -type f -exec cp {} aur-assets/ \; - - echo "AUR asset files:" - find aur-assets -maxdepth 1 -type f -printf '%f\n' | sort - - test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 - test -f aur-assets/appicon.png - test -f aur-assets/io.github.frequency403.openssh_gui.desktop - test -f LICENSE - + + find artifacts -type f | while read -r FILE; do + DEST="aur-assets/$(basename "$FILE")" + if [[ -f "$DEST" ]]; then + echo "::error::AUR artifact name collision: $(basename "$FILE")" + exit 1 + fi + cp "$FILE" "$DEST" + done + + test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 \ + || { echo "::error::Missing nightly linux binary"; exit 1; } + test -f aur-assets/appicon.png \ + || { echo "::error::Missing appicon.png"; exit 1; } + test -f "aur-assets/io.github.frequency403.openssh_gui.desktop" \ + || { echo "::error::Missing .desktop file"; exit 1; } + test -f LICENSE \ + || { echo "::error::Missing LICENSE"; exit 1; } + DATE=$(date +%Y%m%d) - VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" - echo "AUR_VERSION=$VERSION" >> "$GITHUB_ENV" - - SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum aur-assets/io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1) - SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - - sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-nightly/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD - - echo "Updated openssh-gui-nightly PKGBUILD:" + AUR_VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" + echo "AUR_VERSION=$AUR_VERSION" >> "$GITHUB_ENV" + + SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) + SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) + SHA_DESKTOP=$(sha256sum "aur-assets/io.github.frequency403.openssh_gui.desktop" | cut -d' ' -f1) + SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) + + sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-nightly/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ + openssh-gui-nightly/PKGBUILD + + echo "Updated PKGBUILD:" grep -E '^(pkgver=|sha256sums=)' openssh-gui-nightly/PKGBUILD - - name: Update AUR (openssh-gui-nightly) + - name: Publish AUR (openssh-gui-nightly) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-nightly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d80e4a..64cb92e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,39 +6,25 @@ on: pull_request: branches: [ main, master, development ] +concurrency: + group: tests-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + jobs: test: name: Run Tests runs-on: ubuntu-latest + timeout-minutes: 20 steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Determine .NET version from project - id: dotnet-version - run: | - TFM=$(grep -oPm1 '(?<=net)[0-9.]+' Directory.Build.props) - echo "version=${TFM}.x" >> "$GITHUB_OUTPUT" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ steps.dotnet-version.outputs.version }} - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-dotnet-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} - restore-keys: | - ${{ runner.os }}-dotnet- + - name: Setup .NET environment + uses: ./.github/actions/dotnet-setup - name: Restore dependencies run: dotnet restore OpenSSH_GUI.slnx - - name: Build + - name: Build solution run: dotnet build OpenSSH_GUI.slnx --configuration Release --no-restore - - name: Run Tests - run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal + - name: Run tests + run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index aa0b155..7627ab1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.1.2 + 3.1.3 true diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 8d67b0d..cd5acac 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,5 +1,5 @@ pkgname=openssh-gui-bin -pkgver=3.1.2 +pkgver=3.1.3 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') diff --git a/openssh-gui-git/PKGBUILD b/openssh-gui-git/PKGBUILD index bb37a2d..4455ff1 100644 --- a/openssh-gui-git/PKGBUILD +++ b/openssh-gui-git/PKGBUILD @@ -2,7 +2,7 @@ pkgname=openssh-gui-git _pkgname=OpenSSH-GUI pkgver=2.2.1.r0.g845610b pkgrel=1 -pkgdesc="A GUI for OpenSSH configuration and management (GIT version, built from development branch)" +pkgdesc="A GUI for OpenSSH configuration and management (Sourcepackage)" arch=('x86_64') url="https://github.com/frequency403/OpenSSH-GUI" license=('MIT') diff --git a/update-version.sh b/update-version.sh deleted file mode 100644 index 6dc234c..0000000 --- a/update-version.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PROPS="Directory.Build.props" -VERSION=$(grep -oP '(?<=)[^<]+' "${PROPS}") - -echo "→ Version: ${VERSION}" - -PKGBUILD_BIN="openssh-gui-bin/PKGBUILD" -sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "${PKGBUILD_BIN}" -sed -i "s/^pkgrel=.*/pkgrel=1/" "${PKGBUILD_BIN}" -(cd openssh-gui-bin && updpkgsums) - -echo "✓ Done – ${PKGBUILD_BIN} → ${VERSION}" \ No newline at end of file From e4f25b42ad42f63051f4f742d34f4477642cd3a4 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 09:17:44 +0200 Subject: [PATCH 19/58] Add `actions/checkout@v4` to workflows for consistent repository access. --- .github/workflows/build-and-package.yml | 3 +++ .github/workflows/build.yml | 3 +++ .github/workflows/test.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index b88b82a..b058645 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -32,6 +32,9 @@ jobs: asset_extension: '' steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Setup .NET environment uses: ./.github/actions/dotnet-setup diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f049289..96f6419 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -174,6 +174,9 @@ jobs: needs: [ prepare, release ] steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install Komac id: komac uses: ./.github/actions/install-komac diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64cb92e..202c2c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,9 @@ jobs: timeout-minutes: 20 steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup .NET environment uses: ./.github/actions/dotnet-setup From 11803663c2946f4c4b73c644356dfb3b4e35f5f0 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Wed, 10 Jun 2026 09:26:54 +0200 Subject: [PATCH 20/58] Refactor `install-komac` action: enhance reliability with `gh` CLI integration, improve release asset resolution, and simplify output handling. --- .github/actions/install-komac/action.yml | 62 ++++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/.github/actions/install-komac/action.yml b/.github/actions/install-komac/action.yml index 84cd3a0..ae21475 100644 --- a/.github/actions/install-komac/action.yml +++ b/.github/actions/install-komac/action.yml @@ -1,37 +1,57 @@ -name: 'Install Komac' -description: > - Resolves and downloads the latest Komac (linux-amd64) release from GitHub - and makes it executable in the current working directory. +name: install-komac +description: Downloads Komac in a reliable way outputs: komac-path: - description: 'Absolute path to the komac binary' - value: ${{ steps.install.outputs.komac-path }} + description: Path to komac binary + value: ${{ steps.install.outputs.path }} runs: - using: composite + using: "composite" steps: - - name: Resolve and download Komac + - name: Install GitHub CLI (if missing) + shell: bash + run: | + if ! command -v gh >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y gh + fi + + - name: Authenticate gh + shell: bash + run: | + echo "${{ github.token }}" | gh auth login --with-token + + - name: Download Komac release id: install shell: bash run: | set -euo pipefail - KOMAC_URL=$(curl -fsSL \ - https://api.github.com/repos/russellbanks/Komac/releases/latest \ - | grep -o '"browser_download_url": "[^"]*linux-amd64[^"]*"' \ - | grep -v '\.sha' \ - | head -1 \ - | cut -d'"' -f4) + TMP_DIR="$(mktemp -d)" + echo "Using temp dir: $TMP_DIR" + + # deterministisch: latest release holen + TAG=$(gh release view --repo russellbanks/Komac --json tagName -q .tagName) + + echo "Latest Komac tag: $TAG" - if [[ -z "$KOMAC_URL" ]]; then - echo "::error::Failed to resolve Komac download URL from GitHub Releases API" + # gezielt Asset ziehen (kein grep, kein JSON parsing) + gh release download "$TAG" \ + --repo russellbanks/Komac \ + --pattern "*linux*amd64*" \ + --dir "$TMP_DIR" + + FILE=$(find "$TMP_DIR" -type f -name "*komac*" | head -n 1 || true) + + if [[ -z "${FILE:-}" ]]; then + echo "::error::Komac binary not found after download" exit 1 fi - echo "::notice::Downloading Komac from: $KOMAC_URL" - curl -fsSL "$KOMAC_URL" -o komac - chmod +x komac - ./komac --version + chmod +x "$FILE" + + echo "Komac version:" + "$FILE" --version || true - echo "komac-path=$(pwd)/komac" >> "$GITHUB_OUTPUT" \ No newline at end of file + echo "path=$FILE" >> "$GITHUB_OUTPUT" \ No newline at end of file From 07e469a46328ec295f7b06a1823eef1a61bd7fa2 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 09:28:56 +0200 Subject: [PATCH 21/58] Remove `install-komac` action, update GitHub Actions to latest versions, and refactor workflows for consistency and efficiency. --- .github/actions/dotnet-setup/action.yml | 7 +-- .github/actions/install-komac/action.yml | 57 ------------------------ .github/workflows/auto-tag.yml | 16 +++---- .github/workflows/build-and-package.yml | 10 ++--- .github/workflows/build.yml | 51 +++++++++++---------- .github/workflows/staging.yml | 25 ++++++++--- .github/workflows/test.yml | 2 +- 7 files changed, 64 insertions(+), 104 deletions(-) delete mode 100644 .github/actions/install-komac/action.yml diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml index ac20840..67429df 100644 --- a/.github/actions/dotnet-setup/action.yml +++ b/.github/actions/dotnet-setup/action.yml @@ -22,7 +22,7 @@ runs: using: composite steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: ${{ inputs.fetch-depth }} token: ${{ inputs.token }} @@ -46,12 +46,13 @@ runs: echo "::notice::Resolved .NET version: ${TFM}.x" - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 + needs: detect-tfm with: dotnet-version: ${{ steps.detect-tfm.outputs.version }} - name: Restore NuGet cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} diff --git a/.github/actions/install-komac/action.yml b/.github/actions/install-komac/action.yml deleted file mode 100644 index ae21475..0000000 --- a/.github/actions/install-komac/action.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: install-komac -description: Downloads Komac in a reliable way - -outputs: - komac-path: - description: Path to komac binary - value: ${{ steps.install.outputs.path }} - -runs: - using: "composite" - steps: - - name: Install GitHub CLI (if missing) - shell: bash - run: | - if ! command -v gh >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y gh - fi - - - name: Authenticate gh - shell: bash - run: | - echo "${{ github.token }}" | gh auth login --with-token - - - name: Download Komac release - id: install - shell: bash - run: | - set -euo pipefail - - TMP_DIR="$(mktemp -d)" - echo "Using temp dir: $TMP_DIR" - - # deterministisch: latest release holen - TAG=$(gh release view --repo russellbanks/Komac --json tagName -q .tagName) - - echo "Latest Komac tag: $TAG" - - # gezielt Asset ziehen (kein grep, kein JSON parsing) - gh release download "$TAG" \ - --repo russellbanks/Komac \ - --pattern "*linux*amd64*" \ - --dir "$TMP_DIR" - - FILE=$(find "$TMP_DIR" -type f -name "*komac*" | head -n 1 || true) - - if [[ -z "${FILE:-}" ]]; then - echo "::error::Komac binary not found after download" - exit 1 - fi - - chmod +x "$FILE" - - echo "Komac version:" - "$FILE" --version || true - - echo "path=$FILE" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 2e68ff3..9ec425a 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -24,12 +24,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} - - name: Extract and validate version from branch name + - name: Extract and validate version id: version uses: ./.github/actions/determine-version @@ -37,9 +37,9 @@ jobs: id: check run: | set -euo pipefail - if git rev-parse "refs/tags/${{ steps.version.outputs.TAG }}" >/dev/null 2>&1; then + if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then echo "exists=true" >> "$GITHUB_OUTPUT" - echo "::warning::Tag ${{ steps.version.outputs.TAG }} already exists, skipping." + echo "::warning::Tag ${{ steps.version.outputs.tag }} already exists, skipping." else echo "exists=false" >> "$GITHUB_OUTPUT" fi @@ -50,7 +50,7 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.version.outputs.TAG }}" \ - -m "Release ${{ steps.version.outputs.VERSION }}" - git push origin "${{ steps.version.outputs.TAG }}" - echo "::notice::Tag ${{ steps.version.outputs.TAG }} pushed successfully." \ No newline at end of file + git tag -a "${{ steps.version.outputs.tag }}" \ + -m "Release ${{ steps.version.outputs.version }}" + git push origin "${{ steps.version.outputs.tag }}" + echo "::notice::Tag ${{ steps.version.outputs.tag }} pushed successfully." diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index b058645..5f0789c 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup .NET environment uses: ./.github/actions/dotnet-setup @@ -88,7 +88,7 @@ jobs: - name: Upload AppImage if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.appimage.outputs.appimage-name }} path: ${{ steps.appimage.outputs.appimage-name }} @@ -96,7 +96,7 @@ jobs: - name: Upload .desktop file if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: io.github.frequency403.openssh_gui.desktop path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop @@ -104,14 +104,14 @@ jobs: - name: Upload app icon if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: appicon path: AppDir/appicon.png if-no-files-found: error - name: Upload platform binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.rename.outputs.ASSET_NAME }} path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96f6419..e6b2b8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build and Release +name: Build and Release on: push: @@ -16,12 +16,13 @@ jobs: prepare: name: Prepare Metadata runs-on: ubuntu-latest + timeout-minutes: 5 outputs: version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine Version id: version @@ -43,10 +44,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download all build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts/ @@ -68,12 +69,15 @@ jobs: cp LICENSE release-assets/LICENSE - # Assert all expected files are present before creating the release + # Assert all expected files are present before creating the release. + # appicon.png and .desktop are required by openssh-gui-bin PKGBUILD source URLs. for EXPECTED in \ "OpenSSH-GUI-linux-x64" \ "OpenSSH-GUI-win-x64.exe" \ "OpenSSH-GUI-osx-x64" \ - "OpenSSH-GUI-x86_64.AppImage" + "OpenSSH-GUI-x86_64.AppImage" \ + "appicon.png" \ + "io.github.frequency403.openssh_gui.desktop" do if [[ ! -f "release-assets/$EXPECTED" ]]; then echo "::error::Expected release asset missing: $EXPECTED" @@ -85,7 +89,7 @@ jobs: ls -lah release-assets/ - name: Publish GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare.outputs.tag }} files: release-assets/* @@ -99,12 +103,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - fetch-depth: 0 + fetch-depth: 1 - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts/ @@ -140,12 +144,15 @@ jobs: SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-bin/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ openssh-gui-bin/PKGBUILD + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD echo "Updated PKGBUILDs:" - grep -E '^(pkgver=|sha256sums=)' openssh-gui-bin/PKGBUILD + grep -E '^(pkgver=|pkgrel=|sha256sums=)' openssh-gui-bin/PKGBUILD - name: Publish AUR (openssh-gui-bin) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 @@ -169,23 +176,19 @@ jobs: winget: name: Update Winget Package - runs-on: ubuntu-latest + runs-on: ubuntu-slim timeout-minutes: 10 needs: [ prepare, release ] steps: - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install Komac - id: komac - uses: ./.github/actions/install-komac + uses: actions/checkout@v6 - name: Update Winget manifest - run: | - set -euo pipefail - "${{ steps.komac.outputs.komac-path }}" update "frequency403.OpenSSHGUI" \ - --version "${{ needs.prepare.outputs.version }}" \ - --urls "https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.tag }}/OpenSSH-GUI-win-x64.exe" \ - --submit \ - --token "${{ secrets.WINGET_GITHUB_TOKEN }}" \ No newline at end of file + uses: vedantmgoyal9/winget-releaser@main + with: + identifier: frequency403.OpenSSHGUI + version: ${{ needs.prepare.outputs.version }} + release-tag: ${{ needs.prepare.outputs.tag }} + installers-regex: 'win-x64\.exe$' + token: ${{ secrets.WINGET_GITHUB_TOKEN }} diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 3591675..abde5c8 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine Version id: version @@ -62,12 +62,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - fetch-depth: 0 + fetch-depth: 1 - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts/ @@ -105,11 +105,24 @@ jobs: SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-nightly/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-nightly/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ openssh-gui-nightly/PKGBUILD echo "Updated PKGBUILD:" - grep -E '^(pkgver=|sha256sums=)' openssh-gui-nightly/PKGBUILD + grep -E '^(pkgver=|pkgrel=|sha256sums=)' openssh-gui-nightly/PKGBUILD + + - name: Publish / update nightly GitHub Release + uses: softprops/action-gh-release@v3 + with: + tag_name: nightly + name: "Nightly (${{ needs.prepare.outputs.build_date }})" + prerelease: true + files: | + aur-assets/OpenSSH-GUI-nightly-linux-x64 + aur-assets/appicon.png + aur-assets/io.github.frequency403.openssh_gui.desktop + LICENSE - name: Publish AUR (openssh-gui-nightly) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 @@ -119,4 +132,4 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Nightly update ${{ env.AUR_VERSION }}" \ No newline at end of file + commit_message: "Nightly update ${{ env.AUR_VERSION }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 202c2c6..db76ac9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup .NET environment uses: ./.github/actions/dotnet-setup From 56c683ebc4083fb44cd06e42efdbbcea86a5589f Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 09:31:10 +0200 Subject: [PATCH 22/58] Remove unused `needs: detect-tfm` property from `.github/actions/dotnet-setup/action.yml`. --- .github/actions/dotnet-setup/action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml index 67429df..195f138 100644 --- a/.github/actions/dotnet-setup/action.yml +++ b/.github/actions/dotnet-setup/action.yml @@ -47,7 +47,6 @@ runs: - name: Setup .NET SDK uses: actions/setup-dotnet@v5 - needs: detect-tfm with: dotnet-version: ${{ steps.detect-tfm.outputs.version }} From 237e2737cc80485cc49c87eaf8ae53479692d9b7 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 12:53:41 +0200 Subject: [PATCH 23/58] Remove nightly GitHub Release action from staging workflow --- .github/workflows/staging.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index abde5c8..554bbf7 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -112,18 +112,6 @@ jobs: echo "Updated PKGBUILD:" grep -E '^(pkgver=|pkgrel=|sha256sums=)' openssh-gui-nightly/PKGBUILD - - name: Publish / update nightly GitHub Release - uses: softprops/action-gh-release@v3 - with: - tag_name: nightly - name: "Nightly (${{ needs.prepare.outputs.build_date }})" - prerelease: true - files: | - aur-assets/OpenSSH-GUI-nightly-linux-x64 - aur-assets/appicon.png - aur-assets/io.github.frequency403.openssh_gui.desktop - LICENSE - - name: Publish AUR (openssh-gui-nightly) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: From c38234bb65eddef35f4f24f90ac68307fc83af17 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 12:58:27 +0200 Subject: [PATCH 24/58] resolve warning IL3000 --- OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs index 6463ea4..c315d9a 100644 --- a/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs +++ b/OpenSSH_GUI.Core/Lib/Keys/SshKeyFileInformation.cs @@ -24,7 +24,7 @@ public SshKeyFileInformation(SshKeyFileSource keyFileSource) FileInfo = !string.IsNullOrWhiteSpace(keyFileSource.AbsolutePath) ? new FileInfo(keyFileSource.AbsolutePath) - : new FileInfo(Assembly.GetExecutingAssembly().Location); + : new FileInfo(AppContext.BaseDirectory); FileName = FileInfo.Name; FullFileName = FileInfo.FullName; From f0c32b2ab8c7cf39728fb8ded7fa34f37a6b30af Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 13:18:25 +0200 Subject: [PATCH 25/58] Remove nightly support and update workflows to reflect changes --- .github/actions/dotnet-setup/action.yml | 6 +- .github/workflows/auto-tag.yml | 2 +- .github/workflows/build-and-package.yml | 10 +-- .github/workflows/build.yml | 27 +++--- .github/workflows/staging.yml | 104 ++++++------------------ .github/workflows/test.yml | 15 +++- openssh-gui-nightly/PKGBUILD | 26 ------ 7 files changed, 60 insertions(+), 130 deletions(-) delete mode 100644 openssh-gui-nightly/PKGBUILD diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml index 195f138..ac20840 100644 --- a/.github/actions/dotnet-setup/action.yml +++ b/.github/actions/dotnet-setup/action.yml @@ -22,7 +22,7 @@ runs: using: composite steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: ${{ inputs.fetch-depth }} token: ${{ inputs.token }} @@ -46,12 +46,12 @@ runs: echo "::notice::Resolved .NET version: ${TFM}.x" - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ steps.detect-tfm.outputs.version }} - name: Restore NuGet cache - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 9ec425a..faf4d15 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index 5f0789c..b058645 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Setup .NET environment uses: ./.github/actions/dotnet-setup @@ -88,7 +88,7 @@ jobs: - name: Upload AppImage if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: ${{ steps.appimage.outputs.appimage-name }} path: ${{ steps.appimage.outputs.appimage-name }} @@ -96,7 +96,7 @@ jobs: - name: Upload .desktop file if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: io.github.frequency403.openssh_gui.desktop path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop @@ -104,14 +104,14 @@ jobs: - name: Upload app icon if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: appicon path: AppDir/appicon.png if-no-files-found: error - name: Upload platform binary - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: ${{ steps.rename.outputs.ASSET_NAME }} path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6b2b8b..d4f2800 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,23 +13,28 @@ concurrency: cancel-in-progress: false # Never cancel an in-flight release jobs: + test: + name: Run Tests + uses: ./.github/workflows/test.yml + prepare: name: Prepare Metadata runs-on: ubuntu-latest timeout-minutes: 5 + needs: test outputs: version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Determine Version id: version uses: ./.github/actions/determine-version build: - needs: prepare + needs: [ test, prepare ] uses: ./.github/workflows/build-and-package.yml with: version: ${{ needs.prepare.outputs.version }} @@ -40,14 +45,14 @@ jobs: name: Create GitHub Release runs-on: ubuntu-latest timeout-minutes: 15 - needs: [ prepare, build ] + needs: [ test, prepare, build ] steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Download all build artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 with: path: artifacts/ @@ -89,7 +94,7 @@ jobs: ls -lah release-assets/ - name: Publish GitHub Release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.prepare.outputs.tag }} files: release-assets/* @@ -99,16 +104,16 @@ jobs: name: Update AUR Packages runs-on: ubuntu-latest timeout-minutes: 15 - needs: [ prepare, release ] + needs: [ test, prepare, release ] steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Download build artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 with: path: artifacts/ @@ -178,11 +183,11 @@ jobs: name: Update Winget Package runs-on: ubuntu-slim timeout-minutes: 10 - needs: [ prepare, release ] + needs: [ test, prepare, release ] steps: - name: Checkout Repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Update Winget manifest uses: vedantmgoyal9/winget-releaser@main diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 554bbf7..668b488 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -1,4 +1,4 @@ -name: Staging / Nightly Build +name: Staging / Development Build on: push: @@ -9,115 +9,59 @@ permissions: contents: write concurrency: - group: nightly-${{ github.ref }} - cancel-in-progress: true # Supersede previous nightly on rapid pushes + group: staging-${{ github.ref }} + cancel-in-progress: true # Supersede previous run on rapid pushes jobs: + test: + name: Run Tests + uses: ./.github/workflows/test.yml + prepare: name: Prepare Metadata runs-on: ubuntu-latest timeout-minutes: 5 + needs: test outputs: - version: ${{ steps.meta.outputs.version }} - base_version: ${{ steps.meta.outputs.base_version }} - git_hash: ${{ steps.meta.outputs.git_hash }} - build_date: ${{ steps.meta.outputs.build_date }} + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Determine Version id: version uses: ./.github/actions/determine-version - - name: Resolve build metadata - id: meta - run: | - set -euo pipefail - HASH=$(git rev-parse --short HEAD) - DATE=$(date +%Y-%m-%d) - BASE_VERSION="${{ steps.version.outputs.version }}" - - VERSION="${BASE_VERSION}+${HASH}" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT" - echo "git_hash=$HASH" >> "$GITHUB_OUTPUT" - echo "build_date=$DATE" >> "$GITHUB_OUTPUT" - echo "::notice::Nightly version: $VERSION" - - build: - needs: prepare - uses: ./.github/workflows/build-and-package.yml - with: - version: ${{ needs.prepare.outputs.version }} - is_nightly: true - asset_name_prefix: OpenSSH-GUI-nightly - - deploy-aur-nightly: - name: Update AUR Nightly Package + deploy-aur-git: + name: Update AUR Git Package runs-on: ubuntu-latest timeout-minutes: 15 - needs: [ prepare, build ] + needs: [ test, prepare ] steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 1 - - name: Download build artifacts - uses: actions/download-artifact@v8 - with: - path: artifacts/ - - - name: Prepare and update nightly PKGBUILD + - name: Update openssh-gui-git PKGBUILD run: | set -euo pipefail - rm -rf aur-assets - mkdir -p aur-assets - - find artifacts -type f | while read -r FILE; do - DEST="aur-assets/$(basename "$FILE")" - if [[ -f "$DEST" ]]; then - echo "::error::AUR artifact name collision: $(basename "$FILE")" - exit 1 - fi - cp "$FILE" "$DEST" - done - - test -f aur-assets/OpenSSH-GUI-nightly-linux-x64 \ - || { echo "::error::Missing nightly linux binary"; exit 1; } - test -f aur-assets/appicon.png \ - || { echo "::error::Missing appicon.png"; exit 1; } - test -f "aur-assets/io.github.frequency403.openssh_gui.desktop" \ - || { echo "::error::Missing .desktop file"; exit 1; } - test -f LICENSE \ - || { echo "::error::Missing LICENSE"; exit 1; } - - DATE=$(date +%Y%m%d) - AUR_VERSION="${{ needs.prepare.outputs.base_version }}.${DATE}.${{ needs.prepare.outputs.git_hash }}" - echo "AUR_VERSION=$AUR_VERSION" >> "$GITHUB_ENV" - - SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-nightly-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum "aur-assets/io.github.frequency403.openssh_gui.desktop" | cut -d' ' -f1) - SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - - sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-nightly/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-nightly/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ - openssh-gui-nightly/PKGBUILD + VERSION="${{ needs.prepare.outputs.version }}" + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD echo "Updated PKGBUILD:" - grep -E '^(pkgver=|pkgrel=|sha256sums=)' openssh-gui-nightly/PKGBUILD + grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD - - name: Publish AUR (openssh-gui-nightly) + - name: Publish AUR (openssh-gui-git) uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: - pkgname: openssh-gui-nightly - pkgbuild: ./openssh-gui-nightly/PKGBUILD + pkgname: openssh-gui-git + pkgbuild: ./openssh-gui-git/PKGBUILD commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Nightly update ${{ env.AUR_VERSION }}" + commit_message: "Development update ${{ needs.prepare.outputs.tag }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db76ac9..f50b7f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Unit Tests on: push: - branches: [ main, master, development ] pull_request: branches: [ main, master, development ] + workflow_call: concurrency: group: tests-${{ github.ref }}-${{ github.event_name }} @@ -15,11 +15,18 @@ jobs: name: Run Tests runs-on: ubuntu-latest timeout-minutes: 20 + # Run on every push except normal (non-forced) pushes to master/main. + # Always run when called from another workflow (workflow_call). + if: > + github.event_name == 'workflow_call' || + github.event_name == 'pull_request' || + github.event.forced == true || + (github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main') steps: - name: Checkout repository - uses: actions/checkout@v6 - + uses: actions/checkout@v4 + - name: Setup .NET environment uses: ./.github/actions/dotnet-setup @@ -30,4 +37,4 @@ jobs: run: dotnet build OpenSSH_GUI.slnx --configuration Release --no-restore - name: Run tests - run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal \ No newline at end of file + run: dotnet test OpenSSH_GUI.slnx --configuration Release --no-build --verbosity normal diff --git a/openssh-gui-nightly/PKGBUILD b/openssh-gui-nightly/PKGBUILD deleted file mode 100644 index 61cb549..0000000 --- a/openssh-gui-nightly/PKGBUILD +++ /dev/null @@ -1,26 +0,0 @@ -pkgname=openssh-gui-nightly -pkgver=3.0.0.19700101.unknown -pkgrel=1 -pkgdesc="A GUI for OpenSSH configuration and management (Nightly build)" -arch=('x86_64') -url="https://github.com/frequency403/OpenSSH-GUI" -license=('MIT') -depends=('icu' 'openssl' 'zlib' 'krb5' 'libx11') -options=('!strip') -provides=('openssh-gui') -conflicts=('openssh-gui' 'openssh-gui-bin' 'openssh-gui-git') - -_relurl="${url}/releases/download/nightly" - -source=("${pkgname}-${pkgver}::${_relurl}/OpenSSH-GUI-nightly-linux-x64" - "${pkgname}-icon-${pkgver}.png::${_relurl}/appicon.png" - "${pkgname}-desktop-${pkgver}.desktop::${_relurl}/io.github.frequency403.openssh_gui.desktop" - "${pkgname}-license-${pkgver}::${_relurl}/LICENSE") -sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP') - -package() { - install -Dm755 "${pkgname}-${pkgver}" "${pkgdir}/usr/bin/openssh-gui" - install -Dm644 "${pkgname}-icon-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" - install -Dm644 "${pkgname}-desktop-${pkgver}.desktop" "${pkgdir}/usr/share/applications/io.github.frequency403.openssh_gui.desktop" - install -Dm644 "${pkgname}-license-${pkgver}" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -} \ No newline at end of file From 0ac2a6f22361a20e3378ffcbb31caf8b99d56411 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 13:24:58 +0200 Subject: [PATCH 26/58] Update PKGBUILD to include date and commit hash in versioning --- .github/workflows/staging.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 668b488..06cd6a8 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -49,9 +49,13 @@ jobs: - name: Update openssh-gui-git PKGBUILD run: | set -euo pipefail - VERSION="${{ needs.prepare.outputs.version }}" - sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD + DATE=$(date +%Y%m%d) + HASH=$(git rev-parse --short HEAD) + # pkgver must only contain [a-zA-Z0-9._] — no + or - + AUR_VERSION="${{ needs.prepare.outputs.version }}.${DATE}.${HASH}" + + sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-git/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD echo "Updated PKGBUILD:" grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD From d131b9b474e2cdcda8ea10ddbcc124fb8b1d4913 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 13:43:53 +0200 Subject: [PATCH 27/58] Sync auto-tag workflow with development process: - Add pull request write permissions. - Increase timeout for tag creation job. - Implement automatic cherry-picking of release commits into development with conflict handling. - Adjust test workflow triggers for better branch handling. --- .github/workflows/auto-tag.yml | 42 +++++++++++++++++++++++++++++++--- .github/workflows/test.yml | 19 +++++++++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index faf4d15..a1aebcf 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -9,6 +9,7 @@ on: permissions: contents: write + pull-requests: write concurrency: group: auto-tag-${{ github.ref }} @@ -16,11 +17,10 @@ concurrency: jobs: create-tag: - name: Create and Push Tag - # Only run when a release/* branch was actually merged (not just closed) + name: Create Tag and Sync Development if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Checkout repository @@ -54,3 +54,39 @@ jobs: -m "Release ${{ steps.version.outputs.version }}" git push origin "${{ steps.version.outputs.tag }}" echo "::notice::Tag ${{ steps.version.outputs.tag }} pushed successfully." + + - name: Cherry-pick release commit into development + if: steps.check.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + run: | + set -euo pipefail + + RELEASE_SHA="${{ github.event.pull_request.head.sha }}" + VERSION="${{ steps.version.outputs.version }}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git fetch origin development + git checkout -B development origin/development + + if git cherry-pick "$RELEASE_SHA"; then + git push origin development + echo "::notice::Cherry-pick of $RELEASE_SHA into development succeeded." + else + git cherry-pick --abort + + SYNC_BRANCH="chore/sync-release-${VERSION}-to-development" + git checkout -B "$SYNC_BRANCH" origin/development + git push origin "$SYNC_BRANCH" + + gh pr create \ + --base development \ + --head "$SYNC_BRANCH" \ + --title "chore: sync release ${VERSION} into development" \ + --body "$(printf 'Automatic cherry-pick of release \`%s\` (commit \`%s\`) into \`development\` failed due to conflicts.\n\nPlease apply and resolve manually:\n\n```\ngit fetch origin\ngit checkout %s\ngit cherry-pick %s\n# resolve conflicts\ngit push origin %s\n```' \ + "$VERSION" "$RELEASE_SHA" "$SYNC_BRANCH" "$RELEASE_SHA" "$SYNC_BRANCH")" + + echo "::warning::Cherry-pick conflict. PR created: $SYNC_BRANCH → development." + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f50b7f4..edc7170 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,8 +2,14 @@ name: Unit Tests on: push: + branches: + - master + - main pull_request: - branches: [ main, master, development ] + branches: + - master + - main + - development workflow_call: concurrency: @@ -15,13 +21,16 @@ jobs: name: Run Tests runs-on: ubuntu-latest timeout-minutes: 20 - # Run on every push except normal (non-forced) pushes to master/main. - # Always run when called from another workflow (workflow_call). + # Run on: + # - workflow_call (gate for staging + build) + # - pull_request targeting development, master, or main + # - force push to master/main + # NOT on normal (non-forced) pushes to master/main — those are handled + # by auto-tag.yml or build.yml which call this via workflow_call. if: > github.event_name == 'workflow_call' || github.event_name == 'pull_request' || - github.event.forced == true || - (github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main') + (github.event_name == 'push' && github.event.forced == true) steps: - name: Checkout repository From 37cc849cea92e469ccd6afafff0d8ab2255adb86 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 13:48:46 +0200 Subject: [PATCH 28/58] Simplify test workflow by removing conditional triggers based on event type --- .github/workflows/test.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edc7170..060c411 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,16 +21,6 @@ jobs: name: Run Tests runs-on: ubuntu-latest timeout-minutes: 20 - # Run on: - # - workflow_call (gate for staging + build) - # - pull_request targeting development, master, or main - # - force push to master/main - # NOT on normal (non-forced) pushes to master/main — those are handled - # by auto-tag.yml or build.yml which call this via workflow_call. - if: > - github.event_name == 'workflow_call' || - github.event_name == 'pull_request' || - (github.event_name == 'push' && github.event.forced == true) steps: - name: Checkout repository From 8afddd690f4c9672f97bef07fc0afebd04d16b5c Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 14:11:56 +0200 Subject: [PATCH 29/58] Update auto-tag workflow: Remove "Sync Development" from job name --- .github/workflows/auto-tag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 8e1f6e9..58731cb 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -17,7 +17,7 @@ concurrency: jobs: create-tag: - name: Create Tag and Sync Development + name: Create Tag if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest timeout-minutes: 10 From 7be13152c76d28c336162a1b7ac6c43358804cdb Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 14:21:24 +0200 Subject: [PATCH 30/58] Release v3.1.6 -Update workflows and versioning logic for improved CI/CD (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix naming error * fix icon name * Fix startup error when files are not readable. (#18) * remove warnings as error in csproj * remove warnings as error in csproj * Fix and Optimize Configuration, SSH Key Handling, and UI (#21) * Fix startup error when files are not readable. * Added Winget Pipeline * Extended FileInfoWindow * Implemented PasswordBox with toggleable password visibility * Update CI * Minor version bump * Added Password in File Info view * Introduced object for BasicSshKeyFileInformation * Reniced SshKeyFileInformation.cs * Fixes some errors in SshKeyFileInformation.cs; Updated file reading logic for ppk files * Removed obsolete comments * Refactoring #1 * fixed reactive chain * 😁 * Optimized Reactive chains * - * Added new icon, changed resolverchain * Refactor icon size handling, improve SSH key collection tracking, and update reactive bindings. * Relocate and optimize asset management: replace SVG icon with ICO format, restructure image paths, and update project references accordingly. * Add light theme assets, theme selection settings, and dynamic icon support. * Refactor theme resources, improve reactive property bindings, update dependencies, and enhance SSH key file handling logic. * Refactor SubmitButtons as reusable control, replace redundant button definitions, and apply dynamic theme resources. * Add SubmitButtons control to EditKnownHostsWindow.axaml * Removed obsolete Interfaces * Minor fixes * Adjust Colors throughout the application * removed logger from SshKeyFilePassword.cs * Changes on FileInfoWindow * Color occurences changed to dynamic resource * Update ReactiveUI * Started to implement password change * Implemented password change ability * Disabled selection of items in SshKeyComboBoxStyle, when they are not initialized * Changed writing and disabled false-positive error in SshKeyItemContainerTheme * Implemented retry abort mechanism * Reformat & Cleanup; Changed to ServerConnection.cs * Changes in ConnectToServerViewModel * Removed redundant interfaces; reformat & cleanup * Implemented headered item * Minor refactoring in server handling * added ideas and refactoring suggestions * Refactoring of SshKeyManager.cs * Cleanup of old Extensions, compute fingerprint by ourselfs - even if key is encrypted * Enabled Logging by SSH.NET * Refactoring progress * Refactoring Progress #2 * SshKeyFileInformation changed to OAPHs * not working sorter * Update package dependencies across all projects to latest stable versions * Remove custom Sorter control, migrate to Avalonia DataGrid for key listing adjustments * Replace ContextMenus with FlyoutButtons, update styling and font sizes, and add Xaml.Behaviors.Avalonia dependency. * Add font size adjustment feature to Application Settings with binding and reset option. * Add dynamic font size management with resource observables, MaterialIcon size adjustments, and NumericUpDown control in Application Settings. * Refactor icon size handling: adjust MaterialIconSize resource, streamline size-related properties, and improve logging logic. * Update DataGrid column properties, add loading spinner, and improve reactive processing state handling. * Update localization bindings for tab headers, improve string resource handling, and streamline German translations. * Refactor SSH key export logic: simplify case handling, improve error messaging, and update dynamic window title formatting. * Refactor reactive properties and commands: streamline syntax, enhance property accessibility, and improve dynamic window title logic. * Refactor ViewModelBase hierarchy: simplify generics, remove redundant logger dependencies, and streamline initialization patterns. * Refactor various components: optimize property and method signatures, streamline placeholders and localization, and remove redundant dependencies. * Refactor KnownHostsFile and related components: optimize initialization, enhance file handling logic, and streamline key export functionality. * Refactor KnownHosts handling: optimize file updates, consolidate constructors, and streamline bindings in KnownHosts GUI rendering. * Optimize AuthorizedKeys handling: add change detection for file updates, prevent unnecessary writes to file and server, and fix BUG comment. * Refactor dependency injection tests: use DryIoc container directly and refine registration logic to improve clarity and accuracy. * Add `Window_OnClosing` handler for dialogs, adjust window properties, and include ReactiveUI references in project. * add todo; * Prevent window closing interruptions: handle `Window_OnClosing` to cancel external close attempts and streamline closing logic. * Refactor ApplicationSettings components: enhance UI alignment properties, add "Open Cache Folder" command, and streamline font size initialization logic. * Refactor SshKeyManager file watcher: integrate Avalonia Dispatcher for UI thread invocation and streamline event handling logic. * Refactor dialog closing logic: introduce `_isInternalClose` flag to prevent external close interruptions and streamline event handling. Optimize reactive properties in `SshKeyFileInformation` for better initialization and UI thread integration. * Refactor `SshKeyFileInformation`: remove reactive properties and disposables to simplify initialization and make properties immutable. * Refactor `FlyoutButton` UI: simplify tooltip and key count display logic, remove spinning icon animation, and streamline related ViewModel properties. * Refactor `ApplicationSettingsWindow`: update UI for cache options, enhance layout alignment, and remove unused reactive properties in ViewModel. * Replace DryIoc with Microsoft.Extensions.DependencyInjection for dependency injection, update related service registrations, and refactor impacted components accordingly. * Adjust DependencyInjectionExtensions * Cleanuip Dialoghost * Fix icon store filling * Reformat & Cleanup * small fixes * Added processing animation * Icon Updates * Added some findings, need more. * Update package dependencies to latest versions and improve DI test robustness * Refactor key file format handling: simplify `Password.Set` logic, update binding properties for key formats, clean up TODOs, and enhance backup file deletion logic. * Refactor and modularize SSH key management: introduce new interfaces and services (`IDirectoryCrawler`, `IKeyFileBackupService`, `IKeyFileWriterService`, and `ISshKeyGenerator`), implement `SshKeyFactory` and `KeyFileBackupService`, and update `SshKeyManager` to use dependency injection and improve maintainability. * Simplify "Provide Password" button in SshKeyFileStyle and fix export format in MainWindowViewModel. * Update ReactiveUI and ReactiveUI.SourceGenerators to latest versions in project files. * Add TODO comment to enable scanning of additional user-scoped paths in DirectoryCrawler. * Added AppConfiguration; Added WriteableConfiguration interface * Added writeableconfig to appsettingsviewmodel * Restrict ComboBoxItem binding in SshKeyFileStyle and remove redundant DataType specification. * Refactor DirectoryCrawler to support async disk enumeration and enable scanning of configurable lookup paths. * Add `PathExtensions` utility class for file/directory management and integrate usage into configuration and logging workflows. * Added ComboBox for path selection in AddKeyWindow and font size binding adjustments * Ensure main window is brought to front on open, then clear topmost flag * Refactor configuration system: replaced `IWritableConfiguration` with `IMutableConfiguration`, updated usages across the app, and improved settings UI layout. * Refactor SSH key file format handling: replace `SshKeyFormatExtension` constants with `PathExtensions`, add missing utility functions, and update references across the codebase. * Add lookup path management in ApplicationSettings: enable adding and removing paths with UI integration and validation logic * Refactor ApplicationSettings: enhance lookup path management, implement new bindings for HeaderedItem, throttle reactive commands, and extract `PathDeletableConverter` to a dedicated file. * Refactor application configuration: update font size type to `double`, introduce `ApplyConfiguration` method for dynamic theme and log level application, and optimize reactive bindings in `ApplicationSettingsViewModel`. * Add `CollectionIndexConverter` for 1-based indexing in lookup paths and update `ApplicationSettingsWindow` bindings to include index display * Update `ApplicationSettingsWindow`: localize lookup paths header text and add German translation * Add `TooltippedIcon` control and integrate it into `ApplicationSettingsWindow` for enhanced UI element descriptions * Add hover-based tooltip functionality to `TooltippedIcon` with extended pointer handling and popup interactions * Update dependencies: bump Avalonia packages to 12.0.2, ReactiveUI to 23.2.27, and other minor version upgrades * clean up indentation, adjust formatting across services and views, and streamline property definitions. * Centralize NuGet package version management and refactor project files to improve readability and reduce redundancy. * Fix SSH key format detection: ensure `.EndsWith` is used for extension comparison to handle edge cases * Introduce new workflows: auto-tag refinement, unit tests, build-and-package, and optimize release flow. - **Auto-tag Workflow**: Improved conditional checks, logging, and tag push status messages. - Added **Unit Test Workflow** for .NET projects with caching and restoration steps. - Modularized build process with **Build-and-Package Workflow** to handle multi-platform targets and upload artifacts cleanly. - Refined and streamlined **Release Workflows** with reusable job configurations and reduced redundancy. * Bump version to 3.1.0, improve PKGBUILD formatting, and fix SSH key format extension handling. * Add `build-and-package.yml` and `test.yml` workflows; refine asset preparation logic in `staging.yml`. * Refactor workflows: streamline asset preparation, reorganize directories, and remove redundant nightly release job. * Update `staging.yml`: adjust dependencies for `deploy-aur-nightly` and refine artifact handling logic * Refactor GitHub Actions workflows: improve artifact handling and enhance AUR metadata validation * Update `staging.yml`: add `build` dependency for `deploy-aur-nightly` and improve artifact preparation logic * Revamp README: overhaul content structure, include new features, update screenshots, and replace outdated information. * Bump version to 3.1.1; update PKGBUILD, normalize version logic in workflow, and adjust artifact packaging. * Bump version to 3.1.2; update PKGBUILD, enhance version normalization logic in workflow, and streamline artifact preparation. * Enhance Komac installation in workflow: dynamically fetch latest release URL, add validation, and log version. * Refactor GitHub Actions: modularize workflows, add custom composite actions for .NET setup, AppImage building, and publishing, and enhance release process with concurrency and timeout management. * Add `actions/checkout@v4` to workflows for consistent repository access. * Refactor `install-komac` action: enhance reliability with `gh` CLI integration, improve release asset resolution, and simplify output handling. * Remove `install-komac` action, update GitHub Actions to latest versions, and refactor workflows for consistency and efficiency. * Remove unused `needs: detect-tfm` property from `.github/actions/dotnet-setup/action.yml`. * Remove nightly GitHub Release action from staging workflow * resolve warning IL3000 * Remove nightly support and update workflows to reflect changes * Update PKGBUILD to include date and commit hash in versioning * Sync auto-tag workflow with development process: - Add pull request write permissions. - Increase timeout for tag creation job. - Implement automatic cherry-picking of release commits into development with conflict handling. - Adjust test workflow triggers for better branch handling. * Simplify test workflow by removing conditional triggers based on event type * Update auto-tag workflow: Remove "Sync Development" from job name * Bump version to 3.1.6 in PKGBUILD and Directory.Build.props (cherry picked from commit ebe69b11013ed295183deeba4440db7e23c9f9cc) --- Directory.Build.props | 2 +- openssh-gui-bin/PKGBUILD | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 537f9c4..9965a13 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.1.4 + 3.1.6 true diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 2fe387e..34b33f6 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -1,5 +1,5 @@ pkgname=openssh-gui-bin -pkgver=3.1.4 +pkgver=3.1.6 pkgrel=1 pkgdesc="A GUI for OpenSSH configuration and management (Binary version)" arch=('x86_64') From e3a39c0b9f0e474ec5f53378de2a5eadfd4914a4 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:21:24 +0200 Subject: [PATCH 31/58] Update workflows and dependencies: - Remove `install-komac` action. - Replace `actions/checkout@v4` with `v6`. - Replace `actions/upload-artifact@v4` with `v7`. - Update various dependencies in GitHub Actions workflows. - Introduce `deploy-release.yml` for release deployment to AUR and Winget. - Simplify `auto-tag.yml` with reusable tag creation action. - Cleanup and modernize PKGBUILD structure. --- .github/actions/dotnet-setup/action.yml | 6 +- .github/actions/install-komac/action.yml | 37 -------- .github/workflows/auto-tag.yml | 40 +++------ .github/workflows/build-and-package.yml | 12 +-- .github/workflows/build.yml | 108 ++--------------------- .github/workflows/deploy-release.yml | 81 +++++++++++++++++ .github/workflows/staging.yml | 4 +- .github/workflows/test.yml | 2 +- openssh-gui-bin/PKGBUILD | 27 ++---- 9 files changed, 116 insertions(+), 201 deletions(-) delete mode 100644 .github/actions/install-komac/action.yml create mode 100644 .github/workflows/deploy-release.yml diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml index ac20840..195f138 100644 --- a/.github/actions/dotnet-setup/action.yml +++ b/.github/actions/dotnet-setup/action.yml @@ -22,7 +22,7 @@ runs: using: composite steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: ${{ inputs.fetch-depth }} token: ${{ inputs.token }} @@ -46,12 +46,12 @@ runs: echo "::notice::Resolved .NET version: ${TFM}.x" - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ steps.detect-tfm.outputs.version }} - name: Restore NuGet cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props', '**/Directory.Build.props') }} diff --git a/.github/actions/install-komac/action.yml b/.github/actions/install-komac/action.yml deleted file mode 100644 index 84cd3a0..0000000 --- a/.github/actions/install-komac/action.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: 'Install Komac' -description: > - Resolves and downloads the latest Komac (linux-amd64) release from GitHub - and makes it executable in the current working directory. - -outputs: - komac-path: - description: 'Absolute path to the komac binary' - value: ${{ steps.install.outputs.komac-path }} - -runs: - using: composite - steps: - - name: Resolve and download Komac - id: install - shell: bash - run: | - set -euo pipefail - - KOMAC_URL=$(curl -fsSL \ - https://api.github.com/repos/russellbanks/Komac/releases/latest \ - | grep -o '"browser_download_url": "[^"]*linux-amd64[^"]*"' \ - | grep -v '\.sha' \ - | head -1 \ - | cut -d'"' -f4) - - if [[ -z "$KOMAC_URL" ]]; then - echo "::error::Failed to resolve Komac download URL from GitHub Releases API" - exit 1 - fi - - echo "::notice::Downloading Komac from: $KOMAC_URL" - curl -fsSL "$KOMAC_URL" -o komac - chmod +x komac - ./komac --version - - echo "komac-path=$(pwd)/komac" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 58731cb..089d855 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -2,17 +2,16 @@ name: Auto-Tag on Release Merge on: pull_request: - types: [ closed ] + types: [closed] branches: - master - - main permissions: contents: write pull-requests: write concurrency: - group: auto-tag-${{ github.ref }} + group: auto-tag-${{ github.event.pull_request.base.ref }} cancel-in-progress: false jobs: @@ -23,34 +22,21 @@ jobs: timeout-minutes: 10 steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout merge commit + uses: actions/checkout@v6 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.merge_commit_sha }} token: ${{ secrets.PAT_TOKEN }} - name: Extract and validate version id: version uses: ./.github/actions/determine-version - - - name: Check if tag already exists - id: check - run: | - set -euo pipefail - if git rev-parse "refs/tags/${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "::warning::Tag ${{ steps.version.outputs.tag }} already exists, skipping." - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Create and push tag - if: steps.check.outputs.exists == 'false' - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.version.outputs.tag }}" \ - -m "Release ${{ steps.version.outputs.version }}" - git push origin "${{ steps.version.outputs.tag }}" - echo "::notice::Tag ${{ steps.version.outputs.tag }} pushed successfully." \ No newline at end of file + + - name: Create Tag Action + id: create_tag_action + uses: rickstaa/action-create-tag@v1 + with: + commit_sha: ${{ github.event.pull_request.merge_commit_sha }} + github_token: ${{ secrets.PAT_TOKEN }} + tag: ${{ steps.version.outputs.tag }} \ No newline at end of file diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index b058645..d1981cf 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 strategy: - fail-fast: false # One failing target must not kill the others + fail-fast: true # One failing target must kill the others matrix: include: - target: linux-x64 @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup .NET environment uses: ./.github/actions/dotnet-setup @@ -88,7 +88,7 @@ jobs: - name: Upload AppImage if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.appimage.outputs.appimage-name }} path: ${{ steps.appimage.outputs.appimage-name }} @@ -96,7 +96,7 @@ jobs: - name: Upload .desktop file if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: io.github.frequency403.openssh_gui.desktop path: AppDir/usr/share/applications/io.github.frequency403.openssh_gui.desktop @@ -104,14 +104,14 @@ jobs: - name: Upload app icon if: matrix.target == 'linux-x64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: appicon path: AppDir/appicon.png if-no-files-found: error - name: Upload platform binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.rename.outputs.ASSET_NAME }} path: ./publish/${{ steps.rename.outputs.ASSET_NAME }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4f2800..4c215ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: tag: ${{ steps.version.outputs.tag }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine Version id: version @@ -49,10 +49,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download all build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts/ @@ -94,106 +94,8 @@ jobs: ls -lah release-assets/ - name: Publish GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.prepare.outputs.tag }} files: release-assets/* - generate_release_notes: true - - deploy-aur: - name: Update AUR Packages - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [ test, prepare, release ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Prepare AUR assets and update PKGBUILDs - run: | - set -euo pipefail - rm -rf aur-assets - mkdir -p aur-assets - - find artifacts -type f | while read -r FILE; do - DEST="aur-assets/$(basename "$FILE")" - if [[ -f "$DEST" ]]; then - echo "::error::AUR artifact name collision: $(basename "$FILE")" - exit 1 - fi - cp "$FILE" "$DEST" - done - - # Assert all required AUR files are present - test -f aur-assets/OpenSSH-GUI-linux-x64 \ - || { echo "::error::Missing linux binary"; exit 1; } - test -f aur-assets/appicon.png \ - || { echo "::error::Missing appicon.png"; exit 1; } - test -f "aur-assets/io.github.frequency403.openssh_gui.desktop" \ - || { echo "::error::Missing .desktop file"; exit 1; } - test -f LICENSE \ - || { echo "::error::Missing LICENSE"; exit 1; } - - VERSION="${{ needs.prepare.outputs.version }}" - SHA_BIN=$(sha256sum aur-assets/OpenSSH-GUI-linux-x64 | cut -d' ' -f1) - SHA_ICON=$(sha256sum aur-assets/appicon.png | cut -d' ' -f1) - SHA_DESKTOP=$(sha256sum "aur-assets/io.github.frequency403.openssh_gui.desktop" | cut -d' ' -f1) - SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1) - - sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-bin/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" \ - openssh-gui-bin/PKGBUILD - - sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD - - echo "Updated PKGBUILDs:" - grep -E '^(pkgver=|pkgrel=|sha256sums=)' openssh-gui-bin/PKGBUILD - - - name: Publish AUR (openssh-gui-bin) - uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 - with: - pkgname: openssh-gui-bin - pkgbuild: ./openssh-gui-bin/PKGBUILD - commit_username: ${{ github.repository_owner }} - commit_email: ${{ github.repository_owner }}@users.noreply.github.com - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ needs.prepare.outputs.tag }}" - - - name: Publish AUR (openssh-gui-git) - uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 - with: - pkgname: openssh-gui-git - pkgbuild: ./openssh-gui-git/PKGBUILD - commit_username: ${{ github.repository_owner }} - commit_email: ${{ github.repository_owner }}@users.noreply.github.com - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Update to ${{ needs.prepare.outputs.tag }}" - - winget: - name: Update Winget Package - runs-on: ubuntu-slim - timeout-minutes: 10 - needs: [ test, prepare, release ] - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Update Winget manifest - uses: vedantmgoyal9/winget-releaser@main - with: - identifier: frequency403.OpenSSHGUI - version: ${{ needs.prepare.outputs.version }} - release-tag: ${{ needs.prepare.outputs.tag }} - installers-regex: 'win-x64\.exe$' - token: ${{ secrets.WINGET_GITHUB_TOKEN }} + generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 0000000..01d6fe4 --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,81 @@ +name: Deploy release to consumers + +on: + release: + types: [released] + +permissions: + contents: read + +jobs: + get_version: + name: Extract Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.parse.outputs.version }} + tag: ${{ steps.parse.outputs.tag }} + + steps: + - name: Parse release metadata + id: parse + run: | + set -euo pipefail + + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + aur_publish: + name: Publish AUR packages + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: get_version + + steps: + - name: Checkout PKGBUILD only + uses: actions/checkout@v6 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 1 + sparse-checkout-cone-mode: false + sparse-checkout: | + openssh-gui-bin/PKGBUILD + + - name: Update PKGBUILD version + run: | + set -euo pipefail + + VERSION="${{ needs.get_version.outputs.version }}" + + sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-bin/PKGBUILD + + - name: Publish AUR (bin) + uses: KSXGitHub/github-actions-deploy-aur@v4 + with: + pkgname: openssh-gui-bin + pkgbuild: ./openssh-gui-bin/PKGBUILD + commit_username: ${{ github.repository_owner }} + commit_email: ${{ github.repository_owner }}@users.noreply.github.com + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_message: "Update to ${{ needs.get_version.outputs.tag }}" + + winget_publish: + name: Update Winget Package + runs-on: ubuntu-latest + needs: get_version + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Update Winget manifest + uses: vedantmgoyal9/winget-releaser@main + with: + identifier: frequency403.OpenSSHGUI + version: ${{ needs.get_version.outputs.version }} + release-tag: ${{ needs.get_version.outputs.tag }} + installers-regex: 'win-x64\.exe$' + token: ${{ secrets.WINGET_GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 06cd6a8..8ae5528 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine Version id: version @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 060c411..124ee6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup .NET environment uses: ./.github/actions/dotnet-setup diff --git a/openssh-gui-bin/PKGBUILD b/openssh-gui-bin/PKGBUILD index 34b33f6..28501eb 100644 --- a/openssh-gui-bin/PKGBUILD +++ b/openssh-gui-bin/PKGBUILD @@ -12,28 +12,11 @@ options=('!strip') provides=('openssh-gui') conflicts=('openssh-gui' 'openssh-gui-git' 'openssh-gui-nightly') -_relurl="https://github.com/frequency403/OpenSSH-GUI/releases/download/v${pkgver}" -_rawurl="https://raw.githubusercontent.com/frequency403/OpenSSH-GUI/v${pkgver}" - source=( - "${pkgname}-${pkgver}::${_relurl}/OpenSSH-GUI-linux-x64" - "${pkgname}-icon-${pkgver}.png::${_relurl}/appicon.png" - "${pkgname}-desktop-${pkgver}.desktop::${_relurl}/io.github.frequency403.openssh_gui.desktop" - "${pkgname}-license-${pkgver}::${_rawurl}/LICENSE" + "openssh-gui::https://github.com/frequency403/OpenSSH-GUI/releases/download/v${pkgver}/OpenSSH-GUI-linux-x64" + "openssh-gui-icon::https://github.com/frequency403/OpenSSH-GUI/releases/download/v${pkgver}/appicon.png" + "openssh-gui-desktop::https://github.com/frequency403/OpenSSH-GUI/releases/download/v${pkgver}/io.github.frequency403.openssh_gui.desktop" + "LICENSE::https://raw.githubusercontent.com/frequency403/OpenSSH-GUI/v${pkgver}/LICENSE" ) -sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP') - -package() { - install -Dm755 "${srcdir}/${pkgname}-${pkgver}" \ - "${pkgdir}/usr/bin/openssh-gui" - - install -Dm644 "${srcdir}/${pkgname}-icon-${pkgver}.png" \ - "${pkgdir}/usr/share/icons/hicolor/256x256/apps/openssh-gui.png" - - install -Dm644 "${srcdir}/${pkgname}-desktop-${pkgver}.desktop" \ - "${pkgdir}/usr/share/applications/io.github.frequency403.openssh_gui.desktop" - - install -Dm644 "${srcdir}/${pkgname}-license-${pkgver}" \ - "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -} \ No newline at end of file +sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP') \ No newline at end of file From 8e3e88c9f00ed7db50e5db06a275105bc63150dc Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:34:37 +0200 Subject: [PATCH 32/58] Enhance `determine-version` action and workflows: - Add configurable inputs for file, property, and ref. - Update workflows to pass ref to `determine-version`. - Simplify workflows by reducing redundant checkout steps. --- .github/actions/determine-version/action.yml | 35 ++++++++++++++++---- .github/workflows/auto-tag.yml | 4 ++- .github/workflows/build.yml | 2 ++ .github/workflows/staging.yml | 6 ++-- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/.github/actions/determine-version/action.yml b/.github/actions/determine-version/action.yml index 573f13a..72cac93 100644 --- a/.github/actions/determine-version/action.yml +++ b/.github/actions/determine-version/action.yml @@ -1,6 +1,21 @@ name: Determine Version description: Extract and validate a semantic version from Directory.Build.props and optionally validate against a branch or tag name +inputs: + file-name: + description: The filename where the dotnet msbuild command can extract the version. Provide the full path from repo root! + required: false + default: 'Directory.Build.props' + + property: + description: The property where the dotnet msbuild command must look for the verison + required: false + default: 'BaseVersion' + + ref: + description: Where the checkout should be performed + required: true + outputs: version: description: Extracted semantic version without leading v @@ -13,28 +28,34 @@ runs: using: composite steps: + - name: Checkout needed file + uses: actions/checkout@v6 + with: + fetch-depth: '1' + ref: ${{ inputs.ref }} + sparse-checkout: | + ${{ inputs.file-name }} + - name: Extract BaseVersion id: extract shell: bash run: | set -euo pipefail - FILE="Directory.Build.props" - - if [[ ! -f "$FILE" ]]; then - echo "::error::$FILE not found" + if [[ ! -f "${{ inputs.file-name }}" ]]; then + echo "::error::${{ inputs.file-name }} not found" exit 1 fi - VERSION="$(dotnet msbuild "$FILE" -nologo -getProperty:BaseVersion | tr -d '\r')" + VERSION="$(dotnet msbuild "${{ inputs.file-name }}" -nologo -getProperty:${{ inputs.property }} | tr -d '\r')" if [[ -z "$VERSION" ]]; then - echo "::error::BaseVersion not found in $FILE" + echo "::error::${{ inputs.property }} not found in ${{ inputs.file-name }}" exit 1 fi if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Invalid BaseVersion: $VERSION" + echo "::error::Invalid Property ${{ inputs.property }}: $VERSION" exit 1 fi diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 089d855..ad3616a 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -32,7 +32,9 @@ jobs: - name: Extract and validate version id: version uses: ./.github/actions/determine-version - + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + - name: Create Tag Action id: create_tag_action uses: rickstaa/action-create-tag@v1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c215ed..0a1173f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,6 +32,8 @@ jobs: - name: Determine Version id: version uses: ./.github/actions/determine-version + with: + ref: ${{ github.ref }} build: needs: [ test, prepare ] diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 8ae5528..98d4775 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -27,12 +27,11 @@ jobs: tag: ${{ steps.version.outputs.tag }} steps: - - name: Checkout repository - uses: actions/checkout@v6 - - name: Determine Version id: version uses: ./.github/actions/determine-version + with: + ref: ${{ github.ref }} deploy-aur-git: name: Update AUR Git Package @@ -45,6 +44,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 1 + ref: ${{ github.ref }} - name: Update openssh-gui-git PKGBUILD run: | From 7c01d3cfc1c8ad590c8bb5206582c5fb80739d53 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:38:25 +0200 Subject: [PATCH 33/58] Add sparse checkout for `determine-version` action in staging workflow --- .github/workflows/staging.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 98d4775..a4f65e8 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -27,6 +27,15 @@ jobs: tag: ${{ steps.version.outputs.tag }} steps: + - name: Checkout action file + uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + fetch-depth: '1' + sparse-checkout-cone-mode: 'true' + sparse-checkout: | + .github/actions/determine-version + - name: Determine Version id: version uses: ./.github/actions/determine-version From 45183305861bbf82b57a46f68c10fb6aacc8dec6 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:42:56 +0200 Subject: [PATCH 34/58] Disable sparse checkout cone mode in `determine-version` action --- .github/actions/determine-version/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/determine-version/action.yml b/.github/actions/determine-version/action.yml index 72cac93..db4df5a 100644 --- a/.github/actions/determine-version/action.yml +++ b/.github/actions/determine-version/action.yml @@ -33,6 +33,7 @@ runs: with: fetch-depth: '1' ref: ${{ inputs.ref }} + sparse-checkout-cone-mode: 'false' sparse-checkout: | ${{ inputs.file-name }} From 41242344ff3718f6a64b56468b282d0c7dcc7930 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:47:29 +0200 Subject: [PATCH 35/58] Remove redundant `ref` input from `actions/checkout` in staging workflow --- .github/workflows/staging.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index a4f65e8..8a1a815 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -53,7 +53,6 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 1 - ref: ${{ github.ref }} - name: Update openssh-gui-git PKGBUILD run: | From f7c539423ed65500e1cbae6a2142ad007375ef1e Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:51:56 +0200 Subject: [PATCH 36/58] Remove `fetch-depth` input from checkout step in staging workflow --- .github/workflows/staging.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 8a1a815..085d474 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -51,8 +51,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 1 - name: Update openssh-gui-git PKGBUILD run: | From ed7aaaa0c9c23985f7f5f4658a6722fea464d6a3 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 16:56:58 +0200 Subject: [PATCH 37/58] Simplify staging workflow by reducing inputs and removing sparse checkout --- .github/workflows/staging.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 085d474..87d447d 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -27,20 +27,12 @@ jobs: tag: ${{ steps.version.outputs.tag }} steps: - - name: Checkout action file + - name: Checkout repository uses: actions/checkout@v6 - with: - ref: ${{ github.ref }} - fetch-depth: '1' - sparse-checkout-cone-mode: 'true' - sparse-checkout: | - .github/actions/determine-version - + - name: Determine Version id: version uses: ./.github/actions/determine-version - with: - ref: ${{ github.ref }} deploy-aur-git: name: Update AUR Git Package @@ -51,6 +43,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: 1 - name: Update openssh-gui-git PKGBUILD run: | @@ -74,4 +68,4 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Development update ${{ needs.prepare.outputs.tag }}" + commit_message: "Development update ${{ needs.prepare.outputs.tag }}" \ No newline at end of file From aa6cb1a1cc1bdf3ea69212017466bc0206262069 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 17:03:34 +0200 Subject: [PATCH 38/58] .. --- .github/actions/determine-version/action.yml | 24 ++++++-------------- .github/workflows/build.yml | 5 ++-- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.github/actions/determine-version/action.yml b/.github/actions/determine-version/action.yml index db4df5a..ae0f8e9 100644 --- a/.github/actions/determine-version/action.yml +++ b/.github/actions/determine-version/action.yml @@ -11,10 +11,7 @@ inputs: description: The property where the dotnet msbuild command must look for the verison required: false default: 'BaseVersion' - - ref: - description: Where the checkout should be performed - required: true + outputs: version: @@ -28,30 +25,23 @@ runs: using: composite steps: - - name: Checkout needed file - uses: actions/checkout@v6 - with: - fetch-depth: '1' - ref: ${{ inputs.ref }} - sparse-checkout-cone-mode: 'false' - sparse-checkout: | - ${{ inputs.file-name }} - - name: Extract BaseVersion id: extract shell: bash run: | set -euo pipefail - if [[ ! -f "${{ inputs.file-name }}" ]]; then - echo "::error::${{ inputs.file-name }} not found" + FILE="${{ inputs.file-name }}" + + if [[ ! -f "$FILE" ]]; then + echo "::error::$FILE not found" exit 1 fi - VERSION="$(dotnet msbuild "${{ inputs.file-name }}" -nologo -getProperty:${{ inputs.property }} | tr -d '\r')" + VERSION="$(dotnet msbuild "$FILE" -nologo -getProperty:${{ inputs.property }} | tr -d '\r')" if [[ -z "$VERSION" ]]; then - echo "::error::${{ inputs.property }} not found in ${{ inputs.file-name }}" + echo "::error::${{ inputs.property }} not found in $FILE" exit 1 fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a1173f..556f3a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,12 +28,13 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: '1' + ref: ${{ github.ref }} - name: Determine Version id: version uses: ./.github/actions/determine-version - with: - ref: ${{ github.ref }} build: needs: [ test, prepare ] From cbab03c18626a89521e88d0c0bf2aa14f8ca14df Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 17:07:55 +0200 Subject: [PATCH 39/58] verion bump --- .github/workflows/staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 87d447d..b588133 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -61,7 +61,7 @@ jobs: grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD - name: Publish AUR (openssh-gui-git) - uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 + uses: KSXGitHub/github-actions-deploy-aur@v4.1.3 with: pkgname: openssh-gui-git pkgbuild: ./openssh-gui-git/PKGBUILD From f715bf4102131ccbc24e854d751c439f0a377c73 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 17:12:19 +0200 Subject: [PATCH 40/58] Update staging workflow to include post-process step for branch checkout --- .github/workflows/staging.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index b588133..e5a8104 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -68,4 +68,6 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Development update ${{ needs.prepare.outputs.tag }}" \ No newline at end of file + commit_message: "Development update ${{ needs.prepare.outputs.tag }}" + post_process: | + git checkout -B master \ No newline at end of file From 11cc4fa9e0719d0f231164670a0534a2eb0928d9 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 17:17:22 +0200 Subject: [PATCH 41/58] Update staging workflow: fix branch checkout logic in post-process step --- .github/workflows/staging.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index e5a8104..b5a90bc 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -70,4 +70,5 @@ jobs: ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Development update ${{ needs.prepare.outputs.tag }}" post_process: | - git checkout -B master \ No newline at end of file + git fetch origin master + git checkout master || git checkout -b master origin/master \ No newline at end of file From c871fed73ed3ede3d4da33eb82aa9ab9bf1083b5 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 17:24:01 +0200 Subject: [PATCH 42/58] Simplify staging workflow by removing post-process step for branch checkout --- .github/workflows/staging.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index b5a90bc..b588133 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -68,7 +68,4 @@ jobs: commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Development update ${{ needs.prepare.outputs.tag }}" - post_process: | - git fetch origin master - git checkout master || git checkout -b master origin/master \ No newline at end of file + commit_message: "Development update ${{ needs.prepare.outputs.tag }}" \ No newline at end of file From 663e6daa5337d173839d0407724516f92788e416 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 17:32:21 +0200 Subject: [PATCH 43/58] Update dependencies in staging workflow: downgrade checkout action and AUR deploy action --- .github/workflows/staging.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index b588133..45d7255 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Determine Version id: version @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 1 @@ -61,7 +61,7 @@ jobs: grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD - name: Publish AUR (openssh-gui-git) - uses: KSXGitHub/github-actions-deploy-aur@v4.1.3 + uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 with: pkgname: openssh-gui-git pkgbuild: ./openssh-gui-git/PKGBUILD From 9d11fc02f5b7b3cd1e52cf8303d2f34ae253360d Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 22:21:07 +0200 Subject: [PATCH 44/58] =?UTF-8?q?fix:=20correct=20AUR=20action=20parameter?= =?UTF-8?q?s=20(commit=5Fusername=20=E2=86=92=20author=5Fname,=20commit=5F?= =?UTF-8?q?email=20=E2=86=92=20author=5Femail)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 01d6fe4..3eca1df 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -57,8 +57,8 @@ jobs: with: pkgname: openssh-gui-bin pkgbuild: ./openssh-gui-bin/PKGBUILD - commit_username: ${{ github.repository_owner }} - commit_email: ${{ github.repository_owner }}@users.noreply.github.com + author_name: ${{ github.repository_owner }} + author_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Update to ${{ needs.get_version.outputs.tag }}" @@ -78,4 +78,4 @@ jobs: version: ${{ needs.get_version.outputs.version }} release-tag: ${{ needs.get_version.outputs.tag }} installers-regex: 'win-x64\.exe$' - token: ${{ secrets.WINGET_GITHUB_TOKEN }} \ No newline at end of file + token: ${{ secrets.WINGET_GITHUB_TOKEN }} From bc4507de50f3f084c1cc758f99dc3c7e66110da4 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 22:22:49 +0200 Subject: [PATCH 45/58] fix: add force flag and increase fetch-depth for AUR git deployment --- .github/workflows/staging.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 45d7255..0f3619e 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -44,7 +44,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 1 + fetch-depth: 0 - name: Update openssh-gui-git PKGBUILD run: | @@ -65,7 +65,8 @@ jobs: with: pkgname: openssh-gui-git pkgbuild: ./openssh-gui-git/PKGBUILD - commit_username: ${{ github.repository_owner }} - commit_email: ${{ github.repository_owner }}@users.noreply.github.com + author_name: ${{ github.repository_owner }} + author_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Development update ${{ needs.prepare.outputs.tag }}" \ No newline at end of file + commit_message: "Development update ${{ needs.prepare.outputs.tag }}" + force: true From 71cf2d9961ec5ef6a836089e28afdfa679684b71 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 22:26:24 +0200 Subject: [PATCH 46/58] fix: add force flag and revert to correct AUR action parameters --- .github/workflows/deploy-release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 3eca1df..e461b62 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -57,10 +57,11 @@ jobs: with: pkgname: openssh-gui-bin pkgbuild: ./openssh-gui-bin/PKGBUILD - author_name: ${{ github.repository_owner }} - author_email: ${{ github.repository_owner }}@users.noreply.github.com + commit_username: ${{ github.repository_owner }} + commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Update to ${{ needs.get_version.outputs.tag }}" + force: true winget_publish: name: Update Winget Package From a70d0b670ac92c55ee9c216a101307c0c8ad070c Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 22:29:03 +0200 Subject: [PATCH 47/58] fix: correct AUR action to use commit_username and commit_email parameters --- .github/workflows/staging.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 0f3619e..5ad5924 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -65,8 +65,8 @@ jobs: with: pkgname: openssh-gui-git pkgbuild: ./openssh-gui-git/PKGBUILD - author_name: ${{ github.repository_owner }} - author_email: ${{ github.repository_owner }}@users.noreply.github.com + commit_username: ${{ github.repository_owner }} + commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Development update ${{ needs.prepare.outputs.tag }}" force: true From d1562a2a3cb6a9136a7e597d6512bc3d9c17625a Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 22:38:58 +0200 Subject: [PATCH 48/58] fix: explicitly specify master branch for AUR deployment to avoid refspec error --- .github/workflows/staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 5ad5924..393fde2 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -69,4 +69,4 @@ jobs: commit_email: ${{ github.repository_owner }}@users.noreply.github.com ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Development update ${{ needs.prepare.outputs.tag }}" - force: true + branch: master From 94607569131634258f463d3074007ec63ce1f29b Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 22:59:05 +0200 Subject: [PATCH 49/58] refactor: replace KSXGitHub action with custom git clone and push for AUR deployment --- .github/workflows/staging.yml | 58 ++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 393fde2..4f220d0 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -41,32 +41,48 @@ jobs: needs: [ test, prepare ] steps: - - name: Checkout repository + - name: Clone AUR repository + run: | + mkdir -p /tmp/aur-deploy + cd /tmp/aur-deploy + git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . + env: + GIT_SSH_COMMAND: ssh -i /tmp/aur_key -o StrictHostKeyChecking=no + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > /tmp/aur_key + chmod 600 /tmp/aur_key + + - name: Checkout OpenSSH-GUI repository uses: actions/checkout@v4 with: + path: openssh-gui-repo fetch-depth: 0 - - name: Update openssh-gui-git PKGBUILD + - name: Copy and update PKGBUILD run: | - set -euo pipefail + cd /tmp/aur-deploy + cp openssh-gui-repo/openssh-gui-git/PKGBUILD . + DATE=$(date +%Y%m%d) - HASH=$(git rev-parse --short HEAD) - # pkgver must only contain [a-zA-Z0-9._] — no + or - + HASH=$(cd openssh-gui-repo && git rev-parse --short HEAD) AUR_VERSION="${{ needs.prepare.outputs.version }}.${DATE}.${HASH}" + + sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD + + echo "Updated PKGBUILD with version: $AUR_VERSION" + grep -E '^(pkgver=|pkgrel=)' PKGBUILD - sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-git/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD - - echo "Updated PKGBUILD:" - grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD - - - name: Publish AUR (openssh-gui-git) - uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 - with: - pkgname: openssh-gui-git - pkgbuild: ./openssh-gui-git/PKGBUILD - commit_username: ${{ github.repository_owner }} - commit_email: ${{ github.repository_owner }}@users.noreply.github.com - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_message: "Development update ${{ needs.prepare.outputs.tag }}" - branch: master + - name: Push to AUR + run: | + cd /tmp/aur-deploy + git config user.name "${{ github.repository_owner }}" + git config user.email "${{ github.repository_owner }}@users.noreply.github.com" + git add PKGBUILD + git commit -m "Development update ${{ needs.prepare.outputs.tag }}" || echo "No changes to commit" + git push -u origin master + env: + GIT_SSH_COMMAND: ssh -i /tmp/aur_key -o StrictHostKeyChecking=no From 43664f551a5ccd7178ef61606a45db70ed43561f Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:01:31 +0200 Subject: [PATCH 50/58] fix: setup SSH key before cloning AUR repository --- .github/workflows/staging.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 4f220d0..755d6c9 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -41,19 +41,24 @@ jobs: needs: [ test, prepare ] steps: + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur_key + chmod 600 ~/.ssh/aur_key + cat >> ~/.ssh/config << 'EOF' + Host aur.archlinux.org + IdentityFile ~/.ssh/aur_key + User aur + StrictHostKeyChecking no + EOF + chmod 644 ~/.ssh/config + - name: Clone AUR repository run: | mkdir -p /tmp/aur-deploy cd /tmp/aur-deploy git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . - env: - GIT_SSH_COMMAND: ssh -i /tmp/aur_key -o StrictHostKeyChecking=no - - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > /tmp/aur_key - chmod 600 /tmp/aur_key - name: Checkout OpenSSH-GUI repository uses: actions/checkout@v4 @@ -84,5 +89,3 @@ jobs: git add PKGBUILD git commit -m "Development update ${{ needs.prepare.outputs.tag }}" || echo "No changes to commit" git push -u origin master - env: - GIT_SSH_COMMAND: ssh -i /tmp/aur_key -o StrictHostKeyChecking=no From ca475c2c680eda2ec0f18606f6deadc3afe50cf8 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:04:47 +0200 Subject: [PATCH 51/58] fix: simplify AUR deployment with correct paths and environment variables --- .github/workflows/staging.yml | 56 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 755d6c9..a9dabd4 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -41,6 +41,11 @@ jobs: needs: [ test, prepare ] steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup SSH key run: | mkdir -p ~/.ssh @@ -52,40 +57,39 @@ jobs: User aur StrictHostKeyChecking no EOF - chmod 644 ~/.ssh/config - - - name: Clone AUR repository - run: | - mkdir -p /tmp/aur-deploy - cd /tmp/aur-deploy - git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . - - - name: Checkout OpenSSH-GUI repository - uses: actions/checkout@v4 - with: - path: openssh-gui-repo - fetch-depth: 0 - - name: Copy and update PKGBUILD + - name: Update PKGBUILD run: | - cd /tmp/aur-deploy - cp openssh-gui-repo/openssh-gui-git/PKGBUILD . - + set -euo pipefail DATE=$(date +%Y%m%d) - HASH=$(cd openssh-gui-repo && git rev-parse --short HEAD) + HASH=$(git rev-parse --short HEAD) AUR_VERSION="${{ needs.prepare.outputs.version }}.${DATE}.${HASH}" - sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD + sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-git/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD - echo "Updated PKGBUILD with version: $AUR_VERSION" - grep -E '^(pkgver=|pkgrel=)' PKGBUILD + echo "Updated PKGBUILD:" + grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD - - name: Push to AUR + - name: Clone and push to AUR run: | - cd /tmp/aur-deploy + set -euo pipefail + + # Clone AUR repository to temporary location + cd /tmp + rm -rf aur-deploy + mkdir aur-deploy + cd aur-deploy + git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . + + # Copy updated PKGBUILD from original repo + cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . + + # Configure git git config user.name "${{ github.repository_owner }}" git config user.email "${{ github.repository_owner }}@users.noreply.github.com" + + # Commit and push git add PKGBUILD - git commit -m "Development update ${{ needs.prepare.outputs.tag }}" || echo "No changes to commit" - git push -u origin master + git commit -m "Development update ${{ needs.prepare.outputs.tag }}" || true + git push origin master From 7d04c013f2f039b85616c9ab4124f7c54e8dc76b Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:34:11 +0200 Subject: [PATCH 52/58] fix: explicitly create and switch to the master branch during AUR deployment --- .github/workflows/staging.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index a9dabd4..2d19665 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -81,6 +81,7 @@ jobs: mkdir aur-deploy cd aur-deploy git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . + git checkout -B master # Copy updated PKGBUILD from original repo cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . From 66d0b8a5d07793834bdc2651c9f944335073f3a5 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:37:24 +0200 Subject: [PATCH 53/58] fix: ensure branch tracks origin/master during AUR deployment --- .github/workflows/staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 2d19665..87551ce 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -81,7 +81,7 @@ jobs: mkdir aur-deploy cd aur-deploy git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . - git checkout -B master + git checkout -B master origin/master # Copy updated PKGBUILD from original repo cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . From fc4282ba320f53bb3e49498037f438bf74fcff23 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:40:51 +0200 Subject: [PATCH 54/58] fix: include .SRCINFO generation during AUR deployment --- .github/workflows/staging.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 87551ce..4b9887c 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -85,12 +85,13 @@ jobs: # Copy updated PKGBUILD from original repo cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . - - # Configure git + + docker run --rm -v "$(pwd)":/pkg -w /pkg archlinux:latest \ + bash -c "pacman -Sy --noconfirm --needed pacman && makepkg --printsrcinfo > .SRCINFO" + git config user.name "${{ github.repository_owner }}" git config user.email "${{ github.repository_owner }}@users.noreply.github.com" - - # Commit and push - git add PKGBUILD + + git add PKGBUILD .SRCINFO git commit -m "Development update ${{ needs.prepare.outputs.tag }}" || true git push origin master From 09635e9df11a9596d1eefedbbf2eaecdabbfa5e1 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:43:14 +0200 Subject: [PATCH 55/58] fix: run makepkg as non-root user during AUR deployment --- .github/workflows/staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 4b9887c..24a8c0f 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -87,7 +87,7 @@ jobs: cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . docker run --rm -v "$(pwd)":/pkg -w /pkg archlinux:latest \ - bash -c "pacman -Sy --noconfirm --needed pacman && makepkg --printsrcinfo > .SRCINFO" + bash -c "useradd -m builder && chown -R builder /pkg && su builder -c 'makepkg --printsrcinfo > .SRCINFO'" git config user.name "${{ github.repository_owner }}" git config user.email "${{ github.repository_owner }}@users.noreply.github.com" From 33f87afb437b5ef7614c0f6f87c3cc5cb4066d7a Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Thu, 11 Jun 2026 23:46:07 +0200 Subject: [PATCH 56/58] fix: update .SRCINFO generation command in AUR deployment --- .github/workflows/staging.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 24a8c0f..8533077 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -86,8 +86,8 @@ jobs: # Copy updated PKGBUILD from original repo cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . - docker run --rm -v "$(pwd)":/pkg -w /pkg archlinux:latest \ - bash -c "useradd -m builder && chown -R builder /pkg && su builder -c 'makepkg --printsrcinfo > .SRCINFO'" + docker run --rm -v "$(pwd)":/pkg archlinux:latest \ + bash -c "useradd -m builder && cp /pkg/PKGBUILD /home/builder/ && su builder -c 'cd /home/builder && makepkg --printsrcinfo' > /pkg/.SRCINFO" git config user.name "${{ github.repository_owner }}" git config user.email "${{ github.repository_owner }}@users.noreply.github.com" From a92207aea1a22566cc39f5e819e6cb70f35f68c9 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Fri, 12 Jun 2026 00:24:13 +0200 Subject: [PATCH 57/58] refactor: add reusable deploy-aur action and integrate with workflows --- .github/actions/deploy-aur/action.yml | 118 ++++++++++++++++++++++++ .github/actions/dotnet-setup/action.yml | 16 ---- .github/workflows/auto-tag.yml | 2 - .github/workflows/deploy-release.yml | 19 ++-- .github/workflows/staging.yml | 57 ++++-------- 5 files changed, 145 insertions(+), 67 deletions(-) create mode 100644 .github/actions/deploy-aur/action.yml diff --git a/.github/actions/deploy-aur/action.yml b/.github/actions/deploy-aur/action.yml new file mode 100644 index 0000000..d4cdadb --- /dev/null +++ b/.github/actions/deploy-aur/action.yml @@ -0,0 +1,118 @@ +name: 'Deploy AUR Package' +description: > + Publishes a PKGBUILD to AUR. Tries KSXGitHub/github-actions-deploy-aur first, + falls back to a manual clone-and-push with .SRCINFO regeneration via Docker. + +inputs: + pkgname: + description: 'AUR package name' + required: true + pkgbuild: + description: 'Path to the PKGBUILD file' + required: true + ssh_private_key: + description: 'SSH private key with AUR access' + required: true + commit_username: + description: 'Git commit username' + required: true + commit_email: + description: 'Git commit email' + required: true + commit_message: + description: 'Git commit message' + required: true + force_push: + description: 'Force push to AUR' + required: false + default: 'false' + regenerate_srcinfo: + description: 'Regenerate .SRCINFO via Docker in the manual fallback (needed for -git packages)' + required: false + default: 'false' + updpkgsums: + description: 'Run updpkgsums after copying PKGBUILD (requires pacman-contrib in the Action environment)' + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Deploy via AUR Action (primary) + id: aur_action + continue-on-error: true + uses: KSXGitHub/github-actions-deploy-aur@v4.1.3 + with: + pkgname: ${{ inputs.pkgname }} + pkgbuild: ${{ inputs.pkgbuild }} + commit_username: ${{ inputs.commit_username }} + commit_email: ${{ inputs.commit_email }} + ssh_private_key: ${{ inputs.ssh_private_key }} + commit_message: ${{ inputs.commit_message }} + force_push: ${{ inputs.force_push }} + ssh_keyscan_types: rsa,ecdsa,ed25519 + updpkgsums: ${{ inputs.updpkgsums }} + + - name: Setup SSH key for fallback + if: steps.aur_action.outcome == 'failure' + shell: bash + run: | + mkdir -p ~/.ssh + echo "${{ inputs.ssh_private_key }}" > ~/.ssh/aur_key + chmod 600 ~/.ssh/aur_key + cat >> ~/.ssh/config << 'EOF' + Host aur.archlinux.org + IdentityFile ~/.ssh/aur_key + User aur + StrictHostKeyChecking no + EOF + + - name: Deploy via manual push (fallback) + id: aur_manual + continue-on-error: true + if: steps.aur_action.outcome == 'failure' + shell: bash + run: | + set -euo pipefail + echo "::warning::AUR Action failed, falling back to manual push" + + PKGBUILD_DIR="$(dirname "${{ inputs.pkgbuild }}")" + + cd /tmp + rm -rf aur-deploy + mkdir aur-deploy + cd aur-deploy + git clone ssh://aur@aur.archlinux.org/${{ inputs.pkgname }}.git . + git checkout -B master origin/master + + cp "$GITHUB_WORKSPACE/${{ inputs.pkgbuild }}" ./PKGBUILD + + if [[ "${{ inputs.regenerate_srcinfo }}" == "true" ]]; then + docker run --rm -v "$(pwd)":/pkg archlinux:latest \ + bash -c "useradd -m builder \ + && cp /pkg/PKGBUILD /home/builder/ \ + && su builder -c 'cd /home/builder && makepkg --printsrcinfo >> /pkg/.SRCINFO'" + fi + + git config user.name "${{ inputs.commit_username }}" + git config user.email "${{ inputs.commit_email }}" + + git add PKGBUILD + if [[ "${{ inputs.regenerate_srcinfo }}" == "true" ]]; then + git add .SRCINFO + fi + + git commit -m "${{ inputs.commit_message }}" || true + + if [[ "${{ inputs.force_push }}" == "true" ]]; then + git push origin master --force + else + git push origin master + fi + + - name: Verify deployment succeeded + if: steps.aur_action.outcome == 'failure' && steps.aur_manual.outcome == 'failure' + shell: bash + run: | + echo "::error::Both deployment paths failed for ${{ inputs.pkgname }}" + exit 1 \ No newline at end of file diff --git a/.github/actions/dotnet-setup/action.yml b/.github/actions/dotnet-setup/action.yml index 195f138..fa575f3 100644 --- a/.github/actions/dotnet-setup/action.yml +++ b/.github/actions/dotnet-setup/action.yml @@ -3,16 +3,6 @@ description: > Checks out the repository, auto-detects the TargetFramework from Directory.Build.props, sets up the .NET SDK, and restores the NuGet cache. -inputs: - fetch-depth: - description: 'Number of commits to fetch (0 = full history, 1 = shallow)' - required: false - default: '1' - token: - description: 'GitHub token used for checkout' - required: false - default: ${{ github.token }} - outputs: dotnet-version: description: 'Resolved .NET version string, e.g. "10.0.x"' @@ -21,12 +11,6 @@ outputs: runs: using: composite steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: ${{ inputs.fetch-depth }} - token: ${{ inputs.token }} - - name: Detect TargetFramework from Directory.Build.props id: detect-tfm shell: bash diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index ad3616a..36e9f4a 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -32,8 +32,6 @@ jobs: - name: Extract and validate version id: version uses: ./.github/actions/determine-version - with: - ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Create Tag Action id: create_tag_action diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index e461b62..56bab68 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -52,29 +52,32 @@ jobs: sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-bin/PKGBUILD - - name: Publish AUR (bin) - uses: KSXGitHub/github-actions-deploy-aur@v4 + - name: Deploy to AUR + uses: ./.github/actions/deploy-aur with: pkgname: openssh-gui-bin - pkgbuild: ./openssh-gui-bin/PKGBUILD + pkgbuild: openssh-gui-bin/PKGBUILD + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_username: ${{ github.repository_owner }} commit_email: ${{ github.repository_owner }}@users.noreply.github.com - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: "Update to ${{ needs.get_version.outputs.tag }}" - force: true + force_push: 'false' + regenerate_srcinfo: 'false' + updpkgsums: 'true' winget_publish: name: Update Winget Package - runs-on: ubuntu-latest + runs-on: ubuntu-slim needs: get_version - + timeout-minutes: 10 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Update Winget manifest uses: vedantmgoyal9/winget-releaser@main with: + max-versions-to-keep: 5 identifier: frequency403.OpenSSHGUI version: ${{ needs.get_version.outputs.version }} release-tag: ${{ needs.get_version.outputs.tag }} diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 8533077..f953da4 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -10,7 +10,7 @@ permissions: concurrency: group: staging-${{ github.ref }} - cancel-in-progress: true # Supersede previous run on rapid pushes + cancel-in-progress: true jobs: test: @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Determine Version id: version @@ -42,56 +42,31 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur_key - chmod 600 ~/.ssh/aur_key - cat >> ~/.ssh/config << 'EOF' - Host aur.archlinux.org - IdentityFile ~/.ssh/aur_key - User aur - StrictHostKeyChecking no - EOF - - name: Update PKGBUILD run: | set -euo pipefail DATE=$(date +%Y%m%d) HASH=$(git rev-parse --short HEAD) AUR_VERSION="${{ needs.prepare.outputs.version }}.${DATE}.${HASH}" - + sed -i "s/^pkgver=.*/pkgver=$AUR_VERSION/" openssh-gui-git/PKGBUILD sed -i "s/^pkgrel=.*/pkgrel=1/" openssh-gui-git/PKGBUILD - + echo "Updated PKGBUILD:" grep -E '^(pkgver=|pkgrel=)' openssh-gui-git/PKGBUILD - - name: Clone and push to AUR - run: | - set -euo pipefail - - # Clone AUR repository to temporary location - cd /tmp - rm -rf aur-deploy - mkdir aur-deploy - cd aur-deploy - git clone ssh://aur@aur.archlinux.org/openssh-gui-git.git . - git checkout -B master origin/master - - # Copy updated PKGBUILD from original repo - cp $GITHUB_WORKSPACE/openssh-gui-git/PKGBUILD . - - docker run --rm -v "$(pwd)":/pkg archlinux:latest \ - bash -c "useradd -m builder && cp /pkg/PKGBUILD /home/builder/ && su builder -c 'cd /home/builder && makepkg --printsrcinfo' > /pkg/.SRCINFO" - - git config user.name "${{ github.repository_owner }}" - git config user.email "${{ github.repository_owner }}@users.noreply.github.com" - - git add PKGBUILD .SRCINFO - git commit -m "Development update ${{ needs.prepare.outputs.tag }}" || true - git push origin master + - name: Deploy to AUR + uses: ./.github/actions/deploy-aur + with: + pkgname: openssh-gui-git + pkgbuild: openssh-gui-git/PKGBUILD + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_username: ${{ github.repository_owner }} + commit_email: ${{ github.repository_owner }}@users.noreply.github.com + commit_message: "Development update ${{ needs.prepare.outputs.tag }}" + force_push: 'true' + regenerate_srcinfo: 'true' \ No newline at end of file From 06d52654f2402bf49c4af7cd51ed484cd703d127 Mon Sep 17 00:00:00 2001 From: Oliver Schantz Date: Fri, 12 Jun 2026 00:27:55 +0200 Subject: [PATCH 58/58] chore: bump BaseVersion to 3.1.7 in Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9965a13..68eec9f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ enable default https://github.com/frequency403/OpenSSH-GUI - 3.1.6 + 3.1.7 true