Forge
csharpdeeb25a2
1using Avalonia.Media;
2using Avalonia.Threading;
3using CommunityToolkit.Mvvm.ComponentModel;
4
5namespace CascadeIDE.Features.UiChrome;
6
7/// <summary>
8/// Git-строки для полосы Workspace Health и вспышка смены UI-режима Focus/Balanced/Power (bloom overlay).
9/// </summary>
10public sealed partial class UiChromeViewModel : ObservableObject
11{
12 private CancellationTokenSource? _uiModeBloomCts;
13 private string? _lastAppliedUiModeForBloomEffects;
14
15 public static string NormalizeUiMode(string? mode) => UiModeCatalog.NormalizeUiMode(mode);
16
17 /// <summary>Один вызов после обновления git-скаляров в UI (IDE Health / DataBus), без дублирования по <see cref="WorkspaceHealthGitText"/>.</summary>
18 public Action? AfterGitWorkspaceHealthSummaryApplied { get; set; }
19
20 public async Task RefreshGitSummaryAsync(
21 Func<IReadOnlyList<string>, Task<(bool Success, int ExitCode, string Output)>> runGit)
22 {
23 var result = await runGit(["status", "--short", "--branch"]).ConfigureAwait(false);
24 UiScheduler.Default.Post(() =>
25 {
26 if (!result.Success)
27 {
28 GitBranchSummary = "";
29 GitStagedCount = 0;
30 GitUnstagedCount = 0;
31 GitUntrackedCount = 0;
32 FilesChangedBadge = 0;
33 AfterGitWorkspaceHealthSummaryApplied?.Invoke();
34 return;
35 }
36
37 var parsed = ParseGitStatusShortBranch(result.Output);
38 GitBranchSummary = parsed.BranchSummary;
39 GitStagedCount = parsed.Staged;
40 GitUnstagedCount = parsed.Unstaged;
41 GitUntrackedCount = parsed.Untracked;
42 FilesChangedBadge = parsed.ChangedPaths;
43 AfterGitWorkspaceHealthSummaryApplied?.Invoke();
44 });
45 }
46
47 /// <summary>Вызывается из <see cref="MainWindowViewModel"/> после нормализации и сохранения режима UI.</summary>
48 public void NotifyUiModeChangedForBloom(string normalizedMode)
49 {
50 if (_lastAppliedUiModeForBloomEffects is not null
51 && !string.Equals(_lastAppliedUiModeForBloomEffects, normalizedMode, StringComparison.OrdinalIgnoreCase))
52 TriggerUiModeBloom(normalizedMode);
53 _lastAppliedUiModeForBloomEffects = normalizedMode;
54 }
55
56 private void TriggerUiModeBloom(string normalizedMode)
57 {
58 _uiModeBloomCts?.Cancel();
59 _uiModeBloomCts = new CancellationTokenSource();
60 var ct = _uiModeBloomCts.Token;
61 UiModeBloomBrush = PickUiModeBloomBrush(normalizedMode);
62 UiModeBloomOpacity = 0;
63 _ = RunUiModeBloomAsync(normalizedMode, ct);
64 }
65
66 private static IBrush PickUiModeBloomBrush(string mode)
67 {
68 if (string.Equals(mode, "Flight", StringComparison.OrdinalIgnoreCase))
69 return new SolidColorBrush(Color.FromArgb(150, 60, 160, 220));
70 return new SolidColorBrush(Color.FromArgb(120, 255, 235, 200));
71 }
72
73 private static double BloomPeakOpacity(string normalizedMode) =>
74 string.Equals(normalizedMode, "Flight", StringComparison.OrdinalIgnoreCase) ? 0.14 : 0.11;
75
76 private async Task RunUiModeBloomAsync(string normalizedMode, CancellationToken ct)
77 {
78 try
79 {
80 await Task.Delay(18, ct).ConfigureAwait(false);
81 var peak = BloomPeakOpacity(normalizedMode);
82 await UiScheduler.Default.InvokeAsync(() => UiModeBloomOpacity = peak);
83 await Task.Delay(300, ct).ConfigureAwait(false);
84 await UiScheduler.Default.InvokeAsync(() => UiModeBloomOpacity = 0);
85 }
86 catch (OperationCanceledException)
87 {
88 await UiScheduler.Default.InvokeAsync(() => UiModeBloomOpacity = 0);
89 }
90 }
91
92 private static (string BranchSummary, int Staged, int Unstaged, int Untracked, int ChangedPaths)
93 ParseGitStatusShortBranch(string output)
94 {
95 var branch = "";
96 int staged = 0, unstaged = 0, untracked = 0, changedPaths = 0;
97 var lines = (output ?? "")
98 .Replace("\r\n", "\n")
99 .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
100 for (var i = 0; i < lines.Length; i++)
101 {
102 var line = lines[i];
103 if (line.StartsWith("## ", StringComparison.Ordinal))
104 {
105 branch = line[3..].Trim();
106 continue;
107 }
108
109 changedPaths++;
110
111 if (line.StartsWith("??", StringComparison.Ordinal))
112 {
113 untracked++;
114 continue;
115 }
116 if (line.Length < 2)
117 continue;
118 var x = line[0];
119 var y = line[1];
120 if (x != ' ' && x != '?')
121 staged++;
122 if (y != ' ' && y != '?')
123 unstaged++;
124 }
125
126 return (branch, staged, unstaged, untracked, changedPaths);
127 }
128
129 [ObservableProperty]
130 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitText))]
131 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitCockpitShort))]
132 private string _gitBranchSummary = "";
133
134 [ObservableProperty]
135 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitText))]
136 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitCockpitShort))]
137 private int _gitStagedCount;
138
139 [ObservableProperty]
140 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitText))]
141 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitCockpitShort))]
142 private int _gitUnstagedCount;
143
144 [ObservableProperty]
145 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitText))]
146 [NotifyPropertyChangedFor(nameof(WorkspaceHealthGitCockpitShort))]
147 private int _gitUntrackedCount;
148
149 [ObservableProperty]
150 [NotifyPropertyChangedFor(nameof(IsFilesChangedBadgeVisible))]
151 private int _filesChangedBadge;
152
153 /// <summary>Короткая «кинематографичная» вспышка при смене режима UI (Opacity + кисть; разметка — MainWindow).</summary>
154 [ObservableProperty]
155 private double _uiModeBloomOpacity;
156
157 [ObservableProperty]
158 private IBrush _uiModeBloomBrush = Brushes.Transparent;
159
160 public bool IsFilesChangedBadgeVisible => FilesChangedBadge > 0;
161
162 public string WorkspaceHealthGitCockpitShort
163 {
164 get
165 {
166 var br = GitBranchSummary ?? "";
167 if (br.Length > 16)
168 br = string.Concat(br.AsSpan(0, 14), "…");
169 var delta = GitStagedCount + GitUnstagedCount + GitUntrackedCount;
170 return string.IsNullOrWhiteSpace(br) ? $"Δ{delta}" : $"{br} · Δ{delta}";
171 }
172 }
173
174 public string WorkspaceHealthGitText
175 {
176 get
177 {
178 var branch = string.IsNullOrWhiteSpace(GitBranchSummary) ? "" : $" ({GitBranchSummary})";
179 return $"Git: {GitStagedCount} staged, {GitUnstagedCount} unstaged, {GitUntrackedCount} untracked{branch}";
180 }
181 }
182}
183
View only · write via MCP/CIDE