DevRibbonViewModel.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Collections.ObjectModel; 6 using System.Diagnostics; 7 using System.Globalization; 8 using System.Text.RegularExpressions; 9 using CommunityToolkit.Mvvm.ComponentModel; 10 using CommunityToolkit.Mvvm.Input; 11 using CommunityToolkit.Mvvm.Messaging; 12 using ManagedCommon; 13 using Microsoft.CmdPal.UI.Helpers; 14 using Microsoft.CmdPal.UI.Messages; 15 using Microsoft.UI; 16 using Windows.System; 17 using Windows.UI; 18 using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; 19 20 namespace Microsoft.CmdPal.UI.ViewModels; 21 22 internal sealed partial class DevRibbonViewModel : ObservableObject 23 { 24 private const int MaxLogEntries = 2; 25 private const string Release = "Release"; 26 private const string Debug = "Debug"; 27 28 private static readonly Color ReleaseAotColor = ColorHelper.FromArgb(255, 124, 58, 237); 29 private static readonly Color ReleaseColor = ColorHelper.FromArgb(255, 51, 65, 85); 30 private static readonly Color DebugAotColor = ColorHelper.FromArgb(255, 99, 102, 241); 31 private static readonly Color DebugColor = ColorHelper.FromArgb(255, 107, 114, 128); 32 33 private readonly DispatcherQueue _dispatcherQueue; 34 35 public DevRibbonViewModel() 36 { 37 _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); 38 Trace.Listeners.Add(new DevRibbonTraceListener(this)); 39 40 var configLabel = BuildConfiguration == Release ? "RLS" : "DBG"; /* #no-spell-check-line */ 41 var aotLabel = BuildInfo.IsNativeAot ? "⚡AOT" : "NO AOT"; 42 Tag = $"{configLabel} | {aotLabel}"; 43 44 TagColor = (BuildConfiguration, BuildInfo.IsNativeAot) switch 45 { 46 (Release, true) => ReleaseAotColor, 47 (Release, false) => ReleaseColor, 48 (Debug, true) => DebugAotColor, 49 (Debug, false) => DebugColor, 50 _ => Colors.Fuchsia, 51 }; 52 } 53 54 public string BuildConfiguration => BuildInfo.Configuration; 55 56 public bool IsAotReleaseConfiguration => BuildConfiguration == Release && BuildInfo.IsNativeAot; 57 58 public bool IsAot => BuildInfo.IsNativeAot; 59 60 public bool IsPublishTrimmed => BuildInfo.PublishTrimmed; 61 62 public ObservableCollection<LogEntryViewModel> LatestLogs { get; } = []; 63 64 [ObservableProperty] 65 public partial int WarningCount { get; private set; } 66 67 [ObservableProperty] 68 public partial int ErrorCount { get; private set; } 69 70 [ObservableProperty] 71 public partial string Tag { get; private set; } 72 73 [ObservableProperty] 74 public partial Color TagColor { get; private set; } 75 76 [RelayCommand] 77 private async Task OpenLogFileAsync() 78 { 79 var logPath = Logger.CurrentLogFile; 80 if (File.Exists(logPath)) 81 { 82 await Launcher.LaunchUriAsync(new Uri(logPath)); 83 } 84 } 85 86 [RelayCommand] 87 private async Task OpenLogFolderAsync() 88 { 89 var logFolderPath = Logger.CurrentVersionLogDirectoryPath; 90 if (Directory.Exists(logFolderPath)) 91 { 92 await Launcher.LaunchFolderPathAsync(logFolderPath); 93 } 94 } 95 96 [RelayCommand] 97 private void ResetErrorCounters() 98 { 99 WarningCount = 0; 100 ErrorCount = 0; 101 LatestLogs.Clear(); 102 } 103 104 [RelayCommand] 105 private void OpenInternalTools() 106 { 107 WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Internal")); 108 } 109 110 private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener 111 { 112 private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; 113 114 [GeneratedRegex(@"^\[(?<timestamp>.*?)\] \[(?<severity>.*?)\] (?<message>.*)")] 115 private static partial Regex LogRegex(); 116 117 private readonly Lock _lock = new(); 118 private LogEntryViewModel? _latestLogEntry; 119 120 public override void Write(string? message) 121 { 122 // Not required for this scenario. 123 } 124 125 public override void WriteLine(string? message) 126 { 127 if (message is null) 128 { 129 return; 130 } 131 132 lock (_lock) 133 { 134 var match = LogRegex().Match(message); 135 if (match.Success) 136 { 137 var severity = match.Groups["severity"].Value; 138 var isWarning = severity.Equals("Warning", StringComparison.OrdinalIgnoreCase); 139 var isError = severity.Equals("Error", StringComparison.OrdinalIgnoreCase); 140 141 if (isWarning || isError) 142 { 143 var timestampStr = match.Groups["timestamp"].Value; 144 var timestamp = DateTimeOffset.TryParseExact( 145 timestampStr, 146 TimestampFormat, 147 CultureInfo.InvariantCulture, 148 DateTimeStyles.AssumeLocal, 149 out var parsed) 150 ? parsed 151 : DateTimeOffset.Now; 152 153 var logEntry = new LogEntryViewModel( 154 timestamp, 155 severity, 156 match.Groups["message"].Value, 157 string.Empty); 158 159 _latestLogEntry = logEntry; 160 161 viewModel._dispatcherQueue.TryEnqueue(() => 162 { 163 if (isWarning) 164 { 165 viewModel.WarningCount++; 166 } 167 else 168 { 169 viewModel.ErrorCount++; 170 } 171 172 viewModel.LatestLogs.Insert(0, logEntry); 173 174 while (viewModel.LatestLogs.Count > MaxLogEntries) 175 { 176 viewModel.LatestLogs.RemoveAt(viewModel.LatestLogs.Count - 1); 177 } 178 }); 179 } 180 else 181 { 182 _latestLogEntry = null; 183 } 184 185 return; 186 } 187 188 if (IndentLevel > 0 && _latestLogEntry is { } latest) 189 { 190 viewModel._dispatcherQueue.TryEnqueue(() => 191 { 192 latest.AppendDetails(message); 193 }); 194 } 195 } 196 } 197 } 198 }