diff --git a/init.lua b/init.lua index 2668acf..a07bf75 100644 --- a/init.lua +++ b/init.lua @@ -123,21 +123,57 @@ local function consume_update_check(m) end end +-- Composes a per-dep status list keyed by lib_id from the dep-fetcher +-- result's `errors` and `warnings` arrays. Each input dep entry is +-- `{id, version}` (from manifest.module::deps[]); each output entry +-- adds `status = "ok" | "warn" | "error"`. Load-order preserved +-- (L-Q6: no re-sort). +local function compose_dep_status_list(result, module_deps) + local out = {} + local err_ids, warn_ids = {}, {} + for _, e in ipairs(result.errors or {}) do + if e.lib_id then err_ids[e.lib_id] = true end + end + for _, w in ipairs(result.warnings or {}) do + if w.lib_id then warn_ids[w.lib_id] = true end + end + for _, c in ipairs(result.conflicts or {}) do + if c.lib_id then err_ids[c.lib_id] = true end + end + for _, d in ipairs(module_deps) do + local status = "ok" + if err_ids[d.id] then status = "error" + elseif warn_ids[d.id] then status = "warn" + end + out[#out + 1] = { id = d.id, version = d.version, status = status } + end + return out +end + +-- Derives a roll-up kind ("ok" | "warn" | "error") from the dep-fetcher +-- result. Drives the list-panel's bg-tint cue via ctx.list_tint. +local function derive_result_kind(result) + if not result.ok then return "error" end + if (result.warnings and #result.warnings > 0) then return "warn" end + return "ok" +end + -- Lazy: only re-check dep-fetcher when the consumer flow asks for it -- (init + after-update). Heavy-lift in init() is acceptable per the -- plan; an async dep-check is future work. local function run_dep_check(m) local ok, result = pcall(depf.ensure_for_module, m.id) - if ok and type(result) == "table" then - m.status.dep_check_result = result - else - m.status.dep_check_result = { + if not (ok and type(result) == "table") then + result = { ok = false, conflicts = {}, warnings = {}, errors = {{ lib_id = "", kind = "internal", message = tostring(result) }}, closure = {}, } end + result.deps = compose_dep_status_list(result, m._raw_deps or {}) + result.kind = derive_result_kind(result) + m.status.dep_check_result = result end -- ----- lifecycle ----------------------------------------------------- @@ -172,6 +208,7 @@ function init(ctx) modal_open = nil, -- L.7 sets to "quit" | "conflict" carousel_state = {}, -- per-module-id state, L.5 populates manifest_cache = {}, -- L.3 populates + launcher_dir = MODULE_DIR, -- L.4: detail.lua dofiles markdown.lua via this } -- Window split: 1/3 list, 2/3 detail. Margin 12px between panels. @@ -207,6 +244,15 @@ function init(ctx) local meta, blocks = manifest_loader.load_for(entry.id) ctx_panels.manifest_cache[entry.id] = { meta = meta, blocks = blocks } entry.summary = meta.summary + + -- L.4: also surface manifest.module::deps[] verbatim so the + -- detail-panel can render per-dep status glyphs in load order + -- (no re-sort, per L-Q6). + local raw, _ = engine.module.read_file(entry.id, "manifest.module") + if raw then + local data = depf._json_decode(raw) + if data and data.deps then entry._raw_deps = data.deps end + end end local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON) diff --git a/panels/detail.lua b/panels/detail.lua index 99dc181..564e777 100644 --- a/panels/detail.lua +++ b/panels/detail.lua @@ -1,36 +1,116 @@ local M = {} -local PADDING = 24 -local COLOR_BG = 0x282828FF -local COLOR_TEXT = 0xE0E0E0FF -local COLOR_DIM = 0xA0A0A0FF -local FONT_TITLE = 28 -local FONT_BODY = 14 +local PADDING = 24 +local COLOR_BG = 0x282828FF +local COLOR_TEXT = 0xE0E0E0FF +local COLOR_DIM = 0xA0A0A0FF +local COLOR_OK = 0x80C080FF +local COLOR_WARN = 0xE0C040FF +local COLOR_ERR = 0xE05040FF +local FONT_TITLE = 28 +local FONT_SUB = 16 +local FONT_BODY = 14 +local FONT_DEP = 14 + +local md -- lazy-loaded markdown renderer + +local function ensure_md(launcher_dir) + if not md then md = dofile(launcher_dir .. "/markdown.lua") end +end + +local function status_glyph(dep_status) + if dep_status == "ok" then return "OK", COLOR_OK end + if dep_status == "warn" then return "WARN", COLOR_WARN end + if dep_status == "error" then return "ERR", COLOR_ERR end + return "?", COLOR_DIM +end + +local function find_entry(ctx, id) + if id == "news" then return nil end + for _, e in ipairs(ctx.module_entries) do + if e.id == id then return e end + end + return nil +end function M.render(ctx) local p = ctx.detail_panel_rect engine.render.draw_rect(p.x, p.y, p.w, p.h, COLOR_BG) if not ctx.selected_entry then - -- Empty state: background image + hint text (L.10 ships the image). - if ctx.bg_texture then - engine.render.draw_texture(ctx.bg_texture, p.x, p.y) - end + if ctx.bg_texture then engine.render.draw_texture(ctx.bg_texture, p.x, p.y) end local hint = ctx.t("empty_hint") local w, h = engine.render.measure_text(hint, FONT_BODY) - engine.render.draw_text(hint, - p.x + (p.w - w) / 2, p.y + (p.h - h) / 2, - FONT_BODY, COLOR_DIM) + engine.render.draw_text(hint, p.x + (p.w - w) / 2, p.y + (p.h - h) / 2, + FONT_BODY, COLOR_DIM) return end - -- Stub for L.4/L.5/L.6/L.7: full content lands later. - engine.render.draw_text(tostring(ctx.selected_entry), - p.x + PADDING, p.y + PADDING, FONT_TITLE, COLOR_TEXT) + -- News: render news.md. + if ctx.selected_entry == "news" then + ensure_md(ctx.launcher_dir) + local body = ctx.news_body_blocks or {} + md.render(body, p.x + PADDING, p.y + PADDING, p.w - 2 * PADDING, nil) + return + end + + local entry = find_entry(ctx, ctx.selected_entry) + if not entry then return end + local mc = ctx.manifest_cache[entry.id] or { meta = {}, blocks = {} } + + -- Carousel hero placeholder: L.5 swaps this in. For now: a strip. + local hero_h = 160 + if ctx.carousel_state[entry.id] and ctx.carousel_mod then + -- L.5 implements this; render call lands later. + else + engine.render.draw_rect(p.x, p.y, p.w, hero_h, 0x404040FF) + end + + local cur_y = p.y + hero_h + PADDING + -- Title row. + local title = entry.summary or entry.name or entry.id + engine.render.draw_text(title, p.x + PADDING, cur_y, FONT_TITLE, COLOR_TEXT) + local _, title_h = engine.render.measure_text(title, FONT_TITLE) + -- Version + author right-aligned. + local sub = string.format("v%s %s", entry.version, mc.meta.author or "") + local sub_w, _ = engine.render.measure_text(sub, FONT_SUB) + engine.render.draw_text(sub, p.x + p.w - PADDING - sub_w, cur_y + 6, + FONT_SUB, COLOR_DIM) + cur_y = cur_y + title_h + 12 + + -- Description (rendered MD). + ensure_md(ctx.launcher_dir) + cur_y = md.render(mc.blocks, p.x + PADDING, cur_y, + p.w - 2 * PADDING, entry.id) + + cur_y = cur_y + 12 + + -- Dep list — load-order from manifest.module per L-Q6. + local r = entry.status.dep_check_result + local dep_count = (r and r.deps and #r.deps) or 0 + engine.render.draw_text( + string.format("%s (%d)", ctx.t("dependencies"), dep_count), + p.x + PADDING, cur_y, FONT_SUB, COLOR_TEXT) + cur_y = cur_y + 24 + + if r and r.deps then + for _, d in ipairs(r.deps) do + local glyph, col = status_glyph(d.status) + engine.render.draw_text(glyph, p.x + PADDING, cur_y, FONT_DEP, col) + engine.render.draw_text(string.format("%s %s", d.id, d.version), + p.x + PADDING + 40, cur_y, FONT_DEP, COLOR_TEXT) + cur_y = cur_y + 20 + end + end + + -- Whole-list tint hint: bubble up to ctx.list_tint so panels/list.lua picks it up. + if r and r.kind == "error" then ctx.list_tint = "error" + elseif r and r.kind == "warn" then ctx.list_tint = "warn" + end end function M.handle_click(ctx, mx, my) - -- Stub: real hit-tests for launch button + carousel arrows in L.5/L.7. + -- L.5/L.7 add carousel-arrow + launch-button hit-tests. return false end