diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
index f2a08ee85c..4792ece5a5 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
@@ -691,6 +691,40 @@ public void OnMouseEvent (MouseEventArgs a)
/// If simulates the Ctrl key being pressed.
public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
+ internal char _highSurrogate = '\0';
+
+ internal bool IsValidInput (KeyCode keyCode, out KeyCode result)
+ {
+ result = keyCode;
+
+ if (char.IsHighSurrogate ((char)keyCode))
+ {
+ _highSurrogate = (char)keyCode;
+
+ return false;
+ }
+
+ if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode))
+ {
+ result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value;
+ _highSurrogate = '\0';
+
+ return true;
+ }
+
+ if (char.IsSurrogate ((char)keyCode))
+ {
+ return false;
+ }
+
+ if (_highSurrogate > 0)
+ {
+ _highSurrogate = '\0';
+ }
+
+ return true;
+ }
+
#endregion
private AnsiRequestScheduler? _scheduler;
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
index 48bcb713f5..f8101d5468 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
@@ -924,8 +924,11 @@ internal void ProcessInput ()
k &= ~KeyCode.Space;
}
- OnKeyDown (new Key (k));
- OnKeyUp (new Key (k));
+ if (IsValidInput (k, out k))
+ {
+ OnKeyDown (new (k));
+ OnKeyUp (new (k));
+ }
}
}
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
index 5e9a883d72..3dc0082428 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
@@ -352,8 +352,12 @@ private void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo)
}
KeyCode map = MapKey (consoleKeyInfo);
- OnKeyDown (new Key (map));
- OnKeyUp (new Key (map));
+
+ if (IsValidInput (map, out map))
+ {
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+ }
//OnKeyPressed (new KeyEventArgs (map));
}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
index 2ede00a000..59d75b5817 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
@@ -321,8 +321,11 @@ private void ProcessInput (InputResult inputEvent)
break;
}
- OnKeyDown (new (map));
- OnKeyUp (new (map));
+ if (IsValidInput (map, out map))
+ {
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+ }
break;
case EventType.Mouse:
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
index 1f74c0321e..1fc1eb8424 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
@@ -507,9 +507,12 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent)
break;
}
- // This follows convention in NetDriver
- OnKeyDown (new Key (map));
- OnKeyUp (new Key (map));
+ if (IsValidInput (map, out map))
+ {
+ // This follows convention in NetDriver
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+ }
break;
diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs
index e0d4119aa3..dfcd34e672 100644
--- a/Terminal.Gui/Input/Keyboard/Key.cs
+++ b/Terminal.Gui/Input/Keyboard/Key.cs
@@ -138,6 +138,44 @@ public Key (string str)
KeyCode = key.KeyCode;
}
+ ///
+ /// Constructs a new Key from an integer describing the key.
+ /// It parses the integer as Key by calling the constructor with a char or calls the constructor with a
+ /// KeyCode.
+ ///
+ ///
+ /// Don't rely on passed from to because
+ /// would not return the expected keys from 'a' to 'z'.
+ ///
+ /// The integer describing the key.
+ ///
+ ///
+ public Key (int value)
+ {
+ if (value < 0 || value > RuneExtensions.MaxUnicodeCodePoint)
+ {
+ throw new ArgumentOutOfRangeException (@$"Invalid key value: {value}", nameof (value));
+ }
+
+ if (char.IsSurrogate ((char)value))
+ {
+ throw new ArgumentException (@$"Surrogate key not allowed: {value}", nameof (value));
+ }
+
+ Key key;
+
+ if (((Rune)value).IsBmp)
+ {
+ key = new ((char)value);
+ }
+ else
+ {
+ key = new ((KeyCode)value);
+ }
+
+ KeyCode = key.KeyCode;
+ }
+
///
/// The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers.
/// Useful for determining if a key represents is a printable character.
@@ -388,6 +426,11 @@ public static Rune ToRune (KeyCode key)
///
public static implicit operator Key (string str) { return new (str); }
+ /// Cast to a .
+ /// See for more information.
+ ///
+ public static implicit operator Key (int value) { return new (value); }
+
/// Cast a to a .
/// See for more information.
///
@@ -550,7 +593,7 @@ private static string TrimEndSeparator (string input, Rune separator)
// "Ctrl+" (trim)
// "Ctrl++" (trim)
- if (input.Length > 1 && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
+ if (input.Length > 1 && !char.IsHighSurrogate (input [^2]) && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator)
{
return input [..^1];
}
@@ -640,6 +683,13 @@ public static bool TryParse (string text, out Key key)
return false;
}
+ if (text.Length == 2 && char.IsHighSurrogate (text [^2]) && char.IsLowSurrogate (text [^1]))
+ {
+ // It's a surrogate pair and there is no modifiers
+ key = new (new Rune (text [^2], text [^1]).Value);
+ return true;
+ }
+
// e.g. "Ctrl++"
if ((Rune)text [^1] != separator && parts.Any (string.IsNullOrEmpty))
{
diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs
index 6c872d95c4..3d83aaaafb 100644
--- a/Terminal.Gui/Views/TextField.cs
+++ b/Terminal.Gui/Views/TextField.cs
@@ -730,21 +730,11 @@ public virtual void DeleteCharRight ()
/// Use the previous cursor position.
public void InsertText (string toAdd, bool useOldCursorPos = true)
{
- foreach (char ch in toAdd)
+ foreach (Rune rune in toAdd.EnumerateRunes ())
{
- Key key;
-
- try
- {
- key = ch;
- }
- catch (Exception)
- {
- throw new ArgumentException (
- $"Cannot insert character '{ch}' because it does not map to a Key"
- );
- }
-
+ // All rune can be mapped to a Key and no exception will throw here because
+ // EnumerateRunes will replace a surrogate char with the Rune.ReplacementChar
+ Key key = rune.Value;
InsertText (key, useOldCursorPos);
}
}
@@ -1114,7 +1104,7 @@ public virtual void Paste ()
TextModel.SetCol (ref col, Viewport.Width - 1, cols);
}
- int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0);
+ int pos = col - ScrollOffset + Math.Min (Viewport.X, 0);
Move (pos, 0);
return new Point (pos, 0);
diff --git a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
index fbede01b02..de29652781 100644
--- a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
+++ b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
@@ -283,4 +283,115 @@ public void TerminalResized_Simulation (Type driverType)
// Application.Run (win);
// Application.Shutdown ();
// }
+
+ [Theory]
+ [InlineData ('\ud83d', '\udcc4')] // This seems right sequence but Stack is LIFO
+ [InlineData ('\ud83d', '\ud83d')]
+ [InlineData ('\udcc4', '\udcc4')]
+ public void FakeDriver_IsValidInput_Wrong_Surrogate_Sequence (char c1, char c2)
+ {
+ var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver));
+ Application.Init (driver);
+
+ Stack mKeys = new (
+ [
+ new ('a', ConsoleKey.A, false, false, false),
+ new (c1, ConsoleKey.None, false, false, false),
+ new (c2, ConsoleKey.None, false, false, false)
+ ]);
+
+ Console.MockKeyPresses = mKeys;
+
+ Toplevel top = new ();
+ var view = new View { CanFocus = true };
+ var rText = "";
+ var idx = 0;
+
+ view.KeyDown += (s, e) =>
+ {
+ Assert.Equal (new ('a'), e.AsRune);
+ Assert.Equal ("a", e.AsRune.ToString ());
+ rText += e.AsRune;
+ e.Handled = true;
+ idx++;
+ };
+ top.Add (view);
+
+ Application.Iteration += (s, a) =>
+ {
+ if (mKeys.Count == 0)
+ {
+ Application.RequestStop ();
+ }
+ };
+
+ Application.Run (top);
+
+ Assert.Equal ("a", rText);
+ Assert.Equal (1, idx);
+ Assert.Equal (0, ((FakeDriver)driver)._highSurrogate);
+
+ top.Dispose ();
+
+ // Shutdown must be called to safely clean up Application if Init has been called
+ Application.Shutdown ();
+ }
+
+ [Fact]
+ public void FakeDriver_IsValidInput_Correct_Surrogate_Sequence ()
+ {
+ var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver));
+ Application.Init (driver);
+
+ Stack mKeys = new (
+ [
+ new ('a', ConsoleKey.A, false, false, false),
+ new ('\udcc4', ConsoleKey.None, false, false, false),
+ new ('\ud83d', ConsoleKey.None, false, false, false)
+ ]);
+
+ Console.MockKeyPresses = mKeys;
+
+ Toplevel top = new ();
+ var view = new View { CanFocus = true };
+ var rText = "";
+ var idx = 0;
+
+ view.KeyDown += (s, e) =>
+ {
+ if (idx == 0)
+ {
+ Assert.Equal (new (0x1F4C4), e.AsRune);
+ Assert.Equal ("📄", e.AsRune.ToString ());
+ }
+ else
+ {
+ Assert.Equal (new ('a'), e.AsRune);
+ Assert.Equal ("a", e.AsRune.ToString ());
+ }
+
+ rText += e.AsRune;
+ e.Handled = true;
+ idx++;
+ };
+ top.Add (view);
+
+ Application.Iteration += (s, a) =>
+ {
+ if (mKeys.Count == 0)
+ {
+ Application.RequestStop ();
+ }
+ };
+
+ Application.Run (top);
+
+ Assert.Equal ("📄a", rText);
+ Assert.Equal (2, idx);
+
+ top.Dispose ();
+
+ // Shutdown must be called to safely clean up Application if Init has been called
+ Application.Shutdown ();
+ }
}
diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs
index 14c84a282f..11d8ee92c2 100644
--- a/Tests/UnitTests/Views/TextFieldTests.cs
+++ b/Tests/UnitTests/Views/TextFieldTests.cs
@@ -2178,4 +2178,33 @@ public void Draw_Esc_Rune ()
tf.Dispose ();
}
+
+ [Fact]
+ public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair ()
+ {
+ var tf = new TextField ();
+
+ //📄 == \ud83d\udcc4 == \U0001F4C4
+ // � == Rune.ReplacementChar
+ tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d");
+ Assert.Equal ("aA,;📄📄��", tf.Text);
+ }
+
+ [Fact]
+ public void PositionCursor_Respect_GetColumns ()
+ {
+ var tf = new TextField { Width = 5 };
+ tf.BeginInit ();
+ tf.EndInit ();
+
+ tf.NewKeyDownEvent (new ("📄"));
+ Assert.Equal (1, tf.CursorPosition);
+ Assert.Equal (new (2, 0), tf.PositionCursor ());
+ Assert.Equal ("📄", tf.Text);
+
+ tf.NewKeyDownEvent (new (KeyCode.A));
+ Assert.Equal (2, tf.CursorPosition);
+ Assert.Equal (new (3, 0), tf.PositionCursor ());
+ Assert.Equal ("📄a", tf.Text);
+ }
}
diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs
index 0fcb4bead4..26605314a0 100644
--- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs
+++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs
@@ -52,7 +52,8 @@ public class KeyTests
{ "Ctrl-A", Key.A.WithCtrl },
{ "Alt-A", Key.A.WithAlt },
{ "A-Ctrl", Key.A.WithCtrl },
- { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt }
+ { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt },
+ { "📄", (KeyCode)0x1F4C4 }
};
[Theory]
@@ -120,10 +121,13 @@ public void AsRune_ShouldReturnCorrectIntValue (KeyCode key, uint expected)
[InlineData ('\'', (KeyCode)'\'')]
[InlineData ('\xFFFF', (KeyCode)0xFFFF)]
[InlineData ('\x0', (KeyCode)0x0)]
- public void Cast_Char_To_Key (char ch, KeyCode expectedKeyCode)
+ public void Cast_Char_Int_To_Key (char ch, KeyCode expectedKeyCode)
{
var key = (Key)ch;
Assert.Equal (expectedKeyCode, key.KeyCode);
+
+ key = (int)ch;
+ Assert.Equal (expectedKeyCode, key.KeyCode);
}
[Fact]
@@ -140,23 +144,25 @@ public void Cast_Key_ToString ()
[InlineData (KeyCode.A | KeyCode.ShiftMask, KeyCode.A | KeyCode.ShiftMask)]
[InlineData (KeyCode.Z, KeyCode.Z)]
[InlineData (KeyCode.Space, KeyCode.Space)]
- public void Cast_KeyCode_To_Key (KeyCode cdk, KeyCode expected)
+ public void Cast_KeyCode_Int_To_Key (KeyCode cdk, KeyCode expected)
{
- // explicit
+ // KeyCode
var key = (Key)cdk;
Assert.Equal (((Key)expected).ToString (), key.ToString ());
- // implicit
- key = cdk;
+ // Int
+ key = key.AsRune.Value;
Assert.Equal (((Key)expected).ToString (), key.ToString ());
}
// string cast operators
- [Fact]
- public void Cast_String_To_Key ()
+ [Theory]
+ [InlineData ("Ctrl+Q", KeyCode.Q | KeyCode.CtrlMask)]
+ [InlineData ("📄", (KeyCode)0x1F4C4)]
+ public void Cast_String_To_Key (string str, KeyCode expectedKeyCode)
{
- var key = (Key)"Ctrl+Q";
- Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, key.KeyCode);
+ var key = (Key)str;
+ Assert.Equal (expectedKeyCode, key.KeyCode);
}
[Theory]
@@ -190,12 +196,37 @@ public void Casting_Between_Key_And_KeyCode (KeyCode keyCode, KeyCode key)
[InlineData ('\'', (KeyCode)'\'')]
[InlineData ('\xFFFF', (KeyCode)0xFFFF)]
[InlineData ('\x0', (KeyCode)0x0)]
- public void Constructor_Char (char ch, KeyCode expectedKeyCode)
+ public void Constructor_Char_Int (char ch, KeyCode expectedKeyCode)
{
var key = new Key (ch);
Assert.Equal (expectedKeyCode, key.KeyCode);
+
+ key = new ((int)ch);
+ Assert.Equal (expectedKeyCode, key.KeyCode);
+ }
+
+ [Theory]
+ [InlineData (0x1F4C4, (KeyCode)0x1F4C4, "📄")]
+ [InlineData (0x1F64B, (KeyCode)0x1F64B, "🙋")]
+ [InlineData (0x1F46A, (KeyCode)0x1F46A, "👪")]
+ public void Constructor_Int_Non_Bmp (int value, KeyCode expectedKeyCode, string expectedString)
+ {
+ var key = new Key (value);
+ Assert.Equal (expectedKeyCode, key.KeyCode);
+ Assert.Equal (expectedString, key.AsRune.ToString ());
+ Assert.Equal (expectedString, key.ToString ());
}
+ [Theory]
+ [InlineData (-1)]
+ [InlineData (0x11FFFF)]
+ public void Constructor_Int_Invalid_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); }
+
+ [Theory]
+ [InlineData ('\ud83d')]
+ [InlineData ('\udcc4')]
+ public void Constructor_Int_Surrogate_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); }
+
[Fact]
public void Constructor_Default_ShouldSetKeyToNull ()
{