Skip to content

Getting Started

Marco Breveglieri edited this page Jul 21, 2026 · 2 revisions

Getting Started

This guide takes you from zero to a running TUI in Delphi. It covers system requirements, installation, building the library, and building your first interactive application from scratch.


System Requirements

Requirement Detail
Delphi / RAD Studio 13.1 Florence (37.0) or later
Target platform Win32 console ({$APPTYPE CONSOLE})
Windows version Windows 10 build 10586 (v1511) minimum
True Color (24-bit) Windows 10 build 18362 (v1903) or later
Framework None — Blinki does not depend on VCL or FMX
Linux64 Real POSIX backend (termios raw mode, poll(2)); build via the IDE's Linux64 platform + PAServer

Linux / macOS: The POSIX console backend (Blinki.Core.Console.Posix.pas, TTuiPosixConsoleBackend) is a real, complete Linux64 implementation — raw-mode termios, non-blocking poll(2)-based input, a unit-tested escape-sequence decoder (arrow/F-keys, Shift+Tab, SGR mouse), UTF-8 input, and SIGINT/SIGTERM handling that restores the terminal before the process dies. macOS and other POSIX systems are not tested. The codebase includes {$IFDEF FPC}{$MODE DELPHI}{$ENDIF} guards in all core units as a preparatory step for Free Pascal / Lazarus compatibility.


Installation

Blinki has no design-time components and does not require any package installation in the IDE. The only step is making the library's source units visible to your project's compiler.

Step 1 — Clone the repository

git clone https://github.com/marcobreveglieri/blinki.git

Step 2 — Add Source\ to the Unit Search Path

Open your project's Options → Delphi Compiler → Search Path and add the absolute path to the Source\ folder, for example:

C:\Libs\blinki\Source

When building from the command line with MSBuild you can pass the path directly:

msbuild MyApp.dproj /p:Platform=Win32 /p:Config=Debug /p:DCC_UnitSearchPath="C:\Libs\blinki\Source"

That is all. No .bpl to install, no GetIt manifest — the library ships as plain .pas source files.

Installing with Blocks (package manager)

Blinki is also published in the official Blocks community repository as marcobreveglieri.blinki. Blocks downloads, compiles, and wires versioned Delphi packages into your IDE for you.

:: Install the Blocks CLI once (requires Windows + winget)
winget install DelphiBlocks.Blocks

:: From your project folder: initialize the workspace and pick your Delphi version
blocks init

:: Add Blinki to the project
blocks install marcobreveglieri.blinki

Blocks fetches the sources, compiles the Blinki runtime package with MSBuild, and registers the library and search paths in your IDE — then just uses the units you need. To pin a specific release, append a SemVer constraint, e.g. blocks install marcobreveglieri.blinki@^0.1.0.


Building the Library & Demos

Blinki uses MSBuild from the command line as its sole build method. Never use the IDE's Build button directly for this repository — always call MSBuild after sourcing rsvars.bat.

Quick build — use the provided script

# From the repository root:
.\do_build.bat

do_build.bat sources rsvars.bat from the RAD Studio 37.0 installation, then builds in order:

  1. The runtime package (Source\Blinki.dproj, Release/Win32)
  2. All smoke tests (Tests\SmokeTests\Blinki.SmokeTests.groupproj)
  3. All demos (Demos\Blinki.Demos.groupproj)
  4. The unit tests (Tests\UnitTests\)

The script stops at the first MSBuild error.

Manual build

If you need to build only a specific project:

# Source the RAD Studio environment variables
& "C:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat"

# Build the library package (Release)
msbuild Source\Blinki.dproj /t:Build /p:Config=Release /p:Platform=Win32

# Build a single demo (Debug)
msbuild Demos\Form\Form.dproj /t:Build /p:Config=Debug /p:Platform=Win32

Build output lands in <project>\Win32\<Config>\.

Running a demo

After a successful build, launch a demo directly:

.\Demos\Form\Win32\Debug\Form.exe

Keyboard shortcuts common to most demos:

  • Tab / Shift-Tab — move focus between widgets
  • Enter / Space — activate the focused widget
  • Ctrl+T — toggle Dark / Light theme
  • Ctrl+Q — quit

Your First TUI from Scratch

This tutorial builds a small account-registration form step by step, explaining the key concepts as we go. The result is close to the Demos\Form\Form.dpr demo that ships with the library.

Concepts you will use

Concept What it means
Root widget The single top-level widget passed to TTuiApp.SetRoot
Parent owns children Pass the parent as the first constructor argument; the parent frees all children automatically
Layout constraints Replace Left/Top/Width/Height — tell the container how to size a child, not where
Event loop TTuiApp.Run blocks until Quit is called; the terminal is always restored on exit

1. Create the project

In RAD Studio, create a new Console Application (File → New → Console Application). Remove any boilerplate and set the file encoding to UTF-8 with BOM and CRLF line endings before saving (the compiler requires this).

2. Set up uses

program MyForm;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  Blinki.Core.App,
  Blinki.Core.Input,
  Blinki.Core.Widget,
  Blinki.Core.Geometry,
  Blinki.Core.Style,
  Blinki.Core.Theme,
  Blinki.Widgets.Labels,
  Blinki.Widgets.Box,
  Blinki.Widgets.TextInput,
  Blinki.Widgets.Button,
  Blinki.Widgets.Toast,
  Blinki.Widgets.Alert,
  Blinki.Layout.Stack;

3. Build the widget tree

var
  LApp: TTuiApp;
  LRoot: TTuiVStack;
  LHeader: TTuiLabel;
  LFormBox: TTuiBox;
  LFormStack: TTuiVStack;
  LNameLabel: TTuiLabel;
  LNameInput: TTuiTextInput;
  LBtnRow: TTuiHStack;
  LBtnSubmit: TTuiButton;
  LToast: TTuiToast;
begin
  ReportMemoryLeaksOnShutdown := True;

  LApp := TTuiApp.Create;
  LRoot := TTuiVStack.Create;   // NOT created with a parent — will be owned by App via SetRoot
  try
    // ---- Header bar ----
    LHeader := TTuiLabel.Create(LRoot);  // LRoot is the parent → LRoot owns LHeader
    LHeader.Text := '  My First Blinki Form  |  Tab=focus  Ctrl+Q=quit';
    LHeader.LayoutConstraint := TTuiLayoutConstraint.Fixed(1); // exactly 1 row tall

    // ---- Toast notification area ----
    LToast := TTuiToast.Create(LRoot);
    LToast.DurationMs := 3000;
    LToast.LayoutConstraint := TTuiLayoutConstraint.Fixed(3);

    // ---- Main panel (box with rounded border) ----
    LFormBox := TTuiBox.Create(LRoot);
    LFormBox.Title := ' Registration ';
    LFormBox.BoxStyle := bsRounded;
    LFormBox.LayoutConstraint := TTuiLayoutConstraint.Fill(1); // fill remaining space

    // TTuiBox accepts exactly one child; we place a VStack inside it
    LFormStack := TTuiVStack.Create(LFormBox);

    LNameLabel := TTuiLabel.Create(LFormStack);
    LNameLabel.Text := ' Name';
    LNameLabel.LayoutConstraint := TTuiLayoutConstraint.Fixed(1);

    LNameInput := TTuiTextInput.Create(LFormStack);
    LNameInput.Placeholder := 'Your full name...';
    LNameInput.MaxLength := 64;
    LNameInput.LayoutConstraint := TTuiLayoutConstraint.Fixed(1);

    LBtnRow := TTuiHStack.Create(LFormStack);
    LBtnRow.LayoutConstraint := TTuiLayoutConstraint.Fixed(1);

    LBtnSubmit := TTuiButton.Create(LBtnRow);
    LBtnSubmit.Caption := ' Submit ';
    LBtnSubmit.LayoutConstraint := TTuiLayoutConstraint.Fill(1);

Key insight: every widget receives its parent as the first constructor argument. The parent's TObjectList<TTuiWidget> takes ownership and frees the child automatically when the parent is destroyed. Never call Free on a widget that has a parent.

Layout constraints replace absolute positioning:

Constraint Meaning
Fixed(N) Exactly N rows (or columns, depending on the container axis)
Fill(W) Grow to fill available space; W is the relative weight among siblings
Min(N) At least N rows / columns
Max(N) At most N rows / columns
Percentage(P) P percent of the container's available space (0–100)

4. Wire up events

    // ---- Button click handler ----
    LBtnSubmit.OnClick := procedure
      begin
        var LName := Trim(LNameInput.Text);
        if LName = '' then
          LToast.Show(' Please enter your name.', alWarning)
        else
          LToast.Show(Format(' Hello, %s!', [LName]), alSuccess);
      end;

Events are anonymous methods (reference to procedure). They capture variables from the enclosing scope so you can reference LNameInput, LToast, and LApp directly.

5. Start the application

    // ---- App wiring ----
    LApp.SetRoot(LRoot);  // SetRoot transfers ownership of LRoot to App

    LApp.OnKeyPress := procedure(const AKey: TTuiKeyEvent)
      begin
        if (AKey.Code = kcChar) and (kmCtrl in AKey.Modifiers) and (AKey.Character = #17) then
          LApp.Quit;  // Ctrl+Q = ASCII 17
      end;

    LApp.Run;  // blocks here; restores the terminal even if an exception is raised

  finally
    LApp.Free;  // frees App → frees LRoot → frees the entire widget tree
  end;
end.

TTuiApp.Run switches the terminal to an alternate screen buffer, hides the cursor, and enters the event loop. When Quit is called it tears down everything and returns to the normal shell prompt — even if an exception is raised inside the loop, because teardown runs in a finally block.

6. Build and run

& "C:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat"
msbuild MyForm.dproj /t:Build /p:Config=Debug /p:Platform=Win32
.\Win32\Debug\MyForm.exe

You should see a form with a rounded border, a text input, a Submit button, and toast notifications. Tab moves focus; Enter or Space activates the focused button.


Next Steps

  • Explore the 16 demo applications in Demos\ for real-world patterns (modals, data tables, animations, a full Tetris game, a team-chat client, …).
  • Read Architecture & Advanced Usage to understand the event loop, the rendering pipeline, how to write custom widgets, and how to optimise refresh performance.
  • Check Contributing before sending a pull request.