/********************************************************************++ 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