Skip to content

Commit d99e92f

Browse files
authored
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
1 parent e4807aa commit d99e92f

177 files changed

Lines changed: 8221 additions & 4329 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/auto-tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Auto-Tag on Release Merge
22

33
on:
44
pull_request:
5-
types: [closed]
5+
types: [ closed ]
66
branches:
77
- master
88
- main

.github/workflows/build.yml

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
- name: Build AppImage
6969
if: matrix.target == 'linux-x64'
7070
run: |
71+
APPSTREAM_VERSION="${GITHUB_REF_NAME#v}"
7172
# Download appimagetool
7273
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool
7374
chmod +x appimagetool
@@ -147,8 +148,8 @@ jobs:
147148

148149
- name: Prepare extra assets
149150
run: |
150-
cp OpenSSH_GUI/Assets/appicon.png artifacts/
151-
cp LICENSE artifacts/
151+
cp OpenSSH_GUI/Assets/appicon.png artifacts/appicon.png # ← Name explizit
152+
cp LICENSE artifacts/LICENSE
152153
153154
# Create a single release and upload all files from the 'artifacts' directory
154155
- name: Create Release and Upload Assets
@@ -177,7 +178,7 @@ jobs:
177178
with:
178179
name: OpenSSH-GUI-linux-x64
179180
path: ./
180-
181+
181182
- name: Download generated desktop file
182183
uses: actions/download-artifact@v4
183184
with:
@@ -191,11 +192,9 @@ jobs:
191192
SHA_ICON=$(sha256sum OpenSSH_GUI/Assets/appicon.png | cut -d' ' -f1)
192193
SHA_DESKTOP=$(sha256sum io.github.frequency403.openssh_gui.desktop | cut -d' ' -f1)
193194
SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1)
194-
195+
195196
sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-bin/PKGBUILD
196-
sed -i "s/sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD
197-
198-
# Also update openssh-gui-git pkgver for consistency
197+
sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-bin/PKGBUILD
199198
sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-git/PKGBUILD
200199
201200
- name: Update AUR (openssh-gui-bin)
@@ -216,4 +215,32 @@ jobs:
216215
commit_username: ${{ github.repository_owner }}
217216
commit_email: ${{ github.repository_owner }}@users.noreply.github.com
218217
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
219-
commit_message: "Update to ${{ github.ref_name }}"
218+
commit_message: "Update to ${{ github.ref_name }}"
219+
220+
winget:
221+
name: Update Winget Package
222+
runs-on: ubuntu-latest
223+
needs: release
224+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
225+
226+
steps:
227+
- name: Extract version from tag
228+
id: version
229+
run: |
230+
VERSION="${GITHUB_REF_NAME#v}"
231+
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
232+
233+
- name: Install Komac
234+
run: |
235+
curl -sL \
236+
"https://github.com/russellbanks/Komac/releases/latest/download/komac-linux-amd64" \
237+
-o komac
238+
chmod +x komac
239+
240+
- name: Update Winget manifest
241+
run: |
242+
./komac update "frequency403.OpenSSHGUI" \
243+
--version "${{ steps.version.outputs.VERSION }}" \
244+
--urls "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/OpenSSH-GUI-win-x64.exe" \
245+
--submit \
246+
--token "${{ secrets.WINGET_GITHUB_TOKEN }}"

.github/workflows/staging.yml

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
-p:PublishSingleFile=true \
7777
-p:PublishReadyToRun=true \
7878
-p:IncludeNativeLibrariesForSelfExtract=true \
79-
-p:Version="${{ env.VERSION }}"
79+
-p:IsNightly=true
8080
8181
- name: Rename artifact
8282
run: mv ./publish/OpenSSH_GUI${{ matrix.asset_extension }} ./publish/${{ matrix.asset_name }}${{ matrix.asset_extension }}
@@ -121,7 +121,7 @@ jobs:
121121
with:
122122
name: OpenSSH-GUI-nightly-x86_64.AppImage
123123
path: OpenSSH-GUI-nightly-x86_64.AppImage
124-
124+
125125
- name: Upload generated desktop file
126126
if: matrix.target == 'linux-x64'
127127
uses: actions/upload-artifact@v4
@@ -140,12 +140,13 @@ jobs:
140140
name: Create Nightly Release
141141
runs-on: ubuntu-latest
142142
needs: build
143-
143+
144144
steps:
145145
- name: Checkout
146146
uses: actions/checkout@v4
147147
with:
148148
fetch-depth: 0
149+
token: ${{ secrets.PAT_TOKEN }}
149150

150151
- name: Resolve git metadata
151152
run: |
@@ -156,22 +157,34 @@ jobs:
156157
echo "BUILD_DATE=$DATE" >> "$GITHUB_ENV"
157158
echo "VERSION=${BASE_VERSION}+${HASH}" >> "$GITHUB_ENV"
158159
160+
- name: Force-update nightly tag
161+
run: |
162+
git config user.name "github-actions[bot]"
163+
git config user.email "github-actions[bot]@users.noreply.github.com"
164+
git tag -f nightly
165+
git push origin -f nightly
166+
159167
- name: Download all build artifacts
160168
uses: actions/download-artifact@v4
161169
with:
162170
path: artifacts/
163171

164172
- name: Display structure of downloaded files
165173
run: ls -R artifacts
166-
174+
175+
- name: Collect extra release assets
176+
run: |
177+
cp OpenSSH_GUI/Assets/appicon.png artifacts/appicon.png
178+
cp LICENSE artifacts/LICENSE
179+
167180
- name: Update nightly release
168181
env:
169182
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
170183
run: |
171184
NOTES="**Branch:** \`development\`
172185
**Commit:** \`${{ env.GIT_HASH }}\`
173186
**Built:** ${{ env.BUILD_DATE }}
174-
187+
175188
> Automated nightly build — not intended for production use."
176189
177190
if gh release view nightly &>/dev/null; then
@@ -185,10 +198,11 @@ jobs:
185198
186199
find artifacts -type f | xargs gh release upload nightly
187200
else
188-
find artifacts -type f | xargs gh release create nightly \
201+
gh release create nightly \
189202
--title "Nightly (${{ env.BUILD_DATE }}) — ${{ env.VERSION }}" \
190203
--notes "$NOTES" \
191-
--prerelease
204+
--prerelease \
205+
$(find artifacts -type f)
192206
fi
193207
194208
# --- JOB 3: AUR NIGHTLY ---
@@ -208,7 +222,7 @@ jobs:
208222
with:
209223
name: OpenSSH-GUI-nightly-linux-x64
210224
path: ./
211-
225+
212226
- name: Download generated desktop file
213227
uses: actions/download-artifact@v4
214228
with:
@@ -228,7 +242,7 @@ jobs:
228242
SHA_LICENSE=$(sha256sum LICENSE | cut -d' ' -f1)
229243
230244
sed -i "s/^pkgver=.*/pkgver=$VERSION/" openssh-gui-nightly/PKGBUILD
231-
sed -i "s/sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD
245+
sed -i "s/^sha256sums=.*/sha256sums=('$SHA_BIN' '$SHA_ICON' '$SHA_DESKTOP' '$SHA_LICENSE')/" openssh-gui-nightly/PKGBUILD
232246
233247
- name: Update AUR (openssh-gui-nightly)
234248
uses: KSXGitHub/github-actions-deploy-aur@v4.1.1

Directory.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<Nullable>enable</Nullable>
66
<LangVersion>default</LangVersion>
77
<PackageProjectUrl>https://github.com/frequency403/OpenSSH-GUI</PackageProjectUrl>
8-
<BaseVersion>3.0.0</BaseVersion>
8+
<BaseVersion>3.1.0</BaseVersion>
9+
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
910
</PropertyGroup>
1011

1112
<ItemGroup>

Directory.Build.targets

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222
DependsOnTargets="ResolveGitHash">
2323
<PropertyGroup>
2424
<BaseVersion Condition="'$(BaseVersion)' == ''">1.0.0</BaseVersion>
25+
<IsNightly Condition="'$(IsNightly)' == ''">false</IsNightly>
2526

2627
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
27-
2828
<SourceRevisionId>$(GitCommitHash)</SourceRevisionId>
29-
<Version>$(BaseVersion)</Version>
30-
<InformationalVersion>$(BaseVersion)+$(GitCommitHash)</InformationalVersion>
29+
30+
<Version Condition="'$(IsNightly)' != 'true'">$(BaseVersion)</Version>
31+
<InformationalVersion Condition="'$(IsNightly)' != 'true'">$(BaseVersion)</InformationalVersion>
32+
33+
<Version Condition="'$(IsNightly)' == 'true'">$(BaseVersion)-$(GitCommitHash)</Version>
34+
<InformationalVersion Condition="'$(IsNightly)' == 'true'">$(BaseVersion)-$(GitCommitHash)</InformationalVersion>
3135
</PropertyGroup>
3236
</Target>
3337

Directory.Packages.props

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<Project>
2+
<PropertyGroup>
3+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
5+
<NoWarn>$(NoWarn);NU1507</NoWarn>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageVersion Include="Avalonia" Version="12.0.4" />
9+
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.0.0" />
10+
<PackageVersion Include="Avalonia.Desktop" Version="12.0.4" />
11+
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.4" />
12+
<PackageVersion Include="Avalonia.Headless" Version="12.0.4" />
13+
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" />
14+
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
15+
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
16+
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
17+
<PackageVersion Include="Material.Icons" Version="3.0.2" />
18+
<PackageVersion Include="Material.Icons.Avalonia" Version="3.0.2" />
19+
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.9" />
20+
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
21+
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.9" />
22+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
23+
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.9" />
24+
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
25+
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
26+
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
27+
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.9" />
28+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
29+
<PackageVersion Include="NSubstitute" Version="5.3.0" />
30+
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
31+
<PackageVersion Include="ReactiveUI" Version="23.2.28" />
32+
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.0.3" />
33+
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.1.0" />
34+
<PackageVersion Include="ReactiveUI.Validation" Version="7.1.0" />
35+
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
36+
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
37+
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
38+
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
39+
<PackageVersion Include="Shouldly" Version="4.3.0" />
40+
<PackageVersion Include="SSH.NET" Version="2025.1.0" />
41+
<PackageVersion Include="SshNet.Keygen" Version="2024.2.0.1" />
42+
<PackageVersion Include="SshNet.PuttyKeyFile" Version="2024.2.0.3" />
43+
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="12.0.0.11" />
44+
<PackageVersion Include="System.Interactive" Version="7.0.1" />
45+
<PackageVersion Include="System.Interactive.Async" Version="7.0.1" />
46+
<PackageVersion Include="System.Linq.Async" Version="7.0.1" />
47+
<PackageVersion Include="System.Reactive" Version="6.1.0" />
48+
<PackageVersion Include="Xaml.Behaviors.Avalonia" Version="12.0.0.1" />
49+
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
50+
<PackageVersion Include="xunit.v3" Version="3.2.2" />
51+
</ItemGroup>
52+
</Project>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Text.Json.Serialization;
3+
using Microsoft.Extensions.Options;
4+
using OpenSSH_GUI.Core.Enums;
5+
using OpenSSH_GUI.Core.Extensions;
6+
using Serilog.Events;
7+
8+
namespace OpenSSH_GUI.Core.Configuration;
9+
10+
public class ApplicationConfiguration
11+
{
12+
[JsonIgnore]
13+
public static readonly ApplicationConfiguration Default = new()
14+
{
15+
LookupPaths = [SshConfigFilesExtension.GetBaseSshPath()],
16+
PreferredTheme = ThemeVariant.Default,
17+
LogLevel = LogEventLevel.Warning,
18+
FontSize = 14,
19+
LoggerConfiguration = LoggerConfiguration.Default
20+
};
21+
22+
[JsonIgnore]
23+
public static string ApplicationConfigurationPath { get; } = Path.Combine(
24+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
25+
AppDomain.CurrentDomain.FriendlyName);
26+
27+
[JsonIgnore]
28+
public static string ApplicationConfigurationName { get; } = Path.WithJsonExtension(AppDomain.CurrentDomain.FriendlyName.ToLower());
29+
30+
[JsonIgnore]
31+
public static string DefaultApplicationConfigurationFileFullPath { get; } = Path.Combine(ApplicationConfigurationPath, ApplicationConfigurationName);
32+
33+
[Required]
34+
public required string[] LookupPaths { get; set; }
35+
36+
[Required]
37+
public required ThemeVariant PreferredTheme { get; set; }
38+
39+
[Required]
40+
public required LogEventLevel LogLevel { get; set; }
41+
42+
[Required, Range(12, 48, ErrorMessage = "Font size must be between 12 and 48")]
43+
public required double FontSize { get; set; }
44+
45+
[Required, ValidateObjectMembers]
46+
public required LoggerConfiguration LoggerConfiguration { get; set; }
47+
}
48+
49+
[JsonSourceGenerationOptions(WriteIndented = true, UseStringEnumConverter = true), JsonSerializable(typeof(ApplicationConfiguration)), JsonSerializable(typeof(LoggerConfiguration))]
50+
public partial class SourceGenerationContext : JsonSerializerContext
51+
{
52+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization.Metadata;
3+
4+
namespace OpenSSH_GUI.Core.Configuration;
5+
6+
public sealed class JsonFileConfigurationWriter<T>(string filePath, JsonTypeInfo<T> typeInfo)
7+
{
8+
private readonly SemaphoreSlim _lock = new(1, 1);
9+
10+
/// <summary>
11+
/// Reads and deserializes the configuration file into <typeparamref name="T" />.
12+
/// Returns a default instance if the file does not exist.
13+
/// </summary>
14+
public async Task<T?> ReadAsync(CancellationToken ct)
15+
{
16+
if (!File.Exists(filePath))
17+
return default;
18+
19+
await using var stream = File.OpenRead(filePath);
20+
return await JsonSerializer.DeserializeAsync(stream, typeInfo, ct);
21+
}
22+
23+
/// <summary>
24+
/// Atomically writes <paramref name="value" /> to the configuration file via a temp-file swap.
25+
/// </summary>
26+
public async Task WriteAsync(T value, CancellationToken ct)
27+
{
28+
var tempFile = Path.GetTempFileName();
29+
await using (var stream = File.Open(tempFile, FileMode.Truncate))
30+
{
31+
await JsonSerializer.SerializeAsync(stream, value, typeInfo, ct);
32+
}
33+
File.Move(tempFile, filePath, true);
34+
}
35+
36+
/// <summary>
37+
/// Reads the current configuration, applies <paramref name="update" />, then writes the result back atomically.
38+
/// </summary>
39+
public async Task UpdateAsync(Func<T?, Task<T>> update, CancellationToken ct)
40+
{
41+
await _lock.WaitAsync(ct);
42+
try
43+
{
44+
var current = await ReadAsync(ct);
45+
var updated = await update(current);
46+
await WriteAsync(updated, ct);
47+
}
48+
finally
49+
{
50+
_lock.Release();
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)