PromptPlus

PromptPlus # PromptPlus ## **Task — Operations** [![NuGet](https://img.shields.io/badge/NuGet-PromptPlus-blue)](https://www.nuget.org/packages/PromptPlus) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4)](https://dotnet.microsoft.com/)

← Back to HomeNext: Task — Styles →


How the Task control behaves while it runs: the action overloads, the isolated contexts, elapsed time, cancellation, and error handling.


Anatomy of the control

Processing  ⠹  00:00:03                   ← prompt + spinner + elapsed time
Running for 3 second(s)...                 ← ChangeDescription output (optional)
Saved!                                     ← Finish text (on completion)
Esc: cancel                               ← tooltip

The spinner appears with Spinner(...); the elapsed time with ShowElapsedTime(); the description with ChangeDescription. There are no editing keys — the only user action is Esc to abort (when enabled). Each region can be recolored — see Styles.


Choosing an action overload

Attach exactly one work delegate. There are six overloads across Action (sync) and ActionAsync (async), differing only in the data they carry:

Data shape Sync Async
No context Action(Action<CancellationToken>) ActionAsync(Func<CancellationToken, Task>)
Output only Action(Func<CancellationToken, IDictionary?>) ActionAsync(Func<CancellationToken, Task<IDictionary?>>)
Input + output Action(Func<IReadOnlyDictionary, CancellationToken, IDictionary?>) ActionAsync(Func<IReadOnlyDictionary, CancellationToken, Task<IDictionary?>>)

Prefer ActionAsync for I/O-bound work so the run loop stays responsive.


Isolated input & output contexts

The task works with two independent dictionaries:

They are isolated from each other: writing the output never mutates the input.

var context = new Dictionary<string, object?> { ["name"] = "PromptPlus", ["count"] = 10 };

var result = PromptPlus.Controls.Task("Computing")
    .Context(context)
    .ActionAsync(async (input, token) =>
    {
        await Task.Delay(1500, token).ConfigureAwait(false);
        int count = input.TryGetValue("count", out var raw) && raw is int c ? c : 0;
        return new Dictionary<string, object?>
        {
            ["result"] = count * 2,
            ["message"] = $"Processed {input["name"]}"
        };
    })
    .Run();

int doubled = result.Content.GetOutput<int>("result", out bool found);       // 20, true
string message = result.Content.GetOutput<string>("message", out _) ?? "";

GetOutput<T> returns default and sets found = false when the key is missing or the stored value is not a T.


Elapsed time

StateTask.ElapsedTime always reports how long the action ran, whether or not you displayed it. Call ShowElapsedTime(true, format) to render it live; the format defaults to hh\:mm\:ss and honors Culture(...).


Cancellation

Pass a CancellationToken to Run(token) to enforce a deadline, or let the user press Esc. Either way, the token your delegate receives is signalled; forward it to your awaits so the work unwinds:

using (var sw = new CancellationTokenSource(TimeSpan.FromSeconds(2)))
{
    var result = PromptPlus.Controls.Task("Long task", "Runs with a CancellationToken")
        .ShowElapsedTime()
        .ActionAsync(async token => await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false))
        .Run(sw.Token);
    // result.IsAborted == true
}

Errors

If the action throws, the control captures the exception rather than letting it propagate. The run ends, the Finish error text (or a default localized message) is shown, and the exception lands on StateTask.Exception:

var result = PromptPlus.Controls.Task("Fetching data")
    .Finish("Fetched!", "Fetch failed!")
    .ActionAsync(async token =>
    {
        await Task.Delay(1500, token).ConfigureAwait(false);
        throw new TimeoutException("Remote server did not respond");
    })
    .Run();

if (result.Content.Exception is not null)
    PromptPlus.Console.WriteLine($"{result.Content.Exception.GetType().Name}: {result.Content.Exception.Message}");

There is no separate status flag, but Exception is null does not by itself mean success: forwarding the token to your awaits (as recommended above) means a cancelled run throws OperationCanceledException inside the handler, which is caught silently — Exception stays null but IsAborted is true. Always check IsAborted first, and only then treat Exception is null as success. When an action throws before returning an output context, OutputContext is an empty dictionary, not null.


Spinners

Spinner(SpinnersType) shows a looping animation while the task runs. SpinnersType spans several families — braille/dots, lines/bars, shapes, toggles, arrows/motion, and emoji. On a terminal without Unicode support it automatically falls back to the Ascii spinner (- \ | /).

See Spinners for usage and the fallback rules, and the Spinner catalog for every spinner’s frames.


Options that change behavior

Set per instance via Options(...), or globally on PromptPlus.Config:

Option Effect on Task
EnabledAbortKey(false) Removes Esc — the task can only end by finishing/failing
HideAfterFinish(true) Erases the control after completion
ShowTooltip(false) Hides the keyboard-hint line
Prompt(...) / Description(...) Overrides the prompt / description text

Edge cases & gotchas


See also