From e8d47a179ec0ffb13cffb7e920223da89d53f27e Mon Sep 17 00:00:00 2001
From: Anthony Ettinger
Date: Thu, 1 Oct 2026 18:48:24 +0000
Subject: [PATCH] 0.7.1: publish the Nim binding to Nimble
The Nim package compiled against a shared library that only a CMake build in a
checkout produced, so a Nimble install could not run anything. It now compiles
the engine into the program from a vendored copy of ports/c, ports/cpp and
ports/bindings (ports/nim/src/hqtui/engine, generated by
ports/nim/tools/vendor.sh), so `nimble install` needs only Nim and a C/C++
compiler.
- CI: a nim-package job (Linux + macOS) fails on a stale vendored engine, runs
`nimble test`, and builds a program against an installed copy.
- `nimble test` replays the demo-parity conformance group (240 reference
frames) besides the API suite.
- hqtui.nimble and hqtui.nim follow the library version; version.test.ts ties
them, since Nimble resolves the package through the vX.Y.Z tags.
- Bump 0.7.0 -> 0.7.1 everywhere it is written by hand.
Co-Authored-By: Claude Opus 5.5
---
.gitattributes | 2 +
.github/workflows/bindings.yml | 48 +
README.md | 7 +-
apps/benchmark/package.json | 2 +-
apps/demo/package.json | 4 +-
apps/demo/src/main.ts | 2 +-
apps/web/app/docs/page.tsx | 5 +-
apps/web/app/page.tsx | 2 +-
apps/web/package.json | 2 +-
bun.lock | 10 +-
examples/package.json | 2 +-
packages/hqtui/package.json | 2 +-
packages/hqtui/src/cli.ts | 2 +-
packages/hqtui/test/version.test.ts | 11 +
ports/bindings/README.md | 11 +-
ports/nim/README.md | 38 +-
ports/nim/hqtui.nimble | 22 +-
ports/nim/src/hqtui.nim | 54 +-
ports/nim/src/hqtui/engine/README.md | 5 +
ports/nim/src/hqtui/engine/VERSION | 1 +
.../engine/bindings/include/hqtui_bindings.h | 50 +
.../src/hqtui/engine/bindings/src/bridge.cpp | 688 +++
ports/nim/src/hqtui/engine/c/include/hqtui.h | 184 +
ports/nim/src/hqtui/engine/c/src/buffer.c | 188 +
ports/nim/src/hqtui/engine/c/src/color.c | 91 +
ports/nim/src/hqtui/engine/c/src/diff.c | 126 +
ports/nim/src/hqtui/engine/c/src/internal.h | 30 +
ports/nim/src/hqtui/engine/c/src/layout.c | 163 +
ports/nim/src/hqtui/engine/c/src/surface.c | 254 ++
ports/nim/src/hqtui/engine/c/src/theme.c | 32 +
ports/nim/src/hqtui/engine/c/src/unicode.c | 132 +
.../nim/src/hqtui/engine/cpp/demo/collect.cpp | 904 ++++
.../nim/src/hqtui/engine/cpp/demo/collect.hpp | 16 +
.../src/hqtui/engine/cpp/demo/dashboard.cpp | 366 ++
.../engine/cpp/demo/generated/sample.hpp | 3697 +++++++++++++++++
ports/nim/src/hqtui/engine/cpp/demo/json.hpp | 293 ++
ports/nim/src/hqtui/engine/cpp/demo/main.cpp | 704 ++++
ports/nim/src/hqtui/engine/cpp/demo/model.hpp | 181 +
.../src/hqtui/engine/cpp/demo/showcase.cpp | 586 +++
.../src/hqtui/engine/cpp/demo/telemetry.cpp | 545 +++
.../src/hqtui/engine/cpp/include/hqtui.hpp | 129 +
.../engine/cpp/include/hqtui/widgets.hpp | 1206 ++++++
.../nim/src/hqtui/engine/cpp/src/calendar.cpp | 139 +
ports/nim/src/hqtui/engine/cpp/src/canvas.cpp | 139 +
ports/nim/src/hqtui/engine/cpp/src/chart.cpp | 364 ++
.../src/hqtui/engine/cpp/src/scrollbar.cpp | 80 +
ports/nim/src/hqtui/engine/cpp/src/shadow.cpp | 74 +
.../nim/src/hqtui/engine/cpp/src/widgets.cpp | 1314 ++++++
ports/nim/src/hqtui/engine/cpp/src/world.cpp | 169 +
.../src/hqtui/engine/cpp/src/world_data.cpp | 505 +++
ports/nim/tests/parity.nim | 26 +
ports/nim/tools/vendor.sh | 48 +
52 files changed, 13605 insertions(+), 50 deletions(-)
create mode 100644 .gitattributes
create mode 100644 ports/nim/src/hqtui/engine/README.md
create mode 100644 ports/nim/src/hqtui/engine/VERSION
create mode 100644 ports/nim/src/hqtui/engine/bindings/include/hqtui_bindings.h
create mode 100644 ports/nim/src/hqtui/engine/bindings/src/bridge.cpp
create mode 100644 ports/nim/src/hqtui/engine/c/include/hqtui.h
create mode 100644 ports/nim/src/hqtui/engine/c/src/buffer.c
create mode 100644 ports/nim/src/hqtui/engine/c/src/color.c
create mode 100644 ports/nim/src/hqtui/engine/c/src/diff.c
create mode 100644 ports/nim/src/hqtui/engine/c/src/internal.h
create mode 100644 ports/nim/src/hqtui/engine/c/src/layout.c
create mode 100644 ports/nim/src/hqtui/engine/c/src/surface.c
create mode 100644 ports/nim/src/hqtui/engine/c/src/theme.c
create mode 100644 ports/nim/src/hqtui/engine/c/src/unicode.c
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/collect.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/collect.hpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/dashboard.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/generated/sample.hpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/json.hpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/main.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/model.hpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/showcase.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/demo/telemetry.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/include/hqtui.hpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/include/hqtui/widgets.hpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/calendar.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/canvas.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/chart.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/scrollbar.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/shadow.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/widgets.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/world.cpp
create mode 100644 ports/nim/src/hqtui/engine/cpp/src/world_data.cpp
create mode 100644 ports/nim/tests/parity.nim
create mode 100755 ports/nim/tools/vendor.sh
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..55b2e8d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# A generated copy of ports/c, ports/cpp and ports/bindings (ports/nim/tools/vendor.sh).
+ports/nim/src/hqtui/engine/** linguist-generated=true
diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml
index aca8a44..116b5e6 100644
--- a/.github/workflows/bindings.yml
+++ b/.github/workflows/bindings.yml
@@ -9,6 +9,54 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
+ # What `nimble install hqtui` gets: the vendored engine must match ports/c,
+ # ports/cpp and ports/bindings, `nimble test` must pass, and an installed copy
+ # must build a program with no checkout or shared library around it.
+ nim-package:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v7
+ - uses: jdx/mise-action@v5.0.0
+ with:
+ install: false
+ - name: Install Nim
+ run: mise install nim@2.2.12
+ - name: Vendored engine matches its sources
+ shell: bash
+ run: |
+ sh ports/nim/tools/vendor.sh
+ changes=$(git status --porcelain --untracked-files=all -- ports/nim/src/hqtui/engine)
+ if [ -n "$changes" ]; then
+ printf '%s\n' "$changes"
+ git --no-pager diff --stat -- ports/nim/src/hqtui/engine
+ echo "::error::ports/nim/src/hqtui/engine is stale. Run sh ports/nim/tools/vendor.sh and commit the result."
+ exit 1
+ fi
+ - name: nimble test
+ working-directory: ports/nim
+ run: mise exec nim@2.2.12 -- nimble test
+ - name: nimble install, then build against the installed package
+ shell: bash
+ run: |
+ scratch="$RUNNER_TEMP/nimble-install"
+ mkdir -p "$scratch/app"
+ (cd ports/nim && mise exec nim@2.2.12 -- nimble install -y --nimbleDir:"$scratch/nimble")
+ cat > "$scratch/app/hello.nim" <<'EOF'
+ import std/strutils, hqtui
+ let ui = newUI()
+ ui.panel("Installed", proc(p: UI) = p.meter(0.5, %*{"label": "CPU"}))
+ var scene = newScene(30, 4)
+ scene.set(ui)
+ let frame = scene.render()
+ doAssert "Installed" in frame and "CPU" in frame, frame
+ echo frame
+ EOF
+ cd "$scratch/app"
+ mise exec nim@2.2.12 -- nim c -r --hints:off --nimblePath:"$scratch/nimble/pkgs2" hello.nim
bindings:
strategy:
fail-fast: false
diff --git a/README.md b/README.md
index 4c3e3de..f5e5efc 100644
--- a/README.md
+++ b/README.md
@@ -213,8 +213,9 @@ cd ports/python && python examples/dashboard.py # Python
cd ports/zig && zig build run-dashboard # Zig
```
-C++, Ruby, PHP, Perl and Nim build a native library first; their READMEs have the
-CMake invocation.
+C++, Ruby, PHP and Perl build a native library first; their READMEs have the
+CMake invocation. Nim compiles the engine into the program itself:
+`nim c -r ports/nim/examples/dashboard.nim`.
### Traffic — every protocol in and out of the host
@@ -430,7 +431,7 @@ and the same escape bytes, byte for byte.
| [C++](ports/cpp/) | native demo on the C core; library API experimental | none | [README](ports/cpp/README.md) |
| [Ruby](ports/bindings/) | binding over the shared C ABI | the native engine | [README](ports/bindings/README.md) |
| [PHP](ports/bindings/) | binding over the shared C ABI | the native engine | [README](ports/bindings/README.md) |
-| [Nim](ports/nim/) | binding over the shared C ABI | the native engine | [README](ports/nim/README.md) |
+| [Nim](ports/nim/) | binding over the shared C ABI, engine compiled in; `nimble install https://github.com/profullstack/hqtui?subdir=ports/nim` | none (vendored engine) | [README](ports/nim/README.md) |
| [Perl](ports/bindings/) | binding over the shared C ABI | the native engine | [README](ports/bindings/README.md) |
The four native ports are ports, not bindings: no Node is in the picture at
diff --git a/apps/benchmark/package.json b/apps/benchmark/package.json
index 07ca64f..798ae86 100644
--- a/apps/benchmark/package.json
+++ b/apps/benchmark/package.json
@@ -7,6 +7,6 @@
"start": "bun src/main.ts"
},
"dependencies": {
- "@profullstack/hqtui": "^0.7.0"
+ "@profullstack/hqtui": "^0.7.1"
}
}
diff --git a/apps/demo/package.json b/apps/demo/package.json
index 8077c9c..599bd45 100644
--- a/apps/demo/package.json
+++ b/apps/demo/package.json
@@ -1,6 +1,6 @@
{
"name": "@profullstack/hqtui-demo",
- "version": "0.7.0",
+ "version": "0.7.1",
"description": "The HQTUI reference dashboard: a btop-grade terminal system monitor. Runs on real system metrics or a deterministic simulation.",
"license": "MIT",
"type": "module",
@@ -27,7 +27,7 @@
"audit:scroll": "bun scripts/scrollaudit.ts"
},
"dependencies": {
- "@profullstack/hqtui": "^0.7.0"
+ "@profullstack/hqtui": "^0.7.1"
},
"publishConfig": {
"access": "public"
diff --git a/apps/demo/src/main.ts b/apps/demo/src/main.ts
index a8b2b24..1c86ea4 100755
--- a/apps/demo/src/main.ts
+++ b/apps/demo/src/main.ts
@@ -52,7 +52,7 @@ function parseArgs(argv: string[]): Options {
case "-h":
case "--help": printHelp(); process.exit(0);
case "-v":
- case "--version": console.log("hqtui-demo 0.7.0"); process.exit(0);
+ case "--version": console.log("hqtui-demo 0.7.1"); process.exit(0);
}
}
return options;
diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx
index 490eb0f..669445c 100644
--- a/apps/web/app/docs/page.tsx
+++ b/apps/web/app/docs/page.tsx
@@ -103,11 +103,12 @@ export default async function Docs() {
Ruby, PHP, Perl and Nim are bindings, not independent renderer ports. Their widget
APIs submit batched scene updates; the shared ten-screen demo, collectors and
- terminal loop execute inside the calling runtime. They need GCC/Clang and CMake.
+ terminal loop execute inside the calling runtime. They need GCC/Clang, and Ruby,
+ PHP and Perl also need CMake.
Ruby needs Fiddle; Perl needs FFI::Platypus (the launcher installs it into its
private cache using cpanm if missing). PHP uses a small native adapter built
with matching php-config/development headers, or FFI when available.
- Nim uses its built-in C FFI and needs Nim 2.2.4 or newer.
+ Nim compiles the engine into the program from source and needs Nim 2.2.4 or newer.
Mise supplies pinned Ruby/Perl/Nim, prebuilt PHP via its Conda backend, and CMake.
No system packages or global gems/CPAN modules are installed by the launcher.
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index c02f284..1dd0f56 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -158,7 +158,7 @@ export default async function Home() {
High Quality Terminal UI for TypeScript, Rust, Go, Python, Zig and C++
- v0.7.0 · {COUNT} language demos · MIT
+ v0.7.1 · {COUNT} language demos · MIT
Terminal dashboards that
diff --git a/apps/web/package.json b/apps/web/package.json
index 9a9cec6..8301d81 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@base-ui/react": "1.7.0",
- "@profullstack/hqtui": "^0.7.0",
+ "@profullstack/hqtui": "^0.7.1",
"@profullstack/libsql-pg": "^0.1.3",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
diff --git a/bun.lock b/bun.lock
index 44d7421..7837f40 100644
--- a/bun.lock
+++ b/bun.lock
@@ -21,12 +21,12 @@
},
"apps/demo": {
"name": "@profullstack/hqtui-demo",
- "version": "0.7.0",
+ "version": "0.7.1",
"bin": {
"hqtui-demo": "./bin/hqtui-demo.mjs",
},
"dependencies": {
- "@profullstack/hqtui": "^0.7.0",
+ "@profullstack/hqtui": "^0.7.1",
},
},
"apps/web": {
@@ -34,7 +34,7 @@
"version": "0.1.0",
"dependencies": {
"@base-ui/react": "1.7.0",
- "@profullstack/hqtui": "^0.7.0",
+ "@profullstack/hqtui": "^0.7.1",
"@profullstack/libsql-pg": "^0.1.3",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
@@ -59,12 +59,12 @@
"name": "@profullstack/hqtui-examples",
"version": "0.1.0",
"dependencies": {
- "@profullstack/hqtui": "^0.7.0",
+ "@profullstack/hqtui": "^0.7.1",
},
},
"packages/hqtui": {
"name": "@profullstack/hqtui",
- "version": "0.7.0",
+ "version": "0.7.1",
"bin": {
"hqtui": "./bin/hqtui.mjs",
},
diff --git a/examples/package.json b/examples/package.json
index e9777d8..d8fef15 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -4,6 +4,6 @@
"version": "0.1.0",
"type": "module",
"dependencies": {
- "@profullstack/hqtui": "^0.7.0"
+ "@profullstack/hqtui": "^0.7.1"
}
}
diff --git a/packages/hqtui/package.json b/packages/hqtui/package.json
index 1beb590..b238809 100644
--- a/packages/hqtui/package.json
+++ b/packages/hqtui/package.json
@@ -1,6 +1,6 @@
{
"name": "@profullstack/hqtui",
- "version": "0.7.0",
+ "version": "0.7.1",
"description": "High Quality Terminal UI for TypeScript. btop-grade dashboards with a one-import API, dark by default, zero runtime dependencies.",
"license": "MIT",
"type": "module",
diff --git a/packages/hqtui/src/cli.ts b/packages/hqtui/src/cli.ts
index 79fa897..6def84c 100644
--- a/packages/hqtui/src/cli.ts
+++ b/packages/hqtui/src/cli.ts
@@ -14,7 +14,7 @@ import { detectCapabilities } from "./capabilities.ts";
import { themeList, themes } from "./theme.ts";
import { BrailleCanvas } from "./graphics/braille.ts";
-const VERSION = "0.7.0";
+const VERSION = "0.7.1";
function help(): void {
console.log(`hqtui ${VERSION} — High Quality Terminal UI for TypeScript
diff --git a/packages/hqtui/test/version.test.ts b/packages/hqtui/test/version.test.ts
index 5f07404..c9ba17c 100644
--- a/packages/hqtui/test/version.test.ts
+++ b/packages/hqtui/test/version.test.ts
@@ -67,6 +67,17 @@ test("every version badge on the site matches the library", () => {
for (const v of found) assert.equal(v, versionOf("packages", "hqtui"), `page.tsx has ${v}`);
});
+test("the Nim package is versioned with the library", () => {
+ // Nimble resolves `nimble install hqtui` to the newest vX.Y.Z tag of this
+ // repository and reads ports/nim/hqtui.nimble there, so the two must agree
+ // or the registry installs a package that reports another version.
+ const library = versionOf("packages", "hqtui");
+ const nimble = allMatches(read("ports", "nim", "hqtui.nimble"), /^version\s*=\s*"([^"]+)"/gm);
+ assert.deepEqual(nimble, [library], `hqtui.nimble has ${nimble.join(", ")}`);
+ const source = allMatches(read("ports", "nim", "src", "hqtui.nim"), /^const version\*\s*=\s*"([^"]+)"/gm);
+ assert.deepEqual(source, [library], `hqtui.nim has ${source.join(", ")}`);
+});
+
/**
* Whether `version` satisfies `range`, for the range forms this repo uses.
* Returns null for anything else so the caller fails loudly rather than
diff --git a/ports/bindings/README.md b/ports/bindings/README.md
index 25803e8..9c3a7c0 100644
--- a/ports/bindings/README.md
+++ b/ports/bindings/README.md
@@ -6,7 +6,8 @@ uses the C++ layout/widgets plus the C11 framebuffer/diff renderer. The JSON tre
is retained and only reparsed on `set`, not on every render. No per-cell FFI calls,
JSON subprocess pipe, JavaScript runtime or child demo executable is involved.
-Nim uses its built-in C FFI and requires Nim 2.2.4+; see [Nim](../nim/README.md).
+Nim compiles the engine from source into the program (no shared library) and
+requires Nim 2.2.4+; it installs with Nimble, see [Nim](../nim/README.md).
Ruby uses Fiddle. Perl uses FFI::Platypus. PHP prefers a tiny compiled Zend adapter
to the same C ABI (so FFI need not be enabled); an FFI path is available otherwise.
The full ten-screen example calls the shared C++ demo **inside the language VM**,
@@ -57,10 +58,10 @@ matching the PHP binary, then build target `hqtui_php`. Run PHP with
to the current invocation and does not edit php.ini. Without the adapter, PHP
must provide its FFI extension and permit CLI FFI calls.
-The loaders find this conventional checkout build or use `HQTUI_NATIVE_LIB` to
-select an explicitly installed shared library. Copying a binding package alone
-does not install the engine. RubyGems/Composer/CPAN metadata is included, but these
-new packages have **not** been published to those registries.
+The Ruby, PHP and Perl loaders find this conventional checkout build or use
+`HQTUI_NATIVE_LIB` to select an explicitly installed shared library. Copying one of
+those packages alone does not install the engine. RubyGems/Composer/CPAN metadata is
+included, but those packages have **not** been published to their registries.
## API and tests
diff --git a/ports/nim/README.md b/ports/nim/README.md
index 07b8b49..2304e30 100644
--- a/ports/nim/README.md
+++ b/ports/nim/README.md
@@ -4,26 +4,36 @@ Nim 2.2.4+ bindings to the shared C/C++ engine, with no Nim package dependencies
Build custom widget trees in Nim or run the full ten-screen dashboard in the same
process. Linux and macOS are supported; live metrics require Linux.
-## Build and run
+## Install
-From the repository root, with Nim, a C/C++17 compiler and CMake 3.20+ installed:
+```sh
+nimble install https://github.com/profullstack/hqtui?subdir=ports/nim
+```
+
+`nimble install hqtui` works the same way once the package is listed in
+[nim-lang/packages](https://github.com/nim-lang/packages). Nimble picks the
+newest `vX.Y.Z` tag, and the package version follows the hqtui release.
+
+The engine is compiled from source into your program: the package carries a
+copy of `ports/c`, `ports/cpp` and `ports/bindings` in `src/hqtui/engine`, built
+through Nim's `compile` pragma. You need Nim 2.2.4+ and a C11/C++17 compiler
+(GCC or Clang); there is no CMake step, no shared library and nothing to set at
+run time. The first build of a program compiles the engine (about 20 seconds);
+later builds reuse the objects in its nimcache.
+
+## Build and run from a checkout
```sh
-cmake -S ports/cpp -B ports/cpp/build-bindings \
- -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
- -DBUILD_TESTING=OFF -DHQTUI_BUILD_BINDINGS=ON
-cmake --build ports/cpp/build-bindings --target hqtui_bindings --parallel 2
nim c -r ports/nim/examples/hello.nim
nim c -r -d:release ports/nim/examples/dashboard.nim --sim
nim c -r ports/nim/examples/widgets.nim
+cd ports/nim && nimble test
```
-The loader uses that checkout build by default. Set `HQTUI_NATIVE_LIB` to the
-absolute path of `libhqtui_bindings.so` (Linux) or `libhqtui_bindings.dylib`
-(macOS) when installing or moving the executable. The library must remain
-available at runtime. Nimble metadata is included; this package is not published.
+`src/hqtui/engine` is generated. After changing the C, C++ or binding sources,
+run `sh ports/nim/tools/vendor.sh`; CI fails when the copy differs.
-Once this change is on main, the latest-source launcher supports both forms:
+The latest-source launcher runs the demo without a checkout:
```sh
curl -fsSL https://hqtui.com/demo.sh | sh -s -- --system nim
@@ -49,7 +59,7 @@ stdout.write scene.render()
scene.close()
```
-Compile with `--path:ports/nim/src` when using the checkout. Each widget accepts
+Compile with `--path:ports/nim/src` when using the checkout instead of Nimble. Each widget accepts
an optional JSON object for options, with the same fields as the
[shared protocol](../bindings/PROTOCOL.md). Arrays and records use Nim's `%*`
JSON literal macro, exported by `hqtui`. Named procedures cover every widget in
@@ -80,6 +90,10 @@ headless output and the full interactive demo. It does not implement the larger
TypeScript API independently: modal content uses protocol fields rather than
arbitrary nested widgets, and input is not a high-level event/callback API.
+`nimble test` runs the API suite and replays the `demo-parity` conformance
+group (240 TypeScript reference frames). The shared binding runner checks the
+same frames across Ruby, PHP, Perl and Nim, plus interactive behaviour:
+
```sh
sh ports/nim/tests/build.sh
export HQTUI_NATIVE_LIB="$PWD/ports/cpp/build-bindings/libhqtui_bindings.so"
diff --git a/ports/nim/hqtui.nimble b/ports/nim/hqtui.nimble
index 7975f76..4266df4 100644
--- a/ports/nim/hqtui.nimble
+++ b/ports/nim/hqtui.nimble
@@ -1,9 +1,17 @@
-version = "0.1.0"
-author = "HQTUI contributors"
-description = "HQTUI widgets and terminal applications over the shared native engine"
-license = "MIT"
-srcDir = "src"
+# Package
+
+version = "0.7.1"
+author = "HQTUI contributors"
+description = "Terminal UI widgets and the ten-screen HQTUI dashboard for Nim, over HQTUI's shared native engine (compiled from source)"
+license = "MIT"
+srcDir = "src"
+
+# Dependencies
+
requires "nim >= 2.2.4"
-task test, "Run binding API tests (requires native engine)":
- exec "nim c -r --hints:off tests/core.nim"
+# Tasks
+
+task test, "Run the API tests and replay the demo-parity conformance fixtures":
+ exec "nim c -r --hints:off --nimcache:build/nimcache --out:build/core tests/core.nim"
+ exec "nim c -r --hints:off --nimcache:build/nimcache --out:build/parity tests/parity.nim"
diff --git a/ports/nim/src/hqtui.nim b/ports/nim/src/hqtui.nim
index b89c776..8a198e1 100644
--- a/ports/nim/src/hqtui.nim
+++ b/ports/nim/src/hqtui.nim
@@ -1,14 +1,54 @@
## Nim API over HQTUI's shared native rendering engine (ABI 1).
## Scenes own their handles, cannot be copied, and restore terminals on destruction.
-import std/[json, os]
+## The engine is compiled from source into the program (src/hqtui/engine, a
+## generated copy of ports/c, ports/cpp and ports/bindings), so a Nimble install
+## needs only Nim and a C/C++17 compiler: no CMake and no shared library.
+import std/[json, os, strutils]
export json
-const version* = "0.1.0"
-const checkoutLibrary = currentSourcePath().parentDir / "../../cpp/build-bindings" /
- (when defined(macosx): "libhqtui_bindings.dylib" else: "libhqtui_bindings.so")
-proc libraryPath(): string = getEnv("HQTUI_NATIVE_LIB", checkoutLibrary)
-
-{.push cdecl, dynlib: libraryPath(), importc.}
+const version* = "0.7.1"
+
+const
+ engine = currentSourcePath().parentDir / "hqtui" / "engine"
+ engineVersion = staticRead(engine / "VERSION").strip()
+ includes = block:
+ var flags = ""
+ for dir in ["c/include", "cpp/include", "cpp/demo", "cpp/demo/generated", "bindings/include"]:
+ flags.add " -I" & quoteShell(engine / dir)
+ flags
+ # -ffp-contract=off keeps each operation rounded like the TypeScript reference;
+ # a fused multiply-add can move a glyph or colour boundary on ARM.
+ cFlags = "-std=c11 -ffp-contract=off" & includes
+ cppFlags = "-std=c++17 -ffp-contract=off -pthread" & includes &
+ " " & quoteShell("-DHQTUI_DEMO_VERSION=\"" & engineVersion & "\"")
+
+{.compile(engine / "c/src/color.c", cFlags).}
+{.compile(engine / "c/src/unicode.c", cFlags).}
+{.compile(engine / "c/src/buffer.c", cFlags).}
+{.compile(engine / "c/src/layout.c", cFlags).}
+{.compile(engine / "c/src/diff.c", cFlags).}
+{.compile(engine / "c/src/theme.c", cFlags).}
+{.compile(engine / "c/src/surface.c", cFlags).}
+{.compile(engine / "cpp/src/widgets.cpp", cppFlags).}
+{.compile(engine / "cpp/src/scrollbar.cpp", cppFlags).}
+{.compile(engine / "cpp/src/chart.cpp", cppFlags).}
+{.compile(engine / "cpp/src/calendar.cpp", cppFlags).}
+{.compile(engine / "cpp/src/canvas.cpp", cppFlags).}
+{.compile(engine / "cpp/src/shadow.cpp", cppFlags).}
+{.compile(engine / "cpp/src/world.cpp", cppFlags).}
+{.compile(engine / "cpp/src/world_data.cpp", cppFlags).}
+{.compile(engine / "cpp/demo/collect.cpp", cppFlags).}
+{.compile(engine / "cpp/demo/dashboard.cpp", cppFlags).}
+{.compile(engine / "cpp/demo/telemetry.cpp", cppFlags).}
+{.compile(engine / "cpp/demo/showcase.cpp", cppFlags).}
+{.compile(engine / "bindings/src/bridge.cpp", cppFlags).}
+when defined(macosx):
+ {.passL: "-lc++".}
+else:
+ {.passL: "-lstdc++ -pthread".}
+{.passL: "-lm".}
+
+{.push cdecl, importc.}
proc hqb_abi_version(): cint
proc hqb_error(): cstring
proc hqb_create(width, height: cint, theme: cstring): pointer
diff --git a/ports/nim/src/hqtui/engine/README.md b/ports/nim/src/hqtui/engine/README.md
new file mode 100644
index 0000000..6f21feb
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/README.md
@@ -0,0 +1,5 @@
+# Generated: do not edit
+
+A copy of `ports/c`, `ports/cpp` and `ports/bindings`, made by
+`ports/nim/tools/vendor.sh` so the Nim package builds the engine from source.
+Change the originals and rerun the script.
diff --git a/ports/nim/src/hqtui/engine/VERSION b/ports/nim/src/hqtui/engine/VERSION
new file mode 100644
index 0000000..0ea3a94
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/VERSION
@@ -0,0 +1 @@
+0.2.0
diff --git a/ports/nim/src/hqtui/engine/bindings/include/hqtui_bindings.h b/ports/nim/src/hqtui/engine/bindings/include/hqtui_bindings.h
new file mode 100644
index 0000000..731151a
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/bindings/include/hqtui_bindings.h
@@ -0,0 +1,50 @@
+#ifndef HQTUI_BINDINGS_H
+#define HQTUI_BINDINGS_H
+#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+/* Experimental ABI 1. Opaque handles, primitive arguments, no callbacks into a
+ * language runtime. Each scene belongs to one thread. All exceptions stop here.
+ * Output/error strings are borrowed until the next operation on that thread or
+ * scene. Copy them before calling again. NULL destroy is safe; other handles
+ * must be live values returned by create. Never destroy a handle twice.
+ */
+typedef struct hqb_scene hqb_scene;
+int hqb_abi_version(void);
+const char *hqb_error(void);
+hqb_scene *hqb_create(int width, int height, const char *theme);
+void hqb_destroy(hqb_scene *scene);
+/* JSON scene is parsed once per update. At most 1 MiB, 4096 nodes, depth 32.
+ * A rejected update leaves the last valid scene intact. See PROTOCOL.md. */
+int hqb_set(hqb_scene *scene, const char *json, size_t length);
+int hqb_resize(hqb_scene *scene, int width, int height);
+/* Merge the borders of adjacent panels into shared lines, the way CSS collapses
+ * table borders. Applies to the next render or demo frame, and to every one
+ * after it until changed. */
+int hqb_collapse(hqb_scene *scene, int enabled);
+/* text, ansi (full frame), diff (against last rendered frame), hashes (tests).
+ */
+const char *hqb_render(hqb_scene *scene, const char *format);
+/* Render a shared deterministic demo body at the scene's size/theme. */
+const char *hqb_demo_frame(hqb_scene *scene, const char *screen,
+ const char *format);
+/* Optional native terminal transport for custom language applications.
+ * One terminal/demo per process; close/destroy restores it. poll returns a
+ * copied raw UTF-8/escape byte chunk (possibly empty); NULL means failure.
+ * Applications handle keys in their own runtime. present updates terminal size.
+ */
+int hqb_open(hqb_scene *scene);
+int hqb_present(hqb_scene *scene);
+const char *hqb_poll(hqb_scene *scene, int timeout_ms);
+int hqb_interrupted(void);
+void hqb_close(hqb_scene *scene);
+/* Shared ten-screen demo executes INSIDE the calling VM, never execs another
+ * demo. JSON is an array of CLI argument strings; returns the demo exit status.
+ * It owns the terminal for the duration and restores signal handlers on exit.
+ */
+int hqb_demo(const char *arguments, size_t length);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/ports/nim/src/hqtui/engine/bindings/src/bridge.cpp b/ports/nim/src/hqtui/engine/bindings/src/bridge.cpp
new file mode 100644
index 0000000..f0cdd22
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/bindings/src/bridge.cpp
@@ -0,0 +1,688 @@
+#define HQTUI_DEMO_LIBRARY
+#include "../../cpp/demo/main.cpp"
+#include "hqtui_bindings.h"
+#include
+
+namespace {
+using demo::Json;
+using namespace hqtui;
+thread_local std::string error;
+std::atomic terminal_claimed{false};
+void dimensions(int w, int h) {
+ if (w < 1 || h < 1 || w > 500 || h > 200)
+ throw std::runtime_error("dimensions must be 1..500 by 1..200");
+}
+template int checked(F action) noexcept {
+ try {
+ error.clear();
+ action();
+ return 1;
+ } catch (const std::exception &e) {
+ error = e.what();
+ } catch (...) {
+ error = "native operation failed";
+ }
+ return 0;
+}
+Json parse(const char *value, size_t length) {
+ if (!value || length > 1048576)
+ throw std::runtime_error("JSON must be at most 1 MiB");
+ return Json::parse(std::string_view(value, length));
+}
+int integer(const Json &v, int fallback, int lo, int hi) {
+ if (v.null())
+ return fallback;
+ double n = v.n(std::numeric_limits::quiet_NaN());
+ if (!std::isfinite(n) || n != std::floor(n) || n < lo || n > hi)
+ throw std::runtime_error("integer option out of range");
+ return int(n);
+}
+Color color(const Json &v, const hq_theme &t, Color fallback) {
+ if (v.null())
+ return fallback;
+ auto name = v.s("");
+ if (name == "primary")
+ return t.primary;
+ if (name == "secondary")
+ return t.secondary;
+ if (name == "accent")
+ return t.accent;
+ if (name == "muted")
+ return t.muted;
+ if (name == "success")
+ return t.success;
+ if (name == "warning")
+ return t.warning;
+ if (name == "danger")
+ return t.danger;
+ if (name == "foreground")
+ return t.foreground;
+ if (name.size() == 7 && name[0] == '#' &&
+ name.find_first_not_of("0123456789abcdefABCDEF", 1) == name.npos)
+ return hq_hex(name.c_str());
+ throw std::runtime_error("color must be a theme role or #rrggbb");
+}
+Constraint size(const Json &n, Constraint fallback = fr()) {
+ if (n["size"].null())
+ return fallback;
+ return cells(integer(n["size"], 0, 0, 10000));
+}
+void validate(const Json &n, int depth, int &count) {
+ if (depth > 32 || ++count > 4096 ||
+ !std::holds_alternative(n.value))
+ throw std::runtime_error(
+ "scene exceeds depth/node limits or node is not an object");
+ auto type = n["type"].s("");
+ static const std::vector types = {
+ "row", "col", "panel", "text", "spacer", "divider",
+ "meter", "graph", "gauge", "table", "keys", "log",
+ "badge", "progress", "sparkline", "heatbar", "columns", "donut",
+ "list", "tree", "button", "checkbox", "select", "input",
+ "tabs", "statusbar", "label", "heading", "meters", "modal",
+ "commandpalette", "tooltip", "scrollbar", "chart", "calendar"};
+ if (std::find(types.begin(), types.end(), type) == types.end())
+ throw std::runtime_error("unknown widget: " + type);
+ if (!n["children"].null() &&
+ !std::holds_alternative(n["children"].value))
+ throw std::runtime_error("children must be an array");
+ for (auto &child : n["children"].array())
+ validate(child, depth + 1, count);
+}
+/// An overlay drawn after layout, over the whole frame. Collected while the
+/// tree is walked rather than drawn where it appears, because a modal centred
+/// on the node that declared it would land inside that node's rectangle
+/// instead of on the screen.
+struct PendingOverlay {
+ enum Kind { kModal, kPalette, kTooltip } kind;
+ Modal modal;
+ CommandPalette palette;
+ Tooltip tooltip;
+};
+
+void node(UI &ui, const Json &n, std::vector &overlays) {
+ auto type = n["type"].s();
+ auto body = [&n, &overlays](UI &p) {
+ for (auto &child : n["children"].array())
+ node(p, child, overlays);
+ };
+ int gap = integer(n["gap"], 0, 0, 100);
+ auto c = color(n["color"], ui.t(), ui.t().foreground);
+ if (type == "row" || type == "col")
+ ui.group(size(n), gap, type == "row", body);
+ else if (type == "panel")
+ ui.panel(n["title"].s(""), body, size(n), n["subtitle"].s(""));
+ else if (type == "text")
+ ui.text(n["text"].s(""), c, integer(n["align"], HQ_LEFT, 0, 2),
+ size(n, automatic(1)), integer(n["attrs"], 0, 0, 127));
+ else if (type == "spacer")
+ ui.spacer(size(n));
+ else if (type == "divider")
+ ui.divider(n["text"].s(""));
+ else if (type == "meter") {
+ Meter m;
+ m.value = n["value"].n();
+ m.label = n["label"].s("");
+ m.color = c;
+ ui.meter(m);
+ } else if (type == "gauge") {
+ auto v = n["value"].n();
+ auto label = n["label"].s("");
+ ui.draw([v, label](Surface s) { draw_gauge(s, v, label); }, size(n));
+ } else if (type == "graph") {
+ Graph g;
+ g.series.push_back({demo::numbers(n["values"]), c, "", true});
+ if (!n["max"].null())
+ g.max = n["max"].n();
+ g.min = n["min"].n();
+ ui.graph(g, size(n));
+ } else if (type == "keys") {
+ std::vector rows;
+ for (auto &r : n["rows"].array())
+ rows.push_back({r.at(0).s(""), r.at(1).s(""), c});
+ ui.keys(rows);
+ } else if (type == "table") {
+ Table t;
+ for (auto &column : n["columns"].array())
+ t.columns.push_back({column.s(""), -1, 1, 0, HQ_LEFT});
+ for (auto &r : n["rows"].array()) {
+ TableRow row;
+ for (auto &v : r.array())
+ row.cells.push_back(v.s(""));
+ t.rows.push_back(std::move(row));
+ }
+ if (t.columns.size() > 256 || t.rows.size() > 10000)
+ throw std::runtime_error("table too large");
+ int selected = integer(n["selected"], 0, 0, 10000),
+ offset = integer(n["offset"], 0, 0, 10000);
+ ui.draw(
+ [t, selected, offset](Surface s) mutable {
+ Pane p;
+ p.selected = selected;
+ p.offset = offset;
+ t.pane = &p;
+ draw_table(s, t);
+ },
+ size(n));
+ } else if (type == "label") {
+ // Text in the theme's muted colour: captions, and the line under a number
+ // that says what the number is.
+ ui.text(n["text"].s(""), color(n["color"], ui.t(), ui.t().muted),
+ integer(n["align"], HQ_LEFT, 0, 2), size(n, automatic(1)),
+ integer(n["attrs"], 0, 0, 127));
+ } else if (type == "heading") {
+ ui.text(n["text"].s(""), color(n["color"], ui.t(), ui.t().title),
+ integer(n["align"], HQ_LEFT, 0, 2), size(n, automatic(1)),
+ integer(n["attrs"], HQ_BOLD, 0, 127));
+ } else if (type == "meters") {
+ Meters m;
+ for (auto &item : n["items"].array())
+ m.items.push_back({item["label"].s(""), item["value"].n(),
+ item["max"].n(0), color(item["color"], ui.t(), 0),
+ item["text"].s("")});
+ if (m.items.size() > 1000)
+ throw std::runtime_error("too many meters");
+ m.columns = integer(n["columns"], 1, 1, 16);
+ m.label_width = integer(n["labelWidth"], -1, -1, 100);
+ m.value_width = integer(n["valueWidth"], -1, -1, 100);
+ m.style = integer(n["style"], HQ_BAR_SMOOTH, 0, 2);
+ m.gap = integer(n["gap"], 2, 0, 100);
+ int rows = (int(m.items.size()) + m.columns - 1) / m.columns;
+ ui.draw([m](Surface s) { draw_meters(s, m); }, size(n, cells(rows)));
+ } else if (type == "modal") {
+ PendingOverlay overlay;
+ overlay.kind = PendingOverlay::kModal;
+ overlay.modal.title = n["title"].s("");
+ overlay.modal.message = n["message"].s("");
+ overlay.modal.width = integer(n["width"], -1, -1, 1000);
+ overlay.modal.height = integer(n["height"], -1, -1, 1000);
+ overlay.modal.backdrop = n["backdrop"].b(true);
+ overlay.modal.align = integer(n["align"], HQ_CENTER, 0, 2);
+ for (auto &button : n["buttons"].array())
+ overlay.modal.buttons.push_back(
+ {button["label"].s(""),
+ integer(button["variant"], HQ_BUTTON_PRIMARY, 0, 4),
+ button["focused"].b(false)});
+ if (overlay.modal.buttons.size() > 16)
+ throw std::runtime_error("too many modal buttons");
+ overlays.push_back(std::move(overlay));
+ } else if (type == "commandpalette") {
+ PendingOverlay overlay;
+ overlay.kind = PendingOverlay::kPalette;
+ overlay.palette.query = n["query"].s("");
+ overlay.palette.placeholder = n["placeholder"].s("");
+ for (auto &item : n["items"].array())
+ overlay.palette.items.push_back({item["label"].s(""), item["hint"].s("")});
+ if (overlay.palette.items.size() > 1000)
+ throw std::runtime_error("too many palette items");
+ overlay.palette.selected = integer(n["selected"], 0, 0, 1000);
+ overlay.palette.width = integer(n["width"], -1, -1, 1000);
+ overlay.palette.height = integer(n["height"], -1, -1, 1000);
+ overlays.push_back(std::move(overlay));
+ } else if (type == "tooltip") {
+ PendingOverlay overlay;
+ overlay.kind = PendingOverlay::kTooltip;
+ overlay.tooltip.text = n["text"].s("");
+ overlay.tooltip.x = integer(n["x"], 0, 0, 10000);
+ overlay.tooltip.y = integer(n["y"], 0, 0, 10000);
+ overlay.tooltip.color = color(n["color"], ui.t(), 0);
+ overlays.push_back(std::move(overlay));
+ } else if (type == "badge") {
+ Badge b;
+ b.text = n["text"].s("");
+ b.color = n["color"].null() ? 0 : c;
+ b.variant = integer(n["variant"], HQ_BADGE_FILLED, 0, 2);
+ b.align = integer(n["align"], HQ_LEFT, 0, 2);
+ ui.badge(b);
+ } else if (type == "progress") {
+ Progress p;
+ p.value = n["value"].n();
+ p.max = n["max"].null() ? 1 : n["max"].n();
+ p.label = n["label"].s("");
+ p.color = n["color"].null() ? 0 : c;
+ p.show_count = n["count"].b(false);
+ ui.progress(p);
+ } else if (type == "sparkline") {
+ Sparkline sp;
+ sp.values = demo::numbers(n["values"]);
+ sp.label = n["label"].s("");
+ sp.text = n["text"].s("");
+ sp.color = n["color"].null() ? 0 : c;
+ if (!n["min"].null())
+ sp.min = n["min"].n();
+ if (!n["max"].null())
+ sp.max = n["max"].n();
+ ui.sparkline(sp);
+ } else if (type == "heatbar") {
+ HeatBar hb;
+ hb.value = n["value"].n();
+ hb.color = n["color"].null() ? 0 : c;
+ ui.heat_bar(hb);
+ } else if (type == "columns") {
+ Columns cols;
+ cols.values = demo::numbers(n["values"]);
+ cols.color = n["color"].null() ? 0 : c;
+ if (!n["max"].null())
+ cols.max = n["max"].n();
+ ui.columns(cols, size(n));
+ } else if (type == "donut") {
+ Donut d;
+ for (auto &seg : n["segments"].array())
+ d.segments.push_back({seg["value"].n(), 0, seg["label"].s("")});
+ if (d.segments.size() > 64)
+ throw std::runtime_error("too many donut segments");
+ ui.donut(d, size(n));
+ } else if (type == "calendar") {
+ Calendar cal;
+ cal.year = integer(n["year"], 1970, -9999, 9999);
+ cal.month = integer(n["month"], 1, 1, 12);
+ cal.selected = integer(n["selected"], 0, 0, 31);
+ cal.week_start = integer(n["weekStart"], 1, 0, 1);
+ if (!n["header"].null())
+ cal.header = n["header"].b(true);
+ if (!n["weekdays"].null())
+ cal.weekdays = n["weekdays"].b(true);
+ for (auto &mj : n["marks"].array()) {
+ CalendarMark mark;
+ mark.day = integer(mj["day"], 0, 0, 31);
+ mark.bold = mj["bold"].b(false);
+ cal.marks.push_back(mark);
+ }
+ if (cal.marks.size() > 64)
+ throw std::runtime_error("too many calendar marks");
+ ui.calendar(cal);
+ } else if (type == "chart") {
+ Chart chart;
+ for (auto &sj : n["series"].array()) {
+ ChartSeries cs;
+ for (auto &pj : sj["points"].array())
+ cs.points.push_back({pj["x"].n(), pj["y"].n()});
+ if (cs.points.size() > 10000)
+ throw std::runtime_error("chart series too large");
+ cs.label = sj["label"].s("");
+ auto mark = sj["mark"].s("line");
+ cs.mark = mark == "scatter" ? HQ_MARK_SCATTER
+ : mark == "bar" ? HQ_MARK_BAR
+ : HQ_MARK_LINE;
+ cs.fill = sj["fill"].b(false);
+ chart.series.push_back(std::move(cs));
+ }
+ if (chart.series.size() > 64)
+ throw std::runtime_error("too many chart series");
+ chart.axis = n["axis"].b(false);
+ chart.legend = n["legend"].b(false);
+ if (!n["mode"].null())
+ chart.plot.mode = n["mode"].s("braille");
+ auto read_axis = [](const Json &j) -> std::optional {
+ if (j.null())
+ return std::nullopt;
+ Axis a;
+ if (!j["min"].null())
+ a.min = j["min"].n();
+ if (!j["max"].null())
+ a.max = j["max"].n();
+ a.ticks = integer(j["ticks"], 0, 0, 64);
+ return a;
+ };
+ chart.plot.x = read_axis(n["x"]);
+ chart.plot.y = read_axis(n["y"]);
+ ui.chart(chart, size(n));
+ } else if (type == "scrollbar") {
+ Scrollbar bar;
+ bar.total = integer(n["total"], 0, 0, 1000000);
+ bar.viewport = integer(n["viewport"], 0, 0, 1000000);
+ bar.offset = integer(n["offset"], 0, 0, 1000000);
+ auto edge = n["orientation"].s("right");
+ bar.orientation = edge == "left" ? HQ_SCROLLBAR_LEFT
+ : edge == "bottom" ? HQ_SCROLLBAR_BOTTOM
+ : edge == "top" ? HQ_SCROLLBAR_TOP
+ : HQ_SCROLLBAR_RIGHT;
+ ui.scrollbar(bar, n["id"].s(""));
+ } else if (type == "list") {
+ List l;
+ for (auto &item : n["items"].array())
+ l.items.push_back({item.s(""), 0});
+ if (l.items.size() > 10000)
+ throw std::runtime_error("list too large");
+ l.selected = integer(n["selected"], -1, -1, 10000);
+ l.offset = integer(n["offset"], 0, 0, 10000);
+ l.bullet = n["bullet"].s("");
+ l.scrollbar = n["scrollbar"].b(false);
+ ui.list(l, n["id"].s(""));
+ } else if (type == "tree") {
+ Tree tree;
+ // One level of nesting, which is what a record-shaped scene can describe:
+ // a node with its children, not an arbitrary depth.
+ for (auto &node_json : n["nodes"].array()) {
+ TreeNode parent;
+ parent.label = node_json["label"].s("");
+ for (auto &child : node_json["children"].array())
+ parent.children.push_back(TreeNode{child["label"].s("")});
+ tree.nodes.push_back(std::move(parent));
+ }
+ tree.selected = integer(n["selected"], -1, -1, 10000);
+ ui.tree(tree, n["id"].s(""));
+ } else if (type == "button") {
+ Button b;
+ b.label = n["label"].s("");
+ b.focused = n["focused"].b(false);
+ b.variant = integer(n["variant"], HQ_BUTTON_PRIMARY, 0, 4);
+ b.disabled = n["disabled"].b(false);
+ ui.button(b);
+ } else if (type == "checkbox") {
+ Checkbox cb;
+ cb.label = n["label"].s("");
+ cb.checked = n["checked"].b(false);
+ cb.focused = n["focused"].b(false);
+ cb.variant = integer(n["variant"], HQ_CHECKBOX_BOX, 0, 2);
+ ui.checkbox(cb);
+ } else if (type == "select") {
+ Select sel;
+ sel.value = n["value"].s("");
+ sel.open = n["open"].b(false);
+ for (auto &option : n["options"].array())
+ sel.options.push_back(option.s(""));
+ if (sel.options.size() > 1000)
+ throw std::runtime_error("too many options");
+ sel.selected_index = integer(n["selected"], 0, 0, 1000);
+ sel.focused = n["focused"].b(false);
+ ui.select(sel);
+ } else if (type == "input") {
+ TextInput input;
+ input.value = n["value"].s("");
+ input.placeholder = n["placeholder"].s("");
+ input.label = n["label"].s("");
+ input.focused = n["focused"].b(false);
+ input.password = n["password"].b(false);
+ ui.text_input(input);
+ } else if (type == "tabs") {
+ Tabs tabs;
+ for (auto &tab : n["tabs"].array())
+ tabs.tabs.push_back(tab.s(""));
+ if (tabs.tabs.size() > 100)
+ throw std::runtime_error("too many tabs");
+ tabs.active = integer(n["active"], 0, 0, 100);
+ tabs.variant = integer(n["variant"], HQ_TAB_FILLED, 0, 1);
+ ui.tabs(tabs);
+ } else if (type == "statusbar") {
+ StatusBar bar;
+ for (auto &item : n["items"].array())
+ bar.items.push_back({item["key"].s(""), item["label"].s(""), 0,
+ item["active"].b(false)});
+ for (auto &item : n["right"].array())
+ bar.right.push_back({item["key"].s(""), item["label"].s(""), 0, false});
+ if (bar.items.size() + bar.right.size() > 100)
+ throw std::runtime_error("too many status items");
+ ui.status_bar(bar);
+ } else if (type == "log") {
+ std::vector entries;
+ for (auto &v : n["entries"].array())
+ entries.push_back({v["time"].s(""), v["level"].s("INFO"),
+ v["message"].s(""), v["meta"].s("")});
+ int offset = integer(n["offset"], 0, 0, 10000);
+ ui.draw(
+ [entries, offset](Surface s) {
+ Pane p;
+ p.offset = offset;
+ draw_log(s, entries, &p, true);
+ },
+ size(n));
+ }
+}
+std::string hashes(const Buffer &frame) {
+ std::string out = "[";
+ for (int y = 0; y < frame.height(); y++) {
+ uint32_t h = 2166136261u;
+ auto byte = [&](unsigned char b) { h = (h ^ b) * 16777619u; };
+ for (int x = 0; x < frame.width(); x++) {
+ auto cell = frame.cell(x, y);
+ char scratch[5];
+ std::string_view glyph =
+ hq_buffer_cell_text(frame.native_handle(), x, y, scratch);
+ for (unsigned char b : glyph)
+ byte(b);
+ byte(0);
+ for (auto value :
+ {uint32_t(cell.fg), uint32_t(cell.bg), uint32_t(cell.attrs)})
+ for (int i = 0; i < 4; i++)
+ byte((value >> (8 * i)) & 255);
+ }
+ if (y)
+ out += ',';
+ out += std::to_string(h);
+ }
+ return out + "]";
+}
+} // namespace
+struct hqb_scene {
+ Buffer previous, frame;
+ Encoder encoder;
+ const hq_theme *theme;
+ Json tree;
+ std::string output, input;
+ bool first = true, collapse = false;
+ std::unique_ptr terminal;
+ hqb_scene(int w, int h, const hq_theme *t)
+ : previous(w, h), frame(w, h), theme(t) {}
+ void paint(const Json &scene) {
+ frame.clear(theme->background, theme->foreground);
+ UI ui(frame.surface(theme), false, 0, nullptr, collapse);
+ std::vector overlays;
+ node(ui, scene, overlays);
+ ui.flush();
+ // Overlays go on after the flush, so a modal covers the laid-out screen
+ // rather than being covered by it.
+ Surface root = frame.surface(theme);
+ for (const auto &overlay : overlays) {
+ switch (overlay.kind) {
+ case PendingOverlay::kModal:
+ draw_modal(root, overlay.modal);
+ break;
+ case PendingOverlay::kPalette:
+ draw_command_palette(root, overlay.palette);
+ break;
+ case PendingOverlay::kTooltip:
+ draw_tooltip(root, overlay.tooltip);
+ break;
+ }
+ }
+ }
+ const char *finish(const char *format) {
+ if (!format)
+ throw std::runtime_error("missing format");
+ std::string f = format;
+ if (f == "text") {
+ output.clear();
+ for (int y = 0; y < frame.height(); y++)
+ output += frame.row(y) + "\n";
+ } else if (f == "hashes")
+ output = hashes(frame);
+ else if (f == "ansi" || f == "diff")
+ output = encoder.encode(previous, frame, first || f == "ansi").output;
+ else
+ throw std::runtime_error("format must be text, ansi, diff or hashes");
+ previous.copy_from(frame);
+ first = false;
+ return output.c_str();
+ }
+};
+extern "C" {
+int hqb_abi_version(void) { return 1; }
+const char *hqb_error(void) { return error.c_str(); }
+hqb_scene *hqb_create(int w, int h, const char *name) {
+ hqb_scene *s = nullptr;
+ checked([&] {
+ dimensions(w, h);
+ auto t = hq_theme_named(name ? name : "dark");
+ if (!t)
+ throw std::runtime_error("unknown theme");
+ auto candidate = std::make_unique(w, h, t);
+ candidate->tree = Json::Object{{"type", "col"}};
+ s = candidate.release();
+ });
+ return s;
+}
+void hqb_close(hqb_scene *s) {
+ if (s && s->terminal) {
+ s->terminal.reset();
+ terminal_claimed = false;
+ }
+}
+void hqb_destroy(hqb_scene *s) {
+ hqb_close(s);
+ delete s;
+}
+int hqb_set(hqb_scene *s, const char *json, size_t length) {
+ return checked([&] {
+ if (!s)
+ throw std::runtime_error("closed scene");
+ auto tree = parse(json, length);
+ int count = 0;
+ validate(tree, 0, count);
+ // Validate widget options and allocations before committing the new tree.
+ s->paint(tree);
+ s->tree = std::move(tree);
+ });
+}
+int hqb_resize(hqb_scene *s, int w, int h) {
+ return checked([&] {
+ if (!s)
+ throw std::runtime_error("closed scene");
+ dimensions(w, h);
+ s->frame.resize(w, h);
+ s->previous.resize(w, h);
+ s->first = true;
+ });
+}
+int hqb_collapse(hqb_scene *s, int enabled) {
+ return checked([&] {
+ if (!s)
+ throw std::runtime_error("closed scene");
+ s->collapse = enabled != 0;
+ });
+}
+const char *hqb_render(hqb_scene *s, const char *format) {
+ if (!checked([&] {
+ if (!s || !format)
+ throw std::runtime_error("closed scene or missing format");
+ s->paint(s->tree);
+ s->finish(format);
+ }))
+ return nullptr;
+ return s->output.c_str();
+}
+const char *hqb_demo_frame(hqb_scene *s, const char *screen,
+ const char *format) {
+ if (!checked([&] {
+ if (!s || !screen)
+ throw std::runtime_error("closed scene or missing screen");
+ demo::State state;
+ state.data = Json::parse(demo::sample_json);
+ int i = 0;
+ for (; i < 10 && std::string(screen) != demo::screens[i]; i++) {
+ }
+ if (i == 10)
+ throw std::runtime_error("unknown demo screen");
+ state.screen = i;
+ for (int t = 0; t < 9; t++)
+ if (std::string(s->theme->name) == demo::themes[t])
+ state.theme_index = t;
+ // Collapsing changes the layout, not only the glyphs: the screens close
+ // the seams between panels so their borders have something to merge.
+ state.collapse = s->collapse;
+ s->frame.clear(s->theme->background, s->theme->foreground);
+ UI ui(s->frame.surface(s->theme), false, 0, nullptr, s->collapse);
+ demo::render(ui, state, true);
+ ui.flush();
+ s->finish(format);
+ }))
+ return nullptr;
+ return s->output.c_str();
+}
+int hqb_open(hqb_scene *s) {
+ return checked([&] {
+ if (!s)
+ throw std::runtime_error("closed scene");
+ if (s->terminal)
+ return;
+ bool expected = false;
+ if (!terminal_claimed.compare_exchange_strong(expected, true))
+ throw std::runtime_error("terminal already acquired");
+ try {
+ demo::interrupted = 0;
+ s->terminal = std::make_unique();
+ s->first = true;
+ } catch (...) {
+ terminal_claimed = false;
+ throw;
+ }
+ });
+}
+int hqb_present(hqb_scene *s) {
+ return checked([&] {
+ if (!s || !s->terminal)
+ throw std::runtime_error("terminal not open");
+ auto dimensions = s->terminal->size();
+ if (dimensions.first != s->frame.width() ||
+ dimensions.second != s->frame.height())
+ if (!hqb_resize(s, dimensions.first, dimensions.second))
+ throw std::runtime_error(error);
+ auto bytes = hqb_render(s, "diff");
+ if (!bytes)
+ throw std::runtime_error(error);
+ if (!demo::write_all(s->output))
+ throw std::runtime_error("terminal write failed");
+ });
+}
+const char *hqb_poll(hqb_scene *s, int timeout_ms) {
+ if (!checked([&] {
+ if (!s || !s->terminal || timeout_ms < 0 || timeout_ms > 1000)
+ throw std::runtime_error("invalid terminal poll");
+ s->input.clear();
+ struct pollfd fd{0, POLLIN, 0};
+ auto ready = poll(&fd, 1, timeout_ms);
+ if (ready < 0 && errno != EINTR)
+ throw std::runtime_error("terminal poll failed");
+ if (ready > 0 && fd.revents & (POLLHUP | POLLERR))
+ demo::interrupted = SIGHUP;
+ if (ready > 0 && fd.revents & POLLIN) {
+ char b[4096];
+ auto n = read(0, b, sizeof b);
+ if (n > 0)
+ s->input.assign(b, size_t(n));
+ }
+ }))
+ return nullptr;
+ return s->input.c_str();
+}
+int hqb_interrupted(void) { return demo::interrupted; }
+int hqb_demo(const char *arguments, size_t length) {
+ int status = 2;
+ if (!checked([&] {
+ auto args = parse(arguments, length);
+ if (!std::holds_alternative(args.value) ||
+ args.array().size() > 128)
+ throw std::runtime_error(
+ "arguments must be an array of at most 128 strings");
+ std::vector strings{"hqtui-demo-binding"};
+ for (auto &v : args.array()) {
+ if (!std::holds_alternative(v.value) ||
+ v.s().find('\0') != std::string::npos)
+ throw std::runtime_error("arguments must be NUL-free strings");
+ strings.push_back(v.s());
+ }
+ std::vector argv;
+ for (auto &v : strings)
+ argv.push_back(v.data());
+ argv.push_back(nullptr);
+ bool expected = false;
+ if (!terminal_claimed.compare_exchange_strong(expected, true))
+ throw std::runtime_error("terminal already acquired");
+ struct Release {
+ ~Release() { terminal_claimed = false; }
+ } release;
+ status = hqtui_demo_main(int(strings.size()), argv.data());
+ }))
+ return -1;
+ return status;
+}
+}
diff --git a/ports/nim/src/hqtui/engine/c/include/hqtui.h b/ports/nim/src/hqtui/engine/c/include/hqtui.h
new file mode 100644
index 0000000..fd087eb
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/include/hqtui.h
@@ -0,0 +1,184 @@
+#ifndef HQTUI_H
+#define HQTUI_H
+
+/* Native C11 renderer. No terminal state is changed by framebuffer operations.
+ * Objects are independent and may be used on separate threads, but an individual
+ * object requires external synchronization. Strings passed to *_n may contain NUL.
+ * Returned strings are borrowed until the next mutation of the owning object.
+ */
+#include
+#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef uint32_t hq_color;
+enum { HQ_BOLD=1, HQ_DIM=2, HQ_ITALIC=4, HQ_UNDERLINE=8,
+ HQ_BLINK=16, HQ_REVERSE=32, HQ_STRIKE=64 };
+enum { HQ_STYLE_FG=1, HQ_STYLE_BG=2, HQ_STYLE_ATTRS=4, HQ_STYLE_ALL=7 };
+typedef struct { hq_color fg, bg; uint16_t attrs, mask; } hq_style;
+typedef struct { int x, y, width, height; } hq_rect;
+typedef struct hq_buffer hq_buffer;
+typedef struct hq_encoder hq_encoder;
+typedef struct { uint32_t value; hq_color fg, bg; uint16_t attrs; } hq_cell;
+typedef struct {
+ const char *data;
+ size_t length, changed_cells, dirty_rows;
+} hq_encoded;
+
+hq_color hq_rgb(int r, int g, int b);
+hq_color hq_hex(const char *s);
+hq_color hq_indexed(int index);
+hq_color hq_from256(int index);
+int hq_to256(hq_color c);
+int hq_to16(hq_color c);
+hq_color hq_mix(hq_color a, hq_color b, double t);
+hq_color hq_grayscale(hq_color c);
+double hq_luminance(hq_color c);
+double hq_contrast(hq_color a, hq_color b);
+hq_color hq_gradient(const hq_color *stops, size_t count, double t);
+int hq_char_width(uint32_t cp);
+size_t hq_text_width_n(const char *text, size_t length);
+size_t hq_text_width(const char *text);
+
+/* create/resize reject negative or excessive dimensions. resize is transactional
+ * on allocation failure. Zero dimensions are valid. Each dimension <= 32768,
+ * total cells <= 16,777,216. Destroy accepts NULL. */
+hq_buffer *hq_buffer_create(int width, int height);
+void hq_buffer_destroy(hq_buffer *buffer);
+int hq_buffer_resize(hq_buffer *buffer, int width, int height);
+int hq_buffer_width(const hq_buffer *buffer);
+int hq_buffer_height(const hq_buffer *buffer);
+size_t hq_buffer_allocations(const hq_buffer *buffer);
+void hq_buffer_clear(hq_buffer *buffer, hq_color bg, hq_color fg);
+int hq_buffer_cell(const hq_buffer *buffer, int x, int y, hq_cell *out);
+const char *hq_buffer_cell_text(const hq_buffer *buffer, int x, int y, char scratch[5]);
+int hq_buffer_set(hq_buffer *buffer, int x, int y, uint32_t cp, hq_style style);
+size_t hq_buffer_write_n(hq_buffer *buffer, int x, int y, const char *text,
+ size_t length, hq_style style, size_t max_columns);
+size_t hq_buffer_write(hq_buffer *buffer, int x, int y, const char *text,
+ hq_style style, size_t max_columns);
+void hq_buffer_fill(hq_buffer *buffer, hq_rect rect, uint32_t cp, hq_style style);
+void hq_buffer_style(hq_buffer *buffer, hq_rect rect, hq_style style);
+int hq_buffer_copy(hq_buffer *destination, const hq_buffer *source);
+/* Required byte count excluding NUL; safely truncates output to capacity-1. */
+size_t hq_buffer_row(const hq_buffer *buffer, int row, char *out, size_t capacity);
+
+typedef enum { HQ_CELLS, HQ_PERCENT, HQ_FR, HQ_AUTO, HQ_FILL } hq_size_kind;
+typedef struct {
+ hq_size_kind kind;
+ double value;
+ int min, max, intrinsic; /* max=-1 means unbounded */
+} hq_constraint;
+/* count <= 4096. Returns 0 on invalid input/allocation failure. */
+int hq_solve(int total, const hq_constraint *items, size_t count, int gap, int *out);
+int hq_stack(hq_rect rect, const hq_constraint *items, size_t count,
+ int horizontal, int gap, hq_rect *out);
+/* As above, with a gap per seam (count-1 of them) which may be negative.
+ * A negative seam is how collapsed borders work: two panels overlap by the
+ * column their borders share. `gaps` may be NULL when count < 2. */
+int hq_solve_gaps(int total, const hq_constraint *items, size_t count,
+ const int *gaps, int *out);
+int hq_stack_gaps(hq_rect rect, const hq_constraint *items, size_t count,
+ int horizontal, const int *gaps, hq_rect *out);
+
+/* Where space the children leave over goes. It only applies when there is
+ * slack: a container holding any fr or fill child has none, because that child
+ * has already absorbed it. HQ_JUSTIFY_START is what every layout did before
+ * this existed. */
+typedef enum {
+ HQ_JUSTIFY_START = 0,
+ HQ_JUSTIFY_END,
+ HQ_JUSTIFY_CENTER,
+ HQ_JUSTIFY_SPACE_BETWEEN,
+ HQ_JUSTIFY_SPACE_AROUND,
+ HQ_JUSTIFY_SPACE_EVENLY
+} hq_justify;
+
+/* The offset before the first child, and the extra added at each of the
+ * count-1 seams. `seams` may be NULL when count < 2. Returns 0 on bad input. */
+int hq_distribute(int slack, size_t count, hq_justify justify, int *lead, int *seams);
+int hq_stack_justified(hq_rect rect, const hq_constraint *items, size_t count,
+ int horizontal, const int *gaps, hq_justify justify, hq_rect *out);
+
+/* colors: 0 (no color), 16, 256 or 16777216 (truecolor).
+ * encode reuses output capacity; allocation failure invalidates terminal state.
+ * It never writes to stdout. An unchanged frame produces zero bytes. */
+hq_encoder *hq_encoder_create(int colors, int monochrome);
+void hq_encoder_destroy(hq_encoder *encoder);
+void hq_encoder_invalidate(hq_encoder *encoder);
+size_t hq_encoder_allocations(const hq_encoder *encoder);
+int hq_encode(hq_encoder *encoder, const hq_buffer *previous,
+ const hq_buffer *next, int full, hq_encoded *out);
+
+typedef struct {
+ const char *name;
+ int dark;
+ hq_color background, surface, foreground, muted, primary, secondary, accent;
+ hq_color success, warning, danger, info, border, border_focused, title;
+ hq_color selection, selection_text, cursor;
+ hq_color graph[8], heat[8];
+ size_t graph_count, heat_count;
+} hq_theme;
+size_t hq_theme_count(void);
+const hq_theme *hq_theme_at(size_t index);
+const hq_theme *hq_theme_named(const char *name); /* NULL if unknown */
+hq_color hq_heat(const hq_theme *theme, double value);
+hq_color hq_series(const hq_theme *theme, size_t index);
+
+/* Surfaces borrow their buffer and theme; both must outlive all their surfaces.
+ * Surface creation, clipping, and text/box rendering do not allocate per frame
+ * except when interning a new Unicode cluster in the buffer's bounded pool. */
+typedef struct { hq_buffer *buffer; const hq_theme *theme; hq_rect rect, clip; } hq_surface;
+typedef struct { hq_style style; int align, no_ellipsis, max_columns, has_max_columns; } hq_text_options;
+enum { HQ_LEFT, HQ_CENTER, HQ_RIGHT };
+enum { HQ_ROUNDED, HQ_SINGLE, HQ_DOUBLE, HQ_THICK, HQ_DASHED, HQ_ASCII, HQ_NO_BORDER };
+/* Edge bits for a border glyph: 1 up, 2 right, 4 down, 8 left.
+ *
+ * Collapsing two panel borders is the union of their edges. A panel's
+ * top-right corner (down + left) landing on its neighbour's top-left
+ * (down + right) is down + left + right, which is the T that makes the two
+ * read as one frame. */
+enum { HQ_EDGE_UP = 1, HQ_EDGE_RIGHT = 2, HQ_EDGE_DOWN = 4, HQ_EDGE_LEFT = 8 };
+/* The edges of a border glyph, or -1 when it is not one. */
+int hq_border_bits(uint32_t cp);
+/* The glyph in `border` with exactly these edges, or 0 if there is none. */
+uint32_t hq_border_glyph(int border,int bits);
+/* Which edges of a box to draw, as a mask. Zero means all four, so a caller
+ * that has never heard of this gets the box it always got. HQ_SIDES_NONE draws
+ * no rule and insets nothing, the same as a border of HQ_NO_BORDER. */
+enum {
+ HQ_SIDE_TOP = 1,
+ HQ_SIDE_RIGHT = 2,
+ HQ_SIDE_BOTTOM = 4,
+ HQ_SIDE_LEFT = 8,
+ HQ_SIDES_ALL = 15,
+ HQ_SIDES_NONE = 16
+};
+
+typedef struct {
+ int border, title_align, no_fill;
+ /* A mask of HQ_SIDE_*, or 0 for all four. */
+ int sides;
+ /* Merge this border with one already in the same cell rather than
+ * overwriting it. Zero unless the caller asks for collapsed borders. */
+ int collapse;
+ hq_style border_style, title_style, subtitle_style, footer_style;
+ hq_color background;
+ int has_background;
+ const char *title, *subtitle, *footer;
+} hq_box_options;
+hq_rect hq_intersect(hq_rect a,hq_rect b);
+hq_rect hq_inset(hq_rect rect,int top,int right,int bottom,int left);
+hq_surface hq_surface_root(hq_buffer *buffer,const hq_theme *theme);
+hq_surface hq_surface_region(hq_surface parent,hq_rect absolute);
+hq_surface hq_surface_sub(hq_surface parent,hq_rect local);
+void hq_surface_set(hq_surface surface,int x,int y,uint32_t cp,hq_style style);
+size_t hq_surface_text(hq_surface surface,int x,int y,const char *text,hq_text_options options);
+void hq_surface_fill(hq_surface surface,uint32_t cp,hq_style style);
+hq_surface hq_surface_box(hq_surface surface,hq_box_options options);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/ports/nim/src/hqtui/engine/c/src/buffer.c b/ports/nim/src/hqtui/engine/c/src/buffer.c
new file mode 100644
index 0000000..1ba2a1f
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/src/buffer.c
@@ -0,0 +1,188 @@
+#include "internal.h"
+
+static int dimensions(int w,int h) {
+ return w>=0 && h>=0 && w<=32768 && h<=32768 && (size_t)w*(size_t)h<=16777216;
+}
+hq_buffer *hq_buffer_create(int w,int h) {
+ if(!dimensions(w,h)) return NULL;
+ hq_buffer *b=calloc(1,sizeof(*b));
+ if(!b) return NULL;
+ b->allocations=1;
+ if(!hq_buffer_resize(b,w,h)) { hq_buffer_destroy(b); return NULL; }
+ return b;
+}
+void hq_buffer_destroy(hq_buffer *b) {
+ if(!b) return;
+ free(b->chars); free(b->fg); free(b->bg); free(b->attrs);
+ free(b->clusters); free(b->cluster_hash); free(b);
+}
+int hq_buffer_resize(hq_buffer *b,int w,int h) {
+ if(!b || !dimensions(w,h)) return 0;
+ if(w==b->width && h==b->height) return 1;
+ size_t n=(size_t)w*(size_t)h;
+ if(n>b->capacity) {
+ uint32_t *chars=malloc(n*sizeof(*chars)), *fg=malloc(n*sizeof(*fg)), *bg=malloc(n*sizeof(*bg));
+ uint16_t *attrs=malloc(n*sizeof(*attrs));
+ if(!chars || !fg || !bg || !attrs) { free(chars); free(fg); free(bg); free(attrs); return 0; }
+ free(b->chars); free(b->fg); free(b->bg); free(b->attrs);
+ b->chars=chars; b->fg=fg; b->bg=bg; b->attrs=attrs;
+ b->capacity=n; b->allocations+=4;
+ }
+ b->width=w; b->height=h; hq_buffer_clear(b,0,0); return 1;
+}
+int hq_buffer_width(const hq_buffer *b) { return b ? b->width:0; }
+int hq_buffer_height(const hq_buffer *b) { return b ? b->height:0; }
+size_t hq_buffer_allocations(const hq_buffer *b) { return b ? b->allocations:0; }
+void hq_buffer_clear(hq_buffer *b,hq_color bg,hq_color fg) {
+ if(!b) return;
+ size_t n=(size_t)b->width*(size_t)b->height;
+ for(size_t i=0;ichars[i]=32; b->fg[i]=fg; b->bg[i]=bg; b->attrs[i]=0; }
+}
+static int bounds(const hq_buffer *b,int x,int y) { return b && x>=0 && y>=0 && xwidth && yheight; }
+int hq_buffer_cell(const hq_buffer *b,int x,int y,hq_cell *out) {
+ if(!out || !bounds(b,x,y)) return 0;
+ size_t i=(size_t)y*(size_t)b->width+(size_t)x;
+ *out=(hq_cell){b->chars[i],b->fg[i],b->bg[i],b->attrs[i]}; return 1;
+}
+int hq_cell_width(const hq_buffer *b,uint32_t cp) {
+ if(cp==HQ_CONT) return 0;
+ if(cp>=HQ_CLUSTER) return cp-HQ_CLUSTERcluster_count ? b->clusters[cp-HQ_CLUSTER].width:1;
+ return hq_char_width(cp);
+}
+const char *hq_cell_text(const hq_buffer *b,uint32_t cp,char out[5]) {
+ if(cp==HQ_CONT || !cp) return " ";
+ if(cp>=HQ_CLUSTER) return cp-HQ_CLUSTERcluster_count ? b->clusters[cp-HQ_CLUSTER].text:" ";
+ hq_utf8(cp,out); return out;
+}
+const char *hq_buffer_cell_text(const hq_buffer *b,int x,int y,char out[5]) {
+ if(!out || !bounds(b,x,y)) return "";
+ return hq_cell_text(b,b->chars[(size_t)y*(size_t)b->width+(size_t)x],out);
+}
+static void style_at(hq_buffer *b,size_t i,hq_style s) {
+ if(s.mask&HQ_STYLE_FG) b->fg[i]=s.fg;
+ if(s.mask&HQ_STYLE_BG) b->bg[i]=s.bg;
+ if(s.mask&HQ_STYLE_ATTRS) b->attrs[i]=s.attrs&127u;
+}
+static int set(hq_buffer *b,int x,int y,uint32_t cp,hq_style s) {
+ if(!bounds(b,x,y)) return 0;
+ int w=cp>=32 && cp<127 ? 1:hq_cell_width(b,cp);
+ size_t i=(size_t)y*(size_t)b->width+(size_t)x;
+ if(b->chars[i]==HQ_CONT && x>0) b->chars[i-1]=32;
+ b->chars[i]=cp; style_at(b,i,s);
+ if(w==2) {
+ if(x+1width) { b->chars[i+1]=HQ_CONT; style_at(b,i+1,s); }
+ else { b->chars[i]=32; return 1; }
+ }
+ return hq_max(w,1);
+}
+int hq_buffer_set(hq_buffer *b,int x,int y,uint32_t cp,hq_style s) {
+ if(hq_unsafe(cp) || cp==HQ_CONT) cp=32;
+ else if(cp>0x10ffff || (cp>=0xd800 && cp<=0xdfff)) cp=0xfffd;
+ return set(b,x,y,cp,s);
+}
+static uint32_t hash_text(const char *s,size_t n) {
+ uint32_t h=2166136261u;
+ for(size_t i=0;icp,single);
+ if(g->bytes==n && memcmp(single,g->start,n)==0) return g->cp;
+ /* The decoder replaces invalid UTF-8. Never intern its original bytes:
+ * a raw C1 byte could otherwise become a terminal control on output. */
+ if(g->cp==0xfffd && (g->bytes<3 || memcmp(g->start,"\xef\xbf\xbd",3)!=0)) return 0xfffd;
+ if(g->bytes>=sizeof(b->clusters[0].text)) return g->cp;
+ if(!b->cluster_hash) {
+ b->cluster_hash=calloc(HQ_MAX_CLUSTERS*2,sizeof(uint32_t));
+ if(!b->cluster_hash) return g->cp;
+ ++b->allocations;
+ }
+ size_t slot=hash_text(g->start,g->bytes)&(HQ_MAX_CLUSTERS*2-1);
+ while(b->cluster_hash[slot]) {
+ uint32_t id=b->cluster_hash[slot]-1;
+ if(strlen(b->clusters[id].text)==g->bytes && !memcmp(b->clusters[id].text,g->start,g->bytes)) return HQ_CLUSTER+id;
+ slot=(slot+1)&(HQ_MAX_CLUSTERS*2-1);
+ }
+ if(b->cluster_count>=HQ_MAX_CLUSTERS) return g->cp;
+ if(b->cluster_count==b->cluster_capacity) {
+ size_t capacity=b->cluster_capacity ? b->cluster_capacity*2:64;
+ hq_cluster *p=realloc(b->clusters,capacity*sizeof(*p));
+ if(!p) return g->cp;
+ b->clusters=p; b->cluster_capacity=capacity; ++b->allocations;
+ }
+ size_t id=b->cluster_count++;
+ memcpy(b->clusters[id].text,g->start,g->bytes); b->clusters[id].text[g->bytes]=0;
+ b->clusters[id].width=(uint8_t)g->width; b->cluster_hash[slot]=(uint32_t)id+1;
+ return HQ_CLUSTER+(uint32_t)id;
+}
+size_t hq_buffer_write_n(hq_buffer *b,int x,int y,const char *text,size_t n,hq_style s,size_t max) {
+ if(!b || !text || y<0 || y>=b->height) return 0;
+ size_t off=0,used=0; int64_t cx=x; hq_grapheme g;
+ while(hq_next_grapheme(text,n,&off,&g)) {
+ if((size_t)g.width>max-used || cx+g.width>b->width) break;
+ if(cx>=0) set(b,(int)cx,y,intern(b,&g),s);
+ cx+=g.width; used+=(size_t)g.width;
+ }
+ return used;
+}
+size_t hq_buffer_write(hq_buffer *b,int x,int y,const char *s,hq_style style,size_t max) {
+ return s ? hq_buffer_write_n(b,x,y,s,strlen(s),style,max):0;
+}
+static hq_rect clip(const hq_buffer *b,hq_rect r) {
+ int64_t right=(int64_t)r.x+hq_max(r.width,0),bottom=(int64_t)r.y+hq_max(r.height,0);
+ int x=hq_min(hq_max(r.x,0),b->width),y=hq_min(hq_max(r.y,0),b->height);
+ int endx=(int)hq_clamp((double)right,0,b->width),endy=(int)hq_clamp((double)bottom,0,b->height);
+ return (hq_rect){x,y,hq_max(0,endx-x),hq_max(0,endy-y)};
+}
+void hq_buffer_fill(hq_buffer *b,hq_rect r,uint32_t cp,hq_style s) {
+ if(!b) return;
+ r=clip(b,r);
+ for(int y=r.y;ywidth+(size_t)x,s);
+}
+int hq_buffer_copy(hq_buffer *dst,const hq_buffer *src) {
+ if(!dst || !src) return 0;
+ if(dst==src) return 1;
+ /* Retain pools between frames so copying ASCII frames never allocates. */
+ if(src->cluster_count>dst->cluster_capacity) {
+ hq_cluster *p=realloc(dst->clusters,src->cluster_capacity*sizeof(*p));
+ if(!p) return 0;
+ dst->clusters=p; dst->cluster_capacity=src->cluster_capacity; ++dst->allocations;
+ }
+ if(src->cluster_hash && !dst->cluster_hash) {
+ dst->cluster_hash=calloc(HQ_MAX_CLUSTERS*2,sizeof(uint32_t));
+ if(!dst->cluster_hash) return 0;
+ ++dst->allocations;
+ }
+ if(!hq_buffer_resize(dst,src->width,src->height)) return 0;
+ size_t n=(size_t)src->width*(size_t)src->height;
+ if(n) {
+ memcpy(dst->chars,src->chars,n*sizeof(uint32_t)); memcpy(dst->fg,src->fg,n*sizeof(uint32_t));
+ memcpy(dst->bg,src->bg,n*sizeof(uint32_t)); memcpy(dst->attrs,src->attrs,n*sizeof(uint16_t));
+ }
+ dst->cluster_count=src->cluster_count;
+ if(src->cluster_count) memcpy(dst->clusters,src->clusters,src->cluster_count*sizeof(hq_cluster));
+ if(src->cluster_hash) memcpy(dst->cluster_hash,src->cluster_hash,HQ_MAX_CLUSTERS*2*sizeof(uint32_t));
+ else if(dst->cluster_hash) memset(dst->cluster_hash,0,HQ_MAX_CLUSTERS*2*sizeof(uint32_t));
+ return 1;
+}
+size_t hq_buffer_row(const hq_buffer *b,int y,char *out,size_t cap) {
+ size_t used=0;
+ if(out && cap) out[0]=0;
+ if(!b || y<0 || y>=b->height) return 0;
+ for(int x=0;xwidth;++x) {
+ uint32_t cp=b->chars[(size_t)y*(size_t)b->width+(size_t)x];
+ if(cp==HQ_CONT) continue;
+ char scratch[5]; const char *s=hq_cell_text(b,cp,scratch); size_t n=strlen(s);
+ if(out && used
+
+hq_color hq_rgb(int r,int g,int b) {
+ return 0x1000000u | ((uint32_t)r & 255u)<<16 | ((uint32_t)g & 255u)<<8 | ((uint32_t)b & 255u);
+}
+hq_color hq_indexed(int i) { return 0x2000000u | ((uint32_t)i & 255u); }
+static int digit(char c) {
+ if(c>='0' && c<='9') return c-'0';
+ if(c>='a' && c<='f') return c-'a'+10;
+ if(c>='A' && c<='F') return c-'A'+10;
+ return -1;
+}
+hq_color hq_hex(const char *s) {
+ if(!s) return hq_rgb(0,0,0);
+ while(isspace((unsigned char)*s)) ++s;
+ if(*s=='#') ++s;
+ size_t n=strlen(s);
+ while(n && isspace((unsigned char)s[n-1])) --n;
+ uint32_t v=0;
+ for(size_t i=0;i>shift)&255u); }
+static int nearest(int v) {
+ int best=0, dist=INT_MAX;
+ for(int i=0;i<6;++i) if(abs(cube[i]-v)=232) { int v=8+(i-232)*10; return hq_rgb(v,v,v); }
+ i-=16; return hq_rgb(cube[i/36%6],cube[i/6%6],cube[i%6]);
+}
+int hq_to256(hq_color c) {
+ if(c&0x2000000u) return (int)(c&255u);
+ int r=channel(c,16),g=channel(c,8),b=channel(c,0);
+ if(abs(r-g)<8 && abs(g-b)<8) {
+ if(r<8) return 16;
+ if(r>248) return 231;
+ return 232+(int)floor((r-8)/247.0*24+0.5);
+ }
+ return 16+36*nearest(r)+6*nearest(g)+nearest(b);
+}
+int hq_to16(hq_color c) {
+ if(c&0x2000000u) {
+ int i=(int)(c&255u); if(i<16) return i; c=hq_from256(i);
+ }
+ int best=7,dist=INT_MAX;
+ for(int i=0;i<16;++i) {
+ int r=channel(c,16)-base[i][0],g=channel(c,8)-base[i][1],b=channel(c,0)-base[i][2];
+ int d=r*r+g*g+b*b;
+ if(d=n-1) i=n-2;
+ return hq_mix(s[i],s[i+1],p-(double)i);
+}
diff --git a/ports/nim/src/hqtui/engine/c/src/diff.c b/ports/nim/src/hqtui/engine/c/src/diff.c
new file mode 100644
index 0000000..aae045d
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/src/diff.c
@@ -0,0 +1,126 @@
+#include "internal.h"
+#include
+#include
+struct hq_encoder {
+ char *data;
+ size_t length,capacity,allocations;
+ int colors,monochrome,x,y,known,failed,force;
+ hq_color fg,bg;
+ uint16_t attrs;
+};
+static void append(hq_encoder *e,const char *s,size_t n) {
+ if(e->failed) return;
+ if(n>SIZE_MAX-e->length-1) { e->failed=1; return; }
+ size_t required=e->length+n+1;
+ if(required>e->capacity) {
+ size_t cap=e->capacity ? e->capacity:4096;
+ while(capSIZE_MAX/2) { cap=required; break; } cap*=2; }
+ char *p=realloc(e->data,cap);
+ if(!p) { e->failed=1; return; }
+ e->data=p; e->capacity=cap; ++e->allocations;
+ }
+ memcpy(e->data+e->length,s,n); e->length+=n; e->data[e->length]=0;
+}
+static void text(hq_encoder *e,const char *s) { append(e,s,strlen(s)); }
+static void fmt(hq_encoder *e,const char *format,...) {
+ char s[96]; va_list args; va_start(args,format);
+ int n=vsnprintf(s,sizeof(s),format,args); va_end(args);
+ if(n<0 || (size_t)n>=sizeof(s)) { e->failed=1; return; }
+ append(e,s,(size_t)n);
+}
+hq_encoder *hq_encoder_create(int colors,int mono) {
+ if(colors!=0 && colors!=16 && colors!=256 && colors!=16777216) return NULL;
+ hq_encoder *e=calloc(1,sizeof(*e));
+ if(e) { e->colors=colors; e->monochrome=!!mono; e->x=e->y=-1; e->allocations=1; }
+ return e;
+}
+void hq_encoder_destroy(hq_encoder *e) { if(e) { free(e->data); free(e); } }
+void hq_encoder_invalidate(hq_encoder *e) { if(e) e->force=1; }
+size_t hq_encoder_allocations(const hq_encoder *e) { return e ? e->allocations:0; }
+static void color(hq_encoder *e,hq_color c,int background) {
+ if(!e->colors) return;
+ if(!c) { text(e,background ? "\033[49m":"\033[39m"); return; }
+ if(e->monochrome) c=hq_grayscale(c);
+ int code=background ? 48:38;
+ if(e->colors==16777216) fmt(e,"\033[%d;2;%u;%u;%um",code,(c>>16)&255u,(c>>8)&255u,c&255u);
+ else if(e->colors==256) fmt(e,"\033[%d;5;%dm",code,hq_to256(c));
+ else { int idx=hq_to16(c); fmt(e,"\033[%dm",(background ? 40:30)+(idx>=8 ? 60:0)+idx%8); }
+}
+static void style(hq_encoder *e,hq_color fg,hq_color bg,uint16_t attrs) {
+ if(e->fg==fg && e->bg==bg && e->attrs==attrs) return;
+ if(e->attrs & (uint16_t)~attrs) { text(e,"\033[0m"); e->attrs=0; e->fg=e->bg=0; }
+ uint16_t added=attrs & (uint16_t)~e->attrs;
+ if(added) {
+ static const int codes[7]={1,2,3,4,5,7,9}; int first=1;
+ text(e,"\033[");
+ for(int i=0;i<7;++i) if(added&(1u<attrs=attrs;
+ }
+ if(e->fg!=fg) { color(e,fg,0); e->fg=fg; }
+ if(e->bg!=bg) { color(e,bg,1); e->bg=bg; }
+}
+static void move(hq_encoder *e,int x,int y) {
+ if(e->known && e->y==y) {
+ if(e->x==x) return;
+ if(x>e->x && x-e->x<=3) fmt(e,"\033[%dC",x-e->x);
+ else if(!x) text(e,"\r");
+ else fmt(e,"\033[%dG",x+1);
+ } else fmt(e,"\033[%d;%dH",y+1,x+1);
+ e->x=x; e->y=y; e->known=1;
+}
+static int differs(const hq_buffer *a,const hq_buffer *b,size_t i,int same) {
+ if(!same) return 1;
+ if(a->fg[i]!=b->fg[i] || a->bg[i]!=b->bg[i] || a->attrs[i]!=b->attrs[i]) return 1;
+ uint32_t x=a->chars[i],y=b->chars[i];
+ if(x>=HQ_CLUSTER && x!=HQ_CONT && y>=HQ_CLUSTER && y!=HQ_CONT) {
+ char t[5],u[5]; return strcmp(hq_cell_text(a,x,t),hq_cell_text(b,y,u))!=0;
+ }
+ return x!=y;
+}
+int hq_encode(hq_encoder *e,const hq_buffer *prev,const hq_buffer *next,int full,hq_encoded *out) {
+ if(!e || !next || !out) return 0;
+ *out=(hq_encoded){"",0,0,0}; e->length=0; e->failed=0;
+ if(e->data) e->data[0]=0;
+ int same=prev && prev->width==next->width && prev->height==next->height;
+ int repaint=full || !same || e->force;
+ if(repaint) { e->known=0; e->x=e->y=-1; e->fg=e->bg=0; e->attrs=0; e->force=0; text(e,"\033[0m"); }
+ int w=next->width,h=next->height;
+ for(int y=0;y0 && !repaint && same && !prev->cluster_count && !next->cluster_count &&
+ !memcmp(prev->chars+row,next->chars+row,(size_t)w*sizeof(uint32_t)) &&
+ !memcmp(prev->fg+row,next->fg+row,(size_t)w*sizeof(uint32_t)) &&
+ !memcmp(prev->bg+row,next->bg+row,(size_t)w*sizeof(uint32_t)) &&
+ !memcmp(prev->attrs+row,next->attrs+row,(size_t)w*sizeof(uint16_t))) continue;
+ while(x0 && next->chars[row+(size_t)start]==HQ_CONT) --start;
+ int end=start,clean=0;
+ for(int probe=start;probe5) break;
+ }
+ move(e,start,y);
+ for(int cx=start;cx<=end;++cx) {
+ size_t i=row+(size_t)cx; uint32_t cp=next->chars[i];
+ if(cp==HQ_CONT) continue;
+ style(e,next->fg[i],next->bg[i],next->attrs[i]);
+ if(cp>=32 && cp<127) {
+ char ascii=(char)cp; append(e,&ascii,1); ++e->x;
+ } else {
+ char scratch[5]; text(e,hq_cell_text(next,cp,scratch));
+ e->x+=hq_max(1,hq_cell_width(next,cp));
+ }
+ if(differs(prev,next,i,same)) ++out->changed_cells;
+ }
+ if(e->x>=w) e->known=0;
+ dirty=1; x=end+1;
+ }
+ if(dirty) ++out->dirty_rows;
+ }
+ if(e->failed) { hq_encoder_invalidate(e); *out=(hq_encoded){"",0,0,0}; return 0; }
+ out->data=e->data ? e->data:""; out->length=e->length; return 1;
+}
diff --git a/ports/nim/src/hqtui/engine/c/src/internal.h b/ports/nim/src/hqtui/engine/c/src/internal.h
new file mode 100644
index 0000000..86e28ab
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/src/internal.h
@@ -0,0 +1,30 @@
+#ifndef HQ_INTERNAL_H
+#define HQ_INTERNAL_H
+#include "hqtui.h"
+#include
+#include
+#include
+#include
+#define HQ_CONT UINT32_MAX
+#define HQ_CLUSTER 0x110000u
+#define HQ_MAX_CLUSTERS 32768u
+typedef struct { char text[69]; uint8_t width; } hq_cluster;
+struct hq_buffer {
+ int width, height;
+ size_t capacity, allocations;
+ uint32_t *chars, *fg, *bg;
+ uint16_t *attrs;
+ hq_cluster *clusters;
+ uint32_t *cluster_hash;
+ size_t cluster_count, cluster_capacity;
+};
+typedef struct { const char *start; size_t bytes; uint32_t cp; int width; } hq_grapheme;
+int hq_next_grapheme(const char *s, size_t n, size_t *offset, hq_grapheme *g);
+size_t hq_utf8(uint32_t cp, char out[5]);
+int hq_unsafe(uint32_t cp);
+const char *hq_cell_text(const hq_buffer *b, uint32_t cp, char out[5]);
+int hq_cell_width(const hq_buffer *b, uint32_t cp);
+static inline double hq_clamp(double v, double a, double b) { return fmax(a,fmin(b,v)); }
+static inline int hq_min(int a, int b) { return a < b ? a : b; }
+static inline int hq_max(int a, int b) { return a > b ? a : b; }
+#endif
diff --git a/ports/nim/src/hqtui/engine/c/src/layout.c b/ports/nim/src/hqtui/engine/c/src/layout.c
new file mode 100644
index 0000000..df8bb00
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/src/layout.c
@@ -0,0 +1,163 @@
+#include "internal.h"
+typedef struct { double value,fr,min,max; } resolved;
+int hq_solve(int total,const hq_constraint *items,size_t n,int gap,int *out) {
+ if(gap<0) return 0;
+ if(n<2) return hq_solve_gaps(total,items,n,NULL,out);
+ int local[64];
+ int *gaps=n-1<=64 ? local:malloc((n-1)*sizeof(*gaps));
+ if(!gaps) return 0;
+ for(size_t i=0;i+14096 || (n && (!items || !out)) || (n>1 && !gaps)) return 0;
+ if(!n) return 1;
+ resolved local[64];
+ resolved *p=n<=64 ? local:malloc(n*sizeof(*p));
+ if(!p) return 0;
+ int64_t gap_total=0;
+ for(size_t i=0;i+10 ? a:0),used=0,fr=0;
+ for(size_t i=0;i0 ? v:1; break;
+ case HQ_FILL: p[i].fr=1; break;
+ case HQ_AUTO: p[i].value=hq_max(0,c.intrinsic); break;
+ default: if(p!=local) free(p); return 0;
+ }
+ if(p[i].fr>0) { fr+=p[i].fr; out[i]=-1; }
+ else { double val=hq_clamp(floor(p[i].value+.5),p[i].min,fmin(p[i].max,available));
+ out[i]=(int)hq_clamp(val,0,INT_MAX); used+=out[i]; }
+ }
+ double pool=fmax(0,available-used),remaining=fr;
+ if(fr>0) {
+ int changed=1;
+ while(changed) {
+ changed=0;
+ for(size_t i=0;i0 ? pool*p[i].fr/remaining:0;
+ double clamped=hq_clamp(share,p[i].min,p[i].max);
+ if(clamped!=share) {
+ out[i]=(int)hq_clamp(floor(clamped+.5),0,INT_MAX);
+ pool-=out[i]; remaining-=p[i].fr; changed=1;
+ }
+ }
+ }
+ size_t last=n;
+ for(size_t i=0;i0 ? pool*p[i].fr/remaining:0);
+ out[i]=(int)hq_clamp(v,0,INT_MAX); assigned+=out[i];
+ }
+ }
+ int64_t sum=0;
+ for(size_t i=0;iavail;++pass) for(size_t i=n;i-->0 && sum>avail;) {
+ int64_t spare=(int64_t)out[i]-(pass ? 0:(int64_t)p[i].min);
+ int64_t shrink=spare0) { out[i]-=(int)shrink; sum-=shrink; }
+ }
+ if(p!=local) free(p);
+ return 1;
+}
+int hq_stack(hq_rect r,const hq_constraint *items,size_t n,int horizontal,int gap,hq_rect *out) {
+ if(gap<0) return 0;
+ if(n<2) return hq_stack_gaps(r,items,n,horizontal,NULL,out);
+ int local[64];
+ int *gaps=n-1<=64 ? local:malloc((n-1)*sizeof(*gaps));
+ if(!gaps) return 0;
+ for(size_t i=0;i+11 ? fi*slack/(fn-1.0):0.0;
+ case HQ_JUSTIFY_SPACE_EVENLY: return (fi+1.0)*slack/(fn+1.0);
+ case HQ_JUSTIFY_SPACE_AROUND: return (fi+0.5)*slack/fn;
+ default: return 0.0;
+ }
+}
+
+/* Cells are whole, and rounding each gap on its own loses one here and gains
+ * one there. Rounding the cumulative offset and taking differences means the
+ * parts always add up to exactly the slack. */
+static int hq_at(size_t i,int slack,size_t n,hq_justify j) {
+ return (int)floor(hq_before(i,(double)slack,n,j)+0.5);
+}
+
+int hq_distribute(int slack,size_t count,hq_justify justify,int *lead,int *seams) {
+ if(!lead || count>4096 || (count>1 && !seams)) return 0;
+ for(size_t i=0;i+10 ? d:0;
+ }
+ *lead=hq_at(0,slack,count,justify);
+ return 1;
+}
+
+int hq_stack_justified(hq_rect r,const hq_constraint *items,size_t n,int horizontal,const int *gaps,hq_justify justify,hq_rect *out) {
+ if(n>4096 || (n && (!out || !items)) || (n>1 && !gaps) || r.width<0 || r.height<0) return 0;
+ if(!n) return 1;
+ int local[64];
+ int *sizes=n<=64 ? local:malloc(n*sizeof(*sizes));
+ if(!sizes) return 0;
+ int axis=horizontal ? r.width:r.height;
+ if(!hq_solve_gaps(axis,items,n,gaps,sizes)) { if(sizes!=local) free(sizes); return 0; }
+
+ int64_t used=0;
+ for(size_t i=0;i0 ? (int)spare:0;
+
+ int seam_local[64];
+ int *extra=n-1<=64 ? seam_local:malloc((n?n-1:1)*sizeof(*extra));
+ if(!extra) { if(sizes!=local) free(sizes); return 0; }
+ int lead=0;
+ if(!hq_distribute(slack,n,justify,&lead,extra)) {
+ if(sizes!=local) free(sizes);
+ if(extra!=seam_local) free(extra);
+ return 0;
+ }
+
+ int64_t offset=(horizontal ? r.x:r.y)+lead;
+ for(size_t i=0;iINT_MAX) {
+ if(sizes!=local) free(sizes);
+ if(extra!=seam_local) free(extra);
+ return 0;
+ }
+ out[i]=horizontal ? (hq_rect){(int)offset,r.y,sizes[i],r.height} : (hq_rect){r.x,(int)offset,r.width,sizes[i]};
+ offset+=(int64_t)sizes[i]+(i+1
+static int integer(int64_t n) { return nINT_MAX ? INT_MAX:(int)n; }
+hq_rect hq_intersect(hq_rect a,hq_rect b) {
+ int x=hq_max(a.x,b.x),y=hq_max(a.y,b.y);
+ int64_t ax=(int64_t)a.x+hq_max(0,a.width),bx=(int64_t)b.x+hq_max(0,b.width);
+ int64_t ay=(int64_t)a.y+hq_max(0,a.height),by=(int64_t)b.y+hq_max(0,b.height);
+ int64_t w=(ax=s.clip.x && x+width<=(int64_t)s.clip.x+s.clip.width &&
+ y>=s.clip.y && y<(int64_t)s.clip.y+s.clip.height;
+}
+void hq_surface_set(hq_surface s,int x,int y,uint32_t cp,hq_style style) {
+ int64_t ax=(int64_t)s.rect.x+x,ay=(int64_t)s.rect.y+y;
+ int width=hq_max(1,hq_char_width(cp));
+ if(visible(s,ax,ay,width) && ax>=INT_MIN && ax<=INT_MAX && ay>=INT_MIN && ay<=INT_MAX)
+ hq_buffer_set(s.buffer,(int)ax,(int)ay,cp,style);
+}
+void hq_surface_fill(hq_surface s,uint32_t cp,hq_style style) {
+ hq_rect r=hq_intersect(s.clip,(hq_rect){0,0,hq_buffer_width(s.buffer),hq_buffer_height(s.buffer)});
+ int w=hq_max(1,hq_char_width(cp));
+ for(int y=r.y;y=(int64_t)s.clip.y+s.clip.height) return 0;
+ int room=hq_min(32768,hq_max(0,integer((int64_t)s.rect.width-x)));
+ size_t limit=(size_t)(o.has_max_columns ? hq_min(room,hq_max(0,o.max_columns)):room);
+ if(!limit) return 0;
+ size_t bytes=strlen(text),width=hq_text_width_n(text,bytes);
+ int ellipsis=!o.no_ellipsis && width>limit;
+ size_t content=width,off=0;
+ hq_grapheme g;
+ if(ellipsis) {
+ content=0;
+ while(hq_next_grapheme(text,bytes,&off,&g)) {
+ if(content+(size_t)g.width>limit-1) break;
+ content+=(size_t)g.width;
+ }
+ ++content;
+ }
+ size_t padding=contentcap) break;
+ if(visible(s,cx,ay,g.width)) hq_buffer_write_n(s.buffer,(int)cx,(int)ay,g.start,g.bytes,o.style,(size_t)g.width);
+ cx+=g.width; used+=(size_t)g.width;
+ }
+ if(ellipsis) {
+ if(visible(s,cx,ay,1)) hq_buffer_set(s.buffer,(int)cx,(int)ay,0x2026,o.style);
+ ++used; ++cx;
+ }
+ if(o.align!=HQ_LEFT) for(size_t i=left;i=6) return 0;
+ for(int i=0;i<11;++i) if(hq_part_bits[i]==bits) return hq_borders[border][i];
+ return 0;
+}
+
+/* Writes a border glyph, merging it with whatever border is already there.
+ *
+ * Only border glyphs merge. Anything else in the cell is overwritten, which
+ * keeps a panel drawn over a chart looking like a panel rather than growing
+ * junctions out of the data. */
+static void hq_put_border(hq_surface s,int border,int collapse,int x,int y,uint32_t cp,hq_style st) {
+ if(collapse) {
+ hq_cell cell;
+ if(hq_buffer_cell(s.buffer,s.rect.x+x,s.rect.y+y,&cell)) {
+ int before=hq_border_bits(cell.value),after=hq_border_bits(cp);
+ if(before>=0 && after>=0 && before!=after) {
+ uint32_t merged=hq_border_glyph(border,before|after);
+ if(merged) cp=merged;
+ }
+ }
+ }
+ hq_surface_set(s,x,y,cp,st);
+}
+
+/* The glyph for a cell where two edges meet, given which of them are drawn. A
+ * single edge has no glyph of its own, so the plain rule stands in: that cell
+ * is part of a run, not a corner. */
+static uint32_t hq_side_glyph(int border,int bits) {
+ if(!bits) return 0;
+ uint32_t g=hq_border_glyph(border,bits);
+ if(g) return g;
+ return bits&(HQ_EDGE_LEFT|HQ_EDGE_RIGHT) ? hq_borders[border][4]:hq_borders[border][5];
+}
+
+hq_surface hq_surface_box(hq_surface s,hq_box_options o) {
+ const hq_theme *theme=s.theme ? s.theme:hq_theme_at(0);
+ /* Zero means all four, so a caller that predates this gets what it always
+ * got. HQ_SIDES_NONE is an explicit "no rule at all". */
+ int sides=o.sides==0 ? HQ_SIDES_ALL:(o.sides&HQ_SIDES_NONE ? 0:o.sides&HQ_SIDES_ALL);
+ int s_top=(sides&HQ_SIDE_TOP)!=0, s_right=(sides&HQ_SIDE_RIGHT)!=0;
+ int s_bottom=(sides&HQ_SIDE_BOTTOM)!=0, s_left=(sides&HQ_SIDE_LEFT)!=0;
+ int draws_border=o.border!=HQ_NO_BORDER && sides && s.rect.width>=2 && s.rect.height>=1;
+
+ if(o.has_background && !o.no_fill) {
+ hq_style bg={0,o.background,0,HQ_STYLE_BG};
+ if(draws_border && o.collapse) {
+ /* The border ring is left to the border drawing below, which merges
+ * with whatever a neighbour already put in the shared column.
+ * Filling it here first would erase that border, and the merge
+ * would then find a blank cell and simply overwrite it -- which is
+ * why a panel with a background collapsed its seams into a corner
+ * rather than a junction. Every cell skipped here is painted by the
+ * border, in the same bg. */
+ hq_surface_fill(hq_surface_region(s,hq_inset(s.rect,s_top,s_right,
+ (s.rect.height>1 && s_bottom)?1:0,s_left)),32,bg);
+ } else {
+ hq_surface_fill(s,32,bg);
+ }
+ }
+
+ if(o.border==HQ_NO_BORDER || !sides) return s;
+ /* The interior follows the sides actually drawn, so a top-only box costs
+ * one row rather than two. */
+ hq_surface inner=hq_surface_region(s,hq_inset(s.rect,s_top,s_right,s_bottom,s_left));
+ if(s.rect.width<2 || s.rect.height<1) return inner;
+ int border=o.border>=0 && o.border<6 ? o.border:0;
+ const uint32_t *c=hq_borders[border];
+ int w=s.rect.width,h=s.rect.height;
+ hq_style bs=default_style(o.border_style,theme->border,o);
+ /* With collapsing on a border glyph landing on another becomes the union
+ * of the two; without it this is the plain write it always was, so a
+ * screen that never asks for collapsing renders byte for byte as before. */
+ /* A corner belongs to the two sides that meet there, so it exists only when
+ * both are drawn; where one is, the rule runs straight through the cell the
+ * corner would have occupied. */
+ #define HQ_CORNER(a,abit,b,bbit) hq_side_glyph(border,((a)?(abit):0)|((b)?(bbit):0))
+ uint32_t tl=HQ_CORNER(s_top,HQ_EDGE_RIGHT,s_left,HQ_EDGE_DOWN);
+ uint32_t tr=HQ_CORNER(s_top,HQ_EDGE_LEFT,s_right,HQ_EDGE_DOWN);
+
+ if(s_top) for(int x=1;x1) {
+ uint32_t bl=HQ_CORNER(s_bottom,HQ_EDGE_RIGHT,s_left,HQ_EDGE_UP);
+ uint32_t br=HQ_CORNER(s_bottom,HQ_EDGE_LEFT,s_right,HQ_EDGE_UP);
+ if(s_bottom) for(int x=1;x=(size_t)w) sw=0; }
+ if(o.title && s_top) {
+ label(title,o.title);
+ int limit=sw ? w-1-(int)sw:w-2,room=hq_max(0,limit-2);
+ size_t tw=hq_text_width(title);
+ if(tw>(size_t)room) {
+ /* Measure the actual truncated width (wide glyphs can leave a gap). */
+ size_t off=0,used=0; hq_grapheme g;
+ while(hq_next_grapheme(title,strlen(title),&off,&g)) {
+ if(room==0 || used+(size_t)g.width>(size_t)room-1) break;
+ used+=(size_t)g.width;
+ }
+ tw=room>0 ? used+1:0;
+ }
+ int tx=o.title_align==HQ_RIGHT ? hq_max(2,limit-(int)tw):o.title_align==HQ_CENTER ? hq_max(2,hq_min(limit-(int)tw,(w-(int)tw)/2)):2;
+ hq_text_options t={0}; t.style=default_style(o.title_style,theme->title,o);
+ t.style.attrs=HQ_BOLD; t.style.mask|=HQ_STYLE_ATTRS; t.has_max_columns=1; t.max_columns=room;
+ hq_surface_text(s,tx,0,title,t);
+ }
+ if(sw) {
+ hq_text_options t={0}; t.style=default_style(o.subtitle_style,theme->muted,o);
+ hq_surface_text(s,w-2-(int)sw,0,sub,t);
+ }
+ if(o.footer && s_bottom && h>2) {
+ label(foot,o.footer);
+ if(hq_text_width(foot)+4<(size_t)w) {
+ hq_text_options t={0}; t.style=default_style(o.footer_style,theme->muted,o);
+ hq_surface_text(s,2,h-1,foot,t);
+ }
+ }
+ return inner;
+}
diff --git a/ports/nim/src/hqtui/engine/c/src/theme.c b/ports/nim/src/hqtui/engine/c/src/theme.c
new file mode 100644
index 0000000..f4d14ed
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/src/theme.c
@@ -0,0 +1,32 @@
+/* Palette data copied from the TypeScript theme conformance fixture.
+ * This is theme metadata, not prerendered UI. */
+#include "internal.h"
+static const hq_theme themes[]={
+ {"dark",1,17106698u,17436180u,29806811u,22702973u,22587135u,29201401u,22467805u,23068551u,33544031u,33516395u,22467805u,19148864u,22467805u,25092863u,18692690u,31912703u,22467805u,
+ {22587135u,23068551u,33520070u,33544031u,22467805u,33531479u}, {23068551u,27852640u,33544031u,33531479u,33516395u},6,5},
+ {"dracula",1,18422305u,18948652u,33093874u,23229092u,29201401u,33520070u,25946621u,22084219u,32635532u,33510741u,25946621u,21251930u,29201401u,33520070u,21251930u,33093874u,22467805u,
+ {29201401u,22084219u,33520070u,32635532u,25946621u,33536108u}, {22084219u,32635532u,33536108u,33510741u},6,4},
+ {"nord",1,19805248u,20134730u,31844848u,24873121u,25739472u,28610221u,26197179u,27508364u,32230283u,29319530u,25272769u,21187678u,25739472u,26197179u,21187678u,32305140u,22467805u,
+ {25739472u,27508364u,28610221u,32230283u,25272769u,30443376u}, {27508364u,32230283u,30443376u,29319530u},6,4},
+ {"tokyo-night",1,18488102u,18817845u,29412085u,22437769u,24814327u,29072119u,25022463u,27184746u,31502184u,32994958u,25022463u,19871049u,24814327u,25022463u,19412055u,29412085u,22467805u,
+ {24814327u,27184746u,29072119u,31502184u,25022463u,33529444u}, {27184746u,31502184u,33529444u,32994958u},6,4},
+ {"gruvbox",1,18685985u,19408936u,32234418u,26379124u,25404824u,30639771u,26132604u,28883750u,33209647u,33245492u,25404824u,20723766u,33209647u,33209647u,20723766u,33288647u,22467805u,
+ {25404824u,28883750u,30639771u,33209647u,26132604u,33456153u}, {28883750u,33209647u,33456153u,33245492u},6,4},
+ {"matrix",1,16777216u,16910850u,27131805u,19884847u,16842561u,16828499u,24969084u,16842561u,30736128u,33504048u,16836016u,17973279u,16842561u,16842561u,17641236u,30015433u,22467805u,
+ {16842561u,16828499u,24969084u,16836016u,30736128u,19897924u}, {17791534u,16828499u,16842561u,30736128u},6,4},
+ {"monochrome",1,16777216u,17500939u,30462160u,24014446u,33554431u,29409472u,31514848u,32041192u,28883128u,33554431u,27304096u,20593210u,30462160u,33554431u,19935280u,33554431u,22467805u,
+ {33554431u,29935816u,26251408u,23619688u,28356784u,25198720u}, {22567000u,26251408u,29935816u,33554431u},6,4},
+ {"high-contrast",1,16777216u,16777216u,33554431u,29409472u,16842751u,33489151u,33554176u,16842496u,33554176u,33488896u,16842751u,33554431u,33554176u,33554431u,33554431u,16777216u,22467805u,
+ {16842751u,16842496u,33489151u,33554176u,33554431u,33521664u}, {16842496u,33554176u,33521664u,33488896u},6,4},
+ {"light",0,33291517u,33554431u,18621744u,23820940u,17523408u,24918765u,17724560u,17992255u,27814400u,29763624u,17724560u,30661604u,17523408u,17513848u,30861051u,17507653u,17523408u,
+ {17523408u,17992255u,27473034u,27814400u,17724560u,29507852u}, {17992255u,24814336u,27814400u,29507852u,29763624u},6,5}
+};
+size_t hq_theme_count(void) { return sizeof(themes)/sizeof(themes[0]); }
+const hq_theme *hq_theme_at(size_t i) { return iheat,t->heat_count,value):0; }
+hq_color hq_series(const hq_theme *t,size_t i) { return t && t->graph_count ? t->graph[i%t->graph_count]:0; }
diff --git a/ports/nim/src/hqtui/engine/c/src/unicode.c b/ports/nim/src/hqtui/engine/c/src/unicode.c
new file mode 100644
index 0000000..161ce6b
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/c/src/unicode.c
@@ -0,0 +1,132 @@
+#include "internal.h"
+typedef struct { uint32_t lo,hi; } range;
+static const range zero[]={
+ {0x0300,0x036f},{0x0483,0x0489},{0x0591,0x05bd},{0x0610,0x061a},
+ {0x064b,0x065f},{0x0670,0x0670},{0x06d6,0x06dc},{0x0730,0x074a},
+ {0x07a6,0x07b0},{0x0816,0x0819},{0x08e3,0x0903},{0x093a,0x093c},
+ {0x0951,0x0957},{0x0e31,0x0e31},{0x0e34,0x0e3a},{0x0eb1,0x0eb1},
+ {0x1ab0,0x1aff},{0x1dc0,0x1dff},{0x200b,0x200f},{0x2028,0x202e},
+ {0x2060,0x2064},{0x2066,0x2069},{0x20d0,0x20f0},{0xfe00,0xfe0f},
+ {0xfe20,0xfe2f},{0xfeff,0xfeff},{0xe0100,0xe01ef}
+};
+static const range wide[]={
+ {0x1100,0x115f},{0x2e80,0x303e},{0x3041,0x33ff},{0x3400,0x4dbf},
+ {0x4e00,0x9fff},{0xa000,0xa4cf},{0xa960,0xa97f},{0xac00,0xd7a3},
+ {0xf900,0xfaff},{0xfe10,0xfe19},{0xfe30,0xfe6f},{0xff00,0xff60},
+ {0xffe0,0xffe6},{0x1f004,0x1f004},{0x1f0cf,0x1f0cf},{0x1f18e,0x1f18e},
+ {0x1f191,0x1f19a},{0x1f200,0x1f320},{0x1f32d,0x1f335},{0x1f337,0x1f37c},
+ {0x1f37e,0x1f393},{0x1f3a0,0x1f3ca},{0x1f3cf,0x1f3d3},{0x1f3e0,0x1f3f0},
+ {0x1f3f4,0x1f3f4},{0x1f3f8,0x1f43e},{0x1f440,0x1f440},{0x1f442,0x1f4fc},
+ {0x1f4ff,0x1f53d},{0x1f54b,0x1f54e},{0x1f550,0x1f567},{0x1f57a,0x1f57a},
+ {0x1f595,0x1f596},{0x1f5a4,0x1f5a4},{0x1f5fb,0x1f64f},{0x1f680,0x1f6c5},
+ {0x1f6cc,0x1f6cc},{0x1f6d0,0x1f6d2},{0x1f6eb,0x1f6ec},{0x1f910,0x1f9ff},
+ {0x20000,0x2fffd},{0x30000,0x3fffd}
+};
+static const range pictographic[]={
+ {0xa9,0xa9},{0xae,0xae},{0x203c,0x203c},{0x2049,0x2049},{0x2122,0x2122},
+ {0x2139,0x2139},{0x2194,0x21aa},{0x231a,0x23fa},{0x24c2,0x24c2},
+ {0x25aa,0x25fe},{0x2600,0x27bf},{0x2934,0x2935},{0x2b00,0x2bff},
+ {0x3030,0x3030},{0x303d,0x303d},{0x3297,0x3299},{0x1f000,0x1faff},{0x1fc00,0x1fffd}
+};
+static int in_ranges(uint32_t cp,const range *r,size_t n) {
+ size_t lo=0,hi=n;
+ while(lor[mid].hi) lo=mid+1; else return 1;
+ }
+ return 0;
+}
+#define IN(cp,r) in_ranges(cp,r,sizeof(r)/sizeof(r[0]))
+int hq_unsafe(uint32_t cp) {
+ return cp<32 || (cp>=127 && cp<=159) || (cp>=0x202a && cp<=0x202e) || (cp>=0x2066 && cp<=0x2069);
+}
+int hq_char_width(uint32_t cp) {
+ if(cp<32 || (cp>=127 && cp<160)) return 0;
+ if(cp<0x300) return 1;
+ if(IN(cp,zero)) return 0;
+ return IN(cp,wide) ? 2:1;
+}
+/* Invalid encodings consume one byte, preventing reads past truncated input. */
+static uint32_t decode(const char *s,size_t n,size_t *used) {
+ *used=1;
+ unsigned char c=(unsigned char)s[0];
+ if(c<128) return c;
+ size_t len=c>=0xc2 && c<=0xdf ? 2 : c>=0xe0 && c<=0xef ? 3 : c>=0xf0 && c<=0xf4 ? 4:0;
+ if(!len || n=0xd800 && cp<=0xdfff) || cp>0x10ffff) return 0xfffd;
+ *used=len; return cp;
+}
+size_t hq_utf8(uint32_t cp,char out[5]) {
+ if(cp>0x10ffff || (cp>=0xd800 && cp<=0xdfff)) cp=0xfffd;
+ size_t n;
+ if(cp<128) { out[0]=(char)cp; n=1; }
+ else if(cp<2048) { out[0]=(char)(0xc0|(cp>>6)); out[1]=(char)(0x80|(cp&63)); n=2; }
+ else if(cp<65536) { out[0]=(char)(0xe0|(cp>>12)); out[1]=(char)(0x80|(cp>>6&63)); out[2]=(char)(0x80|(cp&63)); n=3; }
+ else { out[0]=(char)(0xf0|(cp>>18)); out[1]=(char)(0x80|(cp>>12&63)); out[2]=(char)(0x80|(cp>>6&63)); out[3]=(char)(0x80|(cp&63)); n=4; }
+ out[n]=0; return n;
+}
+static int is_regional(uint32_t cp) { return cp>=0x1f1e6 && cp<=0x1f1ff; }
+static int is_tone(uint32_t cp) { return cp>=0x1f3fb && cp<=0x1f3ff; }
+static int is_tag(uint32_t cp) { return cp>=0xe0020 && cp<=0xe007f; }
+static int is_keycap_base(uint32_t cp) { return (cp>='0' && cp<='9') || cp=='#' || cp=='*'; }
+/* Columns a multi-codepoint cluster occupies. Emoji presentation makes it two
+ even when its base alone is one: a flag (two regional indicators), a keycap,
+ and a text-default pictograph with U+FE0F or a skin tone. */
+static int cluster_width(const char *s,size_t n,uint32_t first,int base) {
+ if(base==2) return 2;
+ size_t off,u; decode(s,n,&off);
+ if(is_regional(first)) return off=n) break;
+ size_t ab; uint32_t after=decode(s+start+bytes+nb,n-start-bytes-nb,&ab);
+ if(!IN(cp,pictographic) || !IN(after,pictographic) || parts+2>16) break;
+ bytes+=nb+ab; parts+=2; continue;
+ }
+ /* Emoji that are one picture but several codepoints: a skin tone
+ after a person or hand, the second half of a flag, and the tags
+ that spell a subdivision flag. Each belongs to the cell before it. */
+ if((IN(cp,pictographic) && (is_tone(next) || is_tag(next))) ||
+ (is_regional(cp) && is_regional(next) && parts==1)) {
+ bytes+=nb; ++parts; continue;
+ }
+ if(hq_char_width(next)!=0 || hq_unsafe(next)) break;
+ bytes+=nb; ++parts;
+ }
+ *offset=start+bytes;
+ if(!width) continue;
+ if(parts>1) width=cluster_width(s+start,bytes,cp,width);
+ g->start=s+start; g->bytes=bytes; g->cp=cp; g->width=width;
+ return 1;
+ }
+ return 0;
+}
+size_t hq_text_width_n(const char *s,size_t n) {
+ if(!s) return 0;
+ size_t off=0,w=0; hq_grapheme g;
+ while(hq_next_grapheme(s,n,&off,&g)) w+=(size_t)g.width;
+ return w;
+}
+size_t hq_text_width(const char *s) { return s ? hq_text_width_n(s,strlen(s)):0; }
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/collect.cpp b/ports/nim/src/hqtui/engine/cpp/demo/collect.cpp
new file mode 100644
index 0000000..486d6e8
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/collect.cpp
@@ -0,0 +1,904 @@
+#include "collect.hpp"
+#include
+#include
+#include
+#include
+#include
+#if defined(__linux__)
+#include
+#include
+#include
+#endif
+#if defined(__unix__) || defined(__APPLE__)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+extern char **environ;
+#endif
+namespace demo {
+std::string read_file(const std::string &path, std::size_t limit) {
+ std::ifstream in(path, std::ios::binary);
+ if (!in)
+ return {};
+ std::string out;
+ char buffer[4096];
+ while (in && out.size() < limit) {
+ in.read(buffer,
+ std::streamsize(std::min(sizeof buffer, limit - out.size())));
+ out.append(buffer, std::size_t(in.gcount()));
+ }
+ return out;
+}
+static double now() {
+ return std::chrono::duration(
+ std::chrono::steady_clock::now().time_since_epoch())
+ .count();
+}
+std::string command(const std::vector &args,
+ const std::atomic *cancel, int timeout_ms) {
+#if defined(__unix__) || defined(__APPLE__)
+ if (args.empty() || (cancel && cancel->load()))
+ return {};
+ int fd[2];
+ if (pipe(fd))
+ return {};
+ fcntl(fd[0], F_SETFD, FD_CLOEXEC);
+ fcntl(fd[1], F_SETFD, FD_CLOEXEC);
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0);
+ posix_spawn_file_actions_addopen(&actions, 2, "/dev/null", O_WRONLY, 0);
+ posix_spawn_file_actions_adddup2(&actions, fd[1], 1);
+ posix_spawn_file_actions_addclose(&actions, fd[0]);
+ posix_spawn_file_actions_addclose(&actions, fd[1]);
+ posix_spawnattr_t attr;
+ posix_spawnattr_init(&attr);
+ posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETPGROUP);
+ posix_spawnattr_setpgroup(&attr, 0);
+ std::vector argv;
+ for (auto &s : args)
+ argv.push_back(const_cast(s.c_str()));
+ argv.push_back(nullptr);
+ pid_t pid = 0;
+ int error =
+ posix_spawnp(&pid, argv[0], &actions, &attr, argv.data(), environ);
+ posix_spawn_file_actions_destroy(&actions);
+ posix_spawnattr_destroy(&attr);
+ close(fd[1]);
+ if (error) {
+ close(fd[0]);
+ return {};
+ }
+ fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | O_NONBLOCK);
+ std::string out;
+ double deadline = now() + timeout_ms / 1000.;
+ int status = 0;
+ bool done = false;
+ while (now() < deadline && out.size() < 262144 &&
+ !(cancel && cancel->load())) {
+ char buffer[8192];
+ ssize_t n =
+ read(fd[0], buffer, std::min(sizeof buffer, 262144 - out.size()));
+ if (n > 0)
+ out.append(buffer, std::size_t(n));
+ else if (n == 0) {
+ done = true;
+ break;
+ } else if (errno != EAGAIN && errno != EINTR)
+ break;
+ struct pollfd pfd{fd[0], POLLIN, 0};
+ poll(&pfd, 1, 20);
+ }
+ // A child may close stdout and continue running. Reaping is bounded too.
+ pid_t reaped;
+ do {
+ reaped = waitpid(pid, &status, WNOHANG);
+ } while (reaped < 0 && errno == EINTR);
+ if (!done || reaped == 0) {
+ kill(-pid, SIGKILL);
+ while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {
+ }
+ }
+ close(fd[0]);
+ return out;
+#else
+ (void)args;
+ (void)cancel;
+ (void)timeout_ms;
+ return {};
+#endif
+}
+static std::vector lines(const std::string &s) {
+ std::vector out;
+ std::istringstream in(s);
+ std::string line;
+ while (std::getline(in, line))
+ out.push_back(line);
+ return out;
+}
+static std::vector words(const std::string &s) {
+ std::vector out;
+ std::istringstream in(s);
+ std::string word;
+ while (in >> word)
+ out.push_back(word);
+ return out;
+}
+static std::string trim(std::string s) {
+ auto first = s.find_first_not_of(" \t\r\n"),
+ last = s.find_last_not_of(" \t\r\n");
+ return first == s.npos ? "" : s.substr(first, last - first + 1);
+}
+static double numeric(const std::string &s) {
+ char *end = nullptr;
+ double v = std::strtod(s.c_str(), &end);
+ return end != s.c_str() && std::isfinite(v) ? v : 0;
+}
+static Json clear(const Json &v) {
+ if (std::holds_alternative(v.value)) {
+ Json::Object out;
+ for (auto &item : std::get(v.value))
+ out[item.first] = clear(item.second);
+ return out;
+ }
+ if (std::holds_alternative(v.value))
+ return Json::Array{};
+ if (std::holds_alternative(v.value))
+ return 0;
+ if (std::holds_alternative(v.value))
+ return "";
+ if (std::holds_alternative(v.value))
+ return false;
+ return {};
+}
+static void history(Json &j, double value) {
+ auto &a = j.array();
+ if (a.size() >= 240)
+ a.erase(a.begin());
+ a.emplace_back(value);
+}
+Collector::Collector(const Json &shape) : data(clear(shape)) {
+ data["telemetry"]["http"] = Json();
+ data["telemetry"]["power"] = Json();
+}
+void Collector::refresh(const std::atomic *cancel) {
+#ifndef __linux__
+ (void)cancel;
+ throw std::runtime_error(
+ "Live collectors require Linux; use --sim on this platform.");
+#else
+ double time = now(), elapsed = last ? std::max(.001, time - last) : 0;
+ auto delta = [&](std::string key, double value) {
+ auto it = counters.find(key);
+ double out = it != counters.end() && elapsed
+ ? std::max(0., value - it->second) / elapsed
+ : 0;
+ counters[key] = value;
+ return out;
+ };
+ auto &cpu = data["cpu"], &memory = data["memory"], &system = data["system"],
+ &network = data["network"], &telemetry = data["telemetry"],
+ &kernel = telemetry["kernel"];
+ data["time"] = time;
+ struct utsname os{};
+ uname(&os);
+ system["kernel"] = os.release;
+ system["hostname"] = os.nodename;
+ system["os"] = os.sysname;
+ for (auto &line : lines(read_file("/etc/os-release")))
+ if (line.rfind("PRETTY_NAME=", 0) == 0) {
+ auto value = line.substr(12);
+ if (value.size() >= 2 && value.front() == '"' && value.back() == '"')
+ value = value.substr(1, value.size() - 2);
+ system["os"] = value;
+ }
+ system["uptime"] = numeric(read_file("/proc/uptime"));
+ const char *shell = std::getenv("SHELL");
+ system["shell"] = shell ? shell : "";
+ Json::Array cores;
+ for (auto &line : lines(read_file("/proc/stat"))) {
+ auto v = words(line);
+ if (v.size() < 2)
+ continue;
+ if (v[0].rfind("cpu", 0) == 0 && v.size() >= 5) {
+ double total = 0;
+ for (std::size_t i = 1; i < std::min(v.size(), std::size_t(9)); i++)
+ total += numeric(v[i]);
+ double idle = numeric(v[4]) + (v.size() > 5 ? numeric(v[5]) : 0),
+ dt = delta(v[0] + ".total", total),
+ di = delta(v[0] + ".idle", idle),
+ used = dt ? ratio((dt - di) / dt) : 0;
+ if (v[0] == "cpu") {
+ cpu["total"] = used;
+ history(cpu["history"], used * 100);
+ } else
+ cores.emplace_back(used);
+ } else if (v[0] == "ctxt") {
+ system["contextSwitches"] = numeric(v[1]);
+ kernel["contextSwitchRate"] = delta("ctxt", numeric(v[1]));
+ } else if (v[0] == "intr")
+ kernel["interruptRate"] = delta("intr", numeric(v[1]));
+ else if (v[0] == "processes")
+ kernel["forkRate"] = delta("forks", numeric(v[1]));
+ else if (v[0] == "procs_running")
+ kernel["procsRunning"] = numeric(v[1]);
+ else if (v[0] == "procs_blocked")
+ kernel["procsBlocked"] = numeric(v[1]);
+ }
+ cpu["cores"] = cores;
+ Json::Array sensors, temps;
+ std::size_t core = 0;
+ double mhz = 0;
+ for (auto &line : lines(read_file("/proc/cpuinfo"))) {
+ auto colon = line.find(':');
+ if (colon == line.npos)
+ continue;
+ auto key = trim(line.substr(0, colon)),
+ value = trim(line.substr(colon + 1));
+ if (key == "model name")
+ cpu["model"] = value;
+ if (key == "cpu MHz") {
+ double n = numeric(value);
+ mhz += n;
+ sensors.push_back(
+ Json::Object{{"label", "cpu" + std::to_string(core++) + " Clock"},
+ {"value", fixed(n) + " MHz"}});
+ }
+ }
+ cpu["frequencyGhz"] = core ? mhz / core / 1000 : 0;
+ Json::Array load;
+ auto averages = words(read_file("/proc/loadavg"));
+ for (int i = 0; i < 3 && i < int(averages.size()); i++)
+ load.emplace_back(numeric(averages[i]));
+ cpu["load"] = load;
+ std::map mem;
+ for (auto &line : lines(read_file("/proc/meminfo"))) {
+ auto v = words(line);
+ if (v.size() > 1)
+ mem[v[0]] = numeric(v[1]) * 1024;
+ }
+ memory["total"] = mem["MemTotal:"];
+ memory["available"] = mem["MemAvailable:"];
+ memory["used"] = std::max(0., mem["MemTotal:"] - mem["MemAvailable:"]);
+ memory["free"] = mem["MemFree:"];
+ memory["cached"] = mem["Cached:"];
+ memory["buffers"] = mem["Buffers:"];
+ memory["swapTotal"] = mem["SwapTotal:"];
+ memory["swapUsed"] = std::max(0., mem["SwapTotal:"] - mem["SwapFree:"]);
+ history(memory["history"],
+ memory["used"].n() / std::max(1., memory["total"].n()) * 100);
+ std::error_code ec;
+ for (auto device :
+ std::filesystem::directory_iterator("/sys/class/hwmon", ec)) {
+ auto chip = trim(read_file(device.path().string() + "/name", 256));
+ for (auto file : std::filesystem::directory_iterator(device.path(), ec)) {
+ auto name = file.path().filename().string();
+ if (name.size() < 7 || name.substr(name.size() - 6) != "_input")
+ continue;
+ std::string prefix = name.substr(0, name.size() - 6),
+ label = trim(read_file(
+ device.path().string() + "/" + prefix + "_label", 256));
+ if (label.empty())
+ label = chip + " " + prefix;
+ auto raw = trim(read_file(file.path().string(), 80));
+ if (raw.empty())
+ continue;
+ double value = numeric(raw);
+ if (prefix.rfind("temp", 0) == 0 && value >= -40000 && value <= 150000) {
+ double maximum =
+ numeric(read_file(device.path().string() + "/" + prefix + "_crit",
+ 80)) /
+ 1000;
+ temps.push_back(Json::Object{{"label", label},
+ {"value", value / 1000},
+ {"max", maximum > 0 ? maximum : 100}});
+ } else if (prefix.rfind("fan", 0) == 0 && value > 0)
+ sensors.push_back(
+ Json::Object{{"label", label}, {"value", fixed(value) + " RPM"}});
+ else if (prefix.rfind("in", 0) == 0)
+ sensors.push_back(Json::Object{
+ {"label", label}, {"value", fixed(value / 1000, 2) + " V"}});
+ else if (prefix.rfind("curr", 0) == 0)
+ sensors.push_back(Json::Object{
+ {"label", label}, {"value", fixed(value / 1000, 2) + " A"}});
+ else if (prefix.rfind("power", 0) == 0)
+ sensors.push_back(Json::Object{
+ {"label", label}, {"value", fixed(value / 1e6, 2) + " W"}});
+ }
+ }
+ data["sensors"] = sensors;
+ data["temperatures"] = temps;
+ Json::Array interfaces;
+ std::map addresses;
+ ifaddrs *raw_addresses = nullptr;
+ if (getifaddrs(&raw_addresses) == 0) {
+ std::unique_ptr owned(raw_addresses,
+ freeifaddrs);
+ for (auto *entry = owned.get(); entry; entry = entry->ifa_next) {
+ if (!entry->ifa_addr || !entry->ifa_name)
+ continue;
+ int family = entry->ifa_addr->sa_family;
+ if (family != AF_INET && family != AF_INET6)
+ continue;
+ const void *address =
+ family == AF_INET
+ ? static_cast(
+ &reinterpret_cast(entry->ifa_addr)->sin_addr)
+ : static_cast(
+ &reinterpret_cast(entry->ifa_addr)
+ ->sin6_addr);
+ char formatted[INET6_ADDRSTRLEN];
+ if (inet_ntop(family, address, formatted, sizeof formatted) &&
+ (family == AF_INET || !addresses.count(entry->ifa_name)))
+ addresses[entry->ifa_name] = formatted;
+ }
+ }
+ double rx = 0, tx = 0, rxrate = 0, txrate = 0;
+ for (auto &line : lines(read_file("/proc/net/dev"))) {
+ auto colon = line.find(':');
+ if (colon == line.npos)
+ continue;
+ auto name = trim(line.substr(0, colon));
+ auto v = words(line.substr(colon + 1));
+ if (v.size() < 16)
+ continue;
+ double a = numeric(v[0]), b = numeric(v[8]), ar = delta("rx." + name, a),
+ br = delta("tx." + name, b);
+ Json iface = Json::Object{
+ {"name", name},
+ {"state",
+ trim(read_file("/sys/class/net/" + name + "/operstate", 128))},
+ {"ip", addresses[name]},
+ {"mac", trim(read_file("/sys/class/net/" + name + "/address", 128))},
+ {"mtu", numeric(read_file("/sys/class/net/" + name + "/mtu", 128))},
+ {"rxTotal", a},
+ {"txTotal", b},
+ {"rxRate", ar},
+ {"txRate", br},
+ {"errors", numeric(v[2]) + numeric(v[10])},
+ {"drops", numeric(v[3]) + numeric(v[11])}};
+ for (auto &old : telemetry["interfaces"].array())
+ if (old["name"].s() == name) {
+ iface["rxHistory"] = old["rxHistory"];
+ iface["txHistory"] = old["txHistory"];
+ }
+ history(iface["rxHistory"], ar);
+ history(iface["txHistory"], br);
+ interfaces.push_back(iface);
+ if (name != "lo") {
+ rx += a;
+ tx += b;
+ rxrate += ar;
+ txrate += br;
+ }
+ }
+ telemetry["interfaces"] = interfaces;
+ network["downRate"] = rxrate;
+ network["upRate"] = txrate;
+ network["downTotal"] = rx;
+ network["upTotal"] = tx;
+ network["downPeak"] = std::max(network["downPeak"].n(), rxrate);
+ network["upPeak"] = std::max(network["upPeak"].n(), txrate);
+ history(network["downHistory"], rxrate);
+ history(network["upHistory"], txrate);
+ Json::Array processes;
+ int threads = 0, running = 0, sleeping = 0, stopped = 0, zombie = 0;
+ for (auto &line : lines(command(
+ {"ps", "-eo", "pid=,comm=,pcpu=,pmem=,rss=,nlwp=,stat=,user=,args="},
+ cancel))) {
+ auto v = words(line);
+ if (v.size() < 8)
+ continue;
+ std::string cmd;
+ for (std::size_t i = 8; i < v.size(); i++) {
+ if (!cmd.empty())
+ cmd += ' ';
+ cmd += v[i];
+ }
+ auto state = v[6].substr(0, 1);
+ running += state == "R";
+ stopped += state == "T";
+ zombie += state == "Z";
+ sleeping += state != "R" && state != "T" && state != "Z";
+ threads += int(numeric(v[5]));
+ processes.push_back(Json::Object{{"pid", numeric(v[0])},
+ {"name", v[1]},
+ {"cpu", numeric(v[2])},
+ {"mem", numeric(v[3])},
+ {"rss", numeric(v[4]) * 1024},
+ {"threads", numeric(v[5])},
+ {"state", state},
+ {"user", v[7]},
+ {"command", cmd}});
+ }
+ data["processes"] = processes;
+ system["processCount"] = int(processes.size());
+ system["threadCount"] = threads;
+ telemetry["states"] = Json::Object{{"total", int(processes.size())},
+ {"running", running},
+ {"sleeping", sleeping},
+ {"stopped", stopped},
+ {"zombie", zombie}};
+ Json::Array disks, filesystems;
+ std::map> io;
+ for (auto &line : lines(read_file("/proc/diskstats"))) {
+ auto v = words(line);
+ if (v.size() < 14)
+ continue;
+ io[v[2]] = {delta("diskr." + v[2], numeric(v[5]) * 512),
+ delta("diskw." + v[2], numeric(v[9]) * 512)};
+ }
+ std::map devices;
+ for (auto &line : lines(read_file("/proc/mounts"))) {
+ auto v = words(line);
+ if (v.size() < 3 || devices[v[0]] ||
+ !(v[0].rfind("/dev/", 0) == 0 || v[1] == "/"))
+ continue;
+ struct statvfs stat{};
+ if (statvfs(v[1].c_str(), &stat) || !stat.f_blocks)
+ continue;
+ devices[v[0]] = true;
+ double total = double(stat.f_blocks) * stat.f_frsize,
+ used = double(stat.f_blocks - stat.f_bfree) * stat.f_frsize;
+ std::string name = v[0].substr(v[0].find_last_of('/') + 1);
+ Json disk = Json::Object{{"device", name},
+ {"mount", v[1]},
+ {"type", v[2]},
+ {"total", total},
+ {"used", used},
+ {"readRate", io[name].first},
+ {"writeRate", io[name].second}};
+ for (auto &old : data["disks"].array())
+ if (old["device"].s() == name) {
+ disk["readHistory"] = old["readHistory"];
+ disk["writeHistory"] = old["writeHistory"];
+ }
+ history(disk["readHistory"], io[name].first);
+ history(disk["writeHistory"], io[name].second);
+ disks.push_back(disk);
+ filesystems.push_back(
+ Json::Object{{"mount", v[1]},
+ {"device", v[0]},
+ {"type", v[2]},
+ {"size", total},
+ {"used", used},
+ {"inodesUsed", double(stat.f_files - stat.f_ffree)},
+ {"inodesTotal", double(stat.f_files)}});
+ }
+ data["disks"] = disks;
+ telemetry["filesystems"] = filesystems;
+ auto file_nr = words(read_file("/proc/sys/fs/file-nr"));
+ kernel["openFiles"] = file_nr.empty() ? 0 : numeric(file_nr[0]);
+ kernel["entropy"] =
+ numeric(read_file("/proc/sys/kernel/random/entropy_avail"));
+ for (auto &line : lines(read_file("/proc/vmstat"))) {
+ auto v = words(line);
+ if (v.size() == 2 && v[0] == "pgpgin")
+ kernel["pageIn"] = numeric(v[1]);
+ if (v.size() == 2 && v[0] == "pgpgout")
+ kernel["pageOut"] = numeric(v[1]);
+ }
+ Json::Array sessions;
+ for (auto &line : lines(command({"who"}, cancel))) {
+ auto v = words(line);
+ if (v.size() < 4)
+ continue;
+ auto from = v.size() > 4 ? v.back() : "";
+ if (from.size() > 1 && from.front() == '(' && from.back() == ')')
+ from = from.substr(1, from.size() - 2);
+ sessions.push_back(Json::Object{{"user", v[0]},
+ {"tty", v[1]},
+ {"from", from},
+ {"loginAt", v[2] + " " + v[3]},
+ {"idle", ""}});
+ }
+ telemetry["sessions"] = sessions;
+ history(telemetry["sessionHistory"], double(sessions.size()));
+ auto login_rows = [&](std::vector args) {
+ Json::Array rows;
+ for (auto &line : lines(command(args, cancel))) {
+ auto v = words(line);
+ if (v.size() < 5 || v[0] == "reboot" || v[0] == "wtmp" || v[0] == "btmp")
+ continue;
+ std::string when;
+ for (std::size_t i = 3; i < std::min(v.size(), std::size_t(8)); i++) {
+ if (!when.empty())
+ when += ' ';
+ when += v[i];
+ }
+ rows.push_back(Json::Object{
+ {"user", v[0]},
+ {"tty", v[1]},
+ {"from", v[2]},
+ {"when", when},
+ {"status", line.find("still") != line.npos ? "still" : "closed"}});
+ }
+ return rows;
+ };
+ telemetry["logins"] = login_rows({"last", "-n", "30", "-w"});
+ telemetry["failedLogins"] = login_rows({"lastb", "-n", "20", "-w"});
+ for (auto &row : telemetry["failedLogins"].array())
+ row["status"] = "failed";
+ Json::Array services;
+ for (auto &line :
+ lines(command({"systemctl", "list-units", "--type=service", "--all",
+ "--no-legend", "--no-pager", "--plain"},
+ cancel))) {
+ auto v = words(line);
+ if (v.size() < 4 || v[0].find(".service") == v[0].npos)
+ continue;
+ std::string desc;
+ for (std::size_t i = 4; i < v.size(); i++) {
+ if (!desc.empty())
+ desc += ' ';
+ desc += v[i];
+ }
+ services.push_back(Json::Object{{"name", v[0]},
+ {"active", v[2]},
+ {"sub", v[3]},
+ {"description", desc}});
+ }
+ telemetry["services"] = services;
+ Json::Array connections, listeners, protocols, remotes;
+ std::map protocol_counts, remote_counts;
+ int inbound = 0, outbound = 0;
+ for (auto &line : lines(command({"ss", "-H", "-tunap"}, cancel))) {
+ auto v = words(line);
+ if (v.size() < 6)
+ continue;
+ std::string proto = v[0], state = v[1], local = v[4], remote = v[5],
+ process = v.size() > 6 ? v[6] : "";
+ auto port = [](const std::string &a) {
+ auto colon = a.find_last_of(':');
+ return colon == a.npos ? 0 : int(numeric(a.substr(colon + 1)));
+ };
+ int lp = port(local), rp = port(remote);
+ if (state == "LISTEN" || (state == "UNCONN" && rp == 0)) {
+ listeners.push_back(Json::Object{{"proto", proto},
+ {"port", lp},
+ {"address", local},
+ {"process", process}});
+ continue;
+ }
+ connections.push_back(Json::Object{{"proto", proto},
+ {"local", local},
+ {"remote", remote},
+ {"state", state},
+ {"process", process}});
+ int service = lp < rp ? lp : rp;
+ std::string label = service == 22 ? "SSH"
+ : service == 80 || service == 8080 ? "HTTP"
+ : service == 443 || service == 8443 ? "HTTPS"
+ : service == 53 ? "DNS"
+ : service == 5432 ? "Postgres"
+ : service == 6379 ? "Redis"
+ : proto == "udp" ? "UDP"
+ : "TCP";
+ protocol_counts[label]++;
+ if (lp < rp)
+ inbound++;
+ else
+ outbound++;
+ auto colon = remote.find_last_of(':');
+ remote_counts[remote.substr(0, colon)]++;
+ }
+ for (auto &v : protocol_counts)
+ protocols.push_back(
+ Json::Object{{"protocol", v.first}, {"total", v.second}});
+ for (auto &v : remote_counts)
+ remotes.push_back(Json::Object{
+ {"host", v.first}, {"connections", v.second}, {"protocols", ""}});
+ std::sort(remotes.begin(), remotes.end(), [](const Json &a, const Json &b) {
+ return a["connections"].n() > b["connections"].n();
+ });
+ telemetry["connections"] = connections;
+ telemetry["listeners"] = listeners;
+ telemetry["protocols"] = protocols;
+ telemetry["remotes"] = remotes;
+ telemetry["inboundConnections"] = inbound;
+ telemetry["outboundConnections"] = outbound;
+ history(telemetry["connectionHistory"], double(connections.size()));
+ auto snmp = lines(read_file("/proc/net/snmp"));
+ std::map netstats;
+ for (std::size_t i = 0; i + 1 < snmp.size(); i += 2) {
+ auto keys = words(snmp[i]), values = words(snmp[i + 1]);
+ for (std::size_t k = 1; k < std::min(keys.size(), values.size()); k++)
+ netstats[keys[0] + keys[k]] = numeric(values[k]);
+ }
+ auto &net = telemetry["net"], &rates = net["rates"];
+ net["tcpEstablished"] = netstats["Tcp:CurrEstab"];
+ net["tcpOutRsts"] = netstats["Tcp:OutRsts"];
+ net["icmpInMsgs"] = netstats["Icmp:InMsgs"];
+ net["icmpOutMsgs"] = netstats["Icmp:OutMsgs"];
+ const char *in[] = {"Tcp:InSegs", "Tcp:OutSegs", "Tcp:PassiveOpens",
+ "Tcp:ActiveOpens", "Udp:InDatagrams", "Udp:OutDatagrams",
+ "Tcp:RetransSegs"},
+ *out[] = {"inSegs", "outSegs", "passiveOpens", "activeOpens",
+ "udpIn", "udpOut", "retransSegs"};
+ for (int i = 0; i < 7; i++)
+ rates[out[i]] = delta(in[i], netstats[in[i]]);
+ net["retransRatio"] = rates["outSegs"].n()
+ ? rates["retransSegs"].n() / rates["outSegs"].n()
+ : 0;
+ history(telemetry["netInHistory"], rates["inSegs"].n());
+ history(telemetry["netOutHistory"], rates["outSegs"].n());
+ history(telemetry["retransHistory"], net["retransRatio"].n() * 100);
+ Json::Array logs, ssh;
+ for (auto &line : lines(
+ command({"journalctl", "-n", "80", "--no-pager", "-o", "short-iso"},
+ cancel))) {
+ auto v = words(line);
+ if (v.size() < 4 || line.rfind("--", 0) == 0)
+ continue;
+ std::string time = v[0].size() >= 19 ? v[0].substr(11, 8) : v[0], message;
+ for (std::size_t i = 3; i < v.size(); i++) {
+ if (!message.empty())
+ message += ' ';
+ message += v[i];
+ }
+ logs.push_back(Json::Object{
+ {"time", time},
+ {"level", message.find("error") != message.npos ? "ERROR" : "INFO"},
+ {"message", message},
+ {"meta", v[2]}});
+ if (v[2].find("sshd") != v[2].npos) {
+ auto words_ = words(message);
+ auto from = std::find(words_.begin(), words_.end(), "from"),
+ user = std::find(words_.begin(), words_.end(), "for");
+ std::string action = message.find("Accepted") != message.npos ? "accepted"
+ : message.find("Failed") != message.npos
+ ? "failed"
+ : "disconnect";
+ ssh.push_back(Json::Object{
+ {"time", time},
+ {"action", action},
+ {"user",
+ user != words_.end() && user + 1 != words_.end() ? *(user + 1) : ""},
+ {"from",
+ from != words_.end() && from + 1 != words_.end() ? *(from + 1) : ""},
+ {"method", words_.size() > 1 ? words_[1] : ""}});
+ }
+ }
+ data["logs"] = logs;
+ telemetry["ssh"] = ssh;
+
+ // SSH is queried separately because it can be absent from the short general
+ // journal tail. Parse both ISO and traditional syslog timestamps.
+ ssh.clear();
+ for (auto &line :
+ lines(command({"journalctl", "-u", "ssh", "-u", "sshd", "-n", "100",
+ "--no-pager", "-o", "short-iso"},
+ cancel))) {
+ auto marker = line.find("sshd[");
+ if (marker == line.npos)
+ continue;
+ auto colon = line.find(": ", marker);
+ if (colon == line.npos)
+ continue;
+ auto prefix = words(line.substr(0, marker)),
+ tokens = words(line.substr(colon + 2));
+ if (tokens.empty())
+ continue;
+ std::string time = prefix.empty() ? ""
+ : prefix[0].size() >= 19 ? prefix[0].substr(11, 8)
+ : prefix.size() > 2 ? prefix[2]
+ : "";
+ std::string action =
+ tokens[0] == "Accepted" ? "accepted"
+ : tokens[0] == "Failed" ? "failed"
+ : tokens[0] == "Disconnected" || tokens[0] == "Connection"
+ ? "disconnect"
+ : "";
+ if (action.empty())
+ continue;
+ auto from = std::find(tokens.begin(), tokens.end(), "from"),
+ user = std::find(tokens.begin(), tokens.end(), "for");
+ std::string username, address;
+ if (user != tokens.end() && user + 1 != tokens.end()) {
+ auto i = user + 1;
+ if (*i == "invalid" && tokens.end() - i > 2)
+ i += 2;
+ username = *i;
+ }
+ if (from != tokens.end() && from + 1 != tokens.end())
+ address = *(from + 1);
+ if (action == "disconnect") {
+ auto u = std::find(tokens.begin(), tokens.end(), "user");
+ if (u != tokens.end() && tokens.end() - u > 2) {
+ username = *(u + 1);
+ address = *(u + 2);
+ }
+ }
+ ssh.push_back(Json::Object{{"time", time},
+ {"action", action},
+ {"user", username},
+ {"from", address},
+ {"method", tokens.size() > 1 ? tokens[1] : ""}});
+ }
+ telemetry["ssh"] = ssh;
+
+ Json::Array gpus;
+ for (auto &line :
+ lines(command({"nvidia-smi",
+ "--query-gpu=name,utilization.gpu,memory.used,memory."
+ "total,temperature.gpu,power.draw",
+ "--format=csv,noheader,nounits"},
+ cancel))) {
+ std::vector fields;
+ std::istringstream in(line);
+ std::string field;
+ while (std::getline(in, field, ','))
+ fields.push_back(trim(field));
+ if (fields.size() < 6)
+ continue;
+ auto numeric_or_null = [](const std::string &v) -> Json {
+ char *end = nullptr;
+ double n = std::strtod(v.c_str(), &end);
+ return end != v.c_str() && *end == 0 && std::isfinite(n) ? Json(n)
+ : Json();
+ };
+ auto util = numeric_or_null(fields[1]), used = numeric_or_null(fields[2]),
+ total = numeric_or_null(fields[3]), temp = numeric_or_null(fields[4]);
+ gpus.push_back(Json::Object{
+ {"name", fields[0]},
+ {"utilization", util.null() ? Json() : Json(util.n() / 100)},
+ {"memoryUsed", used.null() ? Json() : Json(used.n() * 1048576)},
+ {"memoryTotal", total.null() ? Json() : Json(total.n() * 1048576)},
+ {"temperature", temp}});
+ sensors.push_back(
+ Json::Object{{"label", fields[0]},
+ {"value", util.null() ? "—" : percent(util.n() / 100)}});
+ if (!temp.null())
+ temps.push_back(
+ Json::Object{{"label", fields[0]}, {"value", temp}, {"max", 100}});
+ }
+ telemetry["gpus"] = gpus;
+ data["sensors"] = sensors;
+ data["temperatures"] = temps;
+ telemetry["power"] = Json();
+ bool ac = false;
+ for (auto &device :
+ std::filesystem::directory_iterator("/sys/class/power_supply", ec)) {
+ auto path = device.path().string(),
+ type = trim(read_file(path + "/type", 80));
+ if (type == "Mains" || type == "USB" || type == "USB_C")
+ ac = ac || numeric(read_file(path + "/online", 80)) == 1;
+ }
+ for (auto &device :
+ std::filesystem::directory_iterator("/sys/class/power_supply", ec)) {
+ auto path = device.path().string();
+ if (trim(read_file(path + "/type", 80)) != "Battery")
+ continue;
+ auto capacity = trim(read_file(path + "/capacity", 80)),
+ draw = trim(read_file(path + "/power_now", 80));
+ Json watts = draw.empty() ? Json() : Json(numeric(draw) / 1e6);
+ telemetry["power"] = Json::Object{
+ {"battery", capacity.empty() ? Json() : Json(numeric(capacity))},
+ {"acConnected", ac},
+ {"timeRemaining", "—"},
+ {"powerDraw", watts}};
+ break;
+ }
+ Json::Array containers;
+ for (auto &line :
+ lines(command({"docker", "--host", "unix:///var/run/docker.sock", "ps",
+ "--format", "{{json .}}"},
+ cancel))) {
+ try {
+ auto container = Json::parse(line);
+ containers.push_back(Json::Object{{"name", container["Names"]},
+ {"image", container["Image"]},
+ {"status", container["Status"]}});
+ } catch (const std::exception &) {
+ }
+ }
+ telemetry["containers"] = containers;
+
+ // Access-log samples are bounded. The rate is explicitly described as an
+ // estimate in the Traffic screen; a rotation/reset never produces a spike.
+ Json http;
+ for (auto path : {"/var/log/nginx/access.log", "/var/log/apache2/access.log",
+ "/var/log/httpd/access_log", "/var/log/caddy/access.log"}) {
+ if (cancel && cancel->load())
+ break;
+ auto content = command({"tail", "-n", "300", "--", path}, cancel);
+ if (content.empty())
+ continue;
+ Json::Array recent;
+ std::map classes, paths;
+ int upgrades = 0, parsed = 0;
+ for (auto &line : lines(content)) {
+ if (line.size() > 16384)
+ continue;
+ std::string client, method, target, time;
+ double status = 0, bytes = 0;
+ if (!line.empty() && line[0] == '{') {
+ try {
+ auto record = Json::parse(line);
+ client = record.path("request.remote_ip").s("");
+ method = record.path("request.method").s("");
+ target = record.path("request.uri").s("");
+ status = record["status"].n();
+ bytes = record["size"].n();
+ time = record["ts"].s("");
+ } catch (const std::exception &) {
+ continue;
+ }
+ } else {
+ auto quote = line.find('"'),
+ end = quote == line.npos ? line.npos : line.find('"', quote + 1);
+ if (end == line.npos)
+ continue;
+ auto request = words(line.substr(quote + 1, end - quote - 1)),
+ result = words(line.substr(end + 1));
+ if (request.size() < 2 || result.size() < 2)
+ continue;
+ auto begin = line.find('['), close = line.find(']', begin);
+ auto fields = words(line.substr(0, quote));
+ if (fields.empty())
+ continue;
+ client = fields[0];
+ method = request[0];
+ target = request[1];
+ status = numeric(result[0]);
+ bytes = numeric(result[1]);
+ if (begin != line.npos && close != line.npos) {
+ auto stamp = line.substr(begin + 1, close - begin - 1);
+ auto colon = stamp.find(':');
+ time = colon != stamp.npos ? stamp.substr(colon + 1, 8) : stamp;
+ }
+ }
+ if (method.empty() || target.empty() || status < 100 || status > 599)
+ continue;
+ // Routes, not request secrets: never display query strings or fragments.
+ target = target.substr(0, target.find_first_of("?#"));
+ parsed++;
+ upgrades += status == 101;
+ classes[std::to_string(int(status) / 100) + "xx"]++;
+ paths[target]++;
+ recent.push_back(Json::Object{{"time", time},
+ {"method", method},
+ {"path", target},
+ {"status", int(status)},
+ {"client", client},
+ {"bytes", bytes}});
+ }
+ if (!parsed)
+ continue;
+ Json::Array status_classes, top_paths;
+ for (auto &item : classes)
+ status_classes.push_back(
+ Json::Object{{"class", item.first}, {"count", item.second}});
+ for (auto &item : paths)
+ top_paths.push_back(
+ Json::Object{{"path", item.first}, {"count", item.second}});
+ std::stable_sort(top_paths.begin(), top_paths.end(),
+ [](const Json &a, const Json &b) {
+ return a["count"].n() > b["count"].n();
+ });
+ std::reverse(recent.begin(), recent.end());
+ if (recent.size() > 50)
+ recent.resize(50);
+ auto size = std::filesystem::file_size(path, ec);
+ double rps = ec ? 0
+ : delta(std::string("http:") + path, double(size)) /
+ std::max(1., double(content.size()) / parsed);
+ http =
+ Json::Object{{"source", path}, {"requestsPerSecond", rps},
+ {"upgrades", upgrades}, {"statusClasses", status_classes},
+ {"topPaths", top_paths}, {"recent", recent}};
+ if (telemetry["http"]["source"].s("") == path)
+ http["history"] = telemetry["http"]["history"];
+ history(http["history"], rps);
+ break;
+ }
+ telemetry["http"] = http;
+ last = time;
+#endif
+}
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/collect.hpp b/ports/nim/src/hqtui/engine/cpp/demo/collect.hpp
new file mode 100644
index 0000000..4382782
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/collect.hpp
@@ -0,0 +1,16 @@
+#pragma once
+#include "model.hpp"
+#include
+namespace demo {
+std::string read_file(const std::string &, std::size_t limit = 1048576);
+std::string command(const std::vector &,
+ const std::atomic *cancel = nullptr,
+ int timeout_ms = 800);
+struct Collector {
+ Json data;
+ double last = 0;
+ std::map counters;
+ explicit Collector(const Json &shape);
+ void refresh(const std::atomic *cancel = nullptr);
+};
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/dashboard.cpp b/ports/nim/src/hqtui/engine/cpp/demo/dashboard.cpp
new file mode 100644
index 0000000..72b8bb9
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/dashboard.cpp
@@ -0,0 +1,366 @@
+#include "model.hpp"
+namespace demo {
+static void cpu_panel(UI &ui, State &s, int columns, Constraint size = fr()) {
+ const auto &c = s.data["cpu"];
+ ui.panel(
+ "CPU Overview",
+ [&, columns](UI &p) {
+ auto t = p.t();
+ p.label(c["model"].s() + " " + fixed(c["frequencyGhz"].n(), 1) +
+ " GHz");
+ p.graph(graph(c["history"], t.success, 100));
+ std::vector items;
+ int i = 0;
+ for (auto &v : c["cores"].array()) {
+ Meter m;
+ m.value = v.n();
+ m.label = "P" + std::to_string(i++);
+ m.label_width = 4;
+ m.value_width = 5;
+ m.segmented = true;
+ items.push_back(m);
+ }
+ p.meters(items, columns);
+ p.divider();
+ std::string load;
+ for (auto &v : c["load"].array()) {
+ if (!load.empty())
+ load += " ";
+ load += fixed(v.n(), 2);
+ }
+ p.keys({kv("Load Avg", load, t.warning)});
+ },
+ size, percent(c["total"].n()));
+}
+static void memory_panel(UI &ui, State &s, Constraint size = fr()) {
+ const auto &m = s.data["memory"];
+ ui.panel("Memory & Swap", [&](UI &p) {
+ auto t = p.t();
+ double used = m["used"].n() / std::max(1., m["total"].n()),
+ swap = m["swapUsed"].n() / std::max(1., m["swapTotal"].n());
+ p.text("Memory " + bytes(m["used"].n()) + " / " +
+ bytes(m["total"].n()) + " (" + percent(used) + ")");
+ p.meter(meter(used));
+ p.spacer(cells(1));
+ p.keys({kv("Used:", bytes(m["used"].n()), t.warning),
+ kv("Available:", bytes(m["available"].n()), t.success),
+ kv("Cached:", bytes(m["cached"].n()), t.accent),
+ kv("Buffers:", bytes(m["buffers"].n()), t.secondary),
+ kv("Free:", bytes(m["free"].n()), t.muted)});
+ p.spacer();
+ p.divider();
+ p.text("Swap " + bytes(m["swapUsed"].n()) + " / " +
+ bytes(m["swapTotal"].n()) + " (" + percent(swap) + ")");
+ p.meter(meter(swap, t.secondary));
+ p.keys(
+ {kv("Used:", bytes(m["swapUsed"].n()), t.secondary),
+ kv("Free:", bytes(m["swapTotal"].n() - m["swapUsed"].n()), t.muted)});
+ }, size);
+}
+static void disks_panel(UI &ui, State &s, Constraint size = fr()) {
+ ui.panel("Disks", [&](UI &p) {
+ auto t = p.t();
+ auto &disks = s.data["disks"].array();
+ if (disks.empty()) {
+ p.label("No disks reported");
+ return;
+ }
+ for (std::size_t i = 0; i < std::min(std::size_t(2), disks.size()); i++) {
+ auto d = disks[i];
+ double used = d["used"].n() / std::max(1., d["total"].n());
+ p.text(d["device"].s() + " — " + bytes(d["total"].n()) + " (" +
+ d["type"].s() + ")");
+ p.text("Used: " + bytes(d["used"].n()) + " (" + percent(used) + ")",
+ t.muted);
+ p.meter(meter(used));
+ p.text("Free: " + bytes(d["total"].n() - d["used"].n()), t.muted);
+ p.row(cells(1), 0, [=](UI &r) {
+ r.text("Read: " + byte_rate(d["readRate"].n()), t.success);
+ r.text("Write: " + byte_rate(d["writeRate"].n()), t.secondary,
+ HQ_RIGHT);
+ });
+ p.graph(
+ multi(d["readHistory"], d["writeHistory"], t.success, t.secondary));
+ if (i == 0 && disks.size() > 1)
+ p.divider();
+ }
+ }, size);
+}
+static void system_panel(UI &ui, State &s, Constraint size = fr()) {
+ const int gap = s.panel_gap();
+ ui.panel("System", [&, gap](UI &p) {
+ auto t = p.t();
+ auto sys = s.data["system"], c = s.data["cpu"], m = s.data["memory"];
+ double used = m["used"].n() / std::max(1., m["total"].n());
+ auto count = sys["processCount"].n()
+ ? sys["processCount"].s()
+ : std::to_string(s.data["processes"].array().size());
+ p.row(cells(6, 6), 2, [=, &s](UI &r) {
+ r.keys({kv("OS:", sys["os"].s()), kv("Kernel:", sys["kernel"].s()),
+ kv("Uptime:", duration(sys["uptime"].n())),
+ kv("Hostname:", sys["hostname"].s()),
+ kv("Shell:", sys["shell"].s()),
+ kv("Source:", s.real ? "linux/proc" : "simulated", t.accent)},
+ false);
+ std::string load;
+ for (auto &v : c["load"].array()) {
+ if (!load.empty())
+ load += ' ';
+ load += fixed(v.n(), 2);
+ }
+ r.keys({kv("CPU:", percent(c["total"].n()), hq_heat(&t, c["total"].n())),
+ kv("Memory:", percent(used) + " (" + bytes(m["used"].n()) + ")",
+ t.warning),
+ kv("Swap:",
+ m["swapTotal"].n()
+ ? percent(m["swapUsed"].n() / m["swapTotal"].n())
+ : "—",
+ t.secondary),
+ kv("Load:", load), kv("Processes:", count),
+ kv("Threads:", sys["threadCount"].s())},
+ false);
+ });
+ p.panel(
+ "CPU History",
+ [=](UI &g) { g.graph(graph(c["history"], t.success, 100, true)); },
+ fr(1, 5));
+ if (p.width() >= 46 && p.height() >= 16)
+ p.row(cells(6), gap, [=, &s](UI &r) {
+ r.panel("Quick Stats", [=, &s](UI &q) {
+ q.keys(
+ {kv("Uptime", duration(sys["uptime"].n()), t.accent),
+ kv("Procs", count, t.accent),
+ kv("Threads",
+ sys["threadCount"].n() ? sys["threadCount"].s() : "—",
+ t.accent),
+ kv("Ctx/s",
+ fixed(s.data.path("telemetry.kernel.contextSwitchRate").n() /
+ 1000,
+ 1) +
+ "K",
+ t.accent)});
+ });
+ r.panel("Memory", [=](UI &q) {
+ q.text(percent(used), t.warning);
+ q.graph(graph(m["history"], t.primary, 100));
+ });
+ auto temp = s.data["temperatures"].at(0);
+ r.panel(
+ "Temp",
+ [=](UI &q) {
+ q.gauge(temp.null()
+ ? c["total"].n()
+ : std::min(1., temp["value"].n() /
+ std::max(1., temp["max"].n(100))),
+ temp.null()
+ ? percent(c["total"].n())
+ : fixed(std::floor(temp["value"].n() + .5)) + "°C");
+ },
+ cells(14));
+ });
+ else {
+ p.divider();
+ p.keys({kv("Threads", sys["threadCount"].s(), t.accent),
+ kv("Ctx switches",
+ fixed(sys["contextSwitches"].n() / 1000, 1) + "K", t.accent)});
+ }
+ }, size);
+}
+static void processes_panel(UI &ui, State &s, Constraint size = fr()) {
+ std::string sort =
+ std::vector{"CPU", "MEM", "PID", "NAME"}[s.sort];
+ ui.panel(
+ "Processes (sorted by " + sort + ")",
+ [&](UI &p) {
+ auto t = p.t();
+ table(p, s, "dashboard.processes", s.processes(),
+ {dc("pid", "PID", 7, 1, 0, true),
+ dc("name", "Name", -1, 8, t.primary),
+ dc(
+ "cpu", "CPU%", 6, 1, 0, true,
+ [](const Json &d) { return fixed(d["cpu"].n(), 1); },
+ [=](const Json &d) {
+ return hq_heat(&t, std::min(1., d["cpu"].n() / 100));
+ }),
+ dc("mem", "MEM%", 6, 1, t.warning, true,
+ [](const Json &d) { return fixed(d["mem"].n(), 1); }),
+ dc("rss", "RSS", 9, 1, 0, true,
+ [](const Json &d) { return bytes(d["rss"].n(), 0); }),
+ dc("threads", "Threads", 7, 1, 0, true),
+ dc("state", "S", 2, 1, 0, false, {},
+ [=](const Json &d) {
+ return d["state"].s() == "R" ? t.success : t.muted;
+ }),
+ dc("user", "User", 10, 1, t.muted),
+ dc("command", "Command", -1, 10, t.muted)});
+ },
+ size, s.filter.empty() ? "" : "filter: " + s.filter,
+ ui.t().border_focused);
+}
+static void network_panel(UI &ui, State &s, Constraint size = fr()) {
+ ui.panel("Network", [&](UI &p) {
+ auto t = p.t();
+ auto n = s.data["network"];
+ p.row(cells(1), 0, [=](UI &r) {
+ r.text("Download: " + bit_rate(n["downRate"].n()), t.primary);
+ r.text("Upload: " + bit_rate(n["upRate"].n()), t.secondary, HQ_RIGHT);
+ });
+ for (auto prefix : {"down", "up"}) {
+ auto g = graph(n[std::string(prefix) + "History"],
+ std::string(prefix) == "down" ? t.primary : t.secondary,
+ {}, true);
+ g.axis_format = [](double v) {
+ auto value = bit_rate(v);
+ value.erase(std::remove(value.begin(), value.end(), ' '), value.end());
+ return value;
+ };
+ p.graph(g);
+ }
+ p.divider();
+ p.row(cells(3), 2, [=](UI &r) {
+ for (auto prefix : {"down", "up"}) {
+ auto color = std::string(prefix) == "down" ? t.primary : t.secondary;
+ r.keys(
+ {kv("Total:", bytes(n[std::string(prefix) + "Total"].n()), color),
+ kv("Current:", bit_rate(n[std::string(prefix) + "Rate"].n()),
+ color),
+ kv("Peak:", bit_rate(n[std::string(prefix) + "Peak"].n()), color)},
+ false);
+ }
+ });
+ }, size);
+}
+static void disk_usage_panel(UI &ui, State &s, Constraint size = fr()) {
+ ui.panel(
+ "Disk Usage",
+ [&](UI &p) {
+ auto t = p.t();
+ for (auto &d : s.data["disks"].array()) {
+ double used = d["used"].n() / std::max(1., d["total"].n());
+ Meter m;
+ m.value = used;
+ m.segmented = true;
+ m.readout = percent(used) + " " + bytes(d["used"].n(), 0) + " / " +
+ bytes(d["total"].n(), 0);
+ p.meter(m);
+ p.label(d["mount"].s() + " (" + d["device"].s() + ")");
+ }
+ p.spacer(cells(1));
+ auto first = s.data["disks"].at(0);
+ p.panel("I/O Summary", [=](UI &io) {
+ io.row(fr(), 2, [=](UI &r) {
+ r.col(fr(), 0, [=](UI &c) {
+ c.text("Read: " + byte_rate(first["readRate"].n()), t.success);
+ c.graph(graph(first["readHistory"], t.success));
+ });
+ r.col(fr(), 0, [=](UI &c) {
+ c.text("Write: " + byte_rate(first["writeRate"].n()),
+ t.secondary);
+ c.graph(graph(first["writeHistory"], t.secondary));
+ });
+ });
+ });
+ },
+ size, s.data["disks"].at(0)["device"].s(""));
+}
+static void temperatures_panel(UI &ui, State &s) {
+ ui.panel("Temperatures", [&](UI &p) {
+ auto temps = s.data["temperatures"].array();
+ auto t = p.t();
+ if (temps.empty()) {
+ p.text("No thermal sensors on this host.", t.muted);
+ p.spacer(cells(1));
+ p.label("Run with --sim to see this panel populated.");
+ return;
+ }
+ int count = 0;
+ for (auto &temp : temps) {
+ if (count++ == 10)
+ break;
+ double v =
+ std::min(1., temp["value"].n() / std::max(1., temp["max"].n(100)));
+ p.row(cells(1), 0, [=](UI &r) {
+ r.text(temp["label"].s(), t.muted, HQ_LEFT, cells(16));
+ r.draw([=](Surface surface) {
+ int w = surface.rect().width, filled = iround(v * w);
+ for (int x = 0; x < w; x++)
+ surface.set(x, 0, 0x25ae,
+ Style().foreground(
+ x < filled
+ ? hq_heat(&t, w <= 1 ? v : double(x) / (w - 1))
+ : hq_mix(t.background, t.border, .75)));
+ });
+ r.text(fixed(std::floor(temp["value"].n() + .5)) + "°C", hq_heat(&t, v),
+ HQ_RIGHT, cells(6));
+ });
+ }
+ });
+}
+static void sensors_panel(UI &ui, State &s) {
+ ui.panel("Sensors", [&](UI &p) {
+ auto &sensors = s.data["sensors"].array();
+ if (sensors.empty()) {
+ p.label("No hardware sensors on this host.");
+ p.spacer(cells(1));
+ p.label("Probed: /sys/class/hwmon, thermal zones, lm-sensors,");
+ p.label("power supplies and nvidia-smi.");
+ return;
+ }
+ std::vector rows;
+ for (auto &v : sensors)
+ rows.push_back(kv(v["label"].s(), v["value"].s(), p.t().accent));
+ p.keys(rows);
+ });
+}
+static void logs_panel(UI &ui, State &s, Constraint size = fr()) {
+ ui.panel("Logs", [&](UI &p) {
+ p.log(logs(s.data["logs"]), &s.panes["dashboard.logs"], "dashboard.logs");
+ }, size);
+}
+// Panels go straight into their row rather than each inside a sizing column. A
+// column is not a bordered child, so a row of them has no seam to merge and `c`
+// could never change this screen; a size on the panel does the same job and
+// leaves the borders adjacent to each other.
+void dashboard(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ if (ui.width() >= 150) {
+ ui.row(cells(ui.height() >= 44 ? 19 : 16), gap, [&](UI &r) {
+ cpu_panel(r, s, 2, fr());
+ memory_panel(r, s, fr(.95));
+ disks_panel(r, s, fr(.95));
+ system_panel(r, s, fr(1.35));
+ });
+ ui.row(fr(), gap, [&](UI &r) {
+ processes_panel(r, s, fr(2));
+ network_panel(r, s, fr(1.2));
+ disk_usage_panel(r, s, fr(1.2));
+ });
+ ui.row(cells(12), gap, [&](UI &r) {
+ temperatures_panel(r, s);
+ sensors_panel(r, s);
+ logs_panel(r, s, fr(1.6));
+ });
+ } else if (ui.width() >= 100) {
+ ui.row(cells(14), gap, [&](UI &r) {
+ cpu_panel(r, s, 2);
+ memory_panel(r, s);
+ system_panel(r, s);
+ });
+ ui.row(fr(), gap, [&](UI &r) {
+ processes_panel(r, s, fr(1.6));
+ network_panel(r, s, fr());
+ });
+ ui.row(cells(10), gap, [&](UI &r) {
+ temperatures_panel(r, s);
+ logs_panel(r, s);
+ });
+ } else {
+ ui.row(cells(10), gap, [&](UI &r) {
+ cpu_panel(r, s, 1);
+ memory_panel(r, s);
+ });
+ ui.col(fr(), 0, [&](UI &c) { processes_panel(c, s); });
+ ui.row(cells(8), gap, [&](UI &r) { network_panel(r, s); });
+ }
+}
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/generated/sample.hpp b/ports/nim/src/hqtui/engine/cpp/demo/generated/sample.hpp
new file mode 100644
index 0000000..fcfdc82
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/generated/sample.hpp
@@ -0,0 +1,3697 @@
+#pragma once
+namespace demo { inline constexpr const char* sample_json=R"hqtuisample({
+ "time": 12.100000000000001,
+ "telemetry": {
+ "sessions": [
+ {
+ "user": "anthony",
+ "tty": "pts/0",
+ "from": "10.0.4.17",
+ "loginAt": "09:14",
+ "idle": ".",
+ "what": "bun run dev"
+ },
+ {
+ "user": "deploy",
+ "tty": "pts/1",
+ "from": "203.0.113.42",
+ "loginAt": "11:02",
+ "idle": "00:04",
+ "what": "tail -f app.log"
+ },
+ {
+ "user": "anthony",
+ "tty": "pts/2",
+ "from": "10.0.4.17",
+ "loginAt": "11:40",
+ "idle": "00:12",
+ "what": "psql"
+ }
+ ],
+ "logins": [
+ {
+ "user": "anthony",
+ "tty": "pts/0",
+ "from": "10.0.4.17",
+ "when": "Aug 20 07:00",
+ "status": "still"
+ },
+ {
+ "user": "deploy",
+ "tty": "pts/1",
+ "from": "203.0.113.42",
+ "when": "Aug 21 08:07",
+ "status": "still"
+ },
+ {
+ "user": "ci",
+ "tty": "pts/2",
+ "from": "198.51.100.9",
+ "when": "Aug 22 09:14",
+ "status": "still"
+ },
+ {
+ "user": "root",
+ "tty": "pts/3",
+ "from": "192.168.1.42",
+ "when": "Aug 23 10:21",
+ "status": "ok"
+ },
+ {
+ "user": "postgres",
+ "tty": "pts/0",
+ "from": "172.16.0.8",
+ "when": "Aug 24 11:28",
+ "status": "ok"
+ },
+ {
+ "user": "anthony",
+ "tty": "pts/1",
+ "from": "10.0.4.17",
+ "when": "Aug 25 12:35",
+ "status": "ok"
+ },
+ {
+ "user": "deploy",
+ "tty": "pts/2",
+ "from": "203.0.113.42",
+ "when": "Aug 26 13:42",
+ "status": "ok"
+ },
+ {
+ "user": "ci",
+ "tty": "pts/3",
+ "from": "198.51.100.9",
+ "when": "Aug 27 14:49",
+ "status": "ok"
+ },
+ {
+ "user": "root",
+ "tty": "pts/0",
+ "from": "192.168.1.42",
+ "when": "Aug 28 15:56",
+ "status": "ok"
+ },
+ {
+ "user": "postgres",
+ "tty": "pts/1",
+ "from": "172.16.0.8",
+ "when": "Aug 20 16:03",
+ "status": "ok"
+ },
+ {
+ "user": "anthony",
+ "tty": "pts/2",
+ "from": "10.0.4.17",
+ "when": "Aug 21 17:10",
+ "status": "ok"
+ },
+ {
+ "user": "deploy",
+ "tty": "pts/3",
+ "from": "203.0.113.42",
+ "when": "Aug 22 18:17",
+ "status": "ok"
+ },
+ {
+ "user": "ci",
+ "tty": "pts/0",
+ "from": "198.51.100.9",
+ "when": "Aug 23 07:24",
+ "status": "ok"
+ },
+ {
+ "user": "root",
+ "tty": "pts/1",
+ "from": "192.168.1.42",
+ "when": "Aug 24 08:31",
+ "status": "ok"
+ },
+ {
+ "user": "postgres",
+ "tty": "pts/2",
+ "from": "172.16.0.8",
+ "when": "Aug 25 09:38",
+ "status": "ok"
+ },
+ {
+ "user": "anthony",
+ "tty": "pts/3",
+ "from": "10.0.4.17",
+ "when": "Aug 26 10:45",
+ "status": "ok"
+ },
+ {
+ "user": "deploy",
+ "tty": "pts/0",
+ "from": "203.0.113.42",
+ "when": "Aug 27 11:52",
+ "status": "ok"
+ },
+ {
+ "user": "ci",
+ "tty": "pts/1",
+ "from": "198.51.100.9",
+ "when": "Aug 28 12:59",
+ "status": "ok"
+ }
+ ],
+ "failedLogins": [
+ {
+ "user": "root",
+ "tty": "ssh",
+ "from": "185.100.20.9",
+ "when": "Aug 30 00:00",
+ "status": "failed"
+ },
+ {
+ "user": "admin",
+ "tty": "ssh",
+ "from": "185.101.23.10",
+ "when": "Aug 30 01:11",
+ "status": "failed"
+ },
+ {
+ "user": "test",
+ "tty": "ssh",
+ "from": "185.102.26.11",
+ "when": "Aug 30 02:22",
+ "status": "failed"
+ },
+ {
+ "user": "oracle",
+ "tty": "ssh",
+ "from": "185.103.29.12",
+ "when": "Aug 30 03:33",
+ "status": "failed"
+ },
+ {
+ "user": "ubuntu",
+ "tty": "ssh",
+ "from": "185.104.32.13",
+ "when": "Aug 30 04:44",
+ "status": "failed"
+ },
+ {
+ "user": "git",
+ "tty": "ssh",
+ "from": "185.105.35.14",
+ "when": "Aug 30 05:55",
+ "status": "failed"
+ }
+ ],
+ "connections": [
+ {
+ "proto": "udp",
+ "local": "10.0.0.7:40000",
+ "remote": "10.0.4.17:443",
+ "state": "ESTAB",
+ "process": "bun/1261"
+ },
+ {
+ "proto": "tcp",
+ "local": "10.0.0.7:40013",
+ "remote": "203.0.113.42:443",
+ "state": "ESTAB",
+ "process": "node/8437"
+ },
+ {
+ "proto": "tcp",
+ "local": "10.0.0.7:40026",
+ "remote": "198.51.100.9:22",
+ "state": "ESTAB",
+ "process": "sshd/1024"
+ },
+ {
+ "proto": "tcp",
+ "local": "10.0.0.7:40039",
+ "remote": "192.168.1.42:5432",
+ "state": "ESTAB",
+ "process": "postgres/2217"
+ },
+ {
+ "proto": "tcp",
+ "local": "10.0.0.7:40052",
+ "remote": "172.16.0.8:6379",
+ "state": "ESTAB",
+ "process": "redis/1555"
+ }
+ ],
+ "listeners": [
+ {
+ "proto": "tcp",
+ "address": "0.0.0.0",
+ "port": "443",
+ "process": "nginx/981"
+ },
+ {
+ "proto": "tcp",
+ "address": "0.0.0.0",
+ "port": "80",
+ "process": "nginx/981"
+ },
+ {
+ "proto": "tcp",
+ "address": "0.0.0.0",
+ "port": "22",
+ "process": "sshd/1024"
+ },
+ {
+ "proto": "tcp",
+ "address": "127.0.0.1",
+ "port": "5432",
+ "process": "postgres/2217"
+ },
+ {
+ "proto": "tcp",
+ "address": "127.0.0.1",
+ "port": "6379",
+ "process": "redis/1555"
+ },
+ {
+ "proto": "tcp",
+ "address": "0.0.0.0",
+ "port": "3000",
+ "process": "bun/1261"
+ },
+ {
+ "proto": "udp",
+ "address": "0.0.0.0",
+ "port": "53",
+ "process": "resolved/712"
+ }
+ ],
+ "services": [
+ {
+ "name": "nginx",
+ "active": "active",
+ "sub": "running",
+ "description": "A high performance web server"
+ },
+ {
+ "name": "postgresql",
+ "active": "active",
+ "sub": "running",
+ "description": "PostgreSQL RDBMS"
+ },
+ {
+ "name": "redis-server",
+ "active": "active",
+ "sub": "running",
+ "description": "Advanced key-value store"
+ },
+ {
+ "name": "ssh",
+ "active": "active",
+ "sub": "running",
+ "description": "OpenBSD Secure Shell server"
+ },
+ {
+ "name": "cron",
+ "active": "active",
+ "sub": "running",
+ "description": "Regular background program processing"
+ },
+ {
+ "name": "docker",
+ "active": "active",
+ "sub": "running",
+ "description": "Docker Application Container Engine"
+ },
+ {
+ "name": "systemd-resolved",
+ "active": "active",
+ "sub": "running",
+ "description": "Network Name Resolution"
+ },
+ {
+ "name": "backup",
+ "active": "failed",
+ "sub": "failed",
+ "description": "Nightly backup job"
+ },
+ {
+ "name": "telemetry-agent",
+ "active": "inactive",
+ "sub": "dead",
+ "description": "Vendor telemetry collector"
+ }
+ ],
+ "containers": [
+ {
+ "id": "9f2c1a4b7e33",
+ "name": "api",
+ "image": "hqtui/api:1.4.2",
+ "status": "Up 2 hours",
+ "cpu": "12.4%",
+ "memory": "184 MiB"
+ },
+ {
+ "id": "3ab7f0912cd5",
+ "name": "worker",
+ "image": "hqtui/worker:1.4.2",
+ "status": "Up 2 hours",
+ "cpu": "6.1%",
+ "memory": "96 MiB"
+ },
+ {
+ "id": "77e10cc4a9b2",
+ "name": "postgres",
+ "image": "postgres:16",
+ "status": "Up 5 days",
+ "cpu": "3.8%",
+ "memory": "412 MiB"
+ },
+ {
+ "id": "1cd8e5b60f47",
+ "name": "redis",
+ "image": "redis:7-alpine",
+ "status": "Up 5 days",
+ "cpu": "0.7%",
+ "memory": "34 MiB"
+ }
+ ],
+ "interfaces": [
+ {
+ "name": "eth0",
+ "ip": "10.0.0.7",
+ "mac": "ac:de:48:00:11:22",
+ "state": "up",
+ "mtu": 1500,
+ "rxRate": 6201653.797448864,
+ "txRate": 2392717.5349607365,
+ "rxTotal": 111298266.09595959,
+ "txTotal": 30485874.58796266,
+ "rxHistory": [
+ 8942898.330648236,
+ 7819426.267671371,
+ 7152002.110760242,
+ 10160910.834934184,
+ 8613752.40919955,
+ 8412521.468347166,
+ 7661161.305374499,
+ 9672737.25724457,
+ 8407198.568981338,
+ 8067683.912462631,
+ 8028790.493543923,
+ 8232778.164100498,
+ 11148599.842076652,
+ 7368141.799049832,
+ 8354982.723369429,
+ 8376578.777089316,
+ 9037252.058889844,
+ 9966464.85838403,
+ 11106984.303399766,
+ 8266374.053213615,
+ 11116245.017054472,
+ 10862688.10564087,
+ 9118295.29047558,
+ 8850074.029993163,
+ 10351763.846631981,
+ 9761925.496383209,
+ 11811617.561797697,
+ 10674856.929594772,
+ 11359117.548118401,
+ 8721780.567627512,
+ 10735508.579914412,
+ 11321728.205507148,
+ 8989944.797068622,
+ 12081629.921554571,
+ 11238438.377412695,
+ 10362430.608474862,
+ 12172556.021251142,
+ 11439244.48583351,
+ 10095463.822241835,
+ 9341405.919821678,
+ 12550778.050317846,
+ 12588006.116090668,
+ 9107496.143747594,
+ 11321669.163912598,
+ 12356536.97051227,
+ 11247532.379102632,
+ 10234033.244196337,
+ 10001163.405604765,
+ 9411000.874807369,
+ 12166709.645224255,
+ 9338884.397517465,
+ 12343288.80133948,
+ 11330510.611898525,
+ 9331733.259774461,
+ 10195461.423021518,
+ 9179035.034397695,
+ 12539500.394477073,
+ 11013196.270436268,
+ 12386394.123113785,
+ 11515870.558950065,
+ 11452690.678991057,
+ 9752290.821706,
+ 11118393.51292545,
+ 9629225.633434977,
+ 9070298.262343401,
+ 11816689.350646047,
+ 9931139.505615719,
+ 11474138.081667783,
+ 11517220.594982445,
+ 11239270.779955266,
+ 8957322.658762204,
+ 10569101.895754693,
+ 10806247.56156728,
+ 11161741.1700801,
+ 11224246.467342712,
+ 10869534.766162924,
+ 9719257.0272072,
+ 8503589.520940784,
+ 8433683.211692097,
+ 8534932.7647757,
+ 8519063.841020243,
+ 7812720.921586106,
+ 8589409.384149145,
+ 10449224.636983464,
+ 7864211.659600998,
+ 7422245.456163474,
+ 8971490.62661733,
+ 7690825.0815702835,
+ 7911474.736168355,
+ 10321692.671227539,
+ 10044894.50434417,
+ 8219564.192396467,
+ 6411967.8325697305,
+ 9242446.016807366,
+ 8017615.685666224,
+ 6447482.184772285,
+ 9423329.752836764,
+ 6525378.726024627,
+ 6177580.807290457,
+ 9195550.648344994,
+ 6294429.508879637,
+ 6459944.920622017,
+ 6179395.187328516,
+ 7943329.750849214,
+ 5503781.385548246,
+ 5822751.926399035,
+ 7612304.483119585,
+ 5669287.4563652,
+ 5954866.025150514,
+ 6580717.289408894,
+ 7761504.26167937,
+ 7934963.268333027,
+ 5992343.5840513,
+ 6795409.301716011,
+ 4761154.42799202,
+ 6675596.415724957,
+ 5882715.62872173,
+ 6509177.373586752,
+ 7694075.972992238,
+ 6351319.821403307,
+ 6201653.797448864
+ ],
+ "txHistory": [
+ 2270632.9930338105,
+ 3150641.8510913076,
+ 3164730.761560943,
+ 2269910.7380544287,
+ 3326303.4198198216,
+ 3548078.4943135493,
+ 2940730.285526083,
+ 2226467.4652782707,
+ 3512532.751237719,
+ 2839177.894865218,
+ 3022547.442720748,
+ 2535888.041943365,
+ 2950196.0500888703,
+ 2989585.1002052985,
+ 3049225.4719804535,
+ 3317580.2297889846,
+ 3896097.5718226256,
+ 3132646.647683994,
+ 3025640.5216478026,
+ 2862570.3116606055,
+ 3496281.379721391,
+ 3003315.423898949,
+ 2765776.8839707216,
+ 3270525.0608555437,
+ 4175502.077289683,
+ 2801601.2096684305,
+ 2822761.80703398,
+ 2950021.4721664013,
+ 2963836.315814588,
+ 2834574.931927112,
+ 3680300.8242428917,
+ 3873467.8786266944,
+ 3942811.876660141,
+ 3686122.235908963,
+ 2884176.7133789025,
+ 3845648.6316185743,
+ 2966288.407341269,
+ 3452234.241673578,
+ 2797200.1837434014,
+ 3916631.448979782,
+ 2793521.162653678,
+ 4095819.926857433,
+ 2672287.9885559655,
+ 2779682.733267254,
+ 3988199.7003284725,
+ 3208497.4381271177,
+ 3534670.7697054716,
+ 3345404.047383772,
+ 3694682.5706425295,
+ 2519403.9286099123,
+ 2434724.161868878,
+ 2744270.5661096433,
+ 2942483.2277822364,
+ 3459163.076447485,
+ 3338458.2722381875,
+ 2784962.0161035913,
+ 3052168.500407206,
+ 2939557.668837717,
+ 2546121.318622807,
+ 3151917.2988432427,
+ 3371087.823512186,
+ 2615322.8481060355,
+ 2159863.789404909,
+ 3138629.563349888,
+ 2587527.795207422,
+ 2357852.9688226827,
+ 1775800.3229938517,
+ 2435711.9791235216,
+ 2115398.948020052,
+ 1681907.714551259,
+ 2421496.312632334,
+ 1490352.6366774892,
+ 2097650.480501896,
+ 2300950.948931084,
+ 1810919.2242014692,
+ 1470770.450868059,
+ 1732983.4829200034,
+ 1603862.7337686024,
+ 2521362.764177108,
+ 1130659.3384177552,
+ 1612502.267577083,
+ 1505268.458093749,
+ 2377356.753971408,
+ 1832337.626558193,
+ 1145606.2450167271,
+ 1911637.5800808626,
+ 1565833.7575490489,
+ 1034997.1268183442,
+ 1248533.3511318383,
+ 1925356.7925359532,
+ 2097216.1302972,
+ 2019900.31005462,
+ 1154721.856164508,
+ 1086611.2687048507,
+ 1947037.5739089497,
+ 1272180.6831931304,
+ 1605094.9402053093,
+ 2130099.1620423123,
+ 986006.5740852527,
+ 1796545.8806793436,
+ 1551106.861751629,
+ 1691539.575123638,
+ 1439703.8332848244,
+ 1674368.6627557043,
+ 2314919.4026194653,
+ 1958650.5971203498,
+ 1465769.6205513012,
+ 2385520.5891274,
+ 2170496.8736131117,
+ 1470967.8854481257,
+ 2600853.1853134623,
+ 2621933.7255653236,
+ 1495342.576051169,
+ 2569516.7226863867,
+ 1757817.2702807086,
+ 2397008.187269696,
+ 1522079.6459215325,
+ 1687269.6498447524,
+ 2544941.2104056985,
+ 1885006.382837907,
+ 2392717.5349607365
+ ],
+ "errors": 0,
+ "drops": 0
+ },
+ {
+ "name": "wg0",
+ "ip": "100.84.2.19",
+ "mac": "00:00:00:00:00:00",
+ "state": "up",
+ "mtu": 1280,
+ "rxRate": 1038566.2146997491,
+ "txRate": 715799.391867375,
+ "rxTotal": 24243920.364585694,
+ "txTotal": 7725853.379705345,
+ "rxHistory": [
+ 2667768.9212758443,
+ 2455714.341069919,
+ 2784825.1895196205,
+ 2300287.368675457,
+ 2687051.817838733,
+ 2873786.201611447,
+ 2918328.612344429,
+ 2408319.5832559876,
+ 2461854.5024702232,
+ 2283179.8470750744,
+ 2854034.898888191,
+ 2688878.8017943385,
+ 2952143.832915581,
+ 2324454.4325359324,
+ 2335906.4411002398,
+ 3163971.1231078766,
+ 2831709.153600971,
+ 2932039.014821898,
+ 2305975.5163223622,
+ 2773004.4907754017,
+ 2694651.025374314,
+ 2481944.200980716,
+ 2329529.414249416,
+ 2330390.3703185953,
+ 2889881.701412787,
+ 2463752.398359143,
+ 2367624.626638026,
+ 3101200.6368227853,
+ 3110976.5175578827,
+ 2821383.5512089874,
+ 2647328.727032909,
+ 2710225.499253036,
+ 2157691.4585936675,
+ 2225336.393423385,
+ 2884549.5089033227,
+ 2985120.3556182007,
+ 2390729.907005625,
+ 2853604.741775856,
+ 2106692.602407843,
+ 2883062.0482794642,
+ 2903712.305886638,
+ 2465138.8199044163,
+ 2425387.970254012,
+ 2204355.995864072,
+ 1996649.7502452973,
+ 2226044.1998604666,
+ 2401447.8671174212,
+ 2036249.1816660026,
+ 2705789.205562428,
+ 2549319.105354981,
+ 2571101.522638817,
+ 2593491.7586266124,
+ 2629918.7148034154,
+ 2333326.5856142067,
+ 2412584.70008663,
+ 1889703.598636361,
+ 2421804.89060073,
+ 2135934.8928439952,
+ 2386260.2835854813,
+ 2391462.4118644954,
+ 2230283.724838422,
+ 2142649.4837340126,
+ 1649396.476096648,
+ 2355761.096884561,
+ 1822904.2888754657,
+ 2012874.3972826977,
+ 1644994.8619036973,
+ 2185951.515932679,
+ 1867084.5818536934,
+ 2123228.7019580454,
+ 2211946.9053353756,
+ 1531556.6018708562,
+ 1425359.540073886,
+ 1401895.0872845717,
+ 1503886.2465451986,
+ 1387935.5744345146,
+ 1547807.4596142862,
+ 1394672.7461130102,
+ 1671762.9706302525,
+ 1528083.3818386872,
+ 1431159.964278196,
+ 1664943.1099912398,
+ 1088756.9305426301,
+ 1090094.9285018994,
+ 1353028.0517548854,
+ 1331388.541196561,
+ 1537397.4587435315,
+ 1366022.8491887844,
+ 1246584.0566985449,
+ 1013761.4209300235,
+ 1193107.3044634587,
+ 1387105.7706621275,
+ 934396.239157009,
+ 1284340.179984821,
+ 1602934.4758260618,
+ 1448240.61685318,
+ 1743490.6813573055,
+ 1639753.1207846862,
+ 1451965.2565857198,
+ 1361857.4758640593,
+ 1061941.8117883517,
+ 916236.1075587256,
+ 1287337.723423187,
+ 1421680.5833876175,
+ 1347016.030766543,
+ 1409139.9978568456,
+ 1639104.5845630178,
+ 1387928.5703410022,
+ 1292394.874038278,
+ 1207655.0334842189,
+ 1010497.6773004284,
+ 798626.274823086,
+ 1738711.4329253251,
+ 1606186.510462564,
+ 1352759.9765746193,
+ 1285168.103892331,
+ 865729.3899873159,
+ 1070403.0400552517,
+ 1267861.230181158,
+ 1505300.8640501588,
+ 1038566.2146997491
+ ],
+ "txHistory": [
+ 868287.9013950664,
+ 800860.8021900501,
+ 732391.4898260867,
+ 850944.171870533,
+ 946638.0365972437,
+ 809045.5377218283,
+ 732069.0130645413,
+ 974547.9069797129,
+ 955095.0547052072,
+ 730063.1101934921,
+ 873968.8156676167,
+ 818383.9108275803,
+ 937044.1173398688,
+ 800224.5471165162,
+ 967977.6455652406,
+ 814328.5955715068,
+ 1038295.8345309992,
+ 1054071.981728739,
+ 1031008.2776729891,
+ 884708.9011893407,
+ 812681.229277598,
+ 855946.6540847082,
+ 682923.5085077522,
+ 710165.1705241965,
+ 674876.7219150782,
+ 946309.9926975413,
+ 977205.416886843,
+ 665663.91466298,
+ 862564.8580206834,
+ 813623.180484486,
+ 728840.1628034097,
+ 686689.376703948,
+ 613407.3042399962,
+ 602045.8081837683,
+ 636845.190562522,
+ 832144.0866721765,
+ 627894.1347048567,
+ 571850.3561432003,
+ 688565.7657772158,
+ 679114.2506832482,
+ 757815.4269971802,
+ 815423.5541062079,
+ 586706.5708623684,
+ 634173.2666389161,
+ 754763.6617037272,
+ 494492.50942386745,
+ 638748.1466508954,
+ 583328.3159771794,
+ 715407.1300022146,
+ 697303.5935887462,
+ 694273.1936660401,
+ 645149.7324181945,
+ 562257.3660043026,
+ 601138.7465905382,
+ 606739.8046226591,
+ 393378.301634307,
+ 538835.3457179379,
+ 378914.7223176727,
+ 484911.19689382846,
+ 296203.1855085664,
+ 406308.32841092563,
+ 389746.81265029346,
+ 579534.7752999465,
+ 498833.7287095899,
+ 449142.93413982994,
+ 269313.912533217,
+ 355608.4468521852,
+ 479892.07667058526,
+ 571683.2275837968,
+ 501449.45155324973,
+ 368856.97992810444,
+ 504679.6879827076,
+ 451391.94112288405,
+ 417454.0401757929,
+ 241585.73441801837,
+ 304245.7064099978,
+ 544842.9471671628,
+ 430872.3899133466,
+ 567502.7655284597,
+ 304820.80365073046,
+ 230960.83959241168,
+ 462062.6711720694,
+ 497476.87402904016,
+ 392364.1454593164,
+ 527955.0325140858,
+ 518558.61427162157,
+ 466112.7687861542,
+ 608237.7349513604,
+ 369901.28109688876,
+ 395410.89068900177,
+ 641826.8014454951,
+ 294480.16552967625,
+ 564718.3558296042,
+ 526463.7726046571,
+ 424545.2897564762,
+ 520360.18512837985,
+ 480887.69400144473,
+ 371486.0017130025,
+ 525434.5381361857,
+ 546444.0370292907,
+ 586895.3065784939,
+ 547912.1479667891,
+ 611679.5786426535,
+ 540633.526924921,
+ 803820.9435367805,
+ 620742.3638842581,
+ 575321.9579580937,
+ 691880.2248260283,
+ 725086.2688669562,
+ 788829.0044416834,
+ 628496.4910794795,
+ 717080.1867580278,
+ 802035.7306501241,
+ 907685.2729565281,
+ 922062.6509600764,
+ 861889.2340430341,
+ 698095.8666233239,
+ 608756.972538148,
+ 821504.9289743097,
+ 909594.8559236407,
+ 715799.391867375
+ ],
+ "errors": 0,
+ "drops": 0
+ }
+ ],
+ "filesystems": [
+ {
+ "mount": "/",
+ "device": "/dev/nvme0n1p2",
+ "type": "ext4",
+ "size": 549755813888,
+ "used": 146028888064,
+ "inodesUsed": 1240000,
+ "inodesTotal": 32000000
+ },
+ {
+ "mount": "/home",
+ "device": "/dev/nvme0n1p3",
+ "type": "ext4",
+ "size": 1052266987520,
+ "used": 667867414528,
+ "inodesUsed": 4100000,
+ "inodesTotal": 61000000
+ },
+ {
+ "mount": "/data",
+ "device": "/dev/sda1",
+ "type": "xfs",
+ "size": 2199023255552,
+ "used": 1121501860331.52,
+ "inodesUsed": 812000,
+ "inodesTotal": 210000000
+ },
+ {
+ "mount": "/boot",
+ "device": "/dev/nvme0n1p1",
+ "type": "vfat",
+ "size": 536870912,
+ "used": 155189248,
+ "inodesUsed": 0,
+ "inodesTotal": 0
+ }
+ ],
+ "kernel": {
+ "contextSwitches": 116159.54467634177,
+ "contextSwitchRate": 11832.41169125606,
+ "interrupts": 65904.5548784161,
+ "interruptRate": 7065.480346110746,
+ "forks": 332.49485456582534,
+ "forkRate": 37.95832767054503,
+ "procsRunning": 7,
+ "procsBlocked": 0,
+ "entropy": 3607,
+ "openFiles": 3452,
+ "maxFiles": 9223372,
+ "bootTime": 0,
+ "pageIn": 22997,
+ "pageOut": 18076,
+ "swapIn": 0,
+ "swapOut": 0
+ },
+ "gpus": [
+ {
+ "name": "NVIDIA RTX A2000",
+ "utilization": 0.4973001578383233,
+ "memoryUsed": 2254857830.4,
+ "memoryTotal": 6442450944,
+ "temperature": 56.92980410379641,
+ "power": 34.5
+ }
+ ],
+ "power": {
+ "battery": 96,
+ "charging": false,
+ "timeRemaining": "Full",
+ "powerDraw": 18.645258292511343,
+ "acConnected": true
+ },
+ "journal": [
+ {
+ "time": "12:00:00",
+ "level": "DEBUG",
+ "unit": "cron",
+ "message": "(anthony) CMD (run-parts /etc/cron.hourly)"
+ },
+ {
+ "time": "12:00:00",
+ "level": "DEBUG",
+ "unit": "sshd",
+ "message": "Received disconnect"
+ },
+ {
+ "time": "12:00:00",
+ "level": "ERROR",
+ "unit": "nginx",
+ "message": "302 requests served"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "unit": "kernel",
+ "message": "EXT4-fs: mounted filesystem with ordered data mode"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "unit": "nginx",
+ "message": "client closed connection while waiting for request"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "unit": "nginx",
+ "message": "upstream timed out"
+ },
+ {
+ "time": "12:00:00",
+ "level": "ERROR",
+ "unit": "cron",
+ "message": "(anthony) CMD (run-parts /etc/cron.hourly)"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "unit": "cron",
+ "message": "(anthony) CMD (run-parts /etc/cron.hourly)"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "unit": "kernel",
+ "message": "TCP: request_sock_TCP: Possible SYN flooding"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "unit": "sshd",
+ "message": "Connection closed by authenticating user root"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "unit": "dockerd",
+ "message": "health check passed"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "unit": "kernel",
+ "message": "EXT4-fs: mounted filesystem with ordered data mode"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "unit": "dockerd",
+ "message": "image pull complete"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "unit": "nginx",
+ "message": "upstream timed out"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "unit": "systemd",
+ "message": "Reloading nginx configuration"
+ }
+ ],
+ "states": {
+ "running": 7,
+ "sleeping": 240,
+ "stopped": 0,
+ "zombie": 0,
+ "total": 247
+ },
+ "connectionHistory": [
+ 63,
+ 58,
+ 67,
+ 75,
+ 66,
+ 76,
+ 67,
+ 72,
+ 60,
+ 62,
+ 69,
+ 57,
+ 75,
+ 72,
+ 69,
+ 66,
+ 78,
+ 57,
+ 76,
+ 61,
+ 54,
+ 59,
+ 61,
+ 66,
+ 71,
+ 63,
+ 62,
+ 60,
+ 72,
+ 69,
+ 69,
+ 73,
+ 62,
+ 55,
+ 60,
+ 60,
+ 67,
+ 82,
+ 61,
+ 66,
+ 67,
+ 50,
+ 69,
+ 65,
+ 67,
+ 56,
+ 63,
+ 65,
+ 66,
+ 60,
+ 61,
+ 68,
+ 63,
+ 61,
+ 59,
+ 59,
+ 60,
+ 57,
+ 65,
+ 70,
+ 61,
+ 61,
+ 75,
+ 76,
+ 63,
+ 56,
+ 64,
+ 65,
+ 51,
+ 63,
+ 61,
+ 65,
+ 67,
+ 62,
+ 79,
+ 64,
+ 55,
+ 70,
+ 66,
+ 77,
+ 71,
+ 63,
+ 64,
+ 57,
+ 69,
+ 77,
+ 57,
+ 59,
+ 77,
+ 70,
+ 58,
+ 66,
+ 57,
+ 79,
+ 81,
+ 56,
+ 64,
+ 69,
+ 68,
+ 75,
+ 80,
+ 68,
+ 63,
+ 62,
+ 75,
+ 90,
+ 59,
+ 84,
+ 71,
+ 72,
+ 72,
+ 68,
+ 64,
+ 71,
+ 78,
+ 75,
+ 93,
+ 69,
+ 83,
+ 61,
+ 86
+ ],
+ "sessionHistory": [
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 4,
+ 4,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3
+ ],
+ "protocols": [
+ {
+ "protocol": "HTTPS",
+ "inbound": 31,
+ "outbound": 10,
+ "total": 41
+ },
+ {
+ "protocol": "ephemeral",
+ "inbound": 2,
+ "outbound": 9,
+ "total": 11
+ },
+ {
+ "protocol": "Postgres",
+ "inbound": 1,
+ "outbound": 8,
+ "total": 9
+ },
+ {
+ "protocol": "HTTP",
+ "inbound": 5,
+ "outbound": 2,
+ "total": 7
+ },
+ {
+ "protocol": "DNS",
+ "inbound": 1,
+ "outbound": 6,
+ "total": 7
+ },
+ {
+ "protocol": "Redis",
+ "inbound": 1,
+ "outbound": 4,
+ "total": 5
+ },
+ {
+ "protocol": "SSH",
+ "inbound": 2,
+ "outbound": 1,
+ "total": 3
+ },
+ {
+ "protocol": "SMTP",
+ "inbound": 0,
+ "outbound": 3,
+ "total": 3
+ }
+ ],
+ "remotes": [
+ {
+ "host": "10.0.4.17",
+ "connections": 12,
+ "protocols": "HTTPS, Postgres"
+ },
+ {
+ "host": "203.0.113.42",
+ "connections": 7,
+ "protocols": "HTTP, Redis"
+ },
+ {
+ "host": "198.51.100.9",
+ "connections": 7,
+ "protocols": "SSH, DNS"
+ },
+ {
+ "host": "192.168.1.42",
+ "connections": 7,
+ "protocols": "Postgres, SMTP"
+ },
+ {
+ "host": "172.16.0.8",
+ "connections": 3,
+ "protocols": "Redis, ephemeral"
+ }
+ ],
+ "inboundConnections": 43,
+ "outboundConnections": 43,
+ "net": {
+ "tcpActiveOpens": 430,
+ "tcpPassiveOpens": 685,
+ "tcpEstablished": 86,
+ "tcpInSegs": 1090422.9482447472,
+ "tcpOutSegs": 997984.8902032655,
+ "tcpRetransSegs": 3071.5726900277377,
+ "tcpInErrs": 0,
+ "tcpOutRsts": 167,
+ "udpInDatagrams": 48917,
+ "udpOutDatagrams": 40561,
+ "udpInErrors": 0,
+ "icmpInMsgs": 136,
+ "icmpOutMsgs": 118,
+ "rates": {
+ "inSegs": 11762.248267226048,
+ "outSegs": 9003.644075129298,
+ "retrans": 38.27017551368568,
+ "passiveOpens": 9.385381579399109,
+ "activeOpens": 5.700383493676782,
+ "udpIn": 553.4054973162711,
+ "udpOut": 449.4523832644336
+ },
+ "retransRatio": 0.004250520699657499
+ },
+ "netInHistory": [
+ 8513.949725526223,
+ 7082.263813203416,
+ 7803.283552943229,
+ 10083.774602565589,
+ 7927.104002165126,
+ 8115.231494965578,
+ 8505.98011425223,
+ 8781.134723621068,
+ 10154.700595394832,
+ 9149.046268201746,
+ 7958.992370103518,
+ 9275.687676134847,
+ 8397.87838804584,
+ 7940.3535318806935,
+ 8946.49550998372,
+ 7729.1750959533865,
+ 9877.702950824685,
+ 9343.689060383575,
+ 7898.750732088336,
+ 8315.659663781591,
+ 8960.249658224779,
+ 8801.192188731111,
+ 8398.715397593787,
+ 9091.515859529474,
+ 8989.345879663444,
+ 8493.159912042764,
+ 9046.97512690054,
+ 8498.35592688208,
+ 8466.881776651871,
+ 8887.307299811786,
+ 10143.443195547388,
+ 8724.078365225732,
+ 7803.627403548946,
+ 8925.0005090097,
+ 9613.322852131792,
+ 9020.54448570069,
+ 7869.7303791587365,
+ 9866.543423782407,
+ 8038.985480889235,
+ 7978.741708399206,
+ 10065.577677999796,
+ 8442.027033169572,
+ 7925.26595273135,
+ 8433.422581666782,
+ 8427.998195803675,
+ 8243.97953875085,
+ 7852.212954958952,
+ 8068.962295514224,
+ 9670.021696515316,
+ 7646.951427358372,
+ 8978.080040811592,
+ 8335.517214820073,
+ 7692.033247345793,
+ 8378.145113430188,
+ 9250.721600722478,
+ 7809.887730877562,
+ 7704.194459096992,
+ 7765.223036916876,
+ 9617.801814205388,
+ 8552.76303736575,
+ 7451.515124704309,
+ 9384.135499067877,
+ 7947.675603685098,
+ 8792.095104582822,
+ 9391.949253381046,
+ 7581.934404342237,
+ 9383.74098321656,
+ 8470.043850622445,
+ 9126.450003398633,
+ 8808.947808558116,
+ 7492.479678451535,
+ 8110.496657889973,
+ 7443.74911850715,
+ 8016.201837376379,
+ 7459.740824505678,
+ 8455.40641102529,
+ 9070.85117633491,
+ 9253.698281711555,
+ 8741.297418633172,
+ 9920.157152275995,
+ 9333.774367233866,
+ 8252.001128145661,
+ 8712.170069929403,
+ 8867.602209001785,
+ 9687.733264847247,
+ 9354.77863305014,
+ 9699.090181376254,
+ 10181.508380250405,
+ 8377.850825465413,
+ 10110.925112397574,
+ 8283.093018820451,
+ 9517.94502185437,
+ 10394.789071265255,
+ 10443.031877185138,
+ 8617.85771525142,
+ 10550.349849988035,
+ 10299.202907987357,
+ 10792.859694856485,
+ 8674.339733503872,
+ 8698.625493442407,
+ 8903.384245944908,
+ 10017.19652992057,
+ 9020.404883892661,
+ 9421.553058317586,
+ 9239.696480934395,
+ 10589.727680103282,
+ 9460.859088893838,
+ 9337.681849944214,
+ 11294.621028520267,
+ 9472.218084427739,
+ 11168.570779708187,
+ 9861.225781661045,
+ 9733.85944167115,
+ 10043.572413362173,
+ 10495.596567387809,
+ 11629.291074480148,
+ 11051.187123219088,
+ 10605.13721955931,
+ 11114.666365841713,
+ 10768.798180101949,
+ 11762.248267226048
+ ],
+ "netOutHistory": [
+ 7673.237749830516,
+ 7449.840342495632,
+ 9056.86788257181,
+ 7926.490850389637,
+ 8495.588608636932,
+ 7229.7012908366705,
+ 7956.765484391486,
+ 7607.287618031791,
+ 8235.360599561787,
+ 7089.134664152338,
+ 7844.936403758076,
+ 8421.719062561237,
+ 6995.501756956468,
+ 7688.4578407094505,
+ 7391.751518271244,
+ 8182.061903472681,
+ 8405.674208765498,
+ 8395.516972840309,
+ 8053.063037078247,
+ 8158.252948520457,
+ 9037.307191775504,
+ 8938.849020474176,
+ 7523.001216550793,
+ 8676.601468400888,
+ 7343.10906249361,
+ 8556.130695972453,
+ 7894.787652988373,
+ 7627.770147683901,
+ 7057.000577376064,
+ 7991.64311171685,
+ 7354.964565551829,
+ 7294.812465570524,
+ 8435.670246850026,
+ 6926.028749290081,
+ 7448.943432341919,
+ 8242.293641583792,
+ 8788.237963253418,
+ 7888.092210268027,
+ 8619.75413048095,
+ 7680.061334184591,
+ 8738.188032744301,
+ 8731.323688435046,
+ 7281.581920630177,
+ 6802.133204583735,
+ 7660.393884257292,
+ 6951.5333179327945,
+ 8269.193264567262,
+ 8869.2917092761,
+ 8409.729627754246,
+ 6832.305126063913,
+ 7712.826568598733,
+ 8003.134205921271,
+ 8489.776809551277,
+ 8444.396621942939,
+ 8729.34161727592,
+ 7659.50102244695,
+ 8360.674171698938,
+ 6956.1087826197345,
+ 6994.468698662971,
+ 8736.196258477066,
+ 8184.976023268693,
+ 7023.57998831277,
+ 6944.669329250661,
+ 7648.753215332867,
+ 6809.637519490116,
+ 6708.281671968651,
+ 7103.59544310001,
+ 8056.551604139821,
+ 8151.0657092622005,
+ 8443.190945619092,
+ 7404.969833931601,
+ 8163.581104006193,
+ 8126.569776116471,
+ 7368.243485666526,
+ 6942.682826097348,
+ 6936.90826175086,
+ 6824.423129747103,
+ 7494.6051481709155,
+ 8032.417829626273,
+ 7661.0578396622395,
+ 7505.489960530891,
+ 8263.437941341504,
+ 7973.664913582713,
+ 8579.078472787907,
+ 7694.595602410702,
+ 8571.61239807551,
+ 9219.926432516715,
+ 8541.127196312875,
+ 7422.086735728393,
+ 7625.955735352272,
+ 8418.484381911447,
+ 8907.301217049744,
+ 8994.269909937233,
+ 8544.416967367137,
+ 9484.191594203923,
+ 7862.832754480348,
+ 9077.12687452851,
+ 8162.818187108041,
+ 9556.470822298768,
+ 8466.069744926197,
+ 8387.013991355681,
+ 9477.215775693163,
+ 9977.775639981926,
+ 8727.981092063534,
+ 10019.264717638234,
+ 8600.6241288169,
+ 8263.45588187682,
+ 10047.729148872797,
+ 10356.408002306955,
+ 9939.840900976871,
+ 8734.396944637152,
+ 9757.597168371401,
+ 10535.37915378542,
+ 10724.998765760522,
+ 8764.576465936174,
+ 9386.136059995166,
+ 10398.138093038877,
+ 10888.461217383072,
+ 10797.301339374673,
+ 10081.864951218868,
+ 9003.644075129298
+ ],
+ "retransHistory": [
+ 12.696417306213089,
+ 30.875512576233568,
+ 30.414946289156653,
+ 34.08012407801684,
+ 35.25219997435574,
+ 23.191382446812554,
+ 27.09737855990577,
+ 9.401346986439512,
+ 41.12909363937521,
+ 10.636725988266168,
+ 12.7474421017658,
+ 25.018885125161514,
+ 29.60878931723247,
+ 8.566041240446257,
+ 9.837386481445924,
+ 25.57284494425814,
+ 14.107691762064361,
+ 12.338560157378357,
+ 10.647463219952549,
+ 37.56073519457399,
+ 18.118514164489557,
+ 31.704744356856995,
+ 34.3626840240305,
+ 31.934543949476044,
+ 29.531492116696665,
+ 29.244025575191,
+ 16.736459274853637,
+ 9.79616754197488,
+ 33.763519406883816,
+ 37.34687967781713,
+ 25.077945354350575,
+ 16.12935375846489,
+ 17.108857976484327,
+ 28.796961434410925,
+ 24.42794031909745,
+ 11.946009521442964,
+ 23.388901173608502,
+ 27.37094491299604,
+ 22.734946193123108,
+ 19.4023313826969,
+ 31.63049964484937,
+ 24.790607497523833,
+ 18.993693345466472,
+ 23.61698889453081,
+ 24.27955611113263,
+ 29.023589286705466,
+ 33.4750151629384,
+ 19.708746507565365,
+ 9.09043583961926,
+ 17.27891109256908,
+ 12.668706282719526,
+ 31.02959664943935,
+ 20.354670658731894,
+ 22.139594458677724,
+ 35.539457259086596,
+ 38.20596827405896,
+ 40.250630208720445,
+ 28.37532503523029,
+ 8.610006049869309,
+ 34.16599722444745,
+ 21.516767360653926,
+ 31.9350562331069,
+ 25.65418607123293,
+ 37.5378067370853,
+ 15.971053042410633,
+ 23.237996880646524,
+ 19.052647697225993,
+ 31.28133565101171,
+ 28.145105494573453,
+ 22.439193920560125,
+ 23.486117928922226,
+ 10.028637723262591,
+ 39.63269344304137,
+ 30.724981224995965,
+ 28.08927914158438,
+ 16.726710860609632,
+ 19.98910444072632,
+ 30.898745945991372,
+ 22.04060972033008,
+ 8.100027938244056,
+ 27.815863162752713,
+ 31.75957406914201,
+ 14.57434088392079,
+ 32.12804279250541,
+ 37.75784493478716,
+ 13.794524221126114,
+ 13.436083658300268,
+ 15.703016442485916,
+ 30.696754626422813,
+ 30.243018929814756,
+ 28.34203137716343,
+ 22.65891222251537,
+ 18.023172634001856,
+ 37.82736311159954,
+ 12.970401837401495,
+ 22.462678503398124,
+ 17.128413661441716,
+ 38.049155772552204,
+ 17.520489782554833,
+ 28.811223020790575,
+ 25.889587462202353,
+ 45.395919551429614,
+ 37.64791149992328,
+ 27.731116343635158,
+ 12.020120967948731,
+ 18.5471146629919,
+ 36.621613405402286,
+ 47.67376396836467,
+ 38.406555034390806,
+ 22.74126162960216,
+ 27.58555727512163,
+ 29.902470779490894,
+ 35.47709672652066,
+ 24.325005901754007,
+ 41.01602110167316,
+ 20.60614895396343,
+ 23.6462369194088,
+ 43.078228587194396,
+ 31.252659212840275,
+ 32.61497644141926,
+ 38.27017551368568
+ ],
+ "ssh": [
+ {
+ "time": "13:49:44",
+ "action": "failed",
+ "user": "admin",
+ "from": "185.172.15.70",
+ "method": "password"
+ },
+ {
+ "time": "13:49:44",
+ "action": "failed",
+ "user": "oracle",
+ "from": "185.141.153.135",
+ "method": "password"
+ },
+ {
+ "time": "13:49:44",
+ "action": "accepted",
+ "user": "anthony",
+ "from": "10.0.4.17",
+ "method": "publickey"
+ },
+ {
+ "time": "13:49:44",
+ "action": "accepted",
+ "user": "ci",
+ "from": "198.51.100.9",
+ "method": "publickey"
+ }
+ ],
+ "http": {
+ "source": "/var/log/nginx/access.log",
+ "requestsPerSecond": 75.02351416898057,
+ "total": 7265,
+ "statusClasses": [
+ {
+ "class": "2xx",
+ "count": 6248
+ },
+ {
+ "class": "3xx",
+ "count": 436
+ },
+ {
+ "class": "4xx",
+ "count": 436
+ },
+ {
+ "class": "5xx",
+ "count": 109
+ },
+ {
+ "class": "1xx",
+ "count": 17
+ }
+ ],
+ "topPaths": [
+ {
+ "path": "/",
+ "count": 2180
+ },
+ {
+ "path": "/api/health",
+ "count": 1308
+ },
+ {
+ "path": "/api/users",
+ "count": 1017
+ },
+ {
+ "path": "/assets/app.js",
+ "count": 872
+ },
+ {
+ "path": "/docs",
+ "count": 654
+ },
+ {
+ "path": "/api/ws",
+ "count": 509
+ },
+ {
+ "path": "/login",
+ "count": 436
+ },
+ {
+ "path": "/api/metrics",
+ "count": 291
+ }
+ ],
+ "topClients": [
+ {
+ "client": "10.0.4.17",
+ "count": 2180
+ },
+ {
+ "client": "203.0.113.42",
+ "count": 1816
+ },
+ {
+ "client": "198.51.100.9",
+ "count": 1453
+ },
+ {
+ "client": "192.168.1.42",
+ "count": 1090
+ },
+ {
+ "client": "172.16.0.8",
+ "count": 726
+ }
+ ],
+ "methods": [
+ {
+ "method": "GET",
+ "count": 5667
+ },
+ {
+ "method": "POST",
+ "count": 1162
+ },
+ {
+ "method": "PUT",
+ "count": 291
+ },
+ {
+ "method": "DELETE",
+ "count": 145
+ }
+ ],
+ "upgrades": 17,
+ "recent": [
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/users",
+ "status": "200",
+ "client": "192.168.1.42",
+ "bytes": 17331
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/metrics",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 7480
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/login",
+ "status": "200",
+ "client": "192.168.1.42",
+ "bytes": 15526
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/api/users",
+ "status": "404",
+ "client": "172.16.0.8",
+ "bytes": 13914
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/assets/app.js",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 19329
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/",
+ "status": "304",
+ "client": "172.16.0.8",
+ "bytes": 19902
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/",
+ "status": "304",
+ "client": "198.51.100.9",
+ "bytes": 880
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/metrics",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 14377
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/login",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 18870
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/users",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 4587
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/health",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 6939
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/api/health",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 992
+ },
+ {
+ "time": "13:49:44",
+ "method": "DELETE",
+ "path": "/api/health",
+ "status": "404",
+ "client": "172.16.0.8",
+ "bytes": 13687
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/api/users",
+ "status": "304",
+ "client": "198.51.100.9",
+ "bytes": 5371
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/metrics",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 21663
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/docs",
+ "status": "200",
+ "client": "192.168.1.42",
+ "bytes": 18577
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/users",
+ "status": "200",
+ "client": "203.0.113.42",
+ "bytes": 10584
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/api/ws",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 21057
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/assets/app.js",
+ "status": "200",
+ "client": "192.168.1.42",
+ "bytes": 20649
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/login",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 2779
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/users",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 3793
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 16255
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/assets/app.js",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 22680
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/users",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 12774
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 2912
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/health",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 17580
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/assets/app.js",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 7604
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/metrics",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 21344
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/ws",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 6905
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/health",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 5448
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/health",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 14582
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/users",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 7234
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/metrics",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 7382
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/assets/app.js",
+ "status": "200",
+ "client": "172.16.0.8",
+ "bytes": 3633
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/api/users",
+ "status": "304",
+ "client": "192.168.1.42",
+ "bytes": 6589
+ },
+ {
+ "time": "13:49:44",
+ "method": "POST",
+ "path": "/docs",
+ "status": "304",
+ "client": "10.0.4.17",
+ "bytes": 5166
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/metrics",
+ "status": "200",
+ "client": "203.0.113.42",
+ "bytes": 18546
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/ws",
+ "status": "200",
+ "client": "198.51.100.9",
+ "bytes": 5823
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/ws",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 13393
+ },
+ {
+ "time": "13:49:44",
+ "method": "GET",
+ "path": "/api/health",
+ "status": "200",
+ "client": "10.0.4.17",
+ "bytes": 14705
+ }
+ ],
+ "history": [
+ 40.436390021623765,
+ 47.70879070697048,
+ 50.841892100647996,
+ 53.04079305727723,
+ 52.11860104933719,
+ 49.50429850300951,
+ 54.38287076547219,
+ 47.11291615872612,
+ 48.75490062280885,
+ 64.92649399369957,
+ 51.276678174515496,
+ 61.8236723704777,
+ 50.61319901522146,
+ 48.36495593249391,
+ 45.9701177310622,
+ 48.50597379015082,
+ 64.35754323575189,
+ 66.492009325431,
+ 70.87250585314258,
+ 52.68691547158225,
+ 64.54027560097853,
+ 63.517682813650666,
+ 70.85903746333838,
+ 54.715656008881794,
+ 63.349372824281765,
+ 51.522365857070376,
+ 62.27363086217653,
+ 63.96117725169689,
+ 71.35048323756462,
+ 48.16528107667171,
+ 63.81596851007158,
+ 47.04819618832378,
+ 49.00981713402804,
+ 66.96883197037971,
+ 67.9610150526199,
+ 53.47055284003308,
+ 56.91773927227673,
+ 50.981768568935756,
+ 67.61023088883043,
+ 50.285427498815864,
+ 63.01600802618209,
+ 49.93120632421554,
+ 54.83862480262195,
+ 59.140809735411395,
+ 49.73554087300515,
+ 52.05814356024155,
+ 47.32096812805897,
+ 53.60007078408499,
+ 47.646535098396214,
+ 43.954878986122885,
+ 52.53705121004512,
+ 51.061524258921665,
+ 46.29803182789158,
+ 68.05977628343658,
+ 61.65761813621003,
+ 52.773693447148155,
+ 60.857947008182514,
+ 65.99496270111491,
+ 58.19588505457204,
+ 57.454333860060764,
+ 55.087333890238725,
+ 50.985320986881575,
+ 47.021977610905694,
+ 61.488571865900155,
+ 63.13366374151643,
+ 58.19468931027307,
+ 60.199830553232474,
+ 66.71264575359407,
+ 54.555522157885505,
+ 52.339882446676455,
+ 65.7500080059293,
+ 57.91865414739831,
+ 65.71576074932884,
+ 46.67950641637434,
+ 53.70941199383459,
+ 67.26631444234346,
+ 52.86779374840245,
+ 66.62847423734351,
+ 59.526086148757074,
+ 55.35002132372329,
+ 52.21357216985061,
+ 69.65207034228496,
+ 67.80084617162964,
+ 67.37533799983797,
+ 49.46310527673381,
+ 69.48435857339635,
+ 69.23129380686277,
+ 55.54287252195427,
+ 63.95291197557332,
+ 52.40938920723164,
+ 65.94200355226596,
+ 70.36515602298988,
+ 71.73516999982965,
+ 54.37300217453563,
+ 55.31650124755595,
+ 73.60092442999091,
+ 58.49817583263001,
+ 56.73028322104097,
+ 62.02400216779057,
+ 54.370674044278566,
+ 65.22404099486593,
+ 76.21024478432989,
+ 57.17902301007006,
+ 78.36178629554834,
+ 73.51587283683389,
+ 63.68508543118871,
+ 69.82875366712116,
+ 71.17245965031299,
+ 74.88571875078213,
+ 81.6725570250365,
+ 58.146924799030906,
+ 60.4663319107603,
+ 66.63799975167215,
+ 69.56295621662575,
+ 82.40708357975906,
+ 61.49180513071526,
+ 74.82437447196973,
+ 84.32502762268928,
+ 81.47078010423807,
+ 84.30236918734536,
+ 75.02351416898057
+ ]
+ },
+ "privileged": false
+ },
+ "cpu": {
+ "total": 0.4838753769323337,
+ "cores": [
+ 0.2859416989002501,
+ 0.2540791499403988,
+ 0.4893248734577541,
+ 0.5989851240754315,
+ 0.5081651587623439,
+ 0.4531481825782587,
+ 0.6001194081242874,
+ 0.5905314122711344,
+ 0.5903672491578601,
+ 0.5950011383356013,
+ 0.5804122560438653,
+ 0.2604288715408184
+ ],
+ "history": [
+ 23.095098489726517,
+ 30.85714228374777,
+ 30.613250708856704,
+ 29.47681393763425,
+ 33.077880359577534,
+ 32.336529266010956,
+ 31.916000969014295,
+ 31.824544826702013,
+ 31.265108637584188,
+ 31.18235297192225,
+ 30.66274609670781,
+ 30.42990500581233,
+ 30.859403984709783,
+ 30.683124637174803,
+ 30.632109882188686,
+ 31.01423345776802,
+ 31.44284106740654,
+ 31.343138969921398,
+ 31.094460916178246,
+ 31.060435066214225,
+ 31.27425356052269,
+ 31.711180494704266,
+ 31.675317870383978,
+ 32.04482670909656,
+ 31.75858168672984,
+ 31.92708607096238,
+ 32.28245918915744,
+ 31.909983645538958,
+ 31.42935468969263,
+ 31.48793245725832,
+ 31.007862542922116,
+ 30.909864812880055,
+ 31.010232985097325,
+ 31.01362671547005,
+ 31.182584952713615,
+ 30.83621649968836,
+ 30.693341697933512,
+ 30.423228167032544,
+ 30.69464883554758,
+ 30.625318091395204,
+ 30.011937289792144,
+ 29.772130496146914,
+ 29.790222223007422,
+ 30.24458811991652,
+ 30.48155348869829,
+ 30.142153586641683,
+ 29.60495821586554,
+ 28.867789489146706,
+ 28.636413753662733,
+ 28.51444880488976,
+ 28.251733604502753,
+ 28.218372250741925,
+ 28.489723965224627,
+ 28.711744148563223,
+ 27.827503752488752,
+ 27.961720653976453,
+ 27.794594863281784,
+ 27.739983153726616,
+ 28.327183083793155,
+ 28.75014556970441,
+ 28.397347735840935,
+ 28.820936821371152,
+ 28.453819450815438,
+ 28.464363562841914,
+ 28.36109206559127,
+ 27.919522182104036,
+ 27.987098893906055,
+ 27.862913332600563,
+ 28.188038347537276,
+ 28.288841957486273,
+ 28.001271903155395,
+ 28.05779115610647,
+ 27.695443021689737,
+ 28.535250159888452,
+ 29.331704607936455,
+ 29.705119896954802,
+ 29.452048146054395,
+ 29.804776245415166,
+ 30.208955108936813,
+ 30.584473777004657,
+ 31.01905517870079,
+ 31.36055861451667,
+ 31.043054914528856,
+ 31.14313022212249,
+ 31.401548270310624,
+ 32.495046560197984,
+ 32.93663975515989,
+ 33.39638510979044,
+ 33.68671057212768,
+ 33.28056204978639,
+ 33.247120004167705,
+ 34.51869302255119,
+ 34.95843510530667,
+ 35.270328118516545,
+ 35.527678847928215,
+ 36.556068678180445,
+ 37.64017307204415,
+ 38.050035874849186,
+ 38.196465195480656,
+ 38.7319297317396,
+ 39.48879952486901,
+ 40.142942994129946,
+ 40.55658085466287,
+ 40.65043288880554,
+ 41.70577419298845,
+ 41.673242332497985,
+ 42.5849335917411,
+ 42.90486903086699,
+ 43.78264969117185,
+ 44.14535591865401,
+ 44.71806569045531,
+ 45.46811258350189,
+ 45.76082021502144,
+ 45.82819526440872,
+ 46.57813932680086,
+ 47.15071992968501,
+ 47.46642882644589,
+ 47.93112964299766,
+ 48.15527013709106,
+ 48.38753769323337
+ ],
+ "load": [
+ 1.3860221806150874,
+ 0.9563440996589672,
+ 0.6748562964062538
+ ],
+ "model": "Intel(R) Core(TM) i7-1260P 12th Gen",
+ "frequencyGhz": 2.7613009046376007
+ },
+ "memory": {
+ "total": 17179869184,
+ "used": 7587867338.071201,
+ "available": 9592001845.928799,
+ "cached": 4253902857.0494933,
+ "buffers": 1202590842.88,
+ "free": 4135508145.999305,
+ "history": [
+ 41.86188151783568,
+ 41.855886761908046,
+ 41.849380492216696,
+ 41.837032963960674,
+ 41.84756119759001,
+ 41.853766825955304,
+ 41.85779082981364,
+ 41.86171662471107,
+ 41.86274112988743,
+ 41.863869369724256,
+ 41.86247491467038,
+ 41.860404346302445,
+ 41.861665350520916,
+ 41.86245650624669,
+ 41.863552708664606,
+ 41.867537338327956,
+ 41.87454466868664,
+ 41.881253484778206,
+ 41.88678462260501,
+ 41.8924846330454,
+ 41.89983187999726,
+ 41.910082381462644,
+ 41.920253954283794,
+ 41.93278247832593,
+ 41.94361527525785,
+ 41.95556530801821,
+ 41.96969759511632,
+ 41.98153558752096,
+ 41.9905447229161,
+ 42.00010131568822,
+ 42.006945725852454,
+ 42.01350560240738,
+ 42.02098496200391,
+ 42.02875556619794,
+ 42.03779580996987,
+ 42.044949809186456,
+ 42.05153238379681,
+ 42.05680820630521,
+ 42.06409134100603,
+ 42.07123642315936,
+ 42.07498555225584,
+ 42.077749455075754,
+ 42.08112425701715,
+ 42.087696518996864,
+ 42.09600141359847,
+ 42.10249355423215,
+ 42.10607623037296,
+ 42.1056944876325,
+ 42.10458070536947,
+ 42.10342733870356,
+ 42.10139124173609,
+ 42.0998920384321,
+ 42.10073051487949,
+ 42.10349313826904,
+ 42.10144546615849,
+ 42.10093807351832,
+ 42.100085194673746,
+ 42.09957848092849,
+ 42.10325077444024,
+ 42.109906968837954,
+ 42.11474256391109,
+ 42.12250615600436,
+ 42.12830633951634,
+ 42.13450647489616,
+ 42.14040288017256,
+ 42.14398014897922,
+ 42.14840830334063,
+ 42.1524933991684,
+ 42.15894761488899,
+ 42.16630571635032,
+ 42.17219140579843,
+ 42.178741966458915,
+ 42.18341006550727,
+ 42.19350186101584,
+ 42.20848514368948,
+ 42.22557618421441,
+ 42.24090974080447,
+ 42.25820754346879,
+ 42.27767912958213,
+ 42.29904288229362,
+ 42.32255759158844,
+ 42.347556252192994,
+ 42.37000962253406,
+ 42.39254942845256,
+ 42.416120380389735,
+ 42.44568036678506,
+ 42.47701745816955,
+ 42.51015065362825,
+ 42.54397255855325,
+ 42.574268812106,
+ 42.60345084287201,
+ 42.63940334816555,
+ 42.676795703092694,
+ 42.71478769387701,
+ 42.75302094684303,
+ 42.796108484026064,
+ 42.84414071787716,
+ 42.89282379205962,
+ 42.940543380491896,
+ 42.98968067439065,
+ 43.041492014489116,
+ 43.095226123852136,
+ 43.149342602557894,
+ 43.201902378273594,
+ 43.258750971784025,
+ 43.31314544217243,
+ 43.37087256224414,
+ 43.42821388151898,
+ 43.48853446730668,
+ 43.548593604670046,
+ 43.60966307134885,
+ 43.67275504674954,
+ 43.735023032128844,
+ 43.79515488289948,
+ 43.85735146044237,
+ 43.920443977427986,
+ 43.982845025633175,
+ 44.045481738247105,
+ 44.10689756761803,
+ 44.167200907082304
+ ],
+ "swapTotal": 2147483648,
+ "swapUsed": 1338470030.7394922
+ },
+ "disks": [
+ {
+ "device": "nvme0n1",
+ "mount": "/",
+ "type": "SSD",
+ "total": 549755813888,
+ "used": 146029056585.75214,
+ "readRate": 23305208.008736372,
+ "writeRate": 19794400.075301528,
+ "readHistory": [
+ 28869695.98196447,
+ 13362792.564556003,
+ 22719380.484893918,
+ 17069921.342656016,
+ 91499910.088256,
+ 23618737.29340732,
+ 30048890.83094895,
+ 24288185.9857589,
+ 35695892.66739786,
+ 22435194.605961442,
+ 20199541.6469872,
+ 39973528.660833836,
+ 72560199.18061793,
+ 17041769.798845053,
+ 22948257.867246866,
+ 23717344.2710191,
+ 21299508.785828948,
+ 12704510.025680065,
+ 21572422.60314524,
+ 22689556.15349114,
+ 24638596.47139907,
+ 24422331.37227595,
+ 28958423.81939292,
+ 28140594.005584717,
+ 15944885.166361928,
+ 14987958.306446671,
+ 16841543.627902865,
+ 35983507.23087788,
+ 25569952.59784162,
+ 14175096.00520134,
+ 33291235.774755478,
+ 22001957.43329823,
+ 21666150.173172355,
+ 29535685.64169109,
+ 25751973.04598987,
+ 28230966.88091755,
+ 19985428.99824679,
+ 18666883.371770382,
+ 12098906.988278031,
+ 34481793.46136749,
+ 47853174.03264344,
+ 20512241.37470126,
+ 27345057.545229793,
+ 26909619.34067309,
+ 26696795.43748498,
+ 13672864.09623921,
+ 33624552.100896835,
+ 19893938.56935203,
+ 35533191.38288498,
+ 61216614.25381899,
+ 29804540.628567338,
+ 32195289.311930537,
+ 17345825.085416436,
+ 40043575.309216976,
+ 26382363.24302852,
+ 34477390.36381245,
+ 25623704.804107547,
+ 30579144.563525915,
+ 22804549.416527152,
+ 20282232.267782092,
+ 19179446.060210466,
+ 33354239.13784325,
+ 13903092.423453927,
+ 27219364.27988112,
+ 16055028.783157468,
+ 27839101.625606418,
+ 21665714.459493756,
+ 23950607.75242746,
+ 14649621.37117982,
+ 32042099.744081497,
+ 16705571.042373776,
+ 22710687.613114715,
+ 21938065.97404182,
+ 28441713.966429234,
+ 34068911.09235585,
+ 97826367.69115925,
+ 35388278.625905514,
+ 29181614.097207785,
+ 16537280.49248457,
+ 14323678.320273757,
+ 20342040.656134486,
+ 12609044.808894396,
+ 17437913.14959526,
+ 35149797.862395644,
+ 35224389.51395452,
+ 32551489.94922638,
+ 19553782.83932805,
+ 20657640.846446157,
+ 25915009.260177612,
+ 12749502.338469028,
+ 26225748.784840107,
+ 29298179.380595684,
+ 31061406.517401338,
+ 24678601.764142513,
+ 25475731.45106435,
+ 34216326.24976337,
+ 17534348.448738456,
+ 16738128.600642085,
+ 15945892.268791795,
+ 31397054.839879274,
+ 28826030.066236854,
+ 20215046.06679082,
+ 28653945.269063115,
+ 31752308.847382665,
+ 26520371.289923787,
+ 32314673.960208893,
+ 30341154.400259256,
+ 19920520.981773734,
+ 33552886.364981532,
+ 27732255.801558495,
+ 25489380.70423901,
+ 34682024.27588403,
+ 32765921.467915177,
+ 12090093.176811934,
+ 35897784.12692249,
+ 33586839.973926544,
+ 15136616.170406342,
+ 23843460.481613874,
+ 31629719.372838736,
+ 23305208.008736372
+ ],
+ "writeHistory": [
+ 6443690.092340112,
+ 15706037.254482506,
+ 12562640.15905559,
+ 9860456.81744814,
+ 33635442.90497899,
+ 8012893.7213122845,
+ 8776167.962551117,
+ 14768775.37988126,
+ 12295753.207281232,
+ 18820515.87201655,
+ 16407962.019592522,
+ 9376837.721318007,
+ 38684423.234164715,
+ 14132024.374455214,
+ 11035102.75952518,
+ 7102480.72206974,
+ 11199828.336238861,
+ 9206720.710024238,
+ 19970185.881704092,
+ 18680202.64863968,
+ 7689268.912598491,
+ 17772577.62797177,
+ 11057206.867486238,
+ 7703328.797444701,
+ 15602776.435241101,
+ 17869653.491154313,
+ 17981688.808500767,
+ 18463852.51455009,
+ 10391966.245919466,
+ 14634004.84755635,
+ 7456274.407580495,
+ 10924875.831082463,
+ 19646951.408162713,
+ 18024177.28252709,
+ 16195269.251838325,
+ 7039010.195285082,
+ 11673018.76232028,
+ 18310244.06388402,
+ 15938933.949396012,
+ 15143369.877934454,
+ 13646195.569857955,
+ 19684450.500681996,
+ 9746749.67251718,
+ 17688411.977738142,
+ 14949244.262874125,
+ 17220318.01700592,
+ 6280004.954561591,
+ 9211970.098167658,
+ 7686373.1662184,
+ 29096119.877174497,
+ 11168808.531984687,
+ 14719382.195770739,
+ 7552774.05180037,
+ 29010397.97835052,
+ 7886539.578661323,
+ 15889711.70246601,
+ 8038710.120543838,
+ 19468726.943284273,
+ 10024006.450027227,
+ 16452584.891617296,
+ 12883128.134384751,
+ 6357466.4602428675,
+ 6392710.183784366,
+ 8497763.145938516,
+ 12229841.793179512,
+ 17340929.815024137,
+ 19803084.11590755,
+ 11535816.67676568,
+ 7910684.633105993,
+ 18342021.075859666,
+ 15028389.478325842,
+ 17565308.347195387,
+ 8464680.605605245,
+ 15754347.16813266,
+ 18223657.94800222,
+ 40471961.00823581,
+ 7683978.999629617,
+ 12968261.116594076,
+ 8924224.971458316,
+ 11953996.99755013,
+ 15260396.467745302,
+ 7007661.508619785,
+ 14798021.47619426,
+ 16944059.12183225,
+ 15768167.300373314,
+ 13159580.795690417,
+ 10733298.452049494,
+ 6065959.706380963,
+ 17519904.607012868,
+ 20035468.750298023,
+ 12046220.526695251,
+ 10578346.979096532,
+ 9304382.766634226,
+ 8366344.533339143,
+ 13411414.838433266,
+ 8078297.1853762865,
+ 16305817.347988484,
+ 7368181.416615844,
+ 10904528.338685632,
+ 17306023.77489209,
+ 13869042.56477952,
+ 15171984.397843478,
+ 13587797.151356936,
+ 9566670.350208879,
+ 16992331.290096045,
+ 12923892.678916454,
+ 19986661.022007465,
+ 7886778.550073504,
+ 14914828.76524329,
+ 15577478.822171686,
+ 10607557.931765914,
+ 17066265.12095332,
+ 15799412.534236906,
+ 18702072.884738445,
+ 8259341.23121202,
+ 19740547.926574945,
+ 8663759.761154652,
+ 13159337.254464626,
+ 14511837.784051893,
+ 19794400.075301528
+ ],
+ "iops": [
+ 1942,
+ 1414
+ ],
+ "temperature": 47.09826967343322
+ },
+ {
+ "device": "sda1",
+ "mount": "/data",
+ "type": "HDD",
+ "total": 2199023255552,
+ "used": 1121501896681.3076,
+ "readRate": 3320122.175384313,
+ "writeRate": 1471346.5730007738,
+ "readHistory": [
+ 4172369.8074929416,
+ 2213106.9749593735,
+ 2666545.920073986,
+ 3000225.519994274,
+ 1634262.756910175,
+ 4285552.43415758,
+ 3960137.808462605,
+ 2366481.3777431846,
+ 3589897.416997701,
+ 4390221.622539684,
+ 2695832.5000014156,
+ 3156009.918311611,
+ 3441200.4947662354,
+ 2170222.307788208,
+ 4108212.816528976,
+ 4236840.480705723,
+ 2875128.2386947423,
+ 4265962.106874213,
+ 2317225.9451821446,
+ 2593909.058952704,
+ 4487254.421459511,
+ 3970185.521990061,
+ 1709992.2085180879,
+ 3825550.532899797,
+ 1886426.4548756182,
+ 1983999.928459525,
+ 3865454.4688761234,
+ 3223383.929114789,
+ 3649666.568264365,
+ 4353497.713571414,
+ 13881243.347655982,
+ 4406689.315102994,
+ 4374768.860405311,
+ 1844096.039654687,
+ 2490424.299845472,
+ 3417932.8023456037,
+ 1838589.1886427999,
+ 4088669.146411121,
+ 1612034.3003422022,
+ 3028927.3287635297,
+ 1728531.5457265824,
+ 2879093.8882157207,
+ 3638516.603037715,
+ 2140546.1893882602,
+ 1577768.1225445122,
+ 3076687.9809089005,
+ 3635670.737363398,
+ 2608161.6247538477,
+ 1930496.0095323622,
+ 2492791.507858783,
+ 4385996.903060004,
+ 2258253.008360043,
+ 2324697.8782117367,
+ 1701120.7721661776,
+ 2026773.5843081027,
+ 2186734.2637851834,
+ 3996485.902927816,
+ 1591723.0392806232,
+ 3233560.698805377,
+ 3979589.986614883,
+ 3564374.8878501356,
+ 1520838.7174643576,
+ 1926713.5122790933,
+ 46123802.3492042,
+ 2734802.4405073375,
+ 3628001.1855997145,
+ 2339093.9575619996,
+ 2584670.529467985,
+ 4397087.176563218,
+ 2444535.034010187,
+ 3089763.3600980043,
+ 2922544.758534059,
+ 4201969.333458692,
+ 1698731.8159081042,
+ 4055744.7778992355,
+ 3992670.9921564907,
+ 2933705.937350169,
+ 2906388.262985274,
+ 4202868.013875559,
+ 2632843.6047770083,
+ 4125825.4456333816,
+ 2067006.4617879689,
+ 2495727.497851476,
+ 4150002.052774653,
+ 3304394.1906653345,
+ 4032050.2433460206,
+ 48168768.30323599,
+ 1969486.6645149887,
+ 1768327.6939205825,
+ 2533307.40169622,
+ 3259481.380227953,
+ 3966343.9630530775,
+ 3787144.0169401467,
+ 3789230.8838199824,
+ 2589099.7988171875,
+ 3088802.519487217,
+ 2238039.400661364,
+ 3142371.8815203756,
+ 49633732.71538876,
+ 2761022.607330233,
+ 32487696.06789574,
+ 4445264.191366732,
+ 3069095.633458346,
+ 1734054.5793995261,
+ 3158187.7707503736,
+ 2255877.8424281627,
+ 3209260.7945669442,
+ 3061645.3906986862,
+ 61131227.1701172,
+ 3391135.9480116516,
+ 1804772.6363874972,
+ 1623668.8869539648,
+ 51900581.40270412,
+ 73432014.07091692,
+ 83404163.0977299,
+ 1932496.8941509724,
+ 3193310.7369113714,
+ 2411281.619919464,
+ 4312619.989039376,
+ 3320122.175384313
+ ],
+ "writeHistory": [
+ 1434945.0026918203,
+ 2051942.7704438565,
+ 784657.0057142526,
+ 2230215.3446059674,
+ 1675908.2031715661,
+ 1368661.5716479719,
+ 1369686.5568682551,
+ 1050834.1791853309,
+ 2228949.5359919965,
+ 1218162.748804316,
+ 1908318.741833791,
+ 2147763.5257691145,
+ 962209.7311355174,
+ 1868831.846611574,
+ 1619444.2341383547,
+ 1052363.2457759231,
+ 1653492.6584735513,
+ 1015477.4981830269,
+ 2150103.279137984,
+ 2095602.7327850459,
+ 2321998.876640573,
+ 2042752.9666386542,
+ 1443651.5239812434,
+ 938202.3942936212,
+ 2000773.321073502,
+ 1018023.0934917927,
+ 1548783.9362863451,
+ 855309.6982464194,
+ 1603271.5112250298,
+ 1572987.9750218242,
+ 6360406.506508589,
+ 1631632.2476416826,
+ 859104.40960899,
+ 1260920.3946590424,
+ 2063272.4016252903,
+ 2262382.9272855073,
+ 752092.2201499343,
+ 1855114.769870415,
+ 980444.8615573347,
+ 769833.0077156425,
+ 770047.3924912512,
+ 2263693.0736526847,
+ 1447082.5780928135,
+ 736193.736763671,
+ 2463992.159049958,
+ 1954787.6165527848,
+ 924973.099892959,
+ 940539.3799580634,
+ 1840577.0708620546,
+ 852370.2737689018,
+ 1195527.6053678244,
+ 1533755.7417154312,
+ 849644.9071727693,
+ 2235301.6691096127,
+ 1400958.870984614,
+ 2303255.2823703736,
+ 966616.9869527221,
+ 2114395.8645220846,
+ 730913.8577710837,
+ 1376662.1820442379,
+ 1193970.2946972102,
+ 1142677.6313502342,
+ 2294462.4040089548,
+ 18783173.6791227,
+ 2243990.282882005,
+ 1458700.1540511847,
+ 1909376.1310726402,
+ 1272045.0623240322,
+ 785006.2695611268,
+ 909436.1212849617,
+ 1836104.021715,
+ 2060353.6569792775,
+ 2483969.936594367,
+ 1322317.7959956229,
+ 1396062.2000321746,
+ 1343578.2436095178,
+ 1568537.9304271191,
+ 1117930.6422639638,
+ 1025709.483101964,
+ 2412415.759898722,
+ 2308808.144442737,
+ 762469.2916218191,
+ 1455019.283182919,
+ 1640308.823743835,
+ 1757490.9292906523,
+ 2348466.3460683078,
+ 19547462.541358545,
+ 2434537.0909944177,
+ 1077820.3459270298,
+ 1996583.1422992048,
+ 837105.0251461565,
+ 1664759.9609848112,
+ 1261935.8010403812,
+ 1813884.7166486082,
+ 889256.3977092505,
+ 2439955.1473371685,
+ 1986208.185162395,
+ 2230393.429873511,
+ 20870435.016816482,
+ 2109309.903373942,
+ 12940428.07652615,
+ 2354791.385103017,
+ 1691842.2786332667,
+ 1093815.9185741097,
+ 1599732.975549996,
+ 1327941.6565597057,
+ 934004.6743210405,
+ 1688899.8115248978,
+ 24880991.20415747,
+ 1603056.4118921757,
+ 1800547.8652473537,
+ 1241758.686332032,
+ 21978064.360776916,
+ 30519908.01010281,
+ 33617383.18827003,
+ 874529.267270118,
+ 2356521.72004804,
+ 1854921.265672892,
+ 1663832.9380005598,
+ 1471346.5730007738
+ ],
+ "iops": [
+ 277,
+ 105
+ ],
+ "temperature": 41.38989277686558
+ }
+ ],
+ "network": {
+ "interface": "en0",
+ "speed": "1 Gb/s",
+ "ip": "192.168.1.42",
+ "mac": "ac:de:48:00:11:22",
+ "downRate": 7891399.331520743,
+ "upRate": 3248038.5673410497,
+ "downHistory": [
+ 7025253.717903149,
+ 8155774.037052375,
+ 7077672.981340021,
+ 8100006.869896598,
+ 7178801.001805875,
+ 9118217.44097301,
+ 7336827.835871135,
+ 10629400.035984848,
+ 9402230.679452717,
+ 10604927.69364883,
+ 7554219.486367154,
+ 7786050.838824329,
+ 9075807.419296775,
+ 7504893.414104206,
+ 9633481.245961636,
+ 9081911.511520736,
+ 9470692.244284954,
+ 10708701.146187054,
+ 10903544.980920086,
+ 8159343.435012636,
+ 8241027.903482816,
+ 11208660.91671394,
+ 11520415.951379733,
+ 10336300.083616331,
+ 9435290.22621462,
+ 8306328.5546635445,
+ 9191239.75899348,
+ 9210980.01701122,
+ 8922021.966883596,
+ 11334766.275318665,
+ 9763709.55025767,
+ 8839104.254333105,
+ 8995500.09486488,
+ 61275985.19275969,
+ 9661484.16623016,
+ 9902805.477272812,
+ 8779635.341230098,
+ 11689816.741633104,
+ 12180333.171237372,
+ 8570329.986898027,
+ 12308887.915022532,
+ 10270312.506237656,
+ 11715656.904563095,
+ 10584688.393093796,
+ 10790465.392442677,
+ 10960420.085194243,
+ 10524032.65295292,
+ 9999135.596075293,
+ 10244528.891895374,
+ 23195724.39004582,
+ 10135335.077177696,
+ 10587181.47859768,
+ 9819249.134377208,
+ 12063709.328501428,
+ 25973251.67396511,
+ 12340900.472933654,
+ 11971353.284190957,
+ 10161713.368705535,
+ 10674605.354705155,
+ 65657597.84591789,
+ 9225177.461000903,
+ 11977100.277427755,
+ 10184919.426456198,
+ 12427949.597814318,
+ 11336809.97304783,
+ 9431676.394310217,
+ 9985457.48564931,
+ 11990432.725512583,
+ 11488888.15395568,
+ 10837185.294258606,
+ 11826946.66407229,
+ 11800569.75395126,
+ 11020519.165718934,
+ 10350266.44095236,
+ 12374875.111146394,
+ 25945999.676867336,
+ 10305429.829687182,
+ 11378173.398592405,
+ 10691787.166441737,
+ 8924958.513920365,
+ 9517088.055364864,
+ 11374894.855767742,
+ 12323214.063903634,
+ 10390262.21847989,
+ 10249545.499129333,
+ 8728546.957897695,
+ 12072986.260060279,
+ 9935770.770613454,
+ 12001704.260609899,
+ 11859235.898630638,
+ 9784533.576170743,
+ 8852142.773003206,
+ 8324099.301974392,
+ 8584526.87829082,
+ 8937788.906124888,
+ 9151376.123102702,
+ 10271945.045942586,
+ 10519436.632021576,
+ 8475911.351731159,
+ 55847897.6871839,
+ 9917657.190729752,
+ 10881740.493898587,
+ 9612312.451714877,
+ 11202555.37705558,
+ 10044539.37910955,
+ 7486665.742287551,
+ 10061132.703376489,
+ 8806366.1228141,
+ 7345961.154687926,
+ 8190537.94160879,
+ 9896787.380893119,
+ 9123573.737345172,
+ 7797309.682702398,
+ 9600980.429275515,
+ 9186893.659106448,
+ 8179873.387415978,
+ 9817668.646690449,
+ 10012912.269399926,
+ 8381661.138098026,
+ 7891399.331520743
+ ],
+ "upHistory": [
+ 2749792.7730349796,
+ 2186300.857132024,
+ 3057963.64488638,
+ 2421436.9057445377,
+ 2491596.670040411,
+ 2066083.8601513624,
+ 2297383.608328673,
+ 3144847.262150008,
+ 2766078.045952854,
+ 2333988.0830698097,
+ 1843592.739101265,
+ 3093005.0027952446,
+ 3124760.581045733,
+ 2908529.5735856267,
+ 3100017.8871882902,
+ 2124555.550576704,
+ 2404679.753356058,
+ 2545417.250388899,
+ 2627775.40745775,
+ 2600887.9436392495,
+ 3116221.2322626607,
+ 2626975.888694112,
+ 2067914.1516513627,
+ 2330019.7956408244,
+ 2566237.791263652,
+ 2917312.1470946134,
+ 2939425.624946569,
+ 2767340.125979031,
+ 3386571.5958579066,
+ 2959477.6212119283,
+ 2405716.5463273637,
+ 2586109.112225018,
+ 2174212.2419287567,
+ 17833274.66139427,
+ 3081420.8116531903,
+ 3095753.7893293775,
+ 2545029.9772268888,
+ 3577001.3476070194,
+ 2812649.437603187,
+ 2437035.620953594,
+ 2551814.9722578563,
+ 3074901.9483456463,
+ 3442916.35615718,
+ 3590510.7719791615,
+ 3352271.758653491,
+ 3542968.546025512,
+ 2481832.8734486955,
+ 2566595.527208781,
+ 3265339.077993569,
+ 6673105.600709838,
+ 2588680.2895890586,
+ 3392794.1591002485,
+ 3575257.2554691667,
+ 2567207.2081094906,
+ 8148081.8757468,
+ 2984303.7481721705,
+ 3546581.88751497,
+ 2977759.6857150425,
+ 2761521.856040421,
+ 18690242.818096157,
+ 2667260.0442900476,
+ 2956203.1711258767,
+ 3524088.362399021,
+ 3321552.3206192674,
+ 2624095.30638679,
+ 3712145.181335554,
+ 3773140.014013581,
+ 2795115.6645983076,
+ 3401187.1511969427,
+ 3318057.892552937,
+ 2819894.5586157427,
+ 2599513.667494444,
+ 3789237.715055664,
+ 2813642.6507586734,
+ 2842081.212625161,
+ 7592921.088959839,
+ 2782749.200533132,
+ 3283249.225408309,
+ 3785472.1723586093,
+ 3317053.002284096,
+ 3622695.740139251,
+ 3705396.132069232,
+ 2871498.597262536,
+ 3890779.0809019,
+ 3452716.7341161235,
+ 4083659.118372198,
+ 2925251.868703734,
+ 2973120.715147059,
+ 2609702.820654114,
+ 3528338.3304564413,
+ 3442459.350587403,
+ 3949627.6095428187,
+ 2640368.0368714696,
+ 3586193.378196718,
+ 2607304.5878690085,
+ 3037891.8189263665,
+ 3077417.1461991533,
+ 3909454.8573102085,
+ 3960419.1649290686,
+ 17537447.533883784,
+ 2668105.652692586,
+ 3807241.2757928986,
+ 3644745.22653316,
+ 2732608.0717954673,
+ 2693841.885261945,
+ 4068870.2211978775,
+ 3380415.210851684,
+ 3507453.997554162,
+ 2931495.4173989464,
+ 3279611.798197654,
+ 2724816.0998861874,
+ 3300110.6583366864,
+ 3339605.0779659715,
+ 2968813.317305172,
+ 3007268.7916498743,
+ 3446088.94347751,
+ 2535654.7330304594,
+ 2673510.563665864,
+ 2885576.378473373,
+ 3248038.5673410497
+ ],
+ "downTotal": 13671914280.978863,
+ "upTotal": 3479195536.054852,
+ "downPeak": 65657597.84591789,
+ "upPeak": 18690242.818096157
+ },
+ "processes": [
+ {
+ "pid": 40344,
+ "name": "ssh",
+ "cpu": 50.021494071464986,
+ "mem": 0.5036076683551074,
+ "rss": 13401679.599609375,
+ "threads": 15,
+ "user": "dev",
+ "command": "ssh deploy@prod",
+ "state": "S"
+ },
+ {
+ "pid": 26848,
+ "name": "docker",
+ "cpu": 31.865783848799765,
+ "mem": 5.059937346726653,
+ "rss": 248470269.2890625,
+ "threads": 2,
+ "user": "root",
+ "command": "dockerd -H unix:///var/run/docker.sock",
+ "state": "S"
+ },
+ {
+ "pid": 13725,
+ "name": "nginx",
+ "cpu": 28.34055506531149,
+ "mem": 3.420652775745839,
+ "rss": 172437430.24804688,
+ "threads": 16,
+ "user": "www-data",
+ "command": "nginx: worker process",
+ "state": "R"
+ },
+ {
+ "pid": 15055,
+ "name": "node",
+ "cpu": 27.762788597960025,
+ "mem": 3.6113079253584126,
+ "rss": 194242417.17773438,
+ "threads": 26,
+ "user": "dev",
+ "command": "node index.js",
+ "state": "R"
+ },
+ {
+ "pid": 28963,
+ "name": "chrome",
+ "cpu": 23.395272554364055,
+ "mem": 2.132857508305461,
+ "rss": 11438956.31640625,
+ "threads": 12,
+ "user": "dev",
+ "command": "chrome --type=renderer",
+ "state": "S"
+ },
+ {
+ "pid": 29751,
+ "name": "python",
+ "cpu": 17.49790712306276,
+ "mem": 6.910427920706572,
+ "rss": 73352427.52929688,
+ "threads": 27,
+ "user": "dev",
+ "command": "python worker.py",
+ "state": "R"
+ },
+ {
+ "pid": 13458,
+ "name": "redis-server",
+ "cpu": 10.037278552539647,
+ "mem": 3.124368785973639,
+ "rss": 28196620.291015625,
+ "threads": 5,
+ "user": "redis",
+ "command": "redis-server *:6379",
+ "state": "R"
+ },
+ {
+ "pid": 27222,
+ "name": "rustc",
+ "cpu": 6.947565608192235,
+ "mem": 4.191203259397298,
+ "rss": 256446053.38867188,
+ "threads": 14,
+ "user": "dev",
+ "command": "rustc --edition 2021 src/main.rs",
+ "state": "R"
+ },
+ {
+ "pid": 16055,
+ "name": "systemd",
+ "cpu": 4.42256407532841,
+ "mem": 5.446574072260411,
+ "rss": 331806683.28125,
+ "threads": 30,
+ "user": "root",
+ "command": "/sbin/init",
+ "state": "R"
+ },
+ {
+ "pid": 44220,
+ "name": "code",
+ "cpu": 3.7363769290968776,
+ "mem": 6.352349482849241,
+ "rss": 37123376.146484375,
+ "threads": 6,
+ "user": "dev",
+ "command": "code --unity-launch",
+ "state": "R"
+ },
+ {
+ "pid": 33905,
+ "name": "postgres",
+ "cpu": 2.37702462868765,
+ "mem": 0.6176048647612333,
+ "rss": 131736989.80078125,
+ "threads": 4,
+ "user": "postgres",
+ "command": "postgres -D /var/lib/postgresql/data",
+ "state": "R"
+ },
+ {
+ "pid": 14107,
+ "name": "bun",
+ "cpu": 2.3552859532646835,
+ "mem": 0.33871206119656566,
+ "rss": 106033327.1015625,
+ "threads": 10,
+ "user": "dev",
+ "command": "bun server.ts",
+ "state": "S"
+ }
+ ],
+ "temperatures": [
+ {
+ "label": "CPU Package",
+ "value": 55.01816847949359,
+ "max": 100
+ },
+ {
+ "label": "CPU Core #1",
+ "value": 46.463604184123504,
+ "max": 100
+ },
+ {
+ "label": "CPU Core #2",
+ "value": 46.172469935128184,
+ "max": 100
+ },
+ {
+ "label": "CPU Core #3",
+ "value": 53.43258825254411,
+ "max": 100
+ },
+ {
+ "label": "CPU Core #4",
+ "value": 54.80641006451574,
+ "max": 100
+ },
+ {
+ "label": "CPU Core #5",
+ "value": 50.654604234103296,
+ "max": 100
+ },
+ {
+ "label": "CPU Core #6",
+ "value": 49.25966466210429,
+ "max": 100
+ },
+ {
+ "label": "GPU Package",
+ "value": 48.07515626086947,
+ "max": 100
+ },
+ {
+ "label": "SSD (nvme0n1)",
+ "value": 47.05614891347823,
+ "max": 85
+ }
+ ],
+ "sensors": [
+ {
+ "label": "Fan Speed 1",
+ "value": "2477 RPM"
+ },
+ {
+ "label": "Fan Speed 2",
+ "value": "2329 RPM"
+ },
+ {
+ "label": "Battery",
+ "value": "97%"
+ },
+ {
+ "label": "CPU Voltage",
+ "value": "0.97 V"
+ },
+ {
+ "label": "CPU Power",
+ "value": "16.6 W"
+ },
+ {
+ "label": "GPU Power",
+ "value": "8.4 W"
+ }
+ ],
+ "system": {
+ "os": "Ubuntu 24.04 LTS",
+ "kernel": "6.8.0-31-generic",
+ "hostname": "devbox",
+ "shell": "bash 5.2.21",
+ "terminal": "hqtui",
+ "uptime": 9266.100000000044,
+ "processCount": 247,
+ "threadCount": 998,
+ "contextSwitches": 57678
+ },
+ "logs": [
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "message": "User authenticated successfully",
+ "meta": "service: auth"
+ },
+ {
+ "time": "12:00:00",
+ "level": "DEBUG",
+ "message": "Response time: 142ms",
+ "meta": "service: api"
+ },
+ {
+ "time": "12:00:00",
+ "level": "ERROR",
+ "message": "Failed to fetch user profile",
+ "meta": "service: api"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "message": "Background job \"cleanup\" completed in 120ms",
+ "meta": "service: job"
+ },
+ {
+ "time": "12:00:00",
+ "level": "DEBUG",
+ "message": "Response time: 142ms",
+ "meta": "service: api"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "message": "Retrying in 2 seconds (attempt 2/3)",
+ "meta": "service: api"
+ },
+ {
+ "time": "12:00:00",
+ "level": "WARN",
+ "message": "Cache miss for key: user:48231",
+ "meta": "service: cache"
+ },
+ {
+ "time": "12:00:00",
+ "level": "ERROR",
+ "message": "Failed to fetch user profile",
+ "meta": "service: api"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "message": "Database connection established",
+ "meta": "service: db"
+ },
+ {
+ "time": "12:00:00",
+ "level": "INFO",
+ "message": "Migration 0042_add_index applied",
+ "meta": "service: db"
+ }
+ ]
+}
+)hqtuisample"; }
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/json.hpp b/ports/nim/src/hqtui/engine/cpp/demo/json.hpp
new file mode 100644
index 0000000..9d4e7f5
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/json.hpp
@@ -0,0 +1,293 @@
+#pragma once
+// Small, bounded JSON value/parser for telemetry and test data. Not a renderer;
+// simulated samples contain measurements, never pre-rendered cells or frames.
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+namespace demo {
+struct Json {
+ using Array = std::vector;
+ using Object = std::map>;
+ std::variant value;
+ Json() = default;
+ Json(double n) : value(n) {}
+ Json(int n) : value(double(n)) {}
+ Json(bool n) : value(n) {}
+ Json(std::string s) : value(std::move(s)) {}
+ Json(const char *s) : value(std::string(s)) {}
+ Json(Array a) : value(std::move(a)) {}
+ Json(Object o) : value(std::move(o)) {}
+ bool null() const { return std::holds_alternative(value); }
+ double n(double fallback = 0) const {
+ auto p = std::get_if(&value);
+ return p ? *p : fallback;
+ }
+ std::string s(std::string fallback = "—") const {
+ if (auto p = std::get_if(&value))
+ return *p;
+ if (auto p = std::get_if(&value)) {
+ char b[80];
+ if (*p == std::floor(*p) && std::abs(*p) < 1e21) {
+ std::snprintf(b, sizeof b, "%.0f", *p);
+ return b;
+ }
+ auto result = std::to_chars(b, b + sizeof b, *p);
+ return std::string(b, result.ptr);
+ }
+ if (auto p = std::get_if(&value))
+ return *p ? "true" : "false";
+ return fallback;
+ }
+ /// A JSON true, or a number the caller wrote instead. Anything else is the
+ /// fallback, so a missing key and an explicit false read the same.
+ bool b(bool fallback = false) const {
+ if (auto p = std::get_if(&value))
+ return *p;
+ if (auto p = std::get_if(&value))
+ return *p != 0;
+ return fallback;
+ }
+ const Array &array() const {
+ static const Array empty;
+ auto p = std::get_if(&value);
+ return p ? *p : empty;
+ }
+ Array &array() {
+ if (!std::holds_alternative(value))
+ value = Array{};
+ return std::get(value);
+ }
+ const Json &operator[](std::string_view key) const {
+ static const Json empty;
+ if (auto p = std::get_if(&value)) {
+ auto it = p->find(key);
+ if (it != p->end())
+ return it->second;
+ }
+ return empty;
+ }
+ Json &operator[](std::string_view key) {
+ if (!std::holds_alternative(value))
+ value = Object{};
+ return std::get(value)[std::string(key)];
+ }
+ const Json &at(std::size_t i) const {
+ static const Json empty;
+ auto &a = array();
+ return i < a.size() ? a[i] : empty;
+ }
+ const Json &path(std::string_view key) const {
+ auto dot = key.find('.');
+ return dot == key.npos
+ ? (*this)[key]
+ : (*this)[key.substr(0, dot)].path(key.substr(dot + 1));
+ }
+ static Json parse(std::string_view source) {
+ if (source.size() > 16 * 1024 * 1024)
+ throw std::runtime_error("JSON input too large");
+ struct Parser {
+ std::string_view s;
+ std::size_t i = 0;
+ [[noreturn]] void fail() {
+ throw std::runtime_error("invalid JSON at byte " + std::to_string(i));
+ }
+ void ws() {
+ while (i < s.size() &&
+ (s[i] == ' ' || s[i] == '\n' || s[i] == '\r' || s[i] == '\t'))
+ i++;
+ }
+ char take() {
+ if (i == s.size())
+ fail();
+ return s[i++];
+ }
+ unsigned hex() {
+ unsigned n = 0;
+ for (int j = 0; j < 4; j++) {
+ char c = take();
+ n *= 16;
+ if (c >= '0' && c <= '9')
+ n += c - '0';
+ else if (c >= 'a' && c <= 'f')
+ n += c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F')
+ n += c - 'A' + 10;
+ else
+ fail();
+ }
+ return n;
+ }
+ std::string string() {
+ if (take() != '"')
+ fail();
+ std::string out;
+ for (;;) {
+ unsigned char c = take();
+ if (c == '"')
+ break;
+ if (c < 32)
+ fail();
+ if (c != '\\') {
+ out += char(c);
+ continue;
+ }
+ switch (take()) {
+ case '"':
+ out += '"';
+ break;
+ case '\\':
+ out += '\\';
+ break;
+ case '/':
+ out += '/';
+ break;
+ case 'b':
+ out += '\b';
+ break;
+ case 'f':
+ out += '\f';
+ break;
+ case 'n':
+ out += '\n';
+ break;
+ case 'r':
+ out += '\r';
+ break;
+ case 't':
+ out += '\t';
+ break;
+ case 'u': {
+ unsigned cp = hex();
+ if (cp >= 0xd800 && cp <= 0xdbff) {
+ if (take() != '\\' || take() != 'u')
+ fail();
+ unsigned lo = hex();
+ if (lo < 0xdc00 || lo > 0xdfff)
+ fail();
+ cp = 0x10000 + ((cp - 0xd800) << 10) + (lo - 0xdc00);
+ } else if (cp >= 0xdc00 && cp <= 0xdfff)
+ fail();
+ out += hqtui::utf8(cp);
+ break;
+ }
+ default:
+ fail();
+ }
+ }
+ return out;
+ }
+ Json parse(int depth = 0) {
+ if (depth > 64)
+ fail();
+ ws();
+ if (i == s.size())
+ fail();
+ char c = s[i];
+ if (c == '"')
+ return Json(string());
+ if (c == '[') {
+ i++;
+ Array a;
+ ws();
+ if (i < s.size() && s[i] == ']') {
+ i++;
+ return a;
+ }
+ for (;;) {
+ a.push_back(parse(depth + 1));
+ ws();
+ char end = take();
+ if (end == ']')
+ return a;
+ if (end != ',')
+ fail();
+ }
+ }
+ if (c == '{') {
+ i++;
+ Object o;
+ ws();
+ if (i < s.size() && s[i] == '}') {
+ i++;
+ return o;
+ }
+ for (;;) {
+ ws();
+ auto key = string();
+ ws();
+ if (take() != ':')
+ fail();
+ auto v = parse(depth + 1);
+ if (!o.emplace(std::move(key), std::move(v)).second)
+ fail();
+ ws();
+ char end = take();
+ if (end == '}')
+ return o;
+ if (end != ',')
+ fail();
+ }
+ }
+ for (auto literal : {"null", "true", "false"})
+ if (s.substr(i, std::char_traits::length(literal)) == literal) {
+ i += std::char_traits::length(literal);
+ return literal[0] == 'n' ? Json() : Json(literal[0] == 't');
+ }
+ std::size_t start = i;
+ if (s[i] == '-')
+ i++;
+ if (i == s.size())
+ fail();
+ if (s[i] == '0')
+ i++;
+ else {
+ if (s[i] < '1' || s[i] > '9')
+ fail();
+ while (i < s.size() && s[i] >= '0' && s[i] <= '9')
+ i++;
+ }
+ if (i < s.size() && s[i] == '.') {
+ i++;
+ auto begin = i;
+ while (i < s.size() && s[i] >= '0' && s[i] <= '9')
+ i++;
+ if (i == begin)
+ fail();
+ }
+ if (i < s.size() && (s[i] == 'e' || s[i] == 'E')) {
+ i++;
+ if (i < s.size() && (s[i] == '+' || s[i] == '-'))
+ i++;
+ auto begin = i;
+ while (i < s.size() && s[i] >= '0' && s[i] <= '9')
+ i++;
+ if (i == begin)
+ fail();
+ }
+ double n = std::strtod(std::string(s.substr(start, i - start)).c_str(),
+ nullptr);
+ if (!std::isfinite(n))
+ fail();
+ return n;
+ }
+ } p{source};
+ Json result = p.parse();
+ p.ws();
+ if (p.i != source.size())
+ p.fail();
+ return result;
+ }
+};
+inline std::vector numbers(const Json &j) {
+ std::vector result;
+ for (auto &v : j.array())
+ result.push_back(v.n());
+ return result;
+}
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/main.cpp b/ports/nim/src/hqtui/engine/cpp/demo/main.cpp
new file mode 100644
index 0000000..ce9602c
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/main.cpp
@@ -0,0 +1,704 @@
+#include "collect.hpp"
+#include "model.hpp"
+#include "sample.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+namespace demo {
+static volatile std::sig_atomic_t interrupted = 0;
+static void signal_handler(int value) { interrupted = value; }
+static bool write_all(std::string_view bytes) {
+ while (!bytes.empty()) {
+ ssize_t n = ::write(STDOUT_FILENO, bytes.data(), bytes.size());
+ if (n < 0) {
+ if (errno == EINTR && !interrupted)
+ continue;
+ return false;
+ }
+ if (!n)
+ return false;
+ bytes.remove_prefix(std::size_t(n));
+ }
+ return true;
+}
+class Terminal {
+ termios before_{};
+ bool active_ = false;
+ using Handler = void (*)(int);
+ Handler old_int_, old_term_, old_hup_, old_pipe_;
+
+public:
+ Terminal() {
+ if (!isatty(0) || !isatty(1) || tcgetattr(0, &before_))
+ throw std::runtime_error("An interactive terminal is required. Use "
+ "--snapshot for headless output.");
+ auto raw = before_;
+ cfmakeraw(&raw);
+ raw.c_cc[VMIN] = 0;
+ raw.c_cc[VTIME] = 0;
+ if (tcsetattr(0, TCSANOW, &raw))
+ throw std::runtime_error("Cannot acquire terminal");
+ active_ = true;
+ old_int_ = std::signal(SIGINT, signal_handler);
+ old_term_ = std::signal(SIGTERM, signal_handler);
+ old_hup_ = std::signal(SIGHUP, signal_handler);
+ old_pipe_ = std::signal(SIGPIPE, signal_handler);
+ write_all("\x1b[?1049h\x1b[?25l\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?"
+ "2004h\x1b[?1004h");
+ }
+ ~Terminal() { restore(); }
+ void restore() {
+ if (!active_)
+ return;
+ write_all("\x1b[0m\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?2004l\x1b[?"
+ "1004l\x1b[?25h\x1b[?1049l");
+ int restored;
+ do {
+ restored = tcsetattr(0, TCSANOW, &before_);
+ } while (restored < 0 && errno == EINTR);
+ if (restored < 0)
+ std::cerr << "Cannot restore terminal: " << std::strerror(errno) << "\n";
+ std::signal(SIGINT, old_int_);
+ std::signal(SIGTERM, old_term_);
+ std::signal(SIGHUP, old_hup_);
+ std::signal(SIGPIPE, old_pipe_);
+ active_ = false;
+ }
+ std::pair size() {
+ winsize size{};
+ if (ioctl(1, TIOCGWINSZ, &size))
+ return {120, 40};
+ return {std::clamp(int(size.ws_col), 1, 500),
+ std::clamp(int(size.ws_row), 1, 200)};
+ }
+};
+class Worker {
+ Collector collector_;
+ std::mutex mutex_;
+ std::condition_variable wake_;
+ std::thread thread_;
+ std::optional ready_;
+ std::string error_;
+
+public:
+ std::atomic stop{false};
+ Worker(const Json &shape, double interval) : collector_(shape) {
+ thread_ = std::thread([this, interval] {
+ while (!stop) {
+ try {
+ collector_.refresh(&stop);
+ if (stop)
+ break;
+ std::lock_guard lock(mutex_);
+ ready_ = collector_.data;
+ } catch (const std::exception &e) {
+ std::lock_guard lock(mutex_);
+ error_ = e.what();
+ }
+ std::unique_lock lock(mutex_);
+ wake_.wait_for(lock, std::chrono::duration(interval),
+ [&] { return stop.load(); });
+ }
+ });
+ }
+ ~Worker() {
+ stop = true;
+ wake_.notify_all();
+ if (thread_.joinable())
+ thread_.join();
+ }
+ bool update(State &s) {
+ std::lock_guard lock(mutex_);
+ if (!error_.empty())
+ throw std::runtime_error(error_);
+ if (!ready_ || s.paused)
+ return false;
+ s.data = std::move(*ready_);
+ ready_.reset();
+ return true;
+ }
+};
+static void body(UI &ui, State &s) {
+ if (s.screen == 0)
+ dashboard(ui, s);
+ else if (s.screen < 5)
+ telemetry(ui, s);
+ else
+ showcase(ui, s);
+}
+void render(UI &ui, State &s, bool body_only) {
+ if (body_only) {
+ body(ui, s);
+ return;
+ }
+ auto t = ui.t();
+ auto regions = ui.regions;
+ ui.row(cells(1), 0, [&, t, regions](UI &r) {
+ r.text(" hqtui.com", t.title, HQ_LEFT, cells(12), HQ_BOLD);
+ r.draw([&, t, regions](Surface surface) {
+ int x = 0;
+ for (int i = 0; i < 10; i++) {
+ std::string label =
+ " " + std::to_string((i + 1) % 10) + " " + screens[i] + " ";
+ int w = int(width(label));
+ if (x >= surface.rect().width)
+ break;
+ bool active = i == s.screen;
+ text(surface, x, 0, label, active ? t.background : t.muted,
+ active ? HQ_BOLD : 0,
+ active ? std::optional(t.primary) : std::nullopt);
+ if (regions)
+ regions->push_back(
+ {hq_intersect(surface.rect(),
+ {surface.rect().x + x, surface.rect().y, w, 1}),
+ "tab:" + std::to_string(i), 0});
+ x += w;
+ }
+ });
+ r.text(std::string(s.paused ? "paused" : "live") + " " +
+ (s.real ? "real" : "simulated") + " " + fixed(s.fps) + "fps " +
+ s.clock + " ",
+ s.paused ? t.warning : t.success, HQ_RIGHT, cells(38));
+ });
+ ui.spacer(cells(1));
+ ui.col(cells(std::max(0, ui.height() - 4)), 0, [&](UI &p) { body(p, s); });
+ ui.spacer(cells(1));
+ ui.draw(
+ [&, t](Surface surface) {
+ int x = 0;
+ std::vector> items = {
+ {"F1", "Help"},
+ {"F2", "Theme (" + std::string(themes[s.theme_index]) + ")"},
+ {"F3", s.filtering ? "Filter: " + s.filter + "_" : "Filter"},
+ {"c", "Collapse"},
+ {"F6", "Sort: " + std::vector{"cpu", "mem", "pid",
+ "name"}[s.sort]},
+ {"^K", "Palette"},
+ {"Tab", "Screen"},
+ {"q", "Quit"}};
+ for (auto &item : items) {
+ text(surface, x, 0, " " + item.first + " ", t.background, HQ_BOLD,
+ t.primary);
+ x += int(width(item.first)) + 2;
+ text(surface, x, 0, " " + item.second + " ", t.muted);
+ x += int(width(item.second)) + 2;
+ }
+ auto right = fixed(s.render_ms, 2) + "ms " +
+ std::to_string(s.changed_cells) + " cells " +
+ std::to_string(s.output_bytes) + "B";
+ if (surface.rect().width - int(width(right)) > x)
+ text(surface, surface.rect().width - int(width(right)), 0, right,
+ t.muted);
+ },
+ cells(1));
+}
+static void overlay(Buffer &frame, State &s) {
+ if (!s.help && !s.modal && !s.palette && !s.filtering)
+ return;
+ auto t = hq_theme_named(themes[s.theme_index]);
+ auto surface = frame.surface(t);
+ int w = std::min(frame.width() - 2, 72),
+ h = std::min(frame.height() - 2, s.palette ? 16 : 11);
+ if (w < 4 || h < 3)
+ return;
+ hq_box_options box{};
+ box.title = s.help ? "hqtui — Help"
+ : s.modal ? "Read-only Demo"
+ : s.palette ? "Command Palette"
+ : "Filter Processes";
+ auto p =
+ surface.sub({(frame.width() - w) / 2, (frame.height() - h) / 2, w, h})
+ .box(box);
+ std::vector lines;
+ if (s.help)
+ lines = {"1–9 / 0 / Tab: change screen",
+ "F2: theme · F3: filter · F6: sort",
+ "Ctrl+K: command palette · Space: pause",
+ "Arrows / PgUp / PgDn / Home / End: scroll",
+ "Mouse: select a pane, click controls, scroll",
+ "e: edit text · Esc: finish · q / Ctrl+C: quit",
+ "Any key closes help"};
+ else if (s.modal)
+ lines = {"No process will be killed and no service changed.",
+ "Press any key to close."};
+ else if (s.filtering)
+ lines = {"Filter: " + s.filter + "_", "Enter or Esc to finish"};
+ else {
+ lines.push_back("> " + s.query + "_");
+ for (int i = 0; i < 10; i++)
+ lines.push_back(std::string(i == s.palette_index ? "▸ " : " ") +
+ screens[i]);
+ }
+ for (int y = 0; y < p.rect().height && y < int(lines.size()); y++)
+ text(p, 1, y, fit(lines[y], std::max(0, p.rect().width - 2)),
+ t->foreground);
+}
+static void activate(State &s, const std::string &id) {
+ if (id.rfind("tab:", 0) == 0) {
+ s.screen = std::stoi(id.substr(4));
+ return;
+ }
+ if (id == "checkbox")
+ s.checkbox = !s.checkbox;
+ else if (id == "toggle")
+ s.toggle = !s.toggle;
+ else if (id == "select")
+ s.select_open = !s.select_open;
+ else if (id.rfind("button:", 0) == 0)
+ s.modal = true;
+ else
+ s.focused = id;
+}
+static bool key(State &s, std::string key) {
+ s.last_key = key.size() == 1 && static_cast(key[0]) < 32
+ ? "Ctrl+" + std::string(1, char(key[0] + 64))
+ : key;
+ s.key_log.push_back(s.last_key);
+ if (s.key_log.size() > 100)
+ s.key_log.erase(s.key_log.begin());
+ if (key == "\x03")
+ return true;
+ if (s.help || s.modal) {
+ s.help = false;
+ s.modal = false;
+ return false;
+ }
+ if (s.filtering || s.editing || s.palette) {
+ std::string &value = s.filtering ? s.filter : s.editing ? s.input : s.query;
+ if (key == "escape" || key == "\r") {
+ if (s.palette && key == "\r")
+ s.screen = s.palette_index;
+ s.filtering = s.editing = s.palette = false;
+ } else if (s.palette && (key == "up" || key == "down"))
+ s.palette_index = (s.palette_index + (key == "up" ? 9 : 1)) % 10;
+ else if (key == "\x7f" || key == "\b") {
+ if (!value.empty()) {
+ std::size_t i = value.size() - 1;
+ while (i && ((static_cast(value[i]) & 192) == 128))
+ i--;
+ value.resize(i);
+ }
+ } else if ((key.size() == 1 || static_cast(key[0]) >= 128) &&
+ key.size() <= 4 && static_cast(key[0]) >= 32 &&
+ value.size() + key.size() <= 4096)
+ value += key;
+ return false;
+ }
+ if (s.select_open) {
+ if (key == "up" || key == "down")
+ s.select_index = (s.select_index + (key == "up" ? 3 : 1)) % 4;
+ else if (key == "\r" || key == "escape")
+ s.select_open = false;
+ return false;
+ }
+ if (key == "q")
+ return true;
+ if (key == "f1" || key == "?")
+ s.help = true;
+ else if (key == "f2")
+ s.theme_index = (s.theme_index + 1) % 9;
+ else if (key == "f3" || key == "/")
+ s.filtering = true;
+ else if (key == "c")
+ // Shows what collapsed borders do, live. Worth a key because the
+ // difference is only visible when panels sit next to each other.
+ s.collapse = !s.collapse;
+ else if (key == "f6")
+ s.sort = (s.sort + 1) % 4;
+ else if (key == "\x0b")
+ s.palette = true;
+ else if (key == "e")
+ s.editing = true;
+ else if (key == " ")
+ s.paused = !s.paused;
+ else if (key == "\t")
+ s.screen = (s.screen + 1) % 10;
+ else if (key == "backtab")
+ s.screen = (s.screen + 9) % 10;
+ else if (key.size() == 1 && key[0] >= '0' && key[0] <= '9')
+ s.screen = (key[0] - '0' + 9) % 10;
+ else if ((key == "left" || key == "right") && s.screen == 7)
+ s.theme_index = (s.theme_index + (key == "left" ? 8 : 1)) % 9;
+ else if (key == "left" || key == "right") {
+ std::vector ids;
+ for (auto &r : s.regions)
+ if (s.panes.count(r.id) &&
+ std::find(ids.begin(), ids.end(), r.id) == ids.end())
+ ids.push_back(r.id);
+ if (!ids.empty()) {
+ auto it = std::find(ids.begin(), ids.end(), s.focused);
+ int i = it == ids.end() ? 0 : int(it - ids.begin());
+ s.focused = ids[(i + (key == "left" ? int(ids.size()) - 1 : 1)) %
+ int(ids.size())];
+ }
+ } else {
+ auto &p = s.panes[s.focused];
+ if (key == "down")
+ p.move(1);
+ else if (key == "up")
+ p.move(-1);
+ else if (key == "pagedown")
+ p.move(std::max(1, p.capacity));
+ else if (key == "pageup")
+ p.move(-std::max(1, p.capacity));
+ else if (key == "home") {
+ if (p.log)
+ p.offset = std::max(0, p.total - p.capacity);
+ else
+ p.selected = 0;
+ } else if (key == "end") {
+ if (p.log)
+ p.offset = 0;
+ else
+ p.selected = std::max(0, p.total - 1);
+ }
+ }
+ return false;
+}
+class Input {
+ std::string pending;
+ bool paste = false;
+ std::chrono::steady_clock::time_point escape_time;
+
+public:
+ bool feed(State &s, std::string_view input) {
+ if (pending.size() + input.size() > 8192)
+ pending.clear();
+ if (pending.empty())
+ escape_time = std::chrono::steady_clock::now();
+ pending += input;
+ while (!pending.empty()) {
+ if (paste) {
+ auto end = pending.find("\x1b[201~");
+ if (end == pending.npos) {
+ if (pending.size() < 4096)
+ return false;
+ end = pending.size() - 6;
+ }
+ std::string &target = s.filtering ? s.filter
+ : s.palette ? s.query
+ : s.input;
+ if (s.editing || s.filtering || s.palette)
+ target.append(pending.substr(0, std::min(end, 4096 - target.size())));
+ pending.erase(0, end);
+ if (pending.rfind("\x1b[201~", 0) == 0) {
+ pending.erase(0, 6);
+ paste = false;
+ }
+ continue;
+ }
+ if (pending.rfind("\x1b[200~", 0) == 0) {
+ pending.erase(0, 6);
+ paste = true;
+ continue;
+ }
+ if (pending.rfind("\x1b[<", 0) == 0) {
+ auto end = pending.find_first_of("mM", 3);
+ if (end == pending.npos)
+ return false;
+ auto sequence = pending.substr(0, end + 1);
+ pending.erase(0, end + 1);
+ int button, x, y;
+ if (std::sscanf(sequence.c_str(), "\x1b[<%d;%d;%d", &button, &x, &y) ==
+ 3 &&
+ x >= 1 && y >= 1 && x <= 500 && y <= 200) {
+ x--;
+ y--;
+ s.last_mouse = "mouse " + std::to_string(x) + "," + std::to_string(y);
+ if (!s.overlay())
+ for (auto it = s.regions.rbegin(); it != s.regions.rend(); it++) {
+ auto r = it->rect;
+ if (x < r.x || y < r.y || x >= r.x + r.width ||
+ y >= r.y + r.height)
+ continue;
+ if (button & 64) {
+ s.focused = it->id;
+ s.panes[it->id].move(button & 1 ? 3 : -3);
+ } else if (sequence.back() == 'M' && (button & 3) == 0 &&
+ !(button & 32)) {
+ activate(s, it->id);
+ if (s.panes.count(it->id) && y - r.y >= it->header) {
+ auto &p = s.panes[it->id];
+ p.selected = std::clamp(p.offset + y - r.y - it->header, 0,
+ std::max(0, p.total - 1));
+ }
+ }
+ break;
+ }
+ }
+ continue;
+ }
+ if (pending[0] == '\x1b') {
+ static const std::vector>
+ sequences = {{"\x1b[A", "up"}, {"\x1b[B", "down"},
+ {"\x1b[C", "right"}, {"\x1b[D", "left"},
+ {"\x1b[H", "home"}, {"\x1b[F", "end"},
+ {"\x1b[5~", "pageup"}, {"\x1b[6~", "pagedown"},
+ {"\x1b[Z", "backtab"}, {"\x1bOP", "f1"},
+ {"\x1bOQ", "f2"}, {"\x1bOR", "f3"},
+ {"\x1b[11~", "f1"}, {"\x1b[12~", "f2"},
+ {"\x1b[13~", "f3"}, {"\x1b[17~", "f6"},
+ {"\x1b[I", "focus"}, {"\x1b[O", "blur"}};
+ bool matched = false;
+ for (auto &entry : sequences)
+ if (pending.rfind(entry.first, 0) == 0) {
+ pending.erase(0, entry.first.size());
+ if (key(s, entry.second))
+ return true;
+ matched = true;
+ break;
+ }
+ if (matched)
+ continue;
+ if (std::chrono::steady_clock::now() - escape_time <
+ std::chrono::milliseconds(35))
+ return false;
+ pending.erase(0, 1);
+ if (key(s, "escape"))
+ return true;
+ continue;
+ }
+ unsigned char c = pending[0];
+ std::size_t n = c < 128 ? 1 : c < 224 ? 2 : c < 240 ? 3 : 4;
+ if (pending.size() < n)
+ return false;
+ auto token = pending.substr(0, n);
+ pending.erase(0, n);
+ if (key(s, token))
+ return true;
+ }
+ return false;
+ }
+};
+static void simulate(State &s) {
+ double time = s.data["time"].n() + .1;
+ s.data["time"] = time;
+ auto &cpu = s.data["cpu"];
+ double used = .45 + std::sin(time * .35) * .22;
+ cpu["total"] = used;
+ auto &cores = cpu["cores"].array();
+ for (std::size_t i = 0; i < cores.size(); i++)
+ cores[i] = ratio(used + std::sin(time + i) * .15);
+ auto &history = cpu["history"].array();
+ if (history.size() >= 240)
+ history.erase(history.begin());
+ history.emplace_back(used * 100);
+}
+} // namespace demo
+#ifndef HQTUI_DEMO_TEST
+int hqtui_demo_main(int argc, char **argv) {
+ demo::interrupted = 0;
+ using namespace demo;
+ try {
+ bool snapshot = false, body_only = false, sim = false, real = false;
+ int w = 160, h = 50, fps = 30, ticks = 0;
+ double interval = 1;
+ std::string format = "text";
+ State state;
+ state.data = Json::parse(sample_json);
+ for (int i = 1; i < argc; i++) {
+ std::string arg = argv[i];
+ if (arg == "--help" || arg == "-h") {
+ std::cout
+ << "hqtui-demo-cpp — native C++ reference demo\n--sim | --real "
+ "--snapshot --screen "
+ "dashboard|traffic|sessions|network|services|components|"
+ "graphics|themes|input|stress\n--theme "
+ "dark|dracula|nord|tokyo-night|gruvbox|matrix|monochrome|high-"
+ "contrast|light\n--width N --height N --fps 1–120 --interval "
+ "0.05–60 --ticks N --format text|ansi\n--version\n";
+ return 0;
+ }
+ if (arg == "--version") {
+ std::cout << HQTUI_DEMO_VERSION << "\n";
+ return 0;
+ }
+ if (arg == "--sim")
+ sim = true;
+ else if (arg == "--real")
+ real = true;
+ else if (arg == "--snapshot")
+ snapshot = true;
+ else if (arg == "--body")
+ body_only = true;
+ else {
+ if (++i == argc)
+ throw std::runtime_error("Missing value for " + arg);
+ std::string v = argv[i];
+ if (arg == "--screen") {
+ int n = 0;
+ for (; n < 10 && v != screens[n]; n++) {
+ }
+ if (n == 10)
+ throw std::runtime_error("Unknown screen");
+ state.screen = n;
+ } else if (arg == "--theme") {
+ int n = 0;
+ for (; n < 9 && v != themes[n]; n++) {
+ }
+ if (n == 9)
+ throw std::runtime_error("Unknown theme");
+ state.theme_index = n;
+ } else if (arg == "--format")
+ format = v;
+ else {
+ std::size_t end = 0;
+ double number = std::stod(v, &end);
+ if (end != v.size() || !std::isfinite(number))
+ throw std::runtime_error("Invalid number");
+ if (arg == "--interval")
+ interval = number;
+ else {
+ if (number != std::floor(number) || number < 0 || number > 10000)
+ throw std::runtime_error("Invalid integer");
+ if (arg == "--width")
+ w = int(number);
+ else if (arg == "--height")
+ h = int(number);
+ else if (arg == "--fps")
+ fps = int(number);
+ else if (arg == "--ticks")
+ ticks = int(number);
+ else
+ throw std::runtime_error("Unknown option " + arg);
+ }
+ }
+ }
+ }
+ if (w < 1 || w > 500 || h < 1 || h > 200 || fps < 1 || fps > 120 ||
+ interval < .05 || interval > 60 || sim && real ||
+ (format != "text" && format != "ansi"))
+ throw std::runtime_error("Invalid options");
+#if defined(__linux__)
+ state.real = real || (!sim && !snapshot);
+#else
+ state.real =
+ real; // Non-Linux hosts default to explicitly labelled sample data.
+#endif
+ if (state.real) {
+ Collector collector(state.data);
+ if (snapshot) {
+ collector.refresh();
+ state.data = collector.data;
+ } else
+ state.data = collector.data;
+ } else
+ for (int i = 0; i < ticks; i++)
+ simulate(state);
+ if (snapshot) {
+ auto t = hq_theme_named(themes[state.theme_index]);
+ Buffer frame(w, h);
+ frame.clear(t->background, t->foreground);
+ UI ui(frame.surface(t), false, 0, &state.regions, state.collapse);
+ render(ui, state, body_only);
+ ui.flush();
+ if (format == "ansi") {
+ Buffer previous(w, h);
+ Encoder encoder;
+ write_all(encoder.encode(previous, frame, true).output);
+ } else
+ for (int y = 0; y < h; y++)
+ std::cout << frame.row(y) << "\n";
+ return 0;
+ }
+ Terminal terminal;
+ std::unique_ptr worker;
+ if (state.real)
+ worker = std::make_unique(Json::parse(sample_json), interval);
+ auto size = terminal.size();
+ Buffer previous(size.first, size.second), frame(size.first, size.second);
+ Encoder encoder;
+ Input input;
+ bool first = true;
+ auto last = std::chrono::steady_clock::now(), last_sim = last;
+ while (!interrupted) {
+ auto start = std::chrono::steady_clock::now();
+ if (worker)
+ worker->update(state);
+ else if (!state.paused &&
+ start - last_sim >= std::chrono::milliseconds(100)) {
+ simulate(state);
+ last_sim = start;
+ }
+ size = terminal.size();
+ if (size.first != frame.width() || size.second != frame.height()) {
+ frame.resize(size.first, size.second);
+ previous.resize(size.first, size.second);
+ first = true;
+ }
+ auto t = hq_theme_named(themes[state.theme_index]);
+ frame.clear(t->background, t->foreground);
+ state.regions.clear();
+ std::time_t wall = std::time(nullptr);
+ std::tm utc{};
+ gmtime_r(&wall, &utc);
+ char clock[9];
+ std::strftime(clock, sizeof clock, "%H:%M:%S", &utc);
+ state.clock = clock;
+ state.fps =
+ first ? fps
+ : 1 / std::max(
+ .0001,
+ std::chrono::duration(start - last).count());
+ last = start;
+ UI ui(frame.surface(t), false, 0, &state.regions, state.collapse);
+ render(ui, state);
+ ui.flush();
+ if (!state.overlay() &&
+ std::none_of(
+ state.regions.begin(), state.regions.end(),
+ [&](const Region &r) { return r.id == state.focused; })) {
+ for (const auto ®ion : state.regions)
+ if (state.panes.count(region.id)) {
+ state.focused = region.id;
+ break;
+ }
+ }
+ overlay(frame, state);
+ auto result = encoder.encode(previous, frame, first);
+ first = false;
+ if (!write_all(result.output))
+ break;
+ state.changed_cells = result.changed_cells;
+ state.output_bytes = result.output.size();
+ previous.copy_from(frame);
+ state.render_ms = std::chrono::duration(
+ std::chrono::steady_clock::now() - start)
+ .count();
+ if (input.feed(state, {}))
+ break;
+ auto elapsed = std::chrono::duration_cast(
+ std::chrono::steady_clock::now() - start)
+ .count();
+ struct pollfd pfd{0, POLLIN, 0};
+ int ready = poll(&pfd, 1, std::max(0, 1000 / fps - int(elapsed)));
+ if (ready > 0) {
+ if (pfd.revents & (POLLHUP | POLLERR))
+ break;
+ char buffer[4096];
+ ssize_t n = read(0, buffer, sizeof buffer);
+ if (n > 0 &&
+ input.feed(state, std::string_view(buffer, std::size_t(n))))
+ break;
+ }
+ }
+ terminal.restore();
+ return interrupted ? 128 + interrupted : 0;
+ } catch (const std::exception &e) {
+ std::cerr << "hqtui-demo-cpp: " << e.what() << "\n";
+ return 2;
+ }
+}
+#ifndef HQTUI_DEMO_LIBRARY
+int main(int argc, char **argv) { return hqtui_demo_main(argc, argv); }
+#endif
+#endif
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/model.hpp b/ports/nim/src/hqtui/engine/cpp/demo/model.hpp
new file mode 100644
index 0000000..9494728
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/model.hpp
@@ -0,0 +1,181 @@
+#pragma once
+#include "json.hpp"
+#include
+#include
+#include
+namespace demo {
+using namespace hqtui;
+inline constexpr const char *screens[] = {
+ "dashboard", "traffic", "sessions", "network", "services",
+ "components", "graphics", "themes", "input", "stress"};
+inline constexpr const char *themes[] = {
+ "dark", "dracula", "nord", "tokyo-night", "gruvbox",
+ "matrix", "monochrome", "high-contrast", "light"};
+inline std::string fixed(double v, int digits = 0) {
+ if (!std::isfinite(v))
+ return "—";
+ std::ostringstream s;
+ s.imbue(std::locale::classic());
+ s << std::fixed << std::setprecision(digits) << v;
+ return s.str();
+}
+inline std::string bytes(double v, int digits = 2) {
+ if (!std::isfinite(v))
+ return "—";
+ v = std::max(0., v);
+ const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB", "PiB"};
+ int i = 0;
+ while (v >= 1024 && i < 5) {
+ v /= 1024;
+ i++;
+ }
+ return fixed(v, i ? digits : 0) + " " + units[i];
+}
+inline std::string percent(double v) {
+ return fixed(std::floor(ratio(v) * 100 + .5)) + "%";
+}
+inline std::string byte_rate(double v) {
+ for (auto item : {std::pair{1e9, "GB/s"},
+ {1e6, "MB/s"},
+ {1e3, "KB/s"}})
+ if (v >= item.first)
+ return fixed(v / item.first, 1) + " " + item.second;
+ return fixed(std::max(0., v)) + " B/s";
+}
+inline std::string bit_rate(double v) {
+ v = std::max(0., v) * 8;
+ for (auto item : {std::pair{1e9, "Gb/s"},
+ {1e6, "Mb/s"},
+ {1e3, "Kb/s"}})
+ if (v >= item.first)
+ return fixed(v / item.first, 1) + " " + item.second;
+ return fixed(v) + " b/s";
+}
+inline std::string duration(double v) {
+ auto n = static_cast(std::max(0., v));
+ auto d = n / 86400, h = n % 86400 / 3600, m = n % 3600 / 60;
+ if (d)
+ return std::to_string(d) + "d " + std::to_string(h) + "h " +
+ std::to_string(m) + "m";
+ if (h)
+ return std::to_string(h) + "h " + std::to_string(m) + "m";
+ return std::to_string(m) + "m " + std::to_string(n % 60) + "s";
+}
+inline KeyValue kv(std::string label, std::string value, Color color = 0) {
+ return {std::move(label), std::move(value), color};
+}
+inline Graph graph(const Json &values, Color color,
+ std::optional max = {}, bool axis = false) {
+ Graph g;
+ g.series.push_back({numbers(values), color, "", true});
+ g.max = max;
+ g.axis = axis;
+ return g;
+}
+inline Graph multi(const Json &a, const Json &b, Color ca, Color cb) {
+ Graph g;
+ g.series = {{numbers(a), ca, "", true}, {numbers(b), cb, "", true}};
+ return g;
+}
+inline Meter meter(double value, Color color = 0) {
+ Meter m;
+ m.value = value;
+ m.color = color;
+ m.show_value = false;
+ // The demo's bars are segmented, like btop's. The library default is smooth,
+ // matching every other port, so this asks rather than assuming.
+ m.segmented = true;
+ return m;
+}
+struct State {
+ Json data;
+ bool real = false, paused = false, help = false, modal = false,
+ palette = false, filtering = false, editing = false, toggle = true,
+ checkbox = true, select_open = false, collapse = false;
+
+ // The seam between panels: zero while collapsed, so their borders merge. The
+ // library merges a seam only where two bordered siblings already touch, so
+ // toggling `collapse` without closing the gap moved nothing.
+ // Read it where the seam is declared, not into a local a nested body
+ // captures by reference: panel bodies are closures the UI runs at flush,
+ // long after the screen function returned.
+ int panel_gap() const { return collapse ? 0 : 1; }
+ int screen = 0, theme_index = 0, sort = 0, select_index = 0,
+ palette_index = 0;
+ double slider = .7, fps = 0, render_ms = 0;
+ std::size_t changed_cells = 0, output_bytes = 0;
+ std::string clock = "12:00:00", input = "", filter, query, last_key = "—",
+ last_mouse = "—", focused = "dashboard.processes";
+ std::vector key_log;
+ std::map panes;
+ std::vector regions;
+ bool overlay() const { return help || modal || palette || filtering; }
+ std::vector processes() const {
+ auto rows = data["processes"].array();
+ rows.erase(std::remove_if(rows.begin(), rows.end(),
+ [&](const Json &p) {
+ return !filter.empty() &&
+ p["name"].s().find(filter) ==
+ std::string::npos &&
+ p["command"].s().find(filter) ==
+ std::string::npos;
+ }),
+ rows.end());
+ std::stable_sort(
+ rows.begin(), rows.end(), [&](const Json &a, const Json &b) {
+ if (sort == 3)
+ return a["name"].s() < b["name"].s();
+ auto key = sort == 0 ? "cpu" : sort == 1 ? "mem" : "pid";
+ return sort == 2 ? a[key].n() < b[key].n() : a[key].n() > b[key].n();
+ });
+ return rows;
+ }
+};
+struct DataColumn {
+ std::string key;
+ Column column;
+ std::function format;
+ std::function color;
+};
+inline DataColumn dc(std::string key, std::string title, int width = -1,
+ int min = 1, Color color = 0, bool right = false,
+ std::function format = {},
+ std::function tint = {}) {
+ return {std::move(key),
+ {std::move(title), width, min, color, right ? HQ_RIGHT : HQ_LEFT},
+ std::move(format),
+ std::move(tint)};
+}
+inline void table(UI &p, State &s, std::string name,
+ const std::vector &rows, std::vector cols,
+ bool zebra = false, bool header = true,
+ bool scrollbar = true) {
+ Table t;
+ t.pane = &s.panes[name];
+ t.zebra = zebra;
+ t.header = header;
+ t.scrollbar = scrollbar;
+ for (auto &c : cols)
+ t.columns.push_back(c.column);
+ for (auto &r : rows) {
+ TableRow row;
+ for (auto &c : cols) {
+ row.cells.push_back(c.format ? c.format(r) : r[c.key].s());
+ row.colors.push_back(c.color ? c.color(r) : c.column.color);
+ }
+ t.rows.push_back(std::move(row));
+ }
+ p.table(std::move(t), name);
+}
+inline std::vector logs(const Json &data) {
+ std::vector rows;
+ for (auto &r : data.array())
+ rows.push_back({r["time"].s(""), r["level"].s(""), r["message"].s(""),
+ "{" + r["meta"].s("") + "}"});
+ return rows;
+}
+void dashboard(UI &, State &);
+void telemetry(UI &, State &);
+void showcase(UI &, State &);
+void render(UI &, State &, bool body = false);
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/showcase.cpp b/ports/nim/src/hqtui/engine/cpp/demo/showcase.cpp
new file mode 100644
index 0000000..425c531
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/showcase.cpp
@@ -0,0 +1,586 @@
+#include "model.hpp"
+namespace demo {
+static void button(UI &p, std::string label, Color color, int width,
+ std::string id) {
+ auto regions = p.regions;
+ p.draw(
+ [=](Surface surface) {
+ auto t = theme(surface);
+ bool focused = id == "button:A";
+ Color bg = focused ? color : hq_mix(t.surface, color, .16),
+ fg = focused ? (t.dark ? t.background : t.surface) : color;
+ aligned(surface, 0, " " + label + " ", fg, HQ_CENTER, HQ_BOLD, bg);
+ if (regions)
+ regions->push_back({surface.rect(), id, 0});
+ },
+ cells(width));
+}
+static void badge(UI &p, std::string label, Color color, int width,
+ std::string variant = "filled",
+ std::optional background = {}) {
+ p.draw(
+ [=](Surface surface) {
+ auto t = theme(surface);
+ std::optional bg =
+ variant == "filled" ? std::optional(color)
+ : variant == "subtle"
+ ? std::optional(hq_mix(t.surface, color, .18))
+ : background;
+ auto fg =
+ variant == "filled" ? (t.dark ? t.background : t.surface) : color;
+ text(surface, 0, 0, " " + label + " ", fg,
+ variant == "subtle" ? 0 : HQ_BOLD, bg);
+ },
+ cells(width));
+}
+static void checkbox(UI &p, State &s, std::string label, int width,
+ bool toggle = false) {
+ auto regions = p.regions;
+ bool checked = toggle ? s.toggle : s.checkbox;
+ p.draw(
+ [=](Surface surface) {
+ auto t = theme(surface);
+ text(surface, 0, 0,
+ toggle ? (checked ? "[▮ ]" : "[ ▮]") : (checked ? "[✓]" : "[ ]"),
+ checked ? t.success : t.muted);
+ text(surface, toggle ? 4 : 3, 0, " " + label, t.muted);
+ if (regions)
+ regions->push_back(
+ {surface.rect(), toggle ? "toggle" : "checkbox", 0});
+ },
+ cells(width));
+}
+static void spark(UI &p, std::string label, const Json &data, std::string value,
+ Color color) {
+ auto values = numbers(data);
+ p.draw(
+ [=](Surface surface) {
+ auto t = theme(surface);
+ int lw = int(width(label)) + 1, vw = int(width(value)) + 1,
+ w = surface.rect().width - lw - vw;
+ text(surface, 0, 0, label, t.muted);
+ double hi = 1;
+ for (int i = std::max(0, int(values.size()) - w);
+ i < int(values.size()); i++)
+ hi = std::max(hi, values[i]);
+ int count = std::min(std::max(0, w), int(values.size()));
+ for (int i = 0; i < count; i++) {
+ int n = std::clamp(
+ iround(ratio(values[values.size() - count + i] / hi) * 8), 0, 8);
+ surface.set(lw + w - count + i, 0, n ? 0x2580 + n : ' ',
+ Style().foreground(color));
+ }
+ text(surface, surface.rect().width - vw, 0, fit(value, vw, HQ_RIGHT),
+ color, HQ_BOLD);
+ },
+ cells(1));
+}
+static void donut(UI &p, double a, double b, Color ca, Color cb) {
+ p.draw([=](Surface surface) {
+ Braille canvas(surface.rect().width, surface.rect().height);
+ double total = std::max(1., a + b), cx = canvas.w / 2., cy = canvas.h / 2.,
+ outer = std::min(canvas.w / 2., canvas.h / 2.) - 1,
+ inner = outer * .55, angle = -std::acos(-1.) / 2;
+ std::vector colors(canvas.dots.size());
+ double vals[] = {a, b};
+ Color tint[] = {ca, cb};
+ for (int i = 0; i < 2; i++) {
+ double sweep = std::max(0., vals[i]) / total * std::acos(-1.) * 2;
+ int steps = std::max(8, iround(sweep * outer * 3));
+ for (int step = 0; step <= steps; step++) {
+ double a = angle + sweep * step / steps;
+ for (double r = inner; r <= outer; r += .4) {
+ int x = iround(cx + std::cos(a) * r),
+ y = iround(cy + std::sin(a) * r * .9);
+ canvas.pixel(x, y);
+ if (x >= 0 && y >= 0 && x < canvas.w && y < canvas.h)
+ colors[std::size_t(y / 4) * canvas.cols + x / 2] = tint[i];
+ }
+ }
+ angle += sweep;
+ }
+ for (int y = 0; y < canvas.rows; y++)
+ for (int x = 0; x < canvas.cols; x++) {
+ auto index = std::size_t(y) * canvas.cols + x;
+ if (canvas.dots[index])
+ surface.set(x, y, 0x2800 + canvas.dots[index],
+ Style().foreground(colors[index] ? colors[index]
+ : theme(surface).muted));
+ }
+ });
+}
+static void list(UI &p, State &s, std::string id,
+ std::vector items, std::string bullet = "",
+ bool selected = true, bool scrollbar = false) {
+ auto regions = p.regions;
+ auto *pane = &s.panes[id];
+ p.draw([=](Surface surface) {
+ auto t = theme(surface);
+ int h = surface.rect().height, w = surface.rect().width - int(scrollbar);
+ pane->total = int(items.size());
+ pane->capacity = h;
+ pane->offset = std::clamp(pane->offset, 0, std::max(0, pane->total - h));
+ if (selected && h > 0) {
+ if (pane->selected < pane->offset)
+ pane->offset = pane->selected;
+ if (pane->selected >= pane->offset + h)
+ pane->offset = pane->selected - h + 1;
+ }
+ for (int y = 0; y < h && pane->offset + y < pane->total; y++) {
+ int i = pane->offset + y;
+ bool active = selected && i == pane->selected;
+ std::optional bg;
+ if (active)
+ bg = t.selection;
+ auto fg = active ? t.selection_text : i == 0 ? t.primary : t.foreground;
+ if (bg)
+ surface.sub({0, y, w, 1}).fill(' ', Style().background(*bg));
+ text(surface, 0, y,
+ fit((bullet.empty() ? "" : bullet + " ") + items[i], w), fg,
+ active ? HQ_BOLD : 0, bg);
+ }
+ if (scrollbar)
+ draw_scrollbar(surface, surface.rect().width - 1, 0, h, pane->total,
+ pane->offset);
+ if (regions)
+ regions->push_back({surface.rect(), id, 0});
+ });
+}
+static void graphics(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ auto t = ui.t();
+ double time = s.data["time"].n();
+ auto wave = [](double phase, double freq) {
+ std::vector a;
+ for (int i = 0; i < 240; i++)
+ a.push_back(std::sin(i / freq + phase) * 50 + 50);
+ return a;
+ };
+ auto a = wave(time / 3, 9), b = wave(time / 3 + 2, 5), c = wave(time / 2, 17);
+ ui.row(fr(), gap, [=](UI &r) {
+ r.col(fr(), gap, [=](UI &p) {
+ p.panel("Braille (2×4 pixels per cell)", [=](UI &g) {
+ Graph o;
+ o.series = {{a, t.accent, "", true}};
+ o.max = 100;
+ o.grid = true;
+ g.graph(o);
+ });
+ p.panel("Block elements", [=](UI &g) {
+ Graph o;
+ o.series = {{a, 0, "", false}};
+ o.max = 100;
+ o.mode = "block";
+ o.colors.assign(t.heat, t.heat + t.heat_count);
+ g.graph(o);
+ });
+ p.panel("ASCII fallback", [=](UI &g) {
+ Graph o;
+ o.series = {{a, t.foreground, "", false}};
+ o.max = 100;
+ o.mode = "ascii";
+ g.graph(o);
+ });
+ }, true);
+ r.col(fr(), gap, [=](UI &p) {
+ p.panel("Multi-series", [=](UI &g) {
+ Graph o;
+ o.series = {{a, t.primary, "alpha", false},
+ {b, t.success, "beta", false},
+ {c, t.secondary, "gamma", false}};
+ o.max = 100;
+ o.axis = true;
+ o.legend = true;
+ g.graph(o);
+ });
+ p.panel("Gradients", [=](UI &g) {
+ g.draw([=](Surface surface) {
+ for (int y = 0; y < surface.rect().height; y++)
+ for (int x = 0; x < surface.rect().width; x++)
+ surface.set(x, y, 0x2588,
+ Style().foreground(hq_gradient(
+ t.heat, t.heat_count,
+ surface.rect().width <= 1
+ ? 0
+ : double(x) / (surface.rect().width - 1))));
+ });
+ });
+ p.panel("Raw Braille canvas", [=](UI &g) {
+ g.draw([=](Surface surface) {
+ Braille canvas(surface.rect().width, surface.rect().height);
+ double cx = canvas.w / 2., cy = canvas.h / 2.,
+ radius = std::min(cx, cy) - 2;
+ int x = iround(
+ std::min(std::abs(radius), double(canvas.w + canvas.h))),
+ y = 0, err = 1 - x;
+ while (x >= y) {
+ canvas.pixel(cx + x, cy + y);
+ canvas.pixel(cx + y, cy + x);
+ canvas.pixel(cx - y, cy + x);
+ canvas.pixel(cx - x, cy + y);
+ canvas.pixel(cx - x, cy - y);
+ canvas.pixel(cx - y, cy - x);
+ canvas.pixel(cx + y, cy - x);
+ canvas.pixel(cx + x, cy - y);
+ y++;
+ if (err < 0)
+ err += 2 * y + 1;
+ else {
+ x--;
+ err += 2 * (y - x) + 1;
+ }
+ }
+ for (int i = 0; i < 12; i++) {
+ double a = double(i) / 12 * std::acos(-1.) * 2 + time / 4;
+ canvas.line(cx, cy, cx + std::cos(a) * radius,
+ cy + std::sin(a) * radius * .9);
+ }
+ canvas.blit(surface, t.accent);
+ });
+ });
+ }, true);
+ });
+}
+static void theme_screen(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ ui.label("Theme " + std::to_string(s.theme_index + 1) + "/9: " + ui.t().name +
+ " ←/→ or F2 to change");
+ ui.spacer(cells(1));
+ ui.col(fr(), gap, [&, gap](UI &grid) {
+ for (int row = 0; row < 3; row++)
+ grid.row(fr(), gap, [&, row](UI &r) {
+ for (int column = 0; column < 3; column++) {
+ int i = row * 3 + column;
+ auto e = *hq_theme_named(themes[i]);
+ r.panel(
+ e.name,
+ [&, e](UI &p) {
+ p.row(cells(1), 1, [=](UI &r) {
+ badge(r, "primary", e.primary, 10);
+ badge(r, "ok", e.success, 5);
+ badge(r, "warn", e.warning, 7);
+ badge(r, "err", e.danger, 6);
+ r.spacer();
+ });
+ Meter m;
+ m.value = .72;
+ m.label = "cpu";
+ m.segmented = false;
+ m.background = e.background;
+ p.meter(m);
+ auto g = graph(s.data.path("cpu.history"), e.graph[0], 100);
+ g.background = e.background;
+ p.graph(g);
+ p.draw(
+ [=](Surface surface) {
+ for (std::size_t i = 0; i < e.graph_count; i++)
+ for (int x = 0; x < 3; x++)
+ surface.set(int(i) * 4 + x, 0, 0x2588,
+ Style()
+ .foreground(e.graph[i])
+ .background(e.background));
+ },
+ cells(1));
+ },
+ fr(), "", i == s.theme_index ? e.border_focused : e.border,
+ e.background);
+ }
+ }, true);
+ });
+}
+static void input_screen(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ auto t = ui.t();
+ ui.row(fr(), gap, [&, t](UI &r) {
+ r.panel("Last Events", [&, t](UI &p) {
+ p.keys({kv("Key", s.last_key, t.accent),
+ kv("Mouse", s.last_mouse, t.primary)});
+ p.spacer(cells(1));
+ p.divider("history");
+ auto history = s.key_log;
+ std::reverse(history.begin(), history.end());
+ list(p, s, "input.history", history, "", false);
+ });
+ r.panel("Try it", [&, t](UI &p) {
+ p.text("Press any key — modifiers are normalized.");
+ p.label("Arrows, Function keys, Ctrl/Alt/Shift combinations,");
+ p.label("paste, focus, mouse move, click, drag and scroll.");
+ p.spacer(cells(1));
+ p.divider("focusable controls");
+ p.spacer(cells(1));
+ p.row(cells(1), 2, [&, t](UI &r) {
+ button(r, "Button A", t.primary, 12, "button:A");
+ button(r, "Button B", t.success, 12, "button:B");
+ checkbox(r, s, "Check", 12);
+ r.spacer();
+ });
+ p.spacer(cells(1));
+ p.label("Tab / Shift+Tab moves focus. Enter activates.");
+ p.spacer();
+ p.keys({kv("Mouse tracking", "on"), kv("Bracketed paste", "on"),
+ kv("Focus events", "on")});
+ });
+ });
+}
+static void stress(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ auto t = ui.t();
+ ui.row(cells(3), gap, [&, t](UI &r) {
+ std::vector titles = {"Render", "Changed cells", "Bytes/frame",
+ "FPS"},
+ values = {fixed(s.render_ms, 2) + " ms/frame",
+ std::to_string(s.changed_cells),
+ std::to_string(s.output_bytes),
+ fixed(s.fps, 1)};
+ Color colors[] = {t.success, t.warning, t.primary, t.accent};
+ for (int i = 0; i < 4; i++) {
+ auto value = values[i];
+ auto color = colors[i];
+ r.panel(titles[i], [=](UI &p) { p.text(value, color); });
+ }
+ });
+ double time = s.data["time"].n();
+ ui.panel("Full-screen churn", [=](UI &p) {
+ p.draw([=](Surface surface) {
+ uint32_t chars[] = {0x2596, 0x2597, 0x2598, 0x2599, 0x259a,
+ 0x259b, 0x259c, 0x259d, 0x259e, 0x259f,
+ 0x2588, 0x2593, 0x2592, 0x2591};
+ for (int y = 0; y < surface.rect().height; y++)
+ for (int x = 0; x < surface.rect().width; x++) {
+ double n =
+ ((std::sin(x / 6. + time) + std::cos(y / 4. - time)) / 2 + 1) / 2;
+ surface.set(
+ x, y, chars[std::clamp(int(std::floor(n * 13)), 0, 13)],
+ Style().foreground(hq_gradient(t.graph, t.graph_count, n)));
+ }
+ });
+ });
+}
+static void components(UI &ui, State &s) {
+ auto t = ui.t();
+ const int gap = s.panel_gap();
+ const auto &c = s.data["cpu"], &m = s.data["memory"], &n = s.data["network"];
+ ui.row(fr(), gap, [&, t](UI &r) {
+ r.col(fr(), s.panel_gap(), [&, t](UI &left) {
+ left.panel(
+ "Buttons & Inputs",
+ [&, t](UI &p) {
+ p.row(cells(1), 1, [&, t](UI &r) {
+ button(r, "Primary", t.primary, 11, "button:Primary");
+ button(r, "Success", t.success, 11, "button:Success");
+ button(r, "Warning", t.warning, 11, "button:Warning");
+ button(r, "Danger", t.danger, 10, "button:Danger");
+ r.spacer();
+ });
+ p.spacer(cells(1));
+ p.row(cells(1), 2, [&, t](UI &r) {
+ auto regions = r.regions;
+ r.draw(
+ [&, t, regions](Surface surface) {
+ const char *options[] = {"Dark", "Dracula", "Nord",
+ "Tokyo Night"};
+ auto &th = theme(surface);
+ auto bg =
+ hq_mix(th.surface,
+ hq_rgb(th.dark ? 255 : 0, th.dark ? 255 : 0,
+ th.dark ? 255 : 0),
+ .05);
+ text(
+ surface, 0, 0,
+ fit(" " + std::string(options[s.select_index % 4]), 18),
+ th.foreground, 0, bg);
+ text(surface, 18, 0, s.select_open ? " ▴" : " ▾", th.border,
+ 0, bg);
+ if (regions)
+ regions->push_back({surface.rect(), "select", 0});
+ },
+ cells(20));
+ checkbox(r, s, "Toggle", 12, true);
+ checkbox(r, s, "Checkbox", 14);
+ r.spacer();
+ });
+ p.spacer(cells(1));
+ p.draw(
+ [&, t](Surface surface) {
+ auto regions = p.regions;
+ (void)regions;
+ std::string value =
+ s.input.empty() ? "type to filter…" : s.input;
+ text(surface, 0, 0, "Search", t.muted);
+ auto field = surface.sub(
+ {7, 0, std::max(0, surface.rect().width - 7), 1});
+ auto bg = hq_mix(t.surface,
+ hq_rgb(t.dark ? 255 : 0, t.dark ? 255 : 0,
+ t.dark ? 255 : 0),
+ .1);
+ field.fill(' ', Style().background(bg));
+ text(field, 1, 0,
+ fit(value, std::min(std::max(0, field.rect().width - 2),
+ int(width(value)))),
+ s.input.empty() ? t.muted : t.foreground, 0, bg);
+ if (field.rect().width > 0) {
+ auto cursor = field.rect();
+ cursor.x += std::min(field.rect().width - 1,
+ 1 + int(width(s.input)));
+ cursor.width = 1;
+ cursor.height = 1;
+ hq_buffer_style(
+ field.native().buffer, cursor,
+ Style().foreground(t.background).background(t.cursor));
+ }
+ },
+ cells(1));
+ p.spacer(cells(1));
+ Meter slider;
+ slider.value = s.slider;
+ slider.label = "Slider";
+ slider.color = t.primary;
+ slider.segmented = false;
+ p.meter(slider);
+ Meter progress;
+ progress.value = 37. / 120;
+ progress.label = "Progress";
+ progress.readout = "37/120";
+ progress.color = t.primary;
+ progress.segmented = false;
+ p.meter(progress);
+ },
+ cells(13));
+ left.panel("Table Widget", [&, t](UI &p) {
+ static const char *files[][4] = {
+ {"src", "4.2 KB", "dir", "2m ago"},
+ {"test", "1.1 KB", "dir", "5m ago"},
+ {"package.json", "1.2 KB", "file", "10m ago"},
+ {"README.md", "3.4 KB", "file", "1h ago"},
+ {"bun.lockb", "12 KB", "file", "1h ago"}};
+ std::vector rows;
+ for (auto &v : files)
+ rows.push_back(Json::Object{{"name", v[0]},
+ {"size", v[1]},
+ {"type", v[2]},
+ {"modified", v[3]}});
+ table(p, s, "components.files", rows,
+ {dc("name", "Name", -1, 10, t.primary),
+ dc("size", "Size", 9, 1, 0, true), dc("type", "Type", 6),
+ dc("modified", "Modified", 10, 1, t.muted, true)},
+ true, true, false);
+ });
+ left.panel(
+ "Log Viewer",
+ [&](UI &p) {
+ p.log(logs(s.data["logs"]), &s.panes["components.logs"],
+ "components.logs");
+ },
+ cells(11));
+ }, true);
+ r.col(fr(), s.panel_gap(), [&, t](UI &right) {
+ right.panel(
+ "Process Tree",
+ [&, t](UI &p) {
+ p.row(cells(1), 0, [=](UI &r) {
+ r.text("Name", t.muted, HQ_LEFT, automatic(1), HQ_BOLD);
+ r.text("CPU% MEM%", t.muted, HQ_RIGHT, automatic(1), HQ_BOLD);
+ });
+ auto pane = &s.panes["components.tree"];
+ auto regions = p.regions;
+ p.draw([=](Surface surface) {
+ const char *names[] = {"systemd", "bash", "bun",
+ "bun:worker", "bun:worker", "node",
+ "node:worker", "postgres"};
+ const char *prefix[] = {"└─ ", " ├─ ", " ├─ ",
+ " │ ├─ ", " │ └─ ", " ├─ ",
+ " │ └─ ", " └─ "};
+ const char *cpu[] = {"1.3", "0.1", "32.8", "12.4",
+ "8.7", "18.1", "6.1", "6.7"},
+ *mem[] = {"0.1", "0.2", "4.2", "1.8",
+ "1.3", "2.1", "0.8", "1.8"};
+ int w = surface.rect().width, h = surface.rect().height,
+ lw = std::max(0, w - 14);
+ pane->total = 8;
+ pane->capacity = h;
+ pane->offset = std::clamp(pane->offset, 0, std::max(0, 8 - h));
+ if (h > 0) {
+ if (pane->selected < pane->offset)
+ pane->offset = pane->selected;
+ if (pane->selected >= pane->offset + h)
+ pane->offset = pane->selected - h + 1;
+ }
+ for (int y = 0; y < h && pane->offset + y < 8; y++) {
+ int i = pane->offset + y;
+ bool selected = i == pane->selected;
+ std::optional bg;
+ if (selected) {
+ bg = t.selection;
+ surface.sub({0, y, w, 1}).fill(' ', Style().background(*bg));
+ }
+ int px = std::min(lw, int(width(prefix[i])));
+ text(surface, 0, y, fit(prefix[i], px),
+ hq_mix(t.border, t.foreground, .15), 0, bg);
+ text(surface, px, y,
+ fit(names[i],
+ std::min(std::max(0, lw - px), int(width(names[i])))),
+ selected ? t.selection_text : t.foreground,
+ selected ? HQ_BOLD : 0, bg);
+ text(surface, lw, y, fit(cpu[i], 6, HQ_RIGHT),
+ selected ? t.selection_text : t.foreground, 0, bg);
+ text(surface, lw + 7, y, fit(mem[i], 6, HQ_RIGHT),
+ selected ? t.selection_text : t.foreground, 0, bg);
+ }
+ if (regions)
+ regions->push_back({surface.rect(), "components.tree", 0});
+ });
+ },
+ cells(13));
+ right.panel(
+ "Sparklines & Gauges",
+ [&, t](UI &p) {
+ spark(p, "CPU ", c["history"], percent(c["total"].n()), t.success);
+ spark(p, "Mem ", m["history"],
+ percent(m["used"].n() / std::max(1., m["total"].n())),
+ t.warning);
+ spark(p, "Net ", n["downHistory"], bytes(n["downRate"].n()) + "/s",
+ t.primary);
+ p.spacer(cells(1));
+ p.row(fr(), 2, [&, t](UI &r) {
+ r.gauge(c["total"].n(), percent(c["total"].n()));
+ donut(r, m["used"].n(), m["available"].n(), t.primary, t.warning);
+ });
+ },
+ cells(12));
+ right.panel("Lists & Badges", [&, t](UI &p) {
+ p.row(cells(1), 1, [=](UI &r) {
+ badge(r, "active", t.success, 10);
+ badge(r, "idle", t.warning, 8, "subtle");
+ badge(r, "failed", t.danger, 10, "outline");
+ r.spacer();
+ });
+ p.spacer(cells(1));
+ list(p, s, "components.list",
+ {"apps/demo", "packages/hqtui", "apps/web", "docs"}, "▸", true,
+ true);
+ });
+ }, true);
+ });
+}
+void showcase(UI &ui, State &s) {
+ switch (s.screen) {
+ case 5:
+ components(ui, s);
+ break;
+ case 6:
+ graphics(ui, s);
+ break;
+ case 7:
+ theme_screen(ui, s);
+ break;
+ case 8:
+ input_screen(ui, s);
+ break;
+ case 9:
+ stress(ui, s);
+ break;
+ default:
+ break;
+ }
+}
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/demo/telemetry.cpp b/ports/nim/src/hqtui/engine/cpp/demo/telemetry.cpp
new file mode 100644
index 0000000..20ebefe
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/demo/telemetry.cpp
@@ -0,0 +1,545 @@
+#include "model.hpp"
+namespace demo {
+static std::string rate(double v) {
+ return v >= 1e6 ? fixed(v / 1e6, 1) + "M/s"
+ : v >= 1000 ? fixed(v / 1000, 1) + "K/s"
+ : fixed(v) + "/s";
+}
+static std::string commas(double v) {
+ auto s = fixed(v);
+ for (int i = int(s.size()) - 3; i > 0; i -= 3)
+ s.insert(i, ",");
+ return s;
+}
+static Color status_color(const hq_theme &t, std::string c) {
+ return c == "1xx" ? t.secondary
+ : c == "2xx" ? t.success
+ : c == "3xx" ? t.accent
+ : c == "4xx" ? t.warning
+ : c == "5xx" ? t.danger
+ : t.muted;
+}
+static void traffic(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ if (s.real)
+ ui.label("Protocol/direction: port-based estimates; HTTP rate: estimated "
+ "from log growth");
+ auto t = ui.t();
+ const auto &d = s.data["telemetry"], &net = d["net"], &rates = net["rates"],
+ &http = d["http"];
+ ui.row(cells(13), gap, [&, t](UI &r) {
+ r.panel(
+ "Protocols",
+ [&, t](UI &p) {
+ auto &data = d["protocols"].array();
+ if (data.empty()) {
+ p.label("No sockets visible.");
+ return;
+ }
+ double maximum = 1;
+ for (auto &v : data)
+ maximum = std::max(maximum, v["total"].n());
+ std::vector items;
+ int i = 0;
+ for (auto &b : data) {
+ if (i == 9)
+ break;
+ Meter m;
+ m.label = b["protocol"].s();
+ m.value = b["total"].n() / maximum;
+ m.color = hq_series(&t, i++);
+ m.readout = b["total"].s();
+ m.label_width = 13;
+ m.value_width = 5;
+ m.segmented = false;
+ items.push_back(m);
+ }
+ p.meters(items);
+ },
+ fr(),
+ d["inboundConnections"].s() + " in / " + d["outboundConnections"].s() +
+ " out",
+ t.accent);
+ r.panel(
+ "TCP",
+ [&, t](UI &p) {
+ p.row(cells(1), 0, [&, t](UI &r) {
+ r.text("↓ " + rate(rates["inSegs"].n()) + " seg", t.primary);
+ r.text("↑ " + rate(rates["outSegs"].n()) + " seg", t.secondary,
+ HQ_RIGHT);
+ });
+ p.graph(multi(d["netInHistory"], d["netOutHistory"], t.primary,
+ t.secondary));
+ p.divider();
+ p.keys({kv("Established", net["tcpEstablished"].s(), t.success),
+ kv("Opens in/out",
+ rate(rates["passiveOpens"].n()) + " / " +
+ rate(rates["activeOpens"].n()),
+ t.accent),
+ kv("Resets sent", commas(net["tcpOutRsts"].n()), t.muted)});
+ },
+ fr(.9), "", t.primary);
+ auto color = net["retransRatio"].n() > .02 ? t.danger : t.success;
+ r.panel(
+ "Retransmits",
+ [&, t, color](UI &p) {
+ p.text(fixed(ratio(net["retransRatio"].n()) * 100, 2) + "%", color,
+ HQ_LEFT, cells(1), HQ_BOLD);
+ p.label("of outbound segments");
+ p.graph(graph(d["retransHistory"], t.danger));
+ p.keys(
+ {kv("UDP in/out",
+ rate(rates["udpIn"].n()) + " / " + rate(rates["udpOut"].n()),
+ t.muted),
+ kv("ICMP",
+ net["icmpInMsgs"].s() + " / " + net["icmpOutMsgs"].s(),
+ t.muted)});
+ },
+ fr(.7), "", color);
+ });
+ ui.row(fr(), gap, [&, t](UI &r) {
+ r.col(fr(), s.panel_gap(), [&, t](UI &c) {
+ c.panel(
+ "HTTP",
+ [&, t](UI &p) {
+ if (http.null()) {
+ p.label("No readable HTTP access log.");
+ p.label("nginx, apache, httpd and caddy logs are");
+ p.label("root/adm readable — run with sudo to track requests.");
+ return;
+ }
+ p.row(cells(1), 0, [&, t](UI &r) {
+ r.text(http["source"].s(), t.muted);
+ r.text(http["upgrades"].s() + " upgrades (ws)", t.secondary,
+ HQ_RIGHT);
+ });
+ p.graph(graph(http["history"], t.success), cells(6));
+ p.divider("status");
+ double maximum = 1;
+ for (auto &b : http["statusClasses"].array())
+ maximum = std::max(maximum, b["count"].n());
+ std::vector items;
+ for (auto &b : http["statusClasses"].array()) {
+ Meter m;
+ m.label = b["class"].s();
+ m.value = b["count"].n() / maximum;
+ m.color = status_color(t, m.label);
+ m.readout = b["count"].s();
+ m.label_width = 5;
+ m.value_width = 7;
+ m.segmented = false;
+ items.push_back(m);
+ }
+ p.meters(items);
+ p.divider("top paths");
+ table(p, s, "traffic.paths", http["topPaths"].array(),
+ {dc("path", "Path", -1, 20, t.primary),
+ dc("count", "Hits", 7, 1, t.accent, true)},
+ false, false);
+ },
+ fr(),
+ http.null() ? "no access log"
+ : fixed(http["requestsPerSecond"].n(), 1) + " req/s",
+ t.success);
+ }, true);
+ r.col(fr(.85), s.panel_gap(), [&, t](UI &c) {
+ c.panel(
+ "SSH Activity",
+ [&, t](UI &p) {
+ auto rows = d["ssh"].array();
+ if (rows.empty()) {
+ p.label("No sshd events in the journal.");
+ return;
+ }
+ std::reverse(rows.begin(), rows.end());
+ table(p, s, "traffic.ssh", rows,
+ {dc("time", "Time", 9, 1, t.muted),
+ dc("action", "Action", 11, 1, 0, false, {},
+ [=](const Json &d) {
+ return d["action"].s() == "accepted" ? t.success
+ : d["action"].s() == "disconnect" ? t.muted
+ : t.danger;
+ }),
+ dc("user", "User", 12, 1, t.primary),
+ dc("from", "From", -1, 14, t.accent),
+ dc("method", "Method", 10, 1, t.muted)},
+ true);
+ },
+ fr(), std::to_string(d["ssh"].array().size()), t.warning);
+ c.panel(
+ "Top Remote Hosts",
+ [&, t](UI &p) {
+ if (d["remotes"].array().empty()) {
+ p.label("No remote peers.");
+ return;
+ }
+ table(p, s, "traffic.remotes", d["remotes"].array(),
+ {dc("host", "Host", -1, 16, t.accent),
+ dc("connections", "Conns", 6, 1, t.success, true),
+ dc("protocols", "Protocols", -1, 12, t.muted)},
+ true);
+ },
+ cells(10), "", t.secondary);
+ }, true);
+ });
+ if (!http.null() && !http["recent"].array().empty())
+ ui.panel(
+ "Recent Requests",
+ [&, t](UI &p) {
+ table(p, s, "traffic.requests", http["recent"].array(),
+ {dc("time", "Time", 9, 1, t.muted),
+ dc("method", "Method", 7, 1, t.secondary),
+ dc("path", "Path", -1, 24, t.primary),
+ dc("status", "Status", 7, 1, 0, true, {},
+ [=](const Json &d) {
+ return status_color(t,
+ d["status"].s().substr(0, 1) + "xx");
+ }),
+ dc("client", "Client", 16, 1, t.accent),
+ dc("bytes", "Bytes", 9, 1, t.muted, true)},
+ true);
+ },
+ cells(10), "", t.primary);
+}
+static void sessions(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ auto t = ui.t();
+ const auto &d = s.data["telemetry"];
+ ui.row(cells(9), gap, [&, t](UI &r) {
+ r.panel(
+ "Active Sessions",
+ [&, t](UI &p) {
+ if (d["sessions"].array().empty()) {
+ p.label("No interactive sessions.");
+ p.label("(`who` reports nothing on this host)");
+ return;
+ }
+ table(p, s, "sessions.active", d["sessions"].array(),
+ {dc("user", "User", 12, 1, t.primary), dc("tty", "TTY", 10),
+ dc("from", "From", -1, 12, t.accent),
+ dc("loginAt", "Login", 14, 1, t.muted),
+ dc("idle", "Idle", 8, 1, 0, true)});
+ },
+ fr(), std::to_string(d["sessions"].array().size()), t.success);
+ r.panel(
+ "Process States",
+ [&, t](UI &p) {
+ auto states = d["states"];
+ const char *keys[] = {"running", "sleeping", "stopped", "zombie"},
+ *labels[] = {"run ", "slp ", "stop", "zomb"};
+ Color colors[] = {t.success, t.primary, t.warning, t.danger};
+ for (int i = 0; i < 4; i++) {
+ Meter m;
+ m.value = states[keys[i]].n() / std::max(1., states["total"].n());
+ m.label = labels[i];
+ m.readout = states[keys[i]].s();
+ m.color = colors[i];
+ m.segmented = false;
+ p.meter(m);
+ }
+ p.spacer(cells(1));
+ p.keys({kv("Total", states["total"].s(), t.accent)});
+ },
+ cells(34), "", t.primary);
+ });
+ ui.row(fr(), gap, [&, t](UI &r) {
+ r.panel(
+ "Recent Logins",
+ [&, t](UI &p) {
+ if (d["logins"].array().empty()) {
+ p.label("No login history available.");
+ return;
+ }
+ table(p, s, "sessions.logins", d["logins"].array(),
+ {dc("user", "User", 12, 1, t.primary),
+ dc("tty", "TTY", 12, 1, t.muted),
+ dc("from", "From", -1, 14, t.accent),
+ dc("when", "When", -1, 16, t.muted),
+ dc("status", "Status", 8, 1, 0, false, {},
+ [=](const Json &d) {
+ return d["status"].s() == "still" ? t.success : t.muted;
+ })},
+ true);
+ },
+ fr(), std::to_string(d["logins"].array().size()) + " from wtmp",
+ t.accent);
+ r.col(fr(.8), s.panel_gap(), [&, t](UI &c) {
+ c.panel(
+ "Failed Logins",
+ [&, t](UI &p) {
+ if (d["failedLogins"].array().empty()) {
+ p.label("None recorded.");
+ p.label("(btmp is usually root-only)");
+ return;
+ }
+ table(p, s, "sessions.failed", d["failedLogins"].array(),
+ {dc("user", "User", 12, 1, t.danger),
+ dc("from", "From", -1, 12),
+ dc("when", "When", -1, 14, t.muted)});
+ },
+ fr(), "", t.danger);
+ c.panel(
+ "Session History",
+ [&, t](UI &p) {
+ p.label("concurrent sessions");
+ p.graph(graph(d["sessionHistory"], t.success));
+ },
+ cells(8), "", t.secondary);
+ }, true);
+ });
+}
+static void network(UI &ui, State &s) {
+ const int gap = s.panel_gap();
+ auto t = ui.t();
+ const auto &d = s.data["telemetry"];
+ auto shown = d["interfaces"].array();
+ std::vector active;
+ for (auto &i : shown)
+ if (i["rxTotal"].n() > 0 || i["state"].s() == "up")
+ active.push_back(i);
+ if (!active.empty())
+ shown = active;
+ if (shown.size() > 3)
+ shown.resize(3);
+ ui.row(cells(13), gap, [&, t, shown](UI &r) {
+ if (shown.empty()) {
+ r.panel("Interfaces", [](UI &p) { p.label("No interfaces reported."); });
+ return;
+ }
+ for (std::size_t i = 0; i < shown.size(); i++) {
+ auto iface = shown[i];
+ Color color = i == 0 ? t.primary : i == 1 ? t.success : t.secondary;
+ r.panel(
+ iface["name"].s() + " (" + iface["state"].s() + ")",
+ [=](UI &p) {
+ p.row(cells(1), 0, [=](UI &r) {
+ r.text("↓ " + byte_rate(iface["rxRate"].n()), t.primary);
+ r.text("↑ " + byte_rate(iface["txRate"].n()), t.secondary,
+ HQ_RIGHT);
+ });
+ p.graph(multi(iface["rxHistory"], iface["txHistory"], t.primary,
+ t.secondary));
+ p.divider();
+ p.keys({kv("RX total", bytes(iface["rxTotal"].n()), t.primary),
+ kv("TX total", bytes(iface["txTotal"].n()), t.secondary),
+ kv("MAC", iface["mac"].s(), t.muted),
+ kv("MTU / err / drop",
+ iface["mtu"].s() + " / " + iface["errors"].s() + " / " +
+ iface["drops"].s(),
+ t.muted)});
+ },
+ fr(), iface["ip"].s(""), color);
+ }
+ });
+ ui.row(fr(), gap, [&, t](UI &r) {
+ r.panel(
+ "Connections",
+ [&, t](UI &p) {
+ if (d["connections"].array().empty()) {
+ p.label("No connections visible (`ss` unavailable).");
+ return;
+ }
+ table(p, s, "network.connections", d["connections"].array(),
+ {dc("proto", "Proto", 6, 1, t.muted),
+ dc("local", "Local", -1, 18),
+ dc("remote", "Remote", -1, 18, t.accent),
+ dc("state", "State", 10, 1, t.success),
+ dc("process", "Process", -1, 12, t.primary)},
+ true);
+ },
+ fr(), std::to_string(d["connections"].array().size()) + " open",
+ t.accent);
+ r.col(fr(.7), s.panel_gap(), [&, t](UI &c) {
+ c.panel(
+ "Listening Ports",
+ [&, t](UI &p) {
+ table(p, s, "network.listeners", d["listeners"].array(),
+ {dc("proto", "Proto", 6, 1, t.muted),
+ dc("port", "Port", 7, 1, t.warning, true),
+ dc("address", "Address", -1, 10, t.muted),
+ dc("process", "Process", -1, 10, t.primary)},
+ true);
+ },
+ fr(), std::to_string(d["listeners"].array().size()), t.warning);
+ c.panel(
+ "Open Connections",
+ [&, t](UI &p) { p.graph(graph(d["connectionHistory"], t.accent)); },
+ cells(6), "", t.secondary);
+ }, true);
+ });
+}
+static void services(UI &ui, State &s) {
+ auto t = ui.t();
+ const int gap = s.panel_gap();
+ const auto &d = s.data["telemetry"];
+ int failed = 0;
+ for (auto &v : d["services"].array())
+ failed += v["active"].s() == "failed";
+ ui.row(fr(), gap, [&, t, failed](UI &r) {
+ r.panel(
+ "Services",
+ [&, t](UI &p) {
+ if (d["services"].array().empty()) {
+ p.label("systemd not available on this host.");
+ return;
+ }
+ table(p, s, "services.units", d["services"].array(),
+ {dc("name", "Unit", -1, 18, t.primary),
+ dc("active", "Active", 10, 1, 0, false, {},
+ [=](const Json &d) {
+ return d["active"].s() == "failed" ? t.danger
+ : d["active"].s() == "active" ? t.success
+ : t.muted;
+ }),
+ dc("sub", "Sub", 10, 1, t.muted),
+ dc("description", "Description", -1, 16, t.muted)},
+ true);
+ },
+ fr(),
+ failed ? std::to_string(failed) + " failed"
+ : std::to_string(d["services"].array().size()) + " units",
+ failed ? t.danger : t.success, {}, failed ? t.danger : t.muted);
+ r.col(fr(.85), s.panel_gap(), [&, t](UI &c) {
+ c.panel(
+ "Kernel",
+ [&, t](UI &p) {
+ auto k = d["kernel"];
+ auto krate = [](double v) {
+ return v >= 1000 ? fixed(v / 1000, 1) + "K/s" : fixed(v) + "/s";
+ };
+ p.keys({kv("Context switches", krate(k["contextSwitchRate"].n()),
+ t.accent),
+ kv("Interrupts", krate(k["interruptRate"].n()), t.accent),
+ kv("Forks", krate(k["forkRate"].n()), t.accent),
+ kv("Procs running", k["procsRunning"].s(), t.success),
+ kv("Procs blocked", k["procsBlocked"].s(),
+ k["procsBlocked"].n() ? t.warning : t.muted),
+ kv("Open file descriptors", commas(k["openFiles"].n()),
+ t.primary),
+ kv("Entropy available", k["entropy"].s(),
+ k["entropy"].n() < 200 ? t.warning : t.success),
+ kv("Page in / out",
+ fixed(k["pageIn"].n() / 1000) + "K / " +
+ fixed(k["pageOut"].n() / 1000) + "K",
+ t.muted)});
+ },
+ cells(11), "", t.accent);
+ c.panel(
+ "Containers",
+ [&, t](UI &p) {
+ if (d["containers"].array().empty()) {
+ p.label("No running containers.");
+ p.label("(docker not installed or not reachable)");
+ return;
+ }
+ table(p, s, "services.containers", d["containers"].array(),
+ {dc("name", "Name", -1, 12, t.primary),
+ dc("image", "Image", -1, 14, t.muted),
+ dc("status", "Status", -1, 12, t.success)},
+ true);
+ },
+ cells(9), "", t.primary);
+ c.panel(
+ "Hardware",
+ [&, t](UI &p) {
+ std::vector rows;
+ auto power = d["power"];
+ if (!power.null()) {
+ rows.push_back(kv("Battery",
+ power["battery"].s() + "% (" +
+ power["timeRemaining"].s() + ")",
+ t.success));
+ rows.push_back(kv("AC",
+ power["acConnected"].s() == "true"
+ ? "connected"
+ : "on battery",
+ t.muted));
+ rows.push_back(kv("Draw",
+ power["powerDraw"].null()
+ ? "—"
+ : fixed(power["powerDraw"].n(), 1) + " W",
+ t.warning));
+ }
+ for (auto &g : d["gpus"].array()) {
+ rows.push_back(
+ kv(g["name"].s(),
+ (g["utilization"].null() ? "—"
+ : percent(g["utilization"].n())) +
+ " · " + g["temperature"].s() + "°C",
+ t.accent));
+ rows.push_back(kv(
+ "GPU memory",
+ (g["memoryUsed"].null() ? "—" : bytes(g["memoryUsed"].n())) +
+ " / " +
+ (g["memoryTotal"].null() ? "—"
+ : bytes(g["memoryTotal"].n())),
+ t.muted));
+ }
+ if (rows.empty()) {
+ p.label("No battery or GPU telemetry on this host.");
+ return;
+ }
+ p.keys(rows);
+ },
+ fr(), "", t.warning);
+ }, true);
+ });
+ ui.panel(
+ "Filesystems",
+ [&, t](UI &p) {
+ if (d["filesystems"].array().empty()) {
+ p.label("No filesystems reported.");
+ return;
+ }
+ table(p, s, "services.filesystems", d["filesystems"].array(),
+ {dc("mount", "Mount", -1, 14, t.primary),
+ dc("device", "Device", -1, 12, t.muted),
+ dc("type", "Type", 8, 1, t.muted),
+ dc("size", "Size", 10, 1, 0, true,
+ [](const Json &d) { return bytes(d["size"].n(), 0); }),
+ dc("used", "Used", 10, 1, 0, true,
+ [](const Json &d) { return bytes(d["used"].n(), 0); }),
+ dc(
+ "pct", "Use%", 6, 1, 0, true,
+ [](const Json &d) {
+ return d["size"].n()
+ ? percent(d["used"].n() / d["size"].n())
+ : "-";
+ },
+ [=](const Json &d) {
+ return d["size"].n() && d["used"].n() / d["size"].n() > .9
+ ? t.danger
+ : t.warning;
+ }),
+ dc("inodes", "Inodes", 16, 1, t.muted, true,
+ [](const Json &d) {
+ return d["inodesTotal"].n()
+ ? percent(d["inodesUsed"].n() /
+ d["inodesTotal"].n()) +
+ " of " +
+ fixed(d["inodesTotal"].n() / 1e6, 1) + "M"
+ : "-";
+ })},
+ true);
+ },
+ cells(10), "", t.secondary);
+}
+void telemetry(UI &ui, State &s) {
+ switch (s.screen) {
+ case 1:
+ traffic(ui, s);
+ break;
+ case 2:
+ sessions(ui, s);
+ break;
+ case 3:
+ network(ui, s);
+ break;
+ case 4:
+ services(ui, s);
+ break;
+ default:
+ break;
+ }
+}
+} // namespace demo
diff --git a/ports/nim/src/hqtui/engine/cpp/include/hqtui.hpp b/ports/nim/src/hqtui/engine/cpp/include/hqtui.hpp
new file mode 100644
index 0000000..15b21b6
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/include/hqtui.hpp
@@ -0,0 +1,129 @@
+#ifndef HQTUI_HPP
+#define HQTUI_HPP
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// C++17 ownership and value types over the same native C hot path. No virtual
+// dispatch, callback erasure, GC, or per-cell C++ allocation. Static linking with
+// LTO can inline across the C/C++ boundary; correctness never depends on LTO.
+namespace hqtui {
+using Color = hq_color;
+using Rect = hq_rect;
+using Cell = hq_cell;
+using Constraint = hq_constraint;
+
+struct Style : hq_style {
+ constexpr Style() noexcept : hq_style{0,0,0,0} {}
+ constexpr Style(Color fg, Color bg, std::uint16_t attrs=0) noexcept
+ : hq_style{fg,bg,attrs,HQ_STYLE_ALL} {}
+ constexpr Style foreground(Color color) const noexcept {
+ auto s=*this; s.fg=color; s.mask|=HQ_STYLE_FG; return s;
+ }
+ constexpr Style background(Color color) const noexcept {
+ auto s=*this; s.bg=color; s.mask|=HQ_STYLE_BG; return s;
+ }
+ constexpr Style attributes(std::uint16_t flags) const noexcept {
+ auto s=*this; s.attrs=flags; s.mask|=HQ_STYLE_ATTRS; return s;
+ }
+};
+constexpr Color rgb(int r,int g,int b) noexcept {
+ return 0x1000000u | (static_cast(r)&255u)<<16 |
+ (static_cast(g)&255u)<<8 | (static_cast(b)&255u);
+}
+inline std::size_t width(std::string_view text) noexcept { return hq_text_width_n(text.data(),text.size()); }
+
+class Surface {
+ hq_surface surface_;
+public:
+ // A borrowed view: keep its Buffer and any custom theme alive.
+ explicit Surface(hq_surface surface) noexcept : surface_(surface) {}
+ hq_surface native() const noexcept { return surface_; }
+ Rect rect() const noexcept { return surface_.rect; }
+ Surface sub(Rect local) const noexcept { return Surface(hq_surface_sub(surface_,local)); }
+ Surface region(Rect absolute) const noexcept { return Surface(hq_surface_region(surface_,absolute)); }
+ Surface box(hq_box_options options={}) const noexcept { return Surface(hq_surface_box(surface_,options)); }
+ void set(int x,int y,std::uint32_t cp,Style style={}) const noexcept { hq_surface_set(surface_,x,y,cp,style); }
+ void fill(std::uint32_t cp=' ',Style style={}) const noexcept { hq_surface_fill(surface_,cp,style); }
+ std::size_t text(int x,int y,const char *text,hq_text_options options={}) const noexcept {
+ return hq_surface_text(surface_,x,y,text,options);
+ }
+};
+
+class Buffer {
+ struct Destroy { void operator()(hq_buffer *p) const noexcept { hq_buffer_destroy(p); } };
+ std::unique_ptr buffer_;
+public:
+ Buffer(int width,int height) : buffer_(hq_buffer_create(width,height)) {
+ if(!buffer_) throw std::runtime_error("hqtui: invalid buffer dimensions or allocation failure");
+ }
+ Buffer(Buffer&&) noexcept=default;
+ Buffer& operator=(Buffer&&) noexcept=default;
+ Buffer(const Buffer&)=delete;
+ Buffer& operator=(const Buffer&)=delete;
+ hq_buffer *native_handle() noexcept { return buffer_.get(); }
+ const hq_buffer *native_handle() const noexcept { return buffer_.get(); }
+ int width() const noexcept { return hq_buffer_width(buffer_.get()); }
+ int height() const noexcept { return hq_buffer_height(buffer_.get()); }
+ std::size_t allocations() const noexcept { return hq_buffer_allocations(buffer_.get()); }
+ Surface surface(const hq_theme *theme=nullptr) noexcept { return Surface(hq_surface_root(buffer_.get(),theme)); }
+ void resize(int width,int height) {
+ if(!hq_buffer_resize(buffer_.get(),width,height)) throw std::runtime_error("hqtui: resize failed");
+ }
+ void clear(Color background=0,Color foreground=0) noexcept { hq_buffer_clear(buffer_.get(),background,foreground); }
+ int set(int x,int y,std::uint32_t cp,Style style={}) noexcept { return hq_buffer_set(buffer_.get(),x,y,cp,style); }
+ std::size_t write(int x,int y,std::string_view text,Style style={},
+ std::size_t max_columns=std::numeric_limits::max()) noexcept {
+ return hq_buffer_write_n(buffer_.get(),x,y,text.data(),text.size(),style,max_columns);
+ }
+ void fill(Rect rect,std::uint32_t cp=' ',Style style={}) noexcept { hq_buffer_fill(buffer_.get(),rect,cp,style); }
+ void restyle(Rect rect,Style style) noexcept { hq_buffer_style(buffer_.get(),rect,style); }
+ Cell cell(int x,int y) const {
+ Cell c{};
+ if(!hq_buffer_cell(buffer_.get(),x,y,&c)) throw std::out_of_range("hqtui: cell outside buffer");
+ return c;
+ }
+ void copy_from(const Buffer &other) {
+ if(!hq_buffer_copy(buffer_.get(),other.buffer_.get())) throw std::bad_alloc();
+ }
+ std::string row(int y) const {
+ std::size_t n=hq_buffer_row(buffer_.get(),y,nullptr,0);
+ std::string result(n,'\0');
+ hq_buffer_row(buffer_.get(),y,result.data(),n+1);
+ return result;
+ }
+};
+
+struct Encoded {
+ // Borrowed until the next encode() call or destruction of the encoder.
+ std::string_view output;
+ std::size_t changed_cells,dirty_rows;
+};
+class Encoder {
+ struct Destroy { void operator()(hq_encoder *p) const noexcept { hq_encoder_destroy(p); } };
+ std::unique_ptr encoder_;
+public:
+ explicit Encoder(int colors=16777216,bool monochrome=false)
+ : encoder_(hq_encoder_create(colors,monochrome)) {
+ if(!encoder_) throw std::runtime_error("hqtui: invalid color mode or allocation failure");
+ }
+ Encoder(Encoder&&) noexcept=default;
+ Encoder& operator=(Encoder&&) noexcept=default;
+ Encoder(const Encoder&)=delete;
+ Encoder& operator=(const Encoder&)=delete;
+ void invalidate() noexcept { hq_encoder_invalidate(encoder_.get()); }
+ std::size_t allocations() const noexcept { return hq_encoder_allocations(encoder_.get()); }
+ Encoded encode(const Buffer &previous,const Buffer &next,bool full=false) {
+ hq_encoded out{};
+ if(!hq_encode(encoder_.get(),previous.native_handle(),next.native_handle(),full,&out)) throw std::bad_alloc();
+ return {{out.data,out.length},out.changed_cells,out.dirty_rows};
+ }
+};
+}
+#endif
diff --git a/ports/nim/src/hqtui/engine/cpp/include/hqtui/widgets.hpp b/ports/nim/src/hqtui/engine/cpp/include/hqtui/widgets.hpp
new file mode 100644
index 0000000..21ff4ad
--- /dev/null
+++ b/ports/nim/src/hqtui/engine/cpp/include/hqtui/widgets.hpp
@@ -0,0 +1,1206 @@
+#ifndef HQTUI_WIDGETS_HPP
+#define HQTUI_WIDGETS_HPP
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace hqtui {
+using Paint = std::function;
+inline int iround(double n) {
+ return std::isfinite(n) ? int(std::floor(n + .5)) : 0;
+}
+inline double ratio(double n) { return n > 0 ? std::min(1., n) : 0; }
+inline std::string utf8(uint32_t c) {
+ std::string s;
+ if (c < 128)
+ s += char(c);
+ else if (c < 2048) {
+ s += char(192 | (c >> 6));
+ s += char(128 | (c & 63));
+ } else if (c < 65536) {
+ s += char(224 | (c >> 12));
+ s += char(128 | ((c >> 6) & 63));
+ s += char(128 | (c & 63));
+ } else {
+ s += char(240 | (c >> 18));
+ s += char(128 | ((c >> 12) & 63));
+ s += char(128 | ((c >> 6) & 63));
+ s += char(128 | (c & 63));
+ }
+ return s;
+}
+/// `s` with its first `columns` display columns removed.
+///
+/// For scrolling a line sideways. Slicing by bytes would cut inside a grapheme
+/// and corrupt it, and a scroll that lands in the middle of a wide character
+/// cannot draw half of it -- what is left of that character is a space, which
+/// is what a terminal shows when a double-width cell is clipped.
+inline std::string drop_columns(std::string_view s, int columns) {
+ if (columns <= 0)
+ return std::string(s);
+ std::string out;
+ int skipped = 0;
+ std::size_t i = 0;
+ while (i < s.size()) {
+ unsigned char c = s[i];
+ std::size_t len = c < 128 ? 1 : c < 224 ? 2 : c < 240 ? 3 : 4;
+ len = std::min(len, s.size() - i);
+ auto cell = s.substr(i, len);
+ int cw = int(width(cell));
+ if (skipped >= columns) {
+ out.append(cell);
+ } else {
+ skipped += cw;
+ // A wide character straddling the cut leaves its trailing half behind.
+ if (skipped > columns)
+ out.append(std::size_t(skipped - columns), ' ');
+ }
+ i += len;
+ }
+ return out;
+}
+inline std::string fit(std::string_view s, int columns, int align = HQ_LEFT,
+ bool ellipsis = true) {
+ if (columns <= 0)
+ return {};
+ std::string out(s);
+ int n = int(width(s));
+ if (n > columns) {
+ int budget = columns - (ellipsis ? 1 : 0);
+ std::size_t i = 0, end = 0;
+ int used = 0;
+ while (i < s.size()) {
+ unsigned char c = s[i];
+ std::size_t len = c < 128 ? 1 : c < 224 ? 2 : c < 240 ? 3 : 4;
+ len = std::min(len, s.size() - i);
+ int w = int(width(s.substr(i, len)));
+ if (used + w > budget)
+ break;
+ used += w;
+ i += len;
+ end = i;
+ }
+ out = std::string(s.substr(0, end)) + (ellipsis ? "…" : "");
+ n = used + (ellipsis ? 1 : 0);
+ }
+ int padding = std::max(0, columns - n), left = align == HQ_RIGHT ? padding
+ : align == HQ_CENTER
+ ? padding / 2
+ : 0;
+ return std::string(left, ' ') + out + std::string(padding - left, ' ');
+}
+inline const hq_theme &theme(Surface s) { return *s.native().theme; }
+/// `4` rather than `4.0`, `4.2` rather than `4.20`.
+std::string number(double);
+/// Returns the columns written, so a caller laying items out in a row can
+/// advance by what was actually drawn rather than by what it hoped to draw.
+inline std::size_t text(Surface s, int x, int y, std::string_view value, Color fg,
+ uint16_t attrs = 0, std::optional bg = {}) {
+ hq_text_options o{};
+ o.style = Style().foreground(fg).attributes(attrs);
+ if (bg)
+ o.style = Style(o.style.fg, *bg, attrs);
+ return s.text(x, y, std::string(value).c_str(), o);
+}
+inline void aligned(Surface s, int y, std::string_view value, Color fg,
+ int align = HQ_LEFT, uint16_t attrs = 0,
+ std::optional bg = {}) {
+ text(s, 0, y, fit(value, s.rect().width, align), fg, attrs, bg);
+}
+inline Constraint cells(int n, int min = 0) {
+ return {HQ_CELLS, double(n), min, -1, 0};
+}
+inline Constraint fr(double n = 1, int min = 0) {
+ return {HQ_FR, n, min, -1, 0};
+}
+inline Constraint automatic(int intrinsic, int min = 0) {
+ return {HQ_AUTO, 0., min, -1, intrinsic};
+}
+/// Aligned, styled, optionally wrapped copy. Every other widget is built on it.
+struct TextStyle {
+ Color fg = 0;
+ std::optional bg;
+ int align = HQ_LEFT;
+ int attrs = 0;
+ /// Wrap on the surface width rather than truncating at the edge.
+ bool wrap = false;
+ /// First line to show, counted after wrapping.
+ ///
+ /// After wrapping is the only place this can be correct: the caller does not
+ /// know how many lines their text became, and pre-slicing the string means
+ /// re-deciding every time the width changes.
+ int scroll = 0;
+ /// Columns to shift the text left by, for lines wider than the surface.
+ int scroll_x = 0;
+};
+void draw_text(Surface, std::string_view content, const TextStyle & = {});
+
+struct Clear {
+ /// What to leave behind. Zero means the theme's background.
+ Color background = 0;
+};
+/// Reset a region to empty, so an overlay can draw over what was there.
+///
+/// Without this an overlay is drawn *into* whatever it lands on: the cells it
+/// does not touch keep the widget underneath, and a dialog ends up with someone
+/// else's table showing through the gaps between its words.
+void draw_clear(Surface, const Clear & = {});
+
+struct Fill {
+ /// The symbol to repeat. A wide one is stepped over rather than written per
+ /// column, since each glyph owns a continuation cell.
+ std::string symbol = " ";
+ Color fg = 0;
+ std::optional bg;
+ int attrs = 0;
+};
+/// Flood a region with one repeated symbol and style.
+void draw_fill(Surface, const Fill & = {});
+
+enum BadgeVariant { HQ_BADGE_FILLED, HQ_BADGE_OUTLINE, HQ_BADGE_SUBTLE };
+struct Badge {
+ std::string text;
+ Color color = 0;
+ int variant = HQ_BADGE_FILLED;
+ int align = HQ_LEFT;
+};
+/// Returns the columns the chip occupies.
+int draw_badge(Surface, const Badge &);
+
+struct Divider {
+ std::string label;
+ Color color = 0;
+ /// The rule character. Empty means the theme's horizontal line.
+ std::string ch;
+ int align = HQ_LEFT;
+};
+void draw_divider(Surface, const Divider & = {});
+
+struct KeyValue {
+ std::string label, value;
+ Color color = 0;
+};
+struct Series {
+ std::vector values;
+ Color color = 0;
+ std::string label;
+ bool fill = false;
+};
+struct Graph {
+ std::vector series;
+ double min = 0;
+ std::optional max;
+ bool axis = false, grid = false, legend = false;
+ /// Labels along the bottom, e.g. {"60s", "30s", "0s"}. Costs a row.
+ std::vector time_axis;
+ std::string mode = "braille";
+ std::vector colors;
+ std::optional