using AppDiagnostic.Logging; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; namespace AppDiagnostic { public partial class MainForm : Form { private readonly Func connectedClientCountProvider; private readonly Func activationRequestProvider; private readonly Timer refreshTimer; private readonly Font regularLogFont; private readonly Font boldLogFont; private List colorRules; private long lastRenderedVersion = -1; private int observedEntryCount; private int visibleEntryCount; private bool loadingFilters; public MainForm() : this(null, null) { } public MainForm(Func connectedClientCountProvider, Func activationRequestProvider) { this.connectedClientCountProvider = connectedClientCountProvider; this.activationRequestProvider = activationRequestProvider; InitializeComponent(); Icon = Properties.Resources.TBF_icon; regularLogFont = new Font(logView.Font, FontStyle.Regular); boldLogFont = new Font(logView.Font, FontStyle.Bold); colorRules = LogColorRuleRepository.Load(); InitializeFilters(); refreshTimer = new Timer { Interval = 250 }; refreshTimer.Tick += refreshTimer_Tick; refreshTimer.Start(); RefreshView(true); } protected override void OnFormClosing(FormClosingEventArgs e) { refreshTimer.Stop(); refreshTimer.Dispose(); regularLogFont.Dispose(); boldLogFont.Dispose(); base.OnFormClosing(e); } private void InitializeFilters() { loadingFilters = true; levelComboBox.Items.AddRange(new object[] { "All levels", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" }); levelComboBox.SelectedIndex = 0; applicationComboBox.Items.Add("All applications"); applicationComboBox.SelectedIndex = 0; loadingFilters = false; } private void refreshTimer_Tick(object sender, EventArgs e) { ProcessActivationRequest(); RefreshView(false); } private void ProcessActivationRequest() { if (activationRequestProvider == null || !activationRequestProvider()) { return; } if (WindowState == FormWindowState.Minimized) { WindowState = FormWindowState.Normal; } Show(); BringToFront(); Activate(); } private void RefreshView(bool forceFullRefresh) { List snapshot = DiagnosticLogStore.Instance.GetSnapshot(); UpdateApplicationFilter(snapshot); if (pauseButton.Checked) { UpdateStatus(snapshot.Count); return; } long currentVersion = DiagnosticLogStore.Instance.Version; if (!forceFullRefresh && currentVersion == lastRenderedVersion) { UpdateStatus(snapshot.Count); return; } bool canAppend = !forceFullRefresh && snapshot.Count > observedEntryCount; if (canAppend) { AppendEntries(snapshot.Skip(observedEntryCount)); } else { RebuildView(snapshot); } observedEntryCount = snapshot.Count; lastRenderedVersion = currentVersion; UpdateStatus(snapshot.Count); } private void RebuildView(IEnumerable entries) { logView.SuspendLayout(); logView.Clear(); visibleEntryCount = 0; AppendEntries(entries, false); logView.ResumeLayout(); if (autoScrollButton.Checked) { ScrollToEnd(); } } private void AppendEntries(IEnumerable entries, bool scrollAfterAppend = true) { int originalSelectionStart = logView.SelectionStart; int originalSelectionLength = logView.SelectionLength; foreach (DiagnosticLogEntry entry in entries) { if (!MatchesFilter(entry)) { continue; } AppendEntry(entry); visibleEntryCount++; } if (scrollAfterAppend && autoScrollButton.Checked) { ScrollToEnd(); } else if (!autoScrollButton.Checked) { int safeStart = Math.Min(originalSelectionStart, logView.TextLength); int safeLength = Math.Min(originalSelectionLength, logView.TextLength - safeStart); logView.Select(safeStart, safeLength); } } private void AppendEntry(DiagnosticLogEntry entry) { string text = DiagnosticLogFormatter.Format(entry); if (logView.TextLength > 0) { logView.AppendText(Environment.NewLine); } int startIndex = logView.TextLength; logView.AppendText(text); LogColorizer.Apply(logView, startIndex, text.Length, text, colorRules, regularLogFont, boldLogFont); logView.Select(logView.TextLength, 0); } private bool MatchesFilter(DiagnosticLogEntry entry) { string selectedLevel = levelComboBox.SelectedItem as string; if (!string.IsNullOrEmpty(selectedLevel) && selectedLevel != "All levels" && !string.Equals(entry.Level, selectedLevel, StringComparison.OrdinalIgnoreCase)) { return false; } string selectedApplication = applicationComboBox.SelectedItem as string; if (!string.IsNullOrEmpty(selectedApplication) && selectedApplication != "All applications" && !string.Equals(entry.ApplicationName, selectedApplication, StringComparison.OrdinalIgnoreCase)) { return false; } string filter = filterTextBox.Text.Trim(); return filter.Length == 0 || DiagnosticLogFormatter.Format(entry).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; } private void UpdateApplicationFilter(IEnumerable entries) { string selectedApplication = applicationComboBox.SelectedItem as string ?? "All applications"; List applications = entries.Select(entry => entry.ApplicationName).Where(name => !string.IsNullOrWhiteSpace(name)).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name).ToList(); List currentApplications = applicationComboBox.Items.Cast().Skip(1).ToList(); if (applications.SequenceEqual(currentApplications, StringComparer.OrdinalIgnoreCase)) { return; } loadingFilters = true; applicationComboBox.BeginUpdate(); applicationComboBox.Items.Clear(); applicationComboBox.Items.Add("All applications"); applicationComboBox.Items.AddRange(applications.Cast().ToArray()); applicationComboBox.SelectedItem = applicationComboBox.Items.Cast().FirstOrDefault(item => string.Equals(item, selectedApplication, StringComparison.OrdinalIgnoreCase)) ?? "All applications"; applicationComboBox.EndUpdate(); loadingFilters = false; } private void UpdateStatus(int storedCount) { int clients = connectedClientCountProvider?.Invoke() ?? 0; receiverStatusLabel.Text = pauseButton.Checked ? $"Paused | Connected sources: {clients}" : $"Listening | Connected sources: {clients}"; countStatusLabel.Text = $"Received: {DiagnosticLogStore.Instance.TotalReceived:N0} | Stored: {storedCount:N0} | Visible: {visibleEntryCount:N0}"; } private void ScrollToEnd() { logView.Select(logView.TextLength, 0); logView.ScrollToCaret(); } private void pauseButton_Click(object sender, EventArgs e) { pauseButton.Text = pauseButton.Checked ? "Resume" : "Pause"; if (!pauseButton.Checked) { RefreshView(true); } else { UpdateStatus(DiagnosticLogStore.Instance.GetSnapshot().Count); } } private void refreshButton_Click(object sender, EventArgs e) { RefreshView(true); } private void clearButton_Click(object sender, EventArgs e) { DiagnosticLogStore.Instance.Clear(); observedEntryCount = 0; visibleEntryCount = 0; RefreshView(true); } private void saveButton_Click(object sender, EventArgs e) { using (var dialog = new SaveFileDialog()) { dialog.Filter = "Text files (*.txt)|*.txt|Log files (*.log)|*.log|All files (*.*)|*.*"; dialog.Title = "Save visible diagnostic logs"; dialog.DefaultExt = "log"; dialog.FileName = $"AppDiagnostic_{DateTime.Now:yyyyMMdd_HHmmss}.log"; if (dialog.ShowDialog(this) != DialogResult.OK) { return; } try { File.WriteAllText(dialog.FileName, logView.Text); } catch (Exception exception) { MessageBox.Show(this, $"Unable to save diagnostic logs: {exception.Message}", "Save error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void copyButton_Click(object sender, EventArgs e) { CopySelectionOrCurrentLine(); } private void copySelectionMenuItem_Click(object sender, EventArgs e) { if (logView.SelectionLength > 0) { CopyText(logView.SelectedText); } } private void copyLineMenuItem_Click(object sender, EventArgs e) { CopyCurrentLine(); } private void copyAllButton_Click(object sender, EventArgs e) { CopyText(logView.Text); } private void CopySelectionOrCurrentLine() { if (logView.SelectionLength > 0) { CopyText(logView.SelectedText); } else { CopyCurrentLine(); } } private void CopyCurrentLine() { if (logView.Lines.Length == 0) { return; } int lineIndex = logView.GetLineFromCharIndex(logView.SelectionStart); if (lineIndex >= 0 && lineIndex < logView.Lines.Length) { CopyText(logView.Lines[lineIndex]); } } private void CopyText(string text) { if (string.IsNullOrEmpty(text)) { return; } try { Clipboard.SetText(text); } catch (Exception exception) { MessageBox.Show(this, $"Unable to copy text: {exception.Message}", "Clipboard error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void wrapButton_Click(object sender, EventArgs e) { logView.WordWrap = wrapButton.Checked; } private void alwaysOnTopButton_Click(object sender, EventArgs e) { TopMost = alwaysOnTopButton.Checked; } private void colorRulesButton_Click(object sender, EventArgs e) { using (var editor = new ColorRuleEditorForm(colorRules)) { if (editor.ShowDialog(this) != DialogResult.OK) { return; } colorRules = editor.Rules; try { LogColorRuleRepository.Save(colorRules); } catch (Exception exception) { MessageBox.Show(this, $"Unable to save color rules: {exception.Message}", "Settings error", MessageBoxButtons.OK, MessageBoxIcon.Warning); } RefreshView(true); } } private void applyFilterButton_Click(object sender, EventArgs e) { RefreshView(true); } private void clearFilterButton_Click(object sender, EventArgs e) { loadingFilters = true; filterTextBox.Clear(); levelComboBox.SelectedIndex = 0; applicationComboBox.SelectedIndex = 0; loadingFilters = false; RefreshView(true); } private void filterValueChanged(object sender, EventArgs e) { if (!loadingFilters) { RefreshView(true); } } private void filterTextBox_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { RefreshView(true); e.SuppressKeyPress = true; } else if (e.KeyCode == Keys.Escape) { clearFilterButton_Click(sender, EventArgs.Empty); e.SuppressKeyPress = true; } } private void closeButton_Click(object sender, EventArgs e) { Close(); } private void MainForm_KeyDown(object sender, KeyEventArgs e) { if (e.Control && e.KeyCode == Keys.F) { filterTextBox.Focus(); filterTextBox.SelectAll(); e.SuppressKeyPress = true; } else if (e.Control && e.KeyCode == Keys.S) { saveButton_Click(sender, EventArgs.Empty); e.SuppressKeyPress = true; } else if (e.KeyCode == Keys.F5) { RefreshView(true); e.SuppressKeyPress = true; } } } }