Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ fallbacks) for inline images/maps.
**M1 delivered, plus substantial M2–M4 work.** `SharpMUTerm.slnx` builds all ten projects on
`net10.0`, with the full test suite passing. In place:

- **Core** — `AnsiParser` (SGR 16/256/truecolor), styled-line + `ScrollbackBuffer` model,
- **Core** — `AnsiParser` (SGR 16/256/truecolor), styled-line + `ScrollbackBuffer` model (a capped
in-memory ring plus a **file-backed spill**, `FileScrollbackSpill`, so history deeper than memory is
paged off an ephemeral per-session cache under `$XDG_CACHE_HOME`; absolute line indices, ranged
reads capped at `MaxRangeLines`, and any disk failure degrades to memory-only. Emphatically **not**
the session log — that stays `PlainTextLogSink`/`HtmlLogSink`, opt-in and kept),
`TcpTransport` (TLS + IPv6), `TelnetSession` (wraps TelnetNegotiationCore **2.5.3**),
trigger/alias/macro engines + `IntervalScheduler`, plain-text + HTML logging, versioned JSON
config (worlds → characters + shared trigger sets, with migration),
Expand Down
9 changes: 9 additions & 0 deletions src/SharpMUTerm.Core/Configuration/AppConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SharpMUTerm.Core.Text;
using SharpMUTerm.Core.Theming;
using SharpMUTerm.Core.Workspaces;

Expand All @@ -24,6 +25,14 @@ public sealed class AppConfiguration
/// <summary>Maximum scrollback lines retained per session.</summary>
public int ScrollbackLines { get; set; } = 20_000;

/// <summary>
/// How much history beyond <see cref="ScrollbackLines"/> is kept in a per-session disk cache so
/// the view can scroll further back than memory holds. An ephemeral cache, deleted when the
/// session closes — not a session transcript, which is the separate, opt-in
/// <c>PlainTextLogSink</c>/<c>HtmlLogSink</c> feature.
/// </summary>
public ScrollbackSpillOptions ScrollbackSpill { get; set; } = new();

/// <summary>
/// Forces a graphics protocol regardless of capability detection: one of
/// <c>none</c>, <c>halfblock</c>, <c>sixel</c>, <c>kitty</c>. Null means auto-detect.
Expand Down
13 changes: 9 additions & 4 deletions src/SharpMUTerm.Core/Session/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using SharpMUTerm.Core.Configuration;
using SharpMUTerm.Core.Logging;
using SharpMUTerm.Core.Telnet;
using SharpMUTerm.Core.Text;
using SharpMUTerm.Core.Transport;

namespace SharpMUTerm.Core.Session;
Expand Down Expand Up @@ -49,15 +50,17 @@ public WorldSession Open(
int scrollbackCapacity = 20_000,
TextSettings? text = null,
InputSettings? input = null,
Func<ConnectionOptions, ITelnetSession>? sessionFactory = null)
Func<ConnectionOptions, ITelnetSession>? sessionFactory = null,
ScrollbackSpillOptions? spill = null)
{
ArgumentNullException.ThrowIfNull(world);
var session = new WorldSession(
world,
sessionFactory: sessionFactory,
scrollbackCapacity: scrollbackCapacity,
text: text,
input: input);
input: input,
spill: spill);
session.Logger = Logger;
Add(session);
return session;
Expand All @@ -84,7 +87,8 @@ public WorldSession Open(
ILogSink? log = null,
TextSettings? text = null,
InputSettings? input = null,
Func<ConnectionOptions, ITelnetSession>? sessionFactory = null)
Func<ConnectionOptions, ITelnetSession>? sessionFactory = null,
ScrollbackSpillOptions? spill = null)
{
ArgumentNullException.ThrowIfNull(world);
ArgumentNullException.ThrowIfNull(character);
Expand All @@ -97,7 +101,8 @@ public WorldSession Open(
log: log,
scrollbackCapacity: scrollbackCapacity,
text: text,
input: input);
input: input,
spill: spill);
session.Logger = Logger;
Add(session);
return session;
Expand Down
32 changes: 29 additions & 3 deletions src/SharpMUTerm.Core/Session/WorldSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class WorldSession : IAsyncDisposable
private readonly TimerDefinition[] _timers;
private readonly List<IDisposable> _timerHandles = new();
private ITelnetSession? _telnet;
private ILogger _logger = NullLogger.Instance;

/// <summary>
/// Creates a session for a world and (optionally) the character being connected as. Automation
Expand All @@ -55,7 +56,8 @@ public WorldSession(
ILogSink? log = null,
int scrollbackCapacity = 20_000,
TextSettings? text = null,
InputSettings? input = null)
InputSettings? input = null,
ScrollbackSpillOptions? spill = null)
{
World = world ?? throw new ArgumentNullException(nameof(world));
Character = character;
Expand All @@ -67,7 +69,7 @@ public WorldSession(
_emoji = world.Emoji.Enabled
? new EmojiSubstitutor(world.Emoji.Emoticons, world.Emoji.Shortcodes)
: null;
Scrollback = new ScrollbackBuffer(scrollbackCapacity);
Scrollback = new ScrollbackBuffer(scrollbackCapacity, CreateSpill(spill ?? new ScrollbackSpillOptions()));

var sets = triggerSets ?? Array.Empty<TriggerSet>();
Triggers = new TriggerEngine(sets.SelectMany(s => s.Triggers));
Expand All @@ -79,6 +81,16 @@ public WorldSession(
.ToArray();
}

/// <summary>
/// The disk cache behind this session's scrollback, or null when it is memory-only. Created
/// lazily — the directory does not exist until a line is actually evicted from memory — and
/// deleted when the session is disposed. See <see cref="FileScrollbackSpill"/> for why this is a
/// cache and not a transcript: the session <em>log</em> is <see cref="AttachLog"/>'s business and
/// stays opt-in.
/// </summary>
private IScrollbackSpill? CreateSpill(ScrollbackSpillOptions options) =>
options.Enabled ? new FileScrollbackSpill(options, SessionKey) : null;

private static ILineParser CreateParser(ContentFormat format) => format switch
{
ContentFormat.Mxp => new MxpParser(),
Expand All @@ -91,8 +103,21 @@ public WorldSession(
/// <see cref="DefaultSessionFactory"/> hands straight to <see cref="TelnetSession"/>. Defaults to
/// <see cref="NullLogger.Instance"/>, so Core stays free of any logging implementation; the app sets
/// it (via <see cref="SessionManager.Logger"/>) to its client diagnostics pipeline.
/// <para>
/// Assigning it also re-points the <see cref="Scrollback"/> buffer (and its spill cache), which is
/// constructed before the app gets a chance to set this — otherwise a disk fault in the scrollback
/// cache would report itself to <c>NullLogger</c>, i.e. nowhere.
/// </para>
/// </summary>
public ILogger Logger { get; set; } = NullLogger.Instance;
public ILogger Logger
{
get => _logger;
set
{
_logger = value ?? NullLogger.Instance;
Scrollback.Logger = _logger;
}
}

public WorldDefinition World { get; }

Expand Down Expand Up @@ -498,6 +523,7 @@ public async ValueTask DisposeAsync()
StopTimers();
Scheduler.Dispose();
DetachLog(); // flushes what the session logged before closing the file behind it
Scrollback.Dispose(); // deletes this session's scrollback spill cache; the log above is kept
if (_telnet is not null)
{
await _telnet.DisposeAsync().ConfigureAwait(false);
Expand Down
Loading
Loading