Skip to content
Draft
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: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ and a release's notes are its section. The procedure is in [`docs/RELEASING.md`]

### Changed

- Game output is read by MarkupString 2.27.0 instead of SharpMUTerm's own ANSI, MXP and Pueblo readers. What you
see should not change, with a few exceptions: ` ` is a non-breaking space; an erase-line sequence now erases;
Pueblo pictures show an `[image: …]` label; an MXP picture that would cover text the world wrote shows inline
instead; and a world's link whose address is not a web address (such as one that spells out a command) is shown
as plain text instead of a link that refuses to open.

- The Windows installer and the Linux AppImage are now built with Velopack, which is what lets them update. Windows
installs per user under `%LocalAppData%\SharpMUTerm`, adds the `sharpmuterm` folder to PATH and installs the Symbols
Nerd Font Mono font, with no questions asked. If you installed an earlier version with the old installer, uninstall
Expand Down
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ fallbacks) for inline images/maps.
**M1 delivered, plus substantial M2–M4 work.** `SharpMUTerm.slnx` builds all fifteen projects on
`net10.0`, with the full test suite passing. In place:

- **Core** — `AnsiParser` (SGR 16/256/truecolor), styled-line + `ScrollbackBuffer` model (a capped
- **Core** — `MarkupLineParser` (MarkupString's `Decoder` with `WithVt`, `WithMxp` and `WithPueblo`, folded into
`StyledLine`s), 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**
Expand Down Expand Up @@ -3552,6 +3553,14 @@ markup (`[bold #rrggbb on #rrggbb]…[/]`, `[[`/`]]` escaping, `[link=url]…[/]
already wires one callback to both plugins, so the signal arrives on the version bump.
- **What is still ours**: a server that sends no boundary marker at all — no GA, no EOR, no newline
— is now `PacketPatchProtocol`'s, which infers a boundary from silence.
- **The escape, MXP and Pueblo parsers are MarkupString's now (2026-11-07).** `Text/MarkupLineParser` wraps one
`MarkupString.Decoding.Decoder` per session (`WithVt`, a literal-tab plugin, `WithMxp(MxpOptions { ShowRefusedTags,
Supports })`, `WithPueblo`) and folds each decoded line into `StyledLine`/`StyledSpan`; `Switch(format)` turns
the protocols on and off. `AnsiParser`, `MxpParser`, `PuebloParser`, `ILineParser`, `SgrCodes`, `MxpTagCategory`
and `MxpLineMode` are gone. The notes below about them record why the behaviour is what it is; the behaviour
now lives in MarkupString, and the old parser test suites still run against the adapter. A gap found there is
fixed in MarkupString, not worked around here. Server links outside `UrlPolicy.Safe` (such as a forged
`mux:send:`) are read as text by the decoder, before the click-time http(s) gate.
- **MXP's line-security model is the parser's real job, and the allow-list is chosen deliberately over
a deny-list** (`MxpLineMode`, `MxpTagCategory`, `MxpParser` — the eight `ESC[#z` line tags: OPEN,
SECURE, LOCKED, TEMP SECURE, and LOCK OPEN/SECURE/LOCKED plus RESET). The spec draws its own line in
Expand Down
9 changes: 9 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
<!-- TUI framework: compositor, split layouts, tabs, mouse, Kitty graphics (stable, net10). -->
<PackageVersion Include="SharpConsoleUI" Version="2.7.8" />

<!--
Reading server output: ANSI/VT, MXP and Pueblo into MarkupText, which Core folds into StyledLine
(Text/MarkupLineParser). One version for the family.
-->
<PackageVersion Include="MarkupString" Version="2.27.0" />
<PackageVersion Include="MarkupString.Ansi" Version="2.27.0" />
<PackageVersion Include="MarkupString.Mxp" Version="2.27.0" />
<PackageVersion Include="MarkupString.Pueblo" Version="2.27.0" />

<!-- Telnet negotiation: TELOPT/NAWS/MTTS/EOR/CHARSET/MSSP/GMCP/MSDP/MCCP/MXP negotiation. -->
<PackageVersion Include="TelnetNegotiationCore" Version="4.5.3" />

Expand Down
7 changes: 1 addition & 6 deletions src/SharpMUTerm.Core/Protocols/ChannelCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,7 @@ public static (string Window, IReadOnlyList<StyledLine> Lines)? Read(string json
text = text[..MaxTextLength];
}

var parser = new AnsiParser();
var lines = new List<StyledLine>(parser.Feed(text.Replace("\r", string.Empty, StringComparison.Ordinal)));
if (parser.Flush() is { } last)
{
lines.Add(last);
}
var lines = new List<StyledLine>(MarkupLineParser.ParseAll(text.Replace("\r", string.Empty, StringComparison.Ordinal)));

while (lines.Count > 0 && lines[^1].Text.Trim().Length == 0)
{
Expand Down
70 changes: 70 additions & 0 deletions src/SharpMUTerm.Core/Protocols/MxpClientSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using SharpMUTerm.Core.Telnet;

namespace SharpMUTerm.Core.Protocols;

/// <summary>
/// What this client says about its MXP: its <c>&lt;VERSION&gt;</c> and the <c>&lt;SUPPORT&gt;</c> answer. The telnet
/// layer answers both itself, because the library takes those lines out of the stream before the reader sees them.
/// </summary>
/// <remarks>
/// <b>An honest list, and deliberately not an aspirational one:</b> a SUPPORTS answer is a claim a server acts on, so
/// naming a tag nothing here acts on makes it send markup that shows as nothing. A tag is listed when MarkupString
/// reads it <em>and</em> <see cref="SharpMUTerm.Core.Text.MarkupLineParser"/> does something with what it becomes.
/// </remarks>
public static class MxpClientSupport
{
/// <summary>What this client answers a VERSION request with.</summary>
public const string ClientName = GmcpHandshake.ClientName;

/// <summary>The MXP revision a VERSION reply names.</summary>
public const string MxpRevision = "1.0";

/// <summary>The style-sheet version a VERSION reply names: none is ever in effect.</summary>
public const string StyleVersion = "0";

/// <summary>The client version a VERSION reply names: this build's, as MinVer stamped it.</summary>
public static string ClientVersion => GmcpHandshake.ClientVersion;

/// <summary>The tags this client renders, by name.</summary>
public static IReadOnlyList<string> SupportedTags { get; } =
["b", "i", "u", "s", "h", "color", "font", "send", "a", "br", "image",
"!element", "!entity", "var", "gauge", "stat", "sound", "music", "frame", "dest"];

/// <summary>
/// The attributes acted on, by tag, for a <c>&lt;SUPPORT tag.attr&gt;</c> or <c>&lt;SUPPORT tag.*&gt;</c>. The same
/// honesty rule as <see cref="SupportedTags"/>: <c>image.ismap</c>, read and ignored, is not listed.
/// </summary>
public static IReadOnlyDictionary<string, string[]> SupportedAttributes { get; } =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["color"] = ["fore", "back"],
["send"] = ["href", "hint", "prompt"],
["a"] = ["href", "hint"],
["image"] = ["fname", "url", "t", "w", "h"],
["gauge"] = ["entity", "max", "caption"],
["stat"] = ["entity", "max", "caption"],
["sound"] = ["fname", "v", "l", "p", "t", "u"],
["music"] = ["fname", "v", "l", "c", "t", "u"],
["frame"] = ["name", "action", "title"],
["dest"] = ["name"],
};

/// <summary>The same table as MarkupString takes it.</summary>
internal static IReadOnlyDictionary<string, IReadOnlyList<string>> Supports { get; } =
SupportedTags.ToDictionary(
tag => tag,
tag => (IReadOnlyList<string>)(SupportedAttributes.GetValueOrDefault(tag) ?? []),
StringComparer.OrdinalIgnoreCase);

private static readonly MarkupString.Mxp.Decoding.MxpOptions Options = new() { Supports = Supports };

/// <summary>
/// The answer to a <c>&lt;SUPPORT&gt;</c> that asked about <paramref name="asked"/>: every tag in
/// <see cref="SupportedTags"/> when it asked about nothing, otherwise each entry it named, as one this client
/// renders or one it does not. <c>tag.attr</c> is supported when <see cref="SupportedAttributes"/> lists it, and
/// <c>tag.*</c> is answered with each attribute listed for the tag; a tag this client does not render is answered
/// <c>-tag.*</c>.
/// </summary>
public static (IReadOnlyList<string> Supported, IReadOnlyList<string> Unsupported) AnswerSupport(IReadOnlyList<string> asked) =>
Options.Answer(asked);
}
15 changes: 15 additions & 0 deletions src/SharpMUTerm.Core/Protocols/MxpEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using SharpMUTerm.Core.Text;

namespace SharpMUTerm.Core.Protocols;

/// <summary>A server's entity changed: <c>&lt;!ENTITY&gt;</c>, <c>&lt;VAR&gt;</c> or an element's <c>FLAG="set x"</c>. A null value is a deletion.</summary>
public sealed record MxpEntityChange(string Name, string? Value);

/// <summary>The text of an element whose <c>FLAG</c> says what it is: <c>RoomName</c>, <c>RoomDesc</c>, <c>RoomExit</c>, <c>RoomNum</c>…</summary>
public sealed record MxpFlagText(string Flag, string Text);

/// <summary>A <c>&lt;GAUGE&gt;</c> or <c>&lt;STAT&gt;</c>: show entity <see cref="Entity"/> (out of <see cref="Max"/>) on the status row.</summary>
public sealed record MxpGauge(string Entity, string? Max, string? Caption, bool IsStat);

/// <summary>Text a <c>&lt;DEST&gt;</c>, a redirecting <c>&lt;FRAME&gt;</c> or a Pueblo pane sent to a named window instead of the main one.</summary>
public sealed record MxpRedirect(string Frame, StyledLine Line);
16 changes: 0 additions & 16 deletions src/SharpMUTerm.Core/Protocols/MxpLineMode.cs

This file was deleted.

Loading
Loading