灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-kernel/src/drivers/locale.rs
//! Locale/timezone table: real Earth regions grounded in actual UTC offsets,
//! the Moon grounded in the real synodic month (its actual day/night cycle
//! length, tidally locked so it differs from its ~27.3-day sidereal
//! rotation), and a couple of invented celestial/mythological locales named
//! after real figures from Chinese folk religion (月宫/Moon Palace — Chang'e
//! 嫦娥's home; 龙宫/Dragon Palace — the undersea court of the Dragon Kings,
//! from Journey to the West and wider folklore) rather than made up whole
//! cloth, per the request this was built for.
//!
//! Honest limits: there is no calendar/date math here beyond `arch::rtc`'s
//! Unix timestamp — a fictional planet's "timezone" is necessarily symbolic
//! (an offset applied to Earth-UTC, not a real independent calendar), and is
//! disclosed as such via `Kind::Celestial` rather than presented as if it
//! were a real astronomical authority. The Moon's day-cycle fraction is a
//! real computation (`moon_day_progress`), not decoration.

use crate::arch::rtc;

#[derive(Clone, Copy, PartialEq)]
pub enum Kind {
    /// A real Earth timezone — `utc_offset_min` is a genuine UTC offset.
    Earth,
    /// A locale with no real independent clock of its own — `utc_offset_min`
    /// is a symbolic offset from Earth-UTC for flavor/sorting, not a claim
    /// about how time actually works there.
    Celestial,
}

pub struct Locale {
    /// Stable short id, e.g. "zh-CN" / "moon" / "ling-country".
    pub id: &'static str,
    /// Display name in the locale's own script (UTF-8) — rendered via
    /// `drivers::font_unicode` when non-ASCII, `font8x8` otherwise.
    pub native_name: &'static str,
    /// Latin/English name — always renderable, used in text-mode contexts
    /// that can't show the native script (see this module's doc + the
    /// installer's text-mode picker).
    pub latin_name: &'static str,
    pub utc_offset_min: i32,
    pub kind: Kind,
    /// Whether this locale's UI text should render through the Daemon
    /// glyph reskin (see `font_unicode`'s doc) instead of plain Latin.
    pub uses_daemon_script: bool,
    pub flag_id: u32,
}

pub static LOCALES: &[Locale] = &[
    Locale {
        id: "en-US",
        native_name: "English (US)",
        latin_name: "English (US)",
        utc_offset_min: -5 * 60,
        kind: Kind::Earth,
        uses_daemon_script: false,
        flag_id: 0,
    },
    Locale {
        id: "en-GB",
        native_name: "English (UK)",
        latin_name: "English (UK)",
        utc_offset_min: 0,
        kind: Kind::Earth,
        uses_daemon_script: false,
        flag_id: 1,
    },
    Locale {
        id: "zh-CN",
        native_name: "简体中文",
        latin_name: "Chinese (Simplified)",
        utc_offset_min: 8 * 60,
        kind: Kind::Earth,
        uses_daemon_script: false,
        flag_id: 2,
    },
    Locale {
        id: "ko-KR",
        native_name: "한국어",
        latin_name: "Korean",
        utc_offset_min: 9 * 60,
        kind: Kind::Earth,
        uses_daemon_script: false,
        flag_id: 3,
    },
    Locale {
        id: "th-TH",
        native_name: "ภาษาไทย",
        latin_name: "Thai",
        utc_offset_min: 7 * 60,
        kind: Kind::Earth,
        uses_daemon_script: false,
        flag_id: 4,
    },
    // -- Fictional / celestial locales -----------------------------------
    Locale {
        id: "ling-country",
        native_name: "Ling Country",
        latin_name: "Ling Country",
        // 8 is a lucky number in Chinese culture (sounds like "prosper") --
        // an offset no real timezone uses, deliberately: this locale isn't
        // claiming to be a real place.
        utc_offset_min: 8 * 60 + 8,
        kind: Kind::Celestial,
        uses_daemon_script: true,
        flag_id: 5,
    },
    Locale {
        id: "new-thailand",
        native_name: "New Thailand",
        latin_name: "New Thailand",
        utc_offset_min: 7 * 60 + 30,
        kind: Kind::Celestial,
        uses_daemon_script: false,
        flag_id: 6,
    },
    Locale {
        id: "moon-palace",
        native_name: "月宫",
        latin_name: "Moon Palace (Chang'e, 嫦娥)",
        // Symbolic sort key only -- see `moon_day_progress` for the real
        // computation this locale's clock actually runs on.
        utc_offset_min: 0,
        kind: Kind::Celestial,
        uses_daemon_script: false,
        flag_id: 7,
    },
    Locale {
        id: "dragon-palace",
        native_name: "龙宫",
        latin_name: "Dragon Palace (water world, ~3x Earth radius)",
        // Invented rotation period for this water world: 34h, expressed as
        // an offset purely for display ordering, not a real clock model.
        utc_offset_min: 34 * 60,
        kind: Kind::Celestial,
        uses_daemon_script: false,
        flag_id: 8,
    },
];

pub fn count() -> usize {
    LOCALES.len()
}

pub fn get(i: usize) -> Option<&'static Locale> {
    LOCALES.get(i)
}

/// Index of the locale with this `id` (case-insensitive -- a typed prompt
/// shouldn't reject "ZH-CN"/"zh-cn" just because the table's own id is
/// mixed-case), or 0 (`en-US`, the table's first/default entry) if `id` is
/// empty or matches nothing, so a blank/mistyped entry falls back to a sane
/// default rather than an error mid-setup.
pub fn index_of_id(id: &str) -> usize {
    LOCALES
        .iter()
        .position(|l| l.id.eq_ignore_ascii_case(id))
        .unwrap_or(0)
}

static mut SELECTED: Option<usize> = None;

/// Record the user's locale choice — kernel-side state for the same reason
/// `wm_liquid`'s spring simulation is (see that module's doc): `.ling`'s
/// AOT kernel path has no mutable-reassignment construct, so a selection
/// made on one frame of an infinite event loop can't be remembered by a
/// `bind` on the next frame. The picker screen polls [`selected`] each
/// frame instead.
pub fn select(i: usize) {
    unsafe { SELECTED = Some(i) };
}

pub fn selected() -> Option<usize> {
    unsafe { SELECTED }
}

/// Minimal UI translation: map an English source string to the currently
/// selected locale, falling back to the English key itself when a locale has
/// no entry (so untranslated call sites still read correctly). Keyed by the
/// English string so the code stays readable at the call site.
///
/// Curated, not exhaustive: only the app vocabulary whose glyphs are baked
/// into the Unicode atlas (see `ling`'s `ZH_CHARS`/`KO_CHARS` + this repo's
/// `font_unicode` doc). Translated strings contain non-ASCII, so callers MUST
/// draw the result with `font_unicode::draw_utf8_str`, not the ASCII-only
/// `font8x8`. Thai and the fictional locales fall back to English for now.
pub fn tr(key: &'static str) -> &'static str {
    tr_lookup(key).unwrap_or(key)
}

/// Translation lookup by runtime string: `Some(translated)` for a known key in
/// the current locale, `None` otherwise. The FFI (`ling_kernel_tr`) uses this
/// to echo the original English when there's no translation, since the input
/// isn't `'static`. Kernel-internal callers use `tr()` (which falls back to
/// the `'static` key literal).
pub fn tr_lookup(key: &str) -> Option<&'static str> {
    let id = selected().and_then(|i| LOCALES.get(i)).map(|l| l.id).unwrap_or("en-US");
    match id {
        // The two celestial Chinese-culture locales (月宫 / 龙宫) speak Chinese.
        "zh-CN" | "moon-palace" | "dragon-palace" => tr_zh(key),
        "ko-KR" => tr_ko(key),
        "th-TH" | "new-thailand" => tr_th(key),
        // en-US / en-GB use English. Ling Country also returns English strings
        // -- its constructed script is a *reskin* of Latin, so the words stay
        // English and `render_daemon()` tells the drawing code to paint them in
        // the Daemon glyphs (the daemon atlas is keyed by ASCII).
        _ => None,
    }
}

/// Should UI text be painted through the Daemon (constructed-script) reskin?
/// True only for the Ling Country locale; callers pass this as the `daemon`
/// flag to `font_unicode::draw_utf8_str` so localized strings render in-script.
pub fn render_daemon() -> bool {
    selected().and_then(|i| LOCALES.get(i)).map(|l| l.uses_daemon_script).unwrap_or(false)
}

fn tr_zh(key: &str) -> Option<&'static str> {
    Some(match key {
        // Window / app titles.
        "Files" => "文件",
        "Settings" => "设置",
        "Terminal" => "终端",
        "Editor" => "编辑器",
        "Messenger" => "信使",
        "Gallery" => "图库",
        "Media Player" => "媒体播放器",
        "Packages" => "软件包",
        "About LingOS" => "关于 LingOS",
        // File browser.
        "(empty)" => "(空)",
        "(unreadable)" => "(无法读取)",
        "enter: open   backspace: up" => "回车打开  退格上级",
        "backspace: back" => "退格返回",
        // Shared buttons.
        "Apply" => "应用",
        "OK" => "确定",
        // Installer step names + prompts.
        "Keyboard" => "键盘",
        "Language" => "语言",
        "Locale" => "区域",
        "Disk" => "磁盘",
        "Root" => "管理员",
        "User" => "用户",
        "Network" => "网络",
        "Finish" => "完成",
        "Choose your keyboard layout" => "选择键盘布局",
        "Choose your system language" => "选择系统语言",
        "Choose your locale and timezone" => "选择区域和时区",
        "up/down to move, enter to select" => "上下移动,回车选择",
        "Install" => "安装",
        "Next" => "下一步",
        "Back" => "上一步",
        "Erase disk and install LingOS" => "清除磁盘并安装 LingOS",
        "Installing..." => "正在安装...",
        "Welcome to LingOS" => "欢迎使用 LingOS",
        // Installer step titles.
        "Confirm erase" => "确认清除",
        "Choose the installation disk" => "选择安装磁盘",
        "Root account" => "管理员账户",
        "User account" => "用户账户",
        "Finishing up" => "正在完成",
        "Setup" => "安装设置",
        "Erasing disk" => "正在清除",
        // Installer prompts + instructions.
        "Password:" => "密码:",
        "Confirm password:" => "确认密码:",
        "Type ERASE (all caps) to wipe this disk and install:" => "输入 ERASE 清除此磁盘并安装:",
        "Hostname for this system:" => "此系统的主机名:",
        "Create a user account (username, blank to skip):" => "创建用户 (留空跳过):",
        "Password for this account:" => "此账户的密码:",
        "Set a password for the root account:" => "设置管理员密码:",
        "Start the SSH server at boot? (yes/no):" => "开机启动 SSH 服务? (yes/no):",
        "Package set (1-7):" => "软件包组 (1-7):",
        "Package name from the registry (blank to skip):" => "软件包名 (留空跳过):",
        "Choose a package set to install now:" => "选择要安装的软件包组:",
        "Detected disk (the only one this kernel drives today):" => "检测到的磁盘 (目前仅支持一个):",
        "Installing ERASES this disk: bootloader, filesystem, everything." => "安装将清除此磁盘的一切.",
        // Installer status lines.
        "Erasing boot sectors and filesystem headers..." => "正在清除引导扇区和文件系统...",
        "Mounting disk..." => "正在挂载磁盘...",
        "Setting up network..." => "正在设置网络...",
        "Installing bootloader..." => "正在安装引导程序...",
        "Done. Remove the installation media and reboot." => "完成. 请移除安装介质并重启.",
        "Package set installed." => "软件包已安装.",
        _ => return None,
    })
}

fn tr_ko(key: &str) -> Option<&'static str> {
    Some(match key {
        "Files" => "파일",
        "Settings" => "설정",
        "Terminal" => "터미널",
        "Editor" => "편집기",
        "Messenger" => "메신저",
        "Gallery" => "갤러리",
        "Media Player" => "미디어 플레이어",
        "Packages" => "패키지",
        "About LingOS" => "LingOS 정보",
        "(empty)" => "(비었음)",
        "(unreadable)" => "(읽기 실패)",
        "enter: open   backspace: up" => "엔터 열기  백스페이스 위로",
        "backspace: back" => "백스페이스 뒤로",
        "Apply" => "적용",
        "OK" => "확인",
        "Keyboard" => "키보드",
        "Language" => "언어",
        "Locale" => "지역",
        "Disk" => "디스크",
        "Root" => "루트",
        "User" => "사용자",
        "Network" => "네트워크",
        "Finish" => "완료",
        "Choose your keyboard layout" => "키보드 배열 선택",
        "Choose your system language" => "시스템 언어 선택",
        "Choose your locale and timezone" => "지역 및 시간대 선택",
        "up/down to move, enter to select" => "위아래 이동, 엔터 선택",
        "Install" => "설치",
        "Next" => "다음",
        "Back" => "뒤로",
        "Erase disk and install LingOS" => "디스크를 지우고 LingOS 설치",
        "Installing..." => "설치 중...",
        "Welcome to LingOS" => "LingOS 에 오신 것을 환영합니다",
        "Confirm erase" => "지우기 확인",
        "Choose the installation disk" => "설치 디스크 선택",
        "Root account" => "루트 계정",
        "User account" => "사용자 계정",
        "Finishing up" => "마무리",
        "Setup" => "설정",
        "Erasing disk" => "디스크 지우는 중",
        "Password:" => "비밀번호:",
        "Confirm password:" => "비밀번호 확인:",
        "Type ERASE (all caps) to wipe this disk and install:" => "ERASE 입력하여 디스크 지우고 설치:",
        "Hostname for this system:" => "호스트 이름:",
        "Create a user account (username, blank to skip):" => "사용자 만들기 (비우면 건너뜀):",
        "Password for this account:" => "계정 비밀번호:",
        "Set a password for the root account:" => "루트 비밀번호 설정:",
        "Start the SSH server at boot? (yes/no):" => "부팅 시 SSH 시작? (yes/no):",
        "Package set (1-7):" => "패키지 세트 (1-7):",
        "Package name from the registry (blank to skip):" => "패키지 이름 (비우면 건너뜀):",
        "Choose a package set to install now:" => "설치할 패키지 세트 선택:",
        "Detected disk (the only one this kernel drives today):" => "감지된 디스크 (현재 하나만 지원):",
        "Installing ERASES this disk: bootloader, filesystem, everything." => "설치는 이 디스크를 모두 지웁니다.",
        "Erasing boot sectors and filesystem headers..." => "부트 섹터와 파일시스템 지우는 중...",
        "Mounting disk..." => "디스크 마운트 중...",
        "Setting up network..." => "네트워크 설정 중...",
        "Installing bootloader..." => "부트로더 설치 중...",
        "Done. Remove the installation media and reboot." => "완료. 미디어를 빼고 재부팅.",
        "Package set installed." => "패키지 설치됨.",
        _ => return None,
    })
}

// Thai (also used for the fictional "New Thailand"). Note: `font_unicode` does
// not do Thai shaping, so combining vowel/tone marks render as their own cells
// -- readable, not typographically perfect (disclosed in that module's doc).
fn tr_th(key: &str) -> Option<&'static str> {
    Some(match key {
        "Files" => "ไฟล์",
        "Settings" => "ตั้งค่า",
        "Terminal" => "เทอร์มินัล",
        "Editor" => "แก้ไข",
        "Messenger" => "แชท",
        "Gallery" => "แกลเลอรี",
        "Media Player" => "มีเดีย",
        "Packages" => "แพ็กเกจ",
        "About LingOS" => "เกี่ยวกับ LingOS",
        "(empty)" => "(ว่าง)",
        "(unreadable)" => "(อ่านไม่ได้)",
        "enter: open   backspace: up" => "Enter เปิด  Backspace ขึ้น",
        "backspace: back" => "Backspace ย้อน",
        "Apply" => "ใช้",
        "OK" => "ตกลง",
        "Keyboard" => "แป้นพิมพ์",
        "Language" => "ภาษา",
        "Locale" => "ภูมิภาค",
        "Disk" => "ดิสก์",
        "Root" => "รูท",
        "User" => "ผู้ใช้",
        "Network" => "เครือข่าย",
        "Finish" => "เสร็จ",
        "Choose your keyboard layout" => "เลือกแป้นพิมพ์",
        "Choose your system language" => "เลือกภาษาระบบ",
        "Choose your locale and timezone" => "เลือกภูมิภาคและเขตเวลา",
        "up/down to move, enter to select" => "ขึ้นลงเลื่อน กด Enter เลือก",
        "Install" => "ติดตั้ง",
        "Next" => "ถัดไป",
        "Back" => "ย้อน",
        "Erase disk and install LingOS" => "ล้างดิสก์และติดตั้ง LingOS",
        "Installing..." => "กำลังติดตั้ง...",
        "Welcome to LingOS" => "ยินดีต้อนรับสู่ LingOS",
        "Confirm erase" => "ยืนยันการล้าง",
        "Choose the installation disk" => "เลือกดิสก์ติดตั้ง",
        "Root account" => "บัญชีรูท",
        "User account" => "บัญชีผู้ใช้",
        "Finishing up" => "กำลังเสร็จ",
        "Setup" => "ตั้งค่า",
        "Erasing disk" => "กำลังล้างดิสก์",
        "Password:" => "รหัสผ่าน:",
        "Confirm password:" => "ยืนยันรหัสผ่าน:",
        "Type ERASE (all caps) to wipe this disk and install:" => "พิมพ์ ERASE เพื่อล้างดิสก์และติดตั้ง:",
        "Hostname for this system:" => "ชื่อโฮสต์:",
        "Create a user account (username, blank to skip):" => "สร้างผู้ใช้ (เว้นว่างข้าม):",
        "Password for this account:" => "รหัสผ่านบัญชี:",
        "Set a password for the root account:" => "ตั้งรหัสผ่านรูท:",
        "Start the SSH server at boot? (yes/no):" => "เริ่ม SSH เมื่อบูต? (yes/no):",
        "Package set (1-7):" => "ชุดแพ็กเกจ (1-7):",
        "Package name from the registry (blank to skip):" => "ชื่อแพ็กเกจ (เว้นว่างข้าม):",
        "Choose a package set to install now:" => "เลือกชุดแพ็กเกจติดตั้ง:",
        "Detected disk (the only one this kernel drives today):" => "ดิสก์ที่พบ (รองรับหนึ่งตัว):",
        "Installing ERASES this disk: bootloader, filesystem, everything." => "การติดตั้งจะล้างดิสก์นี้ทั้งหมด.",
        "Erasing boot sectors and filesystem headers..." => "กำลังล้างบูตเซกเตอร์...",
        "Mounting disk..." => "กำลังเมานต์ดิสก์...",
        "Setting up network..." => "กำลังตั้งค่าเครือข่าย...",
        "Installing bootloader..." => "กำลังติดตั้งบูตโหลดเดอร์...",
        "Done. Remove the installation media and reboot." => "เสร็จ. นำสื่อออกแล้วรีบูต.",
        "Package set installed." => "ติดตั้งแพ็กเกจแล้ว.",
        _ => return None,
    })
}

/// Restore the locale the installer persisted to `/locale` (its stable id,
/// e.g. "zh-CN") and select the matching entry. Returns true if a saved id was
/// found and matched. Called at installed-disk boot so the login screen and
/// desktop come up in the language chosen during install, instead of defaulting
/// back to English.
pub fn restore() -> bool {
    let mut buf = [0u8; 64];
    let Ok(Some(n)) = crate::fs::lingfs::read_file_all("/locale", &mut buf) else {
        return false;
    };
    let Ok(id) = core::str::from_utf8(&buf[..n]) else { return false };
    let id = id.trim();
    if id.is_empty() {
        return false;
    }
    for (i, loc) in LOCALES.iter().enumerate() {
        if loc.id == id {
            select(i);
            return true;
        }
    }
    false
}

/// Clear the current pick, so [`selected`] reports "unselected" again --
/// lets a multi-step wizard (language, then a separate locale/timezone
/// step) run the same picker screen twice over the same table for two
/// independent choices, rather than the second call just re-confirming the
/// first pick instantly.
pub fn reset() {
    unsafe { SELECTED = None };
    unsafe { CURSOR = 0 };
}

static mut CURSOR: usize = 0;

/// The picker's currently-highlighted row -- separate from [`selected`]
/// (which only becomes `Some` once the user actually confirms with
/// Enter), for real arrow-key/Enter navigation instead of typing a raw
/// digit for the row you want. Kernel-side for the same reason as
/// `selected`: no persistent mutable state on the `.ling` side of an
/// infinite per-frame loop.
pub fn cursor() -> usize {
    unsafe { CURSOR }
}

pub fn cursor_up() {
    unsafe {
        CURSOR = if CURSOR == 0 { LOCALES.len() - 1 } else { CURSOR - 1 };
    }
}

pub fn cursor_down() {
    unsafe {
        CURSOR = (CURSOR + 1) % LOCALES.len();
    }
}

/// Confirm whatever row is currently highlighted -- the Enter-key action.
pub fn confirm_cursor() {
    unsafe { SELECTED = Some(CURSOR) };
}

/// How far through its real day/night cycle the Moon currently is, as a
/// permille (0..1000) of a full cycle. Grounded in the actual synodic month
/// (29.530589 days — the Moon's tidally-locked day length, distinct from
/// its ~27.3-day sidereal rotation) applied to the real RTC-derived Unix
/// timestamp, not an invented number.
pub fn moon_day_progress_permille() -> u32 {
    const SYNODIC_MONTH_SECS: i64 = 2_551_443; // 29.530589 days
    // A fixed reference new-moon epoch (2000-01-06 18:14 UTC, a commonly
    // cited J2000-era reference new moon) so the phase is at least
    // approximately right rather than arbitrary from an epoch-zero start.
    const REFERENCE_NEW_MOON_UNIX: i64 = 947_182_440;
    let ts = rtc::unix_timestamp();
    let elapsed = (ts - REFERENCE_NEW_MOON_UNIX).rem_euclid(SYNODIC_MONTH_SECS);
    (elapsed * 1000 / SYNODIC_MONTH_SECS) as u32
}

← Back to index