Files
PowerShell-PSReadLine/PSReadLine/ScreenCapture.cs
T
Jason Shirk a7af25aa37 Stop dispatching on ConsoleKeyInfo (#831)
There are likely issues introduced by this PR,
but I'm merging for a new Beta so this code gets more testing.

* Introduce PSKeyInfo type alias
* Explicit conversions b/w ConsoleKeyInfo and PSKeyInfo
* Change tests to use actual ConsoleKeyInfo data
* Add fr-FR keyboard layout tests
* Add KeyInfo-ru-RU-windows.json (#839)

All impossible without Shift shortcuts labeled as "Investigate: true"
Alt+Spacebar tracked by windows itself

* Add KeyInfo for Polish (Programmers) layout on Windows and Linux (#870)

On Windows *Alt+Spacebar* is hijacked by Windows
On Linux any *Alt+F1-12* combination was not detected
2019-03-20 16:14:05 -07:00

463 lines
18 KiB
C#

/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.PowerShell.Internal;
namespace Microsoft.PowerShell
{
internal static class ScreenCapture
{
internal struct COORD
{
public short X;
public short Y;
public override string ToString()
{
return String.Format(CultureInfo.InvariantCulture, "{0},{1}", X, Y);
}
}
internal struct CHAR_INFO
{
public ushort UnicodeChar;
public ushort Attributes;
public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background)
{
UnicodeChar = c;
Attributes = (ushort)(((int)background << 4) | (int)foreground);
}
public ConsoleColor ForegroundColor
{
get => (ConsoleColor)(Attributes & 0xf);
set => Attributes = (ushort)((Attributes & 0xfff0) | ((int)value & 0xf));
}
public ConsoleColor BackgroundColor
{
get => (ConsoleColor)((Attributes & 0xf0) >> 4);
set => Attributes = (ushort)((Attributes & 0xff0f) | (((int)value & 0xf) << 4));
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append((char)UnicodeChar);
if (ForegroundColor != Console.ForegroundColor)
sb.AppendFormat(" fg: {0}", ForegroundColor);
if (BackgroundColor != Console.BackgroundColor)
sb.AppendFormat(" bg: {0}", BackgroundColor);
return sb.ToString();
}
public override bool Equals(object obj)
{
if (!(obj is CHAR_INFO))
{
return false;
}
var other = (CHAR_INFO)obj;
return this.UnicodeChar == other.UnicodeChar && this.Attributes == other.Attributes;
}
public override int GetHashCode()
{
return UnicodeChar.GetHashCode() + Attributes.GetHashCode();
}
}
[DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool WriteConsoleOutput(IntPtr consoleOutput, CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT writeRegion);
[DllImport("KERNEL32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ReadConsoleOutput(IntPtr consoleOutput, [Out] CHAR_INFO[] buffer, COORD bufferSize, COORD bufferCoord, ref SMALL_RECT readRegion);
[StructLayout(LayoutKind.Sequential)]
struct COLORREF
{
internal uint ColorDWORD;
internal uint R => ColorDWORD & 0xff;
internal uint G => (ColorDWORD >> 8) & 0xff;
internal uint B => (ColorDWORD >> 16) & 0xff;
}
public struct SMALL_RECT
{
public short Left;
public short Top;
public short Right;
public short Bottom;
public override string ToString()
{
return String.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3}", Left, Top, Right, Bottom);
}
}
[StructLayout(LayoutKind.Sequential)]
struct CONSOLE_SCREEN_BUFFER_INFO_EX
{
internal int cbSize;
internal COORD dwSize;
internal COORD dwCursorPosition;
internal ushort wAttributes;
internal SMALL_RECT srWindow;
internal COORD dwMaximumWindowSize;
internal ushort wPopupAttributes;
internal bool bFullscreenSupported;
internal COLORREF Black;
internal COLORREF DarkBlue;
internal COLORREF DarkGreen;
internal COLORREF DarkCyan;
internal COLORREF DarkRed;
internal COLORREF DarkMagenta;
internal COLORREF DarkYellow;
internal COLORREF Gray;
internal COLORREF DarkGray;
internal COLORREF Blue;
internal COLORREF Green;
internal COLORREF Cyan;
internal COLORREF Red;
internal COLORREF Magenta;
internal COLORREF Yellow;
internal COLORREF White;
}
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetConsoleScreenBufferInfoEx(IntPtr hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO_EX csbe);
internal static CHAR_INFO[] ReadBufferLines(int top, int count, int bufferWidth)
{
var result = new CHAR_INFO[bufferWidth * count];
var handle = PlatformWindows.GetStdHandle((uint) PlatformWindows.StandardHandleId.Output);
var readBufferSize = new COORD {
X = (short)bufferWidth,
Y = (short)count};
var readBufferCoord = new COORD {X = 0, Y = 0};
var readRegion = new SMALL_RECT
{
Top = (short)top,
Left = 0,
Bottom = (short)(top + count),
Right = (short)(bufferWidth - 1)
};
ReadConsoleOutput(handle, result, readBufferSize, readBufferCoord, ref readRegion);
return result;
}
internal static void WriteBufferLines(CHAR_INFO[] buffer, int top, IConsole console)
{
var handle = PlatformWindows.GetStdHandle((uint) PlatformWindows.StandardHandleId.Output);
int bufferWidth = Console.BufferWidth;
int bufferLineCount = buffer.Length / bufferWidth;
var bufferSize = new COORD
{
X = (short) bufferWidth,
Y = (short) bufferLineCount
};
var bufferCoord = new COORD {X = 0, Y = 0};
var bottom = top + bufferLineCount - 1;
var writeRegion = new SMALL_RECT
{
Top = (short) top,
Left = 0,
Bottom = (short) bottom,
Right = (short) (bufferWidth - 1)
};
WriteConsoleOutput(handle, buffer,
bufferSize, bufferCoord, ref writeRegion);
}
internal static void InvertLines(int start, int count, IConsole console)
{
var buffer = ReadBufferLines(start, count, console.BufferWidth);
for (int i = 0; i < buffer.Length; i++)
{
buffer[i].ForegroundColor = (ConsoleColor)((int)buffer[i].ForegroundColor ^ 7);
buffer[i].BackgroundColor = (ConsoleColor)((int)buffer[i].BackgroundColor ^ 7);
}
WriteBufferLines(buffer, start, console);
}
internal const string CmdColorTable = @"
\red0\green0\blue0;
\red0\green0\blue128;
\red0\green128\blue0;
\red0\green128\blue128;
\red128\green0\blue0;
\red128\green0\blue128;
\red128\green128\blue0;
\red192\green192\blue192;
\red128\green128\blue128;
\red0\green0\blue255;
\red0\green255\blue0;
\red0\green255\blue255;
\red255\green0\blue0;
\red255\green0\blue255;
\red255\green255\blue0;
\red255\green255\blue255;
";
internal const string PowerShellColorTable = @"
\red1\green36\blue86;
\red0\green0\blue128;
\red0\green128\blue0;
\red0\green128\blue128;
\red128\green0\blue0;
\red1\green36\blue86;
\red238\green237\blue240;
\red192\green192\blue192;
\red128\green128\blue128;
\red0\green0\blue255;
\red0\green255\blue0;
\red0\green255\blue255;
\red255\green0\blue0;
\red255\green0\blue255;
\red255\green255\blue0;
\red255\green255\blue255;
";
static string GetRTFColorFromColorRef(COLORREF colorref)
{
return String.Concat("\\red", colorref.R.ToString("D"),
"\\green", colorref.G.ToString("D"),
"\\blue", colorref.B.ToString("D"), ";");
}
internal static string GetColorTable(IConsole console)
{
var handle = PlatformWindows.GetStdHandle((uint) PlatformWindows.StandardHandleId.Output);
var csbe = new CONSOLE_SCREEN_BUFFER_INFO_EX
{
cbSize = Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFO_EX))
};
if (GetConsoleScreenBufferInfoEx(handle, ref csbe))
{
return GetRTFColorFromColorRef(csbe.Black) + GetRTFColorFromColorRef(csbe.DarkBlue) + GetRTFColorFromColorRef(csbe.DarkGreen) + GetRTFColorFromColorRef(csbe.DarkCyan) + GetRTFColorFromColorRef(csbe.DarkRed) + GetRTFColorFromColorRef(csbe.DarkMagenta) + GetRTFColorFromColorRef(csbe.DarkYellow) + GetRTFColorFromColorRef(csbe.Gray) + GetRTFColorFromColorRef(csbe.DarkGray) + GetRTFColorFromColorRef(csbe.Blue) + GetRTFColorFromColorRef(csbe.Green) + GetRTFColorFromColorRef(csbe.Cyan) + GetRTFColorFromColorRef(csbe.Red) + GetRTFColorFromColorRef(csbe.Magenta) + GetRTFColorFromColorRef(csbe.Yellow) + GetRTFColorFromColorRef(csbe.White);
}
// A bit of a hack if the above failed - assume PowerShell's color scheme if the
// background color is Magenta, otherwise we assume the default scheme.
return console.BackgroundColor == ConsoleColor.DarkMagenta
? PowerShellColorTable
: CmdColorTable;
}
internal static void DumpScreenToClipboard(int top, int count, IConsole console)
{
var buffer = ReadBufferLines(top, count, console.BufferWidth);
var bufferWidth = console.BufferWidth;
var textBuffer = new StringBuilder(buffer.Length + count);
var rtfBuffer = new StringBuilder();
rtfBuffer.Append(@"{\rtf\ansi{\fonttbl{\f0 Consolas;}}");
var colorTable = GetColorTable(console);
rtfBuffer.AppendFormat(@"{{\colortbl;{0}}}{1}", colorTable, Environment.NewLine);
rtfBuffer.Append(@"\f0 \fs18 ");
var charInfo = buffer[0];
var fgColor = (int)charInfo.ForegroundColor;
var bgColor = (int)charInfo.BackgroundColor;
rtfBuffer.AppendFormat(@"{{\cf{0}\chshdng0\chcbpat{1} ", fgColor + 1, bgColor + 1);
for (int i = 0; i < count; i++)
{
var spaces = 0;
var rtfSpaces = 0;
for (int j = 0; j < bufferWidth; j++)
{
charInfo = buffer[i * bufferWidth + j];
if ((int)charInfo.ForegroundColor != fgColor || (int)charInfo.BackgroundColor != bgColor)
{
if (rtfSpaces > 0)
{
rtfBuffer.Append(' ', rtfSpaces);
rtfSpaces = 0;
}
fgColor = (int)charInfo.ForegroundColor;
bgColor = (int)charInfo.BackgroundColor;
rtfBuffer.AppendFormat(@"}}{{\cf{0}\chshdng0\chcbpat{1} ", fgColor + 1, bgColor + 1);
}
var c = (char)charInfo.UnicodeChar;
if (c == ' ')
{
// Trailing spaces are skipped, we'll add them back if we find a non-space
// before the end of line
++spaces;
++rtfSpaces;
}
else
{
if (spaces > 0)
{
textBuffer.Append(' ', spaces);
spaces = 0;
}
if (rtfSpaces > 0)
{
rtfBuffer.Append(' ', rtfSpaces);
rtfSpaces = 0;
}
textBuffer.Append(c);
switch (c)
{
case '\\': rtfBuffer.Append(@"\\"); break;
case '\t': rtfBuffer.Append(@"\tab"); break;
case '{': rtfBuffer.Append(@"\{"); break;
case '}': rtfBuffer.Append(@"\}"); break;
default: rtfBuffer.Append(c); break;
}
}
}
rtfBuffer.AppendFormat(@"\shading0 \cbpat{0} \par{1}", bgColor + 1, Environment.NewLine);
textBuffer.Append(Environment.NewLine);
}
rtfBuffer.Append("}}");
Clipboard.SetRtf(textBuffer.ToString(), rtfBuffer.ToString());
}
}
public partial class PSConsoleReadLine
{
/// <summary>
/// Start interactive screen capture - up/down arrows select lines, enter copies
/// selected text to clipboard as text and html.
/// </summary>
public static void CaptureScreen(ConsoleKeyInfo? key = null, object arg = null)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Ding();
return;
}
PlatformWindows.CallUsingOurInputMode(CaptureScreenImpl);
}
internal static void CaptureScreenImpl()
{
int selectionTop = _singleton._console.CursorTop;
int selectionHeight = 1;
int currentY = selectionTop;
IConsole console = _singleton._console;
// We'll keep the current selection line (currentY) at least 4 lines
// away from the top or bottom of the window.
const int margin = 5;
bool TooCloseToTop() => (currentY - console.WindowTop) < margin;
bool TooCloseToBottom() => ((console.WindowTop + console.WindowHeight) - currentY) < margin;
void UpdateSelection()
{
// Shift not pressed - unselect current selection
ScreenCapture.InvertLines(selectionTop, selectionHeight, console);
selectionTop = currentY;
selectionHeight = 1;
ScreenCapture.InvertLines(selectionTop, selectionHeight, console);
}
// Current lines starts out selected
ScreenCapture.InvertLines(selectionTop, selectionHeight, console);
bool done = false;
while (!done)
{
var k = ReadKey();
if (k == Keys.K || k == Keys.ucK || k == Keys.UpArrow || k == Keys.ShiftUpArrow)
{
if (TooCloseToTop())
{
ScrollDisplayUpLine();
}
if (currentY > 0)
{
currentY -= 1;
if (k.Shift)
{
if (currentY < selectionTop)
{
// Extend selection up, only invert newly selected line.
ScreenCapture.InvertLines(currentY, 1, console);
selectionTop = currentY;
selectionHeight += 1;
}
else if (currentY >= selectionTop)
{
// Selection shortend 1 line, invert unselected line.
ScreenCapture.InvertLines(currentY + 1, 1, console);
selectionHeight -= 1;
}
}
else
{
UpdateSelection();
}
}
}
else if (k == Keys.J || k == Keys.ucJ || k == Keys.DownArrow || k == Keys.ShiftDownArrow)
{
if (TooCloseToBottom())
ScrollDisplayDownLine();
if (currentY < (console.BufferHeight - 1))
{
currentY += 1;
if (k.Shift)
{
if (currentY == (selectionTop + selectionHeight))
{
// Extend selection down, only invert newly selected line.
ScreenCapture.InvertLines(selectionTop + selectionHeight, 1, console);
selectionHeight += 1;
}
else if (currentY == (selectionTop + 1))
{
// Selection shortend 1 line, invert unselected line.
ScreenCapture.InvertLines(selectionTop, 1, console);
selectionTop = currentY;
selectionHeight -= 1;
}
}
else
{
UpdateSelection();
}
}
}
else if (k == Keys.Enter)
{
ScreenCapture.InvertLines(selectionTop, selectionHeight, console);
ScreenCapture.DumpScreenToClipboard(selectionTop, selectionHeight, console);
ScrollDisplayToCursor();
return;
}
else if (k == Keys.Escape || k == Keys.CtrlC || k == Keys.CtrlG)
{
done = true;
}
else
{
Ding();
}
}
ScreenCapture.InvertLines(selectionTop, selectionHeight, console);
ScrollDisplayToCursor();
}
}
}