Skip to content

Commit 56eecfd

Browse files
committed
Add configurable update wait for Live and Run
1 parent 573ba39 commit 56eecfd

File tree

7 files changed

+115
-2
lines changed

7 files changed

+115
-2
lines changed

site/docs/hosting.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ Terminal.Live(
5656
options: new TerminalLiveOptions { EnableMouse = true, MouseMode = TerminalMouseMode.Move });
5757
```
5858

59+
### Update tick wait (default: 1ms)
60+
61+
`Terminal.Live(...)` and `Terminal.Run(...)` use a 1ms wait between loop ticks by default.
62+
You can increase this wait if you want to reduce update frequency and CPU usage:
63+
64+
```csharp
65+
Terminal.Live(
66+
visual,
67+
onUpdate: () => TerminalLoopResult.Continue,
68+
options: new TerminalLiveOptions { UpdateWaitDuration = TimeSpan.FromMilliseconds(20) });
69+
70+
Terminal.Run(
71+
visual,
72+
onUpdate: () => TerminalLoopResult.Continue,
73+
options: new TerminalRunOptions { UpdateWaitDuration = TimeSpan.FromMilliseconds(20) });
74+
```
75+
76+
Larger values can make animations (for example spinners) update less smoothly.
77+
5978
### Filling the viewport height
6079

6180
Inline live regions measure with an "infinite" height by default, so a simple visual like a `VStack("Hello")` will

src/XenoAtom.Terminal.UI.Tests/TerminalExtensionsTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the BSD-Clause 2 license.
33
// See license.txt file in the project root for full license information.
44

5+
using System.Diagnostics;
56
using XenoAtom.Terminal;
67
using XenoAtom.Terminal.Backends;
78
using XenoAtom.Terminal.UI.Controls;
@@ -141,4 +142,58 @@ await session.Instance.LiveAsync(root, async _ =>
141142
StringAssert.Contains(screen.GetText(), "Count: 3");
142143
StringAssert.Contains(screen.GetText(), "Done");
143144
}
145+
146+
[TestMethod]
147+
public void Live_Options_UpdateWaitDuration_IsApplied()
148+
{
149+
var backend = new InMemoryTerminalBackend(new TerminalSize(30, 10));
150+
using var session = Terminal.Open(backend, new TerminalOptions { ImplicitStartInput = true }, force: true);
151+
152+
var tickCount = 0;
153+
var wait = TimeSpan.FromMilliseconds(35);
154+
var stopwatch = Stopwatch.StartNew();
155+
156+
session.Instance.Live(
157+
new TextBlock("Wait test"),
158+
_ =>
159+
{
160+
tickCount++;
161+
return tickCount >= 2 ? TerminalLoopResult.Stop : TerminalLoopResult.Continue;
162+
},
163+
new TerminalLiveOptions { UpdateWaitDuration = wait });
164+
165+
stopwatch.Stop();
166+
167+
Assert.AreEqual(2, tickCount);
168+
Assert.IsTrue(
169+
stopwatch.Elapsed >= TimeSpan.FromMilliseconds(50),
170+
$"Expected configured wait duration to slow down loop ticks. Elapsed: {stopwatch.Elapsed}.");
171+
}
172+
173+
[TestMethod]
174+
public void Run_Options_UpdateWaitDuration_IsApplied()
175+
{
176+
var backend = new InMemoryTerminalBackend(new TerminalSize(30, 10));
177+
using var session = Terminal.Open(backend, new TerminalOptions { ImplicitStartInput = true }, force: true);
178+
179+
var tickCount = 0;
180+
var wait = TimeSpan.FromMilliseconds(35);
181+
var stopwatch = Stopwatch.StartNew();
182+
183+
session.Instance.Run(
184+
new TextBlock("Wait test"),
185+
_ =>
186+
{
187+
tickCount++;
188+
return tickCount >= 2 ? TerminalLoopResult.Stop : TerminalLoopResult.Continue;
189+
},
190+
new TerminalRunOptions { UpdateWaitDuration = wait });
191+
192+
stopwatch.Stop();
193+
194+
Assert.AreEqual(2, tickCount);
195+
Assert.IsTrue(
196+
stopwatch.Elapsed >= TimeSpan.FromMilliseconds(50),
197+
$"Expected configured wait duration to slow down loop ticks. Elapsed: {stopwatch.Elapsed}.");
198+
}
144199
}

src/XenoAtom.Terminal.UI/TerminalApp.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ public TerminalApp(Visual root, TerminalInstance? terminal = null, TerminalAppOp
194194
ArgumentNullException.ThrowIfNull(root);
195195
_terminal = terminal ?? global::XenoAtom.Terminal.Terminal.Instance;
196196
_options = options ?? new TerminalAppOptions();
197+
if (_options.UpdateWaitDuration < TimeSpan.Zero)
198+
{
199+
throw new ArgumentOutOfRangeException(nameof(options), "The update wait duration cannot be negative.");
200+
}
197201

198202
ContentRoot = root;
199203
if (_options.HostKind == TerminalHostKind.Fullscreen)
@@ -651,7 +655,11 @@ private void RunCore(CancellationToken cancellationToken, ManualResetEventSlim?
651655
while (!token.IsCancellationRequested)
652656
{
653657
Tick();
654-
Thread.Sleep(1);
658+
var updateWaitDuration = _options.UpdateWaitDuration;
659+
if (updateWaitDuration > TimeSpan.Zero)
660+
{
661+
Thread.Sleep(updateWaitDuration);
662+
}
655663
}
656664
}
657665
finally

src/XenoAtom.Terminal.UI/TerminalAppOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ public sealed class TerminalAppOptions
7777
/// by setting <see cref="Styling.CultureStyle.Key"/>.
7878
/// </remarks>
7979
public CultureInfo Culture { get; init; } = CultureInfo.InvariantCulture;
80+
81+
/// <summary>
82+
/// Gets the wait duration between host loop ticks.
83+
/// </summary>
84+
/// <remarks>
85+
/// The default is 1ms to keep animation updates responsive while yielding the CPU.
86+
/// Increase this value to reduce CPU usage when frequent updates are not required.
87+
/// </remarks>
88+
public global::System.TimeSpan UpdateWaitDuration { get; init; } = global::System.TimeSpan.FromMilliseconds(1);
8089
}
8190

8291
/// <summary>

src/XenoAtom.Terminal.UI/TerminalExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ public TerminalInstance Live(Visual visual, Func<TerminalRunningContext, Termina
351351
Culture = options.Culture ?? CultureInfo.InvariantCulture,
352352
EnableMouse = options.EnableMouse,
353353
MouseMode = options.MouseMode,
354+
UpdateWaitDuration = options.UpdateWaitDuration,
354355
};
355356
RunHostedAsync(instance, visual, appOptions, onUpdate, CancellationToken.None)
356357
.AsTask()
@@ -418,6 +419,7 @@ public async ValueTask<TerminalInstance> LiveAsync(Visual visual, Func<TerminalR
418419
Culture = options.Culture ?? CultureInfo.InvariantCulture,
419420
EnableMouse = options.EnableMouse,
420421
MouseMode = options.MouseMode,
422+
UpdateWaitDuration = options.UpdateWaitDuration,
421423
};
422424
await RunHostedAsync(instance, visual, appOptions, onUpdate, cancellationToken).ConfigureAwait(false);
423425

@@ -436,6 +438,7 @@ public async ValueTask<TerminalInstance> LiveAsync(Visual visual, Func<TerminalR
436438
Culture = options.Culture ?? CultureInfo.InvariantCulture,
437439
EnableMouse = options.EnableMouse,
438440
MouseMode = options.MouseMode,
441+
UpdateWaitDuration = options.UpdateWaitDuration,
439442
};
440443
await RunHostedAsync(instance, visual, appOptions, onUpdate, cancellationToken).ConfigureAwait(false);
441444

@@ -459,6 +462,7 @@ public TerminalInstance Run(Visual visual, Func<TerminalRunningContext, Terminal
459462
HostKind = TerminalHostKind.Fullscreen,
460463
ExitGesture = options.ExitGesture,
461464
Culture = options.Culture ?? CultureInfo.InvariantCulture,
465+
UpdateWaitDuration = options.UpdateWaitDuration,
462466
};
463467
RunHostedAsync(instance, visual, appOptions, onUpdate, CancellationToken.None)
464468
.AsTask()
@@ -485,6 +489,7 @@ public async ValueTask<TerminalInstance> RunAsync(Visual visual, Func<TerminalRu
485489
HostKind = TerminalHostKind.Fullscreen,
486490
ExitGesture = options.ExitGesture,
487491
Culture = options.Culture ?? CultureInfo.InvariantCulture,
492+
UpdateWaitDuration = options.UpdateWaitDuration,
488493
};
489494
await RunHostedAsync(instance, visual, appOptions, onUpdate, cancellationToken).ConfigureAwait(false);
490495

@@ -508,6 +513,7 @@ public async ValueTask<TerminalInstance> RunAsync(Visual visual, Func<TerminalRu
508513
HostKind = TerminalHostKind.Fullscreen,
509514
ExitGesture = options.ExitGesture,
510515
Culture = options.Culture ?? CultureInfo.InvariantCulture,
516+
UpdateWaitDuration = options.UpdateWaitDuration,
511517
};
512518
await RunHostedAsync(instance, visual, appOptions, onUpdate, cancellationToken).ConfigureAwait(false);
513519

src/XenoAtom.Terminal.UI/TerminalLiveOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,12 @@ public readonly record struct TerminalLiveOptions()
3333
/// Gets the mouse reporting mode used when <see cref="EnableMouse"/> is <see langword="true"/>.
3434
/// </summary>
3535
public TerminalMouseMode MouseMode { get; init; } = TerminalMouseMode.Move;
36+
37+
/// <summary>
38+
/// Gets the wait duration between update ticks.
39+
/// </summary>
40+
/// <remarks>
41+
/// The default is 1ms. Increase this value to reduce update frequency and CPU usage.
42+
/// </remarks>
43+
public global::System.TimeSpan UpdateWaitDuration { get; init; } = global::System.TimeSpan.FromMilliseconds(1);
3644
}

src/XenoAtom.Terminal.UI/TerminalRunOptions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace XenoAtom.Terminal.UI;
88
/// Provides options for fullscreen hosting via
99
/// <see cref="TerminalExtensions.Run(Visual, System.Func{TerminalRunningContext, TerminalLoopResult}, TerminalRunOptions)"/>.
1010
/// </summary>
11-
public readonly record struct TerminalRunOptions
11+
public readonly record struct TerminalRunOptions()
1212
{
1313
/// <summary>
1414
/// Gets the culture used for formatting values (for example when converting numbers to strings).
@@ -25,4 +25,12 @@ public readonly record struct TerminalRunOptions
2525
/// When <see langword="null"/>, the default gesture is used (<c>Ctrl+Q</c>).
2626
/// </remarks>
2727
public global::XenoAtom.Terminal.UI.Input.KeyGesture? ExitGesture { get; init; }
28+
29+
/// <summary>
30+
/// Gets the wait duration between update ticks.
31+
/// </summary>
32+
/// <remarks>
33+
/// The default is 1ms. Increase this value to reduce update frequency and CPU usage.
34+
/// </remarks>
35+
public global::System.TimeSpan UpdateWaitDuration { get; init; } = global::System.TimeSpan.FromMilliseconds(1);
2836
}

0 commit comments

Comments
 (0)