modified release pipeline (#4)
Release / release (push) Canceled after 47s

await certificate from ssl.com

Reviewed-on: #4
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
2026-08-13 11:17:22 +00:00
committed by alex
parent 5e11b16758
commit 42974ecc8f
34 changed files with 495 additions and 2091 deletions
+1 -4
View File
@@ -55,7 +55,6 @@ public partial class App : Application
MainWindow.Show();
_ = _services.GetRequiredService<SettingsViewModel>().InitializeAsync();
_ = _services.GetRequiredService<UpdateViewModel>().StartAsync();
}
protected override void OnExit(ExitEventArgs e)
@@ -73,7 +72,7 @@ public partial class App : Application
internal static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(new UpdateOptions());
services.AddSingleton(AppVersion.Current);
services.AddSingleton<SettingsService>();
services.AddSingleton(provider => provider.GetRequiredService<SettingsService>().Load());
@@ -85,9 +84,7 @@ public partial class App : Application
services.AddSingleton<ILocalizationService, LocalizationService>();
services.AddSingleton<IStartupService, StartupService>();
services.AddSingleton<IUpdateService, UpdateService>();
services.AddSingleton<UpdateViewModel>();
services.AddSingleton<SettingsViewModel>();
services.AddSingleton<MainWindow>();
@@ -16,15 +16,15 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>..\CursorLang.Core\Resources\CursorLang.ico</ApplicationIcon>
<PlatformTarget>AnyCPU</PlatformTarget>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<SelfContained Condition="'$(RuntimeIdentifier)' != ''">true</SelfContained>
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' == 'win-x64'">true</PublishReadyToRun>
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Product>CursorLang</Product>
<Company>Aleksandr Neychev</Company>
<Company>Aleksandr Neichev</Company>
<Description>Settings window of CursorLang</Description>
<Copyright>Copyright (c) 2026</Copyright>
</PropertyGroup>
@@ -65,6 +65,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
];
private readonly IStartupService _startup;
private readonly Version _version;
private StartupState _startupState = StartupState.Unavailable;
@@ -72,12 +73,12 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
AppSettings settings,
ILocalizationService localization,
IStartupService startup,
UpdateViewModel updates)
Version version)
{
Settings = settings;
Localization = localization;
Updates = updates;
_startup = startup;
_version = version;
// The interface language is a setting like any other and is stored in the same place
Localization.CurrentLanguage = settings.Language;
@@ -95,8 +96,23 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
public ILocalizationService Localization { get; }
/// <summary>The updates section: it has a state and commands of its own.</summary>
public UpdateViewModel Updates { get; }
/// <summary>
/// The title of the window, with the version in it.
/// </summary>
/// <remarks>
/// The version is nowhere else in the interface: a package installed from the
/// Store is updated by the Store, so the window has nothing to say about
/// updates — but which version is running is still worth knowing, if only to
/// name it in a bug report.
///
/// Three numbers, not four: the fourth one is the revision the Store keeps
/// for itself, and it is a zero in every package we build. The numbers are
/// put together by hand rather than by <c>ToString(3)</c>, which throws on a
/// version that has fewer of them than asked for.
/// </remarks>
public string Title => string.Format(
Localization["SettingsTitle"],
$"{_version.Major}.{_version.Minor}.{Math.Max(_version.Build, 0)}");
public IReadOnlyList<Color> BackgroundPalette { get; } = Palette;
@@ -188,6 +204,7 @@ public sealed partial class SettingsViewModel : ObservableObject, IDisposable
return;
}
OnPropertyChanged(nameof(Title));
Translate(Themes);
Translate(PlacementModes);
Translate(AnchorSides);
@@ -1,238 +0,0 @@
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Windows.Data;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CursorLang.Core.Models;
using CursorLang.Core.Services;
namespace CursorLang.Settings.ViewModels;
/// <summary>
/// The updates section of the settings window.
/// </summary>
/// <remarks>
/// The section always says where things stand: a check flies off the moment the window
/// appears, and its outcome — including an unreachable network — stays written in the
/// status line. Nothing here is silent, and nothing disappears: a user who opened the
/// window is told about updates whether they came looking for them or not.
/// </remarks>
public sealed partial class UpdateViewModel : ObservableObject, IDisposable
{
private readonly IUpdateService _updates;
private readonly ILocalizationService _localization;
private CancellationTokenSource? _work;
private ReleaseInfo? _release;
private string? _packagePath;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(IsBusy))]
[NotifyPropertyChangedFor(nameof(CanCheck))]
[NotifyPropertyChangedFor(nameof(IsProgressUnknown))]
[NotifyPropertyChangedFor(nameof(IsDownloadOffered))]
[NotifyPropertyChangedFor(nameof(IsInstallOffered))]
[NotifyPropertyChangedFor(nameof(IsProgressShown))]
[NotifyPropertyChangedFor(nameof(IsReleaseLinkShown))]
private UpdateStatus _status = UpdateStatus.Idle;
/// <summary>The downloaded fraction: from 0 to 1.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsProgressUnknown))]
private double _progress;
public UpdateViewModel(IUpdateService updates, ILocalizationService localization)
{
_updates = updates;
_localization = localization;
_localization.PropertyChanged += OnLocalizationChanged;
}
/// <summary>Whether to show the updates section at all.</summary>
public bool IsSupported => _updates.IsSupported;
public string CurrentVersion => _updates.CurrentVersion.ToString();
/// <summary>A request or a download is in flight — the buttons freeze for that time.</summary>
public bool IsBusy => Status is UpdateStatus.Checking or UpdateStatus.Downloading;
public bool CanCheck => !IsBusy;
/// <summary>
/// The package size is unknown, and the bar shows only the fact of the download.
/// Its start looks the same until the first report arrives.
/// </summary>
public bool IsProgressUnknown => Status == UpdateStatus.Downloading && Progress <= 0;
public bool IsDownloadOffered => Status == UpdateStatus.Available;
public bool IsInstallOffered => Status == UpdateStatus.Ready;
public bool IsProgressShown => Status == UpdateStatus.Downloading;
public bool IsReleaseLinkShown => _release?.PageUrl is not null && Status is not UpdateStatus.Checking;
/// <summary>The release page: the release notes live there too.</summary>
public Uri? ReleaseUrl => _release?.PageUrl;
/// <summary>
/// What the section says. There is a line for every state, the one before the
/// first check included: the status is never an empty spot in the window.
/// </summary>
public string StatusText => Status switch
{
UpdateStatus.Checking => _localization["UpdateChecking"],
UpdateStatus.UpToDate => _localization["UpdateUpToDate"],
UpdateStatus.Available => Format("UpdateAvailable", _release?.Tag),
UpdateStatus.Downloading => _localization["UpdateDownloading"],
UpdateStatus.Ready => _localization["UpdateReady"],
UpdateStatus.Failed => _localization["UpdateFailed"],
_ => _localization["UpdateNotChecked"],
};
/// <summary>
/// Asks about new versions. Called once, when the window has just appeared.
/// </summary>
/// <remarks>
/// It used to be called when the application started, which back then meant when
/// the machine was switched on. The background half is a separate process now and
/// does not go to the network at all — nothing in it could show the answer — so the
/// question is asked when there is a window to answer into.
///
/// And it is asked every time that window appears: opening it is a deliberate act
/// of the user, rare enough that a request costs nothing, and the answer is what
/// the section exists for. A remembered answer from yesterday is worth less than
/// today's, so nothing is remembered.
/// </remarks>
public Task StartAsync() => CheckAsync();
public void Dispose()
{
_localization.PropertyChanged -= OnLocalizationChanged;
_work?.Cancel();
_work?.Dispose();
_work = null;
}
[RelayCommand]
private async Task CheckAsync()
{
if (!IsSupported)
{
return;
}
CancellationToken token = StartWork();
Status = UpdateStatus.Checking;
try
{
_release = await _updates.CheckAsync(token);
_packagePath = null;
Status = _release is null ? UpdateStatus.UpToDate : UpdateStatus.Available;
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
// The check was cancelled by the next piece of work: it has already set its own state
return;
}
catch (Exception e) when (IsExpected(e))
{
Status = UpdateStatus.Failed;
}
OnPropertyChanged(nameof(ReleaseUrl));
OnPropertyChanged(nameof(IsReleaseLinkShown));
}
[RelayCommand]
private async Task DownloadAsync()
{
if (_release is not { } release)
{
return;
}
CancellationToken token = StartWork();
Progress = 0;
Status = UpdateStatus.Downloading;
try
{
var progress = new Progress<double>(value => Progress = value);
_packagePath = await _updates.DownloadAsync(release, progress, token);
Status = UpdateStatus.Ready;
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
// The download was interrupted by the next piece of work: it has already set its own state
}
catch (Exception e) when (IsExpected(e))
{
Status = UpdateStatus.Failed;
}
}
[RelayCommand]
private void Install()
{
if (_packagePath is null || !File.Exists(_packagePath))
{
// The file was removed by the temp folder cleanup — downloading it again is what is left
Status = _release is null ? UpdateStatus.Idle : UpdateStatus.Available;
return;
}
try
{
_updates.Install(_packagePath);
}
catch (Exception e) when (IsExpected(e))
{
Status = UpdateStatus.Failed;
}
}
/// <summary>
/// The errors that simply leave the update undone: an unreachable network, an
/// unexpected response, a file in use. Everything else is a reason to crash.
/// </summary>
/// <remarks>
/// <c>OperationCanceledException</c> means an expired request deadline here:
/// cancellation by the application itself is caught by a separate handler above.
/// </remarks>
private static bool IsExpected(Exception e) =>
e is HttpRequestException or JsonException or IOException or UnauthorizedAccessException
or NotSupportedException or InvalidOperationException or Win32Exception
or OperationCanceledException;
/// <summary>
/// Starts a new piece of work, cancelling the previous one: the user may have
/// pressed "Check" in the middle of a download.
/// </summary>
private CancellationToken StartWork()
{
_work?.Cancel();
_work?.Dispose();
_work = new CancellationTokenSource();
return _work.Token;
}
private string Format(string key, string? argument) =>
string.Format(CultureInfo.CurrentCulture, _localization[key], argument);
private void OnLocalizationChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == Binding.IndexerName)
{
OnPropertyChanged(nameof(StatusText));
}
}
}
+1 -67
View File
@@ -7,7 +7,7 @@
xmlns:vm="clr-namespace:CursorLang.Settings.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
Title="{Binding Localization[SettingsTitle]}"
Title="{Binding Title}"
Width="1000" SizeToContent="Height" MaxHeight="900"
ResizeMode="CanMinimize"
Background="{DynamicResource Theme.WindowBackground}"
@@ -196,72 +196,6 @@
</Grid>
</GroupBox>
<!-- The section is absent for an app from the Store: the Store updates it itself -->
<GroupBox Header="{Binding Localization[SectionUpdates]}"
Visibility="{Binding Updates.IsSupported, Converter={StaticResource BooleanToVisibility}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="164" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource FieldLabel}"
Text="{Binding Localization[CurrentVersionLabel]}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center"
Text="{Binding Updates.CurrentVersion}" />
<Button Grid.Column="2" Padding="12,4"
Command="{Binding Updates.CheckCommand}"
IsEnabled="{Binding Updates.CanCheck}"
Content="{Binding Localization[CheckUpdatesButton]}" />
<!-- The status is always on show: the check flies off with the
window, and where things stand is the answer the user came for -->
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,12,0,0" TextWrapping="Wrap"
Text="{Binding Updates.StatusText}" />
<!-- The downloaded fraction is not always known: not every hosting reports the file size -->
<ProgressBar Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,8,0,0" Height="4" Maximum="1"
Value="{Binding Updates.Progress, Mode=OneWay}"
IsIndeterminate="{Binding Updates.IsProgressUnknown}"
Visibility="{Binding Updates.IsProgressShown, Converter={StaticResource BooleanToVisibility}}" />
<StackPanel Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,12,0,0" Orientation="Horizontal">
<Button Padding="12,4"
Command="{Binding Updates.DownloadCommand}"
Visibility="{Binding Updates.IsDownloadOffered, Converter={StaticResource BooleanToVisibility}}"
Content="{Binding Localization[DownloadUpdateButton]}" />
<Button Padding="12,4"
Command="{Binding Updates.InstallCommand}"
Visibility="{Binding Updates.IsInstallOffered, Converter={StaticResource BooleanToVisibility}}"
Content="{Binding Localization[InstallUpdateButton]}" />
<TextBlock Margin="12,0,0,0" VerticalAlignment="Center"
Visibility="{Binding Updates.IsReleaseLinkShown, Converter={StaticResource BooleanToVisibility}}">
<Hyperlink NavigateUri="{Binding Updates.ReleaseUrl}"
RequestNavigate="OnReleaseLinkNavigate">
<Run Text="{Binding Localization[ReleasePageLink], Mode=OneWay}" />
</Hyperlink>
</TextBlock>
</StackPanel>
<TextBlock Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,6,0,0" TextWrapping="Wrap"
Foreground="{DynamicResource Theme.SecondaryForeground}"
Visibility="{Binding Updates.IsInstallOffered, Converter={StaticResource BooleanToVisibility}}"
Text="{Binding Localization[UpdateInstallHint]}" />
</Grid>
</GroupBox>
</StackPanel>
<StackPanel Grid.Column="1" Margin="16,0,0,0">
@@ -1,7 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Navigation;
using CursorLang.Settings.Services;
using CursorLang.Settings.ViewModels;
@@ -28,23 +25,4 @@ public partial class MainWindow : Window
theme.Register(this);
placement.Attach(this);
}
/// <summary>
/// Opens the release page in a browser. A link in WPF leads nowhere on its own:
/// where to hand it over is up to the application.
/// </summary>
private void OnReleaseLinkNavigate(object sender, RequestNavigateEventArgs e)
{
e.Handled = true;
try
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true })?.Dispose();
}
catch (Exception exception) when (exception is Win32Exception or InvalidOperationException)
{
// There is no browser in the system — that does not get in the way of the
// update, which is downloaded by the button next to it anyway
}
}
}