Repository: gui-cs/Terminal.Gui (2026-04-04T00:39:44Z)

Issue:
SpinnerView AutoSpin does not advance animation
## Describe the bug

I noticed a spinner in my app was no longer functioning. I ran the SpinnerStyles scenario and it is not working there either.

## To Reproduce

1. Run the following code:
```csharp
namespace UICatalog.Scenarios;

[ScenarioMetadata ("Spinner Demo", "Minimal SpinnerView demo with auto-spin.")]
[ScenarioCategory ("Controls")]
[ScenarioCategory ("Progress")]
public class SpinnerDemo : Scenario
{
    public override void Main ()
    {
        using IApplication app = Application.Create ();
        app.Init ();

        using Window main = new () { Title = GetQuitKeyAndName () };

        SpinnerView spinner = new ()
        {
            X = Pos.Center (),
            Y = Pos.Center (),
            AutoSpin = true
        };

        main.Add (spinner);

        app.Run (main);
    }
}
```

2. Expected behavior: There should be a spinner, spinning.
3. Actual behavior: The first character of the spinner sequence is displayed, without changing.

## Note:
Manually advancing the spinner with AdvanceAnimation() does work.

## Environment

Tested on Windows 11 with dotnet, windows, and ansi drivers.

### Terminal.Gui Version

v2.0.0-beta.1-113-g6c4f01d6d

## Screenshots, GIFs, or Terminal Output

![Image](https://github.com/user-attachments/assets/3fa658b7-4d5f-4dfb-b23e-f88766f357e4)


Discussion:

Patch:
diff --git a/Examples/UICatalog/Scenarios/MinimalSpinnerDemo.cs b/Examples/UICatalog/Scenarios/MinimalSpinnerDemo.cs
new file mode 100644
index 0000000000..534b6ec363
--- /dev/null
+++ b/Examples/UICatalog/Scenarios/MinimalSpinnerDemo.cs
@@ -0,0 +1,68 @@
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("Spinner Demo", "Minimal SpinnerView demo with auto-spin.")]
+[ScenarioCategory ("Controls")]
+[ScenarioCategory ("Progress")]
+public class MinimalSpinnerDemo : Scenario
+{
+    public override void Main ()
+    {
+        using IApplication app = Application.Create ();
+        app.Init ();
+
+        using Window main = new () { Title = GetQuitKeyAndName () };
+
+        SpinnerView spinner = new ()
+        {
+            X = Pos.Center (),
+            Y = Pos.Center (),
+            AutoSpin = true
+        };
+
+        CheckBox chkAutoSpin = new ()
+        {
+            Text = "AutoSpin",
+            X = Pos.Center (),
+            Y = Pos.Bottom (spinner) + 1,
+            Value = CheckState.Checked
+        };
+        chkAutoSpin.ValueChanged += (_, e) => spinner.AutoSpin = e.NewValue == CheckState.Checked;
+
+        Label lblSequence = new ()
+        {
+            Text = "Sequence (comma-separated):",
+            X = Pos.Center (),
+            Y = Pos.Bottom (chkAutoSpin) + 1
+        };
+
+        TextField tfSequence = new ()
+        {
+            Text = string.Join (",", spinner.Sequence),
+            X = Pos.Center (),
+            Y = Pos.Bottom (lblSequence),
+            Width = 30
+        };
+        tfSequence.Accepting += (_, _) =>
+                                {
+                                    string [] frames = tfSequence.Text
+                                                                 .Split (',', StringSplitOptions.RemoveEmptyEntries);
+
+                                    if (frames.Length > 0)
+                                    {
+                                        spinner.Sequence = frames;
+                                    }
+                                };
+
+        Button btnAdvance = new ()
+        {
+            Text = "Advance",
+            X = Pos.Center (),
+            Y = Pos.Bottom (tfSequence) + 1
+        };
+        btnAdvance.Accepting += (_, _) => spinner.AdvanceAnimation ();
+
+        main.Add (spinner, chkAutoSpin, lblSequence, tfSequence, btnAdvance);
+
+        app.Run (main);
+    }
+}
diff --git a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs
index 127c0fa50c..c297a7df82 100644
--- a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs
+++ b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs
@@ -18,6 +18,7 @@ public class SpinnerView : View, IDesignable
     private const int DEFAULT_DELAY = 130;
     private static readonly SpinnerStyle DEFAULT_STYLE = new SpinnerStyle.Line ();
 
+    private bool _autoSpin;
     private bool _bounce = DEFAULT_STYLE.SpinBounce;
     private bool _bounceReverse;
     private int _currentIdx;
@@ -46,9 +47,11 @@ public SpinnerView ()
     /// </summary>
     public bool AutoSpin
     {
-        get => _timeout != null;
+        get => _autoSpin;
         set
         {
+            _autoSpin = value;
+
             if (value)
             {
                 AddAutoSpinTimeout ();
@@ -199,6 +202,17 @@ protected override void Dispose (bool disposing)
         base.Dispose (disposing);
     }
 
+    /// <inheritdoc/>
+    public override void EndInit ()
+    {
+        base.EndInit ();
+
+        if (_autoSpin)
+        {
+            AddAutoSpinTimeout ();
+        }
+    }
+
     private void AddAutoSpinTimeout ()
     {
         // Only add timeout if we are initialized and not already spinning


Test patch:
diff --git a/Tests/IntegrationTests/FluentTests/SpinnerViewTests.cs b/Tests/IntegrationTests/FluentTests/SpinnerViewTests.cs
new file mode 100644
index 0000000000..fb465a1e64
--- /dev/null
+++ b/Tests/IntegrationTests/FluentTests/SpinnerViewTests.cs
@@ -0,0 +1,43 @@
+// Copilot
+using System.Reflection;
+using AppTestHelpers;
+using AppTestHelpers.XunitHelpers;
+
+namespace IntegrationTests;
+
+/// <summary>
+///     Integration tests for <see cref="SpinnerView"/> that require a running application.
+/// </summary>
+public class SpinnerViewTests (ITestOutputHelper outputHelper) : TestsAllDrivers
+{
+    private readonly TextWriter _out = new TestOutputWriter (outputHelper);
+
+    [Theory]
+    [MemberData (nameof (GetAllDriverNames))]
+    public void AutoSpin_SetBeforeAppRun_TimeoutRegisteredAfterEndInit (string d)
+    {
+        // Regression test for https://github.com/gui-cs/Terminal.Gui/issues/4879.
+        //
+        // Before the fix: AutoSpin = true set before App.Run() left _timeout null because
+        // AddAutoSpinTimeout() silently exited early (App was null). The spinner showed its
+        // first frame and never moved.
+        //
+        // After the fix: EndInit() calls AddAutoSpinTimeout() again once App is set, so the
+        // timeout IS registered and the spinner animates.
+
+        SpinnerView spinner = new () { AutoSpin = true };
+
+        FieldInfo timeoutField = typeof (SpinnerView)
+            .GetField ("_timeout", BindingFlags.NonPublic | BindingFlags.Instance)!;
+
+        // Before being part of a running application, _timeout must be null.
+        Assert.Null (timeoutField.GetValue (spinner));
+
+        // Add the spinner to a running Window. Because the Window is already initialised,
+        // View.Add() triggers BeginInit()/EndInit() on the spinner immediately, with App set.
+        using AppTestHelper _ = With.A<Window> (40, 10, d, _out).Add (spinner);
+
+        // EndInit() should have called AddAutoSpinTimeout(), which registers the timeout.
+        Assert.NotNull (timeoutField.GetValue (spinner));
+    }
+}
diff --git a/Tests/UnitTestsParallelizable/Views/SpinnerViewTests.cs b/Tests/UnitTestsParallelizable/Views/SpinnerViewTests.cs
index b595b39610..f4c92ce987 100644
--- a/Tests/UnitTestsParallelizable/Views/SpinnerViewTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/SpinnerViewTests.cs
@@ -37,6 +37,26 @@ public void AdvanceAnimation_DefaultDelay_ThrottlesRapidCalls ()
         Assert.Equal (frame1, frame2);
     }
 
+    [Fact]
+    public void AutoSpin_SetBeforeEndInit_GetterReturnsTrueWithNoApp ()
+    {
+        // Regression test for https://github.com/gui-cs/Terminal.Gui/issues/4879
+        // Before the fix AutoSpin returned `_timeout != null`. When App is null the timeout
+        // can never be registered, so the getter falsely returned false even though the
+        // caller had set AutoSpin = true. The fix uses a dedicated _autoSpin backing field.
+        SpinnerView spinner = new () { AutoSpin = true };
+
+        // App is null here (no running application), so _timeout is null.
+        // The getter must still report true based on the backing field.
+        Assert.True (spinner.AutoSpin);
+
+        spinner.BeginInit ();
+        spinner.EndInit ();
+
+        // After init the intent must be preserved.
+        Assert.True (spinner.AutoSpin);
+    }
+
     [Fact]
     public void AdvanceAnimation_ZeroDelay_AdvancesFrame ()
     {