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}");Navigation Behavior
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:
| Service | Lifetime | Description |
|---|---|---|
IAnsiTerminal | Singleton | Terminal abstraction |
TerminaApplication | Singleton | Main application |
TerminaHostedService | HostedService | Runs the app |
TPage | Transient | Each page type |
TViewModel | Transient | Each ViewModel type |
Application Lifecycle
- Host Starts -
IHostedService.StartAsynccalled - Initial Navigation - Route to start page
- Input Loop - Process keyboard/mouse events
- Navigation - Create/activate pages as needed
- Shutdown -
Shutdown()called or Ctrl+C received - 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<MainMenuPage, MainMenuViewModel>("main-menu");
/// termina.RegisterPage<SettingsPage, SettingsViewModel>("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<TViewModel>).</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<TodoListPage, TodoListViewModel>("/todos")
/// .RegisterRoute<TodoDetailPage, TodoDetailViewModel>("/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<TViewModel>).</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;
}
}