// // System.ConsoleDriver // // Authors: // Gonzalo Paniagua Javier (gonzalo@ximian.com) // // (C) 2005,2006 Novell, Inc (http://www.novell.com) // Copyright (c) Microsoft. // Copyright 2014 Xamarin Inc // // // 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. // // This code contains the ParameterizedStrings implementation from .NET's // Core System.Console: // https://github.com/dotnet/corefx // src/System.Console/src/System/ConsolePal.Unix.cs // #if !MOBILE // // Defining this writes the output to console.log //#define DEBUG using System.Collections; using System.IO; using System.Text; using System.Runtime.InteropServices; namespace System { class TermInfoDriver : IConsoleDriver { // This points to a variable that is updated by unmanage code on window size changes. unsafe static int *native_terminal_size; // The current size that we believe we have static int terminal_size; //static uint flag = 0xdeadbeef; readonly static string [] locations = { "/usr/share/terminfo", "/etc/terminfo", "/usr/lib/terminfo", "/lib/terminfo" }; TermInfoReader reader; int cursorLeft; int cursorTop; string title = String.Empty; string titleFormat = String.Empty; bool cursorVisible = true; string csrVisible; string csrInvisible; string clear; string bell; string term; StreamReader stdin; CStreamWriter stdout; int windowWidth; int windowHeight; //int windowTop; //int windowLeft; int bufferHeight; int bufferWidth; char [] buffer; int readpos; int writepos; string keypadXmit, keypadLocal; bool controlCAsInput; bool inited; object initLock = new object (); bool initKeys; string origPair; string origColors; string cursorAddress; ConsoleColor fgcolor = ConsoleColor.White; ConsoleColor bgcolor = ConsoleColor.Black; string setfgcolor; string setbgcolor; int maxColors; bool noGetPosition; Hashtable keymap; ByteMatcher rootmap; int rl_startx = -1, rl_starty = -1; byte [] control_characters; // Indexed by ControlCharacters.XXXXXX #if DEBUG StreamWriter logger; #endif static string TryTermInfoDir (string dir, string term ) { string path = String.Format ("{0}/{1:x}/{2}", dir, (int)(term [0]), term); if (File.Exists (path)) return path; path = Path.Combine (dir, term.Substring (0, 1), term); if (File.Exists (path)) return path; return null; } static string SearchTerminfo (string term) { if (term == null || term == String.Empty) return null; string path; string terminfo = Environment.GetEnvironmentVariable ("TERMINFO"); if (terminfo != null && Directory.Exists (terminfo)){ path = TryTermInfoDir (terminfo, term); if (path != null) return path; } foreach (string dir in locations) { if (!Directory.Exists (dir)) continue; path = TryTermInfoDir (dir, term); if (path != null) return path; } return null; } void WriteConsole (string str) { if (str == null) return; stdout.InternalWriteString (str); } public TermInfoDriver () : this (Environment.GetEnvironmentVariable ("TERM")) { } public TermInfoDriver (string term) { #if DEBUG File.Delete ("console.log"); logger = new StreamWriter (File.OpenWrite ("console.log")); #endif this.term = term; string filename = SearchTerminfo (term); if (filename != null) reader = new TermInfoReader (term, filename); else { // fallbacks if (term == "xterm") { reader = new TermInfoReader (term, KnownTerminals.xterm); } else if (term == "linux") { reader = new TermInfoReader (term, KnownTerminals.linux); } } if (reader == null) reader = new TermInfoReader (term, KnownTerminals.ansi); if (!(Console.stdout is CStreamWriter)) { // Application set its own stdout, we need a reference to the real stdout stdout = new CStreamWriter (Console.OpenStandardOutput (0), Console.OutputEncoding, false); ((StreamWriter) stdout).AutoFlush = true; } else { stdout = (CStreamWriter) Console.stdout; } } public bool Initialized { get { return inited; } } public void Init () { if (inited) return; lock (initLock){ if (inited) return; inited = true; /* This should not happen any more, since it is checked for in Console */ if (!ConsoleDriver.IsConsole) throw new IOException ("Not a tty."); ConsoleDriver.SetEcho (false); string endString = null; keypadXmit = reader.Get (TermInfoStrings.KeypadXmit); keypadLocal = reader.Get (TermInfoStrings.KeypadLocal); if (keypadXmit != null) { WriteConsole (keypadXmit); // Needed to get the arrows working if (keypadLocal != null) endString += keypadLocal; } origPair = reader.Get (TermInfoStrings.OrigPair); origColors = reader.Get (TermInfoStrings.OrigColors); setfgcolor = reader.Get (TermInfoStrings.SetAForeground); setbgcolor = reader.Get (TermInfoStrings.SetABackground); maxColors = reader.Get (TermInfoNumbers.MaxColors); maxColors = Math.Max (Math.Min (maxColors, 16), 1); string resetColors = (origColors == null) ? origPair : origColors; if (resetColors != null) endString += resetColors; unsafe { if (!ConsoleDriver.TtySetup (keypadXmit, endString, out control_characters, out native_terminal_size)){ control_characters = new byte [17]; native_terminal_size = null; //throw new IOException ("Error initializing terminal."); } } stdin = new StreamReader (Console.OpenStandardInput (0), Console.InputEncoding); clear = reader.Get (TermInfoStrings.ClearScreen); bell = reader.Get (TermInfoStrings.Bell); if (clear == null) { clear = reader.Get (TermInfoStrings.CursorHome); clear += reader.Get (TermInfoStrings.ClrEos); } csrVisible = reader.Get (TermInfoStrings.CursorNormal); if (csrVisible == null) csrVisible = reader.Get (TermInfoStrings.CursorVisible); csrInvisible = reader.Get (TermInfoStrings.CursorInvisible); if (term == "cygwin" || term == "linux" || (term != null && term.StartsWith ("xterm")) || term == "rxvt" || term == "dtterm") { titleFormat = "\x1b]0;{0}\x7"; // icon + window title } else if (term == "iris-ansi") { titleFormat = "\x1bP1.y{0}\x1b\\"; // not tested } else if (term == "sun-cmd") { titleFormat = "\x1b]l{0}\x1b\\"; // not tested } cursorAddress = reader.Get (TermInfoStrings.CursorAddress); GetCursorPosition (); #if DEBUG logger.WriteLine ("noGetPosition: {0} left: {1} top: {2}", noGetPosition, cursorLeft, cursorTop); logger.Flush (); #endif if (noGetPosition) { WriteConsole (clear); cursorLeft = 0; cursorTop = 0; } } } void IncrementX () { cursorLeft++; if (cursorLeft >= WindowWidth) { cursorTop++; cursorLeft = 0; if (cursorTop >= WindowHeight) { // Writing beyond the initial screen if (rl_starty != -1) rl_starty--; cursorTop--; } } } // Should never get called unless inited public void WriteSpecialKey (ConsoleKeyInfo key) { switch (key.Key) { case ConsoleKey.Backspace: if (cursorLeft > 0) { if (cursorLeft <= rl_startx && cursorTop == rl_starty) break; cursorLeft--; SetCursorPosition (cursorLeft, cursorTop); WriteConsole (" "); SetCursorPosition (cursorLeft, cursorTop); } #if DEBUG logger.WriteLine ("BS left: {0} top: {1}", cursorLeft, cursorTop); logger.Flush (); #endif break; case ConsoleKey.Tab: int n = 8 - (cursorLeft % 8); for (int i = 0; i < n; i++){ IncrementX (); } WriteConsole ("\t"); break; case ConsoleKey.Clear: WriteConsole (clear); cursorLeft = 0; cursorTop = 0; break; case ConsoleKey.Enter: break; default: break; } #if DEBUG logger.WriteLine ("left: {0} top: {1}", cursorLeft, cursorTop); logger.Flush (); #endif } // Should never get called unless inited public void WriteSpecialKey (char c) { WriteSpecialKey (CreateKeyInfoFromInt (c, false)); } public bool IsSpecialKey (ConsoleKeyInfo key) { if (!inited) return false; switch (key.Key) { case ConsoleKey.Backspace: return true; case ConsoleKey.Tab: return true; case ConsoleKey.Clear: return true; case ConsoleKey.Enter: cursorLeft = 0; cursorTop++; if (cursorTop >= WindowHeight) { cursorTop--; //TODO: scroll up } return false; default: // CStreamWriter will handle writing this key IncrementX (); return false; } } public bool IsSpecialKey (char c) { return IsSpecialKey (CreateKeyInfoFromInt (c, false)); } /// /// The values of the ConsoleColor enums unfortunately don't map to the /// corresponding ANSI values. We need to do the mapping manually. /// See http://en.wikipedia.org/wiki/ANSI_escape_code#Colors /// private static readonly int[] _consoleColorToAnsiCode = new int[] { // Dark/Normal colors 0, // Black, 4, // DarkBlue, 2, // DarkGreen, 6, // DarkCyan, 1, // DarkRed, 5, // DarkMagenta, 3, // DarkYellow, 7, // Gray, // Bright colors 8, // DarkGray, 12, // Blue, 10, // Green, 14, // Cyan, 9, // Red, 13, // Magenta, 11, // Yellow, 15 // White }; void ChangeColor (string format, ConsoleColor color) { int ccValue = (int)color; if ((ccValue & ~0xF) != 0) throw new ArgumentException("Invalid Console Color"); int ansiCode = _consoleColorToAnsiCode[ccValue] % maxColors; WriteConsole (ParameterizedStrings.Evaluate (format, ansiCode)); } public ConsoleColor BackgroundColor { get { if (!inited) { Init (); } return bgcolor; } set { if (!inited) { Init (); } ChangeColor (setbgcolor, value); bgcolor = value; } } public ConsoleColor ForegroundColor { get { if (!inited) { Init (); } return fgcolor; } set { if (!inited) { Init (); } ChangeColor (setfgcolor, value); fgcolor = value; } } void GetCursorPosition () { int row = 0, col = 0; int b; // First, get any data in the input buffer. Merely reduces the likelyhood of getting an error int inqueue = ConsoleDriver.InternalKeyAvailable (0); while (inqueue-- > 0){ b = stdin.Read (); AddToBuffer (b); } // Then try to probe for the cursor coordinates WriteConsole ("\x1b[6n"); if (ConsoleDriver.InternalKeyAvailable (1000) <= 0) { noGetPosition = true; return; } b = stdin.Read (); while (b != '\x1b') { AddToBuffer (b); if (ConsoleDriver.InternalKeyAvailable (100) <= 0) return; b = stdin.Read (); } b = stdin.Read (); if (b != '[') { AddToBuffer ('\x1b'); AddToBuffer (b); return; } b = stdin.Read (); if (b != ';') { row = b - '0'; b = stdin.Read (); while ((b >= '0') && (b <= '9')) { row = row * 10 + b - '0'; b = stdin.Read (); } // Row/col is 0 based row --; } b = stdin.Read (); if (b != 'R') { col = b - '0'; b = stdin.Read (); while ((b >= '0') && (b <= '9')) { col = col * 10 + b - '0'; b = stdin.Read (); } // Row/col is 0 based col --; } #if DEBUG logger.WriteLine ("GetCursorPosition: {0}, {1}", col, row); logger.Flush (); #endif cursorLeft = col; cursorTop = row; } public int BufferHeight { get { if (!inited) { Init (); } CheckWindowDimensions (); return bufferHeight; } set { if (!inited) { Init (); } throw new NotSupportedException (); } } public int BufferWidth { get { if (!inited) { Init (); } CheckWindowDimensions (); return bufferWidth; } set { if (!inited) { Init (); } throw new NotSupportedException (); } } public bool CapsLock { get { if (!inited) { Init (); } return false; } } public int CursorLeft { get { if (!inited) { Init (); } return cursorLeft; } set { if (!inited) { Init (); } SetCursorPosition (value, CursorTop); } } public int CursorTop { get { if (!inited) { Init (); } return cursorTop; } set { if (!inited) { Init (); } SetCursorPosition (CursorLeft, value); } } public bool CursorVisible { get { if (!inited) { Init (); } return cursorVisible; } set { if (!inited) { Init (); } cursorVisible = value; WriteConsole ((value ? csrVisible : csrInvisible)); } } // we have CursorNormal vs. CursorVisible... [MonoTODO] public int CursorSize { get { if (!inited) { Init (); } return 1; } set { if (!inited) { Init (); } } } public bool KeyAvailable { get { if (!inited) { Init (); } return (writepos > readpos || ConsoleDriver.InternalKeyAvailable (0) > 0); } } // We don't know these next 2 values, so return something reasonable public int LargestWindowHeight { get { return WindowHeight; } } public int LargestWindowWidth { get { return WindowWidth; } } public bool NumberLock { get { if (!inited) { Init (); } return false; } } public string Title { get { if (!inited) { Init (); } return title; } set { if (!inited) { Init (); } title = value; WriteConsole (String.Format (titleFormat, value)); } } public bool TreatControlCAsInput { get { if (!inited) { Init (); } return controlCAsInput; } set { if (!inited) { Init (); } if (controlCAsInput == value) return; ConsoleDriver.SetBreak (value); controlCAsInput = value; } } // // Requries that caller calls Init () if not !inited. // unsafe void CheckWindowDimensions () { if (native_terminal_size == null || terminal_size == *native_terminal_size) return; if (*native_terminal_size == -1){ int c = reader.Get (TermInfoNumbers.Columns); if (c != 0) windowWidth = c; c = reader.Get (TermInfoNumbers.Lines); if (c != 0) windowHeight = c; } else { terminal_size = *native_terminal_size; windowWidth = terminal_size >> 16; windowHeight = terminal_size & 0xffff; } bufferHeight = windowHeight; bufferWidth = windowWidth; } public int WindowHeight { get { if (!inited) { Init (); } CheckWindowDimensions (); return windowHeight; } set { if (!inited) { Init (); } throw new NotSupportedException (); } } public int WindowLeft { get { if (!inited) { Init (); } //CheckWindowDimensions (); return 0; } set { if (!inited) { Init (); } throw new NotSupportedException (); } } public int WindowTop { get { if (!inited) { Init (); } //CheckWindowDimensions (); return 0; } set { if (!inited) { Init (); } throw new NotSupportedException (); } } public int WindowWidth { get { if (!inited) { Init (); } CheckWindowDimensions (); return windowWidth; } set { if (!inited) { Init (); } throw new NotSupportedException (); } } public void Clear () { if (!inited) { Init (); } WriteConsole (clear); cursorLeft = 0; cursorTop = 0; } public void Beep (int frequency, int duration) { if (!inited) { Init (); } WriteConsole (bell); } public void MoveBufferArea (int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop, Char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor) { if (!inited) { Init (); } throw new NotImplementedException (); } void AddToBuffer (int b) { if (buffer == null) { buffer = new char [1024]; } else if (writepos >= buffer.Length) { char [] newbuf = new char [buffer.Length * 2]; Buffer.BlockCopy (buffer, 0, newbuf, 0, buffer.Length); buffer = newbuf; } buffer [writepos++] = (char) b; } void AdjustBuffer () { if (readpos >= writepos) { readpos = writepos = 0; } } ConsoleKeyInfo CreateKeyInfoFromInt (int n, bool alt) { char c = (char) n; ConsoleKey key = (ConsoleKey)n; bool shift = false; bool ctrl = false; switch (n){ case 10: key = ConsoleKey.Enter; break; case 0x20: key = ConsoleKey.Spacebar; break; case 45: key = ConsoleKey.Subtract; break; case 43: key = ConsoleKey.Add; break; case 47: key = ConsoleKey.Divide; break; case 42: key = ConsoleKey.Multiply; break; case 8: case 9: case 12: case 13: case 19: /* Values in ConsoleKey */ break; case 27: key = ConsoleKey.Escape; break; default: if (n >= 1 && n <= 26) { // For Ctrl-a to Ctrl-z. ctrl = true; key = ConsoleKey.A + n - 1; } else if (n >= 'a' && n <= 'z') { key = ConsoleKey.A - 'a' + n; } else if (n >= 'A' && n <= 'Z') { shift = true; } else if (n >= '0' && n <= '9') { } else key = 0; break; } return new ConsoleKeyInfo (c, key, shift, alt, ctrl); } object GetKeyFromBuffer (bool cooked) { if (readpos >= writepos) return null; int next = buffer [readpos]; if (!cooked || !rootmap.StartsWith (next)) { readpos++; AdjustBuffer (); return CreateKeyInfoFromInt (next, false); } int used; TermInfoStrings str = rootmap.Match (buffer, readpos, writepos - readpos, out used); if ((int) str == -1){ // Escape sequences: alt keys are sent as ESC-key if (buffer [readpos] == 27 && (writepos - readpos) >= 2){ readpos += 2; AdjustBuffer (); if (buffer [readpos+1] == 127) return new ConsoleKeyInfo ((char)8, ConsoleKey.Backspace, false, true, false); return CreateKeyInfoFromInt (buffer [readpos+1], true); } else return null; } ConsoleKeyInfo key; if (keymap [str] != null) { key = (ConsoleKeyInfo) keymap [str]; } else { readpos++; AdjustBuffer (); return CreateKeyInfoFromInt (next, false); } readpos += used; AdjustBuffer (); return key; } ConsoleKeyInfo ReadKeyInternal (out bool fresh) { if (!inited) Init (); InitKeys (); object o; if ((o = GetKeyFromBuffer (true)) == null) { do { if (ConsoleDriver.InternalKeyAvailable (150) > 0) { do { AddToBuffer (stdin.Read ()); } while (ConsoleDriver.InternalKeyAvailable (0) > 0); } else if (stdin.DataAvailable ()) { do { AddToBuffer (stdin.Read ()); } while (stdin.DataAvailable ()); } else { if ((o = GetKeyFromBuffer (false)) != null) break; AddToBuffer (stdin.Read ()); } o = GetKeyFromBuffer (true); } while (o == null); // freshly read character fresh = true; } else { // this char was pre-buffered (e.g. not fresh) fresh = false; } return (ConsoleKeyInfo) o; } #region Input echoing optimization bool InputPending () { // check if we've got pending input we can read immediately return readpos < writepos || stdin.DataAvailable (); } char [] echobuf = null; int echon = 0; // Queues a character to be echo'd back to the console void QueueEcho (char c) { if (echobuf == null) echobuf = new char [1024]; echobuf[echon++] = c; if (echon == echobuf.Length || !InputPending ()) { // blit our echo buffer to the console stdout.InternalWriteChars (echobuf, echon); echon = 0; } } // Queues a key to be echo'd back to the console void Echo (ConsoleKeyInfo key) { if (!IsSpecialKey (key)) { QueueEcho (key.KeyChar); return; } // flush pending echo's EchoFlush (); WriteSpecialKey (key); } // Flush the pending echo queue void EchoFlush () { if (echon == 0) return; // flush our echo buffer to the console stdout.InternalWriteChars (echobuf, echon); echon = 0; } #endregion public int Read ([In, Out] char [] dest, int index, int count) { bool fresh, echo = false; StringBuilder sbuf; ConsoleKeyInfo key; int BoL = 0; // Beginning-of-Line marker (can't backspace beyond this) object o; char c; sbuf = new StringBuilder (); // consume buffered keys first (do not echo, these have already been echo'd) while (true) { if ((o = GetKeyFromBuffer (true)) == null) break; key = (ConsoleKeyInfo) o; c = key.KeyChar; if (key.Key != ConsoleKey.Backspace) { if (key.Key == ConsoleKey.Enter) BoL = sbuf.Length; sbuf.Append (c); } else if (sbuf.Length > BoL) { sbuf.Length--; } } // continue reading until Enter is hit rl_startx = cursorLeft; rl_starty = cursorTop; do { key = ReadKeyInternal (out fresh); echo = echo || fresh; c = key.KeyChar; if (key.Key != ConsoleKey.Backspace) { if (key.Key == ConsoleKey.Enter) BoL = sbuf.Length; sbuf.Append (c); } else if (sbuf.Length > BoL) { sbuf.Length--; } else { continue; } // echo fresh keys back to the console if (echo) Echo (key); } while (key.Key != ConsoleKey.Enter); EchoFlush (); rl_startx = -1; rl_starty = -1; // copy up to count chars into dest int nread = 0; while (count > 0 && nread < sbuf.Length) { dest[index + nread] = sbuf[nread]; nread++; count--; } // put the rest back into our key buffer for (int i = nread; i < sbuf.Length; i++) AddToBuffer (sbuf[i]); return nread; } public ConsoleKeyInfo ReadKey (bool intercept) { bool fresh; ConsoleKeyInfo key = ReadKeyInternal (out fresh); if (!intercept && fresh) { // echo the fresh key back to the console Echo (key); EchoFlush (); } return key; } public string ReadLine () { if (!inited) Init (); // Hack to make Iron Python work (since it goes behind our backs // when writing to the console thus preventing us from keeping // cursor state normally). GetCursorPosition (); StringBuilder builder = new StringBuilder (); bool fresh, echo = false; ConsoleKeyInfo key; char c; rl_startx = cursorLeft; rl_starty = cursorTop; char eof = (char) control_characters [ControlCharacters.EOF]; do { key = ReadKeyInternal (out fresh); echo = echo || fresh; c = key.KeyChar; // EOF -> Ctrl-D (EOT) pressed. if (c == eof && c != 0 && builder.Length == 0) return null; if (key.Key != ConsoleKey.Enter) { if (key.Key != ConsoleKey.Backspace) { builder.Append (c); } else if (builder.Length > 0) { builder.Length--; } else { // skips over echoing the key to the console continue; } } // echo fresh keys back to the console if (echo) Echo (key); } while (key.Key != ConsoleKey.Enter); EchoFlush (); rl_startx = -1; rl_starty = -1; return builder.ToString (); } public void ResetColor () { if (!inited) { Init (); } string str = (origPair != null) ? origPair : origColors; WriteConsole (str); } public void SetBufferSize (int width, int height) { if (!inited) { Init (); } throw new NotImplementedException (String.Empty); } public void SetCursorPosition (int left, int top) { if (!inited) { Init (); } CheckWindowDimensions (); if (left < 0 || left >= bufferWidth) throw new ArgumentOutOfRangeException ("left", "Value must be positive and below the buffer width."); if (top < 0 || top >= bufferHeight) throw new ArgumentOutOfRangeException ("top", "Value must be positive and below the buffer height."); // Either CursorAddress or nothing. // We might want to play with up/down/left/right/home when ca is not available. if (cursorAddress == null) throw new NotSupportedException ("This terminal does not suport setting the cursor position."); WriteConsole (ParameterizedStrings.Evaluate (cursorAddress, top, left)); cursorLeft = left; cursorTop = top; } public void SetWindowPosition (int left, int top) { if (!inited) { Init (); } // No need to throw exceptions here. //throw new NotSupportedException (); } public void SetWindowSize (int width, int height) { if (!inited) { Init (); } // No need to throw exceptions here. //throw new NotSupportedException (); } void CreateKeyMap () { keymap = new Hashtable (); keymap [TermInfoStrings.KeyBackspace] = new ConsoleKeyInfo ('\0', ConsoleKey.Backspace, false, false, false); keymap [TermInfoStrings.KeyClear] = new ConsoleKeyInfo ('\0', ConsoleKey.Clear, false, false, false); // Delete character... keymap [TermInfoStrings.KeyDown] = new ConsoleKeyInfo ('\0', ConsoleKey.DownArrow, false, false, false); keymap [TermInfoStrings.KeyF1] = new ConsoleKeyInfo ('\0', ConsoleKey.F1, false, false, false); keymap [TermInfoStrings.KeyF10] = new ConsoleKeyInfo ('\0', ConsoleKey.F10, false, false, false); keymap [TermInfoStrings.KeyF2] = new ConsoleKeyInfo ('\0', ConsoleKey.F2, false, false, false); keymap [TermInfoStrings.KeyF3] = new ConsoleKeyInfo ('\0', ConsoleKey.F3, false, false, false); keymap [TermInfoStrings.KeyF4] = new ConsoleKeyInfo ('\0', ConsoleKey.F4, false, false, false); keymap [TermInfoStrings.KeyF5] = new ConsoleKeyInfo ('\0', ConsoleKey.F5, false, false, false); keymap [TermInfoStrings.KeyF6] = new ConsoleKeyInfo ('\0', ConsoleKey.F6, false, false, false); keymap [TermInfoStrings.KeyF7] = new ConsoleKeyInfo ('\0', ConsoleKey.F7, false, false, false); keymap [TermInfoStrings.KeyF8] = new ConsoleKeyInfo ('\0', ConsoleKey.F8, false, false, false); keymap [TermInfoStrings.KeyF9] = new ConsoleKeyInfo ('\0', ConsoleKey.F9, false, false, false); keymap [TermInfoStrings.KeyHome] = new ConsoleKeyInfo ('\0', ConsoleKey.Home, false, false, false); keymap [TermInfoStrings.KeyLeft] = new ConsoleKeyInfo ('\0', ConsoleKey.LeftArrow, false, false, false); keymap [TermInfoStrings.KeyLl] = new ConsoleKeyInfo ('\0', ConsoleKey.NumPad1, false, false, false); keymap [TermInfoStrings.KeyNpage] = new ConsoleKeyInfo ('\0', ConsoleKey.PageDown, false, false, false); keymap [TermInfoStrings.KeyPpage] = new ConsoleKeyInfo ('\0', ConsoleKey.PageUp, false, false, false); keymap [TermInfoStrings.KeyRight] = new ConsoleKeyInfo ('\0', ConsoleKey.RightArrow, false, false, false); keymap [TermInfoStrings.KeySf] = new ConsoleKeyInfo ('\0', ConsoleKey.PageDown, false, false, false); keymap [TermInfoStrings.KeySr] = new ConsoleKeyInfo ('\0', ConsoleKey.PageUp, false, false, false); keymap [TermInfoStrings.KeyUp] = new ConsoleKeyInfo ('\0', ConsoleKey.UpArrow, false, false, false); keymap [TermInfoStrings.KeyA1] = new ConsoleKeyInfo ('\0', ConsoleKey.NumPad7, false, false, false); keymap [TermInfoStrings.KeyA3] = new ConsoleKeyInfo ('\0', ConsoleKey.NumPad9, false, false, false); keymap [TermInfoStrings.KeyB2] = new ConsoleKeyInfo ('\0', ConsoleKey.NumPad5, false, false, false); keymap [TermInfoStrings.KeyC1] = new ConsoleKeyInfo ('\0', ConsoleKey.NumPad1, false, false, false); keymap [TermInfoStrings.KeyC3] = new ConsoleKeyInfo ('\0', ConsoleKey.NumPad3, false, false, false); keymap [TermInfoStrings.KeyBtab] = new ConsoleKeyInfo ('\0', ConsoleKey.Tab, true, false, false); keymap [TermInfoStrings.KeyBeg] = new ConsoleKeyInfo ('\0', ConsoleKey.Home, false, false, false); keymap [TermInfoStrings.KeyCopy] = new ConsoleKeyInfo ('C', ConsoleKey.C, false, true, false); keymap [TermInfoStrings.KeyEnd] = new ConsoleKeyInfo ('\0', ConsoleKey.End, false, false, false); keymap [TermInfoStrings.KeyEnter] = new ConsoleKeyInfo ('\n', ConsoleKey.Enter, false, false, false); keymap [TermInfoStrings.KeyHelp] = new ConsoleKeyInfo ('\0', ConsoleKey.Help, false, false, false); keymap [TermInfoStrings.KeyPrint] = new ConsoleKeyInfo ('\0', ConsoleKey.Print, false, false, false); keymap [TermInfoStrings.KeyUndo] = new ConsoleKeyInfo ('Z', ConsoleKey.Z , false, true, false); keymap [TermInfoStrings.KeySbeg] = new ConsoleKeyInfo ('\0', ConsoleKey.Home, true, false, false); keymap [TermInfoStrings.KeyScopy] = new ConsoleKeyInfo ('C', ConsoleKey.C , true, true, false); keymap [TermInfoStrings.KeySdc] = new ConsoleKeyInfo ('\x9', ConsoleKey.Delete, true, false, false); keymap [TermInfoStrings.KeyShelp] = new ConsoleKeyInfo ('\0', ConsoleKey.Help, true, false, false); keymap [TermInfoStrings.KeyShome] = new ConsoleKeyInfo ('\0', ConsoleKey.Home, true, false, false); keymap [TermInfoStrings.KeySleft] = new ConsoleKeyInfo ('\0', ConsoleKey.LeftArrow, true, false, false); keymap [TermInfoStrings.KeySprint] = new ConsoleKeyInfo ('\0', ConsoleKey.Print, true, false, false); keymap [TermInfoStrings.KeySright] = new ConsoleKeyInfo ('\0', ConsoleKey.RightArrow, true, false, false); keymap [TermInfoStrings.KeySundo] = new ConsoleKeyInfo ('Z', ConsoleKey.Z, true, false, false); keymap [TermInfoStrings.KeyF11] = new ConsoleKeyInfo ('\0', ConsoleKey.F11, false, false, false); keymap [TermInfoStrings.KeyF12] = new ConsoleKeyInfo ('\0', ConsoleKey.F12 , false, false, false); keymap [TermInfoStrings.KeyF13] = new ConsoleKeyInfo ('\0', ConsoleKey.F13, false, false, false); keymap [TermInfoStrings.KeyF14] = new ConsoleKeyInfo ('\0', ConsoleKey.F14, false, false, false); keymap [TermInfoStrings.KeyF15] = new ConsoleKeyInfo ('\0', ConsoleKey.F15, false, false, false); keymap [TermInfoStrings.KeyF16] = new ConsoleKeyInfo ('\0', ConsoleKey.F16, false, false, false); keymap [TermInfoStrings.KeyF17] = new ConsoleKeyInfo ('\0', ConsoleKey.F17, false, false, false); keymap [TermInfoStrings.KeyF18] = new ConsoleKeyInfo ('\0', ConsoleKey.F18, false, false, false); keymap [TermInfoStrings.KeyF19] = new ConsoleKeyInfo ('\0', ConsoleKey.F19, false, false, false); keymap [TermInfoStrings.KeyF20] = new ConsoleKeyInfo ('\0', ConsoleKey.F20, false, false, false); keymap [TermInfoStrings.KeyF21] = new ConsoleKeyInfo ('\0', ConsoleKey.F21, false, false, false); keymap [TermInfoStrings.KeyF22] = new ConsoleKeyInfo ('\0', ConsoleKey.F22, false, false, false); keymap [TermInfoStrings.KeyF23] = new ConsoleKeyInfo ('\0', ConsoleKey.F23, false, false, false); keymap [TermInfoStrings.KeyF24] = new ConsoleKeyInfo ('\0', ConsoleKey.F24, false, false, false); // These were previously missing: keymap [TermInfoStrings.KeyDc] = new ConsoleKeyInfo ('\0', ConsoleKey.Delete, false, false, false); keymap [TermInfoStrings.KeyIc] = new ConsoleKeyInfo ('\0', ConsoleKey.Insert, false, false, false); } void InitKeys () { if (initKeys) return; CreateKeyMap (); rootmap = new ByteMatcher (); // // The keys that we know about and use // var UsedKeys = new [] { TermInfoStrings.KeyBackspace, TermInfoStrings.KeyClear, TermInfoStrings.KeyDown, TermInfoStrings.KeyF1, TermInfoStrings.KeyF10, TermInfoStrings.KeyF2, TermInfoStrings.KeyF3, TermInfoStrings.KeyF4, TermInfoStrings.KeyF5, TermInfoStrings.KeyF6, TermInfoStrings.KeyF7, TermInfoStrings.KeyF8, TermInfoStrings.KeyF9, TermInfoStrings.KeyHome, TermInfoStrings.KeyLeft, TermInfoStrings.KeyLl, TermInfoStrings.KeyNpage, TermInfoStrings.KeyPpage, TermInfoStrings.KeyRight, TermInfoStrings.KeySf, TermInfoStrings.KeySr, TermInfoStrings.KeyUp, TermInfoStrings.KeyA1, TermInfoStrings.KeyA3, TermInfoStrings.KeyB2, TermInfoStrings.KeyC1, TermInfoStrings.KeyC3, TermInfoStrings.KeyBtab, TermInfoStrings.KeyBeg, TermInfoStrings.KeyCopy, TermInfoStrings.KeyEnd, TermInfoStrings.KeyEnter, TermInfoStrings.KeyHelp, TermInfoStrings.KeyPrint, TermInfoStrings.KeyUndo, TermInfoStrings.KeySbeg, TermInfoStrings.KeyScopy, TermInfoStrings.KeySdc, TermInfoStrings.KeyShelp, TermInfoStrings.KeyShome, TermInfoStrings.KeySleft, TermInfoStrings.KeySprint, TermInfoStrings.KeySright, TermInfoStrings.KeySundo, TermInfoStrings.KeyF11, TermInfoStrings.KeyF12, TermInfoStrings.KeyF13, TermInfoStrings.KeyF14, TermInfoStrings.KeyF15, TermInfoStrings.KeyF16, TermInfoStrings.KeyF17, TermInfoStrings.KeyF18, TermInfoStrings.KeyF19, TermInfoStrings.KeyF20, TermInfoStrings.KeyF21, TermInfoStrings.KeyF22, TermInfoStrings.KeyF23, TermInfoStrings.KeyF24, // These were missing TermInfoStrings.KeyDc, TermInfoStrings.KeyIc }; foreach (TermInfoStrings tis in UsedKeys) AddStringMapping (tis); rootmap.AddMapping (TermInfoStrings.KeyBackspace, new byte [] { control_characters [ControlCharacters.Erase] }); rootmap.Sort (); initKeys = true; } void AddStringMapping (TermInfoStrings s) { byte [] bytes = reader.GetStringBytes (s); if (bytes == null) return; rootmap.AddMapping (s, bytes); } } /// Provides support for evaluating parameterized terminfo database format strings. internal static class ParameterizedStrings { /// A cached stack to use to avoid allocating a new stack object for every evaluation. [ThreadStatic] private static LowLevelStack _cachedStack; /// Evaluates a terminfo formatting string, using the supplied arguments. /// The format string. /// The arguments to the format string. /// The formatted string. public static string Evaluate(string format, params FormatParam[] args) { if (format == null) throw new ArgumentNullException("format"); if (args == null) throw new ArgumentNullException("args"); // Initialize the stack to use for processing. LowLevelStack stack = _cachedStack; if (stack == null) _cachedStack = stack = new LowLevelStack(); else stack.Clear(); // "dynamic" and "static" variables are much less often used (the "dynamic" and "static" // terminology appears to just refer to two different collections rather than to any semantic // meaning). As such, we'll only initialize them if we really need them. FormatParam[] dynamicVars = null, staticVars = null; int pos = 0; return EvaluateInternal(format, ref pos, args, stack, ref dynamicVars, ref staticVars); // EvaluateInternal may throw IndexOutOfRangeException and InvalidOperationException // if the format string is malformed or if it's inconsistent with the parameters provided. } /// Evaluates a terminfo formatting string, using the supplied arguments and processing data structures. /// The format string. /// The position in to start processing. /// The arguments to the format string. /// The stack to use as the format string is evaluated. /// A lazily-initialized collection of variables. /// A lazily-initialized collection of variables. /// /// The formatted string; this may be empty if the evaluation didn't yield any output. /// The evaluation stack will have a 1 at the top if all processing was completed at invoked level /// of recursion, and a 0 at the top if we're still inside of a conditional that requires more processing. /// private static string EvaluateInternal( string format, ref int pos, FormatParam[] args, LowLevelStack stack, ref FormatParam[] dynamicVars, ref FormatParam[] staticVars) { // Create a StringBuilder to store the output of this processing. We use the format's length as an // approximation of an upper-bound for how large the output will be, though with parameter processing, // this is just an estimate, sometimes way over, sometimes under. StringBuilder output = new StringBuilder(format.Length); // Format strings support conditionals, including the equivalent of "if ... then ..." and // "if ... then ... else ...", as well as "if ... then ... else ... then ..." // and so on, where an else clause can not only be evaluated for string output but also // as a conditional used to determine whether to evaluate a subsequent then clause. // We use recursion to process these subsequent parts, and we track whether we're processing // at the same level of the initial if clause (or whether we're nested). bool sawIfConditional = false; // Process each character in the format string, starting from the position passed in. for (; pos < format.Length; pos++){ // '%' is the escape character for a special sequence to be evaluated. // Anything else just gets pushed to output. if (format[pos] != '%') { output.Append(format[pos]); continue; } // We have a special parameter sequence to process. Now we need // to look at what comes after the '%'. ++pos; switch (format[pos]) { // Output appending operations case '%': // Output the escaped '%' output.Append('%'); break; case 'c': // Pop the stack and output it as a char output.Append((char)stack.Pop().Int32); break; case 's': // Pop the stack and output it as a string output.Append(stack.Pop().String); break; case 'd': // Pop the stack and output it as an integer output.Append(stack.Pop().Int32); break; case 'o': case 'X': case 'x': case ':': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': // printf strings of the format "%[[:]flags][width[.precision]][doxXs]" are allowed // (with a ':' used in front of flags to help differentiate from binary operations, as flags can // include '-' and '+'). While above we've special-cased common usage (e.g. %d, %s), // for more complicated expressions we delegate to printf. int printfEnd = pos; for (; printfEnd < format.Length; printfEnd++) // find the end of the printf format string { char ec = format[printfEnd]; if (ec == 'd' || ec == 'o' || ec == 'x' || ec == 'X' || ec == 's') { break; } } if (printfEnd >= format.Length) throw new InvalidOperationException("Terminfo database contains invalid values"); string printfFormat = format.Substring(pos - 1, printfEnd - pos + 2); // extract the format string if (printfFormat.Length > 1 && printfFormat[1] == ':') printfFormat = printfFormat.Remove(1, 1); output.Append(FormatPrintF(printfFormat, stack.Pop().Object)); // do the printf formatting and append its output break; // Stack pushing operations case 'p': // Push the specified parameter (1-based) onto the stack pos++; stack.Push(args[format[pos] - '1']); break; case 'l': // Pop a string and push its length stack.Push(stack.Pop().String.Length); break; case '{': // Push integer literal, enclosed between braces pos++; int intLit = 0; while (format[pos] != '}') { intLit = (intLit * 10) + (format[pos] - '0'); pos++; } stack.Push(intLit); break; case '\'': // Push literal character, enclosed between single quotes stack.Push((int)format[pos + 1]); pos += 2; break; // Storing and retrieving "static" and "dynamic" variables case 'P': // Pop a value and store it into either static or dynamic variables based on whether the a-z variable is capitalized pos++; int setIndex; FormatParam[] targetVars = GetDynamicOrStaticVariables(format[pos], ref dynamicVars, ref staticVars, out setIndex); targetVars[setIndex] = stack.Pop(); break; case 'g': // Push a static or dynamic variable; which is based on whether the a-z variable is capitalized pos++; int getIndex; FormatParam[] sourceVars = GetDynamicOrStaticVariables(format[pos], ref dynamicVars, ref staticVars, out getIndex); stack.Push(sourceVars[getIndex]); break; // Binary operations case '+': case '-': case '*': case '/': case 'm': case '^': // arithmetic case '&': case '|': // bitwise case '=': case '>': case '<': // comparison case 'A': case 'O': // logical int second = stack.Pop().Int32; // it's a stack... the second value was pushed last int first = stack.Pop().Int32; char c = format[pos]; stack.Push( c == '+' ? (first + second) : c == '-' ? (first - second) : c == '*' ? (first * second) : c == '/' ? (first / second) : c == 'm' ? (first % second) : c == '^' ? (first ^ second) : c == '&' ? (first & second) : c == '|' ? (first | second) : c == '=' ? AsInt(first == second) : c == '>' ? AsInt(first > second) : c == '<' ? AsInt(first < second) : c == 'A' ? AsInt(AsBool(first) && AsBool(second)) : c == 'O' ? AsInt(AsBool(first) || AsBool(second)) : 0); // not possible; we just validated above break; // Unary operations case '!': case '~': int value = stack.Pop().Int32; stack.Push( format[pos] == '!' ? AsInt(!AsBool(value)) : ~value); break; // Augment first two parameters by 1 case 'i': args[0] = 1 + args[0].Int32; args[1] = 1 + args[1].Int32; break; // Conditional of the form %? if-part %t then-part %e else-part %; // The "%e else-part" is optional. case '?': sawIfConditional = true; break; case 't': // We hit the end of the if-part and are about to start the then-part. // The if-part left its result on the stack; pop and evaluate. bool conditionalResult = AsBool(stack.Pop().Int32); // Regardless of whether it's true, run the then-part to get past it. // If the conditional was true, output the then results. pos++; string thenResult = EvaluateInternal(format, ref pos, args, stack, ref dynamicVars, ref staticVars); if (conditionalResult) { output.Append(thenResult); } // We're past the then; the top of the stack should now be a Boolean // indicating whether this conditional has more to be processed (an else clause). if (!AsBool(stack.Pop().Int32)) { // Process the else clause, and if the conditional was false, output the else results. pos++; string elseResult = EvaluateInternal(format, ref pos, args, stack, ref dynamicVars, ref staticVars); if (!conditionalResult) { output.Append(elseResult); } // Now we should be done (any subsequent elseif logic will have bene handled in the recursive call). if (!AsBool(stack.Pop().Int32)) { throw new InvalidOperationException("Terminfo database contains invalid values"); } } // If we're in a nested processing, return to our parent. if (!sawIfConditional) { stack.Push(1); return output.ToString(); } // Otherwise, we're done processing the conditional in its entirety. sawIfConditional = false; break; case 'e': case ';': // Let our caller know why we're exiting, whether due to the end of the conditional or an else branch. stack.Push(AsInt(format[pos] == ';')); return output.ToString(); // Anything else is an error default: throw new InvalidOperationException("Terminfo database contains invalid values"); } } stack.Push(1); return output.ToString(); } /// Converts an Int32 to a Boolean, with 0 meaning false and all non-zero values meaning true. /// The integer value to convert. /// true if the integer was non-zero; otherwise, false. static bool AsBool(Int32 i) { return i != 0; } /// Converts a Boolean to an Int32, with true meaning 1 and false meaning 0. /// The Boolean value to convert. /// 1 if the Boolean is true; otherwise, 0. static int AsInt(bool b) { return b ? 1 : 0; } static string StringFromAsciiBytes(byte[] buffer, int offset, int length) { // Special-case for empty strings if (length == 0) return string.Empty; // new string(sbyte*, ...) doesn't exist in the targeted reference assembly, // so we first copy to an array of chars, and then create a string from that. char[] chars = new char[length]; for (int i = 0, j = offset; i < length; i++, j++) chars[i] = (char)buffer[j]; return new string(chars); } [DllImport("libc")] static extern unsafe int snprintf(byte* str, IntPtr size, string format, string arg1); [DllImport("libc")] static extern unsafe int snprintf(byte* str, IntPtr size, string format, int arg1); /// Formats an argument into a printf-style format string. /// The printf-style format string. /// The argument to format. This must be an Int32 or a String. /// The formatted string. static unsafe string FormatPrintF(string format, object arg) { // Determine how much space is needed to store the formatted string. string stringArg = arg as string; int neededLength = stringArg != null ? snprintf(null, IntPtr.Zero, format, stringArg) : snprintf(null, IntPtr.Zero, format, (int)arg); if (neededLength == 0) return string.Empty; if (neededLength < 0) throw new InvalidOperationException("The printf operation failed"); // Allocate the needed space, format into it, and return the data as a string. byte[] bytes = new byte[neededLength + 1]; // extra byte for the null terminator fixed (byte* ptr = bytes){ int length = stringArg != null ? snprintf(ptr, (IntPtr)bytes.Length, format, stringArg) : snprintf(ptr, (IntPtr)bytes.Length, format, (int)arg); if (length != neededLength) { throw new InvalidOperationException("Invalid printf operation"); } } return StringFromAsciiBytes(bytes, 0, neededLength); } /// Gets the lazily-initialized dynamic or static variables collection, based on the supplied variable name. /// The name of the variable. /// The lazily-initialized dynamic variables collection. /// The lazily-initialized static variables collection. /// The index to use to index into the variables. /// The variables collection. private static FormatParam[] GetDynamicOrStaticVariables( char c, ref FormatParam[] dynamicVars, ref FormatParam[] staticVars, out int index) { if (c >= 'A' && c <= 'Z'){ index = c - 'A'; return staticVars ?? (staticVars = new FormatParam[26]); // one slot for each letter of alphabet } else if (c >= 'a' && c <= 'z') { index = c - 'a'; return dynamicVars ?? (dynamicVars = new FormatParam[26]); // one slot for each letter of alphabet } else throw new InvalidOperationException("Terminfo database contains invalid values"); } /// /// Represents a parameter to a terminfo formatting string. /// It is a discriminated union of either an integer or a string, /// with characters represented as integers. /// public struct FormatParam { /// The integer stored in the parameter. private readonly int _int32; /// The string stored in the parameter. private readonly string _string; // null means an Int32 is stored /// Initializes the parameter with an integer value. /// The value to be stored in the parameter. public FormatParam(Int32 value) : this(value, null) { } /// Initializes the parameter with a string value. /// The value to be stored in the parameter. public FormatParam(String value) : this(0, value ?? string.Empty) { } /// Initializes the parameter. /// The integer value. /// The string value. private FormatParam(Int32 intValue, String stringValue) { _int32 = intValue; _string = stringValue; } /// Implicit converts an integer into a parameter. public static implicit operator FormatParam(int value) { return new FormatParam(value); } /// Implicit converts a string into a parameter. public static implicit operator FormatParam(string value) { return new FormatParam(value); } /// Gets the integer value of the parameter. If a string was stored, 0 is returned. public int Int32 { get { return _int32; } } /// Gets the string value of the parameter. If an Int32 or a null String were stored, an empty string is returned. public string String { get { return _string ?? string.Empty; } } /// Gets the string or the integer value as an object. public object Object { get { return _string ?? (object)_int32; } } } /// Provides a basic stack data structure. /// Specifies the type of data in the stack. private sealed class LowLevelStack { private const int DefaultSize = 4; private FormatParam[] _arr; private int _count; public LowLevelStack() { _arr = new FormatParam[DefaultSize]; } public FormatParam Pop() { if (_count == 0) throw new InvalidOperationException("Terminfo: Invalid Stack"); var item = _arr[--_count]; _arr[_count] = default(FormatParam); return item; } public void Push(FormatParam item) { if (_arr.Length == _count){ var newArr = new FormatParam[_arr.Length * 2]; Array.Copy(_arr, 0, newArr, 0, _arr.Length); _arr = newArr; } _arr[_count++] = item; } public void Clear() { Array.Clear(_arr, 0, _count); _count = 0; } } } class ByteMatcher { Hashtable map = new Hashtable (); Hashtable starts = new Hashtable (); public void AddMapping (TermInfoStrings key, byte [] val) { if (val.Length == 0) return; map [val] = key; starts [(int) val [0]] = true; } public void Sort () { } public bool StartsWith (int c) { return (starts [c] != null); } public TermInfoStrings Match (char [] buffer, int offset, int length, out int used) { foreach (byte [] bytes in map.Keys) { for (int i = 0; i < bytes.Length && i < length; i++) { if ((char) bytes [i] != buffer [offset + i]) break; if (bytes.Length - 1 == i) { used = bytes.Length; return (TermInfoStrings) map [bytes]; } } } used = 0; return (TermInfoStrings) (-1); } } } #endif