Terminal: measure characters with Unicode 11 widths, so an emoji takes the two columns the remote counts
ci / build and test (push) Successful in 3m37s
ci / android head (push) Successful in 3m45s
ci / api image (push) Successful in 8s
ci / desktop nightly (push) Successful in 1m44s

This commit is contained in:
jaap-jan committed 2026-10-04 13:55:20 +02:00
1 parent ad1a129159
commit 6806e542f9
9 files changed
+136 -6

No files matched your search

@@ -203,12 +203,22 @@ function FakeTerminal(options) {
this.focuses = 0;
this.disposed = false;
this.pane = null;
this.addons = [];
// The proposed unicode API, at xterm's own default, so a test can see which table the page picked.
this.unicode = { activeVersion: '6' };
terminals.push(this);
}
FakeTerminal.prototype.loadAddon = function () {};
FakeTerminal.prototype.open = function (pane) { this.pane = pane; };
FakeTerminal.prototype.loadAddon = function (addon) { this.addons.push(addon); };
FakeTerminal.prototype.open = function (pane) {
this.pane = pane;
// Taken at open() because that is when the first frame is laid out: a table chosen afterwards would leave
// whatever was drawn first measured with the old one.
this.unicodeAtOpen = this.unicode.activeVersion;
};
FakeTerminal.prototype.attachCustomKeyEventHandler = function (handler) { this.keys = handler; };
FakeTerminal.prototype.onData = function (handler) { this.data = handler; };
FakeTerminal.prototype.onResize = function (handler) { this.resized = handler; };
@@ -242,6 +252,12 @@ var FitAddon = {
},
};
var Unicode11Addon = {
Unicode11Addon: function () {
this.kind = 'unicode11';
},
};
var WebglAddon = {
WebglAddon: function () {
this.onContextLoss = function () {};
@@ -489,6 +505,16 @@ function typed(sessionId) {
return terminalFor(sessionId).inputs.join('|');
}
/** Which character-width table the pane was opened with. */
function unicodeAtOpen(sessionId) {
return terminalFor(sessionId).unicodeAtOpen;
}
/** Whether that pane loaded the Unicode 11 addon. */
function hasUnicode11(sessionId) {
return terminalFor(sessionId).addons.some(function (addon) { return addon.kind === 'unicode11'; });
}
/** How many times that pane has been asked for the caret. */
function focuses(sessionId) {
return terminalFor(sessionId).focuses;
@@ -94,6 +94,14 @@ internal sealed class RendererPage
public string Typed(uint sessionId) =>
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"typed({sessionId})")).AsString();
/// <summary>Which character-width table that pane was opened with.</summary>
public string UnicodeAtOpen(uint sessionId) =>
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"unicodeAtOpen({sessionId})")).AsString();
/// <summary>Whether that pane loaded the Unicode 11 addon.</summary>
public bool HasUnicode11(uint sessionId) =>
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"hasUnicode11({sessionId})")).AsBoolean();
/// <summary>How many times that session's pane has been asked for the caret.</summary>
public int Focuses(uint sessionId) =>
(int)engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"focuses({sessionId})")).AsNumber();
@@ -0,0 +1,43 @@
namespace DodoSSH.Client.Terminal.Tests;
/// <summary>
/// The renderer page measures characters with Unicode 11's widths, so an emoji takes the two columns the
/// remote counts for it.
/// </summary>
/// <remarks>
/// What is asserted is that the page asks for the table, and asks before the first frame. Whether xterm then
/// really draws 😀 across two cells is xterm's business, and the fake terminal in the harness is a stub with
/// a memory, not an emulator; <c>docs/manual-checks.md</c> 1.13 is where a person looks at the real one.
/// </remarks>
public sealed class RendererUnicodeTests
{
[Fact]
public void APane_LoadsTheUnicode11Addon()
{
var page = Opened();
page.HasUnicode11(7).ShouldBeTrue();
}
/// <remarks>
/// Before open() and not after it: a table chosen once the pane is open leaves whatever it drew first
/// measured with Unicode 6, which is the drift this exists to stop.
/// </remarks>
[Fact]
public void APane_IsOpenedWithUnicode11WidthsAlreadyActive()
{
var page = Opened();
page.UnicodeAtOpen(7).ShouldBe("11");
}
private static RendererPage Opened()
{
var page = RendererPage.Load();
page.Do("accept(0)");
page.Do("openSession(7)");
return page;
}
}