diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 0000000..2170695
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,89 @@
+# CI/CD Documentation
+
+## Overview
+
+This repository uses GitHub Actions for continuous integration with a two-job structure:
+
+1. **package** - Quality checks and NuGet package creation
+2. **samples** - Cross-platform testing and AOT sample binaries
+
+## Jobs
+
+### package (Linux only)
+
+Runs on Ubuntu and produces the main NuGet package artifact.
+
+| Step | Description |
+|------|-------------|
+| format | Code style validation (`dotnet format --verify-no-changes`) |
+| inspect | Static analysis (JetBrains InspectCode) |
+| build | Compile solution |
+| test | Run unit tests |
+| pack | Create NuGet package |
+
+**Artifact:** `Wiry.Tui.{version}.nupkg`
+
+### samples (matrix: 1-4 OS)
+
+Runs after `package` job. Verifies tests on multiple platforms and builds AOT sample binaries.
+
+| Context | Platforms |
+|---------|-----------|
+| Feature branches | Linux only |
+| main / PR to main / tags | Linux, Windows, macOS x64, macOS ARM64 |
+
+| Step | Description |
+|------|-------------|
+| build | Compile solution |
+| test | Run unit tests (cross-platform verification) |
+| publish | AOT publish sample applications |
+
+**Artifacts:** `samples-{rid}.zip` for each platform
+
+## Scripts
+
+All scripts are in the `scripts/` directory:
+
+| Script | Purpose | Used in CI |
+|--------|---------|------------|
+| `build.sh` | Restore and build solution | Yes |
+| `test.sh` | Run tests | Yes |
+| `pack.sh` | Create NuGet package | Yes |
+| `format.sh` | Check code formatting | Yes |
+| `inspect.sh` | Run JetBrains InspectCode | Local only* |
+| `publish-samples.sh` | AOT publish samples for RID | Yes |
+
+*CI uses the JetBrains GitHub Action instead of the script.
+
+## Local Development
+
+Run the same checks locally:
+
+```bash
+# Format check
+./scripts/format.sh
+
+# Build and test
+./scripts/build.sh
+./scripts/test.sh
+
+# Create package
+./scripts/pack.sh
+
+# Static analysis (requires JetBrains tools)
+dotnet tool install -g JetBrains.ReSharper.GlobalTools
+./scripts/inspect.sh
+
+# Publish samples for current platform
+./scripts/publish-samples.sh
+```
+
+## Artifacts
+
+| Artifact | Source | When |
+|----------|--------|------|
+| `nuget-package` | package job | Always |
+| `samples-linux-x64` | samples job | Always |
+| `samples-win-x64` | samples job | main/PR/tags |
+| `samples-osx-x64` | samples job | main/PR/tags |
+| `samples-osx-arm64` | samples job | main/PR/tags |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c9324bb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,113 @@
+name: CI
+
+on:
+ push:
+ branches: [main, feature/*, fix/*]
+ tags: ['v*']
+ pull_request:
+ branches: [main]
+
+jobs:
+ # =============================================================================
+ # Package Job: Quality checks + NuGet package
+ # Runs on Linux only (IL is platform-independent)
+ # =============================================================================
+ package:
+ name: Package
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ - name: Check formatting
+ shell: bash
+ run: ./scripts/format.sh
+
+ - name: Run InspectCode
+ uses: JetBrains/ReSharper-InspectCode@v0.11
+ with:
+ solution: Wiry.Tui.sln
+ tool-version: 2025.3.0.3
+
+ - name: Build
+ shell: bash
+ run: ./scripts/build.sh
+
+ - name: Test
+ shell: bash
+ run: ./scripts/test.sh
+
+ - name: Pack
+ shell: bash
+ run: ./scripts/pack.sh
+
+ - name: Upload NuGet package
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-package
+ path: ./artifacts/packages/*.nupkg
+
+ # =============================================================================
+ # Samples Job: Cross-platform tests + AOT binaries
+ # Matrix: 1 OS for feature branches, 4 OS for main/PR/tags
+ # =============================================================================
+ prepare-matrix:
+ name: Prepare matrix
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set.outputs.matrix }}
+ steps:
+ - name: Set matrix based on context
+ id: set
+ shell: bash
+ run: |
+ if [[ "$GITHUB_REF" == "refs/heads/main" ]] || \
+ [[ "$GITHUB_REF" == refs/tags/* ]] || \
+ [[ "$GITHUB_EVENT_NAME" == "pull_request" && "$GITHUB_BASE_REF" == "main" ]]; then
+ echo "[OK] Full matrix: Linux, Windows, macOS x64, macOS ARM64"
+ echo 'matrix={"include":[{"name":"Linux x64","os":"ubuntu-latest","rid":"linux-x64"},{"name":"Windows x64","os":"windows-latest","rid":"win-x64"},{"name":"macOS x64","os":"macos-13","rid":"osx-x64"},{"name":"macOS ARM64","os":"macos-latest","rid":"osx-arm64"}]}' >> "$GITHUB_OUTPUT"
+ else
+ echo "[OK] Linux-only matrix (feature branch)"
+ echo 'matrix={"include":[{"name":"Linux x64","os":"ubuntu-latest","rid":"linux-x64"}]}' >> "$GITHUB_OUTPUT"
+ fi
+
+ samples:
+ name: Samples (${{ matrix.name }})
+ runs-on: ${{ matrix.os }}
+ needs: [package, prepare-matrix]
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(needs.prepare-matrix.outputs.matrix) }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Build
+ shell: bash
+ run: ./scripts/build.sh
+
+ - name: Test
+ shell: bash
+ run: ./scripts/test.sh
+
+ - name: Publish samples (AOT)
+ shell: bash
+ run: ./scripts/publish-samples.sh ${{ matrix.rid }}
+
+ - name: Upload samples
+ uses: actions/upload-artifact@v4
+ with:
+ name: samples-${{ matrix.rid }}
+ path: ./artifacts/samples/${{ matrix.rid }}/*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..261db31
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,67 @@
+# Build results
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+artifacts/
+
+# .NET
+*.nupkg
+*.snupkg
+project.lock.json
+
+# Visual Studio
+.vs/
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# ReSharper
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JetBrains InspectCode reports
+*.sarif.json
+inspectcode-report.*
+
+# Test results
+[Tt]est[Rr]esult*/
+*.trx
+coverage*.json
+coverage*.xml
+coverage*.info
+*.coverage
+*.coveragexml
+
+# MSBuild
+*.binlog
+
+# Mono
+mono_crash.*
+
+# macOS
+.DS_Store
+._*
+
+# Windows
+Thumbs.db
+[Dd]esktop.ini
+
+# Vim
+*.swp
+
+# dotenv
+.env
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5b54572
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Dmitry Razumikhin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Wiry.Tui.sln b/Wiry.Tui.sln
new file mode 100644
index 0000000..e8b176f
--- /dev/null
+++ b/Wiry.Tui.sln
@@ -0,0 +1,73 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wiry.Tui", "src\Wiry.Tui\Wiry.Tui.csproj", "{BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wiry.Tui.Tests", "tests\Wiry.Tui.Tests\Wiry.Tui.Tests.csproj", "{D9780FC8-7106-473D-9F4B-67776EF61177}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wiry.Tui.Samples.BasicUsage", "samples\Wiry.Tui.Samples.BasicUsage\Wiry.Tui.Samples.BasicUsage.csproj", "{0654838D-8339-45D1-9EF0-676138ABD4C1}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Debug|x64.Build.0 = Debug|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Debug|x86.Build.0 = Debug|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Release|x64.ActiveCfg = Release|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Release|x64.Build.0 = Release|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Release|x86.ActiveCfg = Release|Any CPU
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65}.Release|x86.Build.0 = Release|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Debug|x64.Build.0 = Debug|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Debug|x86.Build.0 = Debug|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Release|x64.ActiveCfg = Release|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Release|x64.Build.0 = Release|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Release|x86.ActiveCfg = Release|Any CPU
+ {D9780FC8-7106-473D-9F4B-67776EF61177}.Release|x86.Build.0 = Release|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Debug|x64.Build.0 = Debug|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Debug|x86.Build.0 = Debug|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Release|x64.ActiveCfg = Release|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Release|x64.Build.0 = Release|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Release|x86.ActiveCfg = Release|Any CPU
+ {0654838D-8339-45D1-9EF0-676138ABD4C1}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {BC969CC0-822B-430A-9DCC-3E8BCF2AEE65} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {D9780FC8-7106-473D-9F4B-67776EF61177} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {0654838D-8339-45D1-9EF0-676138ABD4C1} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
+ EndGlobalSection
+EndGlobal
diff --git a/samples/Wiry.Tui.Samples.BasicUsage/Program.cs b/samples/Wiry.Tui.Samples.BasicUsage/Program.cs
new file mode 100644
index 0000000..0953588
--- /dev/null
+++ b/samples/Wiry.Tui.Samples.BasicUsage/Program.cs
@@ -0,0 +1,3 @@
+// Basic usage sample for Wiry.Tui
+
+Console.WriteLine("Wiry.Tui BasicUsage Sample");
diff --git a/samples/Wiry.Tui.Samples.BasicUsage/Wiry.Tui.Samples.BasicUsage.csproj b/samples/Wiry.Tui.Samples.BasicUsage/Wiry.Tui.Samples.BasicUsage.csproj
new file mode 100644
index 0000000..cf2ed66
--- /dev/null
+++ b/samples/Wiry.Tui.Samples.BasicUsage/Wiry.Tui.Samples.BasicUsage.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..ea5fb05
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+set -e
+
+echo "=== Building Wiry.Tui ==="
+cd "$(dirname "$0")/.."
+
+dotnet restore
+dotnet build --no-restore --configuration Release
+
+echo "[OK] Build completed"
diff --git a/scripts/format.sh b/scripts/format.sh
new file mode 100755
index 0000000..c78b7eb
--- /dev/null
+++ b/scripts/format.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+echo "=== Checking code formatting ==="
+cd "$(dirname "$0")/.."
+
+dotnet format --verify-no-changes --verbosity normal
+
+echo "[OK] Formatting check passed"
diff --git a/scripts/inspect.sh b/scripts/inspect.sh
new file mode 100755
index 0000000..0f472c4
--- /dev/null
+++ b/scripts/inspect.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+set -e
+
+echo "=== Running JetBrains InspectCode ==="
+cd "$(dirname "$0")/.."
+
+SOLUTION="Wiry.Tui.sln"
+OUTPUT_FILE="${1:-inspectcode-report.sarif}"
+MIN_SEVERITY="${2:-WARNING}"
+
+# Check if jb inspectcode is installed
+if ! command -v jb &> /dev/null; then
+ echo "[ERROR] JetBrains CLI tools not found"
+ echo "Install: dotnet tool install -g JetBrains.ReSharper.GlobalTools"
+ exit 1
+fi
+
+echo "[OK] Running static analysis on $SOLUTION"
+echo "[OK] Minimum severity: $MIN_SEVERITY"
+echo "[OK] Output: $OUTPUT_FILE"
+
+jb inspectcode "$SOLUTION" \
+ --output="$OUTPUT_FILE" \
+ --severity="$MIN_SEVERITY" \
+ --verbosity=WARN
+
+# Check if SARIF file was created
+if [ ! -f "$OUTPUT_FILE" ]; then
+ echo "[ERROR] InspectCode failed to produce output file"
+ exit 1
+fi
+
+# Parse results using jq if available
+if command -v jq &> /dev/null; then
+ echo ""
+ echo "=== Analysis Results ==="
+
+ ERRORS=$(jq -r '[.runs[0].results[] | select(.level=="error")] | length' "$OUTPUT_FILE" 2>/dev/null || echo "0")
+ WARNINGS=$(jq -r '[.runs[0].results[] | select(.level=="warning")] | length' "$OUTPUT_FILE" 2>/dev/null || echo "0")
+ TOTAL=$(jq -r '[.runs[0].results[]] | length' "$OUTPUT_FILE" 2>/dev/null || echo "0")
+
+ echo "Total issues: $TOTAL"
+ echo " Errors: $ERRORS"
+ echo " Warnings: $WARNINGS"
+
+ if [ "$MIN_SEVERITY" = "ERROR" ] && [ "$ERRORS" -gt 0 ]; then
+ echo ""
+ echo "[FAIL] Found $ERRORS error(s)"
+ exit 1
+ fi
+
+ if [ "$MIN_SEVERITY" = "WARNING" ] && [ $((ERRORS + WARNINGS)) -gt 0 ]; then
+ echo ""
+ echo "[FAIL] Found $((ERRORS + WARNINGS)) issue(s) at WARNING level or above"
+ exit 1
+ fi
+
+ echo ""
+ echo "[OK] Static analysis passed"
+else
+ echo ""
+ echo "[WARN] jq not found - skipping result validation"
+ echo "[WARN] Install jq for detailed statistics"
+fi
+
+echo "Analysis complete"
diff --git a/scripts/pack.sh b/scripts/pack.sh
new file mode 100755
index 0000000..74b0091
--- /dev/null
+++ b/scripts/pack.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+set -e
+
+echo "=== Packing Wiry.Tui ==="
+cd "$(dirname "$0")/.."
+
+OUTPUT_DIR="./artifacts/packages"
+mkdir -p "$OUTPUT_DIR"
+
+dotnet pack src/Wiry.Tui/Wiry.Tui.csproj \
+ --no-build \
+ --configuration Release \
+ --output "$OUTPUT_DIR"
+
+echo "[OK] Package created in $OUTPUT_DIR"
+ls -la "$OUTPUT_DIR"
diff --git a/scripts/publish-samples.sh b/scripts/publish-samples.sh
new file mode 100755
index 0000000..64ccdf9
--- /dev/null
+++ b/scripts/publish-samples.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -e
+
+echo "=== Publishing Wiry.Tui Samples (AOT) ==="
+cd "$(dirname "$0")/.."
+
+RID="${1:-}"
+
+if [ -z "$RID" ]; then
+ # Auto-detect RID
+ case "$(uname -s)-$(uname -m)" in
+ Darwin-arm64) RID="osx-arm64" ;;
+ Darwin-x86_64) RID="osx-x64" ;;
+ Linux-x86_64) RID="linux-x64" ;;
+ Linux-aarch64) RID="linux-arm64" ;;
+ MINGW*|MSYS*) RID="win-x64" ;;
+ *) echo "[ERROR] Unknown platform"; exit 1 ;;
+ esac
+ echo "[OK] Auto-detected RID: $RID"
+fi
+
+OUTPUT_DIR="./artifacts/samples/$RID"
+mkdir -p "$OUTPUT_DIR"
+
+echo "[OK] Publishing samples for: $RID"
+echo "[OK] Output: $OUTPUT_DIR"
+
+# Find and publish all sample projects
+for sample in samples/*/; do
+ if [ -d "$sample" ]; then
+ sample_name=$(basename "$sample")
+ echo ""
+ echo "--- Publishing $sample_name ---"
+
+ dotnet publish "$sample" \
+ --configuration Release \
+ --runtime "$RID" \
+ --output "$OUTPUT_DIR"
+ fi
+done
+
+echo ""
+echo "[OK] All samples published to $OUTPUT_DIR"
+ls -la "$OUTPUT_DIR"
diff --git a/scripts/test.sh b/scripts/test.sh
new file mode 100755
index 0000000..c8f9d4b
--- /dev/null
+++ b/scripts/test.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+echo "=== Testing Wiry.Tui ==="
+cd "$(dirname "$0")/.."
+
+dotnet test --no-build --configuration Release --verbosity normal
+
+echo "[OK] Tests passed"
diff --git a/src/Wiry.Tui/TuiConsole.cs b/src/Wiry.Tui/TuiConsole.cs
new file mode 100644
index 0000000..6176a68
--- /dev/null
+++ b/src/Wiry.Tui/TuiConsole.cs
@@ -0,0 +1,8 @@
+namespace Wiry.Tui;
+
+///
+/// Entry point for styled console output.
+///
+public static class TuiConsole
+{
+}
diff --git a/src/Wiry.Tui/Wiry.Tui.csproj b/src/Wiry.Tui/Wiry.Tui.csproj
new file mode 100644
index 0000000..2eea125
--- /dev/null
+++ b/src/Wiry.Tui/Wiry.Tui.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0;net10.0
+ enable
+ enable
+
+
+
+ Dmitry Razumikhin
+ MIT
+
+
+
diff --git a/tests/Wiry.Tui.Tests/TuiConsoleTests.cs b/tests/Wiry.Tui.Tests/TuiConsoleTests.cs
new file mode 100644
index 0000000..d32d1ab
--- /dev/null
+++ b/tests/Wiry.Tui.Tests/TuiConsoleTests.cs
@@ -0,0 +1,12 @@
+using Xunit;
+
+namespace Wiry.Tui.Tests;
+
+public class TuiConsoleTests
+{
+ [Fact]
+ public void Placeholder_ShouldPass()
+ {
+ Assert.True(true);
+ }
+}
diff --git a/tests/Wiry.Tui.Tests/Wiry.Tui.Tests.csproj b/tests/Wiry.Tui.Tests/Wiry.Tui.Tests.csproj
new file mode 100644
index 0000000..1e4b82c
--- /dev/null
+++ b/tests/Wiry.Tui.Tests/Wiry.Tui.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0;net10.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+