1 // 2 // Copyright (c) 2010-2023 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 using System; 8 using System.Collections.Generic; 9 using AntShell.Terminal; 10 using Antmicro.Renode.Utilities; 11 12 namespace Antmicro.Renode.UI 13 { 14 public class ConsoleIOSource : IActiveIOSource 15 { ConsoleIOSource()16 public ConsoleIOSource() 17 { 18 isInputRedirected = Console.IsInputRedirected; 19 if(!isInputRedirected) 20 { 21 Console.TreatControlCAsInput = true; 22 } 23 24 checker = new UTF8Checker(); 25 26 var inputHandler = new System.Threading.Thread(HandleInput) 27 { 28 IsBackground = true, 29 Name = "Console IO handler thread" 30 }; 31 32 inputHandler.Start(); 33 } 34 Dispose()35 public void Dispose() 36 { 37 } 38 Flush()39 public void Flush() 40 { 41 } 42 Pause()43 public void Pause() 44 { 45 // Required by IActiveIOSource interface 46 } 47 Resume()48 public void Resume() 49 { 50 // Required by IActiveIOSource interface 51 } 52 Write(byte b)53 public void Write(byte b) 54 { 55 if(checker.TryDecode(b, out var c)) 56 { 57 try 58 { 59 Console.Write(c); 60 } 61 catch(ArgumentOutOfRangeException) 62 { 63 // this sometimes happens in System.TermInfoDriver.SetCursorPosition 64 // no idea why, but there is not much we can do 65 ; 66 } 67 } 68 } 69 70 public bool IsAnythingAttached => (ByteRead != null); 71 72 public event Action<int> ByteRead; 73 HandleInput()74 private void HandleInput() 75 { 76 if(isInputRedirected) 77 { 78 RedirectedHandling(); 79 } 80 else 81 { 82 StandardHandling(); 83 } 84 } 85 RedirectedHandling()86 private void RedirectedHandling() 87 { 88 // For cases in which input has been redirected from a file 89 while(true) 90 { 91 ByteRead?.Invoke(Console.Read()); 92 } 93 } 94 StandardHandling()95 private void StandardHandling() 96 { 97 var mappings = new Dictionary<ConsoleKey, byte[]>() 98 { 99 { ConsoleKey.Enter, new [] { (byte)'\n' } }, 100 { ConsoleKey.UpArrow, new [] { ESCCode, CSICode, (byte)'A' } }, 101 { ConsoleKey.DownArrow, new [] { ESCCode, CSICode, (byte)'B' } }, 102 { ConsoleKey.RightArrow, new [] { ESCCode, CSICode, (byte)'C' } }, 103 { ConsoleKey.LeftArrow, new [] { ESCCode, CSICode, (byte)'D' } }, 104 { ConsoleKey.Delete, new [] { ESCCode, CSICode, (byte)'3', (byte)'~' } }, 105 { ConsoleKey.Home, new [] { ESCCode, CSICode, (byte)'H' } }, 106 { ConsoleKey.End, new [] { ESCCode, CSICode, (byte)'F' } }, 107 { ConsoleKey.Tab, new [] { (byte)'\t' } }, 108 { ConsoleKey.Backspace, new [] { (byte)127 } } 109 }; 110 111 while(true) 112 { 113 var key = Console.ReadKey(true); 114 if(key.Key == (ConsoleKey)0 && (key.Modifiers & ConsoleModifiers.Alt) != 0) 115 { 116 ByteRead?.Invoke(ESCCode); 117 ByteRead?.Invoke(CSICode); 118 continue; 119 } 120 121 // It seems that Mono inserts Vt100 escape sequences, but this is not a case in .NET 122 // Add handling for CtrlLeftArrow and CtrlRightArrow 123 if(key.Modifiers.HasFlag(ConsoleModifiers.Control)) 124 { 125 if(key.Key == ConsoleKey.RightArrow || key.Key == ConsoleKey.LeftArrow) 126 { 127 // Invoke correct Vt100 sequence for AntShell 128 ByteRead?.Invoke(ESCCode); 129 ByteRead?.Invoke(CSICode); 130 ByteRead?.Invoke('1'); 131 ByteRead?.Invoke(';'); 132 ByteRead?.Invoke('5'); 133 134 if(key.Key == ConsoleKey.LeftArrow) 135 { 136 ByteRead?.Invoke('D'); 137 } 138 else if(key.Key == ConsoleKey.RightArrow) 139 { 140 ByteRead?.Invoke('C'); 141 } 142 } 143 } 144 145 if(mappings.TryGetValue(key.Key, out var sequence)) 146 { 147 foreach(var b in sequence) 148 { 149 ByteRead?.Invoke(b); 150 } 151 } 152 else 153 { 154 foreach(var b in checker.UTF8Encoder.GetBytes(new [] { key.KeyChar })) 155 { 156 ByteRead?.Invoke(b); 157 } 158 } 159 } 160 } 161 162 private readonly UTF8Checker checker; 163 private readonly bool isInputRedirected; 164 165 private const byte ESCCode = 0x1B; 166 private const byte CSICode = 0x5B; 167 168 private class UTF8Checker 169 { TryDecode(byte b, out char c)170 public bool TryDecode(byte b, out char c) 171 { 172 if(state == State.Idle) 173 { 174 if(b <= 0x7F) 175 { 176 c = (char)b; 177 return true; 178 } 179 else 180 { 181 buffer.Enqueue(b); 182 if((b & 0xE0) == 0xC0) 183 { 184 state = State.Need1; 185 } 186 else if((b & 0xF0) == 0xE0) 187 { 188 state = State.Need2; 189 } 190 else if((b & 0xF8) == 0xF0) 191 { 192 state = State.Need3; 193 } 194 else if((b & 0xFC) == 0xF8) 195 { 196 state = State.Need4; 197 } 198 else 199 { 200 state = State.Need5; 201 } 202 } 203 } 204 else 205 { 206 buffer.Enqueue(b); 207 state = state - 1; 208 209 if(state == State.Idle) 210 { 211 c = encoder.GetString(buffer.DequeueAll())[0]; 212 return true; 213 } 214 } 215 216 c = default(char); 217 return false; 218 } 219 220 public System.Text.UTF8Encoding UTF8Encoder => encoder; 221 222 private State state; 223 private readonly Queue<byte> buffer = new Queue<byte>(); 224 private readonly System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding(); 225 226 private enum State 227 { 228 Idle = 0, 229 Need1 = 1, 230 Need2 = 2, 231 Need3 = 3, 232 Need4 = 4, 233 Need5 = 5, 234 } 235 } 236 } 237 } 238