Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
34 changes: 34 additions & 0 deletions Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,40 @@ public void OnMouseEvent (MouseEventArgs a)
/// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param>
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;
Expand Down
7 changes: 5 additions & 2 deletions Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
7 changes: 5 additions & 2 deletions Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
52 changes: 51 additions & 1 deletion Terminal.Gui/Input/Keyboard/Key.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,44 @@ public Key (string str)
KeyCode = key.KeyCode;
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Don't rely on <paramref name="value"/> passed from <see cref="KeyCode.A"/> to <see cref="KeyCode.Z"/> because
/// would not return the expected keys from 'a' to 'z'.
/// </remarks>
/// <param name="value">The integer describing the key.</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentException"></exception>
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;
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -388,6 +426,11 @@ public static Rune ToRune (KeyCode key)
/// <param name="str"></param>
public static implicit operator Key (string str) { return new (str); }

/// <summary>Cast <see langword="int"/> to a <see cref="Key"/>.</summary>
/// <remarks>See <see cref="Key(int)"/> for more information.</remarks>
/// <param name="value"></param>
public static implicit operator Key (int value) { return new (value); }

/// <summary>Cast a <see cref="Key"/> to a <see langword="string"/>.</summary>
/// <remarks>See <see cref="Key(string)"/> for more information.</remarks>
/// <param name="key"></param>
Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -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))
{
Expand Down
20 changes: 5 additions & 15 deletions Terminal.Gui/Views/TextField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -730,21 +730,11 @@ public virtual void DeleteCharRight ()
/// <param name="useOldCursorPos">Use the previous cursor position.</param>
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);
}
}
Expand Down Expand Up @@ -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);
Expand Down
111 changes: 111 additions & 0 deletions Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConsoleKeyInfo> 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<ConsoleKeyInfo> 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 ();
}
}
29 changes: 29 additions & 0 deletions Tests/UnitTests/Views/TextFieldTests.cs
Comment thread
tig marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading