diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index 4f69396fb..e5fbd6e11 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1590,18 +1590,16 @@ namespace Terminal.Gui { if (Top != null) return; - if (!UseSystemConsole) { - var p = Environment.OSVersion.Platform; - if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) - UseSystemConsole = true; - } - //UseSystemConsole = true; + var p = Environment.OSVersion.Platform; + if (UseSystemConsole) Driver = new NetDriver (); + else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + Driver = new WindowsDriver(); else Driver = new CursesDriver (); Driver.Init (TerminalResized); - MainLoop = new Mono.Terminal.MainLoop (Driver is CursesDriver); + MainLoop = new Mono.Terminal.MainLoop (Driver is CursesDriver, UseSystemConsole); SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext (MainLoop)); Top = Toplevel.Create (); Current = Top; diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 17d206d27..aa2e7e7d1 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -1,5 +1,34 @@ -using System; +// +// WindowsDriver.cs: Windows specific driver +// +// Authors: +// Nick Van Dyck (vandyck.nick@outlook.com) +// +// Copyright (c) 2018 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +using System; using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Mono.Terminal; +using NStack; namespace Terminal.Gui { @@ -8,15 +37,131 @@ namespace Terminal.Gui { public const int STD_INPUT_HANDLE = -10; public const int STD_ERROR_HANDLE = -12; - [DllImport ("kernel32.dll", SetLastError = true)] - static extern IntPtr GetStdHandle (int nStdHandle); + IntPtr InputHandle, OutputHandle; - IntPtr inputHandle, outputHandle; + IntPtr ScreenBuffer; public WindowsConsole () { - inputHandle = GetStdHandle (STD_INPUT_HANDLE); - outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); + InputHandle = GetStdHandle (STD_INPUT_HANDLE); + OutputHandle = GetStdHandle (STD_OUTPUT_HANDLE); + } + + public CharInfo[] OriginalStdOutChars; + + public bool WriteToConsole (CharInfo[] charInfoBuffer, Coord coords, SmallRect window) + { + if (ScreenBuffer == IntPtr.Zero) + { + ScreenBuffer = CreateConsoleScreenBuffer ( + DesiredAccess.GenericRead | DesiredAccess.GenericWrite, + ShareMode.FileShareRead | ShareMode.FileShareWrite, + IntPtr.Zero, + 1, + IntPtr.Zero + ); + + var err = Marshal.GetLastWin32Error (); + + if (err != 0) + { + throw new System.ComponentModel.Win32Exception(err); + } + + if (!SetConsoleActiveScreenBuffer (ScreenBuffer)) + { + err = Marshal.GetLastWin32Error(); + throw new System.ComponentModel.Win32Exception(err); + } + + OriginalStdOutChars = new CharInfo[Console.WindowHeight * Console.WindowWidth]; + + ReadConsoleOutput ( + OutputHandle, + OriginalStdOutChars, + coords, + new Coord() { X = 0, Y = 0 }, + ref window + ); + } + + return WriteConsoleOutput ( + ScreenBuffer, + charInfoBuffer, + coords, + new Coord() { X = 0, Y = 0 }, + ref window + ); + } + + public bool SetCursorPosition(Coord position) + { + return SetConsoleCursorPosition (ScreenBuffer, position); + } + + public void PollEvents (Action inputEventHandler) + { + if (OriginalConsoleMode != 0) + return; + + OriginalConsoleMode = ConsoleMode; + + ConsoleMode |= (uint)ConsoleModes.EnableMouseInput; + ConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; + ConsoleMode |= (uint)ConsoleModes.EnableExtendedFlags; + + Task.Run (() => + { + uint numberEventsRead = 0; + uint length = 1; + InputRecord[] records = new InputRecord[length]; + + while ( + ContinueListeningForConsoleEvents && + ReadConsoleInput(InputHandle, records, length, out numberEventsRead) && + numberEventsRead > 0 + ) + { + inputEventHandler (records[0]); + } + }); + } + + public void Cleanup () + { + ContinueListeningForConsoleEvents = false; + ConsoleMode = OriginalConsoleMode; + OriginalConsoleMode = 0; + + if (!SetConsoleActiveScreenBuffer (OutputHandle)) + { + var err = Marshal.GetLastWin32Error (); + Console.WriteLine("Error: {0}", err); + } + } + + private bool ContinueListeningForConsoleEvents = true; + + private uint OriginalConsoleMode = 0; + + public uint ConsoleMode { + get { + uint v; + GetConsoleMode (InputHandle, out v); + return v; + } + + set { + SetConsoleMode (InputHandle, value); + } + } + + [Flags] + public enum ConsoleModes : uint + { + EnableMouseInput = 16, + EnableQuickEditMode = 64, + EnableExtendedFlags = 128, } [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] @@ -160,6 +305,69 @@ namespace Terminal.Gui { } }; + [Flags] + enum ShareMode : uint + { + FileShareRead = 1, + FileShareWrite = 2, + } + + [Flags] + enum DesiredAccess : uint + { + GenericRead = 2147483648, + GenericWrite = 1073741824, + } + + [StructLayout(LayoutKind.Sequential)] + public struct ConsoleScreenBufferInfo + { + public Coord dwSize; + public Coord dwCursorPosition; + public ushort wAttributes; + public SmallRect srWindow; + public Coord dwMaximumWindowSize; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Coord + { + public short X; + public short Y; + + public Coord(short X, short Y) + { + this.X = X; + this.Y = Y; + } + }; + + [StructLayout(LayoutKind.Explicit, CharSet=CharSet.Unicode)] + public struct CharUnion + { + [FieldOffset(0)] public char UnicodeChar; + [FieldOffset(0)] public byte AsciiChar; + } + + [StructLayout(LayoutKind.Explicit, CharSet=CharSet.Unicode)] + public struct CharInfo + { + [FieldOffset(0)] public CharUnion Char; + [FieldOffset(2)] public ushort Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SmallRect + { + public short Left; + public short Top; + public short Right; + public short Bottom; + } + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern IntPtr GetStdHandle (int nStdHandle); + [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] public static extern bool ReadConsoleInput ( IntPtr hConsoleInput, @@ -167,27 +375,446 @@ namespace Terminal.Gui { uint nLength, out uint lpNumberOfEventsRead); + [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] + static extern bool ReadConsoleOutput( + IntPtr hConsoleOutput, + [Out] CharInfo[] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpReadRegion + ); + + [DllImport("kernel32.dll", EntryPoint="WriteConsoleOutput", SetLastError=true, CharSet=CharSet.Unicode)] + static extern bool WriteConsoleOutput( + IntPtr hConsoleOutput, + CharInfo[] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpWriteRegion + ); + + [DllImport ("kernel32.dll")] + static extern bool SetConsoleCursorPosition(IntPtr hConsoleOutput, Coord dwCursorPosition); + [DllImport ("kernel32.dll")] static extern bool GetConsoleMode (IntPtr hConsoleHandle, out uint lpMode); + [DllImport ("kernel32.dll")] static extern bool SetConsoleMode (IntPtr hConsoleHandle, uint dwMode); - public uint ConsoleMode { - get { - uint v; - GetConsoleMode (inputHandle, out v); - return v; - } + [DllImport("kernel32.dll", SetLastError = true)] + static extern IntPtr CreateConsoleScreenBuffer( + DesiredAccess dwDesiredAccess, + ShareMode dwShareMode, + IntPtr secutiryAttributes, + UInt32 flags, + IntPtr screenBufferData + ); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool SetConsoleActiveScreenBuffer(IntPtr Handle); + + } + + internal class WindowsDriver : ConsoleDriver { + + Action TerminalResized; + + WindowsConsole WinConsole; + + WindowsConsole.CharInfo[] OutputBuffer; + + int cols, rows; + + public override int Cols => cols; + + public override int Rows => rows; + + static bool sync; - set { - SetConsoleMode (inputHandle, value); - } - } - } - public class WindowsDriver { public WindowsDriver () { + WinConsole = new WindowsConsole(); + cols = Console.WindowWidth; + rows = Console.WindowHeight - 1; + ResizeScreen (); + UpdateOffScreen (); } + + private WindowsConsole.ButtonState? LastMouseButtonPressed = null; + + private MouseEvent ToDriverMouse(WindowsConsole.MouseEventRecord mouseEvent) + { + MouseFlags mouseFlag = MouseFlags.AllEvents; + + // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. + // This will tell when a mouse button is pressed. When the button is released this event will + // be fired with it's bit set to 0. So when the button is up ButtonState will be 0. + // To map to the correct driver events we save the last pressed mouse button so we can + // map to the correct clicked event. + if (LastMouseButtonPressed != null && mouseEvent.ButtonState != 0) + { + LastMouseButtonPressed = null; + } + + if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed == null) + { + switch (mouseEvent.ButtonState) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Pressed; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Pressed; + break; + + case WindowsConsole.ButtonState.Button3Pressed: + mouseFlag = MouseFlags.Button3Pressed; + break; + } + LastMouseButtonPressed = mouseEvent.ButtonState; + } + else if (mouseEvent.EventFlags == 0 && LastMouseButtonPressed != null) + { + switch (LastMouseButtonPressed) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Clicked; + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Clicked; + break; + + case WindowsConsole.ButtonState.Button3Pressed: + mouseFlag = MouseFlags.Button3Clicked; + break; + } + LastMouseButtonPressed = null; + } + else if(mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) + { + mouseFlag = MouseFlags.ReportMousePosition; + } + + return new MouseEvent () { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y, + Flags = mouseFlag + }; + } + + private ConsoleKeyInfo ToConsoleKeyInfo (WindowsConsole.KeyEventRecord keyEvent) + { + var state = keyEvent.dwControlKeyState; + + bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; + bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; + bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; + + return new ConsoleKeyInfo(keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); + } + + public Key MapKey (ConsoleKeyInfo keyInfo) + { + switch (keyInfo.Key) { + case ConsoleKey.Escape: + return Key.Esc; + case ConsoleKey.Tab: + return Key.Tab; + case ConsoleKey.Home: + return Key.Home; + case ConsoleKey.End: + return Key.End; + case ConsoleKey.LeftArrow: + return Key.CursorLeft; + case ConsoleKey.RightArrow: + return Key.CursorRight; + case ConsoleKey.UpArrow: + return Key.CursorUp; + case ConsoleKey.DownArrow: + return Key.CursorDown; + case ConsoleKey.PageUp: + return Key.PageUp; + case ConsoleKey.PageDown: + return Key.PageDown; + case ConsoleKey.Enter: + return Key.Enter; + case ConsoleKey.Spacebar: + return Key.Space; + case ConsoleKey.Backspace: + return Key.Backspace; + case ConsoleKey.Delete: + return Key.DeleteChar; + + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + return (Key)((uint)keyInfo.KeyChar); + } + + var key = keyInfo.Key; + if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + var delta = key - ConsoleKey.A; + if (keyInfo.Modifiers == ConsoleModifiers.Control) + return (Key)((uint)Key.ControlA + delta); + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'A' + delta)); + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + return (Key)((uint)'A' + delta); + else + return (Key)((uint)'a' + delta); + } + if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + var delta = key - ConsoleKey.D0; + if (keyInfo.Modifiers == ConsoleModifiers.Alt) + return (Key)(((uint)Key.AltMask) | ((uint)'0' + delta)); + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + return (Key)((uint)keyInfo.KeyChar); + return (Key)((uint)'0' + delta); + } + if (key >= ConsoleKey.F1 && key <= ConsoleKey.F10) { + var delta = key - ConsoleKey.F1; + + return (Key)((int)Key.F1 + delta); + } + return (Key)(0xffffffff); + } + + public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action mouseHandler) + { + WinConsole.PollEvents (inputEvent => + { + switch(inputEvent.EventType) + { + case WindowsConsole.EventType.Key: + if (inputEvent.KeyEvent.bKeyDown == false) + return; + var map = MapKey (ToConsoleKeyInfo (inputEvent.KeyEvent)); + if (map == (Key) 0xffffffff) + return; + keyHandler (new KeyEvent (map)); + break; + + case WindowsConsole.EventType.Mouse: + mouseHandler (ToDriverMouse (inputEvent.MouseEvent)); + break; + + case WindowsConsole.EventType.WindowBufferSize: + cols = inputEvent.WindowBufferSizeEvent.size.X; + rows = inputEvent.WindowBufferSizeEvent.size.Y - 1; + ResizeScreen (); + UpdateOffScreen (); + TerminalResized (); + break; + + } + }); + } + + public override void Init (Action terminalResized) + { + TerminalResized = terminalResized; + + Colors.Base = new ColorScheme (); + Colors.Dialog = new ColorScheme (); + Colors.Menu = new ColorScheme (); + Colors.Error = new ColorScheme (); + + HLine = '\u2500'; + VLine = '\u2502'; + Stipple = '\u2592'; + Diamond = '\u25c6'; + ULCorner = '\u250C'; + LLCorner = '\u2514'; + URCorner = '\u2510'; + LRCorner = '\u2518'; + LeftTee = '\u251c'; + RightTee = '\u2524'; + TopTee = '\u22a4'; + BottomTee = '\u22a5'; + + Colors.Base.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Blue); + Colors.Base.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); + Colors.Base.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Blue); + Colors.Base.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); + + Colors.Menu.HotFocus = MakeColor (ConsoleColor.Yellow, ConsoleColor.Black); + Colors.Menu.Focus = MakeColor (ConsoleColor.White, ConsoleColor.Black); + Colors.Menu.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Cyan); + Colors.Menu.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Cyan); + + Colors.Dialog.Normal = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Dialog.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Cyan); + Colors.Dialog.HotNormal = MakeColor (ConsoleColor.Blue, ConsoleColor.Gray); + Colors.Dialog.HotFocus = MakeColor (ConsoleColor.Blue, ConsoleColor.Cyan); + + Colors.Error.Normal = MakeColor (ConsoleColor.White, ConsoleColor.Red); + Colors.Error.Focus = MakeColor (ConsoleColor.Black, ConsoleColor.Gray); + Colors.Error.HotNormal = MakeColor (ConsoleColor.Yellow, ConsoleColor.Red); + Colors.Error.HotFocus = Colors.Error.HotNormal; + Console.Clear (); + } + + void ResizeScreen () + { + OutputBuffer = new WindowsConsole.CharInfo[Rows * Cols]; + Clip = new Rect (0, 0, Cols, Rows); + } + + void UpdateOffScreen () + { + for (int row = 0; row < rows; row++) + for (int col = 0; col < cols; col++) + { + int position = row * cols + col; + OutputBuffer[position].Attributes = (ushort)MakeColor(ConsoleColor.White, ConsoleColor.Blue); + OutputBuffer[position].Char.UnicodeChar = ' '; + } + } + + int ccol, crow; + public override void Move (int col, int row) + { + ccol = col; + crow = row; + } + + public override void AddRune (Rune rune) + { + var position = crow * Cols + ccol; + + if (Clip.Contains (ccol, crow)) + { + OutputBuffer[position].Attributes = (ushort)currentAttribute; + OutputBuffer[position].Char.UnicodeChar = (char)rune; + } + + ccol++; + if (ccol == Cols) { + ccol = 0; + if (crow + 1 < Rows) + crow++; + } + if (sync) + UpdateScreen (); + } + + public override void AddStr (ustring str) + { + foreach (var rune in str) + AddRune (rune); + } + + int currentAttribute; + public override void SetAttribute (Attribute c) + { + currentAttribute = c.value; + } + + private Attribute MakeColor (ConsoleColor f, ConsoleColor b) + { + // Encode the colors into the int value. + return new Attribute () + { + value = ((int)f | (int)b << 4) + }; + } + + public override void Refresh() + { + var bufferCoords = new WindowsConsole.Coord () + { + X = (short)Clip.Width, + Y = (short)Clip.Height + }; + + var window = new WindowsConsole.SmallRect () + { + Top = 0, + Left = 0, + Right = (short)Clip.Right, + Bottom = (short)Clip.Bottom + }; + + UpdateCursor(); + WinConsole.WriteToConsole (OutputBuffer, bufferCoords, window); + } + + public override void UpdateScreen () + { + var bufferCoords = new WindowsConsole.Coord () + { + X = (short)Clip.Width, + Y = (short)Clip.Height + }; + + var window = new WindowsConsole.SmallRect () + { + Top = 0, + Left = 0, + Right = (short)Clip.Right, + Bottom = (short)Clip.Bottom + }; + + UpdateCursor(); + WinConsole.WriteToConsole (OutputBuffer, bufferCoords, window); + } + + public override void UpdateCursor() + { + var position = new WindowsConsole.Coord() + { + X = (short)ccol, + Y = (short)crow + }; + WinConsole.SetCursorPosition(position); + } + public override void End () + { + WinConsole.Cleanup(); + } + + #region Unused + public override void SetColors (ConsoleColor foreground, ConsoleColor background) + { + } + + public override void SetColors (short foregroundColorId, short backgroundColorId) + { + } + + public override void Suspend () + { + } + + public override void StartReportingMouseMoves () + { + } + + public override void StopReportingMouseMoves () + { + } + + public override void UncookMouse () + { + } + + public override void CookMouse () + { + } + #endregion } } diff --git a/Terminal.Gui/MonoCurses/mainloop.cs b/Terminal.Gui/MonoCurses/mainloop.cs index 7af17db46..771b889e9 100644 --- a/Terminal.Gui/MonoCurses/mainloop.cs +++ b/Terminal.Gui/MonoCurses/mainloop.cs @@ -119,7 +119,7 @@ namespace Mono.Terminal { { } - public MainLoop (bool useUnix) + public MainLoop (bool useUnix, bool useNet=false) { this.useUnix = useUnix; if (useUnix) { @@ -128,7 +128,7 @@ namespace Mono.Terminal { read (wakeupPipes [0], ignore, (IntPtr)1); return true; }); - } else { + } else if (useNet) { Thread readThread = new Thread (WindowsKeyReader); readThread.Start (); }