diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
index 9ee447ad89..928cf29cee 100644
--- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
+++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
@@ -14,6 +14,8 @@ internal partial class ApplicationImpl
///
public bool Initialized { get; set; }
+ internal SynchronizationContext? SynchronizationContext { get; private set; }
+
///
public event EventHandler>? InitializedChanged;
@@ -79,7 +81,8 @@ public IApplication Init (string? driverName = null)
RaiseInitializedChanged (this, new EventArgs (true));
SubscribeDriverEvents ();
- SynchronizationContext.SetSynchronizationContext (new SynchronizationContext ());
+ SynchronizationContext = new MainLoopSyncContext (this);
+ SynchronizationContext.SetSynchronizationContext (SynchronizationContext);
_result = null;
diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs
index 3b4cbfbd1f..ae46f50c92 100644
--- a/Terminal.Gui/App/ApplicationImpl.Run.cs
+++ b/Terminal.Gui/App/ApplicationImpl.Run.cs
@@ -136,6 +136,9 @@ public void Invoke (Action action)
// Set the application reference in the runnable
runnable.SetApp (this);
+ // Set the synchronization context to MainLoopSyncContext for this application
+ SynchronizationContext.SetSynchronizationContext (SynchronizationContext);
+
// Ensure the mouse is ungrabbed
Mouse.UngrabMouse ();
diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
index 42819ecbd9..9894e3f648 100644
--- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
+++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs
@@ -71,8 +71,7 @@ public async Task StartInputTaskAsync (IApplication? app)
Task waitForSemaphore = _startupSemaphore.WaitAsync ();
// Wait for either the semaphore to be released or the input task to crash.
- // ReSharper disable once UseConfigureAwaitFalse
- Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask);
+ Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false);
// Check if the task was the input task and if it has failed.
if (completedTask == _inputTask)
diff --git a/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs b/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs
index b9375ccca4..7fbe940851 100644
--- a/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs
+++ b/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs
@@ -1,43 +1,78 @@
-#nullable disable
namespace Terminal.Gui.App;
-/////
-///// provides the sync context set while executing code in Terminal.Gui, to let
-///// users use async/await on their code
-/////
-//internal sealed class MainLoopSyncContext : SynchronizationContext
-//{
-// public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
-
-// public override void Post (SendOrPostCallback d, object state)
-// {
-// // Queue the task using the modern architecture
-// ApplicationImpl.Instance.Invoke (() => { d (state); });
-// }
-
-// //_mainLoop.Driver.Wakeup ();
-// public override void Send (SendOrPostCallback d, object state)
-// {
-// if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
-// {
-// d (state);
-// }
-// else
-// {
-// var wasExecuted = false;
-
-// ApplicationImpl.Instance.Invoke (
-// () =>
-// {
-// d (state);
-// wasExecuted = true;
-// }
-// );
-
-// while (!wasExecuted)
-// {
-// Thread.Sleep (15);
-// }
-// }
-// }
-//}
+///
+/// Provides the sync context set while executing code in Terminal.Gui, to let
+/// users use async/await on their code
+///
+internal sealed class MainLoopSyncContext : SynchronizationContext
+{
+ private readonly IApplication _app;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The application instance that owns the main loop.
+ public MainLoopSyncContext (IApplication app) => _app = app;
+
+ ///
+ public override SynchronizationContext CreateCopy () => new MainLoopSyncContext (_app);
+
+ ///
+ public override void Post (SendOrPostCallback d, object? state)
+ {
+ ArgumentNullException.ThrowIfNull (d);
+
+ // Queue the task using the modern architecture
+ _app.Invoke (() => d (state));
+ }
+
+ ///
+ public override void Send (SendOrPostCallback d, object? state)
+ {
+ ArgumentNullException.ThrowIfNull (d);
+
+ if (_app.MainThreadId == Thread.CurrentThread.ManagedThreadId)
+ {
+ d (state);
+
+ return;
+ }
+
+ object gate = new ();
+ bool wasExecuted = false;
+ Exception? error = null;
+
+ _app.Invoke (() =>
+ {
+ try
+ {
+ d (state);
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ }
+ finally
+ {
+ lock (gate)
+ {
+ wasExecuted = true;
+ Monitor.Pulse (gate);
+ }
+ }
+ });
+
+ lock (gate)
+ {
+ while (!wasExecuted)
+ {
+ Monitor.Wait (gate);
+ }
+ }
+
+ if (error is { })
+ {
+ System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (error).Throw ();
+ }
+ }
+}
diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
index 5febabda5f..99f9274063 100644
--- a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
+++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs
@@ -53,6 +53,124 @@ public void Dispose_Resets_SyncContext ()
Assert.Null (SynchronizationContext.Current);
}
+ [Fact]
+ public void Init_Posts_To_The_App_Main_Thread ()
+ {
+ IApplication app = Application.Create ();
+ app.Init (DriverRegistry.Names.ANSI);
+
+ int mainThreadId = app.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
+ int? callbackThreadId = null;
+ ManualResetEventSlim callbackCalled = new (false);
+
+ SynchronizationContext.Current!.Post (_ =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ callbackCalled.Set ();
+ },
+ null);
+
+ app.AddTimeout (TimeSpan.FromMilliseconds (100),
+ () =>
+ {
+ app.RequestStop ();
+
+ return false;
+ });
+
+ app.Run (new Window ());
+
+ Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
+ Assert.Equal (mainThreadId, callbackThreadId);
+
+ app.Dispose ();
+ }
+
+ [Fact]
+ public void Init_Posts_To_The_App_Instance_Main_Thread ()
+ {
+ IApplication app1 = Application.Create ();
+ app1.Init (DriverRegistry.Names.ANSI);
+
+ IApplication app2 = Application.Create ();
+ app2.Init (DriverRegistry.Names.ANSI);
+
+ int mainThreadId1 = app1.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
+ int? callbackThreadId1 = null;
+ SynchronizationContext? synchronizationContext1 = null;
+
+ ManualResetEventSlim callbackCalled = new (false);
+
+ app1.AddTimeout (TimeSpan.FromMilliseconds (100),
+ () =>
+ {
+ if (synchronizationContext1 is null)
+ {
+ synchronizationContext1 = SynchronizationContext.Current;
+ synchronizationContext1?.Post (_ =>
+ {
+ callbackThreadId1 = Thread.CurrentThread.ManagedThreadId;
+ callbackCalled.Set ();
+ },
+ null);
+
+ return true;
+ }
+ else
+ {
+ app1.RequestStop ();
+
+ return false;
+ }
+ });
+
+ app1.Run (new Window ());
+
+ Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
+ Assert.Equal (mainThreadId1, callbackThreadId1);
+ Assert.Equal (synchronizationContext1, SynchronizationContext.Current);
+
+ callbackCalled.Reset ();
+
+ int mainThreadId2 = app2.MainThreadId ?? Thread.CurrentThread.ManagedThreadId;
+ int? callbackThreadId2 = null;
+ SynchronizationContext? synchronizationContext2 = null;
+
+ app2.AddTimeout (TimeSpan.FromMilliseconds (100),
+ () =>
+ {
+ if (synchronizationContext2 is null)
+ {
+ synchronizationContext2 = SynchronizationContext.Current;
+ synchronizationContext2?.Post (_ =>
+ {
+ callbackThreadId2 = Thread.CurrentThread.ManagedThreadId;
+ callbackCalled.Set ();
+ },
+ null);
+
+ return true;
+ }
+ else
+ {
+ app2.RequestStop ();
+
+ return false;
+ }
+ });
+
+ app2.Run (new Window ());
+
+ Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken));
+ Assert.Equal (mainThreadId2, callbackThreadId2);
+ Assert.NotEqual (synchronizationContext1, SynchronizationContext.Current);
+ Assert.Equal (synchronizationContext2, SynchronizationContext.Current);
+ Assert.NotEqual (synchronizationContext1, synchronizationContext2);
+
+ app1.Dispose ();
+ app2.Dispose ();
+ }
+
[Fact]
public void Dispose_Alone_Does_Nothing ()
{