Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
7926382
fix(tui): status row, meters and settings screens at 80 columns
claude Nov 3, 2026
ba27130
feat(tui): /connect opens a world by name or adds one from a host
claude Nov 3, 2026
e330da2
feat(window): bundle JetBrains Mono and Nerd Font symbols
claude Nov 4, 2026
d86a892
fix(tui): keep the close key in a narrow header; changelog
claude Nov 4, 2026
1c96079
Add the layout model: saved layouts, resolution, suggestion and panel…
claude Nov 4, 2026
d4ed400
Merge branch 'worktree-agent-a94c8d2b52f34c075' into claude/project-t…
claude Nov 4, 2026
dc332af
Add SharpMUTerm.Client with a contribution registry; ⌃P reads its com…
claude Nov 4, 2026
4c7b3da
Merge branch 'worktree-agent-a8e308caa73918324' into claude/project-t…
claude Nov 4, 2026
266eb2e
Add the trigger builder: a rule from selected output, plain-language …
claude Nov 4, 2026
ab388bd
Settings screens follow the theme; pane tints re-picked for colour-bl…
claude Nov 4, 2026
2ab2dc4
Merge branch 'worktree-agent-a2db8ef28857e2cd5' into claude/project-t…
claude Nov 4, 2026
a59445a
Discoverability: fuzzy ⌃P with recents, a key column, and F4 clash wa…
claude Nov 4, 2026
a5e329c
Wire layouts into the client: panes per panel, Alt+M list, F5 rows, s…
claude Nov 4, 2026
928a3d1
Merge branch 'worktree-agent-a3bcea835117f9e38' into claude/project-t…
claude Nov 4, 2026
0137731
Settings, rebuilt (first pass): declared F7/F8, search, changed marks…
claude Nov 4, 2026
3ddbbef
Merge branch 'worktree-agent-a71e03acf6c7e8a9d' into claude/project-t…
claude Nov 4, 2026
2fc7f4e
Add the who-here, vitals, quick bar and docked composer panels
claude Nov 4, 2026
3ddfa3d
Merge branch 'worktree-agent-a702d77f2e905aa18' into claude/project-t…
claude Nov 4, 2026
b246c8c
Deliver headless MXP pictures on the driving thread, not the pool
claude Nov 4, 2026
82ba823
Desktop window: native Layouts button, vitals gauges and map over the…
claude Nov 4, 2026
5afb6bb
Split a scrolled-back pane, and join lines a server hard-wrapped
claude Nov 4, 2026
f39a967
Size strip panels to their content and keep panels off the rail
claude Nov 4, 2026
2f4874c
Merge branch 'worktree-agent-a4a491eaa46513742' into claude/project-t…
claude Nov 4, 2026
c5e73d5
Merge branch 'worktree-agent-a55144d979f540e6c' into claude/project-t…
claude Nov 4, 2026
1202ada
Open a first run on a welcome, and leave a key tour after its first c…
claude Nov 4, 2026
3d2091c
Merge branch 'worktree-agent-adf5458a0396c1fb8' into claude/project-t…
claude Nov 4, 2026
f224898
Drop the divider row a tab strip or a strip panel already marks
claude Nov 4, 2026
208ae65
Merge branch 'worktree-agent-a4f3865c6fa217802' into claude/project-t…
claude Nov 4, 2026
9212ea2
Desktop window: native settings window for F7 and F8
claude Nov 4, 2026
9cb5646
Merge branch 'worktree-agent-a47495d255fba8445' into claude/project-t…
claude Nov 4, 2026
cba6e34
Describe the header and sidebar as models and draw them natively in t…
claude Nov 4, 2026
69d3d28
Merge branch 'worktree-agent-a753f9a41eb7dce88' into claude/project-t…
claude Nov 4, 2026
db0040f
Draw who-here and the quick bar natively in the desktop window
claude Nov 4, 2026
bb87d99
Merge branch 'worktree-agent-a1693cc6084640f56' into claude/project-t…
claude Nov 4, 2026
7665670
Open a native command palette on Ctrl+P in the desktop window
claude Nov 4, 2026
f82e23a
WIP native layout
claude Nov 4, 2026
9f15db4
Merge branch 'claude/project-thread-izbu0c' into worktree-agent-ad463…
claude Nov 4, 2026
71824ce
Give the desktop header and sidebar room of their own beside the cells
claude Nov 4, 2026
dad8be5
Surface catalog: one declaration per surface, held to both hosts
claude Nov 4, 2026
c605286
Narrow layout below 100 columns, and mode-aware key hints on the stat…
claude Nov 4, 2026
0f538f3
Notices as toasts in the window; area colours, stairs and a legend on…
claude Nov 4, 2026
20c5dc3
Merge branch 'worktree-agent-ac2831380ef20242c' into claude/project-t…
claude Nov 4, 2026
3c9676d
Theme picker in both hosts; F5 and F9 declared with scoped bindings
claude Nov 4, 2026
5ea7669
Merge branch 'worktree-agent-aebad6fb240222003' into claude/project-t…
claude Nov 4, 2026
9a73112
Theme switch repaints history; sidebar keys, scrolling and row menus
claude Nov 4, 2026
ee67930
feat(window): the composer is a native editor in the desktop window
claude Nov 4, 2026
6ff7171
Declare F2, F3, F4 and F6 as list sections, with a native master-deta…
claude Nov 4, 2026
97bfa46
Merge claude/project-thread-izbu0c (theme repaint, Alt+C rail keys)
claude Nov 4, 2026
3672ce8
Merge branch 'claude/project-thread-izbu0c' into worktree-agent-a2f4f…
claude Nov 4, 2026
f4a9fd2
Merge claude/project-thread-izbu0c (native composer)
claude Nov 4, 2026
e9ceb07
feat(packages): declare F10 in Client, native package manager in the …
claude Nov 4, 2026
8f96ec2
Open telnet:// links, keep passwords in the OS store, title the windo…
claude Nov 4, 2026
546544e
feat(window): native quit prompt, search, history, layouts and welcome
claude Nov 4, 2026
4287ec1
Merge branch 'claude/project-thread-izbu0c' into worktree-agent-ad01c…
claude Nov 4, 2026
aee2287
Merge branch 'worktree-agent-a7129bb56d055865d' into claude/project-t…
claude Nov 4, 2026
b010bb5
Merge branch 'claude/project-thread-izbu0c' into worktree-agent-ad01c…
claude Nov 4, 2026
c14aa5c
feat(window): pop native panels out into floating windows
claude Nov 4, 2026
f79f690
Fix ten two-host papercuts, plus settings focus and one key spelling
claude Nov 4, 2026
fe28502
Merge branch 'worktree-agent-a01153639ae4a167c' into claude/project-t…
claude Nov 4, 2026
4b08665
feat(ui): one design language for the window, themed terminal popups
claude Nov 4, 2026
9b59425
UX wave 10: bars fit their pane, one name per character, palette, F4,…
claude Nov 4, 2026
58c7ba9
Merge wave 10 defect batch
claude Nov 4, 2026
f2eca91
Notices, status row, toasts, tab overflow, one-row input, composer le…
claude Nov 4, 2026
930f02c
Merge wave 10 notices, narrow, composer
claude Nov 4, 2026
f3f55e6
Settings resize, toasts, one name spelling, chip click, notice sources
claude Nov 4, 2026
ebd1491
Light-theme contrast, composer spelling, and status/panel polish (UX …
claude Nov 4, 2026
108abc6
Merge wave 12 Light contrast
claude Nov 4, 2026
9ec7b16
feat: auto-reconnect, attention alerts and command stacking
claude Nov 4, 2026
95eeaed
test: notice source assertion follows held notice ink
claude Nov 4, 2026
114370d
Merge branch 'worktree-agent-ae0f8bf4259daec73' into claude/project-t…
claude Nov 4, 2026
6a4c839
Alert titles use the shared name spelling
claude Nov 4, 2026
967e0f5
In-app help, a spelling dictionary on request, and a join-wraps sugge…
claude Nov 4, 2026
c300bc9
Merge wave 12 help, dictionary, join wraps
claude Nov 4, 2026
bcff583
Dictionary notices carry a Spelling source label, not a key
claude Nov 4, 2026
1fbc6de
feat(import): bring worlds and automation over from MUSHclient, Mudle…
claude Nov 4, 2026
8ad3bb3
feat(map): name rooms on the terminal map, and edit maps by hand in b…
claude Nov 4, 2026
b2fba0c
Merge wave 13 map names and editing
claude Nov 4, 2026
aa9883f
Script console, GMCP channel windows, highlighted names and scene logs
claude Nov 4, 2026
d3e9b44
Merge wave 13 script console, channels, names, scene log
claude Nov 4, 2026
f3afd4c
Maps: other levels and areas, drag to move, hand edits kept, find room
claude Nov 4, 2026
d3e6827
Polish wave 14: F5 scroll, Light contrast, [dim] exemption, native ke…
claude Nov 4, 2026
4b755f4
Merge branch 'worktree-agent-a1671deb8cac2d8e5' into claude/project-t…
claude Nov 4, 2026
196fa9e
Alerts reach the desktop on X11 and Windows; page alerts on for new w…
claude Nov 4, 2026
cbec8a4
Fit wave 16: pinned import keys, laddered key rows, named map rooms
claude Nov 4, 2026
5c324a4
Merge branch 'worktree-agent-ac749aeaaaa776e42' into claude/project-t…
claude Nov 4, 2026
0d05be5
UX wave 16: grouped F5 world page, ⌃P groups, scene mark, client rows…
claude Nov 4, 2026
602f363
Merge wave 16 F5 groups, palette groups, scene mark, client rows
claude Nov 4, 2026
f771b94
polish(wave17): settings headers ladder, help blocks, connection rows…
claude Nov 4, 2026
91fa7ad
feat: script editing loop and deeper history search (⌃F ⌥H)
claude Nov 4, 2026
535c021
Merge branch 'worktree-agent-ac7d91b703fbc6ce2' into claude/project-t…
claude Nov 4, 2026
a7e8fa7
fix: nullability of the deep search's held-line sets
claude Nov 4, 2026
37535bd
feat(wave18a): script error line, centred history view, ⌥H offer on ⌃…
claude Nov 4, 2026
38b0f9f
feat(welcome): Browse worlds lists mu*index; a pick fills in the conn…
claude Nov 4, 2026
632ad11
Merge browse worlds (mu*index)
claude Nov 4, 2026
af87acb
feat(wave18b): history view fits 80x24, one count row in Ctrl+F, cons…
claude Nov 4, 2026
ddb74bf
Merge branch 'worktree-agent-a211b2904b87acb4c' into claude/project-t…
claude Nov 4, 2026
8052333
Wave 18: map fit, pop-out hover, palette tidy-up, F5 at 80 columns, g…
claude Nov 4, 2026
cd3f115
Browse worlds: choose among a world's addresses; carry its charset; a…
claude Nov 4, 2026
6e6955f
Merge branch 'worktree-agent-a463f4ab71fb79d1d' into claude/project-t…
claude Nov 4, 2026
788e18d
Give the package link test ten seconds under a loaded test host
claude Nov 4, 2026
26ca3c9
Window: panel headings, docked composer footer, page welcome
claude Nov 4, 2026
47ecfc5
Merge wave 19C: panels and welcome to the mockups
claude Nov 4, 2026
825c4ad
Window: native status footer, command-line field, bundled Inter
claude Nov 4, 2026
9fdd4ea
Merge wave 19B: native command line, status footer, bundled Inter
claude Nov 4, 2026
c02bd74
feat(window): native tab strips and scrolled-back band per pane
claude Nov 4, 2026
73e1668
Merge wave 19A: native tab strips and scrolled-back band
claude Nov 4, 2026
a2308d7
Wave 19 slice E: the window's floating surfaces in the mockup's language
claude Nov 4, 2026
44aa2ca
Merge branch 'worktree-agent-a3def3c244e68bebc' into claude/project-t…
claude Nov 4, 2026
3f4811b
Wave 19 terminal audit: modal padding, freeze bar width, header brand…
claude Nov 4, 2026
1c02d37
Merge branch 'worktree-agent-ae06e973e3f41ab93' into claude/project-t…
claude Nov 4, 2026
c3f3388
Welcome: choose a world's address in the Find a world card
claude Nov 4, 2026
aac2880
Wave 19 D: native settings window to the Desktop-Settings mockup
claude Nov 4, 2026
87881f5
Merge wave 19D: settings window to the Desktop-Settings mockup
claude Nov 4, 2026
8fd4029
Wave 20 C: window polish — two-row native tab strips, pane gutter, pl…
claude Nov 4, 2026
c1d4574
Layouts: built-ins arranged as the mockups, with narrow arrangements
claude Nov 4, 2026
4e43c5a
Wave 20 B: model-first cleanups — status model, toast card, body 13, …
claude Nov 4, 2026
d04dd7c
Wave 21 A: window seams — hairline pane dividers, prompt-hugging comm…
claude Nov 4, 2026
27a7572
Wave 21 B: one spacing scale and device-pixel rules in the window
claude Nov 4, 2026
553d230
Wave 21 C: the Dark theme is the mockups' navy; one selected sidebar …
claude Nov 4, 2026
a419479
Wave 21 D: the window's command line sits in the output's column
claude Nov 4, 2026
bfdd3aa
Wave 22 C: the window's map, header and status row as the mockups dra…
claude Nov 4, 2026
f009b1c
Wave 22 B: the composer's Command and Text fields, in both hosts
claude Nov 4, 2026
8ab365b
Wave 22 A: Combat's quick bar as a strip and the collapsed chat as a bar
claude Nov 4, 2026
281b82a
Wave 23 A: the window's map column uses its space
claude Nov 4, 2026
2f4b678
Wave 23 C: the terminal's cells toward the terminal mockups
claude Nov 4, 2026
c1d8455
Merge branch 'worktree-agent-acdd89b0ab3dd4ed2' into claude/project-t…
claude Nov 4, 2026
7180407
Packages test waits for the review mode, not the window
claude Nov 4, 2026
72aa798
Wave 23 B: the window's Light theme and the settings window toward th…
claude Nov 4, 2026
76df2de
Wave 24 A: a panel's heading band is the stacked boundary; the rail's…
claude Nov 4, 2026
ac5d0c3
Wave 24 C: meters have colours, and a low one is the warning red
claude Nov 4, 2026
9d06e08
Window: no heading band under native panels, so the Compose/Here seam…
claude Nov 4, 2026
e26badb
Wave 24 B: undo and redo of committed settings edits, in both hosts
claude Nov 4, 2026
dddf7fd
Merge branch 'worktree-agent-ac550406794ae0cad' into claude/project-t…
claude Nov 4, 2026
b5af1ae
Scrollback split: half-and-half, history on its own plane, the window…
claude Nov 4, 2026
467d42c
First run: both welcomes drawn to the FirstRun mockups
claude Nov 4, 2026
352cc6c
feat(window): drag a native tab to reorder it or drop it on a pane
claude Nov 4, 2026
8c4119e
fix(window): give the command line's column a width floor
claude Nov 4, 2026
ebd929d
feat(window): every native control is named, ringed and reached by Tab
claude Nov 4, 2026
1e51680
feat(window): map names never cross a line; the vitals card ends at i…
claude Nov 4, 2026
03af574
fix(window): Light history plane, one drag zone, undoable package switch
claude Nov 4, 2026
00800de
feat(window): settings window measured against its mockup; Ctrl+Z aft…
claude Nov 5, 2026
115698a
feat(window): roleplay layout measured against its mockup
claude Nov 5, 2026
a9c71f8
feat(window): combat and first-run measured against their mockups
claude Nov 5, 2026
83ca7e7
Terminal audit against the Terminal mockups (wave 29 slice B)
claude Nov 5, 2026
6ff46ed
feat(window): map shades and the collapsed Chat bar toward Desktop-Co…
claude Nov 5, 2026
ea83835
Terminal map and settings nav measured against their mockups (wave 30…
claude Nov 5, 2026
4a70220
Merge branch 'claude/project-thread-izbu0c' into worktree-agent-a092f…
claude Nov 5, 2026
51830af
feat(window): light text hinting, 2 px map exits, composer chip (wave…
claude Nov 5, 2026
a42baa1
First connect walked end to end in both hosts (wave 31 slice B)
claude Nov 5, 2026
22f549f
test(window): allow Windows' one-row-taller heading capitals
claude Nov 5, 2026
fe470a4
Merge branch 'worktree-agent-a684371dca3914b87' into claude/project-t…
claude Nov 5, 2026
b0a6c14
Fit the first connect on a small terminal (wave 32 slice B)
claude Nov 5, 2026
f37d33d
Window welcome: Esc steps back, Browse goes back, the page fits (wave…
claude Nov 5, 2026
3974f88
Merge branch 'worktree-agent-a6479406d559fecca' into claude/project-t…
claude Nov 5, 2026
f108b08
Keep the added-world notice inside an 80- and 60-column status row
claude Nov 5, 2026
93fc97e
Fit the client's own words to 80- and 60-column rows instead of cutti…
claude Nov 5, 2026
19c2f02
Fit the window to its screen and its pixels at 125% and 150% (wave 33 B)
claude Nov 5, 2026
85d9744
Merge branch 'worktree-agent-a73bab0ab5f7c38f4' into claude/project-t…
claude Nov 5, 2026
b761d86
Draw the window's tabs as the mockup's card and fold the sidebar from…
claude Nov 5, 2026
455e041
Merge branch 'worktree-agent-a581e4e305aa70d70' into claude/project-t…
claude Nov 5, 2026
270ccfe
Fix what the terminal completion audit found in the cells (wave 34 B)
claude Nov 5, 2026
5dcaea1
Window completion audit: sidebar, palette, modals, field, composer
claude Nov 5, 2026
837cf02
Merge branch 'worktree-agent-a189741a86483d769' into claude/project-t…
claude Nov 5, 2026
dc8ecb3
Window: rules meet the pane divider's hairline (wave 35, divider seams)
claude Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ jobs:
shell: bash
run: dotnet run -c Release --no-build --project tests/SharpMUTerm.Core.Tests/SharpMUTerm.Core.Tests.csproj

- name: Test — Client
if: ${{ !cancelled() }}
shell: bash
run: dotnet run -c Release --no-build --project tests/SharpMUTerm.Client.Tests/SharpMUTerm.Client.Tests.csproj

- name: Test — Graphics
if: ${{ !cancelled() }}
shell: bash
Expand Down
654 changes: 654 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

1,904 changes: 1,864 additions & 40 deletions CLAUDE.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<PackageVersion Include="Avalonia" Version="12.1.4" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.4" />
<PackageVersion Include="Avalonia.Themes.Simple" Version="12.1.4" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.4" />
<PackageVersion Include="Avalonia.Headless" Version="12.1.4" />

<!-- PNG encoder for tests that need real image bytes; the sibling of the decoder SharpConsoleUI uses. -->
Expand All @@ -87,6 +88,8 @@
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-j3p4-wp97-rph4" />
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-j9gm-c75j-xc9q" />
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-jjfr-hcj7-qf5w" />
<!-- TIFF fax (T4/T6/MH) tile decoding: unreachable, ImageDecoder configures the WebP module only. -->
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-v76p-62qx-wwq2" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions SharpMUTerm.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<Folder Name="/src/">
<Project Path="src/SharpMUTerm.Audio/SharpMUTerm.Audio.csproj" />
<Project Path="src/SharpMUTerm.Avalonia/SharpMUTerm.Avalonia.csproj" />
<Project Path="src/SharpMUTerm.Client/SharpMUTerm.Client.csproj" />
<Project Path="src/SharpMUTerm.Core/SharpMUTerm.Core.csproj" />
<Project Path="src/SharpMUTerm.Graphics/SharpMUTerm.Graphics.csproj" />
<Project Path="src/SharpMUTerm.Scripting/SharpMUTerm.Scripting.csproj" />
Expand All @@ -10,6 +11,7 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpMUTerm.Avalonia.Tests/SharpMUTerm.Avalonia.Tests.csproj" />
<Project Path="tests/SharpMUTerm.Client.Tests/SharpMUTerm.Client.Tests.csproj" />
<Project Path="tests/SharpMUTerm.Core.Tests/SharpMUTerm.Core.Tests.csproj" />
<Project Path="tests/SharpMUTerm.Graphics.Tests/SharpMUTerm.Graphics.Tests.csproj" />
<Project Path="tests/SharpMUTerm.Scripting.Tests/SharpMUTerm.Scripting.Tests.csproj" />
Expand Down
41 changes: 41 additions & 0 deletions docs/PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,47 @@ dotnet publish src/SharpMUTerm.Avalonia -p:PublishProfile=linux-x64 -o out/linux

Both use `packaging/icons/sharpmuterm.png` / `.ico`; the `.ico` is also each executable's Windows icon.

### `telnet://` links

Both packages can make `telnet://` and `telnets://` links open the window. The program never registers itself;
the package does, and only where the player agreed to it:

- **Linux** — `sharpmuterm.desktop` declares `MimeType=x-scheme-handler/telnet;x-scheme-handler/telnets;` and runs
`sharpmuterm-window --open-link %U`. It takes effect once the desktop integrates the AppImage (AppImageLauncher,
appimaged), or by hand: copy the desktop file to `~/.local/share/applications/`, point its `Exec=` at the AppImage,
and run `xdg-mime default sharpmuterm.desktop x-scheme-handler/telnet x-scheme-handler/telnets`.
- **Windows** — the installer's *Open telnet:// and telnets:// links in SharpMUTerm* task (unchecked by default)
writes `Software\Classes\telnet` and `…\telnets` under `HKA` (the current user in the default per-user install)
with `"…\sharpmuterm-window.exe" --open-link "%1"`. Uninstalling removes both keys.

`--open-link` is why the handler can be trusted with a link: with it on the command line, the client reads the one
argument after it and drops every other (`ClientOptions.NarrowToLink`), so a link holding a quote cannot add
arguments of its own. The link itself is read as server text (`TelnetLink`): host, port and TLS only — no user
name or password, no path, no command. A running client is handed the link over a socket in
`$XDG_RUNTIME_DIR/sharpmuterm/` (or beside `config.json`), and the new launch exits.

### Alerts and the AppUserModelID

The window takes the AppUserModelID `SharpMUTerm.Window` at start (`SetCurrentProcessExplicitAppUserModelID`), and
the installer gives its Start menu and desktop shortcuts the same id and writes
`Software\Classes\AppUserModelId\SharpMUTerm.Window` (`DisplayName`, and `IconUri` pointing at the installed
`sharpmuterm.png`) under `HKA`, removed on uninstall. That key is what Windows needs before it shows a toast under the
id, and its presence is how the window knows it may: an alert then shows a toast (through Windows PowerShell 5.1's
WinRT `ToastNotificationManager`, the text base64-encoded inside the script) as well as flashing the taskbar. A copy
run from the `.zip` has no key and only flashes. The program never writes the key itself. **Built, never run on
Windows**: neither the toast nor the shortcut's id has been seen on a desktop.

Linux needs nothing from the package: `notify-send` and, on X11, the window's urgency hint. macOS uses
`/usr/bin/osascript`, which files the notification under Script Editor and asks once whether to allow it.

### Passwords

Character passwords go to the system's credential store when there is one that answers: Windows Credential Manager
(built in), the Secret Service through libsecret's `secret-tool` on Linux (`libsecret-tools` on Debian and Ubuntu,
`libsecret` on Fedora and Arch — recommend it, do not require it), and the login Keychain through `security` on
macOS. Without one, or for a password it refuses, they stay in `secrets.json` beside `config.json`, as before.
Existing `secrets.json` entries move into the store the first time the client starts with one.

## CI / releases

`.github/workflows/release.yml` runs on `v*` tags (and manual dispatch). For `linux-x64` and `win-x64` it
Expand Down
6 changes: 5 additions & 1 deletion packaging/linux/sharpmuterm.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ Type=Application
Name=SharpMUTerm
GenericName=MU* Client
Comment=A MUSH, MUCK and MUD client
Exec=sharpmuterm-window %U
Exec=sharpmuterm-window --open-link %U
Icon=sharpmuterm
Terminal=false
Categories=Game;RolePlaying;
Keywords=MUD;MUSH;MUCK;MU*;telnet;
StartupWMClass=sharpmuterm-window
# telnet:// and telnets:// links open the client. Registered only when the desktop integrates the AppImage
# (AppImageLauncher, appimaged, or `xdg-mime default sharpmuterm.desktop x-scheme-handler/telnet`); the program never
# registers itself.
MimeType=x-scheme-handler/telnet;x-scheme-handler/telnets;
26 changes: 24 additions & 2 deletions packaging/windows/sharpmuterm.iss
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,37 @@ WizardStyle=modern
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "addtopath"; Description: "Add the install folder to PATH, so sharpmuterm runs from any terminal"; GroupDescription: "Terminal:"
Name: "telnetlinks"; Description: "Open telnet:// and telnets:// links in SharpMUTerm"; GroupDescription: "Links:"; Flags: unchecked

[Files]
Source: "{#PublishDir}\sharpmuterm-window.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#PublishDir}\sharpmuterm.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\LICENSE"; DestDir: "{app}"; DestName: "LICENSE.txt"; Flags: ignoreversion
Source: "..\icons\sharpmuterm.png"; DestDir: "{app}"; Flags: ignoreversion

; AppUserModelID: the window takes the same id at start (WindowsAppId in src/SharpMUTerm.Avalonia/DesktopAlert.cs), so
; the taskbar groups it with this shortcut and an alert's toast is shown under it.
[Icons]
Name: "{autoprograms}\SharpMUTerm"; Filename: "{app}\sharpmuterm-window.exe"
Name: "{autodesktop}\SharpMUTerm"; Filename: "{app}\sharpmuterm-window.exe"; Tasks: desktopicon
Name: "{autoprograms}\SharpMUTerm"; Filename: "{app}\sharpmuterm-window.exe"; AppUserModelID: "SharpMUTerm.Window"
Name: "{autodesktop}\SharpMUTerm"; Filename: "{app}\sharpmuterm-window.exe"; AppUserModelID: "SharpMUTerm.Window"; Tasks: desktopicon

; telnet:// and telnets:// links, for this user (HKA is HKCU in the default per-user install). Only with the task above;
; the program never registers itself. --open-link makes the client read the link and nothing else, so a link holding a
; quote cannot add arguments of its own to the command line Windows builds from this template.
[Registry]
Root: HKA; Subkey: "Software\Classes\telnet"; ValueType: string; ValueName: ""; ValueData: "URL:Telnet Protocol"; Flags: uninsdeletekey; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnet"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnet\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\sharpmuterm-window.exe"",0"; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnet\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\sharpmuterm-window.exe"" --open-link ""%1"""; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnets"; ValueType: string; ValueName: ""; ValueData: "URL:Telnet over TLS Protocol"; Flags: uninsdeletekey; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnets"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnets\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: """{app}\sharpmuterm-window.exe"",0"; Tasks: telnetlinks
Root: HKA; Subkey: "Software\Classes\telnets\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\sharpmuterm-window.exe"" --open-link ""%1"""; Tasks: telnetlinks

; The AppUserModelID's name and icon for toasts. Always written: its presence is what tells the window a toast can be
; shown (a portable copy has none and only flashes the taskbar).
Root: HKA; Subkey: "Software\Classes\AppUserModelId\SharpMUTerm.Window"; ValueType: string; ValueName: "DisplayName"; ValueData: "SharpMUTerm"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\AppUserModelId\SharpMUTerm.Window"; ValueType: string; ValueName: "IconUri"; ValueData: "{app}\sharpmuterm.png"

[Run]
Filename: "{app}\sharpmuterm-window.exe"; Description: "{cm:LaunchProgram,SharpMUTerm}"; Flags: nowait postinstall skipifsilent
Expand Down
4 changes: 4 additions & 0 deletions src/SharpMUTerm.Avalonia/App.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Primitives;
using Avalonia.Themes.Simple;

namespace SharpMUTerm.Avalonia;
Expand All @@ -19,6 +20,9 @@ public override void OnFrameworkInitializationCompleted()
// The client decides when the program ends: closing the window asks it to quit, and it is the
// client's loop ending that shuts the application down.
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;

// A menu's or a tooltip's popup is a top level of its own, so it does not inherit the windows' light hinting.
Control.LoadedEvent.AddClassHandler<PopupRoot>((root, _) => NativeTokens.ApplyTextRendering(root));
desktop.MainWindow = new MainWindow(Options, desktop);
}

Expand Down
65 changes: 64 additions & 1 deletion src/SharpMUTerm.Avalonia/AvaloniaConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ public AvaloniaConsoleDriver(int columns, int rows)
/// <summary>The window's screen-reader regions; the window adds them beside the view.</summary>
public WindowAnnouncer Announcer { get; } = new();

/// <summary>The window's native settings window, which the main window owns (<see cref="SettingsPresenter"/>).</summary>
public SettingsPresenter Settings { get; } = new();

/// <summary>The window's native ⌃P, drawn over everything in the main window (<see cref="PalettePresenter"/>).</summary>
public PalettePresenter Palette { get; } = new();

/// <summary>The window's native F1, drawn over the cells in the main window (<see cref="ComposerPresenter"/>).</summary>
public ComposerPresenter Composer { get; } = new();

/// <summary>
/// The window's native quit prompt, ⌃F, ⌃R, ⌥M and welcome, drawn over everything in the main window, the quit prompt
/// always in front (<see cref="ModalPresenter"/>).
/// </summary>
public ModalPresenter Modals { get; } = new();

/// <summary>The panels popped out into windows of their own (<see cref="ClientHost.FloatingPanels"/>).</summary>
public FloatingPanelPresenter Floating { get; } = new();

/// <summary>The window's title for what is in front, which the main window shows (<see cref="WindowTitleBar"/>).</summary>
public WindowTitleBar TitleBar { get; } = new();

/// <summary>The window's alerts while it is behind another (<see cref="WindowAttention"/>).</summary>
public WindowAttention Attention { get; } = new();

/// <summary>The desktop's file picker, for an import from another client (<see cref="WindowFilePicker"/>).</summary>
public WindowFilePicker FilePicker { get; } = new();

public bool IsRunning { get; private set; }

// ── IConsoleDriver: output ────────────────────────────────────────────────────────────────
Expand All @@ -90,7 +117,19 @@ public void Stop()

public void Flush()
{
Volatile.Write(ref _frame, _grid.Snapshot(_caret));
// The band the client said while this frame was painted (PostBufferPaint runs before the flush, on this thread).
var frame = _grid.Snapshot(_caret);
if (Pointer.Band is { } band && band.Top + band.Rows <= frame.Height)
{
frame = frame.WithBand(band);
}

if (Pointer.Dividers is { Count: > 0 } dividers)
{
frame = frame.WithDividers(dividers);
}

Volatile.Write(ref _frame, frame);
Interlocked.Increment(ref _frames);
FrameReady?.Invoke();
}
Expand Down Expand Up @@ -156,3 +195,27 @@ public void Resize(int columns, int rows)

public void Dispose() => Pictures.Dispose();
}

/// <summary>
/// The window's half of <see cref="IWindowTitle"/>: the client hands it <c>Character@World — SharpMUTerm</c> on its own
/// thread, and the main window, which subscribes, puts it on the title bar and the taskbar entry on Avalonia's.
/// </summary>
internal sealed class WindowTitleBar : IWindowTitle
{
/// <summary>The last title, or null before the client has said one.</summary>
public string? Current { get; private set; }

/// <summary>Raised on the client's thread with each new title.</summary>
public event Action<string>? Changed;

public void Show(string title)
{
Current = title;
Changed?.Invoke(title);
}

/// <summary>A window's title goes with the window; there is nothing to give back.</summary>
public void Restore()
{
}
}
109 changes: 109 additions & 0 deletions src/SharpMUTerm.Avalonia/BundledFonts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Fonts;

namespace SharpMUTerm.Avalonia;

/// <summary>
/// The faces this program carries (<c>Fonts/</c>, embedded): Inter for every native view and JetBrains Mono for game
/// text, registered with Avalonia's font manager so a view naming either finds the bundled face on every OS — before
/// this, "JetBrains Mono" resolved only where it happened to be installed, and the UI face was whatever the platform's
/// default was (Segoe UI, Cantarell, San Francisco), so the window looked different on each.
/// <para>
/// Read from the assembly's manifest resources, the same streams <see cref="CellFonts"/> reads for the cells, so each
/// file is carried once. The bare family names are mapped onto the bundle (<see cref="Options"/>), so a view that says
/// <c>"JetBrains Mono"</c> or <c>"Inter"</c> gets the bundled face and an installed copy cannot replace it.
/// </para>
/// </summary>
/// <para>
/// <b>The faces are made once per process and shared by every collection.</b> A font manager disposes its collections,
/// and the base collection disposes every face it holds; the headless tests set up a fresh application, and so a fresh
/// font manager and collection, for every test, so the faces were read, made and disposed once per test, hundreds of
/// times a run, and now and then one test failed to find Inter. Shared and never disposed, the eight faces are made
/// once; the window has one font manager, so nothing changes there.
/// </para>
internal sealed class BundledFonts : FontCollectionBase, IDisposable
{
private static readonly object Gate = new();
private static GlyphTypeface[]? s_faces;

/// <summary>The collection's key: families in it are named <c>fonts:SharpMUTerm#Inter</c>.</summary>
public static readonly Uri CollectionKey = new("fonts:SharpMUTerm");

/// <summary>The bundled UI face.</summary>
public const string Ui = "fonts:SharpMUTerm#Inter";

/// <summary>The bundled text face, the cells' own.</summary>
public const string Mono = "fonts:SharpMUTerm#JetBrains Mono";

/// <summary>Every face that is registered: the four Inter weights the views use, and JetBrains Mono's four.</summary>
internal static readonly string[] Files =
[
"Inter-Regular.ttf", "Inter-Medium.ttf", "Inter-SemiBold.ttf", "Inter-Bold.ttf",
"JetBrainsMono-Regular.ttf", "JetBrainsMono-Bold.ttf", "JetBrainsMono-Italic.ttf", "JetBrainsMono-BoldItalic.ttf",
];

public override Uri Key => CollectionKey;

/// <summary>How many faces were loaded. Internal for tests: a packaging slip that drops a file shows here.</summary>
internal int Loaded { get; }

public BundledFonts()
{
GlyphTypeface[] shared;
lock (Gate)
{
if (s_faces is null)
{
var faces = new List<GlyphTypeface>();
foreach (var file in Files)
{
using var stream = typeof(BundledFonts).Assembly.GetManifestResourceStream($"Fonts.{file}");
if (stream is not null && TryAddGlyphTypeface(stream, out var face))
{
faces.Add(face);
}
}

s_faces = [.. faces];
Loaded = s_faces.Length;
return;
}

shared = s_faces;
}

foreach (var face in shared)
{
if (TryAddGlyphTypeface(face))
{
Loaded++;
}
}
}

/// <summary>The shared faces outlive any one font manager, so a manager disposing this collection disposes none of them.</summary>
void IDisposable.Dispose()
{
}

/// <summary>
/// Inter as the default family (what <see cref="FontFamily.Default"/> resolves to, so every view that names none
/// is Inter), and the bare names mapped onto the bundle.
/// </summary>
public static FontManagerOptions Options => new()
{
DefaultFamilyName = Ui,
FontFamilyMappings = new Dictionary<string, FontFamily>(StringComparer.OrdinalIgnoreCase)
{
["Inter"] = new FontFamily(Ui),
["JetBrains Mono"] = new FontFamily(Mono),
},
};

/// <summary>Registers the bundle and the options on an app being built: the window's and the headless tests' alike.</summary>
public static AppBuilder Use(AppBuilder builder) =>
builder
.ConfigureFonts(manager => manager.AddFontCollection(new BundledFonts()))
.With(Options);
}
Loading
Loading