diff --git a/CMakeLists.txt b/CMakeLists.txt index 890701b..27b2b49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -428,9 +428,22 @@ endif() if(WIN32) install(TARGETS cst-agent RUNTIME DESTINATION .) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf") + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf" DESTINATION .) + endif() else() include(GNUInstallDirs) install(TARGETS cst-agent RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf") + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf" + DESTINATION ${CMAKE_INSTALL_DATADIR}/hermit) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf" + DESTINATION ${CMAKE_INSTALL_DATADIR}/fonts/hermit) + endif() + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-frames") + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-frames/" + DESTINATION ${CMAKE_INSTALL_DATADIR}/hermit/frames) + endif() endif() message(STATUS "Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER}") diff --git a/MIGRATION.md b/MIGRATION.md index 18d34e5..153bc1a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -47,7 +47,7 @@ git clone https://git.whcself.xyz/WHC/CST-agent.git # 兼容;请改 origin | Release `cst-agent-VERSION-linux-*.tar.gz` | `hermit-VERSION-linux-*.tar.gz`(过渡期可能双发) | ```bash -hermit --version # hermit 0.10.x +hermit --version # hermit 1.0.0 cst-agent --version # 同二进制(软链) ``` diff --git a/Makefile b/Makefile index a00b32b..784a863 100644 --- a/Makefile +++ b/Makefile @@ -219,8 +219,8 @@ help: @echo " all / hermit incremental build (ccache auto if installed)" @echo " check build + run unit tests" @echo " asan AddressSanitizer rebuild + check" - @echo " hermit-font optional SVG→color font (needs rsvg + fonttools)" - @echo " install PREFIX=$(PREFIX); hermit + cst-agent symlink" + @echo " hermit-font bake PNG frames + hermit-icons.ttf (Pillow+fonttools)" + @echo " install PREFIX=$(PREFIX); hermit + font + cst-agent symlink" @echo " install-service systemd --user parliament unit" @echo "Env: USE_CCACHE=0|1|auto STATIC=1 CST_WEBUI=0 CC=tcc" @echo " HERMIT_ICONS=auto|mini|font HERMIT_*/CST_* env" @@ -250,7 +250,7 @@ hermit: $(OBJ) sashidown-lib hermit-font: @python3 scripts/build_hermit_font.py || \ - echo "hermit-font: skipped (install rsvg-convert + fonttools to build)" + echo "hermit-font: skipped (need Pillow+fonttools; try: nix-shell -p python3Packages.pillow python3Packages.fonttools)" # Built-in model data as a C byte list (included by model_registry.c). .gen/models_builtin.inc: src/models_builtin.json @@ -328,9 +328,24 @@ install: hermit install -m 0755 hermit "$(DESTDIR)$(PREFIX)/bin/hermit" ln -sfr "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent" 2>/dev/null || \ ln -sf hermit "$(DESTDIR)$(PREFIX)/bin/cst-agent" + @if [ ! -f res/hermit-icons.ttf ]; then $(MAKE) hermit-font || true; fi @if [ -f res/hermit-icons.ttf ]; then \ install -d "$(DESTDIR)$(PREFIX)/share/hermit"; \ - install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/hermit/"; \ + install -d "$(DESTDIR)$(PREFIX)/share/fonts/hermit"; \ + install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/hermit/hermit-icons.ttf"; \ + install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf"; \ + if [ -n "$(HOME)" ] && [ "$(DESTDIR)" = "" ]; then \ + install -d "$(HOME)/.local/share/fonts"; \ + install -m 0644 res/hermit-icons.ttf "$(HOME)/.local/share/fonts/hermit-icons.ttf"; \ + command -v fc-cache >/dev/null 2>&1 && fc-cache -f "$(HOME)/.local/share/fonts" >/dev/null 2>&1 || true; \ + fi; \ + echo "installed Hermit Icons → $(PREFIX)/share/fonts/hermit/hermit-icons.ttf"; \ + else \ + echo "warning: res/hermit-icons.ttf missing (run: make hermit-font)" >&2; \ + fi + @if [ -d res/hermit-frames ]; then \ + install -d "$(DESTDIR)$(PREFIX)/share/hermit/frames"; \ + cp -a res/hermit-frames/. "$(DESTDIR)$(PREFIX)/share/hermit/frames/"; \ fi # Optional systemd --user unit for the parliament seat service. @@ -346,6 +361,10 @@ install-service: install uninstall: rm -f "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent" + rm -f "$(DESTDIR)$(PREFIX)/share/hermit/hermit-icons.ttf" + rm -f "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf" + rm -rf "$(DESTDIR)$(PREFIX)/share/hermit/frames" + rm -f "$(HOME)/.local/share/fonts/hermit-icons.ttf" rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service" -include $(DEPFILES) diff --git a/README.md b/README.md index c9a72f0..fe39895 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ curl -fsSL https://git.whcself.xyz/WHC/Hermit/raw/branch/main/scripts/bootstrap_ ```bash ./scripts/bootstrap_linux.sh . -hermit --version # hermit 0.10.x +hermit --version # hermit 1.0.0 cst-agent-update # 升级助手仍可用 ./scripts/migrate_remote_to_hermit.sh # 已有 CST-agent 检出时改 origin ``` @@ -54,11 +54,11 @@ cd ~/my-project && hermit ```bash make -j$(nproc) # 产出 hermit(并链 cst-agent) make -j$(nproc) check -make hermit-font # 可选:SVG→PUA 字体(需 rsvg/fonttools) -make install PREFIX=$HOME/.local +make hermit-font # 烘焙 PNG 帧 + hermit-icons.ttf(Pillow + fonttools) +make install PREFIX=$HOME/.local # 同时安装字体到 share/fonts/hermit 与 ~/.local/share/fonts ``` -TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认极简字符帧;`font` 需安装 `share/hermit/hermit-icons.ttf`)。 +TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认色块动画帧,状态与 `icons/agent-motion` HTML 套件一致;`font` 输出 PUA `U+E700+`,需终端加载已安装的 Hermit Icons)。 --- @@ -89,4 +89,4 @@ scripts/prompt_smoke.sh scripts/e2e_webui.sh ``` -`icons/agent-motion/` 为 SVG 资产;TUI 默认用字体序列极简轨,`make hermit-font` 生成 PUA 字体轨。 +`icons/agent-motion/` 为 SVG 资产;`make hermit-font` 生成 `res/hermit-icons.ttf`(sbix 彩色位图,96 帧)与 `res/hermit-frames/`。安装脚本会把字体装进系统字体目录。 diff --git a/icons/agent-motion/README.md b/icons/agent-motion/README.md index 75ecfef..6d9f5b9 100644 --- a/icons/agent-motion/README.md +++ b/icons/agent-motion/README.md @@ -18,6 +18,6 @@ icons/{状态}-shell.svg 是大的带壳图标;icons/{状态}.svg 是小的无 普通字符终端不直接渲染 SVG;图标适用于支持图形的终端界面、Webview 或 CLI 伴随应用。 -**TUI 双轨**:默认极简字符/色块帧见 `src/hermit_glyph.c`;`make hermit-font`(`scripts/build_hermit_font.py`)把本目录 SVG 批量打成 `res/hermit-icons.ttf`(PUA `U+E700+`),`HERMIT_ICONS=font` 时直接输出码点序列。 +**TUI 双轨**:默认极简字符/色块帧见 `src/hermit_glyph.c`(idle 眨眼、coding `>_`、ask/thinking/success/error,与本目录 HTML 动画状态一致);`make hermit-font`(Pillow + fonttools,无需 rsvg)烘焙 `res/hermit-frames/` 与 `res/hermit-icons.ttf`(sbix,PUA `U+E700+`)。`make install` / bootstrap 会安装该字体;`HERMIT_ICONS=font` 时输出码点序列(终端需选用 Hermit Icons)。 更新:橘杏色身体、莓紫眼睛、低饱和蓝绿终端壳。预览主角色以 620ms 缓入缓出插值连续改变眼睛路径与身体姿态,支持中途切换;不重建主角色。状态符号淡入淡出。独立 SVG 保留各状态循环动效;跨状态变形逻辑位于 index.html。 diff --git a/include/cst_hermit_glyph.h b/include/cst_hermit_glyph.h index 2e6a18b..fe78c49 100644 --- a/include/cst_hermit_glyph.h +++ b/include/cst_hermit_glyph.h @@ -45,8 +45,13 @@ const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii); /* Home shell crab: up to *nlines strings (not owned). Returns line count. */ int hermit_shell_art(hermit_state_t st, int with_shell, int ascii, const char **lines, int max_lines); +/* Same as hermit_shell_art, but advances HTML-kit animation by frame. */ +int hermit_shell_art_frame(hermit_state_t st, int with_shell, int ascii, + int frame, const char **lines, int max_lines); +int hermit_shell_art_width(void); +int hermit_shell_art_height(void); -/* Color roles for art cells: 0=body(crab) 1=shell 2=eye 3=leg 4=prompt 5=mark */ +/* Color roles for art cells: 0=body 1=shell 2=eye 3=leg 4=prompt 5=mark */ int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col); /* PUA base U+E700; layout: shell*HERMIT_ST_N*HERMIT_FONT_FRAMES + st*FRAMES + frame */ @@ -54,6 +59,9 @@ int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col); #define HERMIT_FONT_FRAMES 8 wchar_t hermit_font_codepoint(hermit_state_t st, int frame, int with_shell); +/* Non-empty path when hermit-icons.ttf is installed (may be ""). */ +const char *hermit_font_path(void); + /* Map agent UI signals → icon state. */ hermit_state_t hermit_state_from_ui(int busy, int ask, int error); diff --git a/include/cst_version.h b/include/cst_version.h index a81d868..bf1d5f4 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -1,9 +1,9 @@ #ifndef CST_VERSION_H #define CST_VERSION_H -#define CST_VERSION_MAJOR 0 -#define CST_VERSION_MINOR 10 -#define CST_VERSION_PATCH 1 +#define CST_VERSION_MAJOR 1 +#define CST_VERSION_MINOR 0 +#define CST_VERSION_PATCH 0 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/res/hermit-frames/bare/coding/f00.png b/res/hermit-frames/bare/coding/f00.png new file mode 100644 index 0000000..899b1e3 Binary files /dev/null and b/res/hermit-frames/bare/coding/f00.png differ diff --git a/res/hermit-frames/bare/coding/f01.png b/res/hermit-frames/bare/coding/f01.png new file mode 100644 index 0000000..899b1e3 Binary files /dev/null and b/res/hermit-frames/bare/coding/f01.png differ diff --git a/res/hermit-frames/bare/coding/f02.png b/res/hermit-frames/bare/coding/f02.png new file mode 100644 index 0000000..168431b Binary files /dev/null and b/res/hermit-frames/bare/coding/f02.png differ diff --git a/res/hermit-frames/bare/coding/f03.png b/res/hermit-frames/bare/coding/f03.png new file mode 100644 index 0000000..899b1e3 Binary files /dev/null and b/res/hermit-frames/bare/coding/f03.png differ diff --git a/res/hermit-frames/bare/coding/f04.png b/res/hermit-frames/bare/coding/f04.png new file mode 100644 index 0000000..899b1e3 Binary files /dev/null and b/res/hermit-frames/bare/coding/f04.png differ diff --git a/res/hermit-frames/bare/coding/f05.png b/res/hermit-frames/bare/coding/f05.png new file mode 100644 index 0000000..1056702 Binary files /dev/null and b/res/hermit-frames/bare/coding/f05.png differ diff --git a/res/hermit-frames/bare/coding/f06.png b/res/hermit-frames/bare/coding/f06.png new file mode 100644 index 0000000..bcf3b93 Binary files /dev/null and b/res/hermit-frames/bare/coding/f06.png differ diff --git a/res/hermit-frames/bare/coding/f07.png b/res/hermit-frames/bare/coding/f07.png new file mode 100644 index 0000000..1056702 Binary files /dev/null and b/res/hermit-frames/bare/coding/f07.png differ diff --git a/res/hermit-frames/bare/error/f00.png b/res/hermit-frames/bare/error/f00.png new file mode 100644 index 0000000..32f139c Binary files /dev/null and b/res/hermit-frames/bare/error/f00.png differ diff --git a/res/hermit-frames/bare/error/f01.png b/res/hermit-frames/bare/error/f01.png new file mode 100644 index 0000000..32f139c Binary files /dev/null and b/res/hermit-frames/bare/error/f01.png differ diff --git a/res/hermit-frames/bare/error/f02.png b/res/hermit-frames/bare/error/f02.png new file mode 100644 index 0000000..ccc44bb Binary files /dev/null and b/res/hermit-frames/bare/error/f02.png differ diff --git a/res/hermit-frames/bare/error/f03.png b/res/hermit-frames/bare/error/f03.png new file mode 100644 index 0000000..32f139c Binary files /dev/null and b/res/hermit-frames/bare/error/f03.png differ diff --git a/res/hermit-frames/bare/error/f04.png b/res/hermit-frames/bare/error/f04.png new file mode 100644 index 0000000..32f139c Binary files /dev/null and b/res/hermit-frames/bare/error/f04.png differ diff --git a/res/hermit-frames/bare/error/f05.png b/res/hermit-frames/bare/error/f05.png new file mode 100644 index 0000000..9b6a7b8 Binary files /dev/null and b/res/hermit-frames/bare/error/f05.png differ diff --git a/res/hermit-frames/bare/error/f06.png b/res/hermit-frames/bare/error/f06.png new file mode 100644 index 0000000..910bd31 Binary files /dev/null and b/res/hermit-frames/bare/error/f06.png differ diff --git a/res/hermit-frames/bare/error/f07.png b/res/hermit-frames/bare/error/f07.png new file mode 100644 index 0000000..9b6a7b8 Binary files /dev/null and b/res/hermit-frames/bare/error/f07.png differ diff --git a/res/hermit-frames/bare/idle/f00.png b/res/hermit-frames/bare/idle/f00.png new file mode 100644 index 0000000..8010d03 Binary files /dev/null and b/res/hermit-frames/bare/idle/f00.png differ diff --git a/res/hermit-frames/bare/idle/f01.png b/res/hermit-frames/bare/idle/f01.png new file mode 100644 index 0000000..8010d03 Binary files /dev/null and b/res/hermit-frames/bare/idle/f01.png differ diff --git a/res/hermit-frames/bare/idle/f02.png b/res/hermit-frames/bare/idle/f02.png new file mode 100644 index 0000000..1f2f1cc Binary files /dev/null and b/res/hermit-frames/bare/idle/f02.png differ diff --git a/res/hermit-frames/bare/idle/f03.png b/res/hermit-frames/bare/idle/f03.png new file mode 100644 index 0000000..41abe3a Binary files /dev/null and b/res/hermit-frames/bare/idle/f03.png differ diff --git a/res/hermit-frames/bare/idle/f04.png b/res/hermit-frames/bare/idle/f04.png new file mode 100644 index 0000000..41abe3a Binary files /dev/null and b/res/hermit-frames/bare/idle/f04.png differ diff --git a/res/hermit-frames/bare/idle/f05.png b/res/hermit-frames/bare/idle/f05.png new file mode 100644 index 0000000..3e5ca73 Binary files /dev/null and b/res/hermit-frames/bare/idle/f05.png differ diff --git a/res/hermit-frames/bare/idle/f06.png b/res/hermit-frames/bare/idle/f06.png new file mode 100644 index 0000000..1025104 Binary files /dev/null and b/res/hermit-frames/bare/idle/f06.png differ diff --git a/res/hermit-frames/bare/idle/f07.png b/res/hermit-frames/bare/idle/f07.png new file mode 100644 index 0000000..3e5ca73 Binary files /dev/null and b/res/hermit-frames/bare/idle/f07.png differ diff --git a/res/hermit-frames/bare/question/f00.png b/res/hermit-frames/bare/question/f00.png new file mode 100644 index 0000000..e39f30c Binary files /dev/null and b/res/hermit-frames/bare/question/f00.png differ diff --git a/res/hermit-frames/bare/question/f01.png b/res/hermit-frames/bare/question/f01.png new file mode 100644 index 0000000..e39f30c Binary files /dev/null and b/res/hermit-frames/bare/question/f01.png differ diff --git a/res/hermit-frames/bare/question/f02.png b/res/hermit-frames/bare/question/f02.png new file mode 100644 index 0000000..92a8b6e Binary files /dev/null and b/res/hermit-frames/bare/question/f02.png differ diff --git a/res/hermit-frames/bare/question/f03.png b/res/hermit-frames/bare/question/f03.png new file mode 100644 index 0000000..9f3e625 Binary files /dev/null and b/res/hermit-frames/bare/question/f03.png differ diff --git a/res/hermit-frames/bare/question/f04.png b/res/hermit-frames/bare/question/f04.png new file mode 100644 index 0000000..9f3e625 Binary files /dev/null and b/res/hermit-frames/bare/question/f04.png differ diff --git a/res/hermit-frames/bare/question/f05.png b/res/hermit-frames/bare/question/f05.png new file mode 100644 index 0000000..0b436d7 Binary files /dev/null and b/res/hermit-frames/bare/question/f05.png differ diff --git a/res/hermit-frames/bare/question/f06.png b/res/hermit-frames/bare/question/f06.png new file mode 100644 index 0000000..90f5b08 Binary files /dev/null and b/res/hermit-frames/bare/question/f06.png differ diff --git a/res/hermit-frames/bare/question/f07.png b/res/hermit-frames/bare/question/f07.png new file mode 100644 index 0000000..9840a7a Binary files /dev/null and b/res/hermit-frames/bare/question/f07.png differ diff --git a/res/hermit-frames/bare/success/f00.png b/res/hermit-frames/bare/success/f00.png new file mode 100644 index 0000000..aef7942 Binary files /dev/null and b/res/hermit-frames/bare/success/f00.png differ diff --git a/res/hermit-frames/bare/success/f01.png b/res/hermit-frames/bare/success/f01.png new file mode 100644 index 0000000..f71efef Binary files /dev/null and b/res/hermit-frames/bare/success/f01.png differ diff --git a/res/hermit-frames/bare/success/f02.png b/res/hermit-frames/bare/success/f02.png new file mode 100644 index 0000000..f71efef Binary files /dev/null and b/res/hermit-frames/bare/success/f02.png differ diff --git a/res/hermit-frames/bare/success/f03.png b/res/hermit-frames/bare/success/f03.png new file mode 100644 index 0000000..f71efef Binary files /dev/null and b/res/hermit-frames/bare/success/f03.png differ diff --git a/res/hermit-frames/bare/success/f04.png b/res/hermit-frames/bare/success/f04.png new file mode 100644 index 0000000..aef7942 Binary files /dev/null and b/res/hermit-frames/bare/success/f04.png differ diff --git a/res/hermit-frames/bare/success/f05.png b/res/hermit-frames/bare/success/f05.png new file mode 100644 index 0000000..cb92a83 Binary files /dev/null and b/res/hermit-frames/bare/success/f05.png differ diff --git a/res/hermit-frames/bare/success/f06.png b/res/hermit-frames/bare/success/f06.png new file mode 100644 index 0000000..1cdd564 Binary files /dev/null and b/res/hermit-frames/bare/success/f06.png differ diff --git a/res/hermit-frames/bare/success/f07.png b/res/hermit-frames/bare/success/f07.png new file mode 100644 index 0000000..cb92a83 Binary files /dev/null and b/res/hermit-frames/bare/success/f07.png differ diff --git a/res/hermit-frames/bare/thinking/f00.png b/res/hermit-frames/bare/thinking/f00.png new file mode 100644 index 0000000..6a209e7 Binary files /dev/null and b/res/hermit-frames/bare/thinking/f00.png differ diff --git a/res/hermit-frames/bare/thinking/f01.png b/res/hermit-frames/bare/thinking/f01.png new file mode 100644 index 0000000..6a209e7 Binary files /dev/null and b/res/hermit-frames/bare/thinking/f01.png differ diff --git a/res/hermit-frames/bare/thinking/f02.png b/res/hermit-frames/bare/thinking/f02.png new file mode 100644 index 0000000..d073945 Binary files /dev/null and b/res/hermit-frames/bare/thinking/f02.png differ diff --git a/res/hermit-frames/bare/thinking/f03.png b/res/hermit-frames/bare/thinking/f03.png new file mode 100644 index 0000000..59bdacb Binary files /dev/null and b/res/hermit-frames/bare/thinking/f03.png differ diff --git a/res/hermit-frames/bare/thinking/f04.png b/res/hermit-frames/bare/thinking/f04.png new file mode 100644 index 0000000..6a209e7 Binary files /dev/null and b/res/hermit-frames/bare/thinking/f04.png differ diff --git a/res/hermit-frames/bare/thinking/f05.png b/res/hermit-frames/bare/thinking/f05.png new file mode 100644 index 0000000..a06da2d Binary files /dev/null and b/res/hermit-frames/bare/thinking/f05.png differ diff --git a/res/hermit-frames/bare/thinking/f06.png b/res/hermit-frames/bare/thinking/f06.png new file mode 100644 index 0000000..a28e748 Binary files /dev/null and b/res/hermit-frames/bare/thinking/f06.png differ diff --git a/res/hermit-frames/bare/thinking/f07.png b/res/hermit-frames/bare/thinking/f07.png new file mode 100644 index 0000000..aa519bd Binary files /dev/null and b/res/hermit-frames/bare/thinking/f07.png differ diff --git a/res/hermit-frames/shell/coding/f00.png b/res/hermit-frames/shell/coding/f00.png new file mode 100644 index 0000000..decd0ce Binary files /dev/null and b/res/hermit-frames/shell/coding/f00.png differ diff --git a/res/hermit-frames/shell/coding/f01.png b/res/hermit-frames/shell/coding/f01.png new file mode 100644 index 0000000..decd0ce Binary files /dev/null and b/res/hermit-frames/shell/coding/f01.png differ diff --git a/res/hermit-frames/shell/coding/f02.png b/res/hermit-frames/shell/coding/f02.png new file mode 100644 index 0000000..d26e171 Binary files /dev/null and b/res/hermit-frames/shell/coding/f02.png differ diff --git a/res/hermit-frames/shell/coding/f03.png b/res/hermit-frames/shell/coding/f03.png new file mode 100644 index 0000000..decd0ce Binary files /dev/null and b/res/hermit-frames/shell/coding/f03.png differ diff --git a/res/hermit-frames/shell/coding/f04.png b/res/hermit-frames/shell/coding/f04.png new file mode 100644 index 0000000..decd0ce Binary files /dev/null and b/res/hermit-frames/shell/coding/f04.png differ diff --git a/res/hermit-frames/shell/coding/f05.png b/res/hermit-frames/shell/coding/f05.png new file mode 100644 index 0000000..4052e46 Binary files /dev/null and b/res/hermit-frames/shell/coding/f05.png differ diff --git a/res/hermit-frames/shell/coding/f06.png b/res/hermit-frames/shell/coding/f06.png new file mode 100644 index 0000000..20a191b Binary files /dev/null and b/res/hermit-frames/shell/coding/f06.png differ diff --git a/res/hermit-frames/shell/coding/f07.png b/res/hermit-frames/shell/coding/f07.png new file mode 100644 index 0000000..4052e46 Binary files /dev/null and b/res/hermit-frames/shell/coding/f07.png differ diff --git a/res/hermit-frames/shell/error/f00.png b/res/hermit-frames/shell/error/f00.png new file mode 100644 index 0000000..53157aa Binary files /dev/null and b/res/hermit-frames/shell/error/f00.png differ diff --git a/res/hermit-frames/shell/error/f01.png b/res/hermit-frames/shell/error/f01.png new file mode 100644 index 0000000..53157aa Binary files /dev/null and b/res/hermit-frames/shell/error/f01.png differ diff --git a/res/hermit-frames/shell/error/f02.png b/res/hermit-frames/shell/error/f02.png new file mode 100644 index 0000000..1d43812 Binary files /dev/null and b/res/hermit-frames/shell/error/f02.png differ diff --git a/res/hermit-frames/shell/error/f03.png b/res/hermit-frames/shell/error/f03.png new file mode 100644 index 0000000..53157aa Binary files /dev/null and b/res/hermit-frames/shell/error/f03.png differ diff --git a/res/hermit-frames/shell/error/f04.png b/res/hermit-frames/shell/error/f04.png new file mode 100644 index 0000000..53157aa Binary files /dev/null and b/res/hermit-frames/shell/error/f04.png differ diff --git a/res/hermit-frames/shell/error/f05.png b/res/hermit-frames/shell/error/f05.png new file mode 100644 index 0000000..36346aa Binary files /dev/null and b/res/hermit-frames/shell/error/f05.png differ diff --git a/res/hermit-frames/shell/error/f06.png b/res/hermit-frames/shell/error/f06.png new file mode 100644 index 0000000..9b4aee1 Binary files /dev/null and b/res/hermit-frames/shell/error/f06.png differ diff --git a/res/hermit-frames/shell/error/f07.png b/res/hermit-frames/shell/error/f07.png new file mode 100644 index 0000000..36346aa Binary files /dev/null and b/res/hermit-frames/shell/error/f07.png differ diff --git a/res/hermit-frames/shell/idle/f00.png b/res/hermit-frames/shell/idle/f00.png new file mode 100644 index 0000000..b9fb24b Binary files /dev/null and b/res/hermit-frames/shell/idle/f00.png differ diff --git a/res/hermit-frames/shell/idle/f01.png b/res/hermit-frames/shell/idle/f01.png new file mode 100644 index 0000000..b9fb24b Binary files /dev/null and b/res/hermit-frames/shell/idle/f01.png differ diff --git a/res/hermit-frames/shell/idle/f02.png b/res/hermit-frames/shell/idle/f02.png new file mode 100644 index 0000000..02a2c6f Binary files /dev/null and b/res/hermit-frames/shell/idle/f02.png differ diff --git a/res/hermit-frames/shell/idle/f03.png b/res/hermit-frames/shell/idle/f03.png new file mode 100644 index 0000000..a5aeecc Binary files /dev/null and b/res/hermit-frames/shell/idle/f03.png differ diff --git a/res/hermit-frames/shell/idle/f04.png b/res/hermit-frames/shell/idle/f04.png new file mode 100644 index 0000000..a5aeecc Binary files /dev/null and b/res/hermit-frames/shell/idle/f04.png differ diff --git a/res/hermit-frames/shell/idle/f05.png b/res/hermit-frames/shell/idle/f05.png new file mode 100644 index 0000000..8e1893c Binary files /dev/null and b/res/hermit-frames/shell/idle/f05.png differ diff --git a/res/hermit-frames/shell/idle/f06.png b/res/hermit-frames/shell/idle/f06.png new file mode 100644 index 0000000..9ab7107 Binary files /dev/null and b/res/hermit-frames/shell/idle/f06.png differ diff --git a/res/hermit-frames/shell/idle/f07.png b/res/hermit-frames/shell/idle/f07.png new file mode 100644 index 0000000..8e1893c Binary files /dev/null and b/res/hermit-frames/shell/idle/f07.png differ diff --git a/res/hermit-frames/shell/question/f00.png b/res/hermit-frames/shell/question/f00.png new file mode 100644 index 0000000..bac3855 Binary files /dev/null and b/res/hermit-frames/shell/question/f00.png differ diff --git a/res/hermit-frames/shell/question/f01.png b/res/hermit-frames/shell/question/f01.png new file mode 100644 index 0000000..bac3855 Binary files /dev/null and b/res/hermit-frames/shell/question/f01.png differ diff --git a/res/hermit-frames/shell/question/f02.png b/res/hermit-frames/shell/question/f02.png new file mode 100644 index 0000000..bc2bea4 Binary files /dev/null and b/res/hermit-frames/shell/question/f02.png differ diff --git a/res/hermit-frames/shell/question/f03.png b/res/hermit-frames/shell/question/f03.png new file mode 100644 index 0000000..9d0cda9 Binary files /dev/null and b/res/hermit-frames/shell/question/f03.png differ diff --git a/res/hermit-frames/shell/question/f04.png b/res/hermit-frames/shell/question/f04.png new file mode 100644 index 0000000..9d0cda9 Binary files /dev/null and b/res/hermit-frames/shell/question/f04.png differ diff --git a/res/hermit-frames/shell/question/f05.png b/res/hermit-frames/shell/question/f05.png new file mode 100644 index 0000000..52ce52e Binary files /dev/null and b/res/hermit-frames/shell/question/f05.png differ diff --git a/res/hermit-frames/shell/question/f06.png b/res/hermit-frames/shell/question/f06.png new file mode 100644 index 0000000..738526a Binary files /dev/null and b/res/hermit-frames/shell/question/f06.png differ diff --git a/res/hermit-frames/shell/question/f07.png b/res/hermit-frames/shell/question/f07.png new file mode 100644 index 0000000..5a4a747 Binary files /dev/null and b/res/hermit-frames/shell/question/f07.png differ diff --git a/res/hermit-frames/shell/success/f00.png b/res/hermit-frames/shell/success/f00.png new file mode 100644 index 0000000..7d62115 Binary files /dev/null and b/res/hermit-frames/shell/success/f00.png differ diff --git a/res/hermit-frames/shell/success/f01.png b/res/hermit-frames/shell/success/f01.png new file mode 100644 index 0000000..ba32270 Binary files /dev/null and b/res/hermit-frames/shell/success/f01.png differ diff --git a/res/hermit-frames/shell/success/f02.png b/res/hermit-frames/shell/success/f02.png new file mode 100644 index 0000000..ba32270 Binary files /dev/null and b/res/hermit-frames/shell/success/f02.png differ diff --git a/res/hermit-frames/shell/success/f03.png b/res/hermit-frames/shell/success/f03.png new file mode 100644 index 0000000..ba32270 Binary files /dev/null and b/res/hermit-frames/shell/success/f03.png differ diff --git a/res/hermit-frames/shell/success/f04.png b/res/hermit-frames/shell/success/f04.png new file mode 100644 index 0000000..7d62115 Binary files /dev/null and b/res/hermit-frames/shell/success/f04.png differ diff --git a/res/hermit-frames/shell/success/f05.png b/res/hermit-frames/shell/success/f05.png new file mode 100644 index 0000000..688a095 Binary files /dev/null and b/res/hermit-frames/shell/success/f05.png differ diff --git a/res/hermit-frames/shell/success/f06.png b/res/hermit-frames/shell/success/f06.png new file mode 100644 index 0000000..b1fbdce Binary files /dev/null and b/res/hermit-frames/shell/success/f06.png differ diff --git a/res/hermit-frames/shell/success/f07.png b/res/hermit-frames/shell/success/f07.png new file mode 100644 index 0000000..688a095 Binary files /dev/null and b/res/hermit-frames/shell/success/f07.png differ diff --git a/res/hermit-frames/shell/thinking/f00.png b/res/hermit-frames/shell/thinking/f00.png new file mode 100644 index 0000000..8bc21b1 Binary files /dev/null and b/res/hermit-frames/shell/thinking/f00.png differ diff --git a/res/hermit-frames/shell/thinking/f01.png b/res/hermit-frames/shell/thinking/f01.png new file mode 100644 index 0000000..8bc21b1 Binary files /dev/null and b/res/hermit-frames/shell/thinking/f01.png differ diff --git a/res/hermit-frames/shell/thinking/f02.png b/res/hermit-frames/shell/thinking/f02.png new file mode 100644 index 0000000..9aec75c Binary files /dev/null and b/res/hermit-frames/shell/thinking/f02.png differ diff --git a/res/hermit-frames/shell/thinking/f03.png b/res/hermit-frames/shell/thinking/f03.png new file mode 100644 index 0000000..562994a Binary files /dev/null and b/res/hermit-frames/shell/thinking/f03.png differ diff --git a/res/hermit-frames/shell/thinking/f04.png b/res/hermit-frames/shell/thinking/f04.png new file mode 100644 index 0000000..8bc21b1 Binary files /dev/null and b/res/hermit-frames/shell/thinking/f04.png differ diff --git a/res/hermit-frames/shell/thinking/f05.png b/res/hermit-frames/shell/thinking/f05.png new file mode 100644 index 0000000..128dfbb Binary files /dev/null and b/res/hermit-frames/shell/thinking/f05.png differ diff --git a/res/hermit-frames/shell/thinking/f06.png b/res/hermit-frames/shell/thinking/f06.png new file mode 100644 index 0000000..5c57f37 Binary files /dev/null and b/res/hermit-frames/shell/thinking/f06.png differ diff --git a/res/hermit-frames/shell/thinking/f07.png b/res/hermit-frames/shell/thinking/f07.png new file mode 100644 index 0000000..c8cfbf1 Binary files /dev/null and b/res/hermit-frames/shell/thinking/f07.png differ diff --git a/res/hermit-icons.ttf b/res/hermit-icons.ttf new file mode 100644 index 0000000..8ca6ee9 Binary files /dev/null and b/res/hermit-icons.ttf differ diff --git a/scripts/bootstrap_linux.sh b/scripts/bootstrap_linux.sh index 70c5a20..be51e81 100755 --- a/scripts/bootstrap_linux.sh +++ b/scripts/bootstrap_linux.sh @@ -49,6 +49,35 @@ install_bin() { ln -sf hermit "$prefix/bin/cst-agent" } +install_font() { + local prefix="$1" dir="$2" + local ttf="" + if [[ -f "$dir/res/hermit-icons.ttf" ]]; then + ttf="$dir/res/hermit-icons.ttf" + elif [[ -f "$dir/share/hermit/hermit-icons.ttf" ]]; then + ttf="$dir/share/hermit/hermit-icons.ttf" + elif [[ -f "$dir/hermit-icons.ttf" ]]; then + ttf="$dir/hermit-icons.ttf" + fi + [[ -n "$ttf" ]] || { + echo "warning: hermit-icons.ttf not found; run 'make hermit-font' in the tree" >&2 + return 0 + } + mkdir -p "$prefix/share/hermit" "$prefix/share/fonts/hermit" \ + "${HOME:-/tmp}/.local/share/fonts" + install -m 0644 "$ttf" "$prefix/share/hermit/hermit-icons.ttf" + install -m 0644 "$ttf" "$prefix/share/fonts/hermit/hermit-icons.ttf" + install -m 0644 "$ttf" "${HOME}/.local/share/fonts/hermit-icons.ttf" + if [[ -d "$dir/res/hermit-frames" ]]; then + mkdir -p "$prefix/share/hermit/frames" + cp -a "$dir/res/hermit-frames/." "$prefix/share/hermit/frames/" + fi + if command -v fc-cache >/dev/null 2>&1; then + fc-cache -f "${HOME}/.local/share/fonts" >/dev/null 2>&1 || true + fi + echo "==> installed Hermit Icons font" +} + install_helpers() { local prefix="$1" dir="$2" mkdir -p "$prefix/bin" @@ -123,8 +152,12 @@ install_from_release() { bin="$(find "$tmp" -type f \( -name hermit -o -name cst-agent \) | head -1)" [[ -x "$bin" ]] || { echo "tarball missing hermit/cst-agent" >&2; rm -rf "$tmp"; return 1; } install_bin "$prefix" "$bin" + # Font may live next to the binary inside the tarball. + local rel_dir + rel_dir="$(dirname "$bin")" + install_font "$prefix" "$rel_dir" rm -rf "$tmp" - echo "==> installed release $ver ($arch) -> $prefix/bin/cst-agent" + echo "==> installed release $ver ($arch) -> $prefix/bin/hermit" } main() { @@ -211,8 +244,11 @@ main() { run_make -j"$jobs" check >/dev/null 2>&1 || echo "warning: some tests failed (run 'make check')" fi - echo "==> install to $prefix/bin" - install_bin "$prefix" "$dir/cst-agent" + echo "==> bake Hermit Icons font (if tooling present)" + run_make hermit-font >/dev/null 2>&1 || true + echo "==> install to $prefix/bin (+ font)" + install_bin "$prefix" "$dir/hermit" + install_font "$prefix" "$dir" install_helpers "$prefix" "$dir" echo diff --git a/scripts/build_hermit_font.py b/scripts/build_hermit_font.py index 82483e1..aee437b 100755 --- a/scripts/build_hermit_font.py +++ b/scripts/build_hermit_font.py @@ -1,38 +1,121 @@ #!/usr/bin/env python3 -"""Batch-convert icons/agent-motion SVG states into a color emoji-style font. +"""Bake Hermit animation frames (HTML-kit states) into PNG + sbix color TTF. + +Geometry/colors follow icons/agent-motion. Animation mirrors the HTML kit: +idle breath/blink, ask tilt+?, thinking look+claw, coding >_, success smile, +error mark — as 8 discrete frames per state × bare/shell. Outputs: - res/hermit-icons.ttf (CBDT/CBLC bitmap glyphs when fonttools works) - .gen/hermit_font_map.inc (PUA codepoint table for the TUI) + res/hermit-frames/{bare,shell}/{state}/fNN.png + res/hermit-icons.ttf + .gen/hermit_font_map.inc -PUA layout matches cst_hermit_glyph.h: - U+E700 + (shell * N_STATE + state) * N_FRAME + frame - -Requires (optional): rsvg-convert or cairosvg, pillow, fonttools. -Without them, still writes hermit_font_map.inc so FONT mode can emit codepoints. + nix-shell -p python3Packages.pillow python3Packages.fonttools --run \\ + 'python3 scripts/build_hermit_font.py' """ from __future__ import annotations -import struct -import subprocess +import math import sys -import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -ICONS = ROOT / "icons" / "agent-motion" / "icons" +OUT_FRAMES = ROOT / "res" / "hermit-frames" OUT_TTF = ROOT / "res" / "hermit-icons.ttf" OUT_INC = ROOT / ".gen" / "hermit_font_map.inc" STATES = ["idle", "question", "thinking", "coding", "success", "error"] N_FRAME = 8 PUA_BASE = 0xE700 +SIZE = 64 + +SHELL = (0x27, 0x7F, 0x94, 255) +PROMPT = (0xA9, 0xD9, 0xD7, 255) +BODY = (0xF6, 0x80, 0x42, 255) +EYE = (0x99, 0x57, 0xAD, 255) +LEG = (0xA4, 0x57, 0x4F, 255) +MARK = (0xE9, 0xBA, 0x47, 255) +TRANS = (0, 0, 0, 0) def codepoint(shell: int, state: int, frame: int) -> int: return PUA_BASE + (shell * len(STATES) + state) * N_FRAME + frame +def draw_icon(state: str, frame: int, with_shell: bool): + from PIL import Image, ImageDraw + + im = Image.new("RGBA", (SIZE, SIZE), TRANS) + d = ImageDraw.Draw(im) + phase = frame * (math.pi * 2 / N_FRAME) + t = frame / max(N_FRAME - 1, 1) + + cx, cy = (40, 38) if with_shell else (32, 30) + breath = math.sin(phase) * 1.2 + rot = 0.0 + if state == "question": + rot = 6 * math.sin(phase * 0.5) + elif state == "coding": + rot = 1.4 * math.sin(phase * 2) + elif state == "error": + rot = 3 * math.sin(phase * 3) if 0.7 < t < 0.95 else 0 + elif state == "success": + breath -= abs(math.sin(phase)) * 2.0 + elif state == "thinking": + rot = -2 + + if with_shell: + d.rounded_rectangle([4, 8, 52, 44], radius=3, fill=SHELL) + d.line([(12, 18), (16, 22), (12, 26)], fill=PROMPT, width=2) + d.line([(22, 27), (30, 27)], fill=PROMPT, width=2) + + ly = cy + 14 + breath * 0.3 + d.line([(cx - 9, ly), (cx - 9, ly + 6)], fill=LEG, width=3) + d.line([(cx + 9, ly), (cx + 9, ly + 6)], fill=LEG, width=3) + + bw, bh = 22, 15 + bx0 = cx - bw // 2 + by0 = int(cy - bh // 2 + breath) + shift = int(rot * 0.3) + d.rounded_rectangle( + [bx0 + shift, by0, bx0 + bw + shift, by0 + bh], radius=3, fill=BODY + ) + + blink = state in ("idle", "question") and 3 <= frame <= 4 + ey = by0 + (7 if not blink else 9) + eh = 1 if blink else 4 + bx0s = bx0 + shift + + if state == "coding": + d.line([(bx0s + 4, ey), (bx0s + 7, ey + 2), (bx0s + 4, ey + 4)], fill=EYE, width=2) + d.line([(bx0s + 12, ey + 3), (bx0s + 17, ey + 3)], fill=EYE, width=2) + elif state == "success": + d.arc([bx0s + 4, ey, bx0s + 9, ey + 5], 200, 340, fill=EYE, width=2) + d.arc([bx0s + 12, ey, bx0s + 17, ey + 5], 200, 340, fill=EYE, width=2) + elif state == "error": + d.line([(bx0s + 4, ey), (bx0s + 8, ey + 2)], fill=EYE, width=2) + d.line([(bx0s + 14, ey + 2), (bx0s + 18, ey)], fill=EYE, width=2) + elif state == "thinking": + look = 1 if frame % 4 >= 2 else 0 + d.line([(bx0s + 6 + look, ey), (bx0s + 6 + look, ey + eh)], fill=EYE, width=2) + d.line([(bx0s + 14 + look, ey), (bx0s + 14 + look, ey + eh)], fill=EYE, width=2) + else: + d.line([(bx0s + 6, ey), (bx0s + 6, ey + eh)], fill=EYE, width=2) + d.line([(bx0s + 14, ey), (bx0s + 14, ey + eh)], fill=EYE, width=2) + + if state == "question": + d.arc([bx0s + bw - 2, by0 - 10, bx0s + bw + 8, by0], 200, 40, fill=EYE, width=2) + d.point((bx0s + bw + 3, by0 + 2), fill=EYE) + elif state == "thinking": + d.line([(bx0s + bw - 2, by0 + bh - 2), (bx0s + bw - 2, by0 + bh + 5)], fill=MARK, width=3) + d.line([(bx0s + bw - 2, by0 + bh + 5), (bx0s + bw - 8, by0 + bh + 5)], fill=MARK, width=3) + elif state == "error": + d.line([(bx0s + bw + 2, by0 - 6), (bx0s + bw + 2, by0)], fill=EYE, width=2) + d.point((bx0s + bw + 2, by0 + 4), fill=EYE) + + return im + + def write_map_inc() -> None: OUT_INC.parent.mkdir(parents=True, exist_ok=True) lines = [ @@ -40,162 +123,109 @@ def write_map_inc() -> None: f"#define HERMIT_FONT_PUA_BASE_GEN 0x{PUA_BASE:X}", f"#define HERMIT_FONT_FRAMES_GEN {N_FRAME}", f"#define HERMIT_FONT_STATES_GEN {len(STATES)}", - "/* state names: " + ", ".join(STATES) + " */", - "", + "static const char *const HERMIT_FONT_STATE_NAMES[] = {", ] + for s in STATES: + lines.append(f' "{s}",') + lines.append("};") OUT_INC.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"wrote {OUT_INC}") -def render_svg(svg: Path, png: Path, size: int) -> bool: - png.parent.mkdir(parents=True, exist_ok=True) +def build_sbix_font(images: dict[int, object]) -> bool: try: - if subprocess.call( - ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(png), str(svg)], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) == 0: - return png.is_file() - except FileNotFoundError: - pass - try: - import cairosvg # type: ignore - - cairosvg.svg2png(url=str(svg), write_to=str(png), output_width=size, output_height=size) - return png.is_file() - except Exception: - return False - - -def build_ttf(png_by_cp: dict[int, Path]) -> bool: - try: - from fontTools.fontBuilder import FontBuilder # type: ignore - from fontTools.ttLib import TTFont # type: ignore - from fontTools.ttLib.tables.E_B_D_T_ import table_E_B_D_T_ # noqa: F401 - from fontTools.ttLib.tables.C_B_D_T_ import table_C_B_D_T_ # noqa: F401 - from fontTools.ttLib.tables.C_B_L_C_ import table_C_B_L_C_ # noqa: F401 - from fontTools.ttLib.tables._g_l_y_f import Glyph # type: ignore - from fontTools.pens.ttGlyphPen import TTGlyphPen # type: ignore + from fontTools.fontBuilder import FontBuilder + from fontTools.pens.ttGlyphPen import TTGlyphPen + from fontTools.ttLib import newTable + from fontTools.ttLib.tables.sbixGlyph import Glyph as SbixGlyph + from fontTools.ttLib.tables.sbixStrike import Strike except Exception as e: - print(f"fonttools CBDT path unavailable: {e}", file=sys.stderr) + print(f"fontTools missing: {e}", file=sys.stderr) return False - # Minimal TTF with empty glyf + Unicode cmap to PUA; color bitmaps via nanoemoji-less stub: - # Prefer shipping PNGs + cmap so terminals that map PUA→font face still get something. - try: - from PIL import Image # type: ignore - except Exception as e: - print(f"pillow required for font pack: {e}", file=sys.stderr) - return False + cps = sorted(images) + glyph_order = [".notdef"] + [f"u{cp:04X}" for cp in cps] + cmap = {cp: f"u{cp:04X}" for cp in cps} - # Simplified: write a placeholder TTF that only has cmap entries; bitmaps - # need full CBDT authoring. For now embed as sbix-like custom is heavy — - # create a valid empty-outline font with correct cmap so install works, and - # document that full color CBDT needs `nanoemoji` when available. - glyph_order = [".notdef"] - cmap = {} - for cp, png in sorted(png_by_cp.items()): - name = f"u{cp:04X}" - glyph_order.append(name) - cmap[cp] = name - - fb = FontBuilder(1024, isTTF=True) + fb = FontBuilder(SIZE, isTTF=True) fb.setupGlyphOrder(glyph_order) fb.setupCharacterMap(cmap) pen = TTGlyphPen(None) empty = pen.glyph() - glyphs = {".notdef": empty} - for name in glyph_order[1:]: - glyphs[name] = empty - fb.setupGlyf(glyphs) - fb.setupHorizontalMetrics({n: (1024, 0) for n in glyph_order}) - fb.setupHorizontalHeader(ascent=1024, descent=0) + fb.setupGlyf({n: empty for n in glyph_order}) + fb.setupHorizontalMetrics({n: (SIZE, 0) for n in glyph_order}) + fb.setupHorizontalHeader(ascent=SIZE, descent=0) fb.setupNameTable( { "familyName": "Hermit Icons", "styleName": "Regular", - "uniqueFontIdentifier": "Hermit Icons", + "uniqueFontIdentifier": "Hermit Icons 1.0", "fullName": "Hermit Icons", "psName": "HermitIcons", - "version": "0.10", + "version": "Version 1.000", } ) - fb.setupOS2() + fb.setupOS2(sTypoAscender=SIZE, sTypoDescender=0, usWinAscent=SIZE, usWinDescent=0) fb.setupPost() + font = fb.font + + import io + + strike = Strike(ppem=SIZE, resolution=72) + strike.glyphs = {} + for name in glyph_order: + if name == ".notdef": + continue + cp = int(name[1:], 16) + im = images[cp] + bio = io.BytesIO() + im.save(bio, format="PNG") + g = SbixGlyph() + g.glyphName = name + g.originOffsetX = 0 + g.originOffsetY = 0 + g.graphicType = "png " + g.imageData = bio.getvalue() + strike.glyphs[name] = g + + sbix = newTable("sbix") + sbix.version = 1 + sbix.flags = 1 + sbix.strikes = {SIZE: strike} + font["sbix"] = sbix + OUT_TTF.parent.mkdir(parents=True, exist_ok=True) - fb.save(str(OUT_TTF)) - print(f"wrote {OUT_TTF} (cmap + empty outlines; color CBDT via nanoemoji optional)") - # Touch PNGs kept beside for tooling - dump = ROOT / "res" / "hermit-icon-frames" - dump.mkdir(parents=True, exist_ok=True) - for cp, png in png_by_cp.items(): - target = dump / f"u{cp:04X}.png" - target.write_bytes(png.read_bytes()) - print(f"wrote {len(png_by_cp)} frames under {dump}") + font.save(str(OUT_TTF)) + print(f"wrote {OUT_TTF} (sbix, {len(images)} glyphs)") return True -def try_nanoemoji(svgs: list[Path]) -> bool: - """If nanoemoji is installed, build a proper color font from SVGs.""" - if not svgs: - return False - try: - with tempfile.TemporaryDirectory() as td: - td = Path(td) - for i, s in enumerate(svgs): - (td / f"{i:03d}.svg").write_bytes(s.read_bytes()) - out = td / "HermitIcons.ttf" - rc = subprocess.call( - ["nanoemoji", f"--output_file={out}", *sorted(str(p) for p in td.glob("*.svg"))], - cwd=str(td), - ) - if rc == 0 and out.is_file(): - OUT_TTF.parent.mkdir(parents=True, exist_ok=True) - OUT_TTF.write_bytes(out.read_bytes()) - print(f"wrote {OUT_TTF} via nanoemoji") - return True - except FileNotFoundError: - pass - except Exception as e: - print(f"nanoemoji failed: {e}", file=sys.stderr) - return False - - def main() -> int: - write_map_inc() - if not ICONS.is_dir(): - print(f"missing {ICONS}", file=sys.stderr) - return 1 - - svg_list: list[Path] = [] - png_by_cp: dict[int, Path] = {} - with tempfile.TemporaryDirectory() as td: - td = Path(td) - for shell, suffix in ((0, ""), (1, "-shell")): - for si, st in enumerate(STATES): - svg = ICONS / f"{st}{suffix}.svg" - if not svg.is_file(): - print(f"skip missing {svg.name}", file=sys.stderr) - continue - svg_list.append(svg) - # One master frame per state for now; duplicate across N_FRAME. - png = td / f"{shell}_{st}.png" - if not render_svg(svg, png, 64): - print(f"render failed: {svg.name}", file=sys.stderr) - continue - for fr in range(N_FRAME): - png_by_cp[codepoint(shell, si, fr)] = png - - if try_nanoemoji(svg_list): - return 0 - if png_by_cp and build_ttf(png_by_cp): - return 0 + try: + from PIL import Image # noqa: F401 + except ImportError: print( - "font binary not built (install rsvg-convert/cairosvg + pillow + fonttools " - "or nanoemoji). Map .inc is ready for PUA emission.", + "need Pillow: nix-shell -p python3Packages.pillow python3Packages.fonttools", file=sys.stderr, ) - return 0 # map is enough for compile + return 1 + + write_map_inc() + images: dict[int, object] = {} + for shell in (0, 1): + for si, st in enumerate(STATES): + d = OUT_FRAMES / ("shell" if shell else "bare") / st + d.mkdir(parents=True, exist_ok=True) + for fr in range(N_FRAME): + im = draw_icon(st, fr, with_shell=bool(shell)) + im.save(d / f"f{fr:02d}.png") + images[codepoint(shell, si, fr)] = im + print(f"wrote {len(images)} PNGs under {OUT_FRAMES}") + + if not build_sbix_font(images): + print("TTF not written — PNGs ready; install fonttools for the font", file=sys.stderr) + return 1 + return 0 if __name__ == "__main__": diff --git a/scripts/package_linux.sh b/scripts/package_linux.sh index 71975bc..947faab 100755 --- a/scripts/package_linux.sh +++ b/scripts/package_linux.sh @@ -59,6 +59,20 @@ ln -sfr "$rel/hermit" "$rel/cst-agent" 2>/dev/null || ln -sf hermit "$rel/cst-ag "${T}strip" "$rel/hermit" cp -f README.md "$rel/README.md" echo "$ver" >"$rel/VERSION" +# Ship icon font + animation frames with the release tarball. +if [[ ! -f res/hermit-icons.ttf ]]; then + python3 scripts/build_hermit_font.py >/dev/null 2>&1 || true +fi +if [[ -f res/hermit-icons.ttf ]]; then + mkdir -p "$rel/share/hermit" "$rel/share/fonts/hermit" + cp -f res/hermit-icons.ttf "$rel/hermit-icons.ttf" + cp -f res/hermit-icons.ttf "$rel/share/hermit/hermit-icons.ttf" + cp -f res/hermit-icons.ttf "$rel/share/fonts/hermit/hermit-icons.ttf" +fi +if [[ -d res/hermit-frames ]]; then + mkdir -p "$rel/share/hermit/frames" + cp -a res/hermit-frames/. "$rel/share/hermit/frames/" +fi tgz="$(cst_pack_tgz dist "$name")" # Compat alias package name for one release cycle cp -f "$tgz" "dist/cst-agent-$ver-linux-$ARCH.tar.gz" diff --git a/src/hermit_glyph.c b/src/hermit_glyph.c index 9c7df97..14274ad 100644 --- a/src/hermit_glyph.c +++ b/src/hermit_glyph.c @@ -1,5 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include "cst_hermit_glyph.h" +#include "os_compat.h" #include #include @@ -18,6 +19,40 @@ const char *hermit_getenv(const char *suffix) { return e ? e : ""; } +static int file_ok(const char *path) { + os_stat_t st; + return path && path[0] && os_lstat(path, &st) == 0 && st.is_reg && + st.size > 0; +} + +/* Prefer HERMIT_FONT_PATH, then common install locations. */ +const char *hermit_font_path(void) { + static char buf[512]; + const char *e = hermit_getenv("FONT_PATH"); + if (file_ok(e)) + return e; + const char *home = getenv("HOME"); + if (home && home[0]) { + snprintf(buf, sizeof(buf), "%s/.local/share/fonts/hermit-icons.ttf", home); + if (file_ok(buf)) + return buf; + snprintf(buf, sizeof(buf), "%s/.local/share/hermit/hermit-icons.ttf", home); + if (file_ok(buf)) + return buf; + } + static const char *const cands[] = { + "/usr/local/share/fonts/hermit/hermit-icons.ttf", + "/usr/local/share/hermit/hermit-icons.ttf", + "/usr/share/fonts/hermit/hermit-icons.ttf", + "/usr/share/hermit/hermit-icons.ttf", + "res/hermit-icons.ttf", + NULL}; + for (int i = 0; cands[i]; i++) + if (file_ok(cands[i])) + return cands[i]; + return ""; +} + hermit_icon_mode_t hermit_icon_mode(void) { const char *e = hermit_getenv("ICONS"); if (!e[0] || !strcmp(e, "auto")) @@ -33,9 +68,10 @@ hermit_icon_mode_t hermit_icon_mode_eff(int ascii) { hermit_icon_mode_t m = hermit_icon_mode(); if (m != HERMIT_ICON_AUTO) return m; - /* AUTO: font only when forced path exists or HERMIT_ICON_FONT=1. */ if (hermit_getenv("ICON_FONT")[0] == '1') return HERMIT_ICON_FONT; + /* AUTO stays on the mini art track; font needs a terminal that loads + * Hermit Icons (installed by make install / bootstrap). */ return HERMIT_ICON_MINI; } @@ -101,67 +137,135 @@ const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii) { } } -/* --- home shell art (fixed grid) ----------------------------------------- */ -/* 6 rows x 14 cols. Roles: 0 body 1 shell 2 eye 3 leg 4 prompt 5 empty/mark */ +/* --- home shell art (fixed grid, state+frame animated like HTML kit) ----- */ +/* 6 rows x 12 cols. Roles: 0 body 1 shell 2 eye 3 leg 4 prompt 5 empty/mark */ #define ART_H 6 -#define ART_W 14 +#define ART_W 12 -static const char *const ART_SHELL[ART_H] = { - " ######## ", /* shell top */ - " ## >_ ## ", /* shell + prompt */ - " ########## ", /* shell bottom */ - " ###### ", /* body */ - " #::## ", /* eyes :: on body */ - " | | ", /* legs */ -}; +/* Base grids use: + * @ shell # body : open eye = blink >_ prompt | leg ? mark ~ claw + * Eyes/prompt mutate per state+frame below. */ -static const char *const ART_NOSHELL[ART_H] = { - " ", - " ", - " ###### ", - " #::## ", - " ###### ", - " | | ", -}; +static void fill_art(hermit_state_t st, int with_shell, int frame, int ascii, + char out[ART_H][ART_W + 1]) { + static const char *const shell_base[ART_H] = { + " @@@@@@@@ ", + " @@ >_ @@ ", + " @@@@@@@@ ", + " #### ", + " #::# ", + " | | ", + }; + static const char *const bare_base[ART_H] = { + " ", + " ", + " #### ", + " #::# ", + " #### ", + " | | ", + }; + const char *const *src = with_shell ? shell_base : bare_base; + int f = frame < 0 ? 0 : frame % HERMIT_FONT_FRAMES; + for (int r = 0; r < ART_H; r++) { + memcpy(out[r], src[r], ART_W); + out[r][ART_W] = 0; + } -static const char *const ART_SHELL_ASC[ART_H] = { - " ######## ", - " ## >_ ## ", - " ########## ", - " ###### ", - " #::## ", - " | | ", -}; + /* Eyes on body row (row 4 with shell, row 3 bare). */ + int er = with_shell ? 4 : 3; + int blink = (st == HERMIT_ST_IDLE || st == HERMIT_ST_QUESTION) && + (f == 3 || f == 4); + if (st == HERMIT_ST_CODING) { + out[er][4] = '>'; + out[er][5] = '_'; + out[er][6] = ' '; + out[er][7] = '#'; + } else if (st == HERMIT_ST_SUCCESS) { + out[er][5] = ascii ? '^' : '^'; + out[er][6] = ascii ? '^' : '^'; + } else if (st == HERMIT_ST_ERROR) { + out[er][5] = 'x'; + out[er][6] = 'x'; + } else if (st == HERMIT_ST_THINKING) { + int look = (f % 4) >= 2; + out[er][5] = look ? ' ' : ':'; + out[er][6] = look ? ':' : ' '; + } else if (blink) { + out[er][5] = '='; + out[er][6] = '='; + } + + /* Accents */ + if (st == HERMIT_ST_QUESTION && with_shell) { + out[1][10] = '?'; + } else if (st == HERMIT_ST_THINKING) { + out[er][9] = '~'; + } else if (st == HERMIT_ST_ERROR && with_shell) { + out[2][10] = '!'; + } + + /* Soft breath: nudge body one cell on even frames for idle/coding. */ + if ((st == HERMIT_ST_IDLE || st == HERMIT_ST_CODING) && (f & 1) && + with_shell) { + /* already subtle via blink; keep grid stable */ + } + + if (ascii) { + for (int r = 0; r < ART_H; r++) + for (int c = 0; c < ART_W; c++) { + char ch = out[r][c]; + if (ch == '@') + out[r][c] = '#'; + else if (ch == ':' || ch == '=') + out[r][c] = (ch == '=') ? '-' : ':'; + } + } +} int hermit_shell_art(hermit_state_t st, int with_shell, int ascii, const char **lines, int max_lines) { - (void)st; + return hermit_shell_art_frame(st, with_shell, ascii, 0, lines, max_lines); +} + +int hermit_shell_art_frame(hermit_state_t st, int with_shell, int ascii, + int frame, const char **lines, int max_lines) { + static char buf[ART_H][ART_W + 1]; + static char *ptrs[ART_H]; if (!lines || max_lines < 1) return 0; - const char *const *src = - with_shell ? (ascii ? ART_SHELL_ASC : ART_SHELL) : ART_NOSHELL; + fill_art(st, with_shell, frame, ascii, buf); int n = ART_H < max_lines ? ART_H : max_lines; - for (int i = 0; i < n; i++) - lines[i] = src[i]; + for (int i = 0; i < n; i++) { + ptrs[i] = buf[i]; + lines[i] = ptrs[i]; + } return n; } -int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col) { - (void)st; +int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, + int col) { + static char grid[ART_H][ART_W + 1]; + fill_art(st, with_shell, 0, 0, grid); if (row < 0 || row >= ART_H || col < 0 || col >= ART_W) return 5; - const char *const *src = with_shell ? ART_SHELL : ART_NOSHELL; - char c = src[row][col]; + char c = grid[row][col]; if (c == ' ' || !c) return 5; if (c == '>' || c == '_') return 4; - if (c == ':') + if (c == ':' || c == '=' || c == '^' || c == 'x') return 2; - if (c == '|') + if (c == '|' ) return 3; + if (c == '?' || c == '!' || c == '~') + return 5; /* mark */ + if (c == '@') + return 1; if (with_shell && row <= 2) - return 1; /* shell */ - return 0; /* body */ + return 1; + return 0; } + +int hermit_shell_art_width(void) { return ART_W; } +int hermit_shell_art_height(void) { return ART_H; } diff --git a/src/tui.c b/src/tui.c index 32afa40..7331c7b 100644 --- a/src/tui.c +++ b/src/tui.c @@ -2721,6 +2721,8 @@ static int hermit_role_pair(int role) { return COLORS >= 256 ? CP_MUTED : CP_ERROR; /* brick legs */ case 4: return CP_SGR_CYAN; /* >_ */ + case 5: + return CP_ASSIST; /* ? ! claw mark */ default: return CP_MUTED; } @@ -2743,15 +2745,31 @@ static void draw_hermit_icon(WINDOW *win, int y0, int x0, hermit_state_t st, } } const char *lines[8]; - int n = hermit_shell_art(st, with_shell, g_ascii, lines, 8); + int n = hermit_shell_art_frame(st, with_shell, g_ascii, frame, lines, 8); for (int r = 0; r < n; r++) { const char *row = lines[r]; for (int c = 0; row[c]; c++) { - if (row[c] == ' ') + char raw = row[c]; + if (raw == ' ') continue; - char ch[2] = {row[c], 0}; - int pair = hermit_role_pair(hermit_shell_cell_role(st, with_shell, r, c)); - draw_ansi_at(win, y0 + r, x0 + c, 1, ch, pair, 0, 0); + char draw = (raw == '@') ? '#' : raw; + int role = 0; + if (raw == '@') + role = 1; + else if (raw == '#' ) + role = (with_shell && r <= 2) ? 1 : 0; + else if (raw == '>' || raw == '_') + role = 4; + else if (raw == ':' || raw == '=' || raw == '^' || raw == 'x') + role = 2; + else if (raw == '|') + role = 3; + else if (raw == '?' || raw == '!' || raw == '~') + role = 5; + else + role = hermit_shell_cell_role(st, with_shell, r, c); + char ch[2] = {draw, 0}; + draw_ansi_at(win, y0 + r, x0 + c, 1, ch, hermit_role_pair(role), 0, 0); } } } @@ -2804,10 +2822,12 @@ static void draw_home(tui_t *t) { const char *lb = hermit_getenv("LOGO"); want_block = lb[0] && !strcmp(lb, "block"); } - /* Crab on the right of the brand stack when there is room. */ - int crab_w = 14, crab_h = 6; + hermit_state_t crab_st = hermit_state_from_ui( + t->busy, t->ask_pending || t->awaiting_perm, 0); + int crab_w = hermit_shell_art_width(); + int crab_h = hermit_shell_art_height(); int crab_x = w - crab_w - 2; - int show_crab = crab_x > LOGO_W + 4 && box_top >= crab_h + 1; + int show_crab = crab_x > 20 && box_top >= crab_h + 1 && w >= 48; if (want_block && !(g_ascii || !g_utf8 || g_logo_text) && box_top - LOGO_H - 2 >= 0 && w >= LOGO_W) { int x0 = show_crab ? (crab_x - LOGO_W) / 2 : (w - LOGO_W) / 2; @@ -2822,7 +2842,7 @@ static void draw_home(tui_t *t) { } else { const char *mark = "HERMIT"; int mw = (int)strlen(mark); - int x = show_crab ? (crab_x - mw) / 2 : (w - mw) / 2; + int x = (w - mw) / 2; if (x < 0) x = 0; if (brand_y >= 1) @@ -2830,32 +2850,42 @@ static void draw_home(tui_t *t) { draw_ansi_at(t->logw, brand_y, x, mw, mark, CP_ACCENT, 1, 0); } if (show_crab) { - int cy = box_top - crab_h; + int cy = brand_y; + if (cy + crab_h > box_top) + cy = box_top - crab_h; if (cy < 0) cy = 0; - draw_hermit_icon(t->logw, cy, crab_x, HERMIT_ST_IDLE, 1, t->spin); + draw_hermit_icon(t->logw, cy, crab_x, crab_st, 1, t->spin); } int ty = brand_y + 1; if (ty < box_top) { const char *tag = g_ascii ? "code . plan . parliament" : "code · plan · parliament"; int tw = (int)strlen(tag); - int x = show_crab ? (crab_x - tw) / 2 : (w - tw) / 2; + int x = (w - tw) / 2; if (x < 0) x = 0; draw_ansi_at(t->logw, ty, x, tw, tag, CP_MUTED, 0, 0); } + /* Compact parliament chip — never dump full status_text on the home. */ if (cst_parliament_is_active() && ty + 1 < box_top) { - char *st = cst_parliament_status_text(); - if (st && st[0]) { - char chip[160]; - snprintf(chip, sizeof(chip), "%s %s", g_ascii ? "*" : "◆", st); - int cw = vis_cols(chip); - if (cw > w - 2) - cw = w - 2; - draw_ansi_at(t->logw, ty + 1, (w - cw) / 2, cw, chip, CP_ACCENT, 0, 0); - } - free(st); + cst_parliament_meta_t m; + memset(&m, 0, sizeof(m)); + cst_parliament_load(&m); + char sid[64] = {0}; + int sess = cst_parliament_session_ok(sid, sizeof(sid)); + int chair = sess && cst_parliament_is_chair(); + char chip[96]; + snprintf(chip, sizeof(chip), "%s parliament · rev=%lu · %s", + g_ascii ? "*" : "◆", (unsigned long)m.revision, + !sess ? "login" : chair ? "chair" : "member"); + int cw = vis_cols(chip); + if (cw > w - 4) + cw = w - 4; + int cx = (w - cw) / 2; + if (cx < 0) + cx = 0; + draw_ansi_at(t->logw, ty + 1, cx, cw, chip, CP_ACCENT, 0, 0); } int right = t->in_x - t->log_x + t->in_w; if (box_bot < h) {