Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/bindings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Ruby PHP Perl bindings
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
bindings:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: jdx/mise-action@v4.3.0
with:
install: false
- name: Install only binding test toolchains
run: mise install ruby@4.0.6 conda:php@8.5.9 perl@5.44.0.0 cmake@4.4.3 python@3.12.13
- name: Install private Perl dependencies
run: mise exec perl@5.44.0.0 -- cpanm --local-lib-contained "${{ runner.temp }}/hqtui-perl" --notest --mirror https://cpan.metacpan.org --mirror-only FFI::Platypus@2.11
- name: Build optimized binding library and PHP adapter
shell: bash
run: |
mise exec -- cmake -S ports/cpp -B ports/cpp/build-bindings -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=ON -DHQTUI_LTO=ON -DHQTUI_BUILD_BINDINGS=ON -DHQTUI_PHP_CONFIG="$(mise exec conda:php@8.5.9 -- which php-config)"
mise exec -- cmake --build ports/cpp/build-bindings --target hqtui_bindings hqtui_php hqtui_bindings_test --parallel 3
mise exec -- ctest --test-dir ports/cpp/build-bindings -R bindings_abi --output-on-failure
- name: Check APIs, 360 reference frames, real data and terminal cleanup
shell: bash
run: |
extension=so
if [ "$(uname -s)" = Darwin ]; then extension=dylib; fi
export HQTUI_NATIVE_LIB="$PWD/ports/cpp/build-bindings/libhqtui_bindings.$extension"
export PERL5LIB="${{ runner.temp }}/hqtui-perl/lib/perl5"
mise exec -- python ports/bindings/tests/check.py
- name: Verify vanilla PHP FFI fallback without the native PHP adapter
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y php-cli php-common
export HQTUI_NATIVE_LIB="$PWD/ports/cpp/build-bindings/libhqtui_bindings.so"
export HQTUI_BINDING_COMMANDS='{"php":["/usr/bin/php","-d","ffi.enable=true"]}'
mise exec -- python ports/bindings/tests/check.py
6 changes: 3 additions & 3 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: C and C++ core
on:
push:
branches: [main]
paths: ['ports/c/**', 'ports/cpp/**', 'ports/conformance/**', 'apps/benchmark/src/*native-core.ts', '.github/workflows/c-cpp.yml']
paths: ['ports/c/**', 'ports/cpp/**', 'ports/bindings/**', 'ports/php/native/**', 'ports/conformance/**', 'apps/benchmark/src/*native-core.ts', '.github/workflows/c-cpp.yml']
pull_request:
paths: ['ports/c/**', 'ports/cpp/**', 'ports/conformance/**', 'apps/benchmark/src/*native-core.ts', '.github/workflows/c-cpp.yml']
paths: ['ports/c/**', 'ports/cpp/**', 'ports/bindings/**', 'ports/php/native/**', 'ports/conformance/**', 'apps/benchmark/src/*native-core.ts', '.github/workflows/c-cpp.yml']
permissions:
contents: read
jobs:
Expand Down Expand Up @@ -43,7 +43,7 @@ jobs:
with:
bun-version: '1.4.0'
- name: Sanitizer build
run: cmake -S ports/cpp -B ports/cpp/build-sanitize -DCMAKE_BUILD_TYPE=Debug -DHQTUI_SANITIZE=ON
run: cmake -S ports/cpp -B ports/cpp/build-sanitize -DCMAKE_BUILD_TYPE=Debug -DHQTUI_SANITIZE=ON -DHQTUI_BUILD_BINDINGS=ON
- run: cmake --build ports/cpp/build-sanitize --parallel
- run: ctest --test-dir ports/cpp/build-sanitize --output-on-failure
- name: Optimized build
Expand Down
9 changes: 6 additions & 3 deletions apps/demo/scripts/check-native-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
}
if os.environ.get('HQTUI_CPP_DEMO'):
COMMANDS['cpp'] = [os.environ['HQTUI_CPP_DEMO']]
if os.environ.get('HQTUI_BINDING_DEMOS'):
COMMANDS.update(json.loads(os.environ['HQTUI_BINDING_DEMOS']))
SHARED = {'cpp', 'ruby', 'php', 'perl'}
if os.environ.get('HQTUI_NATIVE_LANGUAGES'):
selected = os.environ['HQTUI_NATIVE_LANGUAGES'].split(',')
COMMANDS = {language: COMMANDS[language] for language in selected}
Expand All @@ -43,7 +46,7 @@ def main():
'''
with tempfile.TemporaryDirectory(prefix='hqtui-native-data-') as directory:
utilities = ['who', 'last', 'lastb', 'ss', 'journalctl', 'nvidia-smi']
if 'cpp' in COMMANDS:
if SHARED.intersection(COMMANDS):
utilities += ['tail', 'docker']
for name in utilities:
path = Path(directory) / name
Expand All @@ -55,7 +58,7 @@ def main():
cases = [('dashboard', ['Sensors', 'Test GPU', '25%']),
('traffic', ['HTTPS', 'DNS', 'SSH', 'accepted', 'alice']),
('sessions', ['alice', 'eve', 'still', 'failed'])]
if language == 'cpp':
if language in SHARED:
cases[1][1].extend(['/health', '/missing', '/chat'])
cases.append(('services', ['cpp-fixture-container', 'test:local']))
cases.append(('network', ['127.0.0.1']))
Expand All @@ -67,7 +70,7 @@ def main():
for label in expected:
assert label.lower() in result.stdout.lower(), (language, screen, f'missing {label}')
assert 'simulated' not in result.stdout.lower(), (language, screen)
if language == 'cpp':
if language in SHARED:
assert 'token=secret' not in result.stdout, 'HTTP query secret leaked'
print(f'{language} {screen}: fixture utilities reached the real collector and rendered rows', flush=True)

Expand Down
63 changes: 60 additions & 3 deletions apps/demo/scripts/test-updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def git(self, *args):
return subprocess.check_output(["git", *args], cwd=self.repo, stderr=subprocess.DEVNULL, text=True).strip()

def commit(self, label):
(self.repo / "mise.toml").write_text('[tools]\nbun = "1.4.0"\npython = "3.12.13"\nrust = "1.97.1"\ngo = "1.26.0"\nzig = "0.16.0"\ncmake = "4.4.3"\n')
(self.repo / "mise.toml").write_text('[tools]\nbun = "1.4.0"\npython = "3.12.13"\nrust = "1.97.1"\ngo = "1.26.0"\nzig = "0.16.0"\ncmake = "4.4.3"\nruby = "4.0.6"\n"conda:php" = "8.5.9"\nperl = "5.44.0.0"\n')
(self.repo / ".gitignore").write_text('__pycache__/\n')
path = self.repo / "ports/python/examples"
path.mkdir(parents=True, exist_ok=True)
Expand All @@ -49,7 +49,7 @@ def commit(self, label):
'import json, os, sys\n'
f'print(json.dumps({{"revision": {label!r}, "args": sys.argv[1:], "cwd": os.getcwd(), "tty": os.isatty(0)}}), flush=True)\n'
'if "--wait" in sys.argv: input()\n')
for language in ("rust", "go", "zig", "cpp"):
for language in ("rust", "go", "zig", "cpp", "ruby", "php", "perl"):
(self.repo / "ports" / language).mkdir(exist_ok=True)
(self.repo / "ports" / language / "source").write_text(label)
self.git("add", ".")
Expand Down Expand Up @@ -115,6 +115,17 @@ def stub_compilers(self):
assert args[0:2]==["--no-config","exec"]
assert args[3]=="--"
os.execvp(args[4],args[4:])
elif tool in ("ruby","php","perl"):
if args[0]=="-r":
if args[1].startswith('echo PHP_VERSION'): print(os.environ.get('UPDATER_PHP_VERSION','8.5.9'))
sys.exit(0)
if args[0] in ("-rfiddle/import","-MFFI::Platypus=2.11"): sys.exit(0)
if args[0]=="-MConfig": print("fixture-perl-abi");sys.exit(0)
if args[0]=="-d": args=args[2:]
label=(pathlib.Path(args[0]).parents[1]/"source").read_text()
print(json.dumps(dict(revision=label,args=args[1:])))
sys.exit(0)
elif tool=="php-config": print(os.environ.get('UPDATER_PHP_CONFIG_VERSION',os.environ.get('UPDATER_PHP_VERSION','8.5.9')));sys.exit(0)
elif tool=="cargo":
assert args==["build","--release","--example","dashboard"]
output=pathlib.Path(os.environ["CARGO_TARGET_DIR"])/"release/examples/dashboard"
Expand All @@ -130,6 +141,11 @@ def stub_compilers(self):
build.mkdir(parents=True,exist_ok=True)
(build/"source").write_text((source/"source").read_text())
sys.exit(0)
if args[0]=="--build" and args[3] in ("hqtui_bindings","hqtui_php"):
build=pathlib.Path(args[1])
for name in ('libhqtui_bindings.so','libhqtui_bindings.dylib','hqtui_php.so'):
(build/name).write_text('fixture shared library')
sys.exit(0)
assert args[0]=="--build" and args[2:4]==["--target","hqtui-demo-cpp"]
output=pathlib.Path(args[1])/"hqtui-demo-cpp"
else: raise AssertionError(tool)
Expand All @@ -138,7 +154,7 @@ def stub_compilers(self):
output.write_text("#!/usr/bin/env python3\\nimport json,sys\\nprint(json.dumps(dict(revision="+repr(label)+",args=sys.argv[1:])))\\n")
output.chmod(0o700)
'''
for name in ("mise", "cargo", "go", "zig", "cmake"):
for name in ("mise", "cargo", "go", "zig", "cmake", "ruby", "php", "perl", "php-config"):
path = self.bin / name
path.write_text(code)
path.chmod(0o700)
Expand All @@ -163,6 +179,47 @@ def test_vanilla_and_mise_build_latest_and_reuse_only_same_revision(self):
log = [json.loads(line) for line in (self.root / "tools.jsonl").read_text().splitlines()]
self.assertEqual(sum(row[0] in ("cargo", "go", "zig") or row[:2]==["cmake","--build"] for row in log), 12)

def test_bindings_update_both_managers_and_keep_builds_outside_source(self):
self.stub_compilers()
for manager in ('--system', '--mise'):
for language in ('ruby', 'php', 'perl'):
result=self.run_demo(manager,language,'--snapshot','literal argument')
self.assertEqual(result.returncode,0,result.stderr)
self.assertEqual(json.loads(result.stdout),dict(revision='first',args=['--snapshot','literal argument']))
def builds():
return sum(json.loads(line)[:2]==['cmake','--build'] for line in (self.root/'tools.jsonl').read_text().splitlines())
count=builds()
self.assertEqual(count,8) # engine × three languages, plus PHP adapter; two managers
for language in ('ruby','php','perl'):
self.assertEqual(self.run_demo('--mise',language,'--version').returncode,0)
self.assertEqual(builds(),count)
self.commit('second')
for language in ('ruby','php','perl'):
result=self.run_demo('--mise',language,'--snapshot')
self.assertEqual(result.returncode,0,result.stderr)
self.assertEqual(json.loads(result.stdout)['revision'],'second')
self.assertEqual(builds(),count+4)
self.assertFalse((self.root/'cache/v1/update.lock').exists())

def test_php_upgrade_rebuilds_adapter_and_mismatched_headers_are_rejected(self):
self.stub_compilers()
first=self.run_demo('--system','php','--version')
self.assertEqual(first.returncode,0,first.stderr)
def builds():
return sum(json.loads(line)[:2]==['cmake','--build'] for line in
(self.root/'tools.jsonl').read_text().splitlines())
self.assertEqual(builds(),2)
upgraded={**self.env,'UPDATER_PHP_VERSION':'8.6.0'}
result=self.run_demo('--system','php','--version',env=upgraded)
self.assertEqual(result.returncode,0,result.stderr)
self.assertEqual(builds(),4)
bad={**upgraded,'UPDATER_PHP_CONFIG_VERSION':'8.5.9'}
result=self.run_demo('--system','php','--version',env=bad)
self.assertNotEqual(result.returncode,0)
self.assertIn('php-config must match',result.stderr)
self.assertEqual(result.stdout,'')
self.assertEqual(builds(),4)

@unittest.skipUnless(os.name == "posix", "requires a controlling PTY")
def test_pipe_launcher_reattaches_keyboard_and_releases_build_lock(self):
pid, master = pty.fork()
Expand Down
20 changes: 16 additions & 4 deletions apps/web/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default async function Docs() {
HQTUI builds terminal applications in TypeScript, Rust, Go, Python and Zig,
with a new native C++ ten-screen demo over the shared C rendering core.
The C++ library API remains experimental.
Ruby, PHP and Perl now have experimental bindings to the same native engine.
</P>

<H2 id="languages">Choose a language</H2>
Expand All @@ -80,9 +81,9 @@ export default async function Docs() {
Rust, Go, Python, Zig and C++ demos need no JavaScript runtime.
Both the vanilla and mise commands below fetch latest main before running.
They work from any directory and never switch branches, reset, or pull in your checkout.
All five native dashboard commands launch ten-screen demos. Live metrics
All dashboard commands launch ten-screen demos. Live metrics in the native demos
currently require Linux; use --sim for generated sample data on other platforms.
All five native demos use the TypeScript reference&apos;s ten screen layouts,
The native demos and bindings use the TypeScript reference&apos;s ten screen layouts,
including its responsive dashboard, detailed telemetry tabs and widget showcases.
Each port is checked against 120 shared TypeScript reference frames across four
terminal sizes and three themes. Live-data availability still depends on the host
Expand All @@ -95,7 +96,17 @@ export default async function Docs() {
Its mise command supplies pinned CMake; you still need your platform&apos;s
C/C++ build tools. The interactive C++ terminal supports Linux/macOS;
live collection and exact 120-frame parity are currently tested on Linux.
The C-only demo and other mise language integrations are not ready yet.
The C-only demo and languages beyond those listed here are not ready yet.
</P>
<P>
Ruby, PHP and Perl 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.
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.
Mise supplies pinned Ruby/Perl, prebuilt PHP via its Conda backend, and CMake.
No system packages or global gems/CPAN modules are installed by the launcher.
</P>
<P>
Linux sensor panels now collect available hwmon temperatures, fans, voltage,
Expand All @@ -109,7 +120,7 @@ export default async function Docs() {
<P>
Vanilla commands require Git, curl and the language&apos;s installed toolchain
(Bun for TypeScript). The <a href="https://mise.jdx.dev/" className="text-[#5fff87] underline underline-offset-4">mise</a>
alternatives install/use only the selected pinned toolchain without loading project
alternatives install/use the selected pinned runtime and required build tools without loading project
hooks. The launcher prints the full Git revision, reuses completed builds for that
revision, and fails rather than silently launching stale code if fetching fails.
First builds take longer. Append --sim or --snapshot directly; no extra Cargo/Zig separator is needed.
Expand All @@ -123,6 +134,7 @@ export default async function Docs() {
{PORTS.map((port) => (
<section key={port.id} id={port.id} className="min-w-0 scroll-mt-20">
<h3 className="text-xl font-bold">{port.name}</h3>
<p className="mt-2 text-sm text-white/60">{port.description}</p>
<p className="mt-3 text-sm text-white/60">Interactive dashboard</p>
<CommandBlock className="mt-2" command={port.interactiveDemo} label={`${port.name} demo`} />
<p className="mt-4 text-sm text-white/60">With mise</p>
Expand Down
5 changes: 3 additions & 2 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export default async function Home() {
High Quality Terminal UI for TypeScript, Rust, Go, Python, Zig and C++
</p>
<Badge variant="secondary" className="mb-5 font-mono text-xs">
v0.1.12 · 6 language demos · MIT
v0.1.12 · 9 language demos · MIT
</Badge>
<p className="text-balance text-3xl font-bold tracking-tight sm:text-5xl">
Terminal dashboards that
Expand Down Expand Up @@ -227,10 +227,11 @@ export default async function Home() {
</section>

<section id="languages" className="mx-auto max-w-7xl scroll-mt-14 px-4 pt-20 sm:px-6">
<h2 className="text-3xl font-bold tracking-tight">One terminal UI, six language demos</h2>
<h2 className="text-3xl font-bold tracking-tight">One terminal UI, nine language demos</h2>
<p className="mt-3 max-w-3xl text-white/60">
Build in TypeScript or use a native Rust, Go, Python or Zig implementation.
C++ now has a native ten-screen demo over the shared C rendering core, with an experimental library API.
Ruby, PHP and Perl add experimental bindings to that same engine, with APIs for your own CLI applications.
The native demos need no JavaScript runtime.
</p>
<div className="mt-6 max-w-2xl">
Expand Down
10 changes: 9 additions & 1 deletion apps/web/lib/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,25 @@ export type Language = {
/** Same latest-source demo with a pinned mise-managed toolchain. */
miseDemo: string;
native: boolean;
binding?: boolean;
};

/** Optional developer checkout; public demo commands do not require it. */
export const CLONE = "git clone https://github.com/profullstack/hqtui";
export const LAUNCHER = "https://hqtui.com/demo.sh";
export function latestDemo(language: string, mise = false): string {
if (!["typescript", "rust", "go", "python", "zig", "cpp"].includes(language)) throw new Error("Unsupported demo language");
if (!["typescript", "rust", "go", "python", "zig", "cpp", "ruby", "php", "perl"].includes(language)) throw new Error("Unsupported demo language");
return `curl -fsSL ${LAUNCHER} | sh -s -- --${mise ? "mise" : "system"} ${language}`;
}

export const LANGUAGES: readonly Language[] = [
...([['Ruby', 'ruby'], ['PHP', 'php'], ['Perl', 'perl']] as const).map(([name, id]) => ({
name, id, href: `/docs#${id}`,
description: `${name} bindings with a batched widget API. The shared C/C++ demo engine runs inside your language runtime; not an independent port.`,
demo: latestDemo(id), interactiveDemo: latestDemo(id),
snapshotDemo: `${latestDemo(id)} --snapshot`, miseDemo: latestDemo(id, true),
native: true, binding: true,
})),
{
name: "C++",
id: "cpp",
Expand Down
Loading