/ src / modules / cmdpal / Microsoft.CmdPal.UI / ViewModels / DevRibbonViewModel.cs
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  }