灵符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/pkgman.rs
//! Package Manager -- a windowed front end over the real `lingfu` client
//! (drivers::lingfu). It lists the synced catalog, moves a selection with the
//! arrow keys, and installs the highlighted package on Enter (`S` re-syncs).
//! The network work -- catalog sync, download, `.lpkg` unpack -- is the exact
//! real-HTTP path the Terminal's `lingfu` command uses; this file is only the
//! GUI over it. Toolchain verbs (new/build/run) need the `ling` compiler and
//! aren't offered here -- see `lingfu`'s module doc for why that's a
//! userspace/loader project, not a missing button.

use crate::drivers::{font8x8, framebuffer, image, lingfu, theme};

static mut SEL: usize = 0;
static mut SYNCED: bool = false;
/// A sync has been requested but not yet run. Set by `open()`/`S`, serviced by
/// `service()` from the WM loop one frame LATER -- so the window paints its
/// "syncing…" state and presents it before the blocking network fetch begins,
/// instead of freezing on a stale frame. See `service()`.
static mut PENDING: bool = false;
static mut STATUS: [u8; 96] = [0; 96];
static mut STATUS_LEN: usize = 0;

// -- Package icons ---------------------------------------------------------
// Each package on fu.ling-lang.org has an auto-generated avatar at
// /avatars/pkg-<name>.auto.png. We fetch them lazily -- one per frame, only
// for rows currently on screen -- decode the PNG, and downscale to a small
// square thumbnail cached per catalog index, so the list fills in
// progressively (like a web page loading its images) instead of blocking on
// 45 TLS handshakes at once. Icons are optional chrome: a failed or
// not-yet-loaded fetch just leaves a placeholder, never blocks the list.
const ICON_DIM: usize = 20;
const MAX_ICONS: usize = 64;
const ICON_EMPTY: u8 = 0;
const ICON_READY: u8 = 1;
const ICON_FAILED: u8 = 2;
static mut ICON_STATE: [u8; MAX_ICONS] = [ICON_EMPTY; MAX_ICONS];
static mut ICON_PX: [[u32; ICON_DIM * ICON_DIM]; MAX_ICONS] =
    [[0u32; ICON_DIM * ICON_DIM]; MAX_ICONS];
/// Download scratch for one avatar PNG. The registry's auto-avatars are
/// 300x300 RGBA PNGs (~84KiB), so this holds the full HTTP response.
static mut ICON_DL: [u8; 160 * 1024] = [0; 160 * 1024];
/// Viewport range recorded by `draw()` so `tick_icons()` only fetches icons
/// for rows the user can actually see right now.
static mut VIS_START: usize = 0;
static mut VIS_END: usize = 0;

/// Tab: the "LingOS apps for this machine" view (default) or every package.
static mut TAB_ALL: bool = false;
/// An install requested by Enter, run by `service()` after the next repaint.
static mut PENDING_INSTALL: [u8; 64] = [0; 64];
static mut PENDING_INSTALL_LEN: usize = 0;

/// Catalog line indices shown under the current tab.
fn visible() -> alloc::vec::Vec<usize> {
    let mut v = alloc::vec::Vec::new();
    for i in 0..line_count() {
        let Some(line) = nth_line(i) else { continue };
        if unsafe { TAB_ALL } {
            v.push(i);
            continue;
        }
        let d = alloc::string::String::from_utf8_lossy(desc(line)).into_owned();
        let (lingos, x86, _pi) = lingfu::for_lingos(&d);
        if lingos && x86 {
            v.push(i);
        }
    }
    v
}

fn set_status(s: &[u8]) {
    unsafe {
        let st = &mut *&raw mut STATUS;
        let n = s.len().min(st.len());
        st[..n].copy_from_slice(&s[..n]);
        STATUS_LEN = n;
    }
}

/// Called when the window opens: request a one-time catalog sync. The sync
/// itself is deferred to `service()` (next frame) so the window can paint a
/// "syncing…" state first -- a multi-second TLS fetch run inline here would
/// freeze the desktop on a blank frame and, worse, drop enough PS/2 mouse
/// bytes to desync the cursor. Bounded HTTP; DNS/ARP fast-fail if no repo.
pub fn open() {
    unsafe { SEL = 0 };
    if unsafe { SYNCED } {
        return;
    }
    set_status(b"syncing catalog from fu.ling-lang.org ...");
    unsafe { PENDING = true };
}

/// Run a pending catalog sync, if any. Called once per frame by the WM loop
/// AFTER the previous frame (showing "syncing…") was presented, so the
/// blocking network fetch never freezes a stale screen. Cheap no-op when
/// nothing is pending. Resyncs the mouse afterward: the fetch holds the loop
/// off the CPU long enough to drop PS/2 bytes, so the packet framing needs
/// realigning or the cursor jumps erratically.
pub fn service() {
    let pl = unsafe { PENDING_INSTALL_LEN };
    if pl > 0 {
        let mut nb = [0u8; 64];
        unsafe { core::ptr::copy_nonoverlapping((&raw const PENDING_INSTALL) as *const u8, nb.as_mut_ptr(), pl) };
        unsafe { PENDING_INSTALL_LEN = 0 };
        if let Ok(name) = core::str::from_utf8(&nb[..pl]) {
            lingfu::begin_capture();
            let _ = lingfu::install(name);
            let cap = lingfu::end_capture();
            let last = cap.split(|&b| b == b'\n').filter(|l| !l.is_empty()).last().unwrap_or(b"install done");
            set_status(last);
            crate::drivers::mouse::resync();
        }
        return;
    }
    if !unsafe { PENDING } {
        return;
    }
    unsafe {
        PENDING = false;
        SYNCED = true;
    }
    lingfu::begin_capture();
    let n = lingfu::sync();
    lingfu::end_capture();
    if n == 0 {
        set_status(b"no packages -- is a repo reachable? (S to retry)");
    } else {
        set_status(b"catalog synced -- up/down select, Enter install, S resync");
    }
    // The catalog changed; drop any cached icons so they re-fetch for the new
    // set of packages (indices no longer refer to the same names).
    unsafe {
        let st = &mut *&raw mut ICON_STATE;
        for s in st.iter_mut() {
            *s = ICON_EMPTY;
        }
    }
    crate::drivers::mouse::resync();
}

/// Background icon loader task. Spawned once (see `ensure_icon_task`), it runs
/// on the cooperative scheduler. When any on-screen icon still needs loading it
/// opens ONE keep-alive TLS session and fetches the whole visible batch over it
/// -- paying the ~190ms handshake once, then ~60ms per image -- yielding
/// between each so the desktop keeps rendering while avatars stream in. The
/// network waits inside each fetch also yield (see timer::poll_until), so even
/// a large download never freezes the UI; only the brief PNG decode is
/// CPU-bound. Loaded/failed icons are never re-fetched, so this never loops on
/// the network once the visible set is resolved.
extern "C" fn icon_task() {
    loop {
        if next_pending_icon().is_some() {
            if lingfu::open_official() {
                while let Some(idx) = next_pending_icon() {
                    // An install or sync is waiting for the network: let go.
                    if unsafe { PENDING_INSTALL_LEN > 0 || PENDING } {
                        break;
                    }
                    fetch_icon(idx);
                    crate::proc::sched::yield_now();
                }
                lingfu::close_official();
            } else {
                // Handshake failed (host unreachable): mark the visible batch
                // failed so we don't retry an expensive connect every frame.
                // A manual resync (S) clears icon state and tries again.
                while let Some(idx) = next_pending_icon() {
                    unsafe { ICON_STATE[idx] = ICON_FAILED };
                }
            }
        }
        crate::proc::sched::yield_now();
    }
}

static mut ICON_TASK_SPAWNED: bool = false;

/// Spawn the background icon loader if it isn't running yet. Called by the WM
/// once the desktop loop is up (the scheduler's slot 0 must exist first).
pub fn ensure_icon_task() {
    unsafe {
        if ICON_TASK_SPAWNED {
            return;
        }
        ICON_TASK_SPAWNED = true;
    }
    crate::proc::sched::spawn(icon_task as *const () as usize as u64);
}

/// The next visible package index whose icon hasn't been fetched yet, or None.
/// Cheap: a no-op scan when the catalog isn't synced or all visible icons are
/// already resolved.
fn next_pending_icon() -> Option<usize> {
    if !unsafe { SYNCED } {
        return None;
    }
    let count = line_count();
    if count == 0 {
        return None;
    }
    let (start, end) = unsafe { (VIS_START, VIS_END.min(count).min(MAX_ICONS)) };
    let mut i = start;
    while i < end {
        if unsafe { ICON_STATE[i] } == ICON_EMPTY {
            return Some(i);
        }
        i += 1;
    }
    None
}

/// Fetch + decode the avatar for catalog index `idx` over the open session,
/// caching the thumbnail. Decodes into ICON_PX first, then flips state to
/// READY last -- the draw task only reads pixels once state is READY, and no
/// yield happens between the decode and the flip, so it never sees a
/// half-written thumbnail.
fn fetch_icon(idx: usize) {
    let Some(line) = nth_line(idx) else {
        unsafe { ICON_STATE[idx] = ICON_FAILED };
        return;
    };
    let name = field(line, 0);

    // Build "/avatars/pkg-<name>.auto.png".
    let mut path = [0u8; 96];
    let prefix = b"/avatars/pkg-";
    let suffix = b".auto.png";
    let mut p = 0usize;
    for &b in prefix {
        path[p] = b;
        p += 1;
    }
    for &b in name {
        if p + suffix.len() + 1 >= path.len() {
            break;
        }
        path[p] = b;
        p += 1;
    }
    for &b in suffix {
        path[p] = b;
        p += 1;
    }
    let Ok(pathstr) = core::str::from_utf8(&path[..p]) else {
        unsafe { ICON_STATE[idx] = ICON_FAILED };
        return;
    };

    let dl = unsafe { &mut *&raw mut ICON_DL };
    let ok = match lingfu::next_official(pathstr, dl) {
        Some(len) if len > 8 => decode_into_icon(&dl[..len], idx),
        _ => false,
    };
    unsafe { ICON_STATE[idx] = if ok { ICON_READY } else { ICON_FAILED } };
}

/// Decode a PNG avatar and nearest-neighbor downscale it into icon slot `idx`.
fn decode_into_icon(png: &[u8], idx: usize) -> bool {
    let Some(img) = image::decode_png(png) else { return false };
    if img.w == 0 || img.h == 0 {
        return false;
    }
    let dst = unsafe { &mut (&mut *&raw mut ICON_PX)[idx] };
    for oy in 0..ICON_DIM {
        let sy = (oy as u32 * img.h / ICON_DIM as u32).min(img.h - 1);
        for ox in 0..ICON_DIM {
            let sx = (ox as u32 * img.w / ICON_DIM as u32).min(img.w - 1);
            dst[oy * ICON_DIM + ox] = img.px[(sy * img.w + sx) as usize];
        }
    }
    true
}

/// Blit icon slot `idx` (if ready) at back-buffer (x,y). Returns true if drawn.
fn blit_icon(idx: usize, x: u32, y: u32) -> bool {
    if idx >= MAX_ICONS || unsafe { ICON_STATE[idx] } != ICON_READY {
        return false;
    }
    let src = unsafe { &(&*&raw const ICON_PX)[idx] };
    for oy in 0..ICON_DIM as u32 {
        for ox in 0..ICON_DIM as u32 {
            framebuffer::back_set_pixel(x + ox, y + oy, src[(oy as usize) * ICON_DIM + ox as usize]);
        }
    }
    true
}

fn line_count() -> usize {
    lingfu::catalog_raw().split(|&b| b == b'\n').filter(|l| !l.is_empty()).count()
}

fn nth_line(i: usize) -> Option<&'static [u8]> {
    lingfu::catalog_raw().split(|&b| b == b'\n').filter(|l| !l.is_empty()).nth(i)
}

/// The `idx`-th TAB-delimited field of a catalog line
/// ("name \t meta \t filename \t description"). Empty fields are preserved so
/// indices stay stable (a package with no filename still has a description at
/// index 3).
fn field(line: &[u8], idx: usize) -> &[u8] {
    line.split(|&b| b == b'\t').nth(idx).unwrap_or(b"")
}

/// The human description -- the 4th field.
fn desc(line: &[u8]) -> &[u8] {
    field(line, 3)
}

pub fn draw(x: u32, y: u32, w: u32, h: u32) {
    let text = theme::color(theme::SLOT_TEXT);
    let dim = theme::color(theme::SLOT_DIM);
    let panel = theme::color(theme::SLOT_PANEL);
    let accent = theme::color(theme::SLOT_ACCENT);
    let hl = theme::color(theme::SLOT_PANEL_BORDER);

    let tab_all = unsafe { TAB_ALL };
    font8x8::draw_str(x, y, if tab_all { b"lingfu -- all packages (LingOS x86-64)" } else { b"lingfu -- LingOS apps for x86-64" }, accent, panel);
    font8x8::draw_str(x, y + 14, b"up/down select   Enter install/run   Tab apps|all   S resync", dim, panel);

    let vis = visible();
    let count = vis.len();
    let list_y = y + 36;
    let row_h = 28u32;
    let avail = h.saturating_sub(36 + 22);
    let rows = (avail / row_h).max(1) as usize;
    let sel = unsafe { SEL.min(count.saturating_sub(1)) };
    let start = if sel >= rows { sel + 1 - rows } else { 0 };

    // Record the visible range so tick_icons() only fetches on-screen icons.
    unsafe {
        VIS_START = vis.get(start).copied().unwrap_or(0);
        VIS_END = vis.get((start + rows).min(count).saturating_sub(1)).map(|i| i + 1).unwrap_or(0);
    }

    // Icon column: a square inset into each row, text shifted right past it.
    let icon_pad = 4u32;
    let text_x = x + ICON_DIM as u32 + icon_pad * 2;

    if count == 0 {
        font8x8::draw_str(
            x,
            list_y,
            if line_count() == 0 {
                b"(no packages -- press S to sync a reachable repo)" as &[u8]
            } else {
                b"(no package describes itself as a LingOS app for x86-64 -- Tab shows all)"
            },
            dim,
            panel,
        );
    }
    let mut r = 0;
    while r < rows {
        let vi = start + r;
        if vi >= count {
            break;
        }
        let i = vis[vi];
        if let Some(line) = nth_line(i) {
            let ry = list_y + r as u32 * row_h;
            let rowbg = if vi == sel { hl } else { panel };
            framebuffer::back_fill_rounded_rect(
                x.saturating_sub(4),
                ry.saturating_sub(2),
                w.saturating_sub(8),
                row_h - 2,
                5,
                rowbg,
            );
            // Icon (or a placeholder tile until it loads / if it failed).
            let iy = ry.saturating_sub(1);
            if !blit_icon(i, x, iy) {
                framebuffer::back_fill_rounded_rect(x, iy, ICON_DIM as u32, ICON_DIM as u32, 4, dim);
                framebuffer::back_fill_rounded_rect(
                    x + 1,
                    iy + 1,
                    ICON_DIM as u32 - 2,
                    ICON_DIM as u32 - 2,
                    4,
                    rowbg,
                );
            }
            let name = field(line, 0);
            let ver = field(line, 1);
            font8x8::draw_str(text_x, ry, name, if vi == sel { accent } else { text }, rowbg);
            font8x8::draw_str(text_x + (name.len() as u32 + 1) * 8, ry, ver, dim, rowbg);
            // Installed / runnable state from storage.
            if let Ok(ns) = core::str::from_utf8(name) {
                if lingfu::is_installed(ns) {
                    let badge: &[u8] = if lingfu::is_app(ns) { b"installed - app (Enter runs)" } else { b"installed" };
                    let bx = x + w.saturating_sub(badge.len() as u32 * 8 + 12);
                    font8x8::draw_str(bx, ry, badge, accent, rowbg);
                }
            }
            let d = desc(line);
            let maxd = (w.saturating_sub(text_x - x + 8) / 8) as usize;
            font8x8::draw_str(text_x, ry + 12, &d[..d.len().min(maxd)], dim, rowbg);
        }
        r += 1;
    }

    let st = unsafe { &(&*&raw const STATUS)[..STATUS_LEN] };
    font8x8::draw_str(x, y + h.saturating_sub(16), st, dim, panel);
}

fn install_selected() {
    let count = line_count();
    if count == 0 {
        set_status(b"nothing to install -- sync a repo first (S)");
        return;
    }
    let vis = visible();
    if vis.is_empty() {
        set_status(b"nothing to install under this tab");
        return;
    }
    let sel = unsafe { SEL.min(vis.len() - 1) };
    let Some(line) = nth_line(vis[sel]) else { return };
    let name = field(line, 0);
    let mut nb = [0u8; 64];
    let n = name.len().min(nb.len());
    nb[..n].copy_from_slice(&name[..n]);
    let Ok(namestr) = core::str::from_utf8(&nb[..n]) else { return };
    // Installed and runnable: Enter runs it (the Terminal's `ling run`).
    if lingfu::is_installed(namestr) && lingfu::is_app(namestr) {
        let mut cmd = [0u8; 96];
        let pre = b"ling run pkg-";
        cmd[..pre.len()].copy_from_slice(pre);
        cmd[pre.len()..pre.len() + n].copy_from_slice(&nb[..n]);
        crate::drivers::wm::open_terminal_with(core::str::from_utf8(&cmd[..pre.len() + n]).unwrap_or(""));
        return;
    }
    // Shown first; service() downloads after this frame is on screen.
    let mut st = [0u8; 96];
    let pre = b"installing ";
    st[..pre.len()].copy_from_slice(pre);
    st[pre.len()..pre.len() + n].copy_from_slice(&nb[..n]);
    let post = b" from fu.ling-lang.org ...";
    st[pre.len() + n..pre.len() + n + post.len()].copy_from_slice(post);
    set_status(&st[..pre.len() + n + post.len()]);
    unsafe {
        core::ptr::copy_nonoverlapping(nb.as_ptr(), (&raw mut PENDING_INSTALL) as *mut u8, n);
        PENDING_INSTALL_LEN = n;
    }
}

pub fn key(k: u8) {
    let count = visible().len();
    match k {
        b'\t' => unsafe {
            TAB_ALL = !TAB_ALL;
            SEL = 0;
        },
        0x11 => unsafe { SEL = SEL.saturating_sub(1) }, // up
        0x12 => unsafe {
            if SEL + 1 < count {
                SEL += 1;
            }
        }, // down
        b'\n' | b'\r' => install_selected(), // enter
        b's' | b'S' => {
            // Request a fresh sync; service() runs it next frame (see open()).
            unsafe { SYNCED = false };
            open();
        },
        _ => {},
    }
}

← Back to index