Skip to content

Hosting & Dependency Injection

Termina integrates with Microsoft.Extensions.Hosting for dependency injection and application lifecycle management.

Basic Setup

csharp
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddTermina("/", termina =>
{
    termina.RegisterRoute<HomePage, HomeViewModel>("/");
    termina.RegisterRoute<SettingsPage, SettingsViewModel>("/settings");
});

var app = builder.Build();
await app.RunAsync();

Service Registration

AddTermina

The primary extension method for configuring Termina:

csharp
// With start page
builder.Services.AddTermina("/dashboard", termina =>
{
    termina.RegisterRoute<DashboardPage, DashboardViewModel>("/dashboard");
});

// Without start page (navigate manually)
builder.Services.AddTermina(termina =>
{
    termina.RegisterRoute<MainPage, MainViewModel>("main");
});

RegisterRoute

Register pages with route templates:

csharp
termina.RegisterRoute<TodoPage, TodoViewModel>("/todos");
termina.RegisterRoute<DetailPage, DetailViewModel>("/todos/{id:int}");

Control page lifecycle during navigation:

csharp
// Default: create new instances each time
termina.RegisterRoute<Page, ViewModel>("/path");

// Preserve state across navigations
termina.RegisterRoute<Page, ViewModel>(
    "/path",
    NavigationBehavior.PreserveState);

Dependency Injection

Pages and ViewModels support constructor injection:

csharp
public class TodoViewModel : ReactiveViewModel
{
    private readonly ITodoService _todoService;
    private readonly ILogger<TodoViewModel> _logger;

    public TodoViewModel(
        ITodoService todoService,
        ILogger<TodoViewModel> logger)
    {
        _todoService = todoService;
        _logger = logger;
    }
}

Register your services normally:

csharp
builder.Services.AddSingleton<ITodoService, TodoService>();
builder.Services.AddSingleton<IDataStore, JsonDataStore>();

Custom Input Sources

Console Input (Default)

Standard keyboard input from the terminal:

csharp
builder.Services.AddTerminaConsoleInput();

Virtual Input (Testing)

For automated testing with simulated input:

csharp
var inputSource = new VirtualInputSource();
builder.Services.AddTerminaVirtualInput(inputSource);

// Later, in tests:
inputSource.SendKey(ConsoleKey.Enter);
inputSource.SendKey(ConsoleKey.UpArrow);

Custom Factories

For complex initialization beyond DI:

csharp
termina.RegisterRoute<CustomPage, CustomViewModel>(
    "/custom",
    pageFactory: sp => new CustomPage(sp.GetService<IConfig>()),
    viewModelFactory: sp => new CustomViewModel(
        sp.GetRequiredService<IDataService>(),
        customSetting: true));

What Gets Registered

AddTermina registers these services:

ServiceLifetimeDescription
IAnsiTerminalSingletonTerminal abstraction
TerminaApplicationSingletonMain application
TerminaHostedServiceHostedServiceRuns the app
TPageTransientEach page type
TViewModelTransientEach ViewModel type

Application Lifecycle

  1. Host Starts - IHostedService.StartAsync called
  2. Initial Navigation - Route to start page
  3. Input Loop - Process keyboard/mouse events
  4. Navigation - Create/activate pages as needed
  5. Shutdown - Shutdown() called or Ctrl+C received
  6. Host Stops - Graceful shutdown

Source Code

View TerminaServiceCollectionExtensions
csharp
// Copyright (c) Petabridge, LLC. All rights reserved.
// Licensed under the Apache 2.0 license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Termina.Input;
using Termina.Terminal;

namespace Termina.Hosting;

/// <summary>
/// Extension methods for registering Termina with dependency injection.
/// </summary>
public static class TerminaServiceCollectionExtensions
{
    /// <summary>
    /// Adds Termina TUI framework services to the service collection.
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="configure">Action to configure page registrations.</param>
    /// <returns>The service collection for chaining.</returns>
    /// <example>
    /// <code>
    /// builder.Services.AddTermina(termina =>
    /// {
    ///     termina.RegisterPage&lt;MainMenuPage, MainMenuViewModel&gt;("main-menu");
    ///     termina.RegisterPage&lt;SettingsPage, SettingsViewModel&gt;("settings");
    /// });
    /// </code>
    /// </example>
    public static IServiceCollection AddTermina(
        this IServiceCollection services,
        Action<TerminaBuilder> configure)
    {
        return AddTermina(services, startPage: null, configure);
    }

    /// <summary>
    /// Adds Termina TUI framework services to the service collection with a start page.
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="startPage">The page key to navigate to on startup.</param>
    /// <param name="configure">Action to configure page registrations.</param>
    /// <returns>The service collection for chaining.</returns>
    public static IServiceCollection AddTermina(
        this IServiceCollection services,
        string? startPage,
        Action<TerminaBuilder> configure)
    {
        var builder = new TerminaBuilder(services);
        configure(builder);

        // Register IAnsiTerminal if not already registered
        services.TryAddSingleton<IAnsiTerminal, AnsiTerminal>();

        // Register TerminaApplication
        services.AddSingleton<TerminaApplication>(sp =>
        {
            var terminal = sp.GetRequiredService<IAnsiTerminal>();
            var app = new TerminaApplication(terminal, sp);

            // Register all pages from the builder
            foreach (var descriptor in builder.PageDescriptors)
            {
                app.RegisterPageFromDescriptor(descriptor);
            }

            // Navigate to start page if specified
            if (!string.IsNullOrEmpty(startPage))
            {
                app.NavigateTo(startPage);
            }

            return app;
        });

        // Register hosted service to run Termina
        services.AddHostedService<TerminaHostedService>();

        return services;
    }

    /// <summary>
    /// Adds a console input source to Termina.
    /// This is the default if no other input source is configured.
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <returns>The service collection for chaining.</returns>
    public static IServiceCollection AddTerminaConsoleInput(this IServiceCollection services)
    {
        services.AddSingleton<IInputSource, ConsoleInputSource>();
        return services;
    }

    /// <summary>
    /// Adds a virtual input source to Termina for testing.
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="inputSource">The virtual input source instance.</param>
    /// <returns>The service collection for chaining.</returns>
    public static IServiceCollection AddTerminaVirtualInput(
        this IServiceCollection services,
        VirtualInputSource inputSource)
    {
        services.AddSingleton<IInputSource>(inputSource);
        return services;
    }
}
View TerminaBuilder
csharp
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Termina.Pages;
using Termina.Reactive;
using Termina.Routing;

namespace Termina.Hosting;

/// <summary>
/// Builder for configuring Termina page registrations.
/// </summary>
public sealed class TerminaBuilder
{
    private readonly IServiceCollection _services;
    internal readonly List<ReactivePageRegistrationDescriptor> PageDescriptors = new();

    internal TerminaBuilder(IServiceCollection services)
    {
        _services = services;
    }

    /// <summary>
    /// Register a reactive page with a route template.
    /// Both the Page and ViewModel will be resolved from DI, allowing constructor injection.
    /// </summary>
    /// <typeparam name="TPage">The page type (must extend ReactivePage&lt;TViewModel&gt;).</typeparam>
    /// <typeparam name="TViewModel">The ViewModel type.</typeparam>
    /// <param name="routeTemplate">Route template (e.g., "/tasks/{id:int}").</param>
    /// <param name="behavior">How the page behaves on navigation (default: ResetOnNavigation).</param>
    /// <returns>This builder for fluent chaining.</returns>
    /// <example>
    /// <code>
    /// builder.RegisterRoute&lt;TodoListPage, TodoListViewModel&gt;("/todos")
    ///        .RegisterRoute&lt;TodoDetailPage, TodoDetailViewModel&gt;("/todos/{id:int}");
    /// </code>
    /// </example>
    public TerminaBuilder RegisterRoute<
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPage,
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TViewModel>(
        string routeTemplate,
        NavigationBehavior behavior = NavigationBehavior.ResetOnNavigation)
        where TPage : ReactivePage<TViewModel>
        where TViewModel : ReactiveViewModel
    {
        var parsedTemplate = RouteParser.Parse(routeTemplate);

        // Register Page and ViewModel with DI
        _services.AddTransient<TPage>();
        _services.AddTransient<TViewModel>();

        // Store descriptor for later registration with TerminaApplication
        PageDescriptors.Add(new ReactivePageRegistrationDescriptor(
            parsedTemplate,
            typeof(TPage),
            typeof(TViewModel),
            behavior,
            sp => sp.GetRequiredService<TPage>(),
            sp => sp.GetRequiredService<TViewModel>()));

        return this;
    }

    /// <summary>
    /// Register a page with a route template and custom factories for Page and ViewModel.
    /// Use this when you need custom initialization beyond DI.
    /// </summary>
    /// <typeparam name="TPage">The page type (must extend ReactivePage&lt;TViewModel&gt;).</typeparam>
    /// <typeparam name="TViewModel">The ViewModel type.</typeparam>
    /// <param name="routeTemplate">Route template (e.g., "/tasks/{id:int}").</param>
    /// <param name="pageFactory">Factory to create the Page instance.</param>
    /// <param name="viewModelFactory">Factory to create the ViewModel instance.</param>
    /// <param name="behavior">How the page behaves on navigation (default: ResetOnNavigation).</param>
    /// <returns>This builder for fluent chaining.</returns>
    public TerminaBuilder RegisterRoute<TPage, TViewModel>(
        string routeTemplate,
        Func<IServiceProvider, TPage> pageFactory,
        Func<IServiceProvider, TViewModel> viewModelFactory,
        NavigationBehavior behavior = NavigationBehavior.ResetOnNavigation)
        where TPage : ReactivePage<TViewModel>
        where TViewModel : ReactiveViewModel
    {
        var parsedTemplate = RouteParser.Parse(routeTemplate);

        PageDescriptors.Add(new ReactivePageRegistrationDescriptor(
            parsedTemplate,
            typeof(TPage),
            typeof(TViewModel),
            behavior,
            sp => pageFactory(sp),
            sp => viewModelFactory(sp)));

        return this;
    }
}

/// <summary>
/// Descriptor for a reactive page registration, used to defer registration until the application starts.
/// </summary>
internal sealed class ReactivePageRegistrationDescriptor
{
    /// <summary>
    /// The parsed route template for this page.
    /// </summary>
    public RouteTemplate RouteTemplate { get; }

    /// <summary>
    /// A unique key derived from the route template.
    /// Used internally for page caching.
    /// </summary>
    public string PageKey => RouteTemplate.Template;

    public Type PageType { get; }
    public Type ViewModelType { get; }
    public NavigationBehavior Behavior { get; }
    public Func<IServiceProvider, object> PageFactory { get; }
    public Func<IServiceProvider, ReactiveViewModel> ViewModelFactory { get; }

    public ReactivePageRegistrationDescriptor(
        RouteTemplate routeTemplate,
        Type pageType,
        Type viewModelType,
        NavigationBehavior behavior,
        Func<IServiceProvider, object> pageFactory,
        Func<IServiceProvider, ReactiveViewModel> viewModelFactory)
    {
        RouteTemplate = routeTemplate;
        PageType = pageType;
        ViewModelType = viewModelType;
        Behavior = behavior;
        PageFactory = pageFactory;
        ViewModelFactory = viewModelFactory;
    }
}

Released under the Apache 2.0 License.