/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
#if !UNIX
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.PowerShell.Internal;
namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
private static void InvertLines(int start, int count)
{
var buffer = ReadBufferLines(start, count);
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);
}
_singleton._console.WriteBufferLines(buffer, ref start, false);
}
///
/// Start interactive screen capture - up/down arrows select lines, enter copies
/// selected text to clipboard as text and html
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void CaptureScreen(ConsoleKeyInfo? key = null, object arg = null)
{
int selectionTop = _singleton._console.CursorTop;
int selectionHeight = 1;
int currentY = selectionTop;
Internal.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;
Func tooCloseToTop = () => { return (currentY - console.WindowTop) < margin; };
Func tooCloseToBottom = () => { return ((console.WindowTop + console.WindowHeight) - currentY) < margin; };
// Current lines starts out selected
InvertLines(selectionTop, selectionHeight);
bool done = false;
while (!done)
{
var k = ReadKey();
switch (k.Key)
{
case ConsoleKey.K:
case ConsoleKey.UpArrow:
if (tooCloseToTop())
ScrollDisplayUpLine();
if (currentY > 0)
{
currentY -= 1;
if ((k.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift)
{
if (currentY < selectionTop)
{
// Extend selection up, only invert newly selected line.
InvertLines(currentY, 1);
selectionTop = currentY;
selectionHeight += 1;
}
else if (currentY >= selectionTop)
{
// Selection shortend 1 line, invert unselected line.
InvertLines(currentY + 1, 1);
selectionHeight -= 1;
}
break;
}
goto updateSelectionCommon;
}
break;
case ConsoleKey.J:
case ConsoleKey.DownArrow:
if (tooCloseToBottom())
ScrollDisplayDownLine();
if (currentY < (console.BufferHeight - 1))
{
currentY += 1;
if ((k.Modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift)
{
if (currentY == (selectionTop + selectionHeight))
{
// Extend selection down, only invert newly selected line.
InvertLines(selectionTop + selectionHeight, 1);
selectionHeight += 1;
}
else if (currentY == (selectionTop + 1))
{
// Selection shortend 1 line, invert unselected line.
InvertLines(selectionTop, 1);
selectionTop = currentY;
selectionHeight -= 1;
}
break;
}
goto updateSelectionCommon;
}
break;
updateSelectionCommon:
// Shift not pressed - unselect current selection
InvertLines(selectionTop, selectionHeight);
selectionTop = currentY;
selectionHeight = 1;
InvertLines(selectionTop, selectionHeight);
break;
case ConsoleKey.Enter:
InvertLines(selectionTop, selectionHeight);
DumpScreenToClipboard(selectionTop, selectionHeight);
ScrollDisplayToCursor();
return;
case ConsoleKey.Escape:
done = true;
continue;
case ConsoleKey.C:
case ConsoleKey.G:
if (k.Modifiers == ConsoleModifiers.Control)
{
done = true;
continue;
}
Ding();
break;
default:
Ding();
break;
}
}
InvertLines(selectionTop, selectionHeight);
ScrollDisplayToCursor();
}
private 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;
";
private 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;
";
private static string GetRTFColorFromColorRef(NativeMethods.COLORREF colorref)
{
return string.Concat("\\red", colorref.R.ToString("D"),
"\\green", colorref.G.ToString("D"),
"\\blue", colorref.B.ToString("D"), ";");
}
private static string GetColorTable()
{
var handle = NativeMethods.GetStdHandle((uint) StandardHandleId.Output);
var csbe = new NativeMethods.CONSOLE_SCREEN_BUFFER_INFO_EX
{
cbSize = Marshal.SizeOf()
};
if (NativeMethods.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 _singleton._console.BackgroundColor == ConsoleColor.DarkMagenta
? PowerShellColorTable
: CmdColorTable;
}
private static void DumpScreenToClipboard(int top, int count)
{
var buffer = ReadBufferLines(top, count);
var bufferWidth = _singleton._console.BufferWidth;
var textBuffer = new StringBuilder(buffer.Length + count);
var rtfBuffer = new StringBuilder();
rtfBuffer.Append(@"{\rtf\ansi{\fonttbl{\f0 Consolas;}}");
var colorTable = GetColorTable();
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("}}");
#if !CORECLR // TODO: break dependency on Window.Forms w/ p/invokes to clipboard directly, for now, just silently skip the copy.
var dataObject = new System.Windows.Forms.DataObject();
dataObject.SetData(System.Windows.Forms.DataFormats.Text, textBuffer.ToString());
dataObject.SetData(System.Windows.Forms.DataFormats.Rtf, rtfBuffer.ToString());
ExecuteOnSTAThread(() => System.Windows.Forms.Clipboard.SetDataObject(dataObject, copy: true));
#endif
}
}
}
#endif