Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 27 additions & 1 deletion src/Sharprompt/Forms/FormBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand Down Expand Up @@ -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;
}
}
54 changes: 54 additions & 0 deletions src/Sharprompt/Internal/ClipboardHelper.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
21 changes: 21 additions & 0 deletions src/Sharprompt/Internal/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}
13 changes: 13 additions & 0 deletions src/Sharprompt/PromptConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;

using Sharprompt.Drivers;
using Sharprompt.Internal;

namespace Sharprompt;

Expand All @@ -20,6 +21,18 @@ public Func<IConsoleDriver> ConsoleDriverFactory
}
}

private Func<string?> _clipboardTextProvider = ClipboardHelper.GetText;

public Func<string?> ClipboardTextProvider
{
get => _clipboardTextProvider;
set
{
ArgumentNullException.ThrowIfNull(value);
_clipboardTextProvider = value;
}
}

public PromptColorSchema ColorSchema { get; } = new();

public PromptSymbols Symbols { get; } = new();
Expand Down
82 changes: 82 additions & 0 deletions tests/Sharprompt.Tests/Forms/FormInteractionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
{
Message = "message"
};

using var form = new InputForm<string>(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<string>
{
Message = "message"
};

using var form = new InputForm<string>(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<string>
{
Message = "message"
};

using var form = new InputForm<string>(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<string>
{
Message = "message",
Items = ["apple", "banana", "cherry"]
};

using var form = new SelectForm<string>(options, configuration);

Assert.Equal("cherry", form.Start());
}

[Fact]
public void Escape_ThrowsPromptCanceledException()
{
Expand Down
Loading