diff --git a/crates/isokit-cli/src/serve.rs b/crates/isokit-cli/src/serve.rs
index a69496a..408daf1 100644
--- a/crates/isokit-cli/src/serve.rs
+++ b/crates/isokit-cli/src/serve.rs
@@ -13,7 +13,7 @@ use tiny_http::{Header, Method, Request, Response, Server};
const VIEW_PAGE: &str = r#"
isokit preview
-
+
+
@@ -51,6 +52,7 @@ document.documentElement.setAttribute("data-theme",t);})();
+
diff --git a/studio/studio.css b/studio/studio.css
index 4de5eae..fa8c93d 100644
--- a/studio/studio.css
+++ b/studio/studio.css
@@ -52,6 +52,8 @@ input[type=checkbox] { accent-color: var(--accent); }
.st-hint { position: absolute; top: 52px; left: 50%; transform: translateX(-50%); background: var(--panel); border: 1px solid var(--accent); border-radius: 16px; padding: 4px 14px; z-index: 5; pointer-events: none; }
.st-status { display: flex; align-items: center; gap: 14px; padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--stroke); color: var(--mute); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.st-status select { padding: 1px 4px; }
+.st-ver { color: var(--mute); font-size: 11px; text-decoration: none; white-space: nowrap; }
+.st-ver:hover { color: var(--fg); text-decoration: underline; }
.st-link { background: none; border: 0; color: var(--danger); padding: 0; text-decoration: underline; }
.st-right h3 { margin: 0; font-size: 15px; display: flex; align-items: center; gap: 6px; }
diff --git a/studio/studio.js b/studio/studio.js
index 3b8d895..a246075 100644
--- a/studio/studio.js
+++ b/studio/studio.js
@@ -356,12 +356,28 @@
if (B && B.preview) $("preview").addEventListener("click", function (e) { e.preventDefault(); B.preview(); });
if (B && B.saveTitle) $("save").title = B.saveTitle;
+ // engine version from /api/meta; a web build adds its commit and date
+ // (, tools/build_web.py)
+ function showVersion() {
+ var el = $("ver"), m = document.querySelector('meta[name="isokit-build"]');
+ var b = m ? m.getAttribute("content").split(" ") : [];
+ var text = "isokit " + (S.meta.engine || "?"), tip = "isokit engine " + (S.meta.engine || "?");
+ if (b[0]) {
+ text += " · " + b[0].slice(0, 7) + (b[2] ? "+" : "");
+ tip += ", build " + b[0] + (b[1] ? " (" + b[1] + ")" : "") + (b[2] ? ", with uncommitted changes" : "");
+ el.href = "https://git.b4l.co.th/B4L/isokit/commit/" + b[0];
+ }
+ el.textContent = text;
+ el.title = tip;
+ }
+
// ------------------------------------------------------------ start
Promise.all([api("/api/meta"), api("/api/doc")]).then(function (rs) {
S.meta = rs[0]; takeState(rs[1]);
document.title = S.meta.title + " · isokit Studio";
$("title").textContent = S.meta.title;
if (S.meta.load_error) toast(S.meta.load_error, true);
+ showVersion();
renderPalette(); renderLists(); mountView(); renderRight();
poll();
});
diff --git a/tools/build_web.py b/tools/build_web.py
index 46e7dfb..1055df1 100644
--- a/tools/build_web.py
+++ b/tools/build_web.py
@@ -81,6 +81,24 @@ def demo_files():
return out
+def build_info():
+ """' [dirty]' for the version text in the Studio's
+ status bar; GITHUB_SHA (CI) wins over git. Empty when git is missing."""
+ def git(*args):
+ r = subprocess.run(["git", *args], capture_output=True, text=True, cwd=ROOT)
+ return r.stdout.strip() if r.returncode == 0 else ""
+ try:
+ sha = os.environ.get("GITHUB_SHA") or git("rev-parse", "HEAD")
+ if not sha:
+ return ""
+ parts = [sha, git("log", "-1", "--format=%cs", sha) or "-"]
+ if git("status", "--porcelain", "--untracked-files=no"):
+ parts.append("dirty")
+ return " ".join(parts)
+ except OSError:
+ return ""
+
+
def replace_once(s, old, new):
n = s.count(old)
assert n == 1, f"studio/index.html: expected {old!r} once, found {n}"
@@ -116,7 +134,7 @@ def main():
print(" (wasm-opt not found: the .wasm stays unoptimised by binaryen, about 15% larger)")
for src, dst in [("viewer/isokit-view.js", "viewer"), ("viewer/isokit-view.css", "viewer"),
- ("studio/studio.js", "studio"), ("studio/studio.css", "studio"),
+ ("studio/studio.js", "studio"), ("studio/studio.css", "studio"), ("studio/favicon.svg", "studio"),
("web/web.js", "web"), ("web/web.css", "web")]:
(out / "assets" / dst).mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / src, out / "assets" / dst / Path(src).name)
@@ -129,8 +147,11 @@ def main():
v = "v=" + h.hexdigest()[:10]
html = (ROOT / "studio" / "index.html").read_text(encoding="utf-8")
+ build = build_info()
html = replace_once(html, 'isokit Studio',
- 'isokit Studio\n')
+ 'isokit Studio\n'
+ + (f'\n' if build else ""))
+ html = replace_once(html, 'href="/assets/studio/favicon.svg"', f'href="assets/studio/favicon.svg?{v}"')
html = replace_once(html, 'href="/assets/viewer/isokit-view.css"', f'href="assets/viewer/isokit-view.css?{v}"')
html = replace_once(html, '', '')
html = replace_once(html, '',
@@ -148,7 +169,7 @@ def main():
if p.is_file():
total += p.stat().st_size
print(f" {p.relative_to(out).as_posix():40} {p.stat().st_size / 1024:8.1f} kB")
- print(f"{out} ({total / 1024 / 1024:.1f} MB, {v})")
+ print(f"{out} ({total / 1024 / 1024:.1f} MB, {v}, build {build or 'unknown'})")
if __name__ == "__main__":
diff --git a/tools/check_studio.py b/tools/check_studio.py
index aced5aa..716dab8 100644
--- a/tools/check_studio.py
+++ b/tools/check_studio.py
@@ -48,6 +48,10 @@ with sync_playwright() as p:
n0 = len(doc()["objects"])
check(pg.locator("#objects li").count() == n0, f"object list shows {n0} objects")
check(pg.locator(".st-tile").count() >= 20, "palette lists the prefabs")
+ ver = pg.locator("#ver").inner_text()
+ check(ver == "isokit " + get("/api/meta")["engine"], "status bar shows the engine version: " + ver)
+ icon = urllib.request.urlopen(BASE + "/assets/studio/favicon.svg")
+ check(icon.headers.get_content_type() == "image/svg+xml" and b"