Wire hermit_getenv across the engine, rebrand WebUI/systemd/docs, and add apps/desktop (Tauri 2 + React) with a DeepSeek-Harness-like UI. Qt app/ is frozen; full libcstcore FFI is scaffolded behind --features engine. Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
7.3 KiB
C
262 lines
7.3 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include "cst_hermit_glyph.h"
|
|
#include "os_compat.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* hermit_getenv lives in os_compat.c so light test binaries that link
|
|
* os_compat without hermit_glyph still resolve HERMIT_/CST_ dual env. */
|
|
|
|
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"))
|
|
return HERMIT_ICON_AUTO;
|
|
if (!strcmp(e, "font") || !strcmp(e, "ttf"))
|
|
return HERMIT_ICON_FONT;
|
|
return HERMIT_ICON_MINI;
|
|
}
|
|
|
|
hermit_icon_mode_t hermit_icon_mode_eff(int ascii) {
|
|
if (ascii)
|
|
return HERMIT_ICON_MINI;
|
|
hermit_icon_mode_t m = hermit_icon_mode();
|
|
if (m != HERMIT_ICON_AUTO)
|
|
return m;
|
|
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;
|
|
}
|
|
|
|
hermit_state_t hermit_state_from_ui(int busy, int ask, int error) {
|
|
if (error)
|
|
return HERMIT_ST_ERROR;
|
|
if (ask)
|
|
return HERMIT_ST_QUESTION;
|
|
if (busy)
|
|
return HERMIT_ST_CODING;
|
|
return HERMIT_ST_IDLE;
|
|
}
|
|
|
|
wchar_t hermit_font_codepoint(hermit_state_t st, int frame, int with_shell) {
|
|
if (st < 0 || st >= HERMIT_ST_N)
|
|
st = HERMIT_ST_IDLE;
|
|
if (frame < 0)
|
|
frame = 0;
|
|
frame %= HERMIT_FONT_FRAMES;
|
|
int shell = with_shell ? 1 : 0;
|
|
return (wchar_t)(HERMIT_FONT_PUA_BASE +
|
|
(shell * HERMIT_ST_N + (int)st) * HERMIT_FONT_FRAMES +
|
|
frame);
|
|
}
|
|
|
|
/* --- minimal badge ------------------------------------------------------- */
|
|
|
|
const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii) {
|
|
int f = frame < 0 ? 0 : frame;
|
|
if (ascii) {
|
|
switch (st) {
|
|
case HERMIT_ST_QUESTION:
|
|
return "||?";
|
|
case HERMIT_ST_ERROR:
|
|
return "\\/!";
|
|
case HERMIT_ST_SUCCESS:
|
|
return "||*";
|
|
case HERMIT_ST_THINKING:
|
|
return (f % 2) ? "~|" : "|~";
|
|
case HERMIT_ST_CODING:
|
|
case HERMIT_ST_IDLE:
|
|
default: {
|
|
static const char *const spin[] = {"||", ">_", "||", "-"};
|
|
return spin[f % 4];
|
|
}
|
|
}
|
|
}
|
|
switch (st) {
|
|
case HERMIT_ST_QUESTION:
|
|
return "║║?";
|
|
case HERMIT_ST_ERROR:
|
|
return "╲╱!";
|
|
case HERMIT_ST_SUCCESS:
|
|
return "║║✦";
|
|
case HERMIT_ST_THINKING:
|
|
return (f % 2) ? "║~" : "~║";
|
|
case HERMIT_ST_CODING:
|
|
case HERMIT_ST_IDLE:
|
|
default: {
|
|
static const char *const spin[] = {"║║", ">_", "║║", "─"};
|
|
return spin[f % 4];
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- 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 12
|
|
|
|
/* Base grids use:
|
|
* @ shell # body : open eye = blink >_ prompt | leg ? mark ~ claw
|
|
* Eyes/prompt mutate per state+frame below. */
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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) {
|
|
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;
|
|
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++) {
|
|
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) {
|
|
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;
|
|
char c = grid[row][col];
|
|
if (c == ' ' || !c)
|
|
return 5;
|
|
if (c == '>' || c == '_')
|
|
return 4;
|
|
if (c == ':' || c == '=' || c == '^' || c == 'x')
|
|
return 2;
|
|
if (c == '|' )
|
|
return 3;
|
|
if (c == '?' || c == '!' || c == '~')
|
|
return 5; /* mark */
|
|
if (c == '@')
|
|
return 1;
|
|
if (with_shell && row <= 2)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
int hermit_shell_art_width(void) { return ART_W; }
|
|
int hermit_shell_art_height(void) { return ART_H; }
|