From a652fd879b6fcef1842c6c07848772fb76cb6e86 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:29:46 +0800 Subject: [PATCH] Added simple highlight function to the message view --- v2rayN/ServiceLib/Global.cs | 8 ++ .../Common/TextEditorKeywordHighlighter.cs | 129 ++++++++++++++++++ v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs | 6 + 3 files changed, 143 insertions(+) create mode 100644 v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index b45a6eb3..6c4a4362 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -449,6 +449,14 @@ public class Global "none" ]; + public static readonly Dictionary LogLevelColors = new() + { + { "debug", "#6C757D" }, + { "info", "#2ECC71" }, + { "warning", "#FFA500" }, + { "error", "#E74C3C" }, + }; + public static readonly List InboundTags = [ "socks", diff --git a/v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs b/v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs new file mode 100644 index 00000000..af1de3d0 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs @@ -0,0 +1,129 @@ +using Avalonia.Media; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Rendering; + +namespace v2rayN.Desktop.Common; + +public class KeywordColorizer : DocumentColorizingTransformer +{ + private readonly string[] _keywords; + private readonly Dictionary _brushMap; + + public KeywordColorizer(IDictionary keywordBrushMap) + { + if (keywordBrushMap == null || keywordBrushMap.Count == 0) + { + throw new ArgumentException("keywordBrushMap must not be null or empty", nameof(keywordBrushMap)); + } + + _brushMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in keywordBrushMap) + { + if (string.IsNullOrEmpty(kvp.Key) || kvp.Value == null) + { + continue; + } + + if (!_brushMap.ContainsKey(kvp.Key)) + { + _brushMap[kvp.Key] = kvp.Value; + } + } + + if (_brushMap.Count == 0) + { + throw new ArgumentException("keywordBrushMap must contain at least one non-empty key with a non-null brush", nameof(keywordBrushMap)); + } + + _keywords = _brushMap.Keys.ToArray(); + } + + protected override void ColorizeLine(DocumentLine line) + { + var text = CurrentContext.Document.GetText(line); + if (string.IsNullOrEmpty(text)) + { + return; + } + + foreach (var kw in _keywords) + { + if (string.IsNullOrEmpty(kw)) + { + continue; + } + + var searchStart = 0; + while (true) + { + var idx = text.IndexOf(kw, searchStart, StringComparison.OrdinalIgnoreCase); + if (idx < 0) + { + break; + } + + var kwEndIndex = idx + kw.Length; + if (IsWordCharBefore(text, idx) || IsWordCharAfter(text, kwEndIndex)) + { + searchStart = idx + Math.Max(1, kw.Length); + continue; + } + + var start = line.Offset + idx; + var end = start + kw.Length; + + if (_brushMap.TryGetValue(kw, out var brush) && brush != null) + { + ChangeLinePart(start, end, element => element.TextRunProperties.SetForegroundBrush(brush)); + } + + searchStart = idx + Math.Max(1, kw.Length); + } + } + } + + private static bool IsWordCharBefore(string text, int idx) + { + if (idx <= 0) + { + return false; + } + + var c = text[idx - 1]; + return char.IsLetterOrDigit(c) || c == '_'; + } + + private static bool IsWordCharAfter(string text, int idx) + { + if (idx >= text.Length) + { + return false; + } + + var c = text[idx]; + return char.IsLetterOrDigit(c) || c == '_'; + } +} + +public static class TextEditorKeywordHighlighter +{ + public static void Attach(TextEditor editor, IDictionary keywordBrushMap) + { + ArgumentNullException.ThrowIfNull(editor); + + if (keywordBrushMap == null || keywordBrushMap.Count == 0) + { + return; + } + + if (editor.TextArea?.TextView?.LineTransformers?.OfType().Any() == true) + { + return; + } + + var colorizer = new KeywordColorizer(keywordBrushMap); + editor.TextArea.TextView.LineTransformers.Add(colorizer); + editor.TextArea.TextView.InvalidateVisual(); + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs index 637579ee..6a279284 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs @@ -1,5 +1,6 @@ using System.Reactive.Disposables; using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.ReactiveUI; using Avalonia.Threading; using ReactiveUI; @@ -21,6 +22,11 @@ public partial class MsgView : ReactiveUserControl this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); }); + + TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( + kv => kv.Key, + kv => (IBrush)new SolidColorBrush(Color.Parse(kv.Value)) + )); } private async Task UpdateViewHandler(EViewAction action, object? obj)