التوثيق
البدء
يُنشئ هذا الدليل تطبيق NSH، ويشغّله على المنصات التي تستهدفها، ثم ينشره. تُكتب تطبيقات NSH بلغة Rust أو TSX، على المحرك نفسه: يُترجم Rust تطبيقك إلى شيفرة أصلية بلا آلة JavaScript افتراضية، ويعمل TSX على QuickJS مع تحديثات فورية، في صورة وحدات يستطيع التطبيق الأصلي تحميلها وقت التشغيل. يتناول الدليل تطبيق Rust أولاً، ثم التطبيق نفسه بلغة TSX. ويفترض أن nsh مثبّت وموجود في PATH (التثبيت)؛ وفي نسخة مستنسخة من المستودع استخدم ./target/debug/nsh بدلاً منه.
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, hoverFill, ui } from "./palette";
// The app `nsh init` creates, with a reset button and a short history.
function App() {
const count = signal(0);
const history = signal<number[]>([]);
const change = (delta: number) => {
const next = count() + delta;
count.set(next);
history.update((h) => [next, ...h].slice(0, 4));
};
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, alignItems: "center", justifyContent: "center", gap: 16 }}>
<Text style={{ fontSize: 15, color: ui().muted }}>{desktop ? "You clicked" : "You tapped"}</Text>
<Text style={{ fontSize: 56, fontWeight: "700", color: count() < 0 ? ui().down : ui().text }}>{count()}</Text>
<View style={{ flexDirection: "row", gap: 12 }}>
<View onPress={() => change(-1)} role="button" {...button()}>
<Text style={buttonText()}>−1</Text>
</View>
<View onPress={() => change(1)} role="button" {...button("accent")}>
<Text style={buttonText("accent")}>{desktop ? "+1" : "Tap"}</Text>
</View>
</View>
<If when={history().length > 0}>
<Text style={{ fontSize: 13, color: ui().muted }}>Recent: {history().join(", ")}</Text>
</If>
<View onPress={() => (count.set(0), history.set([]))} hitSlop={8} hoverStyle={hoverFill()} focusStyle={focusRing()} activeStyle={{ opacity: 0.7 }}>
<Text style={{ fontSize: 15, color: ui().accent }}>Reset</Text>
</View>
</View>
);
}
mount(App);#إنشاء تطبيق
في الطرفية، يسأل nsh init عن اللغة التي يُكتب بها التطبيق: rust أو ts أو mixed (TSX مع حزمة Rust خاصة به). ويختار --template من دون سؤال.
#بلغة Rust
nsh init my-app --template rust
ينشئ nsh init المجلد ويكتب مشروع Cargo:
| الملف | ما هو |
|---|---|
src/lib.rs | التطبيق: مكوّنه الجذر وحالته ومعالجاته |
src/main.rs | يركّب الجذر نفسه على macOS أو Windows أو Linux من أجل cargo run |
Cargo.toml | الحزمة، وتعتمد على nsh-core وnsh-base بإصدار CLI نفسه |
nsh.json | اسم التطبيق ومعرّفه وإصداره ورقم البناء، وحزمته ("rust": ".") ومكوّنه الجذر ("root": "App") (بنية المشروع) |
App.test.tsx | اختبار الجذر، بلغة TypeScript (nsh test)، مع tsconfig.json و.nsh/types لمحررك |
README.md | كيفية تشغيل التطبيق واختباره وحزمه، وكيفية إضافة نقطة دخول TSX لاحقاً |
.gitignore | يتجاهل .nsh/ وtarget/ |
يتطلب التطوير بلغة Rust سلسلة أدوات Rust (rustup)؛ وتبني أوامر nsh مشغّلات التطبيق من حزم NSH على crates.io، بإصدار CLI نفسه.
#بلغة TSX
nsh init my-app --template ts
تطبيق TSX هو ملف .tsx واحد أو أكثر مع نقطة دخول: وهي ما يشغّله nsh run، وما يحمّله التطبيق المضيف كوحدة.
| الملف | ما هو |
|---|---|
App.tsx | وحدة الدخول: تعرض التطبيق باستخدام mount |
App.test.tsx | اختباره (nsh test) |
nsh.json | اسم التطبيق ومعرّفه وإصداره ورقم البناء (بنية المشروع) |
tsconfig.json | يمدّد .nsh/tsconfig.json، وهو إعدادات TypeScript الخاصة بـ NSH، التي تحلّ nsh إلى التصريحات الموجودة في .nsh/types |
.gitignore | يتجاهل .nsh/ وtarget/ |
.nsh/types/ | تصريحات مولَّدة لكل وحدات NSH، من أجل محررك |
تُحفظ الملفات الموجودة أصلاً، فيمكنك تشغيله في مجلد يحوي بعضها؛ أما تطبيق Rust فيُنشأ في مجلد جديد.
في الحالتين، يكون اسم العرض افتراضياً اسم المجلد؛ مرّر --name "My App" لاختيار اسم آخر. ويصبح المعرّف com.example.<name>: غيّره في nsh.json قبل النشر. وينشئ nsh init my-app --template mixed تطبيق TSX هو أيضاً حزمة Rust (Cargo.toml وsrc/lib.rs بجانب App.tsx)، ويستورد App.tsx مكوّنها ووحدتها وصنفها (البناء باستخدام Rust).
#المكوّن الجذر
يُنتج القالبان التطبيق نفسه، وهو عدّاد:
// src/lib.rs
use nsh_base::theme;
use nsh_core::prelude::*;
/// The crate's UI set: what a TSX entry may import from `my-app/native`.
#[native_module("my-app/native")]
fn native(registry: &mut Registry) {
registry.register::<App>();
}
/// The application's root component.
#[component]
pub fn App() -> impl IntoElement {
let count = signal(0_u32);
view()
.flex_1()
.bg(move || theme().background)
.text_color(move || theme().foreground)
.child(
view()
.flex_1()
.p_safe_area()
.child(
view()
.flex_1()
.items_center()
.justify_center()
.gap_4()
.child(text(move || format!("Count: {}", count.get())).text_size(px(28.0)))
.child(
view()
.px_5()
.py_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(move || count.update(|value| *value += 1))
.child(
text("Tap")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground),
),
),
),
)
}// App.tsx
import { mount, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
return (
<View class="flex-1 items-center justify-center gap-4 bg-background pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
<Text class="text-3xl font-semibold text-foreground">{count()}</Text>
<View
onPress={() => count.update((c) => c + 1)}
role="button"
class="rounded-(--radius) bg-primary px-5 py-3 active:bg-primary/90"
>
<Text class="text-lg text-primary-foreground">Tap</Text>
</View>
</View>
);
}
mount(App);في Rust:
- يجلب
use nsh_core::prelude::*العناصر الأولية (view()وtext()وscroll()وinput()وimage()…) والإشارات وطرق الأنماط المصنّفة (يضيف.p_safe_area()حشوة إلى العرض بمقدار المنطقة الآمنة)؛ ويحويnsh_baseالمكوّنات الأساسية غير المنسّقة (button()ولوحة المفاتيح والحوارات) وtheme()، أي الألوان وأنصاف الأقطار التي ترسم بها مكوّنات التطبيق (السمة). #[component] pub fn App()مكوّن: دالة تعمل مرة واحدة وتُرجع عناصرها. يسمّيه"root": "App"الخاص بـnsh.json، ويركّبهnsh run. ويُنشئsignal(0_u32)قيمة تتغير مع الزمن.text(move || format!(…, count.get()))يربط النص بالإشارة: عندما يتغيرcount، يشغّل NSH هذا الإغلاق (closure) مرة أخرى ويحدّث هذا النص وحده، ولا شيء غيره. وتقبل الأنماط إغلاقات بالطريقة نفسها: يتبع.bg(move || theme().primary)السمة إلى الوضع الداكن..on_press(..)يجعل العرض يستجيب للضغط؛ و.active(|s| s.opacity(0.6))هو نمطه أثناء الضغط، ويُطبَّق أصلياً. الزر هنا من صنع التطبيق نفسه: يوفّر NSH العناصر الأولية والقدرات، ويرسم التطبيق واجهته منها..px_5()و.rounded(px(10.0))و.gap_4()طرق أنماط مصنّفة سُمّيت كما تُسمّى أدوات Tailwind المساعدة، بالنقاط (px). التخطيط يعتمد flexbox، ويرصّ الأبناء في عمود افتراضياً (التخطيط).#[native_module("my-app/native")]يسمّي ما يجوز لنقطة دخول TSX استيراده من الحزمة، لحين إضافة شاشات TSX فوقها (البناء باستخدام Rust).
في TSX:
import { mount, signal, Text, View } from "nsh":nshهي الوحدة الأساسية: الإشارات وmountوالعناصر الأولية التفاعلية الأخرى والعناصر المضيفة. تقدّمها بيئة التشغيل الأصلية ولا تُضمَّن في الحزمة. ويحويnsh/baseالمكوّنات الأساسية، كما يفعلnsh_baseفي Rust.function App()مكوّن يعمل مرة واحدة. و**{count()}** يقرأ الإشارة داخل JSX: يرى المترجم استدعاءً فيجعل هذا النص يتبع الإشارة.<View onPress>عرض أصلي يتفاعل مع الضغط؛ وأصنافactive:هي نمطه أثناء الضغط. أماViewوTextوImageوScrollوغيرها من العناصر المضيفة فتُستورد منnsh(وInputمنnsh/base)؛ ولا يحتاج إلى استيراد سوىIfوEach.- يقبل
classأصناف Tailwind؛ ويقبلstyle={{ … }}كائناً من خصائص الأنماط بصيغة camelCase، بأرقام بالنقاط. وتسمّيbg-backgroundوbg-primaryوtext-primary-foregroundوrounded-(--radius)رموز سمة التطبيق (رموز السمة): وهي تتبع الوضع الداكن من تلقاء نفسها، ويعيد"theme"فيnsh.jsonتنسيقها. mount(App)يعرض المكوّن في سطح التطبيق.
#التطوير
#التشغيل
يبني nsh run التطبيق ويشغّله ويعيد البناء أثناء التعديل؛ وتظهر سجلات التطبيق في الطرفية. أعطه مجلد التطبيق: فهو يشغّل جذر تطبيق Rust، أو App.tsx الخاص بتطبيق TSX (ويسمّي nsh run my-app/App.tsx نقطة دخول أخرى).
في تطبيق Rust، يبني nsh run مشغّل التطبيق الخاص بالهدف، مع حزمتك والجذر المركّب ودون QuickJS، ثم يشغّله؛ وأي تغيير في شيفرة Rust يعيد البناء ويعيد تشغيل التطبيق. ويشغّل cargo run في مجلد التطبيق الجذر نفسه مباشرة على خلفية سطح المكتب. أما في تطبيق TSX، فيشغّل nsh run خادم تطوير ومضيف تطوير يحمّل التطبيق منه، ويرقّع التطبيق العامل عند كل حفظ.
من دون --target، يستخدم nsh run الجهاز الذي أنت عليه: نافذة AppKit على macOS، ونافذة Win32 على Windows، ونافذة GTK على Linux. اختر منصة أخرى باستخدام --target:
| الهدف | الأمر | يعمل من |
|---|---|---|
| محاكي iOS | nsh run my-app --target ios | macOS مع Xcode |
| Android | nsh run my-app --target android | macOS وWindows وLinux |
| macOS (AppKit) | nsh run my-app | macOS (الافتراضي) |
| Windows | nsh run my-app | Windows (الافتراضي) |
| Linux (GTK 4) | nsh run my-app | Linux (الافتراضي) |
| الويب | nsh run my-app --target web | macOS وWindows وLinux |
| Mac Catalyst | nsh run my-app --target catalyst | macOS |
#محاكي iOS
nsh run my-app --target ios
nsh run my-app --target ios --device "iPhone 16 Pro"
يستخدم nsh محاكياً يعمل، أو يشغّل أحدث iPhone إن لم يكن أي محاكٍ قيد التشغيل، ثم يثبّت التطبيق فيه (مشغّل تطبيق Rust، أو مضيف التطوير). ويختار --device محاكياً بالاسم أو بـ UDID.
#Android
nsh run my-app --target android
شغّل محاكياً أو وصّل جهازاً أولاً (يسردها adb devices؛ ويستخدم NSH أولها). يثبّت nsh التطبيق عليه: libnsh.so الخاص بتطبيق Rust، أو APK مضيف التطوير (لا يُعاد بناؤه إلا عند تغيّر بيئة التشغيل) الذي يُمرَّر منفذ خادم التطوير الخاص به إلى الجهاز بواسطة adb reverse، فيعمل عبر USB دون شبكة مشتركة.
#macOS (AppKit)
nsh run my-app --target macos
يُفتح التطبيق كتطبيق Mac لسطح المكتب (AppKit، في نافذة قابلة لتغيير الحجم)، مع إدخال بالفأرة ولوحة اللمس ولوحة المفاتيح بدلاً من اللمس. ويتطلب macOS 14 أو أحدث؛ راجع macOS.
#Windows (الافتراضي على Windows)
nsh run my-app
يُفتح التطبيق في نافذة سطح مكتب (1200×800 نقطة)، مع إدخال بالفأرة ولوحة اللمس واللمس ولوحة المفاتيح. ويتطلب Windows 10 الإصدار 1809 أو أحدث؛ راجع Windows.
#Linux (الافتراضي على Linux)
nsh run my-app
يُفتح التطبيق في نافذة GTK (390×844 نقطة). ويتطلب GTK 4.14 أو أحدث؛ راجع Linux.
#الويب
nsh run my-app --target web
يطبع nsh عنوان الصفحة؛ افتحه في متصفح. يعمل تطبيق Rust بصيغة WebAssembly مع حزمتك، وتحمّل الصفحة المفتوحة البناء الجديد من تلقاء نفسها بعد كل تغيير؛ راجع الويب.
#Mac Catalyst
nsh run my-app --target catalyst
يُفتح التطبيق في نافذة بحجم iPhone (390×844 نقطة) على جهاز Mac. وهذه أسرع طريقة لرؤية التطبيق كما يظهر على الهاتف: لا حاجة إلى تشغيل محاكٍ. وهو يستخدم UIKit مثل iOS، فما تراه هو ما يعرضه iPhone، باستثناء السلوك الخاص بنظام Mac مثل التمرير بعجلة الفأرة. أما لتطبيق Mac نفسه، فاستخدم --target macos.
#التعديل
اترك nsh run يعمل وغيّر نص الزر:
text("Add one")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground)<Text style={{ color: "white", fontSize: 17 }}>Add one</Text>احفظ. يُعاد بناء تطبيق Rust ويُعاد تشغيله بالنص الجديد؛ ويبدأ العدّاد من جديد. أما تطبيق TSX فيتلقى تحديثاً فورياً: يعيد CLI البناء ويرسل رقعة، وينفّذ المضيف الوحدة المتغيرة مرة أخرى ويعيد عرض المكوّنات التي تغيّرت شيفرتها فقط. وهو هنا App، الذي يملك الإشارة count، فيبدأ العدّاد من جديد أيضاً.
يمكن نقل أجزاء من الواجهة إلى مكوّنات خاصة بها، أو إلى دوال عادية تُرجع عناصر:
/// The app's button: a view that answers presses.
fn tap_button(on_press: impl Fn() + 'static) -> impl IntoElement {
view()
.p_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(on_press)
.child(text("Tap").text_size(px(17.0)).text_color(move || theme().primary_foreground))
}
// In App: .child(tap_button(move || count.update(|value| *value += 1)))function TapButton(props: { onPress: () => void }) {
return (
<View onPress={props.onPress} activeStyle={{ opacity: 0.6 }} style={{ padding: 12, borderRadius: 10, backgroundColor: "#0969da" }}>
<Text style={{ color: "white", fontSize: 17 }}>Tap</Text>
</View>
);
}
// In App: <TapButton onPress={() => count.update((c) => c + 1)} />في TSX، تبقى الحالة عند تعديل مكوّن لا يملكها: اضغط عدة مرات، ثم غيّر لون TapButton، فيتحدّث الزر ويبقى العدّ. يعمل الاستبدال الفوري لكل مكوّن من المستوى الأعلى: دالة باسم يبدأ بحرف كبير ويحوي جسمها JSX. يُعاد عرض المكوّن عندما تتغير شيفرته، أو عندما تتغير شيفرة الوحدة خارج المكوّنات (ثابت يستخدمه، أو دالة مساعدة). وإذا تعذّر تطبيق تعديل في مكانه (مثلاً لأنه يستورد وحدة NSH لم يستوردها التطبيق العامل بعد، أو يغيّر قدرات nsh.json)، يعيد المضيف تحميل التطبيق كاملاً.
#الأخطاء
يُطبع خطأ ترجمة Rust في الطرفية مع مخرجات cargo، ويبني nsh run من جديد عند الحفظ التالي. أما خطأ بناء TSX فيُطبع في الطرفية ويظهر على الجهاز كطبقة حمراء مع الملف والسطر والعمود؛ ويواصل التطبيق تشغيل آخر بناء سليم. وتختفي الطبقة عند البناء الناجح التالي (اضغط عليها لإخفائها قبل ذلك). ويُعرض الاستثناء الذي يُطلق وقت التشغيل بالطريقة نفسها، مع تتبّع المكدس مرتبطاً بمصدر .tsx لديك.
#استضافته في تطبيقك
يعرض التطبيق الأصلي NSH في عرض خاص به: NshAppView (وهو UIView) على iOS، وNshView (وهو View في Android) على Android، وAppView على macOS وWindows وLinux وفي تطبيقات GPUI. ضعه كأي عرض آخر، في أي شاشة، وبالعدد الذي تحتاجه.
الجذر المكتوب بـ Rust جزء من بناء التطبيق: يربط التطبيق حزمتك ويركّب الجذر باستخدام AppSource::Rust على AppView في macOS وWindows وLinux وGPUI وiOS (البناء باستخدام Rust). أما تطبيق TSX فهو وحدة يحمّلها العرض: من خادم التطوير أثناء التطوير، ومن حزمة مبنية أو حزمة موقّعة منزَّلة في الإصدار النهائي. تحمّل الأقسام التالية وحدات TSX.
#iOS
اربط NativeShellDev.xcframework في Debug وNativeShell.xcframework في Release (التضمين في تطبيقات iOS)، ثم:
import NativeShell
let app = NshAppView()
app.onError = { message in print("nsh error: \(message)") }
view.addSubview(app) // and constrain it like any view
#if DEBUG
try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!) // `nsh dev my-app` on your Mac
#else
try app.loadBundle(at: Bundle.main.url(forResource: "bundle", withExtension: "nshc", subdirectory: "nsh")!)
#endif
يقدّم nsh dev my-app الوحدة إلى التطبيق ويدفع كل عملية حفظ، كما يفعل nsh run مع مضيفه الخاص. وللبناء النهائي، يكتب nsh build my-app --release --out MyApp/nsh الحزمة في مجلد تضيفه إلى موارد التطبيق.
#Android
أضف nsh-dev.aar لبناءات التصحيح وnsh.aar لبناءات الإصدار (التضمين في تطبيقات Android)، ثم:
val nsh = NshView(this)
nsh.setErrorListener { message -> Log.e("nsh", message) }
setContentView(nsh) // or add it to any layout
if (BuildConfig.DEBUG) {
nsh.connectDevServer("ws://127.0.0.1:8790/ws") // with `adb reverse tcp:8790 tcp:8790`
} else {
nsh.loadAssetBundle("nsh") // assets/nsh, from `nsh build my-app --release --out app/src/main/assets/nsh`
}
#macOS وWindows وLinux
تضمّن تطبيقات سطح المكتب المكتوبة بـ Rust نظام NSH باستخدام AppView، من حزم nsh-platform-macos وnsh-platform-windows وnsh-platform-linux وnsh-platform-gpui: NSView، أو نافذة فرعية، أو عنصر واجهة GTK، أو عرض GPUI يركّب جذر Rust أو يحمّل حزمة مبنية. راجع macOS وWindows وLinux وGPUI، والمنصات لعرض كل المضيفات جنباً إلى جنب.
#ما الذي يجوز للوحدة فعله
على iOS وAndroid، يقرر المضيف قبل التحميل ما يجوز للوحدة فعله: يحدد grantCapabilities مضيفات الشبكة والقدرات الأخرى التي يجوز لها الوصول إليها، وتضع الحدود سقفاً لذاكرتها ووقت استدعاءاتها وعدد عقدها (الصندوق الرملي والقدرات). ولتزويد الوحدات بعروضك الأصلية الخاصة (خريطة، أو مخطط، أو مكوّن من نظام تصميم)، سجّلها بوصفها مكوّنات أصلية.
#نشر التحديثات كحزم موقّعة
تأتي وحدة TSX التي ينزّلها التطبيق بعد الإصدار في صورة حزمة موقّعة. أنشئ مفتاحاً جذرياً مرة واحدة واحتفظ به دون اتصال، ثم صدّق به مفتاح إصدار، وانشر:
nsh keygen keys/root && nsh keygen keys/release
nsh certify --root keys/root.secret --key keys/release.pub --apps 'com.example.*' -o keys/release.cert
nsh publish my-app --key keys/release.secret --cert keys/release.cert --build 2
يثق التطبيق بالمفتاح العام للجذر ويحمّل ما ينزّله؛ ويتحقق NSH من التوقيع والشهادة وقائمة الإبطال قبل تشغيل أي شيء:
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let trust = try NshTrust(stateDirectory: support.appendingPathComponent("nsh"))
try trust.addRoot(rootPublicKey) // the contents of keys/root.pub
let info = try await app.loadPackage(at: downloadedFile, trust: trust)
يعتمد Android التدفق نفسه مع NshTrust وNshPackage. وللتفاصيل عن الاستضافة والتسليم والإبطال: الحزم الموقّعة.
#أو أصدِره كتطبيق مستقل
يبني nsh package تطبيقًا قابلًا للتثبيت، سواء بـ Rust أو TSX، دون الحاجة إلى مشروع Xcode أو بناء Gradle خاص بك:
nsh package my-app --target ios-simulator --run
nsh package my-app --target android
nsh package my-app --target macos
nsh package my-app --target linux
nsh package my-app --target windows
nsh package my-app --target web
يُحزَّم تطبيق Rust مع مشغّله الخاص، المبني من الـ crate الخاص بك ومن crates الخاصة بـ NSH، مع تركيب جذره؛ أما تطبيق TSX فيُحزَّم مع بيئة التشغيل المبنية مسبقًا. الخيارات والتوقيع والأيقونات: Packaging.
#جرّبه في الطرفية
يشغّل nsh test التطبيق دون واجهة (headless)، بلا جهاز ولا نافذة. عند تمرير nsh.json لتطبيق Rust أو نقطة الدخول لتطبيق TSX، يفتح التطبيق ويتركه يعمل ثم يطبع ما يركّبه؛ ومع --trace يطبع أيضًا عمليات العرض الأصلية (native view operations) ووقت كل commit (Testing):
nsh test my-app/nsh.json --trace # a Rust app's root
nsh test my-app/App.tsx --trace # a TSX app's entry
وعند تمرير مجلد، يشغّل اختبارات التطبيق: يفتح App.test.tsx الخاص بكل قالب التطبيق، ويضغط زره، ويتحقق مما يعرضه (nsh test my-app). أما اختبار تطبيق Rust فيفتح جذره (open("."))؛ ويمكن اختباره أيضًا في cargo test باستخدام nsh_testing::Headless (Building with Rust).
#الخطوات التالية
- Building with Rust: تطبيقات Rust وسمتها (theme)، وTSX فوق crate بلغة Rust.
- JSX and components وReactivity: المكوّنات والإشارات (signals)، في Rust وTSX.
- Platforms: ما تدعمه كل منصة، وصفحتها الخاصة (iOS، Android، macOS، Windows، Linux).
- Signed packages وSandbox and capabilities: التسليم وما يُسمح للوحدات بفعله.
- Project structure: ما يوضع في
nsh.jsonوالأصول والملفات.