Skip to content

Repository files navigation

IVolt Modern Console Library

Package: ModernConsoleLib
Version: 3.1.5
Namespace: IVolt.Tools.ModernConsole
Target Framework: .NET 10.0
License: AGPL-3.0-or-later
Author: Mark Alicz — IVolt, LLC
Website: https://www.ivolt.io


Overview

ModernConsoleLib upgrades .NET console applications from raw Console.ReadLine() workflows into a structured, navigable UI without pulling in a UI framework. It provides a page/navigation model, fully interactive arrow-key menus with text-type-ahead, colored output, structured input helpers, JSON-driven menu configuration, external plugin dispatch via a clean interface, and shell command execution — all AOT-compatible and zero external dependencies beyond the BCL.


Installation

<!-- NuGet (once published) -->
<PackageReference Include="ModernConsoleLib" Version="3.1.5" />

Or reference the assembly directly:

<Reference Include="ModernConsoleLib" />

Architecture

The library is organized around five cooperating layers:

Program  (abstract host, page registry, navigation history)
  └── Page  (abstract display unit with breadcrumb support)
        └── MenuPage  (Page + embedded Menu)
              └── Menu  (interactive option list, JSON-loadable)
                    └── Option  (id / name / value / SubMenu)

Input       — structured console input, menu selection engine
Output      — color-safe WriteLine/prompt helpers
MenuNavigation — static UI config (colors, sounds)
CmdExecutor — synchronous shell command execution
IMenuOptionHandler — plugin dispatch contract

Quick Start

Code-Defined Menu

using IVolt.Tools.ModernConsole;
using IVolt.Tools.ModernConsole.Interfaces;

// 1. Implement the handler
public class MyHandler : IMenuOptionHandler
{
    public object Handle(Menu menu, Option option, string idOrName)
    {
        switch (idOrName)
        {
            case "run_report": RunReport(); break;
            case "settings":   ShowSettings(); break;
        }
        return null;
    }
}

// 2. Build and display the menu
var menu = new Menu { Title = "Main Menu" };
menu.Add("run_report", "Run Report");
menu.Add("settings",   "Settings");
menu.OptionHandler = new MyHandler();
menu.Display();

JSON-Driven Menu

{
  "title": "Main Menu",
  "handler": {
    "dllPath": "###BASEPATH###MyHandler.dll",
    "type":    "MyApp.MyHandler"
  },
  "navigation": {
    "highlightForeground": "Black",
    "highlightBackground": "Cyan",
    "soundsEnabled": false
  },
  "options": [
    { "id": "run_report", "name": "Run Report" },
    {
      "id":   "tools",
      "name": "Tools",
      "submenu": {
        "title": "Tools",
        "options": [
          { "id": "export", "name": "Export Data" },
          { "id": "import", "name": "Import Data" }
        ]
      }
    }
  ]
}
string json = File.ReadAllText("menu.json");
var menu = Menu.LoadFromJSON(json);
menu.Display();

###BASEPATH### is automatically replaced with AppDomain.CurrentDomain.BaseDirectory at load time.


Core Types

Program (abstract)

The application host. Subclass it to create your console app.

public class MyApp : Program
{
    public MyApp() : base("My Application", breadcrumbHeader: true)
    {
        AddPage(new MainMenuPage(this));
        SetPage<MainMenuPage>();
    }
}

new MyApp().Run();
Member Description
Title Sets Console.Title on Run()
BreadcrumbHeader Shows navigation path in page headers
History Stack<Page> — current navigation state
NavigationEnabled True when History depth > 1
ArrowNavigationEnabled Toggle arrow-key vs numeric fallback
AddPage(Page) Register a page by type
SetPage<T>() Push page T onto history (no render)
NavigateTo<T>() Push + render page T
NavigateBack() Pop history + render previous page
NavigateHome() Unwind to root + render
Run() Entry point — renders CurrentPage, handles top-level exceptions
ReadMenuSelection(...) Inline arrow-key or numeric menu picker
RenderOptions(...) Low-level option list renderer (static)

Page (abstract)

A discrete screen in the application.

public class MainMenuPage : Page
{
    public MainMenuPage(Program program) : base("Main Menu", program) { }

    public override void Display()
    {
        base.Display(); // prints title or breadcrumb + "---"
        // your rendering logic
    }
}

base.Display() renders either a breadcrumb trail (Home > Section > Current) or just the page title depending on Program.BreadcrumbHeader.

MenuPage (abstract)

A Page with a Menu pre-wired. Adds "Go back" automatically when navigation is enabled.

public class ToolsPage : MenuPage
{
    public ToolsPage(Program program)
        : base("Tools", program,
               new Option("export", "Export"),
               new Option("import", "Import"))
    {
        Menu.OptionHandler = new MyHandler();
    }
}

Menu

The interactive option list engine.

var menu = new Menu { Title = "Choose" };

// Fluent adds
menu.Add("Display Name");                        // name only (id = null)
menu.Add("my_id", "Display Name");              // id + name
menu.Add("my_id", "Display Name", "payload");   // id + name + value

menu.Add(new Option { Id = "x", Name = "X", SubMenu = subMenu });

menu.OptionHandler = new MyHandler();
menu.Display();  // blocking — runs the full interaction loop

Navigation behavior during Display():

  • ESC on a sub-menu → returns to parent menu
  • ESC on root → prompts Y/N exit confirmation
  • "Go Back" option → same as ESC
  • "Exit to Primary Menu" option → unwinds all sub-menus to root
  • Nested sub-menus automatically inherit the parent OptionHandler unless they define their own

Static helpers:

// Check or retrieve by id or name
bool found = menu.Contains("my_id");
Option opt  = menu.GetMenuOption("my_id");

// JSON load
Menu m = Menu.LoadFromJSON(jsonString);

Option

public class Option
{
    public string?  Id      { get; set; }  // dispatch key; falls back to Name
    public string   Name    { get; set; }  // display text
    public string   Value   { get; set; }  // arbitrary payload
    public Menu?    SubMenu { get; set; }  // nested menu
}

Input (static)

Structured console input. All numeric inputs loop until valid.

int  n = Input.ReadInt("Enter count:", min: 1, max: 100);
int  n = Input.ReadInt(1, 100);   // no prompt overload
int  n = Input.ReadInt();         // unbounded

string s = Input.ReadString("Enter name:");

// Full interactive menu picker (used internally by Menu.Display)
// Returns 0-based index, or -1 if user pressed ESC
int choice = Input.ReadMenuChoice("Choose:", optionNames, optionsTop);

// Enum-driven menu
MyEnum val = Input.ReadEnum<MyEnum>("Select mode:");

ReadMenuChoice supports three simultaneous input modes — arrow keys, numeric entry (1..N), and text type-ahead (exact match → unique prefix → unique contains). All three are active simultaneously; the user can switch freely mid-selection.

Output (static)

Color-safe output. Always calls Console.ResetColor() after writing.

Output.WriteLine(ConsoleColor.Green,  "Success: {0}", result);
Output.WriteLine(ConsoleColor.Red,    "Error: something failed");
Output.WriteLine("Plain text: {0}",   value);          // no color
Output.DisplayPrompt("Enter value:"); // Console.Write, no newline, trims + adds space

MenuNavigation (static)

Global UI configuration. Set once at startup or load from JSON navigation block.

MenuNavigation.HighlightForeground = ConsoleColor.Black;
MenuNavigation.HighlightBackground = ConsoleColor.Cyan;
MenuNavigation.NormalForeground    = ConsoleColor.Gray;   // null = Console.ResetColor()
MenuNavigation.NormalBackground    = null;

MenuNavigation.SoundsEnabled    = true;
MenuNavigation.SoundOnMove      = true;
MenuNavigation.SoundOnSelect    = true;
MenuNavigation.SoundOnInvalid   = false;

// Beep tuning (only used when SoundHandler is null)
MenuNavigation.MoveBeepFrequency   = 800;
MenuNavigation.MoveBeepDurationMs  = 20;
MenuNavigation.SelectBeepFrequency = 1200;
MenuNavigation.SelectBeepDurationMs = 35;

// Custom sound handler (overrides Console.Beep entirely)
MenuNavigation.SoundHandler = ev => {
    if (ev == MenuSoundEvent.Move) PlayClick();
};

MenuSoundEvent values: Move, Select, Invalid.


Plugin System

External handlers are loaded at runtime via IMenuOptionHandler. The handler DLL does not need to ship with the host — it is resolved and loaded on first menu display.

Define the handler (in a separate DLL):

// MyHandler.dll — references ModernConsoleLib
public class AppMenuHandler : IMenuOptionHandler
{
    public object Handle(Menu menu, Option option, string idOrName)
    {
        switch (idOrName)
        {
            case "export": ExportData(option.Value); break;
            case "import": ImportData(); break;
            default:
                Output.WriteLine(ConsoleColor.Yellow, $"Unknown option: {idOrName}");
                break;
        }
        return null;
    }
}

Requirements: public class, parameterless public constructor, implements IMenuOptionHandler.

Wire via JSON:

{
  "handler": {
    "dllPath": "###BASEPATH###MyHandler.dll",
    "type":    "MyApp.AppMenuHandler"
  }
}

Wire in code:

menu.OptionHandler = new AppMenuHandler();

Handler inheritance: sub-menus automatically inherit the parent menu's handler unless they explicitly override it with their own handler block in JSON or set OptionHandler in code.


Shell Command Execution

CmdExecutor runs synchronous cmd.exe commands with captured stdout/stderr.

using IVolt.Tools.ModernConsole.CommandHelper;

CommandResult result = CmdExecutor.Execute("dir /b C:\\temp");

if (result.ExitCode == 0)
{
    foreach (string line in result.StandardOutput)
        Output.WriteLine(ConsoleColor.White, line);
}
else
{
    foreach (string err in result.StandardError)
        Output.WriteLine(ConsoleColor.Red, err);
}

CommandResult properties: int ExitCode, List<string> StandardOutput, List<string> StandardError.

The process runs hidden (CreateNoWindow = true, WindowStyle = Hidden) with shell execute disabled. stdout and stderr are captured asynchronously via BeginOutputReadLine / BeginErrorReadLine and the call blocks on WaitForExit().


Utilities

StringExtensions

Adds a .Format() extension to string for syntactic convenience:

string msg = "Value is {0}".Format(someValue);
// equivalent to string.Format("Value is {0}", someValue)

JSON Schema Reference

Full menu JSON structure:

{
  "title":   "string — displayed as menu header",
  "handler": {
    "dllPath": "string — path to handler DLL; ###BASEPATH### = app base dir",
    "type":    "string — fully qualified type name"
  },
  "navigation": {
    "highlightForeground": "ConsoleColor name string, e.g. Black",
    "highlightBackground": "ConsoleColor name string, e.g. Cyan",
    "normalForeground":    "ConsoleColor name string or omit for reset",
    "normalBackground":    "ConsoleColor name string or omit for reset",
    "soundsEnabled":       false,
    "soundOnMove":         true,
    "soundOnSelect":       true,
    "soundOnInvalid":      false,
    "moveBeepFrequency":   800,
    "moveBeepDurationMs":  20,
    "selectBeepFrequency": 1200,
    "selectBeepDurationMs": 35,
    "invalidBeepFrequency": 200,
    "invalidBeepDurationMs": 80
  },
  "options": [
    {
      "id":   "string — dispatch key (optional, falls back to name)",
      "name": "string — display text (required)",
      "value": "string — arbitrary payload, surfaced to the handler as Option.Value (optional)",
      "submenu": { /* recursive MenuJson */ }
    }
  ]
}

The navigation block is only applied at the root level and configures global MenuNavigation statics. It is ignored in nested sub-menu JSON objects.

JSON deserialization is case-insensitive (PropertyNameCaseInsensitive = true).


Legacy API

MenuActionRegistry is retained for backward compatibility but marked [Obsolete]. Do not use in new code. The replacement is Menu.OptionHandler / IMenuOptionHandler.

// Old pattern — DEPRECATED
MenuActionRegistry.Register("my_action", () => DoSomething());

// New pattern
menu.OptionHandler = new MyHandler(); // centralized dispatch

AOT Compatibility

The project is built with <IsAotCompatible>true</IsAotCompatible> for both Debug and Release configurations. The plugin loader uses Assembly.LoadFrom and reflection for handler instantiation — this path is inherently dynamic and will require appropriate rd.xml / trim annotations if you publish with PublishTrimmed or PublishAot and use the plugin system. Code-defined handlers with menu.OptionHandler = new MyHandler() are fully AOT-safe.


Notes and Caveats

Console.Beep is wrapped in a try/catch and silently swallowed — the library will not throw on platforms or environments where beep is unavailable (redirected output, CI runners, some terminal emulators).

CmdExecutor is Windows-only (cmd.exe). For cross-platform shell execution, replace with ProcessStartInfo { FileName = "/bin/bash", Arguments = $"-c \"{command}\"" }.

Input.ReadMenuChoice returns -1 on ESC. Callers should handle this explicitly if ESC-as-cancel semantics are needed outside of Menu.Display().

Breadcrumb rendering in Page.Display() uses Program.History which is a Stack<Page> — it reflects push order, not page titles in definition order. Ensure pages are pushed in logical navigation order for breadcrumbs to read correctly.


License

AGPL-3.0-or-later. License acceptance is required for NuGet consumption (PackageRequireLicenseAcceptance = true).

© 2026 IVolt, LLC

About

Plugin Architecture, JSON Based Console Menu Definitions. Create and use Console Menus using sound, color, child menus, and many more features. This project Drives the CLI for BPSD (See BPSD In Repositories). A quick and secure way to deploy console menu systems with a modern touch.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages