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
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
build-and-test:
name: build & test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- name: Checkout
uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Set up .NET 10 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Restore
run: dotnet restore MuGlyph.slnx

- name: Build
run: dotnet build MuGlyph.slnx -c Release --no-restore

# TUnit runs on Microsoft.Testing.Platform; execute each test project directly
# (the classic `dotnet test`/VSTest path is not used by MTP on .NET 10).
- name: Test — Core
shell: bash
run: dotnet run -c Release --no-build --project tests/MuClient.Core.Tests/MuClient.Core.Tests.csproj

- name: Test — Graphics
shell: bash
run: dotnet run -c Release --no-build --project tests/MuClient.Graphics.Tests/MuClient.Graphics.Tests.csproj

- name: Test — Scripting
shell: bash
run: dotnet run -c Release --no-build --project tests/MuClient.Scripting.Tests/MuClient.Scripting.Tests.csproj
27 changes: 25 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,31 @@ fallbacks) for inline images/maps.

## Repository state

Skeleton only: `README.md`, `LICENSE`, `.gitignore`, `.editorconfig`, `docs/PLAN.md`, this file.
No solution or code exists yet. The next unit of work is **milestone M1** (below).
**M1 delivered, plus substantial M2–M4 work.** `MuGlyph.slnx` builds all seven projects on
`net10.0`; the solution has **195 passing tests**. In place:

- **Core** — `AnsiParser` (SGR 16/256/truecolor), styled-line + `ScrollbackBuffer` model,
`TcpTransport` (TLS + IPv6), `TelnetSession` (wraps TelnetNegotiationCore **2.5.3**),
trigger/alias/macro engines + `IntervalScheduler`, plain-text + HTML logging, JSON config +
BeipMU importer, `Theme`/`ThemeLibrary`, and `WorldSession`/`SessionManager` orchestration.
- **Graphics** — Kitty encoder + Unicode placeholders, Sixel + half-block fallbacks, capability
probe (no UI dependency).
- **Scripting** — sandboxed MoonSharp `ScriptHost` (world/output/trigger/alias/timer/gmcp/log).
- **Tui** — Terminal.Gui v2 app (truecolor `OutputView`, `CommandInput`, theming, key routing).

### Notes for future agents (learned the hard way)
- **.NET 10 SDK**: install via `apt-get install -y dotnet-sdk-10.0` (the Microsoft CDN is often
blocked; Ubuntu's repo works). NuGet (`api.nuget.org`) is reachable.
- **Tests use TUnit**, not xUnit — projects are `Exe` on Microsoft.Testing.Platform. Run them with
`dotnet run --project <testproj>`; `dotnet test` is **not** wired up (MTP opt-in doesn't work on
this SDK).
- **TelnetNegotiationCore 2.5.3** has a fluent builder API (not the 1.0.0 the plan assumed) and now
provides MCCP/MSDP/MXP negotiation itself. `TelnetSession` sets the init-only
`CallbackOnByteAsync` reflectively to get raw data bytes (incl. unterminated prompts) — a
first-class `OnByte` builder hook is a good upstream PR.
- **Terminal.Gui v2** (2.4.x-develop) dropped `Toplevel`/`TabView`; use `IRunnable`/`Window` and
override `OnDrawingContent(DrawContext)`. The static `Application` API is `[Obsolete]` mid-migration
(suppressed via `NoWarn` in the Tui project).

## Architecture rule (non-negotiable)

Expand Down
20 changes: 20 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project>

<!-- Shared settings for every project in the MuGlyph solution. -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<InvariantGlobalization>true</InvariantGlobalization>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<Authors>Harry Cordewener</Authors>
<Product>MuGlyph</Product>
<Copyright>Copyright (c) 2026 Harry Cordewener</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/HarryCordewener/MuGlyph</RepositoryUrl>
</PropertyGroup>

</Project>
25 changes: 25 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project>

<!-- Central package version management for the whole solution. -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<!-- TUI toolkit (v2 is prerelease; API differs completely from v1). -->
<PackageVersion Include="Terminal.Gui" Version="2.4.18-develop.6" />

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

<!-- Sandboxed Lua scripting (pure-managed, no native deps). -->
<PackageVersion Include="MoonSharp" Version="2.0.0" />

<!-- Logging abstractions (TelnetNegotiationCore takes an ILogger). -->
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />

<!-- Test stack: TUnit (source-generated, runs on Microsoft.Testing.Platform). -->
<PackageVersion Include="TUnit" Version="1.61.38" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions MuGlyph.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/MuClient.Core/MuClient.Core.csproj" />
<Project Path="src/MuClient.Graphics/MuClient.Graphics.csproj" />
<Project Path="src/MuClient.Scripting/MuClient.Scripting.csproj" />
<Project Path="src/MuClient.Tui/MuClient.Tui.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MuClient.Core.Tests/MuClient.Core.Tests.csproj" />
<Project Path="tests/MuClient.Graphics.Tests/MuClient.Graphics.Tests.csproj" />
<Project Path="tests/MuClient.Scripting.Tests/MuClient.Scripting.Tests.csproj" />
</Folder>
</Solution>
57 changes: 53 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ GPU-accelerated terminals (Kitty, WezTerm, Ghostty) on **Windows and Linux**.
The goal is [BeipMU](https://beipdev.github.io/BeipMU/)-class feature parity in a terminal-native
client: rich truecolor text, inline graphics, powerful automation, and full MU\* protocol support.

> **Status:** early scaffolding. See [`docs/PLAN.md`](docs/PLAN.md) for the architecture and roadmap.
> **Status:** milestone **M1 delivered** (usable text client foundation) with substantial work
> from M2–M4 in place — automation engines, inline-graphics subsystem, Lua scripting, and theming.
> `MuClient.Core` is fully unit-tested (195 tests across the solution). See
> [`docs/PLAN.md`](docs/PLAN.md) for the full architecture and roadmap.

## Why a TUI

Expand Down Expand Up @@ -37,16 +40,62 @@ HTML logging · GMCP / MSDP / MSSP / MCCP · MXP + Pueblo · Unicode/emoji.
| `MuClient.Graphics` | Kitty graphics protocol, capability probe, Sixel + half-block fallbacks, `GraphicsView` |
| `MuClient.Scripting` | MoonSharp host + scripting API |
| `MuClient.Tui` | Terminal.Gui v2 application (windows, panes, settings, wiring) |
| `*.Tests` | xUnit test projects |
| `*.Tests` | TUnit test projects |

## What works today

- **Transport & telnet** — TCP with optional TLS (`SslStream`) and IPv6, wrapping
[TelnetNegotiationCore] for NAWS/MTTS/CHARSET/EOR/GA/GMCP/MSSP/MSDP/MCCP negotiation.
Prompt (GA/EOR) detection surfaces prompts separately from scrollback.
- **ANSI parser** — incremental SGR parsing: 16-colour, 256-colour, and 24-bit truecolour
(semicolon and colon forms), with the usual rendition attributes; non-SGR CSI/OSC sequences
are recognised and discarded.
- **Scrollback** — bounded, thread-safe styled-line model with change events.
- **Automation** — regex **triggers** (gag / highlight / rewrite / respond / spawn-route /
script), **aliases** (capture-group expansion, multi-command), **macros/keybinds**, and a
recurring/one-shot **timer** scheduler.
- **Logging** — plain-text and styled **HTML** session logs.
- **Config** — a fresh JSON schema plus a best-effort **BeipMU importer**.
- **Inline graphics** — Kitty graphics-protocol encoder (incl. Unicode placeholders), Sixel and
half-block fallbacks, and a capability probe that degrades cleanly when no protocol is present.
- **Scripting** — sandboxed **Lua** (MoonSharp) exposing `world`/`output`/`trigger`/`alias`/
`timer`/`gmcp`/`log`, with hot-reload.
- **Theming** — yazi-style named themes (Dark / Light / Solarized Dark) with a 16-colour palette
override and semantic UI colours, serialised to the config as hex.
- **TUI** — a Terminal.Gui v2 app: truecolor output pane with wrapping/scrollback, command input
with history and tab-completion, status line, and key routing.

## Building

Requires the **.NET 10 SDK**.

```bash
dotnet build MuGlyph.slnx -c Release
```

## Running

```bash
dotnet build
dotnet run --project src/MuClient.Tui -- <host> <port> [--tls] [--insecure] [--name NAME]
muglyph --help # once published
```

Requires the .NET 10 SDK. (Nothing to build yet — projects land in milestone M1.)
In-app: **PgUp/PgDn** scroll · **Up/Down** input history · **Tab** complete · **Ctrl+Q** quit.

## Testing

The test projects use [TUnit], which runs on the Microsoft.Testing.Platform. Run each directly
(the classic `dotnet test`/VSTest path is not used by MTP on .NET 10):

```bash
dotnet run --project tests/MuClient.Core.Tests
dotnet run --project tests/MuClient.Graphics.Tests
dotnet run --project tests/MuClient.Scripting.Tests
```

## License

[MIT](LICENSE) © 2026 Harry Cordewener

[TelnetNegotiationCore]: https://www.nuget.org/packages/TelnetNegotiationCore/
[TUnit]: https://github.com/thomhurst/TUnit
36 changes: 36 additions & 0 deletions examples/example.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- Example MuGlyph Lua script.
-- Loaded per-world; the sandbox exposes: world, output, trigger, alias, timer, gmcp, log.
-- (No io/os.execute/require — scripting is sandboxed.)

output.print("Loaded example.lua for world: " .. world.name)

-- Trigger: callback receives the whole match followed by each capture group.
trigger.add("(%w+) waves at you", function(whole, who)
world.send("wave " .. who)
output.print("Waved back at " .. who)
end)

-- Alias: string form with $1..$9 capture references.
alias.add("^gt (.+)", "grouptell $1")

-- Alias: function form (same call shape as triggers).
alias.add("^hp$", function()
world.send("score")
end)

-- Timer: recurring; returns a handle with :cancel().
local keepalive = timer.every(60000, function()
world.send("") -- blank line keeps some servers from idling us out
end)

-- Timer: one-shot.
timer.after(2000, function()
output.print("Two seconds since load.")
end)

-- GMCP: a handler registered for "Char" also fires for "Char.Vitals", etc.
gmcp.on("Char.Vitals", function(json)
output.print("Vitals update: " .. json)
end)

log.info("example.lua ready")
34 changes: 34 additions & 0 deletions src/MuClient.Core/Automation/Alias.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace MuClient.Core.Automation;

/// <summary>
/// A command alias: when user input matches <see cref="Pattern"/>, it is expanded into one
/// or more commands via <see cref="Substitution"/> (with <c>$1</c>..<c>$9</c> / <c>${name}</c>
/// capture references). Newlines in the substitution produce multiple commands.
/// </summary>
public sealed class Alias
{
private Regex? _compiled;

public string Name { get; init; } = string.Empty;

public required string Pattern { get; init; }

public bool Enabled { get; set; } = true;

public bool CaseSensitive { get; init; }

/// <summary>The expansion template. May contain multiple newline-separated commands.</summary>
public string Substitution { get; init; } = string.Empty;

/// <summary>Optional named script callback invoked instead of / in addition to expansion.</summary>
public string? ScriptCallback { get; init; }

[JsonIgnore]
public Regex Regex => _compiled ??= new Regex(
Pattern,
RegexOptions.Compiled | (CaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase),
AutomationDefaults.RegexMatchTimeout);
}
Loading
Loading