مرجع توسعهدهنده و هوش مصنوعی برای ورکاسپیس Rustinaz/code-template.
این سند مکمل README.fa.md است. README توضیح میدهد قالب چیست و چطور اجرا میشود؛ این راهنما توضیح میدهد قالب چطور ساخته شده و چطور باید داخلش کد نوشت — قرارداد crateبهcrate، قواعدی که یک تغییر نباید بشکند، و دستورهای آمادهٔ کار.
هدف این است که هم یک آدم و هم یک دستیار هوش مصنوعی بتواند بدون حدسزدن از روی آن کار کند: هر مسیر، هر crate و هر API عمومی که اینجا آمده، واقعاً در همین مخزن وجود دارد.
- 🇬🇧 English guide: TEMPLATE.md
- 🇮🇷 شروع سریع (فارسی): README.fa.md
- 🇬🇧 Getting started (English): README.md
نویسنده: سینا خانزاده · khanzadeh.1377@gmail.com · sina-khanzadeh.ir اینستاگرام / تلگرام / دیسکورد: programmer_1998 مجوز: MIT OR Apache-2.0
- چطور از این سند استفاده کنیم
- قالب چیست
- معماری در یک نگاه
- درخت کامل فایلها
- crateها، یکییکی
- لایهٔ UI با جزئیات
- اپلیکیشنها
- قرارداد سیم و بکاندِ همبسته
- فیچرها و ماتریس
cfg - دستورها: بیلد، اجرا، تست، لینت
- یکپارچهسازی مداوم (CI)
- دستورهای آماده: چطور چیزی اضافه کنیم
- قواعدی که یک تغییر نباید بشکند
- عیبیابی و تلهها
- نکات برای دستیارهای هوش مصنوعی
- منابع و راه ارتباطی
اگر توسعهدهندهٔ انسانی هستی: §۲ تا §۳ را بخوان تا مدل ذهنی را بگیری، §۵ را بهعنوان نقشه مرور کن، و هر وقت چیزی اضافه میکنی سراغ §۱۲ (دستورهای آماده) برو. §۱۴ تلههای رایج را جمع کرده است.
اگر دستیار هوش مصنوعی هستی: پیش از نوشتن هر کدی،
- §۳ را بخوان (گراف وابستگی — تعیین میکند کد جدید کجا باید باشد)،
- §۵ را بخوان (کدام crate مسئول چیست)،
- دستور آمادهٔ متناظر را در §۱۲ بردار،
- §۱۳ (قواعد) و §۹ (فیچرها /
cfg) را رعایت کن، - برای اثبات تغییر، دستورهای §۱۰ را اجرا کن.
Crate، ماژول یا API از خودت نساز. هرچه وجود دارد در §۴ و §۵ فهرست شده. اگر چیزی آنجا نبود، §۱۲ میگوید جایش کجاست.
Rustinaz یک قالب اپلیکیشن کراسپلتفرم با رویکرد «اول راست» است. یک ورکاسپیس اینها را میسازد:
| هدف | خروجی | نقطهٔ ورود |
|---|---|---|
| لینوکس / ویندوز / مک | یک باینری نیتیو دسکتاپ | apps/example/src/main.rs → example_app::run() |
| اندروید | یک .so که android.app.NativeActivity لود میکند |
apps/example/src/lib.rs → android_main() |
| iOS | یک کتابخانهٔ استاتیک برای لینکشدن در اپ Xcode | apps/example/src/lib.rs (نوع staticlib در crate) |
| وب | یک .wasm + index.html که Trunk بستهبندی میکند |
apps/example/src/lib.rs → run_web() |
| (هرکدام) | بکاند JSON همبسته | apps/server/src/main.rs |
شرط قالب ساده است: UI را یکبار با egui بنویس و همان درخت ویجتها را به هر هدف بفرست. فقط چیزهایی که واقعاً بین سیستمعاملها تفاوت دارند — ساخت پنجره، تم پیشفرض، اطلاعات دستگاه، اعلامهای دسترسپذیری، JNI — در کد مخصوص هر پلتفرم میمانند.
هیچ کد اپلیکیشنی به جاوا، کاتلین، Swift یا HTML/CSS وجود ندارد. پروژهٔ اندروید
(android-app/) فقط اسکریپتهای بیلد Gradle (Kotlin DSL) و فایلهای منبع XML (manifest، تم،
آیکون) دارد — APK صفر فایل .class دارد و همهٔ پیکسلها را .so راست میکشد.
اینها واقعی هستند، گرفتهشده از cargo metadata. جهت کاملاً یکطرفه است؛ هیچچیز پایینتر
به چیزی بالاتر وابسته نمیشود.
┌──────────────┐
│ shared │ دامنه، کانفیگ، خطاها، api، سرویسها (بدون UI و OS)
└──────┬───────┘
┌───────────────┼────────────────┬─────────────────┐
│ │ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼───────┐ ┌──────▼────────┐
│ ui │ │ resources │ │ build-config │ │ platform/* │
│ (egui، │ │ (رشتهها، │ │ (نسخه، │ │ (linux، …) │
│ توکنها، │ │ رنگها) │ │ امضا) │ │ │
│ کامپوننتها)│ └──────┬──────┘ └──────────────┘ └──────┬────────┘
└──────┬──────┘ │ │
│ │ ui هم وابستگی هر │
│ │ crate از platform/* است ───┘
└───────────────┴───────────────┬─────────────────┘
│
┌─────────▼──────────┐ ┌──────────┐
│ apps/example │ │apps/server│
│ (خود اپلیکیشن) │ │ (بکاند) │
└────────────────────┘ └─────┬────┘
│
تنها shared
بخوانید:
sharedبه هیچ چیزی داخل پروژه وابسته نیست. تایپهای دامنه، کانفیگ، خطاها، قرارداد سیم (api) و traitهای سرویس + پیادهسازیهای in-memory.uiبهsharedوابسته است. توکنهای تم، ریاضیات layout، ناوبری، کتابخانهٔ کامپوننت و traitهایPlatformUi/Window.platform/*بهsharedوuiوابستهاند. روی traitهایuiکار سیستمعامل را اضافه میکنند.resourcesوbuild-configبهsharedوابستهاند.apps/exampleبهshared،ui،resourcesو هر شش crateplatform/*وابسته است (هر کدام با#[cfg(target_os = …)]مهار شدهاند؛ §۷.۱).apps/serverفقط بهsharedوابسته است — بکاند هیچوقتuiیا crate پلتفرم را وارد باینریاش نمیکند، پس صفر کد GUI حمل میکند.
دو چیز «platform» نام دارند.
uiپیادهسازیهایPlatformUiخودش را زیرcrates/ui/src/platform/دارد، در حالی که crateهای رو به سیستمعامل درcrates/platform/*هستند. فقط این دومیها عضو مستقل ورکاسپیساند و بهuiوابستهاند — نه برعکس.
دسکتاپ و اندروید از همان ترتیب راهاندازی در apps/example/src/lib.rs عبور میکنند:
main() / android_main() / run_web()
│
▼
bootstrap(&CliOptions)
│ ۱. AppConfig::load() (یا --config PATH)
│ ۲. اعمال overrideهای --platform / --server
│ ۳. shared::init_blocking(config) → tracing + globalها
│ ۴. platform_*::init() → آمادهسازی OS (هر هدف)
│ ۵. ResourceManager::init_global(config) → رشتهها/رنگها/تمها
│ ۶. UiBuilder::new(config)
│ .platform(platform) → تصحیح میزبان → دستگاه
│ .framework(UiFramework::Egui)
│ .build() → UiContext
▼
eframe::run_native(...) (دسکتاپ) | WebRunner::start(...) (وب)
│
▼
ExampleApp::update(&egui::Context) → رسم با egui، خواندن از UiContext
UiContext (در crates/ui) همان شیئی است که UI از آن میخواند: AppConfig مشترک،
DeviceInfo تشخیصدادهشده، ThemeTokens حلشده و UiFramework فعلی.
هر چیزی که پایین آمده در مخزن وجود دارد (خروجیهای تولیدشده مثل target/،
apps/example/dist/ و android-app/build/ حذف شدهاند).
code-template/
├── Cargo.toml ورکاسپیس: ۱۲ عضو، نسخهٔ وابستگیها، پروفایلها
├── Cargo.lock عمداً کامیت شده: بیلد اول قابلتکرار
├── rust-toolchain.toml قفل stable + rustfmt + clippy (ادیتور و CI یکی)
├── .editorconfig UTF-8، LF، تودرتوی ۴ (۲ برای TOML/YAML)
├── .gitignore
├── .cargo/
│ └── config.toml تنظیمات شبکه + aliasهای کوتاه (بدون linker ثابت)
├── .github/
│ ├── dependabot.yml
│ └── workflows/ci.yml شش دروازهٔ CI
├── .vscode/ رانکانفیگ، taskها، تنظیم rust-analyzer، افزونهها
│
├── apps/
│ ├── example/ ← اپی که واقعاً ویرایش میکنی
│ │ ├── Cargo.toml crate-type = rlib, cdylib, staticlib؛ وابستگی per-target
│ │ ├── Trunk.toml کانفیگ بیلد وب
│ │ ├── index.html همان <canvas> که eframe در آن mount میشود (id = the_canvas_id)
│ │ └── src/
│ │ ├── lib.rs run()، run_web()، android_main()، bootstrap()، خود UI
│ │ ├── main.rs باینری نازک دسکتاپ → example_app::run()
│ │ └── backend.rs پنل Backend کلاینت (نیتیو) / جایگزین (وب)
│ └── server/ بکاند همبسته
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs روتینگ: نگاشت مسیرها به هندلرها؛ serve()
│ ├── http.rs پارس/نوشتن دستی HTTP/1.1
│ └── main.rs باینری clap (--bind، --no-demo)، seed دمو
│
├── crates/
│ ├── shared/ منطق کسبوکار (بدون UI، بدون OS)
│ │ ├── Cargo.toml فیچر network اینجاست
│ │ └── src/
│ │ ├── lib.rs init()/init_blocking()، re-exportها، alias شبکه
│ │ ├── domain/mod.rs User، AppSettings، DeviceInfo، Platform، ThemeTokens، Route، …
│ │ ├── config/mod.rs AppConfig، ConfigBuilder، ThemeMode، FeatureFlags
│ │ ├── errors/mod.rs AppError + زیرخطاها، ErrorContext
│ │ ├── api/mod.rs قرارداد سیم: endpointها + تایپهای درخواست/پاسخ
│ │ └── services/
│ │ ├── mod.rs traitهای سرویس، پیادهسازیهای in-memory، ServiceContainer، factory
│ │ └── http.rs HttpNetworkService: کلاینت بلوکینگ HTTP/1.1 فقط با std
│ ├── ui/ UI مشترک (کتابخانهٔ ویجت اینجاست)
│ │ ├── Cargo.toml فیچر egui اینجاست (پیشفرض روشن)
│ │ └── src/
│ │ ├── lib.rs UiContext، UiBuilder، UiFramework، هلپرهای responsive
│ │ ├── theme/mod.rs ThemeManager، ThemeBuilder، تمهای آماده، خروجی CSS
│ │ ├── layout/mod.rs LayoutCalculator، FlexLayout، GridLayout، builderها
│ │ ├── navigation/mod.rs NavigationController، deep link، کانفیگ bottom-nav/drawer
│ │ ├── components/ کتابخانهٔ کامپوننت
│ │ │ ├── mod.rs trait Component، ComponentProps، هلپرهای رنگ، re-export
│ │ │ ├── app_bar.rs AppBar (+ BACK_ACTION)
│ │ │ ├── button.rs Button، ButtonStyle، MIN_TOUCH_TARGET
│ │ │ ├── card.rs Card
│ │ │ ├── dialog.rs Dialog، DialogState
│ │ │ ├── navigation_bar.rs NavItem، NavigationBar
│ │ │ ├── scaffold.rs Scaffold، ScaffoldLayout، Slot
│ │ │ └── text_field.rs TextField، TextFieldState
│ │ ├── platform/
│ │ │ ├── mod.rs traitهای PlatformUi + Window، PlatformTheme، PlatformFactory،
│ │ │ │ رفتارهای keyboard/pointer/text-input
│ │ │ ├── linux.rs LinuxUi / LinuxWindow (پیادهسازیهای درونcrate)
│ │ │ ├── windows.rs WindowsUi / WindowsWindow
│ │ │ ├── macos.rs MacosUi / MacosWindow
│ │ │ ├── android.rs AndroidUi / AndroidWindow
│ │ │ ├── ios.rs IosUi / IosWindow
│ │ │ └── web.rs WebUi / WebWindow
│ │ ├── hooks/mod.rs هست ولی declare نشده (هنوز بخشی از egui نیست)
│ │ └── utils/mod.rs هست ولی declare نشده (هنوز بخشی از egui نیست)
│ ├── resources/ رشتهها، رنگها، تمها، assetها (بارگذاری در زمان اجرا)
│ │ ├── Cargo.toml
│ │ └── src/lib.rs ResourceManager، ResourceBuilder، StringMap، ColorMap
│ ├── build-config/ متادیتای نسخه / هدف / امضا
│ │ ├── Cargo.toml
│ │ └── src/lib.rs BuildTimeConfig، VersionInfo، platform_build_config، امضا
│ └── platform/ چسب سیستمعامل (یک crate بهازای هر هدف)
│ ├── linux/ src/lib.rs platform_info()، init()
│ ├── windows/ src/lib.rs init()، هلپرهای پنجره/تم
│ ├── macos/ src/lib.rs init()، هلپرهای پنجره/تم
│ ├── android/ src/lib.rs AndroidApp، install_app/current_app، device_info()، JNI، init()
│ ├── ios/ src/lib.rs IosInfo، device_info()، init()، هپتیک
│ └── web/ src/lib.rs user agent، viewport، تشخیص لمس
│
├── android-app/ پروژهٔ بستهبندی Android Studio (بدون سورس اپ)
│ ├── settings.gradle.kts
│ ├── build.gradle.kts
│ ├── gradle.properties
│ ├── gradlew / gradlew.bat
│ ├── gradle/wrapper/… jar + properties ریپر
│ └── app/
│ ├── build.gradle.kts Cargo را per-ABI اجرا و سپس .so را بستهبندی میکند
│ ├── proguard-rules.pro
│ └── src/main/
│ ├── AndroidManifest.xml به android.app.NativeActivity اشاره میکند
│ └── res/… رشتهها، تم، آیکون لانچر
│
├── resources/ فایلهای دادهی مشترک
│ ├── colors/colors.json
│ └── strings/strings_en.json · strings_es.json
│
└── scripts/ یک اسکریپت per-platform، بهعلاوه حلقهٔ dev
├── android-env.sh linker/CC یادNDK را برای Cargo export میکند (source کن)
├── build-all.sh هر هدفی که این ماشین میتواند را بساز
├── build-linux.sh build-windows.sh build-macos.sh build-ios.sh
├── build-android.sh build-web.sh
└── dev.sh بکاند را بالا میآورد، صبر میکند پورت جواب دهد، کلاینت را اجرا میکند
crate منطق کسبوکار. بدون egui، بدون تایپ سیستمعامل. از ریشهٔ crate
(crates/shared/src/lib.rs) دوباره export شده تا کد پاییندست بتواند use shared::… بنویسد.
| ماژول | مسئول | اقلام عمومی کلیدی |
|---|---|---|
shared::domain |
مدل دادهی اصلی | EntityId (= Uuid)، Timestamp، traitهای Entity/RepositoryEntity، User، UserPreferences، AppSettings، AccessibilitySettings، WindowState، WindowPosition، DeviceInfo، Platform، FormFactor، ScreenSize، Orientation، SafeArea، Route، ThemePreference، ThemeTokens، ColorTokens/ColorRole/ColorValue، TypographyTokens، SpacingTokens، ShapeTokens، MotionTokens، ElevationTokens، Shadow، BuildConfig، SigningConfig |
shared::config |
کانفیگ زمان اجرا | AppConfig، ConfigBuilder، AppInfo، PlatformConfig، UiConfig، ThemeConfig، ThemeMode، ResponsiveBreakpoints، NetworkConfig، StorageConfig، LoggingConfig، LogLevel/LogFormat/LogOutput، FeatureFlags. AppConfig::load()، ::save()، ::device_info()، ::theme_tokens()، ::is_feature_enabled() |
shared::errors |
تنها تایپ خطا | AppError (میپیچد ConfigError، NetworkError، StorageError، AuthError، ValidationError، PlatformError، UiError، SerializationError و همچنین Io، Http، Json، NotFound، Timeout، Unsupported، Multiple)، Result<T>، ErrorContext/ErrorContextExt |
shared::api |
قرارداد سیم | endpoints::{HEALTH, USERS, USERS_SEARCH}، HealthResponse، ErrorResponse، RegisterRequest، UserList، … |
shared::services |
traitهای سرویس + پیادهسازی in-memory | traitهای AuthService، UserService، SettingsService، NetworkService، StorageService، PlatformService، AnalyticsService، NotificationService؛ InMemoryRepository<T>؛ ServiceContainer، AppServices، PlatformServices؛ services::defaults::*؛ services::factory::create_services()؛ HttpNetworkService |
راهاندازی. shared::init_blocking(config) -> Result<Arc<AppConfig>> کانفیگ را لود میکند،
tracing را نصب میکند و کانفیگ مشترک را برمیگرداند. shared::init() همان است ولی async،
برای فراخوانی از داخل یک runtime. از init_blocking در main، android_main یا هر نقطهٔ
ورود extern "C" استفاده کن.
ترفند alias شبکه. shared یک نام را دوباره export میکند، DefaultNetworkService، که
یا کلاینت مبتنی بر reqwest است (فیچر network) یا StubNetworkService (پیشفرض). کد
فراخوان در هر دو حالت یکسان است و در حالت stub در زمان اجرا فقط AppError::Unsupported
میگیرد. HttpNetworkService جداگانه است و همیشه بیرون وب در دسترس است.
crate UI مشترک. دو نیمه که با فیچر egui (پیشفرض روشن) از هم جدا میشوند:
- منطق خالص (همیشه کامپایل میشود): توکنها، ریاضیات layout، builderها، state و اندازهگیری کامپوننت.
- رندر (
#[cfg(feature = "egui")]): متدهایshow(&mut egui::Ui, …).
| ماژول | اقلام عمومی کلیدی |
|---|---|
ui (ریشه) |
UiFramework، UiContext، UiBuilder، ماژول ui::responsive |
ui::theme |
ThemeManager، ThemeBuilder، ThemeChangeListener؛ تمهای آماده material3()، ios()، material_you()، high_contrast()، minimal()؛ generate_css() |
ui::layout |
LayoutConstraints، LayoutCalculator، FlexLayout، GridLayout، LayoutBuilder، FlexLayoutBuilder، GridLayoutBuilder، LayoutDirection، FlexDirection، JustifyContent، AlignItems، AlignContent، FlexWrap، GridAutoFlow |
ui::navigation |
NavigationController، NavEntry، NavDirection، DeepLinkHandler، NavigationBuilder، NavigationListener، BottomNavConfig/BottomNavItem، NavigationDrawerConfig/DrawerHeader/DrawerItem/DrawerFooter |
ui::components |
§۵.۲.۱ را ببین |
ui::platform |
PlatformUi، Window، WindowConfig، PlatformTheme، PlatformColors، PlatformFonts، PlatformSpacing، PlatformShapes، PlatformMotion، PlatformFactory، و هلپرهای keyboard/pointer/text_input |
هر کامپوننت همان شکل دوتکه را دارد: منطق خالص (builderها، state، اندازهگیری) که با خاموشی
egui هم کامپایل میشود، و متد show(…) که پشت #[cfg(feature = "egui")] است.
| کامپوننت | ساخت با | رندر با |
|---|---|---|
AppBar |
AppBar::new(title) + .subtitle()، .action()، .back_enabled()، .center_title()، .elevated() |
show(ui, …) (فیچر egui) |
Button |
Button::new(label) + .style(ButtonStyle)، .size(ComponentSize)، .enabled()، .icon(char)، .full_width() |
یک ButtonResponse برمیگرداند |
Card |
Card::new(title) + .subtitle()، .elevation(f32)، .padding_multiplier()، .clickable() |
show(ui, ctx) -> ButtonResponse / show_with(…) |
Dialog |
Dialog::new(title) + .body()، .dismissable()، .visible()، .add_action() |
show(…)، با یک DialogState |
NavigationBar |
NavigationBar::new(Vec<NavItem>) + .items()، .select()، .select_next()، .show_labels() |
show(…) |
Scaffold |
ScaffoldLayout::new(app_bar, navigation_bar, body) از Slotها |
ریاضیات layout؛ ScaffoldResponse |
TextField |
TextField::new()؛ state با TextFieldState::new(placeholder) + .with_max_length()، .with_password()، .with_disabled()، .set_value()، .take_changed() |
show(…) |
همچنین عمومی: trait Component، ComponentProps، ComponentVariant، ComponentSize،
ComponentState، AccessibilityRole، ButtonResponse، MIN_TOUCH_TARGET، BACK_ACTION و
هلپرهای رنگ role_value، value، role، token، with_alpha، state_fill.
یک crate برای هر هدف. هرکدام به shared و ui وابستهاند و یک API کوچک و امری بهعلاوه یک
پیادهسازی PlatformUi (هرجا سیستمعامل حرفی داشته باشد: پنجره، تم، دسترسپذیری) ارائه میدهند.
| crate | سطح عمومی | از کجا استفاده میشود |
|---|---|---|
platform-linux |
platform_info()، PlatformInfo، init() |
init_platform() در apps/example |
platform-windows |
init()، هلپرهای پنجره/تم |
apps/example |
platform-macos |
init()، هلپرهای پنجره/تم |
apps/example |
platform-android |
AndroidApp، install_app()، current_app()، device_info()، AndroidInfo، init()، material3_theme()، AndroidPlatformUi، AndroidWindow |
android_main() در apps/example |
platform-ios |
IosInfo، device_info()، init()، هپتیک |
apps/example |
platform-web |
تشخیص user-agent، viewport و لمس | apps/example (از مسیر هدف وب) |
crate
uiهمچنین پیادهسازیهایPlatformUiمخصوص هر پلتفرم را زیرcrates/ui/src/platform/*.rsدارد. این تفکیک عمدی است:ui::platformانتزاعها و پیادهسازیهای پیشفرض را تعریف میکند؛ crateهایcrates/platform/*کار واقعی سیستمعامل را انجام میدهند (JNI، پنجرهٔ نیتیو،uname،Win32،web-sys). وقتی سیستمعامل لازم استcrates/platform/*را ویرایش کن؛ وقتی شکل/قرارداد لازم استcrates/ui/src/platform/mod.rsرا.
بارگذاری منابع در زمان اجرا: رشتهها، رنگها، تمها، assetها و فونتها، از پوشهٔ resources/
و کششده در سطح کل پروسه.
ResourceManager::init_global(config) -> Arc<ResourceManager>،::global()،::global_or_init()load_all()،reload()،add_resource_dir(path)- گترها:
get_string(key, locale)،get_string_formatted(…)،get_color(name)،get_theme(name)،current_theme()،get_asset(name)،get_asset_data(name)،get_font(name)،get_raw(name) ResourceBuilder+StringMap/ColorMapبرای ساختن منابع در کد
فایلهای داده: resources/strings/strings_en.json، resources/strings/strings_es.json،
resources/colors/colors.json.
متادیتای نسخه، هدف و امضا.
generate_build_metadata() -> BuildTimeConfigباVersionInfo،GitInfo،RustcInfo،TargetInfoplatform_build_config(platform) -> BuildConfig- امضا:
load_signing_config،save_signing_config،android_signing_config،ios_signing_config BuildTarget::all_targets()،target_for_platform(platform)- یک
Cli/Commandsکوچکclapبرای چاپ اطلاعات بیلد
UiContext همان چیزی است که کد رسم میخواند؛ UiBuilder همان چیزی است که آن را در زمان
راهاندازی میسازد.
// crates/ui/src/lib.rs
pub struct UiContext {
pub config: Arc<RwLock<AppConfig>>, // parking_lot::RwLock
pub device_info: DeviceInfo,
pub theme_tokens: ThemeTokens,
pub framework: UiFramework,
}
impl UiContext {
pub fn platform(&self) -> Platform;
pub fn is_mobile(&self) -> bool;
pub fn is_desktop(&self) -> bool;
pub fn theme_preference(&self) -> ThemePreference;
pub fn color_tokens(&self) -> &ColorTokens;
pub fn spacing(&self) -> &SpacingTokens;
pub fn typography(&self) -> &TypographyTokens;
}let ui_context = UiBuilder::new(config) // Arc<RwLock<AppConfig>>
.platform(Platform::Android) // دستگاه، نه میزبان بیلد
.framework(UiFramework::Egui) // egui تنها بکاند فعلی است
.build();.platform(...) روی اندروید مهم است، جایی که فایل کانفیگ پلتفرمِ میزبان بیلد را ثبت کرده.
.device_info(...) کل DeviceInfo تشخیصدادهشده را جایگزین میکند.
ThemeManager یک ThemeTokens نگه میدارد و روی تغییر به listenerها اطلاع میدهد.
ThemeBuilder توکنها را میسازد؛ تمهای آماده material3()، ios()، material_you()،
high_contrast() و minimal() هستند. generate_css(&tokens) برای سطح وب CSS تولید میکند.
LayoutCalculator::new(device_info) به سؤالهای responsive که ویجتها میپرسند جواب میدهد:
grid_columns(base)، spacing(base)، font_scale(base)، is_compact()، is_two_pane()،
safe_area()، breakpoint()، is_landscape() / is_portrait().
builderهای LayoutBuilder، FlexLayoutBuilder، GridLayoutBuilder یک layout فلکس یا گرید را
بهصورت اعلانی توصیف میکنند و ui::responsive هلپرهای آزاد میدهد (grid_columns،
spacing_multiplier، font_scale، is_compact، is_two_pane).
NavigationController یک پشتهٔ مسیر نگه میدارد (navigate، push، pop، replace،
reset، current، previous، stack، can_go_back، depth، clear) و به
NavigationListenerها اطلاع میدهد. DeepLinkHandler الگوهای URL را به Route نگاشت میکند.
NavigationBuilder::new(initial_route).route(name, route).deep_link(pattern, resolver).build().
// crates/ui/src/platform/mod.rs
pub trait PlatformUi: Send + Sync {
fn platform(&self) -> Platform;
fn create_window(&self, config: WindowConfig) -> Result<Box<dyn Window>>;
fn theme(&self) -> PlatformTheme;
fn announce_for_accessibility(&self, text: &str);
// …بهعلاوهٔ هلپرهای رفتار که پایین میآیند
}
pub struct PlatformFactory;
impl PlatformFactory {
pub fn create(platform: Platform) -> Box<dyn PlatformUi> { /* … */ }
}PlatformTheme سه گروه را کنار هم میگذارد: PlatformColors، PlatformFonts،
PlatformSpacing، PlatformShapes و PlatformMotion. زیرماژولهای keyboard، pointer و
text_input قراردادهای پلتفرم را ترجمه میکنند (platform_key_mapping،
platform_pointer_behavior، platform_text_input_behavior و تایپهای
KeyCode/PointerType/TextInputConfig).
کل اپلیکیشن در کتابخانه (src/lib.rs) زندگی میکند، با یک باینری نازک (src/main.rs) تا
همان کد تبدیل شود به باینری دسکتاپ، .so اندروید، .wasm وب و .a آیاواس. نوعهای crate در
Cargo.toml اعلام شدهاند: crate-type = ["rlib", "cdylib", "staticlib"] و نام کتابخانه
example_app.
نقاط ورود
| نقطهٔ ورود | هدف | نکته |
|---|---|---|
run() |
دسکتاپ | #[cfg(not(target_arch = "wasm32"))]؛ CLI را پارس میکند، boot و run_egui |
run_web() |
وب | #[wasm_bindgen(start)]، async؛ <canvas id="the_canvas_id"> را پیدا میکند |
android_main(app) |
اندروید | #[no_mangle] extern "C"؛ install_app(app) سپس boot و اجرا |
CLI (build_cli() / parse_args() → CliOptions): --platform، --framework، --debug
(-d)، --config (-c PATH)، --server URL. --server برای یک اجرا network.base_url را
override میکند؛ پیشفرضِ توکار از قبل به بکاند همبسته اشاره میکند.
وابستگیهای پلتفرم در Cargo.toml با cfg مهار شدهاند، پس بیلد دسکتاپ هیچوقت کد JNI یا
web-sys را کامپایل نمیکند:
[target.'cfg(target_os = "android")'.dependencies]
winit = { workspace = true }
android-activity = { workspace = true }
platform-android = { path = "../../crates/platform/android" }
[target.'cfg(target_os = "linux")'.dependencies]
platform-linux = { path = "../../crates/platform/linux" }
# …و windows / macos / ios / wasm32مرورگر ساعت ندارد. wasm32-unknown-unknown روی Instant::now() و SystemTime::now()
abort میکند. هر چیزی که زمان لازم دارد از web_time استفاده میکند که روی هدفهای نیتیو همان
std::time است و در مرورگر performance.now()/Date.now().
پنل بکاند (src/backend.rs) بدون قفلکردن UI به سرور همبسته درخواست میزند: درخواستها
روی یک worker thread اجرا میشوند و نتیجه را از طریق یک کانال mpsc برمیگردانند. روی وب — که
سوکت خام وجود ندارد — بهجای تظاهر به اتصال یک جایگزین نشان میدهد.
یک کتابخانه + باینری: منطق روتینگ/HTTP بدون باز کردن سوکت قابل تست است (server::Server،
server::serve) و main.rs باینری clap است.
cargo run --package server # ۱۲۷.۰.۰.۱:۸۰۸۰، کاربران دمو seed میشوند
cargo run --package server -- --bind 0.0.0.0:8080 # قابلدسترس از امولاتور / LAN
cargo run --package server -- --no-demo # مخزن کاربر خالیسطح عمومی: Server::new(services)، Server::handle(&Request) -> Response، Server::services()،
serve(listener, server)، DEFAULT_LIMIT = 50، MAX_LIMIT = 200. کد سیم HTTP/1.1 دستی
نوشته شده (src/http.rs)؛ درزِ جایدادن یک فریمورک مثل axum همان جدول روتینگ در src/lib.rs
است. باینری دو کاربر دمو seed میکند (alice@example.com، bob@example.com، رمز
password123) مگر --no-demo داده شود.
قرارداد یکبار نوشته میشود در crates/shared/src/api/mod.rs و هر دو سر آن را import
میکنند، پس کلاینت و سرور از هم دور نمیافتند:
pub mod endpoints {
pub const HEALTH: &str = "/api/health";
pub const USERS: &str = "/api/users";
pub const USERS_SEARCH: &str = "/api/users/search";
}- سمت سرور:
apps/server/src/lib.rsثابتهایendpointsرا به هندلرها نگاشت میکند. - سمت کلاینت:
shared::services::HttpNetworkService— یک کلاینت بلوکینگ HTTP/1.1 ساختهشده رویstd::net،#[cfg(not(target_arch = "wasm32"))]، که فقطhttp://حرف میزند. ازreqwestاستفاده نمیکند، پس نه فیچرnetworkلازم دارد نه پشتهٔ TLS، و بیلدهای اندروید/وب بدون TLS میمانند.
let client = HttpNetworkService::new("http://127.0.0.1:8080", Duration::from_millis(5_000));
let health = client.get_json(endpoints::HEALTH)?;
let created = client.post_json(endpoints::USERS, &serde_json::to_value(&request)?)?;هر دو نیمه را با scripts/dev.sh با هم اجرا کن (بکاند را بالا میآورد، صبر میکند تا پورت
جواب دهد، کلاینت را با --server به آن اشاره میدهد و با خروج کلاینت بکاند را متوقف میکند).
روی اندروید اول پورت میزبان را فوروارد کن: adb reverse tcp:8080 tcp:8080.
| فیچر | crate | پیشفرض | اثر |
|---|---|---|---|
network |
shared |
خاموش | کلاینت شبکهٔ reqwest (TLS) را فعال میکند. خاموش ⇒ StubNetworkService که AppError::Unsupported برمیگرداند و بدون هیچ زنجیرهٔ ابزار C همهجا کراسکامپایل میشود. |
egui |
ui |
روشن | منطق مشترک کامپوننتها را به ویجت واقعی تبدیل میکند. خاموش ⇒ crate باز هم بیلد میشود (crateهای پلتفرم به توکنها/traitهایش نیاز دارند) اما متدهای show(…) حذف میشوند. |
hot_reload |
resources |
خاموش | برای بارگذاری مجدد منابع در آینده رزرو شده. |
صادقانه تأیید کن:
cargo check -p shared # شبکهٔ stub، بدون TLS
cargo build -p shared --features network # کلاینت واقعی reqwest
cargo check -p ui --no-default-features # کامپوننتها بدون رندررcfg |
مربوط به | چه چیزی را مهار میکند |
|---|---|---|
target_os = "android" |
اندروید | android_main، JNI، platform-android، چسب winit اندروید |
target_os = "linux" / "windows" / "macos" / "ios" |
همان سیستمعامل | crate platform-* متناظر و شاخهٔ init() |
target_arch = "wasm32" |
وب | run_web، WEB_CANVAS_ID، web-sys، getrandom/js، uuid/js |
not(target_arch = "wasm32") |
بقیه | run()، native_options()، HttpNetworkService |
feature = "egui" |
ui |
همهٔ show(…)های کامپوننتها |
feature = "network" |
shared |
کلاینت reqwest |
قاعدهٔ سرانگشتی: کدی که نمیتواند روی یک هدف وجود داشته باشد باید #[cfg] شود، نه با if
در زمان اجرا. قالب این را برای run/run_web/android_main الگو کرده است.
cargo check --workspace --all-targets # ۱. همهچیز، هر crate و هدف
cargo test --workspace --all-targets # ۲. تستها
cargo clippy --workspace --all-targets --all-features -- -D warnings # ۳. لینت = خطابهعلاوهٔ cargo fmt --all. CI همینها را اجرا میکند و همچنین
cargo doc --workspace --no-deps با RUSTDOCFLAGS=-D warnings.
در .cargo/config.toml تعریف شدهاند. هیچکدام عمداً همنام یک زیرفرمان واقعی Cargo نیست (alias
همنام به خودش resolve میشود و بینهایت بازگشت میکند).
| alias | معادل |
|---|---|
cargo c |
check --workspace --all-targets |
cargo t |
test --workspace --all-targets |
cargo b / cargo br |
build --workspace / --release |
cargo f |
fmt --all |
cargo l |
clippy --workspace --all-targets |
cargo check-all |
check --workspace --all-targets --all-features |
cargo server |
run --package server |
cargo deps |
tree --workspace --depth 2 |
cargo docs |
doc --workspace --no-deps |
cargo run --package example-app # دسکتاپ، URL پیشفرض بکاند
cargo run --package example-app -- --debug # -d برای لاگ دیباگ
cargo run --package example-app -- --server http://192.168.1.20:8080
scripts/build-all.sh --list # این ماشین چه چیزی میتواند بسازد
scripts/build-all.sh # همه را بساز
scripts/build-all.sh android web # فقط اینها
scripts/build-linux.sh [--release]
scripts/build-android.sh [--release] [--abi arm64-v8a] [--apk]
scripts/build-windows.sh [--release] # MinGW-w64، یا روی ویندوز اجرا کن
scripts/build-macos.sh [--release] # فقط macOS
scripts/build-ios.sh [--simulator] # فقط macOS
scripts/build-web.sh [--serve] # نیازمند trunk
scripts/dev.sh [--release] [--bind ADDR] [--server-only|--client-only]اندروید بدون Gradle (فقط .so):
source scripts/android-env.sh
cargo build --package example-app --target aarch64-linux-android --release
# -> target/aarch64-linux-android/release/libexample_app.so.github/workflows/ci.yml دروازهها را تعریف میکند. یک تغییر تنها وقتی «تمام» است که همهشان
پاس شوند.
| Job | Runner | چه چیزی را ثابت میکند |
|---|---|---|
checks |
ubuntu | cargo fmt --check، clippy … -D warnings، check --all-features، test --all-features، doc -D warnings |
backend |
ubuntu | server را میسازد، اجرا میکند و /api/health، /api/users، /api/users/search?q=… را curl میکند |
cross-compile |
ubuntu (ماتریس) | اندروید (۴ ABI) libexample_app.so تولید میکند؛ وب با Trunk بیلد میشود |
android-apk |
ubuntu | JDK 17 + NDK + Gradle یک .apk دیباگ واقعی میسازند |
desktop |
ubuntu / macos / windows (ماتریس) | باینری نیتیو روی هر سیستمعامل بیلد میشود |
ios |
macos | کتابخانهٔ استاتیک iOS کامپایل میشود (امضا خارج از حوزهٔ CI است) |
دامنهٔ صادقانه: CI کامپایل، لینت و تست روی هر هدف را ثابت میکند؛ نه یک بیلد امضاشدهٔ فروشگاهی را، و اپ را روی دستگاه فیزیکی اجرا نمیکند. برای بیانیهٔ صراحت خود پروژه بخش «چه چیزی تست شده و چه چیزی نه» را در README ببین.
هر دستور میگوید کدام فایل را باید دست بزنی و کدام قواعد (§۱۳) را رعایت کنی.
- یک variant به
Routeدرcrates/shared/src/domain/mod.rsاضافه کن. - با یک
NavigationController(ui::navigation) به آن برو —navigate(route, NavDirection::Forward). - آن را داخل
ExampleApp::update(یاeframe::Appخودت) با match رویcontroller.current()رندر کن و یک هلپرdraw_*صدا بزن. - کد رسم را در اپ (
apps/example) نگه دار — مدل مسیر جایاش درsharedاست.
crates/ui/src/components/<name>.rsرا بساز.- اول منطق خالص: struct، یک
new(...)، متدهای builder و هر ریاضیات اندازه/state — همه بدون تایپهایeguiتا با خاموشی فیچرeguiهم کامپایل شود. - متد رندر را پشت
#[cfg(feature = "egui")] pub fn show(&self, ui: &mut egui::Ui, ctx: &UiContext)بگذار. - از
crates/ui/src/components/mod.rsدوباره export کن (pub use <name>::…;). - برای منطقی که بدون
eguiاجرا میشود یک تست واحد بگذار.
pub struct Badge { text: String, count: u32 }
impl Badge {
pub fn new(text: impl Into<String>) -> Self { /* … */ }
pub fn count(mut self, count: u32) -> Self { self.count = count; self }
#[cfg(feature = "egui")]
pub fn show(&self, ui: &mut egui::Ui, ctx: &UiContext) {
let color = components::token(ctx, &ctx.color_tokens().primary);
ui.colored_label(color, format!("{} ({})", self.text, self.count));
}
}- مسیر را به
shared::api::endpointsو تایپهای درخواست/پاسخ را بهcrates/shared/src/api/mod.rsاضافه کن (هر دوserde::Serialize + Deserialize). - در
apps/server/src/lib.rsهندلش کن (match روی مسیر، تولید یکResponse). - از کلاینت با
HttpNetworkService::get_json/post_jsonصدا بزن. - هر دو سر روی همان تایپ کامپایل میشوند — یک فیلد را تغییر نام بده و سر دیگر از کامپایل میافتد.
- اگر endpoint کاربرمحور است یک بررسی دودی شبیه job
backendاضافه کن.
- آن را در
crates/shared/src/domain/mod.rsتعریف کن وEntity(و اگر در مخزن مینشیندRepositoryEntity) را پیاده کن. - برای ذخیره از
InMemoryRepository::<T>::new()استفاده کن، یا به یک trait سرویس موجود درcrates/shared/src/services/mod.rsمتد اضافه کن و درservices::defaultsپیادهاش کن. - آن را در
ServiceContainer/AppServicesدرdefaults::*و درservices::factory::create_servicesسیمکشی کن. - داخل
sharedبرگرداندنshared::errors::Result<T>(AppError) را بهanyhowترجیح بده.
- اگر یک قرارداد است (متدی که هر پلتفرم پاسخ میدهد): به
PlatformUi/Windowدرcrates/ui/src/platform/mod.rsاضافه کن و پیشفرض را درcrates/ui/src/platform/*.rsپیاده کن. - اگر کار واقعی OS است (JNI،
Win32،uname،web-sys، هپتیک): درcrates/platform/<os>/src/lib.rsمتناظر و پشت#[cfg(target_os = "…")]پیاده کن. - یک تابع امری کوچک (مثل
platform_android::device_info()) بیرون بده و ازinit_platform/نقطهٔ ورود اپ صدا بزن، مهارشده با#[cfg].
- توکنها را برنامهنویسیشده با
ThemeBuilderتغییر بده (primary_color،secondary_color،font_family،base_font_size،spacing_scale،border_radius_scale،motion_speed). - یا یکی از تمهای آماده را بردار:
material3()،ios()،material_you()،high_contrast()،minimal(). - در زمان اجرا
ThemeManagerیک تم زنده را بهروز میکند وgenerate_css()آن را برای وب export میکند. - انتخاب تم کاربر در
AppConfig.ui.theme.mode(ThemeMode) مینشیند.
- کلید را به
resources/strings/strings_en.json(وstrings_es.json) اضافه کن. - با
ResourceManager::global().get_string("key", Some("en"))بخوان، یا برای placeholderهاget_string_formatted(…). ResourceManager::load_all()/reload()کش را تازه میکند.
- برای یک اجرا:
cargo run --package example-app -- --server http://host:port(برایdev.shهم--serverهست). - بهصورت دائمی:
network.base_urlرا در کانفیگی کهAppConfig::save()مینویسد تغییر بده. - روی اندروید اول
adb reverse tcp:8080 tcp:8080(لوکالهاست دستگاه ≠ لوکالهاست میزبان).
- از
apps/example/src/backend.rsالگو بگیر: یک worker thread بساز، فراخوانی بلوکینگHttpNetworkServiceرا آنجا انجام بده و نتیجه را از طریق کانالmpscبرگردان کهupdate()هر فریم تخلیهاش میکند. هیچوقت کلاینت بلوکینگ را روی ترد egui صدا نزن. - روی وب کلاینت تقلبی نفرست؛ یک جایگزین رندر کن (سوکت خام وجود ندارد) یا روی مسیر
wasm32ازweb_sys::fetchاستفاده کن.
[lib] nameدرapps/example/Cargo.tomlرا عوض کن و دو شناسهٔ اندروید که باید با آن یکی باشند را بهروز کن (android.app.lib_nameدرAndroidManifest.xmlوrustLibNameدر Gradle).WINDOW_TITLEدرapps/example/src/lib.rsرا عوض کن.- متادیتای
[workspace.package](repository،homepage،authors،description) را درCargo.tomlریشه بهروز کن. - application id پروژهٔ
android-appو رشتههایresources/را بهروز کن.
اینها قواعدیاند که ورکاسپیس را کراسکامپایلپذیر و ادعاهای README را درست نگه میدارند.
- وابستگیها یکطرفهاند.
sharedبه هیچ چیز داخلی وابسته نیست؛ui/resources/build-configفقط بهshared؛platform/*بهshared+ui؛apps/serverفقط بهshared. هرگزsharedیاuiرا به یک crate پلتفرم وابسته نکن — این چرخه میسازد وcargo check --workspaceروی ماشینی بدون SDK هم باید کار کند. sharedو نیمهٔ خالصuiهیچeguiو هیچ تایپ OS ندارند.- منطق خالص هر کامپوننت با خاموشی
eguiهم کامپایل میشود.cargo check -p ui --no-default-featuresباید سبز بماند. - قرارداد سیم فقط در
shared::apiزندگی میکند. هر دو سر importش میکنند؛ DTO را تکرار نکن. - نقاط ورودی که نمیتوانند روی یک هدف وجود داشته باشند
#[cfg]میشوند، نه شاخهٔ زمان اجرا. - زمان از
web_timeمیگذرد، هرگز مستقیمstd::time::Instant::now()/SystemTime::now()، تا هدف wasm abort نکند. - نام کتابخانهٔ اندروید بارِ معنایی دارد: باید در
Cargo.toml، manifest و Gradle یکی باشد. - Clippy با
-D warningsوall،pedantic،nurseryوcargoروی warn است (لینت ورکاسپیس). کد جدید باید بدون خطای لینت باشد. Cargo.lockکامیتشده میماند وpanic = "abort"از پروفایل release بیرون میماند (وگرنهcargo test --releaseبیاستفاده میشود).- فیچر
networkپیشفرض خاموش میماند وHttpNetworkServiceبدونreqwestمیماند، تا بیلدهای اندروید/وب بدون زنجیرهٔ ابزار TLS/C کراسکامپایل شوند.
| نشانه | علت / راهحل |
|---|---|
web-sys «no method named class_list/style/head» |
web-sys یک متد را فقط وقتی تولید میکند که فیچرش روشن باشد. فیچر را به لیست web-sys در Cargo.toml ریشه اضافه کن. |
بیلد اندروید linker/cc پیدا نمیکند |
NDK r23+ سیملینکهای خام <arch>-linux-android-clang را حذف کرد. source scripts/android-env.sh (بیانکنندهٔ CARGO_TARGET_*_LINKER، CC_*، CXX_*، AR_*). |
لایهٔ ویندوز فقط هنگام هدفگیری ویندوز خطای cargo check میدهد، ولی cargo check --workspace روی لینوکس سالم است |
کد دسکتاپ پشت #[cfg(target_os = "windows")] است، پس میزبان لینوکسی آن را تایپچک نمیکند. بدون لینکر کراسچک کن: rustup target add x86_64-pc-windows-msvc و سپس cargo check --workspace --target x86_64-pc-windows-msvc. ساخت خروجی واقعی (.exe) همچنان به MinGW-w64 (scripts/build-windows.sh) یا میزبان ویندوز نیاز دارد. |
کد android_main در ادیتور type-check نمیشود |
rust-analyzer برای میزبان تحلیل میکند. در .vscode/settings.json کامیتنشدهٔ خودت rust-analyzer.cargo.target = "aarch64-linux-android" بگذار. |
دو تایپ ناسازگار JNIEnv |
دو نسخهٔ jni در گراف. jni = "0.22" باید با آنچه android-activity 0.6 resolve میکند یکی باشد. |
خطای uuid/getrandom روی wasm |
آنتروپی به بکاند js نیاز دارد: getrandom { features = ["js"] } و uuid { features = ["v4","serde","js"] } برای گراف wasm (از قبل در apps/example/Cargo.toml اعلام شده). |
| فریز/پنیک روی زمان در مرورگر | روی wasm32 از web_time استفاده کن، نه std::time. |
cargo <alias> بینهایت بازگشت میکند |
alias نباید همنام زیرفرمان واقعی Cargo باشد. از f/l استفاده کن نه fmt/clippy. |
| بکاند از امولاتور در دسترس نیست | 127.0.0.1 خودِ دستگاه است. adb reverse tcp:8080 tcp:8080 یا --bind 0.0.0.0:8080. |
متدهای ui با --no-default-features نیستند |
انتظار میرود: show(…) پشت فیچر egui است. |
hooks/utils خالی بهنظر میرسند |
بهعنوان فایل هستند ولی در ui/src/lib.rs declare نشدهاند — عمداً فعلاً خارج از پشتهٔ eguiاند. |
قواعد عینی که کد تولیدشده را در این مخزن درست میکند:
- اول پیدا کن، بعد بنویس. از درخت §۴ و نقشهٔ §۵ استفاده کن. اگر نمادی آنجا نیست، وجود ندارد — آن را در crateی که §۳.۱ میگوید بساز.
- لایهها را رعایت کن. منطق کسبوکار →
shared. ویجت قابلاستفادهٔ مجدد →ui::componentsبا یک نیمهٔ منطق خالص و یکshowپشتcfg(egui). فراخوانی OS →crates/platform/*. سیمکشی اپ →apps/example. endpoint →shared::api+apps/server/src/lib.rs. - بیلد بدون
eguiرا سبز نگه دار. درuiهیچ تایپeguiنباید بیرون از بلوکهای#[cfg(feature = "egui")]ظاهر شود. - در
sharedshared::errors::Result/AppErrorرا ترجیح بده؛anyhowدر لبهٔ اپ خوب است. - کد per-target را با
#[cfg]مهار کن، همشکل الگوی موجودrun/run_web/android_main. هرگز یک API اندروید/JNI/web-sysرا بدون مهار صدا نزن. - راه دومی برای کار موجود نساز (کلاینت HTTP دوم، سیستم تم دوم، روتر دوم). درزِ موجود را گسترش بده.
- پیش از اعلام «تمامشدن» دروازههای §۱۰ را اجرا کن و صادقانه بگو چه چیزی را اجرا کردی و چه چیزی را نه (همسبک خود پروژه).
- کامنتها چرا را توضیح میدهند. کامنتهای این کدبیس استدلال را ثبت میکنند (چرا یک
cfg، چراjniمهم است، چرا یک alias کوتاه است) — همان سبک را رعایت کن، نه کامنت «چه».
- README.fa.md — شروع سریع و مرور پروژه (فارسی).
- README.md — نسخهٔ انگلیسی.
- TEMPLATE.md — نسخهٔ انگلیسی این راهنما.
- قرارداد سیم:
crates/shared/src/api/mod.rs - کتابخانهٔ کامپوننت:
crates/ui/src/components/ - traitهای پلتفرم:
crates/ui/src/platform/mod.rs - نقاط ورود:
apps/example/src/lib.rs - بکاند:
apps/server/src/lib.rs - دروازههای CI:
.github/workflows/ci.yml
نویسنده: سینا خانزاده — khanzadeh.1377@gmail.com — sina-khanzadeh.ir
اینستاگرام / تلگرام / دیسکورد: programmer_1998
مجوز: MIT OR Apache-2.0 — بخش [workspace.package] license در Cargo.toml.