← All source files

V13 / 7879e8b5740b

TextLogSink.cs

Lilith/Core/Logging/TextLogSink.cs

Unchanged from the preceding version
CS
namespace Lilith.Core.Logging;

/// <summary>Renders log types consistently; files retain the last update of each progress phase.</summary>
public sealed class TextLogSink(TextWriter writer, bool overwriteProgress = false, Func<int>? width = null)
{
    private readonly object _sync = new();
    private string? _progress;
    private int _drawnLength;
    private bool _inputActive;
    private readonly List<LogEntry> _deferred = [];

    /// <summary>Defer background output until the current console input has finished.</summary>
    public IDisposable BeginInput()
    {
        lock (_sync)
        {
            if (_inputActive) throw new InvalidOperationException("Console input is already active.");
            FinishProgress();
            _inputActive = true;
            return new TextLogInputLease(EndInput);
        }
    }

    public void Handle(LogEntry entry)
    {
        lock (_sync)
        {
            if (_inputActive && entry.Sender == "Scheduler" && entry.Type == LogType.WriteLine)
            {
                // Idle timer notices must be visible without requiring the user to submit input.
                writer.WriteLine();
                writer.WriteLine(entry.Description);
                writer.Write("You: ");
                writer.Flush();
                return;
            }
            if (_inputActive && !(entry.Sender == "Input" && entry.Type == LogType.Write))
            {
                if (entry.Type == LogType.Progress && _deferred.Count > 0 &&
                    _deferred[^1].Type == LogType.Progress && _deferred[^1].Sender == entry.Sender)
                    _deferred[^1] = entry;
                else _deferred.Add(entry);
                return;
            }
            if (entry.Type == LogType.Progress)
            {
                _progress = entry.Description;
                if (overwriteProgress)
                {
                    int available = Math.Max(1, width?.Invoke() ?? 120);
                    string text = entry.Description.Replace('\r', ' ').Replace('\n', ' ');
                    if (text.Length > available) text = text[..available];
                    writer.Write("\r" + text.PadRight(Math.Min(available, Math.Max(_drawnLength, text.Length))));
                    _drawnLength = text.Length;
                    writer.Flush();
                }
                return;
            }

            FinishProgress();
            if (entry.Type == LogType.Write) writer.Write(entry.Description);
            else writer.WriteLine(entry.Description);
            writer.Flush();
        }
    }

    public void Flush()
    {
        lock (_sync)
        {
            FinishProgress();
            writer.Flush();
        }
    }

    private void FinishProgress()
    {
        if (_progress is null) return;
        if (overwriteProgress) writer.WriteLine();
        else writer.WriteLine(_progress);
        _progress = null;
        _drawnLength = 0;
    }

    private void EndInput()
    {
        lock (_sync)
        {
            _inputActive = false;
            if (_deferred.Count > 0) writer.WriteLine();
            foreach (var entry in _deferred) Handle(entry);
            _deferred.Clear();
        }
    }
}