文档

用 Rust 开发

Native Shell 应用可以用 Rust 编写,也可以用 TSX,或者两者结合。三种方式运行在同一个引擎上:响应式图、渲染器、Taffy 布局以及原生模块(导航、对话框、键盘、状态栏)始终是 Rust,各平台都用自己的原生 UI 渲染。不同的是你自己的代码用什么语言,以及应用是否带着 JavaScript 虚拟机。

RustTSXRust + TSX
你的 UI、状态和事件处理RustTSX,运行在 QuickJS 上Rust 组件和模块,TSX 页面使用它们
应用运行的内容只有引擎和你的 crate,所有平台和浏览器都一样QuickJS 和引擎(浏览器里是它的 JavaScript 引擎)QuickJS 和引擎,加上你的 crate(浏览器里是它的 JavaScript 引擎)
适合业务复杂度高的中大型应用运行时下发的应用和模块、快速迭代Rust 核心加上经常变化的部分
修改代码重新构建并重启热更新TSX 热更新,Rust 重新构建并重启
创建nsh init my-app --template rustnsh init my-app --template tsnsh init my-app --template mixed

#如何选择

Rust 适合业务逻辑庞大、要求高的应用:交易、编辑器、数据密集的工具。整个应用都有 Rust 的类型系统和编译期检查,可以使用 crates.io 生态,你自己的代码(不只是 NSH 的代码)也是原生性能,还能用 cargo test 对真实 UI 做无界面测试。应用从头到尾都是 Rust 和原生代码:在 iOS、Android、macOS、Windows、Linux、GPUI 和 Web 上,应用里既不带也不运行 JavaScript 虚拟机。 组件是只运行一次的函数,把信号绑定到显示的内容上,和 TSX 一样(响应式),样式用类型化的方法而不是 class 字符串。

TSX 适合应用发布之后才到达的界面:原生应用在运行时加载的模块,由模型生成或由其他团队编写,以签名包下发,运行在宿主配置的沙箱里,开发时支持热更新。

Rust + TSX 保留 Rust 核心,在值得的地方加上 TSX:页面、设置、引导、实验性功能,任何需要热更新快速迭代、或作为签名包 在运行时下发的部分。TSX 像导入其他 NSH 模块一样导入你的 Rust 组件和模块,两部分通过类型化的接口衔接。TSX 运行在 QuickJS 上,这时应用会带上 QuickJS。

#完全用 Rust 编写的应用

nsh init my-app --template rust
cd my-app
nsh run                        # 本机的后端;--target ios、android、web、macos、linux 或 windows
nsh package --target web       # 或 ios、ios-simulator、android、macos、linux、windows

src/lib.rs 包含整个应用:组件、状态、事件处理和任务。

use nsh_base::{button, theme};
use nsh_core::prelude::*;

/// TSX 通过 `app/native` 可以使用的组件。
#[native_module("app/native")]
fn native(registry: &mut Registry) {
    registry.register::<App>();
}

#[component]
pub fn App() -> impl IntoElement {
    let count = signal(0_u32);
    let quote = signal(SharedString::from("…"));
    // 异步工作在任务里运行;请求走 NSH 的网络栈。
    spawn(async move {
        if let Ok(response) = fetch("https://api.example.com/quote").await {
            quote.set(response.text().into());
        }
    });
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_4()
        .child(text(move || format!("Count: {}", count.get())))
        .child(text(quote))
        .child(
            // nsh_base 的无样式按钮,用主题的颜色绘制。
            button()
                .px(px(20.0))
                .py(px(12.0))
                .rounded(move || theme().radius)
                .bg(move || theme().primary)
                .active(|s| s.bg(move || theme().primary.opacity(0.9)))
                .on_press(move || count.update(|c| *c += 1))
                .child(text("Tap").text_color(move || theme().primary_foreground)),
        )
}

元素和组件用同样的方式创建:一个函数创建它(view()、text(..)、nsh_base 的 button()、你的 #[component] fn Chart 生成的 chart()),它的方法设置 props(每个 prop 一个设置方法,接受值、signal 或闭包)和监听器(每个事件一个方法)。 监听器是接收事件的闭包(move |text: &str| ..),没有数据的事件写成 move || ..;.child(..) 接受一个元素,或元素的 Option、Vec、数组、迭代器(.child(rows.iter().map(|r| row(r))))。

nsh.json 指定根组件,nsh 负责挂载它:

{
  "name": "My App",
  "rust": ".",
  "root": "App",
  "theme": "theme.json",
  "appearance": "system"
}

"root" 是组件在 crate 中的路径(App、shell::Gallery;有多个 crate 时在第一个中);"theme" 是它的组件绘制时使用的主题,"appearance" 和 TSX 应用一样作用于它。 有了它,对应用目录执行 nsh run 和 nsh package 时,会为目标平台构建应用自己的运行器,直接挂载根组件,不链接 QuickJS:

目标运行的内容
macOS、Windows、Linux带有你的 crate 的发布运行器,在窗口中运行;nsh run 使用 debug 构建,并在终端显示应用日志
iOS、iOS 模拟器、Mac Catalyst带有你的 crate 的 iOS 运行器
Android带有你的 crate 的应用 libnsh.so,由运行时的 Activity 启动
Web包含引擎和你的 crate 的 nsh.wasm(整个 Rust gallery 为 3.3 MB,brotli 压缩后 0.75 MB);页面直接挂载根组件

nsh 只构建你的 crate 需要的东西:一个由你的 crate 和 NSH 核心组成的小程序,它向 CLI 描述 crate 的组件和模块,其余工作由 CLI 自己完成;再加上你运行或打包的每个目标的运行器。新 Rust 应用的第一次构建只编译你的 crate 和 NSH 的核心,大约 60 个 crate。nsh test 在同样构建的、包含你的 crate 的运行器中以无界面方式运行应用的测试。

在 Linux 上,由应用 crate 选择可选的平台功能。使用网页视图时,在 nsh-platform-linux 依赖上启用 webview feature;这会链接 WebKitGTK 6.0,构建和运行时都需要安装该系统库。生成的运行器通过 Cargo 的 feature 合并继承应用的选择。

nsh run 期间,修改 crate 的 Rust 代码会重新构建运行器并重启应用;在浏览器中,已打开的页面会自动加载新的运行时。 cargo run 在桌面后端上直接启动同一个根组件(src/main.rs);嵌入 NSH 的原生应用在 macOS、Windows、Linux、GPUI 和 iOS 的 AppView 上用 AppSource::Rust 挂载 Rust 根组件。

要在包括 Web 在内的所有平台上运行的代码,使用 NSH 提供的服务:HTTP 用 fetch(网络), 等待用 timer、set_timeout 和 set_interval,时钟用 nsh_core::time::Instant 和 nsh_core::time::unix_ms()。 background 和 blocking 在原生目标的线程上运行任务。

#桌面标题栏

Rust 应用通过 nsh_core::element::desktop 绘制自定义标题栏。调用 set_title_bar(TitleBarStyle::Hidden, Some(height)),将背景设为 .window_drag(true),并通过 title_bar() 为系统窗口按钮留出空间。 返回值的 top 是标题栏高度,left 和 right 是两侧系统按钮占用的宽度。 读取会订阅变化,包括进入和退出全屏。平台负责窗口拖动和用户设置的标题栏双击行为; 子元素上的 .window_drag(false) 可以排除父元素的拖动区域。

use nsh_core::{
    desktop::TitleBarStyle,
    element::{desktop::{close_window, set_title_bar, title_bar}}, prelude::*,
};

fn window_bar() -> impl IntoElement {
    set_title_bar(TitleBarStyle::Hidden, Some(px(42.0)));
    view()
        .h(px(42.0))
        .flex_row()
        .items_center()
        .window_drag(true)
        .pl(move || title_bar().left + px(12.0))
        .pr(move || title_bar().right + px(12.0))
        .child(text("My app").flex_1())
        .child(text("Close").window_drag(false).on_press(close_window))
}

set_title_bar(TitleBarStyle::Default, None) 恢复系统标题栏。 close_window() 遵循宿主的正常窗口关闭行为。 这个模块还有窗口的其他函数(set_title、minimize、toggle_full_screen……) 和菜单(set_application_menu、popup_menu):见 nsh/desktop。

#文件和文件夹

在桌面平台上,Rust 代码可以显示系统的文件对话框,并把本地路径交给系统处理, 每个结果都在任务中等待:pick_paths 接受 OpenDialog (OpenDialog::files()、::folders() 或 ::files_and_folders()),pick_save_path 接受 SaveDialog,open_path(用文件的默认应用打开)、reveal_path (在访达、文件资源管理器或文件管理器中选中该项)和 move_to_trash (macOS 告知时返回该项移到的位置)。用户取消时对话框返回 Ok(None), 无法显示时返回错误(Linux 上没有文件选择 portal 时,以及 iOS、Android 和 Web 上; 在这些平台上 open_path、reveal_path 和 move_to_trash 也返回错误)。 请求失败时一律返回错误,不返回 false。

use nsh_core::{
    file_dialog::{FileFilter, OpenDialog},
    prelude::*,
};

fn open_notes() {
    let picked = pick_paths(
        OpenDialog::files()
            .multiple(true)
            .filter(FileFilter::new("Markdown", ["md", "markdown"]))
            .button_label("打开"),
    );
    spawn(async move {
        match picked.await {
            Ok(Some(paths)) => {
                for path in paths {
                    let _ = reveal_path(path).await;
                }
            }
            Ok(None) => {} // 用户取消。
            Err(error) => log::warn!("no file dialog: {error}"),
        }
    });
}

#用 TSX 扩展 Rust 应用

crate 的 #[native_module("app/native")] 函数声明 TSX 可以导入的内容:组件(registry.register::<Chart>())、函数模块 (registry.module::<Portfolio>(),在单元结构体及其 impl 上标注 #[module])、类(registry.class::<Ledger>(),在结构体及其 impl 上标注 #[class])和资源;这个属性还会在 crate 根部生成 pub fn registry(),给测试和其他 Rust 代码使用同一个集合 (my_app_native::registry(),与 nsh_base::registry() 并列)。在 #[module] 或 #[class] 的 impl 中,pub fn(包括 pub(crate))会被导出;没有可见性修饰的函数是 辅助函数,只留在 Rust 中。它们的 TypeScript 声明(包括文档注释)由 Rust 类型生成到 .nsh/types,所以 nsh check 和编辑器会据此检查 TSX:

/// 价格图表。
#[derive(Clone, PartialEq, Props)]
pub struct ChartProps {
    /// 要绘制的代码。
    pub symbol: Prop<SharedString>,
    /// 显示的时间范围。
    #[prop(default = SharedString::from("1D"))]
    pub range: Prop<SharedString>,
}

#[derive(Event)]
pub enum ChartEvent {
    /// 选中了一个价格。
    #[args(price)]
    Select(f64),
}

/// 价格图表;按下时选中最新价格。
#[component]
pub fn Chart(props: ChartProps, cx: Scope<ChartEvent>) -> impl IntoElement {
    view()
        .h(px(160.0))
        .bg(move || theme().muted)
        .on_press(move || cx.emit(ChartEvent::Select(101.5)))
        .child(text(move || format!("{} · {}", props.symbol.get(), props.range.get())))
}

/// A ledger kept in Rust.
#[class]
pub struct Ledger {
    entries: Vec<f64>,
}

#[class]
impl Ledger {
    /// An empty ledger.
    pub fn new() -> Self {
        Ledger { entries: Vec::new() }
    }

    /// Adds `amount`; the new balance.
    pub fn add(&mut self, amount: f64) -> f64 {
        self.entries.push(amount);
        self.balance()
    }

    /// The balance.
    #[getter]
    pub fn balance(&self) -> f64 {
        self.entries.iter().sum()
    }

    /// Uploads the entries; resolves when the server has them.
    pub async fn upload(this: Instance<Ledger>, url: String) -> Result<(), String> {
        let body = this.with(|l| format!("{:?}", l.entries));
        fetch(HttpRequest::post(url, body)).await.map(|_| ())
    }
}

/// 应用的持仓。
#[module]
pub struct Portfolio;

#[module]
impl Portfolio {
    /// The value of the holdings.
    pub fn total() -> f64 {
        1250.0
    }
}
import { mount, signal, View, Text } from "nsh";
import { Chart, Ledger, Portfolio } from "app/native";   // Rust

const ledger = new Ledger();   // 状态保存在 Rust 中

function Overview() {
  const balance = signal(ledger.add(Portfolio.total()));   // Rust,立即调用
  return (
    <View class="gap-2 p-4">
      <Text>Balance: {balance()}</Text>
      <Chart symbol="NSH" range="1M" onSelect={(e) => balance.set(ledger.add(e.price))} />
      <View onPress={() => ledger.upload("https://example.com/ledger")} role="button" class="rounded-lg bg-blue-600 px-4 py-2 active:bg-blue-700">
        <Text class="text-white">Upload</Text>
      </View>
    </View>
  );
}

mount(() => <Overview />);

每个导出在 TypeScript 中的文档来自给它命名的那个 Rust 项的文档注释:组件来自 #[component] fn(Chart),模块和类来自它们的 结构体(Portfolio、Ledger),props、事件、函数和成员来自它们自己的文档注释(有 getter 的 setter 不需要)。Props 结构体和 事件枚举本身的文档注释,以及 impl 块上的文档注释,只留在 Rust 中;在 #[module] 或 #[class] 的 impl 块上写文档注释是编译 错误,因为 TypeScript 不会显示它。每个写入 .nsh/types 的命令都会对 TSX 看到的、没有文档注释的导出给出警告,并写出它的名字 (Ledger.add、Chart.range)。

TypeScript 只显示文档注释中第一个标题之前的部分。写给 Rust 调用者的说明,例如 &self 借用了什么、函数为什么接收 cx,放在标题 # Rust(或 rustdoc 的 # Panics、# Examples)下面,只留在 Rust 文档中:

/// 把 `text` 放到剪贴板;resolve 为复制的字符数。
///
/// # Rust
///
/// 接收 `cx` 让它成为一个 op,与 TS 在它之前做的 UI 改动按顺序执行。
pub fn copy(text: String, cx: &mut ModuleCx) -> u32 {
    let count = text.chars().count() as u32;
    cx.set_clipboard(text);
    count
}

props 是组件 Props 结构体的字段,事件是 Event 枚举的变体(onSelect),都用驼峰命名。Rust 代码用组件的构造函数放置同一个组件,每个 prop 一个设置方法,每个事件一个监听方法:chart().symbol("NSH").range("1M").on_select(move |price: &f64| last.set(*price))。Rust 组件自己的内容、事件和状态留在 Rust 中;TSX 传入 props、接收事件。类型不对的 prop(u32 类型的 prop 传 level={1.5})和内置元素上一样给出警告:这个 prop 保持原值,组件的其余部分照常渲染。

函数和成员按 Rust 签名运行。不是 async 的在 TSX 调用中直接运行并返回值(Portfolio.total() 是数字,ledger.balance 是属性): 它们计算并保存状态。async fn 返回 Promise,作为任务运行,可以使用 NSH 的服务(fetch、timer、对话框);带 cx: &mut ModuleCx 参数的模块函数与 UI 按顺序作用于平台(toast、状态栏),组件 ref 的方法也是这样;它们的返回类型决定 TSX 得到什么:返回 () 的得到 void,没有回复(TSX 调用后继续往下执行);返回 Result<(), E> 的得到 Promise<void>,遇到 Err 时以其消息 reject(调用与 UI 的修改一起运行,那时 TSX 的调用已经返回,所以由 Promise 带回错误;没人 await 或 catch 的会记录为 未处理的 rejection);返回值的得到 Promise<T>。参数和返回值的类型来自签名:数字、 字符串、Option、Vec、你的 #[derive(JsObject)] 结构体和 #[derive(PropEnum)] 枚举、你的类的实例,以及 TSX 函数 (Callback<A, R>:Rust 在调用中立即调用它,或之后用 post 和 call_async 调用;Callback<f64> 在 TS 中是 (value: number) => void,#[args(balance)] listener: Callback<f64> 给参数命名,得到 (balance: number) => void)。开始监听的函数命名为 on(event, listener),返回 Stop(Stop::new(|| …)),TSX 得到的是停止监听的函数(() => void);它只运行一次,在 TSX 调用它时, 或在调用 on 的一方(组件,或热重载下模块的这次运行)消失时。事件处理函数 接收一个对象:变体的值是它的字段 value,在变体上写 #[args(price)] 可以给它命名(onSelect({ price })), 有多个值的变体用 #[args(open, reason)] 给各个字段命名。类型不对的参数在调用中抛出 TypeError, 返回 Promise 的调用则以它 reject;Err 以其消息抛出(或 reject)。类的对象拥有它的 Rust 值:对象被回收时释放,或调用 ledger.dispose()(也可以 using ledger = new Ledger())立即释放。指定 TSX 入口来运行它:nsh run App.tsx 或 nsh package App.tsx --target …。TSX 的修改是热更新, Rust 的修改会重新构建并重启应用。CLI 根据 crate 的描述知道它导出了什么,自己打包和编译 TSX;开发宿主和 nsh test 的 无界面运行器则与你的 crate 和 QuickJS 一起构建。

仓库中的 examples/rust-exports 是导出的参考示例:一个小型 TSX 应用,它的 crate 导出一个组件(必填、有默认值和可选的 props,一个事件,子元素)、一个模块(每种函数各一个)和一个类(构造函数、方法、getter 和 setter、静态成员、async 成员、以另一个实例为参数、 Rust 用 post 调用的回调、dispose() 和 using),每个都有简短的文档注释;它的测试(Rust 的,以及 TypeScript 的 App.test.tsx)在应用中逐一点按,并检查每个导出显示的内容。

仓库中的 examples/gallery-rs 是一个完整的 Rust 应用:每个 demo 都是 Rust,nsh run examples/gallery-rs --target web (或 ios、android 等)运行它的根组件,nsh test examples/gallery-rs 以无界面方式运行它的测试,cargo run -p gallery-native 在桌面后端上启动它。

#渲染函数与应用的类

组件用渲染函数为它拥有的值(它的一行数据、文档中的一个代码块)绘制应用的内容:一个 Option<Renderer<T>> 属性,在 TypeScript 中是 (value: T) => JSX.Element。组件在内容所在的位置调用 render(key, value),得到一个视图,像其他视图一样设定样式并放置(在 text(..) 中是一个行内视图)。只要值不变,内容按 key 保留;值改变时重新渲染。Rust 父组件把闭包传给该属性的设置方法(quote().render_badge(|symbol| ..)),由它生成 Renderer。

组件中的样式是类型化的;应用的 Tailwind 类通过一个类名属性和该部件元素上的 .app_class(..) 作用于组件的某个部件。它们在元素自身的样式之后应用,并使用应用的主题、dark: 和各种状态,就像元素上的 class 一样。

/// A quote: its symbol, then content of the app's.
#[derive(Clone, Copy, PartialEq, Props)]
pub struct QuoteProps {
    /// The symbol.
    pub symbol: Prop<SharedString>,
    /// Tailwind classes of the symbol's text.
    pub symbol_class: Prop<Option<SharedString>>,
    /// Content of the app's after the symbol (a change, a badge).
    pub render_badge: Prop<Option<Renderer<SharedString>>>,
}

/// A quote of a symbol.
#[component]
pub fn Quote(props: QuoteProps) -> impl IntoElement {
    view()
        .flex_row()
        .items_center()
        .gap_2()
        .child(text(props.symbol).app_class(props.symbol_class))
        .child(dynamic(move || {
            props.render_badge.get().map(|r| r.render("badge", props.symbol.get()).flex_1())
        }))
}
<Quote
  symbol="NSH"
  symbolClass="text-lg font-semibold"
  renderBadge={(symbol) => <Text class="text-green-600">{`${symbol} ▲ 2.4%`}</Text>}
/>

nsh/base 中的 TextView 就是这样构建的:它的 renderCodeBlock、renderMath 等是渲染函数,它的 classes 是应用按部件给出的类(TextView)。

#测试

Rust 应用的测试以无界面方式运行它,可以在 cargo test 中,也可以在 nsh test 中,使用同一个驱动:运行在真实的引擎上,没有窗口,以 60 Hz 提供显示帧,所以定时器、任务和动画都像在屏幕上一样运行。驱动启动应用,维护一份它所显示视图的模型,并在用选择器指定的视图上执行步骤(press、type_text、submit、scroll、wait_for),选择器可以是文本、无障碍标签或 test id(text:、label:、id:);步骤无法执行时,会带着屏幕上的文字失败。

在 cargo test 中,nsh_testing::Headless(nsh-testing 的默认特性,不含 QuickJS)运行根组件(app.screen():屏幕上的文字、应用发送的命令);EngineConfig::new 是一个手机尺寸、其余都未设置的引擎。nsh test 在用你的 crate 构建的无界面运行器中运行 TypeScript 测试(测试),其中 open(".") 启动 Rust 应用的根组件:

[dev-dependencies]
nsh-engine = "0.1"
nsh-testing = "0.1"
// tests/app.rs:cargo test
use nsh_engine::{EngineConfig, Root};
use nsh_testing::Headless;

#[test]
fn a_tap_counts() {
    let mut app = Headless::start(EngineConfig::new(Root::of::<my_app::App>())).unwrap();
    app.wait_for("Count: 0").unwrap();
    app.press("Tap").unwrap();
    app.wait_for("Count: 1").unwrap();
}

TSX 入口用 nsh-testing 的特性 bundle 按 nsh run 的方式构建,使用它的宿主提供的 UI 集合:NSH 的,以及每个 crate 由 #[native_module] 生成的 registry():

[dev-dependencies]
nsh-testing = { version = "0.1", features = ["bundle"] }
nsh-base = "0.1"
#[test]
fn the_module_formats_money() {
    let sets = vec![nsh_base::registry(), my_app_native::registry()];
    let mut app = nsh_testing::tsx("../App.tsx", sets).unwrap();
    app.wait_for("Formatted at once: $1,234.50").unwrap();
}

nsh_testing::compile_tsx 只构建一次入口供多个测试使用,nsh_testing::start 把批次交给自己维护屏幕模型的测试。 examples/rust-exports 用这种方式测试它的 TSX 入口;examples/gallery-rs 用 TypeScript 测试它的 Rust 根组件, 大致每个 demo 一个测试文件。

#主题

应用的外观由应用自己决定:NSH 提供基础元素和 nsh_base 中无样式的基础组件,你的 crate 是其上带样式的一层,即应用自己的组件库(三层结构)。Rust 组件的颜色、圆角和字体取自主题,即 nsh_base 的 theme(),所以一个文件就能改变应用中所有组件的样式。每种颜色都有浅色和深色两个值;系统切换外观时, .bg(move || theme().primary) 这样的绑定会原生地改变颜色,不需要重新渲染。

主题采用 shadcn/ui 的设计令牌,默认主题是 shadcn 的 neutral 主题:黑白配色,圆角 10 点。 要使用自己的主题,在 nsh.json 中把 "theme" 设为主题文件的路径(相对于 nsh.json),或者直接写入主题内容:

{
  "name": "My App",
  "rust": ".",
  "theme": "brand-theme.json"
}

主题文件就是 shadcn 的 cssVars,即 shadcn 主题和 registry 条目的格式,所以从 shadcn 的主题或 tweakcn 导出的主题可以原样使用。light 设置浅色外观的值,dark 设置深色外观的值,theme 设置两者共用的值:

{
  "cssVars": {
    "theme": {
      "radius": "0.5rem",
      "font-sans": "Inter, sans-serif"
    },
    "light": {
      "primary": "oklch(0.541 0.281 293.009)",
      "primary-foreground": "oklch(0.969 0.016 293.756)",
      "ring": "var(--primary)",
      "up": "#16a34a"
    },
    "dark": {
      "primary": "oklch(0.702 0.183 293.541)",
      "primary-foreground": "oklch(0.283 0.141 291.089)",
      "up": "#4ade80"
    }
  }
}
  • 你的主题叠加在默认主题之上,只需列出要修改的部分。light 设置而 dark 省略的值在两种外观中都使用。
  • 值按 CSS 的写法书写。颜色可以是任意 CSS 颜色(#7c3aed、rgb(…)、hsl(…)、oklch(…)、white)、 color-mix(in oklab, var(--primary) 80%, white),或同一外观的另一个令牌 var(--primary)(可带后备值:var(--brand, #7c3aed))。 长度使用 rem(16 点)或 px(点)。字体是字体族列表:使用其中第一个非通用的字体族,第一个是通用字体族(system-ui、sans-serif)时使用系统字体。
  • 应用自己的令牌。 其他任何名称都是应用的令牌,就像 Tailwind @theme 中的变量:上例的 "up" 在 Rust 中是 theme().color("up"), 在类名中是 bg-up / text-up/70,在 TSX 中是 token("up")(主题 token)。数值如 "radius-card": "14px" 是 theme().number("radius-card") 和 rounded-(--radius-card)。
  • 打包器在构建时检查主题:文件缺失、JSON 无效或不是 cssVars 格式的主题都会让构建失败,并报告 nsh.json: theme: … 错误。

令牌及其默认的浅色和深色值(shadcn 的 neutral):

令牌theme()浅色深色用途
background / foregroundbackground / foregroundoklch(1 0 0) / oklch(0.145 0 0)oklch(0.145 0 0) / oklch(0.985 0 0)应用的背景,以及其上的文字
card / card-foregroundcard / card_foregroundoklch(1 0 0) / oklch(0.145 0 0)oklch(0.205 0 0) / oklch(0.985 0 0)卡片等分组表面,以及其上的文字
popover / popover-foregroundpopover / popover_foregroundoklch(1 0 0) / oklch(0.145 0 0)oklch(0.205 0 0) / oklch(0.985 0 0)弹出框、菜单和提示,以及其上的文字
primary / primary-foregroundprimary / primary_foregroundoklch(0.205 0 0) / oklch(0.985 0 0)oklch(0.922 0 0) / oklch(0.205 0 0)主要操作(主按钮),以及其上的文字
secondary / secondary-foregroundsecondary / secondary_foregroundoklch(0.97 0 0) / oklch(0.205 0 0)oklch(0.269 0 0) / oklch(0.985 0 0)次要操作,以及其上的文字
muted / muted-foregroundmuted / muted_foregroundoklch(0.97 0 0) / oklch(0.556 0 0)oklch(0.269 0 0) / oklch(0.708 0 0)低调的表面;次要文字:说明、标签、占位符
accent / accent-foregroundaccent / accent_foregroundoklch(0.97 0 0) / oklch(0.205 0 0)oklch(0.269 0 0) / oklch(0.985 0 0)悬停或高亮的项,例如菜单项、列表行,以及其文字
destructivedestructiveoklch(0.577 0.245 27.325)oklch(0.704 0.191 22.216)破坏性操作和错误
borderborderoklch(0.922 0 0)oklch(1 0 0 / 10%)分隔线和边框
inputinputoklch(0.922 0 0)oklch(1 0 0 / 15%)输入框的边框
ringringoklch(0.708 0 0)oklch(0.556 0 0)焦点环
chart-1 … chart-5chart_1 … chart_5oklch(0.87 0 0) … oklch(0.269 0 0)同浅色图表的各个系列
sidebar、sidebar-foreground、sidebar-primary、sidebar-primary-foreground、sidebar-accent、sidebar-accent-foreground、sidebar-border、sidebar-ringsidebar、sidebar_foreground 等sidebar oklch(0.985 0 0),sidebar-primary oklch(0.205 0 0)sidebar oklch(0.205 0 0),sidebar-primary oklch(0.488 0.243 264.376)侧边栏的表面、主要操作、高亮项、边框和焦点环
radiusradius0.625rem(10 点)圆角
font-sansfont_sans系统字体应用的字体:没有设置自己字体族的文字都使用它
font-monofont_mono系统等宽字体等宽字体:font-mono、TextView 中的代码

#多个 crate

应用的 Rust 代码可以拆分成多个 crate,每个 crate 用自己的名称导入:"rust" 可以是 crate 路径的列表,列表中的每个 crate 都有自己的 #[native_module] 函数。

{
  "rust": ["native/charts", "native/storage"],
  "root": "App"
}
import { Chart } from "app/charts";        // native/charts:#[native_module("app/charts")]
import { Store } from "app/storage";       // native/storage:#[native_module("app/storage")]

每个运行器和宿主都链接全部这些 crate,.nsh/types 为每个名称生成声明。crate 之间通过 Cargo 路径依赖共享代码 (shared = { path = "../shared" }):一个公司组件 crate 可以被多个 crate 使用、被多个应用列出,只有 "rust" 中列出的 crate 会导出给 TSX。列出的每个 crate 都有自己的包名和导入名称;重复时 nsh 会报告共用它的两个路径。Rust 应用的 "root" 是列出的第一个 crate 中的组件。nsh run 和 nsh dev 期间,修改其中任何一个 crate,或它们通过路径依赖的 crate,都会重新构建并重启应用。

#在 Web 上使用浏览器 API

在 Web 上,crate 可以通过 wasm-bindgen 和 web-sys 调用浏览器的 API(WebSocket、IndexedDB 等);nsh 会随运行时一起构建它们所需的胶水代码。见 Web。

#另请参阅

  • 项目结构:nsh.json 的每个键。
  • Web:浏览器中的 Rust 应用。
  • 网络:在 Rust 中使用 fetch。
  • CLI:nsh init --template rust 和 --template mixed。