Skip to content

Commit ca6d61e

Browse files
authored
Merge branch 'v2_develop' into v2_3875_tabview-left-right-alignment-feature
2 parents c932306 + 62641c8 commit ca6d61e

File tree

68 files changed

+2926
-630
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2926
-630
lines changed

Terminal.Gui/Application/Application.Initialization.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ internal static void InternalInit (
150150
try
151151
{
152152
MainLoop = Driver!.Init ();
153+
SubscribeDriverEvents ();
153154
}
154155
catch (InvalidOperationException ex)
155156
{
@@ -163,11 +164,6 @@ internal static void InternalInit (
163164
);
164165
}
165166

166-
Driver.SizeChanged += Driver_SizeChanged;
167-
Driver.KeyDown += Driver_KeyDown;
168-
Driver.KeyUp += Driver_KeyUp;
169-
Driver.MouseEvent += Driver_MouseEvent;
170-
171167
SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
172168

173169
SupportedCultures = GetSupportedCultures ();
@@ -176,6 +172,26 @@ internal static void InternalInit (
176172
InitializedChanged?.Invoke (null, new (init));
177173
}
178174

175+
internal static void SubscribeDriverEvents ()
176+
{
177+
ArgumentNullException.ThrowIfNull (Driver);
178+
179+
Driver.SizeChanged += Driver_SizeChanged;
180+
Driver.KeyDown += Driver_KeyDown;
181+
Driver.KeyUp += Driver_KeyUp;
182+
Driver.MouseEvent += Driver_MouseEvent;
183+
}
184+
185+
internal static void UnsubscribeDriverEvents ()
186+
{
187+
ArgumentNullException.ThrowIfNull (Driver);
188+
189+
Driver.SizeChanged -= Driver_SizeChanged;
190+
Driver.KeyDown -= Driver_KeyDown;
191+
Driver.KeyUp -= Driver_KeyUp;
192+
Driver.MouseEvent -= Driver_MouseEvent;
193+
}
194+
179195
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
180196
private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
181197
private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }

Terminal.Gui/Application/Application.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,7 @@ internal static void ResetState (bool ignoreDisposed = false)
177177
// Driver stuff
178178
if (Driver is { })
179179
{
180-
Driver.SizeChanged -= Driver_SizeChanged;
181-
Driver.KeyDown -= Driver_KeyDown;
182-
Driver.KeyUp -= Driver_KeyUp;
183-
Driver.MouseEvent -= Driver_MouseEvent;
180+
UnsubscribeDriverEvents ();
184181
Driver?.End ();
185182
Driver = null;
186183
}

Terminal.Gui/Application/MainLoop.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ internal void RunIteration ()
268268
}
269269
}
270270

271+
RunAnsiScheduler ();
272+
271273
MainLoopDriver?.Iteration ();
272274

273275
bool runIdle;
@@ -283,6 +285,11 @@ internal void RunIteration ()
283285
}
284286
}
285287

288+
private void RunAnsiScheduler ()
289+
{
290+
Application.Driver?.GetRequestScheduler ().RunSchedule ();
291+
}
292+
286293
/// <summary>Stops the main loop driver and calls <see cref="IMainLoopDriver.Wakeup"/>. Used only for unit tests.</summary>
287294
internal void Stop ()
288295
{
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#nullable enable
2+
namespace Terminal.Gui;
3+
4+
/// <summary>
5+
/// Describes an Ansi escape sequence. This is a 'blueprint'. If you
6+
/// want to send the sequence you should instead use <see cref="AnsiEscapeSequenceRequest"/>
7+
/// </summary>
8+
public class AnsiEscapeSequence
9+
{
10+
/// <summary>
11+
/// Request to send e.g. see
12+
/// <see>
13+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
14+
/// </see>
15+
/// </summary>
16+
public required string Request { get; init; }
17+
18+
/// <summary>
19+
/// <para>
20+
/// The terminator that uniquely identifies the type of response as responded
21+
/// by the console. e.g. for
22+
/// <see>
23+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Request</cref>
24+
/// </see>
25+
/// the terminator is
26+
/// <see>
27+
/// <cref>EscSeqUtils.CSI_SendDeviceAttributes.Terminator</cref>
28+
/// </see>
29+
/// .
30+
/// </para>
31+
/// <para>
32+
/// After sending a request, the first response with matching terminator will be matched
33+
/// to the oldest outstanding request.
34+
/// </para>
35+
/// </summary>
36+
public required string Terminator { get; init; }
37+
38+
39+
40+
/// <summary>
41+
/// The value expected in the response e.g.
42+
/// <see>
43+
/// <cref>EscSeqUtils.CSI_ReportTerminalSizeInChars.Value</cref>
44+
/// </see>
45+
/// which will have a 't' as terminator but also other different request may return the same terminator with a
46+
/// different value.
47+
/// </summary>
48+
public string? Value { get; init; }
49+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#nullable enable
2+
namespace Terminal.Gui;
3+
4+
/// <summary>
5+
/// Describes an ongoing ANSI request sent to the console.
6+
/// Use <see cref="ResponseReceived"/> to handle the response
7+
/// when console answers the request.
8+
/// </summary>
9+
public class AnsiEscapeSequenceRequest : AnsiEscapeSequence
10+
{
11+
/// <summary>
12+
/// Invoked when the console responds with an ANSI response code that matches the
13+
/// <see cref="AnsiEscapeSequence.Terminator"/>
14+
/// </summary>
15+
public required Action<string> ResponseReceived { get; init; }
16+
17+
/// <summary>
18+
/// Invoked if the console fails to responds to the ANSI response code
19+
/// </summary>
20+
public Action? Abandoned { get; init; }
21+
22+
23+
/// <summary>
24+
/// Sends the <see cref="Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>.
25+
/// Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if
26+
/// sending many requests.
27+
/// </summary>
28+
public void Send () { Application.Driver?.WriteRaw (Request); }
29+
30+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
#nullable enable
2+
using System.Collections.Concurrent;
3+
4+
namespace Terminal.Gui;
5+
6+
/// <summary>
7+
/// Manages <see cref="AnsiEscapeSequenceRequest"/> made to an <see cref="IAnsiResponseParser"/>.
8+
/// Ensures there are not 2+ outstanding requests with the same terminator, throttles request sends
9+
/// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from
10+
/// terminal).
11+
/// </summary>
12+
public class AnsiRequestScheduler
13+
{
14+
private readonly IAnsiResponseParser _parser;
15+
16+
/// <summary>
17+
/// Function for returning the current time. Use in unit tests to
18+
/// ensure repeatable tests.
19+
/// </summary>
20+
internal Func<DateTime> Now { get; set; }
21+
22+
private readonly HashSet<Tuple<AnsiEscapeSequenceRequest, DateTime>> _queuedRequests = new ();
23+
24+
internal IReadOnlyCollection<AnsiEscapeSequenceRequest> QueuedRequests => _queuedRequests.Select (r => r.Item1).ToList ();
25+
26+
/// <summary>
27+
/// <para>
28+
/// Dictionary where key is ansi request terminator and value is when we last sent a request for
29+
/// this terminator. Combined with <see cref="_throttle"/> this prevents hammering the console
30+
/// with too many requests in sequence which can cause console to freeze as there is no space for
31+
/// regular screen drawing / mouse events etc to come in.
32+
/// </para>
33+
/// <para>
34+
/// When user exceeds the throttle, new requests accumulate in <see cref="_queuedRequests"/> (i.e. remain
35+
/// queued).
36+
/// </para>
37+
/// </summary>
38+
private readonly ConcurrentDictionary<string, DateTime> _lastSend = new ();
39+
40+
/// <summary>
41+
/// Number of milliseconds after sending a request that we allow
42+
/// another request to go out.
43+
/// </summary>
44+
private readonly TimeSpan _throttle = TimeSpan.FromMilliseconds (100);
45+
46+
private readonly TimeSpan _runScheduleThrottle = TimeSpan.FromMilliseconds (100);
47+
48+
/// <summary>
49+
/// If console has not responded to a request after this period of time, we assume that it is never going
50+
/// to respond. Only affects when we try to send a new request with the same terminator - at which point
51+
/// we tell the parser to stop expecting the old request and start expecting the new request.
52+
/// </summary>
53+
private readonly TimeSpan _staleTimeout = TimeSpan.FromSeconds (1);
54+
55+
private readonly DateTime _lastRun;
56+
57+
/// <summary>
58+
/// Creates a new instance.
59+
/// </summary>
60+
/// <param name="parser"></param>
61+
/// <param name="now"></param>
62+
public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = null)
63+
{
64+
_parser = parser;
65+
Now = now ?? (() => DateTime.Now);
66+
_lastRun = Now ();
67+
}
68+
69+
/// <summary>
70+
/// Sends the <paramref name="request"/> immediately or queues it if there is already
71+
/// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>.
72+
/// </summary>
73+
/// <param name="request"></param>
74+
/// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns>
75+
public bool SendOrSchedule (AnsiEscapeSequenceRequest request) { return SendOrSchedule (request, true); }
76+
77+
private bool SendOrSchedule (AnsiEscapeSequenceRequest request, bool addToQueue)
78+
{
79+
if (CanSend (request, out ReasonCannotSend reason))
80+
{
81+
Send (request);
82+
83+
return true;
84+
}
85+
86+
if (reason == ReasonCannotSend.OutstandingRequest)
87+
{
88+
// If we can evict an old request (no response from terminal after ages)
89+
if (EvictStaleRequests (request.Terminator))
90+
{
91+
// Try again after evicting
92+
if (CanSend (request, out _))
93+
{
94+
Send (request);
95+
96+
return true;
97+
}
98+
}
99+
}
100+
101+
if (addToQueue)
102+
{
103+
_queuedRequests.Add (Tuple.Create (request, Now ()));
104+
}
105+
106+
return false;
107+
}
108+
109+
private void EvictStaleRequests ()
110+
{
111+
foreach (string stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
112+
{
113+
EvictStaleRequests (stale);
114+
}
115+
}
116+
117+
private bool IsStale (DateTime dt) { return Now () - dt > _staleTimeout; }
118+
119+
/// <summary>
120+
/// Looks to see if the last time we sent <paramref name="withTerminator"/>
121+
/// is a long time ago. If so we assume that we will never get a response and
122+
/// can proceed with a new request for this terminator (returning <see langword="true"/>).
123+
/// </summary>
124+
/// <param name="withTerminator"></param>
125+
/// <returns></returns>
126+
private bool EvictStaleRequests (string withTerminator)
127+
{
128+
if (_lastSend.TryGetValue (withTerminator, out DateTime dt))
129+
{
130+
if (IsStale (dt))
131+
{
132+
_parser.StopExpecting (withTerminator, false);
133+
134+
return true;
135+
}
136+
}
137+
138+
return false;
139+
}
140+
141+
/// <summary>
142+
/// Identifies and runs any <see cref="_queuedRequests"/> that can be sent based on the
143+
/// current outstanding requests of the parser.
144+
/// </summary>
145+
/// <param name="force">
146+
/// Repeated requests to run the schedule over short period of time will be ignored.
147+
/// Pass <see langword="true"/> to override this behaviour and force evaluation of outstanding requests.
148+
/// </param>
149+
/// <returns>
150+
/// <see langword="true"/> if a request was found and run. <see langword="false"/>
151+
/// if no outstanding requests or all have existing outstanding requests underway in parser.
152+
/// </returns>
153+
public bool RunSchedule (bool force = false)
154+
{
155+
if (!force && Now () - _lastRun < _runScheduleThrottle)
156+
{
157+
return false;
158+
}
159+
160+
// Get oldest request
161+
Tuple<AnsiEscapeSequenceRequest, DateTime>? opportunity = _queuedRequests.MinBy (r => r.Item2);
162+
163+
if (opportunity != null)
164+
{
165+
// Give it another go
166+
if (SendOrSchedule (opportunity.Item1, false))
167+
{
168+
_queuedRequests.Remove (opportunity);
169+
170+
return true;
171+
}
172+
}
173+
174+
EvictStaleRequests ();
175+
176+
return false;
177+
}
178+
179+
private void Send (AnsiEscapeSequenceRequest r)
180+
{
181+
_lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ());
182+
_parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, false);
183+
r.Send ();
184+
}
185+
186+
private bool CanSend (AnsiEscapeSequenceRequest r, out ReasonCannotSend reason)
187+
{
188+
if (ShouldThrottle (r))
189+
{
190+
reason = ReasonCannotSend.TooManyRequests;
191+
192+
return false;
193+
}
194+
195+
if (_parser.IsExpecting (r.Terminator))
196+
{
197+
reason = ReasonCannotSend.OutstandingRequest;
198+
199+
return false;
200+
}
201+
202+
reason = default (ReasonCannotSend);
203+
204+
return true;
205+
}
206+
207+
private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
208+
{
209+
if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
210+
{
211+
return Now () - value < _throttle;
212+
}
213+
214+
return false;
215+
}
216+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#nullable enable
2+
namespace Terminal.Gui;
3+
4+
internal record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned)
5+
{
6+
public bool Matches (string cur) { return cur.EndsWith (Terminator); }
7+
}

0 commit comments

Comments
 (0)