diff --git a/src/Sharprompt/Forms/FormBase.cs b/src/Sharprompt/Forms/FormBase.cs index f47892c..7299d20 100644 --- a/src/Sharprompt/Forms/FormBase.cs +++ b/src/Sharprompt/Forms/FormBase.cs @@ -22,7 +22,9 @@ protected FormBase(PromptConfiguration configuration) KeyHandlerMaps = new() { - [new ConsoleKeyBinding(ConsoleKey.Escape)] = HandleEscape + [new ConsoleKeyBinding(ConsoleKey.Escape)] = HandleEscape, + [new ConsoleKeyBinding(ConsoleKey.V, ConsoleModifiers.Control)] = HandlePaste, + [new ConsoleKeyBinding(ConsoleKey.Insert, ConsoleModifiers.Shift)] = HandlePaste }; } @@ -149,4 +151,28 @@ private bool HandleEscape() return true; } + + private bool HandlePaste() + { + // With TreatControlCAsInput enabled, the legacy Windows console no longer + // handles Ctrl+V itself, so the key arrives here as a raw input record. + var text = _configuration.ClipboardTextProvider(); + + if (string.IsNullOrEmpty(text)) + { + return false; + } + + var handled = false; + + foreach (var c in text) + { + if (!char.IsControl(c)) + { + handled |= HandleTextInput(new ConsoleKeyInfo(c, default, false, false, false)); + } + } + + return handled; + } } diff --git a/src/Sharprompt/Internal/ClipboardHelper.cs b/src/Sharprompt/Internal/ClipboardHelper.cs new file mode 100644 index 0000000..0358206 --- /dev/null +++ b/src/Sharprompt/Internal/ClipboardHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.InteropServices; + +namespace Sharprompt.Internal; + +internal static class ClipboardHelper +{ + public static string? GetText() + { + // Only Windows needs to read the clipboard directly: enabling + // TreatControlCAsInput disables the legacy console's own Ctrl+V paste + // handling, so the key arrives as a raw ^V input record. Other platforms + // rely on the terminal emulator to translate paste into key input. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return null; + } + + if (!NativeMethods.IsClipboardFormatAvailable(NativeMethods.CF_UNICODETEXT) || !NativeMethods.OpenClipboard(IntPtr.Zero)) + { + return null; + } + + try + { + var hMem = NativeMethods.GetClipboardData(NativeMethods.CF_UNICODETEXT); + + if (hMem == IntPtr.Zero) + { + return null; + } + + var pointer = NativeMethods.GlobalLock(hMem); + + if (pointer == IntPtr.Zero) + { + return null; + } + + try + { + return Marshal.PtrToStringUni(pointer); + } + finally + { + NativeMethods.GlobalUnlock(hMem); + } + } + finally + { + NativeMethods.CloseClipboard(); + } + } +} diff --git a/src/Sharprompt/Internal/NativeMethods.cs b/src/Sharprompt/Internal/NativeMethods.cs index e0c2217..1673890 100644 --- a/src/Sharprompt/Internal/NativeMethods.cs +++ b/src/Sharprompt/Internal/NativeMethods.cs @@ -11,6 +11,9 @@ internal static class NativeMethods // ReSharper disable once InconsistentNaming public const int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + // ReSharper disable once InconsistentNaming + public const uint CF_UNICODETEXT = 13; + [DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int nStdHandle); @@ -19,4 +22,22 @@ internal static class NativeMethods [DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr hConsoleHandle, int dwMode); + + [DllImport("user32.dll")] + public static extern bool IsClipboardFormatAvailable(uint format); + + [DllImport("user32.dll")] + public static extern bool OpenClipboard(IntPtr hWndNewOwner); + + [DllImport("user32.dll")] + public static extern IntPtr GetClipboardData(uint uFormat); + + [DllImport("user32.dll")] + public static extern bool CloseClipboard(); + + [DllImport("kernel32.dll")] + public static extern IntPtr GlobalLock(IntPtr hMem); + + [DllImport("kernel32.dll")] + public static extern bool GlobalUnlock(IntPtr hMem); } diff --git a/src/Sharprompt/PromptConfiguration.cs b/src/Sharprompt/PromptConfiguration.cs index aef59ca..9846eb5 100644 --- a/src/Sharprompt/PromptConfiguration.cs +++ b/src/Sharprompt/PromptConfiguration.cs @@ -1,6 +1,7 @@ using System; using Sharprompt.Drivers; +using Sharprompt.Internal; namespace Sharprompt; @@ -20,6 +21,18 @@ public Func ConsoleDriverFactory } } + private Func _clipboardTextProvider = ClipboardHelper.GetText; + + public Func ClipboardTextProvider + { + get => _clipboardTextProvider; + set + { + ArgumentNullException.ThrowIfNull(value); + _clipboardTextProvider = value; + } + } + public PromptColorSchema ColorSchema { get; } = new(); public PromptSymbols Symbols { get; } = new(); diff --git a/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs b/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs index 5e39073..e8a58e8 100644 --- a/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs +++ b/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs @@ -199,6 +199,88 @@ public void ConfirmForm_EmptyInputWithDefaultValue_ReturnsDefaultValue() Assert.True(form.Start()); } + [Fact] + public void InputForm_CtrlV_PastesClipboardText() + { + var (driver, configuration) = CreateTestContext(); + + configuration.ClipboardTextProvider = () => "pasted"; + + driver.EnqueueKey(ConsoleKey.V, ConsoleModifiers.Control, '\x16'); + driver.EnqueueEnter(); + + var options = new InputOptions + { + Message = "message" + }; + + using var form = new InputForm(options, configuration); + + Assert.Equal("pasted", form.Start()); + } + + [Fact] + public void InputForm_Paste_StripsControlCharacters() + { + var (driver, configuration) = CreateTestContext(); + + configuration.ClipboardTextProvider = () => "ab\r\ncd"; + + driver.EnqueueKey(ConsoleKey.V, ConsoleModifiers.Control, '\x16'); + driver.EnqueueEnter(); + + var options = new InputOptions + { + Message = "message" + }; + + using var form = new InputForm(options, configuration); + + Assert.Equal("abcd", form.Start()); + } + + [Fact] + public void InputForm_PasteWithEmptyClipboard_KeepsAcceptingInput() + { + var (driver, configuration) = CreateTestContext(); + + configuration.ClipboardTextProvider = () => null; + + driver.EnqueueKey(ConsoleKey.V, ConsoleModifiers.Control, '\x16'); + driver.EnqueueText("ok"); + driver.EnqueueEnter(); + + var options = new InputOptions + { + Message = "message" + }; + + using var form = new InputForm(options, configuration); + + Assert.Equal("ok", form.Start()); + } + + [Fact] + public void SelectForm_ShiftInsert_PastesIntoFilter() + { + var (driver, configuration) = CreateTestContext(); + + configuration.ClipboardTextProvider = () => "che"; + + driver.EnqueueKey(ConsoleKey.Insert, ConsoleModifiers.Shift); + driver.EnqueueEnter(); + + var options = new SelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry"] + }; + + using var form = new SelectForm(options, configuration); + + Assert.Equal("cherry", form.Start()); + } + [Fact] public void Escape_ThrowsPromptCanceledException() {