تشهد واجهات الطرفية نهضة جديدة. أظهرت أدوات مثل lazygit وk9s ووكلاء البرمجة بالذكاء الاصطناعي مثل opencode أن واجهة طرفية مصمّمة جيدًا يمكن أن تكون أسرع وأكثر تركيزًا من علامة تبويب في المتصفح. OpenTUI هو الإطار الذي يقف خلف العديد من هذه التجارب: مكتبة TypeScript بنواة عرض أصلية مكتوبة بلغة Zig، وبنية قائمة على المكوّنات، وتخطيط flexbox، وروابط من الدرجة الأولى لكل من React وSolid.
في هذا الدرس ستبني لوحة مراقبة طرفية حقيقية باستخدام روابط React الخاصة بـ OpenTUI — النموذج التصريحي نفسه الذي تعرفه من الويب، لكن معروضًا في الطرفية بدلاً من DOM. بحلول النهاية ستفهم حلقة العرض ونظام التخطيط ومعالجة لوحة المفاتيح وخطافات الحركة بما يكفي لإطلاق أدوات سطر الأوامر الخاصة بك.
المتطلبات المسبقة
قبل البدء، تأكد من توفّر ما يلي:
- تثبيت Bun 1.1+ (
bun --version) — نواة OpenTUI الأصلية تعمل بأسرع شكل على Bun، لكن Node 20+ يعمل أيضًا - الإلمام بـ TypeScript وخطافات React (
useStateوuseEffect) - طرفية تدعم الألوان الحقيقية truecolor (معظم الطرفيات الحديثة تدعمها)
- محرر أكواد — يُنصح باستخدام VS Code
لست بحاجة إلى معرفة لغة Zig. تُشحن النواة الأصلية كملف ثنائي مُجمّع مسبقًا وتتفاعل معها بالكامل عبر TypeScript.
ما الذي ستبنيه
بحلول نهاية هذا الدرس، سيكون لديك لوحة مراقبة نظام مباشرة تعمل داخل طرفيتك، وتتضمن:
- تخطيطًا محاطًا بإطار ومقسّمًا إلى لوحات باستخدام flexbox.
- ترويسة معروضة بخط ASCII كبير.
- إحصاءات ذاكرة وحِمل تُحدَّث لحظيًا.
- قائمة قابلة للاختيار يمكن التنقل فيها بلوحة المفاتيح.
- شريط تقدم متحرك سلس تقوده الخط الزمني في OpenTUI.
- إيقافًا نظيفًا عند الضغط على المفتاح
q.
الخطوة 1: إعداد المشروع
أنشئ مشروعًا جديدًا وثبّت النواة إلى جانب ربط React. يُقسَّم OpenTUI إلى حزم صغيرة بحيث لا تسحب سوى ما تستخدمه.
mkdir opentui-dashboard && cd opentui-dashboard
bun init -y
bun add @opentui/core @opentui/react reactالحزمتان اللتان تهمّانك هما:
@opentui/core— محرك العرض ومحرك التخطيط ومكوّنات العرض منخفضة المستوى@opentui/react— المُوفّق (reconciler) الذي يتيح لك وصف واجهتك باستخدام JSX
أضف سكربت dev إلى package.json لتشغيل التطبيق مع إعادة التحميل الحيّة:
{
"scripts": {
"dev": "bun --hot run src/index.tsx"
}
}الخطوة 2: أول عملية عرض
يحتاج OpenTUI إلى شيئين: محرك عرض (يملك الطرفية والمخزن المؤقت للإطارات وتدفق الإدخال) وجذر (يوفّق شجرة React مع محرك العرض هذا). أنشئ src/index.tsx:
/** @jsxImportSource @opentui/react */
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return (
<box style={{ border: true, padding: 1 }}>
<text>مرحبًا من الطرفية</text>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)شغّله:
bun run devيجب أن ترى صندوقًا محاطًا بإطار يحمل تحيّتك. هناك بضع نقاط جديرة بالملاحظة هنا. يخبر توجيه jsxImportSource في الأعلى المُصرّف باستخدام زمن تشغيل JSX الخاص بـ OpenTUI بدلاً من React DOM — وهذا ما يربط <box> و<text> بعناصر العرض في الطرفية. العناصر الجوهرية مكتوبة بأحرف صغيرة (box وtext وinput وselect)، ويقبل كل عنصر مرئي خاصية style.
نصيحة: إذا كنت تفضّل عدم كتابة التوجيه في كل ملف، فاضبط
"jsxImportSource": "@opentui/react"فيtsconfig.jsonضمنcompilerOptionsليُطبَّق على المشروع بأكمله.
الخطوة 3: التخطيط باستخدام Flexbox
محرك التخطيط في OpenTUI هو تطبيق لـ flexbox، لذا ينتقل النموذج الذهني مباشرةً من CSS. الصناديق هي حاويات flex؛ وتتحكم في الاتجاه والحجم والتباعد بخصائص مألوفة. لنقسّم الشاشة إلى ترويسة وجسم من عمودين.
function Dashboard() {
return (
<box style={{ flexDirection: "column", height: "100%" }}>
<box style={{ border: true, padding: 1 }}>
<text>مراقب النظام</text>
</box>
<box style={{ flexDirection: "row", flexGrow: 1 }}>
<box style={{ border: true, flexGrow: 1, padding: 1 }}>
<text>اللوحة اليمنى — إحصاءات</text>
</box>
<box style={{ border: true, flexGrow: 1, padding: 1 }}>
<text>اللوحة اليسرى — قائمة</text>
</box>
</box>
</box>
)
}يكدّس الصندوق الخارجي العناصر الأبناء عموديًا (flexDirection: "column") ويملأ ارتفاع الطرفية. ويضع صندوق الجسم لوحتيه جنبًا إلى جنب (flexDirection: "row")، وتجعل flexGrow: 1 كل لوحة تتمدد لتقاسم العرض المتاح بالتساوي. غيّر حجم طرفيتك وسيُعاد حساب التخطيط تلقائيًا — دون أي حسابات يدوية.
الخطوة 4: الحالة المباشرة مع خطافات React
بما أن روابط React تشغّل مُوفّقًا حقيقيًا، تعمل الخطافات القياسية تمامًا كما تتوقع. لنضف إحصاءات ذاكرة وحِمل تُحدَّث كل ثانية. نقرأها من وحدة os في Node، التي يوفّرها Bun.
import { useState, useEffect } from "react"
import os from "node:os"
function useSystemStats() {
const [stats, setStats] = useState({ usedPct: 0, load: 0 })
useEffect(() => {
const tick = () => {
const total = os.totalmem()
const free = os.freemem()
const usedPct = Math.round(((total - free) / total) * 100)
const load = os.loadavg()[0]
setStats({ usedPct, load: Number(load.toFixed(2)) })
}
tick()
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}, [])
return stats
}الآن اعرض هذه القيم في اللوحة اليمنى. لاحظ كيف تحدد fg لون المقدمة وكيف نؤلّف عُقد النص تمامًا مثل React على الويب.
function StatsPanel() {
const { usedPct, load } = useSystemStats()
return (
<box style={{ border: true, flexGrow: 1, padding: 1, flexDirection: "column" }}>
<text style={{ fg: "#7dd3fc" }}>الذاكرة المستخدمة</text>
<text>{usedPct}%</text>
<text style={{ fg: "#c4b5fd" }}>متوسط الحِمل (دقيقة)</text>
<text>{load}</text>
</box>
)
}تُحدَّث اللوحة الآن مرة كل ثانية. ولأن OpenTUI يقارن الشجرة الافتراضية ولا يعيد رسم سوى الخلايا المتغيّرة، تبقى هذه التحديثات منخفضة التكلفة حتى بمعدلات تحديث عالية.
الخطوة 5: التنقل بلوحة المفاتيح وقائمة
تعيش واجهات الطرفية التفاعلية أو تموت بحسب معالجتها للوحة المفاتيح. يوفّر OpenTUI خطاف useKeyboard لأحداث المفاتيح الخام ومكوّن <select> للقوائم. أولاً، لنربط مفتاح خروج عام باستخدام useRenderer للوصول إلى محرك العرض النشط.
import { useKeyboard, useRenderer } from "@opentui/react"
function useQuitOnQ() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "q" || key.name === "escape") {
renderer.destroy()
process.exit(0)
}
})
}بعد ذلك، أضف قائمة على اليسار. يتولى مكوّن <select> التنقل بمفاتيح الأسهم ويُرسل الخيار المختار عبر onSelect.
function MenuPanel() {
const [selected, setSelected] = useState("نظرة عامة")
return (
<box style={{ border: true, flexGrow: 1, padding: 1, flexDirection: "column" }}>
<text style={{ fg: "#a7f3d0" }}>العرض — الحالي: {selected}</text>
<select
style={{ height: 6 }}
options={[
{ name: "نظرة عامة", description: "ملخّص لجميع المقاييس" },
{ name: "العمليات", description: "أهم العمليات النشطة" },
{ name: "الشبكة", description: "إنتاجية الواجهات" },
{ name: "القرص", description: "استخدام وحدات التخزين" },
]}
onSelect={(index, option) => setSelected(option.name)}
/>
</box>
)
}يجب أن يكون <select> في وضع التركيز لكي يستقبل المفاتيح. عندما يكون العنصر التفاعلي الوحيد، يمنحه OpenTUI التركيز تلقائيًا؛ ومع وجود عدة عناصر قابلة للتركيز، تدير التركيز بشكل صريح، وهو ما نتناوله في قسم استكشاف الأخطاء.
الخطوة 6: تقدم متحرك مع الخط الزمني
الأشرطة الثابتة جيدة، لكن الحركة السلسة تنقل الإحساس بالنشاط. يحرّك خطاف useTimeline في OpenTUI أي قيمة رقمية عبر الزمن مع تخفيف — اعتبره محرك انتقال خفيف الوزن. هنا نحرّك عرض التعبئة لتصوير نسبة الذاكرة.
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function ProgressBar({ target }: { target: number }) {
const [width, setWidth] = useState(0)
const timeline = useTimeline({ duration: 600, loop: false })
useEffect(() => {
timeline.add(
{ width },
{
width: target,
duration: 600,
ease: "outCubic",
onUpdate: (anim) => setWidth(Math.round(anim.targets[0].width)),
},
)
}, [target])
return (
<box style={{ flexDirection: "row" }}>
<box style={{ width, height: 1, backgroundColor: "#22d3ee" }} />
<text> {target}%</text>
</box>
)
}في كل مرة تتغيّر فيها target، يحرّك الخط الزمني الشريط إلى عرضه الجديد خلال 600 ميلي ثانية بتخفيف تكعيبي خارج، بحيث تبدو اللوحة حيّة بدلاً من القفز بين الإطارات.
الخطوة 7: تركيب اللوحة الكاملة
الآن اجمع كل شيء في الشجرة النهائية. يربط الجذر معالج الخروج، ويعرض ترويسة ASCII، ويضع لوحتي الإحصاءات والقائمة جنبًا إلى جنب.
/** @jsxImportSource @opentui/react */
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
useQuitOnQ()
const { usedPct } = useSystemStats()
return (
<box style={{ flexDirection: "column", height: "100%" }}>
<box style={{ border: true, padding: 1 }}>
<ascii-font text="MONITOR" font="tiny" />
</box>
<box style={{ flexDirection: "row", flexGrow: 1 }}>
<box style={{ flexDirection: "column", flexGrow: 1 }}>
<StatsPanel />
<box style={{ border: true, padding: 1 }}>
<ProgressBar target={usedPct} />
</box>
</box>
<MenuPanel />
</box>
<box style={{ padding: 1 }}>
<text style={{ fg: "#94a3b8" }}>اضغط q للخروج</text>
</box>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)شغّل bun run dev وسيكون لديك لوحة مراقبة مباشرة تُدار بلوحة المفاتيح: ترويسة ASCII، وإحصاءات ذاتية التحديث، وشريط ذاكرة متحرك، وقائمة قابلة للتنقل — كل ذلك في أقل من 150 سطرًا من TypeScript.
اختبار تنفيذك
تحقّق من أن التطبيق يعمل بشكل صحيح:
- إعادة تدفق التخطيط: غيّر حجم نافذة الطرفية. يجب أن تعيد اللوحات توزيع العرض دون إفساد الإطارات.
- التحديثات المباشرة: راقب نسبة الذاكرة. افتح تطبيقًا ثقيلًا وتأكّد من استجابة الرقم والشريط خلال ثانية.
- التنقل: استخدم مفاتيح الأسهم للتنقل عبر القائمة؛ يجب أن تتبع تسمية «الحالي» اختيارك.
- خروج نظيف: اضغط
q. يجب أن تستعيد الطرفية موجّهك مع ظهور المؤشر ودون بقايا مخلّفات.
إذا بقي المؤشر مخفيًا بعد الخروج، فهذا يعني أن renderer.destroy() لم يُنفَّذ — استدعه دائمًا قبل process.exit.
استكشاف الأخطاء وإصلاحها
مخرجات مشوّشة أو ألوان مفقودة. قد لا تعلن طرفيتك عن دعم truecolor. اضبط COLORTERM=truecolor في بيئتك، أو ارجع إلى أنماط 256 لونًا.
المفاتيح لا تُسجَّل. تستقبل عناصر الواجهة المركّز عليها فقط أحداث المفاتيح. إذا كان لديك أكثر من عنصر تفاعلي، فاستدعِ .focus() على الهدف المقصود، أو أدِر التركيز بحزمة keymap (@opentui/keymap)، التي تربط أوامر مسمّاة باختصارات وتدير حلقات التركيز نيابةً عنك.
وميض عند التحديثات السريعة. يجمّع OpenTUI عمليات العرض لكل إطار، لكن استدعاء setState في حلقة متزامنة ضيّقة قد يُثقل النظام. حدّد التحديثات ضمن فترة معقولة — مرة كل 100 إلى 1000 ميلي ثانية كافية تمامًا للوحة مراقبة.
Node بدلاً من Bun. تعمل النواة أيضًا على Node 20+، لكن تأكّد من أن الملف الثنائي الأصلي المُجمّع مسبقًا يطابق منصّتك؛ أعد تثبيت الاعتماديات إذا بدّلت زمن التشغيل.
الخطوات التالية
أصبح لديك الآن النموذج الذهني الأساسي: محرك عرض، وجذر React، وتخطيط flexbox، وخطافات لوحة المفاتيح، والخط الزمني. من هنا يمكنك:
- إضافة لوحة سجلّ قابلة للتمرير باستخدام مكوّن
<scrollbox>للمخرجات المتدفقة. - عرض مقتطفات ملوّنة نحويًا باستخدام بنية
<code>معSyntaxStyle. - استخراج عناصر واجهة قابلة لإعادة الاستخدام إلى مكتبة مكوّنات داخلية، أو استكشاف أطقم المجتمع المبنية على النواة.
- استبدال ربط React بـ Solid إذا كنت تفضّل تفاعلية دقيقة — النواة متطابقة.
للتعمق أكثر، اطّلع على أدلّتنا ذات الصلة حول بناء أداة سطر أوامر بـ Node.js وTypeScript ووكيل البرمجة بالذكاء الاصطناعي في الطرفية OpenCode، وهو نفسه مبنيّ على OpenTUI.
الخلاصة
يجلب OpenTUI بيئة التطوير التصريحية القائمة على المكوّنات من تطوير الويب الحديث إلى الطرفية، مدعومًا بنواة أصلية سريعة. في هذا الدرس بنيت لوحة مراقبة نظام كاملة: أعددت محرك العرض، ورتّبت اللوحات باستخدام flexbox، وربطت الحالة المباشرة بخطافات React، وعالجت التنقل بلوحة المفاتيح، وحرّكت شريط تقدم بالخط الزمني. تتوسّع البدائيات نفسها من أداة داخلية سريعة إلى أدوات كاملة الميزات مثل opencode. عادت الطرفية لتكون سطح واجهة من الدرجة الأولى — ومع OpenTUI، يشبه التطوير لها التطوير للويب.