Initial BeeOS simulator and asset pipeline
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Clean generated Python cache files from the BeeOS project."""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Remove __pycache__ and .pyc files.")
|
||||
parser.add_argument("--project-root", default=".", help="BeeOS project root")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = Path(args.project_root).resolve()
|
||||
removed = 0
|
||||
|
||||
for cache_dir in project_root.rglob("__pycache__"):
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
removed += 1
|
||||
|
||||
for pyc in project_root.rglob("*.pyc"):
|
||||
pyc.unlink(missing_ok=True)
|
||||
removed += 1
|
||||
|
||||
print(f"Cleaned {removed} cache item(s).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,364 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""BeeOS icon asset conversion tool.
|
||||
|
||||
Converts the curated BeeOS/Lucide PNG icon subset into firmware-friendly formats.
|
||||
|
||||
Outputs:
|
||||
- JSON monochrome bitmap map
|
||||
- generic packed 1-bit C buffers
|
||||
- LVGL v8 image descriptors
|
||||
- LVGL v9 image descriptors
|
||||
- preview sheets
|
||||
|
||||
The LVGL descriptor exports are intentionally generated as separate v8/v9 files
|
||||
because LVGL image descriptor APIs changed between major versions.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
DEFAULT_SIZES = [18, 24, 34, 48]
|
||||
|
||||
|
||||
def load_icon_ids(svg_manifest_path: Path) -> list[str]:
|
||||
data = json.loads(svg_manifest_path.read_text(encoding="utf-8"))
|
||||
return [item["id"] for item in data.get("icons", [])]
|
||||
|
||||
|
||||
def image_to_mono_rows(path: Path, threshold: int = 24) -> tuple[int, int, list[str], list[int], int]:
|
||||
img = Image.open(path).convert("RGBA")
|
||||
width, height = img.size
|
||||
stride = (width + 7) // 8
|
||||
|
||||
rows: list[str] = []
|
||||
packed: list[int] = []
|
||||
|
||||
for y in range(height):
|
||||
bits = []
|
||||
byte = 0
|
||||
count = 0
|
||||
|
||||
for x in range(width):
|
||||
r, g, b, a = img.getpixel((x, y))
|
||||
on = a > threshold and (r + g + b) > threshold
|
||||
bits.append("1" if on else "0")
|
||||
byte = (byte << 1) | (1 if on else 0)
|
||||
count += 1
|
||||
|
||||
if count == 8:
|
||||
packed.append(byte)
|
||||
byte = 0
|
||||
count = 0
|
||||
|
||||
if count:
|
||||
byte = byte << (8 - count)
|
||||
packed.append(byte)
|
||||
|
||||
rows.append("".join(bits))
|
||||
|
||||
return width, height, rows, packed, stride
|
||||
|
||||
|
||||
def c_array_name(icon_id: str, size: int) -> str:
|
||||
clean = icon_id.replace("-", "_").replace(".", "_")
|
||||
return f"beeos_icon_{clean}_{size}"
|
||||
|
||||
|
||||
def export_json(icon_ids: list[str], png_dir: Path, output_dir: Path, size: int) -> Path:
|
||||
icons = {}
|
||||
for icon_id in icon_ids:
|
||||
path = png_dir / str(size) / f"{icon_id}.png"
|
||||
if not path.exists():
|
||||
continue
|
||||
width, height, rows, packed, stride = image_to_mono_rows(path)
|
||||
icons[icon_id] = {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"stride": stride,
|
||||
"rows": rows,
|
||||
"packed_bytes": packed,
|
||||
}
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
out = output_dir / f"beeos_icons_{size}.json"
|
||||
out.write_text(json.dumps({"size": size, "icons": icons}, indent=2), encoding="utf-8")
|
||||
return out
|
||||
|
||||
|
||||
def collect_icon_data(icon_ids: list[str], png_dir: Path, size: int) -> list[dict]:
|
||||
rows = []
|
||||
for icon_id in icon_ids:
|
||||
path = png_dir / str(size) / f"{icon_id}.png"
|
||||
if not path.exists():
|
||||
continue
|
||||
width, height, _rows, packed, stride = image_to_mono_rows(path)
|
||||
name = c_array_name(icon_id, size)
|
||||
rows.append(
|
||||
{
|
||||
"id": icon_id,
|
||||
"name": name,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"stride": stride,
|
||||
"data": packed,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def export_generic_c(icon_rows: list[dict], output_dir: Path, size: int) -> tuple[Path, Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
header = output_dir / f"beeos_icons_{size}.h"
|
||||
source = output_dir / f"beeos_icons_{size}.c"
|
||||
|
||||
header_lines = [
|
||||
"/* Auto-generated BeeOS icon declarations. */",
|
||||
"#pragma once",
|
||||
"",
|
||||
"#include <stdint.h>",
|
||||
"",
|
||||
"typedef struct BeeOSIconAsset {",
|
||||
" const char *name;",
|
||||
" uint16_t width;",
|
||||
" uint16_t height;",
|
||||
" uint16_t stride;",
|
||||
" const uint8_t *data;",
|
||||
" uint32_t data_len;",
|
||||
"} BeeOSIconAsset;",
|
||||
"",
|
||||
]
|
||||
|
||||
source_lines = [
|
||||
"/* Auto-generated BeeOS 1-bit icon data. */",
|
||||
"",
|
||||
f'#include "beeos_icons_{size}.h"',
|
||||
"",
|
||||
]
|
||||
|
||||
for row in icon_rows:
|
||||
header_lines.append(f"extern const uint8_t {row['name']}_data[{len(row['data'])}];")
|
||||
source_lines.append(f"const uint8_t {row['name']}_data[{len(row['data'])}] = {{")
|
||||
for i in range(0, len(row["data"]), 12):
|
||||
chunk = row["data"][i : i + 12]
|
||||
source_lines.append(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
|
||||
header_lines.extend(
|
||||
[
|
||||
"",
|
||||
f"extern const BeeOSIconAsset beeos_icons_{size}[];",
|
||||
f"extern const uint32_t beeos_icons_{size}_count;",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
source_lines.append(f"const BeeOSIconAsset beeos_icons_{size}[] = {{")
|
||||
for row in icon_rows:
|
||||
source_lines.append(
|
||||
f' {{"{row["id"]}", {row["width"]}, {row["height"]}, {row["stride"]}, {row["name"]}_data, {len(row["data"])}}},'
|
||||
)
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
source_lines.append(f"const uint32_t beeos_icons_{size}_count = {len(icon_rows)};")
|
||||
source_lines.append("")
|
||||
|
||||
header.write_text("\\n".join(header_lines), encoding="utf-8")
|
||||
source.write_text("\\n".join(source_lines), encoding="utf-8")
|
||||
return header, source
|
||||
|
||||
|
||||
def export_lvgl_v8(icon_rows: list[dict], output_dir: Path, size: int) -> tuple[Path, Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
header = output_dir / f"beeos_icons_{size}_lvgl_v8.h"
|
||||
source = output_dir / f"beeos_icons_{size}_lvgl_v8.c"
|
||||
|
||||
header_lines = [
|
||||
"/* Auto-generated BeeOS LVGL v8 icon descriptors. */",
|
||||
"#pragma once",
|
||||
"",
|
||||
"#include <lvgl.h>",
|
||||
"",
|
||||
]
|
||||
|
||||
source_lines = [
|
||||
"/* Auto-generated BeeOS LVGL v8 icon descriptors.",
|
||||
" Uses LV_IMG_CF_ALPHA_1BIT for monochrome/alpha icon masks.",
|
||||
" Style code should recolor these with image recolor/opacity if needed. */",
|
||||
"",
|
||||
f'#include "beeos_icons_{size}_lvgl_v8.h"',
|
||||
"",
|
||||
]
|
||||
|
||||
for row in icon_rows:
|
||||
dsc_name = f"{row['name']}_dsc"
|
||||
data_name = f"{row['name']}_data"
|
||||
header_lines.append(f"LV_IMG_DECLARE({dsc_name});")
|
||||
|
||||
source_lines.append(f"static const uint8_t {data_name}[] = {{")
|
||||
for i in range(0, len(row["data"]), 12):
|
||||
chunk = row["data"][i : i + 12]
|
||||
source_lines.append(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
source_lines.append(f"const lv_img_dsc_t {dsc_name} = {{")
|
||||
source_lines.append(" .header.always_zero = 0,")
|
||||
source_lines.append(" .header.reserved = 0,")
|
||||
source_lines.append(" .header.cf = LV_IMG_CF_ALPHA_1BIT,")
|
||||
source_lines.append(f" .header.w = {row['width']},")
|
||||
source_lines.append(f" .header.h = {row['height']},")
|
||||
source_lines.append(f" .data_size = {len(row['data'])},")
|
||||
source_lines.append(f" .data = {data_name},")
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
|
||||
header.write_text("\\n".join(header_lines), encoding="utf-8")
|
||||
source.write_text("\\n".join(source_lines), encoding="utf-8")
|
||||
return header, source
|
||||
|
||||
|
||||
def export_lvgl_v9(icon_rows: list[dict], output_dir: Path, size: int) -> tuple[Path, Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
header = output_dir / f"beeos_icons_{size}_lvgl_v9.h"
|
||||
source = output_dir / f"beeos_icons_{size}_lvgl_v9.c"
|
||||
|
||||
header_lines = [
|
||||
"/* Auto-generated BeeOS LVGL v9 icon descriptors. */",
|
||||
"#pragma once",
|
||||
"",
|
||||
"#include <lvgl.h>",
|
||||
"",
|
||||
]
|
||||
|
||||
source_lines = [
|
||||
"/* Auto-generated BeeOS LVGL v9 icon descriptors.",
|
||||
" Uses LV_COLOR_FORMAT_I1 for monochrome/alpha icon masks.",
|
||||
" Some LVGL v9 ports may need color-format adjustments depending on display setup. */",
|
||||
"",
|
||||
f'#include "beeos_icons_{size}_lvgl_v9.h"',
|
||||
"",
|
||||
]
|
||||
|
||||
for row in icon_rows:
|
||||
dsc_name = f"{row['name']}_dsc"
|
||||
data_name = f"{row['name']}_data"
|
||||
header_lines.append(f"extern const lv_image_dsc_t {dsc_name};")
|
||||
|
||||
source_lines.append(f"static const uint8_t {data_name}[] = {{")
|
||||
for i in range(0, len(row["data"]), 12):
|
||||
chunk = row["data"][i : i + 12]
|
||||
source_lines.append(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
source_lines.append(f"const lv_image_dsc_t {dsc_name} = {{")
|
||||
source_lines.append(" .header.magic = LV_IMAGE_HEADER_MAGIC,")
|
||||
source_lines.append(" .header.cf = LV_COLOR_FORMAT_I1,")
|
||||
source_lines.append(" .header.flags = 0,")
|
||||
source_lines.append(f" .header.w = {row['width']},")
|
||||
source_lines.append(f" .header.h = {row['height']},")
|
||||
source_lines.append(f" .header.stride = {row['stride']},")
|
||||
source_lines.append(f" .data_size = {len(row['data'])},")
|
||||
source_lines.append(f" .data = {data_name},")
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
|
||||
header.write_text("\\n".join(header_lines), encoding="utf-8")
|
||||
source.write_text("\\n".join(source_lines), encoding="utf-8")
|
||||
return header, source
|
||||
|
||||
|
||||
def export_preview(icon_ids: list[str], png_dir: Path, output_dir: Path, size: int) -> Path:
|
||||
cols = 5
|
||||
cell_w = 96
|
||||
cell_h = 84
|
||||
rows = (len(icon_ids) + cols - 1) // cols
|
||||
sheet = Image.new("RGBA", (cols * cell_w, max(1, rows) * cell_h), (0, 0, 0, 255))
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
|
||||
for idx, icon_id in enumerate(icon_ids):
|
||||
x = (idx % cols) * cell_w
|
||||
y = (idx // cols) * cell_h
|
||||
draw.rounded_rectangle((x + 18, y + 6, x + 78, y + 66), radius=18, fill=(20, 22, 28, 255), outline=(255, 204, 0, 255))
|
||||
path = png_dir / str(size) / f"{icon_id}.png"
|
||||
if path.exists():
|
||||
icon = Image.open(path).convert("RGBA")
|
||||
sheet.alpha_composite(icon, (x + (cell_w - icon.width) // 2, y + 18))
|
||||
draw.text((x + 6, y + 68), icon_id[:14], fill=(220, 220, 220, 255))
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
out = output_dir / f"beeos_icons_{size}_preview.png"
|
||||
sheet.save(out)
|
||||
return out
|
||||
|
||||
|
||||
def convert(project_root: Path, sizes: list[int]) -> dict:
|
||||
svg_manifest = project_root / "beeos" / "assets" / "icons" / "lucide" / "manifest.json"
|
||||
png_dir = project_root / "beeos" / "assets" / "icons" / "lucide_png"
|
||||
|
||||
generated_root = project_root / "beeos" / "generated" / "icons"
|
||||
json_dir = generated_root / "bitmap_json"
|
||||
generic_dir = generated_root / "lvgl"
|
||||
v8_dir = generated_root / "lvgl_v8"
|
||||
v9_dir = generated_root / "lvgl_v9"
|
||||
preview_dir = generated_root / "previews"
|
||||
|
||||
icon_ids = load_icon_ids(svg_manifest)
|
||||
|
||||
result = {
|
||||
"icon_count": len(icon_ids),
|
||||
"sizes": {},
|
||||
}
|
||||
|
||||
for size in sizes:
|
||||
icon_rows = collect_icon_data(icon_ids, png_dir, size)
|
||||
json_out = export_json(icon_ids, png_dir, json_dir, size)
|
||||
generic_h, generic_c = export_generic_c(icon_rows, generic_dir, size)
|
||||
v8_h, v8_c = export_lvgl_v8(icon_rows, v8_dir, size)
|
||||
v9_h, v9_c = export_lvgl_v9(icon_rows, v9_dir, size)
|
||||
preview = export_preview(icon_ids, png_dir, preview_dir, size)
|
||||
|
||||
result["sizes"][str(size)] = {
|
||||
"json": str(json_out.relative_to(project_root)),
|
||||
"generic_header": str(generic_h.relative_to(project_root)),
|
||||
"generic_source": str(generic_c.relative_to(project_root)),
|
||||
"lvgl_v8_header": str(v8_h.relative_to(project_root)),
|
||||
"lvgl_v8_source": str(v8_c.relative_to(project_root)),
|
||||
"lvgl_v9_header": str(v9_h.relative_to(project_root)),
|
||||
"lvgl_v9_source": str(v9_c.relative_to(project_root)),
|
||||
"preview": str(preview.relative_to(project_root)),
|
||||
}
|
||||
|
||||
index_out = generated_root / "icon_build_index.json"
|
||||
index_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
index_out.write_text(json.dumps(result, indent=2), encoding="utf-8")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Convert BeeOS curated icons into firmware-friendly assets.")
|
||||
parser.add_argument("--project-root", default=".", help="BeeOS project root")
|
||||
parser.add_argument("--sizes", default="24,34", help="Comma-separated icon sizes to export")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.project_root).resolve()
|
||||
sizes = [int(part.strip()) for part in args.sizes.split(",") if part.strip()]
|
||||
result = convert(root, sizes)
|
||||
|
||||
print("BeeOS icon conversion complete.")
|
||||
print(f"Icons: {result['icon_count']}")
|
||||
for size, outputs in result["sizes"].items():
|
||||
print(f"Size {size}:")
|
||||
for key, path in outputs.items():
|
||||
print(f" {key}: {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Create a starter HiveStore repo index.
|
||||
|
||||
Usage:
|
||||
py tools/create_hivestore_repo.py ./my-hivestore "My HiveStore Repo"
|
||||
|
||||
Then commit the generated hivestore_index.json to GitHub.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_repo(path: Path, name: str) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
index = {
|
||||
"schema": 1,
|
||||
"source_name": name,
|
||||
"source_url": "https://github.com/example/my-hivestore",
|
||||
"packages": [
|
||||
{
|
||||
"id": "example.app.hello",
|
||||
"name": "Hello Bee",
|
||||
"type": "app",
|
||||
"category": "Apps",
|
||||
"version": "0.1.0",
|
||||
"author": "Example Developer",
|
||||
"description": "A simple starter BeeOS app listing.",
|
||||
"repository": "https://github.com/example/hello-bee",
|
||||
"entry": "app.py",
|
||||
"permissions": ["display", "storage"],
|
||||
"rating": "0.0",
|
||||
"color": "#ffcc00",
|
||||
"icon": "▣",
|
||||
"curated": True
|
||||
}
|
||||
]
|
||||
}
|
||||
(path / "hivestore_index.json").write_text(json.dumps(index, indent=2), encoding="utf-8")
|
||||
(path / "README.md").write_text(
|
||||
f"# {name}\\n\\nThis is a BeeOS HiveStore repo.\\n\\nThe package index lives at `hivestore_index.json`.\\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Created HiveStore repo starter at: {path}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 3:
|
||||
print('Usage: py tools/create_hivestore_repo.py <folder> "Repo Name"')
|
||||
raise SystemExit(1)
|
||||
create_repo(Path(sys.argv[1]), " ".join(sys.argv[2:]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
VALID_TYPES = {"app", "script", "script_pack", "watchface", "theme", "plugin"}
|
||||
|
||||
|
||||
def create_template(package_type: str, package_id: str, name: str) -> None:
|
||||
if package_type not in VALID_TYPES:
|
||||
raise SystemExit(f"Invalid package type. Use one of: {', '.join(sorted(VALID_TYPES))}")
|
||||
|
||||
root = Path("beeos") / "sideload" / package_id
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
entry = "app.py"
|
||||
if package_type == "theme":
|
||||
entry = "theme.json"
|
||||
elif package_type == "watchface":
|
||||
entry = "face.py"
|
||||
elif package_type in {"script", "script_pack"}:
|
||||
entry = "script.py"
|
||||
elif package_type == "plugin":
|
||||
entry = "plugin.py"
|
||||
|
||||
manifest = {
|
||||
"id": package_id,
|
||||
"name": name,
|
||||
"type": package_type,
|
||||
"version": "0.1.0",
|
||||
"author": "Local Developer",
|
||||
"description": f"A local BeeOS {package_type} package.",
|
||||
"entry": entry,
|
||||
"permissions": ["display", "storage"],
|
||||
"repository": "",
|
||||
"category": package_type,
|
||||
"color": "#ffcc00",
|
||||
"icon": "⬢",
|
||||
"curated": False,
|
||||
}
|
||||
(root / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
|
||||
if package_type == "app":
|
||||
app_code = f"""class BeeApp:
|
||||
def __init__(self, ctx):
|
||||
self.ctx = ctx
|
||||
|
||||
def on_open(self):
|
||||
launches = self.ctx.storage.get("launches", 0) + 1
|
||||
self.ctx.storage.set("launches", launches)
|
||||
|
||||
def on_draw(self, display):
|
||||
display.title({name!r})
|
||||
display.card("BeeOS App", "Created from the Phase 5 template.")
|
||||
display.list_item("Launches", str(self.ctx.storage.get("launches", 0)))
|
||||
display.list_item("Touches", str(self.ctx.storage.get("touches", 0)))
|
||||
display.progress("Battery", self.ctx.battery_percent())
|
||||
|
||||
def on_touch(self, event):
|
||||
touches = self.ctx.storage.get("touches", 0) + 1
|
||||
self.ctx.storage.set("touches", touches)
|
||||
|
||||
def on_close(self):
|
||||
self.ctx.notify({name!r}, "Session closed")
|
||||
"""
|
||||
(root / entry).write_text(app_code, encoding="utf-8")
|
||||
elif package_type == "theme":
|
||||
(root / entry).write_text(json.dumps({"accent": "#ffcc00", "background": "#000000"}, indent=2), encoding="utf-8")
|
||||
else:
|
||||
script_code = f"""def main(ctx):
|
||||
ctx.display.title({name!r})
|
||||
ctx.display.text("Hello from a BeeOS {package_type} package.")
|
||||
ctx.notify({name!r}, "Package ran through BeeOS Runtime.")
|
||||
"""
|
||||
(root / entry).write_text(script_code, encoding="utf-8")
|
||||
|
||||
print(f"Created BeeOS package template: {root}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: py tools/create_package_template.py <type> <package.id> <Name>")
|
||||
print("Example: py tools/create_package_template.py app local.test.app \"Test App\"")
|
||||
raise SystemExit(1)
|
||||
|
||||
create_template(sys.argv[1], sys.argv[2], " ".join(sys.argv[3:]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Export BeeOS themes into firmware-friendly C and JSON assets."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def hex_to_int(value: str) -> int:
|
||||
value = value.strip().lstrip("#")
|
||||
return int(value, 16)
|
||||
|
||||
|
||||
def load_themes(project_root: Path) -> list[dict]:
|
||||
path = project_root / "beeos" / "data" / "themes.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return list(data.get("themes", []))
|
||||
|
||||
|
||||
def export_themes(project_root: Path) -> dict:
|
||||
themes = load_themes(project_root)
|
||||
out_dir = project_root / "beeos" / "generated" / "themes"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
json_out = out_dir / "beeos_themes.json"
|
||||
json_out.write_text(json.dumps({"themes": themes}, indent=2), encoding="utf-8")
|
||||
|
||||
header = out_dir / "beeos_themes.h"
|
||||
source = out_dir / "beeos_themes.c"
|
||||
|
||||
header_lines = [
|
||||
"/* Auto-generated BeeOS theme declarations. */",
|
||||
"#pragma once",
|
||||
"",
|
||||
"#include <stdint.h>",
|
||||
"",
|
||||
"typedef struct BeeOSTheme {",
|
||||
" const char *id;",
|
||||
" const char *name;",
|
||||
" uint32_t accent;",
|
||||
" uint32_t background;",
|
||||
" uint32_t card;",
|
||||
" uint32_t text;",
|
||||
" uint32_t muted;",
|
||||
"} BeeOSTheme;",
|
||||
"",
|
||||
"extern const BeeOSTheme beeos_themes[];",
|
||||
"extern const uint32_t beeos_themes_count;",
|
||||
"",
|
||||
]
|
||||
|
||||
source_lines = [
|
||||
"/* Auto-generated BeeOS theme table. */",
|
||||
"",
|
||||
'#include "beeos_themes.h"',
|
||||
"",
|
||||
"const BeeOSTheme beeos_themes[] = {",
|
||||
]
|
||||
|
||||
for theme in themes:
|
||||
source_lines.append(
|
||||
' {"%s", "%s", 0x%06x, 0x%06x, 0x%06x, 0x%06x, 0x%06x},'
|
||||
% (
|
||||
theme["id"],
|
||||
theme["name"],
|
||||
hex_to_int(theme["accent"]),
|
||||
hex_to_int(theme["background"]),
|
||||
hex_to_int(theme["card"]),
|
||||
hex_to_int(theme["text"]),
|
||||
hex_to_int(theme["muted"]),
|
||||
)
|
||||
)
|
||||
|
||||
source_lines.append("};")
|
||||
source_lines.append("")
|
||||
source_lines.append(f"const uint32_t beeos_themes_count = {len(themes)};")
|
||||
source_lines.append("")
|
||||
|
||||
header.write_text("\\n".join(header_lines), encoding="utf-8")
|
||||
source.write_text("\\n".join(source_lines), encoding="utf-8")
|
||||
|
||||
index = {
|
||||
"theme_count": len(themes),
|
||||
"outputs": {
|
||||
"json": str(json_out.relative_to(project_root)),
|
||||
"header": str(header.relative_to(project_root)),
|
||||
"source": str(source.relative_to(project_root)),
|
||||
},
|
||||
}
|
||||
index_out = out_dir / "theme_build_index.json"
|
||||
index_out.write_text(json.dumps(index, indent=2), encoding="utf-8")
|
||||
return index
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Export BeeOS themes into firmware-friendly assets.")
|
||||
parser.add_argument("--project-root", default=".", help="BeeOS project root")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = export_themes(Path(args.project_root).resolve())
|
||||
print("BeeOS theme export complete.")
|
||||
print(f"Themes: {result['theme_count']}")
|
||||
for key, value in result["outputs"].items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Import a HiveStore repo into BeeOS.
|
||||
|
||||
Supports:
|
||||
- local JSON file path
|
||||
- raw GitHub JSON URL
|
||||
- normal GitHub repo URL, normalized to /raw/main/hivestore_index.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from beeos.core.hivestore_sources import HiveStoreSourceManager, normalize_github_url, source_id_from_url
|
||||
|
||||
|
||||
def fetch_or_copy_index(url_or_path: str, cache_path: Path) -> dict:
|
||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
local = Path(url_or_path)
|
||||
if local.exists():
|
||||
shutil.copyfile(local, cache_path)
|
||||
return json.loads(cache_path.read_text(encoding="utf-8"))
|
||||
|
||||
normalized = normalize_github_url(url_or_path)
|
||||
with urllib.request.urlopen(normalized, timeout=20) as response:
|
||||
data = response.read()
|
||||
cache_path.write_bytes(data)
|
||||
return json.loads(data.decode("utf-8"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Import a BeeOS HiveStore repo source.")
|
||||
parser.add_argument("source", help="Local hivestore_index.json, raw URL, or GitHub repo URL")
|
||||
parser.add_argument("--project-root", default=".", help="BeeOS project root")
|
||||
parser.add_argument("--name", default="", help="Display name override")
|
||||
parser.add_argument("--no-fetch", action="store_true", help="Add source record without fetching/cache validation")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = Path(args.project_root).resolve()
|
||||
data_dir = project_root / "beeos" / "data"
|
||||
manager = HiveStoreSourceManager(data_dir)
|
||||
|
||||
normalized = normalize_github_url(args.source)
|
||||
source_id = source_id_from_url(normalized)
|
||||
cache_path = data_dir / "hivestore_cache" / f"{source_id}.json"
|
||||
|
||||
name = args.name or "Imported HiveStore Repo"
|
||||
|
||||
if not args.no_fetch:
|
||||
data = fetch_or_copy_index(args.source, cache_path)
|
||||
name = args.name or data.get("source_name", name)
|
||||
index = str(cache_path.relative_to(data_dir))
|
||||
else:
|
||||
index = normalized
|
||||
|
||||
source = manager.add_source(name=name, url=args.source, index=index, enabled=True)
|
||||
print("Imported HiveStore source:")
|
||||
print(f" id: {source.source_id}")
|
||||
print(f" name: {source.name}")
|
||||
print(f" index: {source.index}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Validate the BeeOS project tree.
|
||||
|
||||
Checks:
|
||||
- Python syntax
|
||||
- JSON syntax
|
||||
- HiveStore source loading
|
||||
- package manifest loading
|
||||
- generated asset indexes
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import py_compile
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def check_python(project_root: Path) -> int:
|
||||
count = 0
|
||||
for py_file in project_root.rglob("*.py"):
|
||||
if "__pycache__" in py_file.parts:
|
||||
continue
|
||||
py_compile.compile(str(py_file), doraise=True)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def check_json(project_root: Path) -> int:
|
||||
count = 0
|
||||
for json_file in project_root.rglob("*.json"):
|
||||
json.loads(json_file.read_text(encoding="utf-8"))
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def check_hivestore(project_root: Path) -> tuple[int, int]:
|
||||
if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from beeos.core.hivestore import HiveStoreIndex
|
||||
|
||||
index = HiveStoreIndex(project_root / "beeos" / "data" / "hivestore_index.json")
|
||||
index.load()
|
||||
|
||||
if not index.source_manager.sources:
|
||||
fail("No HiveStore sources configured.")
|
||||
|
||||
if not index.listings:
|
||||
fail("HiveStore loaded zero package listings.")
|
||||
|
||||
return len(index.source_manager.sources), len(index.listings)
|
||||
|
||||
|
||||
def check_generated_assets(project_root: Path) -> dict:
|
||||
icon_index = project_root / "beeos" / "generated" / "icons" / "icon_build_index.json"
|
||||
theme_index = project_root / "beeos" / "generated" / "themes" / "theme_build_index.json"
|
||||
|
||||
result = {
|
||||
"icon_index": icon_index.exists(),
|
||||
"theme_index": theme_index.exists(),
|
||||
"icon_count": 0,
|
||||
"theme_count": 0,
|
||||
}
|
||||
|
||||
if icon_index.exists():
|
||||
data = json.loads(icon_index.read_text(encoding="utf-8"))
|
||||
result["icon_count"] = int(data.get("icon_count", 0))
|
||||
|
||||
if theme_index.exists():
|
||||
data = json.loads(theme_index.read_text(encoding="utf-8"))
|
||||
result["theme_count"] = int(data.get("theme_count", 0))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Validate the BeeOS project.")
|
||||
parser.add_argument("--project-root", default=".", help="BeeOS project root")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = Path(args.project_root).resolve()
|
||||
|
||||
python_count = check_python(project_root)
|
||||
json_count = check_json(project_root)
|
||||
source_count, listing_count = check_hivestore(project_root)
|
||||
assets = check_generated_assets(project_root)
|
||||
|
||||
print("BeeOS validation passed.")
|
||||
print(f"Python files: {python_count}")
|
||||
print(f"JSON files: {json_count}")
|
||||
print(f"HiveStore sources: {source_count}")
|
||||
print(f"HiveStore listings: {listing_count}")
|
||||
print(f"Generated icons: {assets['icon_count']}")
|
||||
print(f"Generated themes: {assets['theme_count']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user