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