Tutorial 3: Settings App
Difficulty: Intermediate–Advanced | Prerequisites: Tutorial 2 or familiarity with windows and layout | Estimated reading time: ~20 minutes
What you'll build: A two-window app with a main window and a Settings window. The Settings window uses NavigationView with two pages: Appearance (RGB sliders that change the window gradient live) and General (text inputs and a checkbox). A status bar shows Save/Cancel key hints.
Step 1: Create the project
Scaffold a new console project and add the SharpConsoleUI package.
dotnet new console -n SettingsApp
cd SettingsApp
dotnet add package SharpConsoleUI
Replace Program.cs entirely with the code that follows.
Step 2: Create the main window
The main window stays open and launches the settings window on demand.
using SharpConsoleUI;
using SharpConsoleUI.Builders;
using SharpConsoleUI.Controls;
using SharpConsoleUI.Drivers;
var driver = new NetConsoleDriver(RenderMode.Buffer);
var windowSystem = new ConsoleWindowSystem(driver);
Window? settingsWindow = null; // built in Step 5, referenced by the click handler
var mainWindow = new WindowBuilder(windowSystem)
.WithTitle("My App")
.WithSize(60, 20)
.Centered()
.Build();
mainWindow.AddControl(Controls.Markup()
.AddLine("[bold]Welcome![/] Press the button below to open Settings.")
.Build());
mainWindow.AddControl(Controls.Button("⚙ Settings")
.OnClick((s, e, win) =>
{
if (settingsWindow != null)
windowSystem.AddWindow(settingsWindow);
})
.Build());
windowSystem.AddWindow(mainWindow);
The settings window is declared as null here and assigned in Step 5 — the click handler captures it by reference.
Step 3: Create the settings window
Create the settings window. The NavigationView will be added to it in Step 5 once all pages are defined inline.
settingsWindow = new WindowBuilder(windowSystem)
.WithTitle("Settings")
.WithSize(70, 22)
.Centered()
.Build();
Step 4: Declare sliders before the NavigationView builder
The sliders must be declared outside the NavigationView builder so that ValueChanged handlers can capture them after the builder call. They are added to the Appearance page inline inside AddItem().
var redSlider = Controls.Slider().Horizontal().WithName("r").WithRange(0, 255).WithValue(30).WithStep(1).Build();
var greenSlider = Controls.Slider().Horizontal().WithName("g").WithRange(0, 255).WithValue(60).WithStep(1).Build();
var blueSlider = Controls.Slider().Horizontal().WithName("b").WithRange(0, 255).WithValue(120).WithStep(1).Build();
Step 5: Build the NavigationView with inline page content
AddItem() accepts an optional content: factory — NavigationView calls it when the item is first selected, passing the content ScrollablePanelControl for you to populate. Both pages are defined inline here.
var nav = Controls.NavigationView()
.WithPaneHeader("[bold]Settings[/]")
.AddItem("Appearance", "🎨", content: panel =>
{
panel.AddControl(Controls.Markup().AddLine("[bold]Background Color[/]").Build());
panel.AddControl(Controls.Markup().AddLine("Red").Build());
panel.AddControl(redSlider);
panel.AddControl(Controls.Markup().AddLine("Green").Build());
panel.AddControl(greenSlider);
panel.AddControl(Controls.Markup().AddLine("Blue").Build());
panel.AddControl(blueSlider);
})
.AddItem("General", "⚙", content: panel =>
{
panel.AddControl(Controls.Markup().AddLine("[bold]General[/]").Build());
panel.AddControl(Controls.Markup().AddLine("Display name:").Build());
panel.AddControl(Controls.Prompt("Display name:").WithName("displayName").Build());
panel.AddControl(Controls.Markup().AddLine("API endpoint:").Build());
panel.AddControl(Controls.Prompt("API endpoint:").WithName("apiEndpoint").Build());
panel.AddControl(Controls.Checkbox("Enable notifications").Checked().Build());
})
.Fill()
.Build();
settingsWindow.AddControl(nav);
NavigationView auto-selects the first item on load. .Fill() sets VerticalAlignment.Fill so the nav spans the full window height.
Alternatively, subscribe to nav.SelectedItemChanged and manipulate nav.ContentPanel directly for content that depends on external runtime state.
Step 6: Wire sliders to the gradient
Each slider's ValueChanged event fires on every drag tick — use it to update the window background gradient immediately.
using SharpConsoleUI.Helpers;
using SharpConsoleUI.Rendering;
void ApplyGradient()
{
var color = new Color(
(byte)redSlider.Value,
(byte)greenSlider.Value,
(byte)blueSlider.Value);
settingsWindow!.BackgroundGradient = new GradientBackground(
ColorGradient.FromColors(color, Color.Black),
GradientDirection.Vertical);
}
redSlider.ValueChanged += (s, e) => ApplyGradient();
greenSlider.ValueChanged += (s, e) => ApplyGradient();
blueSlider.ValueChanged += (s, e) => ApplyGradient();
GradientBackground is a record combining a ColorGradient and a GradientDirection. ColorGradient.FromColors() interpolates smoothly between the stops. See Gradients & Alpha for predefined gradients and more patterns.
Step 7: Add a status bar and keyboard shortcuts
StatusBarControl is display-and-click-only — it shows key hint text but does NOT intercept keyboard events; wire shortcuts separately via window.PreviewKeyPressed.
settingsWindow.AddControl(Controls.StatusBar()
.AddLeft("Ctrl+S", "Save")
.AddLeft("Esc", "Cancel")
.Build());
// PreviewKeyPressed fires before the focused control sees the key — correct for global shortcuts.
settingsWindow.PreviewKeyPressed += (s, e) =>
{
if (e.KeyInfo.Key == ConsoleKey.Escape)
{
windowSystem.CloseWindow(settingsWindow);
e.Handled = true;
}
else if (e.KeyInfo.Key == ConsoleKey.S && e.KeyInfo.Modifiers == ConsoleModifiers.Control)
{
SaveAndClose();
e.Handled = true;
}
};
Pressing Esc closes the settings window and returns focus to the main window. Ctrl+S calls SaveAndClose() which applies the gradient and closes the window.
Step 8: Open and close the settings window
windowSystem.AddWindow() activates the settings window on top; closing it removes it from the stack and returns focus to the main window.
void SaveAndClose()
{
ApplyGradient(); // gradient is already live; call to ensure final state is applied
windowSystem.CloseWindow(settingsWindow!);
}
// Add a Save button in the nav toolbar:
nav.AddContentToolbarButton("Save", (s, e) => SaveAndClose());
// Note: the handler type is EventHandler<ButtonControl> — e is the ButtonControl, not EventArgs.
The library also supports modal dialogs (windowSystem.ShowDialog(...)) that block the calling window — see Dialogs. This tutorial uses a plain second window for simplicity.
Complete Program.cs
using SharpConsoleUI;
using SharpConsoleUI.Builders;
using SharpConsoleUI.Controls;
using SharpConsoleUI.Drivers;
using SharpConsoleUI.Helpers;
using SharpConsoleUI.Layout;
using SharpConsoleUI.Rendering;
var driver = new NetConsoleDriver(RenderMode.Buffer);
var windowSystem = new ConsoleWindowSystem(driver);
// ── Sliders declared before the NavigationView builder so ValueChanged can capture them ──
var redSlider = Controls.Slider().Horizontal().WithName("r").WithRange(0, 255).WithValue(30).WithStep(1).Build();
var greenSlider = Controls.Slider().Horizontal().WithName("g").WithRange(0, 255).WithValue(60).WithStep(1).Build();
var blueSlider = Controls.Slider().Horizontal().WithName("b").WithRange(0, 255).WithValue(120).WithStep(1).Build();
// ── Settings window (assigned below; captured by the Settings button click handler) ──
Window? settingsWindow = null;
// ── Main window ──
var mainWindow = new WindowBuilder(windowSystem)
.WithTitle("My App")
.WithSize(60, 20)
.Centered()
.Build();
mainWindow.AddControl(Controls.Markup()
.AddLine("[bold]Welcome![/] Press the button below to open Settings.")
.Build());
mainWindow.AddControl(Controls.Button("⚙ Settings")
.OnClick((s, e, win) =>
{
if (settingsWindow != null)
windowSystem.AddWindow(settingsWindow);
})
.Build());
windowSystem.AddWindow(mainWindow);
// ── Settings window ──
settingsWindow = new WindowBuilder(windowSystem)
.WithTitle("Settings")
.WithSize(70, 22)
.Centered()
.Build();
// ── NavigationView with inline page content ──
var nav = Controls.NavigationView()
.WithPaneHeader("[bold]Settings[/]")
.AddItem("Appearance", "🎨", content: panel =>
{
panel.AddControl(Controls.Markup().AddLine("[bold]Background Color[/]").Build());
panel.AddControl(Controls.Markup().AddLine("Red").Build());
panel.AddControl(redSlider);
panel.AddControl(Controls.Markup().AddLine("Green").Build());
panel.AddControl(greenSlider);
panel.AddControl(Controls.Markup().AddLine("Blue").Build());
panel.AddControl(blueSlider);
})
.AddItem("General", "⚙", content: panel =>
{
panel.AddControl(Controls.Markup().AddLine("[bold]General[/]").Build());
panel.AddControl(Controls.Markup().AddLine("Display name:").Build());
panel.AddControl(Controls.Prompt("Display name:").WithName("displayName").Build());
panel.AddControl(Controls.Markup().AddLine("API endpoint:").Build());
panel.AddControl(Controls.Prompt("API endpoint:").WithName("apiEndpoint").Build());
panel.AddControl(Controls.Checkbox("Enable notifications").Checked().Build());
})
.Fill()
.Build();
settingsWindow.AddControl(nav);
// ── Gradient helper + slider wiring ──
void ApplyGradient()
{
var color = new Color(
(byte)redSlider.Value,
(byte)greenSlider.Value,
(byte)blueSlider.Value);
settingsWindow!.BackgroundGradient = new GradientBackground(
ColorGradient.FromColors(color, Color.Black),
GradientDirection.Vertical);
}
redSlider.ValueChanged += (s, e) => ApplyGradient();
greenSlider.ValueChanged += (s, e) => ApplyGradient();
blueSlider.ValueChanged += (s, e) => ApplyGradient();
// ── Status bar ──
settingsWindow.AddControl(Controls.StatusBar()
.AddLeft("Ctrl+S", "Save")
.AddLeft("Esc", "Cancel")
.Build());
// ── Keyboard shortcuts via PreviewKeyPressed ──
settingsWindow.PreviewKeyPressed += (s, e) =>
{
if (e.KeyInfo.Key == ConsoleKey.Escape)
{
windowSystem.CloseWindow(settingsWindow);
e.Handled = true;
}
else if (e.KeyInfo.Key == ConsoleKey.S && e.KeyInfo.Modifiers == ConsoleModifiers.Control)
{
SaveAndClose();
e.Handled = true;
}
};
// ── Save toolbar button ──
void SaveAndClose()
{
ApplyGradient();
windowSystem.CloseWindow(settingsWindow!);
}
nav.AddContentToolbarButton("Save", (s, e) => SaveAndClose());
windowSystem.Run();
What you learned
Controls.NavigationView()builder —WithPaneHeader(),AddItem(..., content:), and.Fill()replace manualnew NavigationView { ... }+SetItemContent()callsnav.ContentPanel+SelectedItemChanged— alternative for dynamic content that depends on external runtime stateControls.Slider()builder —.Horizontal(),.WithName(),.WithRange(),.WithValue(),.WithStep()SliderControl.ValueChanged— fires on every drag tick for live updatesControls.Markup().AddLine()— replacesnew MarkupControl(new List<string> { ... })Controls.Prompt().WithName()— replacesnew PromptControl { Name = ... }Controls.Checkbox().Checked()— replacesnew CheckboxControl { IsChecked = true }Controls.StatusBar().AddLeft()— replacesnew StatusBarControl()+AddItem()callsGradientBackground+ColorGradient.FromColors()+GradientDirection— live window background gradients- Form layout:
PromptControl+CheckboxControlin aScrollablePanelControl— no special form container needed StatusBarControl— display+click only; does not intercept key eventswindow.PreviewKeyPressed— global shortcut wiring (fires before focused control sees the key)e.KeyInfo.Key— correct property onKeyPressedEventArgs(.KeyInfoisConsoleKeyInfo)- Multi-window:
windowSystem.AddWindow()/windowSystem.CloseWindow()— stack-based window management
This is the final tutorial in the series.
You've now covered the core SharpConsoleUI patterns. Explore the Controls Reference, Builders Reference, and project templates (dotnet new tui-app) for next steps.