tui.c (14912B)
1 /* tui.c - interfaz de terminal (ncurses, sobre comun/linux/tuikit). 2 * 3 * El tablero son celdas de texto con el fondo del tema y las piezas como glifos 4 * Unicode rellenos, coloreados por bando (como el ajedrez en bash). El tamano 5 * de las casillas se adapta a la terminal. */ 6 #include "tuikit.h" 7 #include <stdlib.h> 8 #include <string.h> 9 #include "frontend.h" 10 11 /* ♙ para los peones de los dos bandos: ♟ (U+265F) muchas terminales lo muestran 12 * como emoji y no respetan el color (el ajedrez en bash tenia el mismo problema) */ 13 static const char *const GLYPH[7] = { " ", "♙", "♞", "♝", "♜", "♛", "♚" }; 14 15 #define FG_WHITE 231 16 #define FG_BLACK 16 17 18 static void cat(char *b, int n, const char *s) { size_t l = strlen(b); snprintf(b + l, (size_t)n - l, "%s", s); } 19 20 /* casilla (fila/columna de pantalla) de una casilla del tablero */ 21 static void cell_of(const View *v, int sq, int *col, int *row) 22 { 23 int f = FILE_OF(sq), r = RANK_OF(sq); 24 if (v->c && v->c->flipped) { f = 7 - f; r = 7 - r; } 25 *col = f; 26 *row = 7 - r; 27 } 28 29 static void draw_board(Frontend *fe, const View *v, int y0, int x0, int sw, int sh) 30 { 31 const Theme *th = &THEMES[v->theme % NTHEMES]; 32 const Game *g = v->g; 33 const Ctrl *c = v->c; 34 int lf = -1, lt = -1; 35 if (g->nply) { lf = MFROM(g->mv[g->nply - 1]); lt = MTO(g->mv[g->nply - 1]); } 36 int check_sq = -1; 37 if ((g->result == R_NONE && pos_in_check(&g->pos)) || g->reason == RS_MATE) check_sq = g->pos.king[g->pos.side]; 38 for (int sq = 0; sq < 64; sq++) { 39 int col, row; 40 cell_of(v, sq, &col, &row); 41 int x = x0 + col * sw, y = y0 + row * sh; 42 bool light = (FILE_OF(sq) + RANK_OF(sq)) & 1; 43 short bg = light ? th->xterm[TH_LIGHT] : th->xterm[TH_DARK]; 44 if (sq == lf || sq == lt) bg = th->xterm[TH_PATH]; 45 if (c && sq == c->sel) bg = th->xterm[TH_FROM]; 46 if (sq == check_sq) bg = th->xterm[TH_CAPTURE]; 47 bool target = c && ctrl_is_target(c, sq); 48 bool cursor = c && sq == c->cursor && g->result == R_NONE; 49 for (int j = 0; j < sh; j++) { 50 tk_att(0, bg, 0); 51 tk_fill(y + j, x, sw, " "); 52 } 53 int cy = y + sh / 2, cx = x + sw / 2; 54 uint8_t pc = g->pos.sq[sq]; 55 if (pc) { 56 tk_att(PCOLOR(pc) == WHITE ? FG_WHITE : FG_BLACK, bg, A_BOLD); 57 mvaddstr(cy, cx, GLYPH[PTYPE(pc)]); 58 } 59 if (target) { 60 short tc = pc ? th->xterm[TH_CAPTURE] : th->xterm[TH_TARGET]; 61 if (pc) { 62 /* captura: el borde de la casilla en el color de captura */ 63 tk_att(0, tc, 0); 64 for (int j = 0; j < sh; j++) { mvaddstr(y + j, x, " "); mvaddstr(y + j, x + sw - 1, " "); } 65 } else { 66 tk_att(tc, bg, A_BOLD); 67 mvaddstr(cy, cx, "●"); 68 } 69 } 70 if (cursor) { 71 tk_att(0, tk_xterm(C_CURSOR), A_BOLD); 72 if (sw >= 5) { 73 mvaddstr(y, x, "▛"); mvaddstr(y, x + sw - 1, "▜"); 74 if (sh >= 2) { mvaddstr(y + sh - 1, x, "▙"); mvaddstr(y + sh - 1, x + sw - 1, "▟"); } 75 tk_att(tk_xterm(C_CURSOR), bg, A_BOLD); 76 mvaddstr(y, x, "▛"); mvaddstr(y, x + sw - 1, "▜"); 77 if (sh >= 2) { mvaddstr(y + sh - 1, x, "▙"); mvaddstr(y + sh - 1, x + sw - 1, "▟"); } 78 } else { 79 tk_att(tk_xterm(C_CURSOR), bg, A_BOLD); 80 mvaddstr(cy, x, "["); 81 mvaddstr(cy, x + sw - 1, "]"); 82 } 83 } 84 hit_add(&fe->hits, x, y, sw, sh, HIT_SQUARE, sq); 85 } 86 /* coordenadas */ 87 bool fl = c && c->flipped; 88 tk_att(244, TK_DEF, 0); 89 for (int i = 0; i < 8; i++) { 90 char fc[2] = { (char)('a' + (fl ? 7 - i : i)), 0 }, rc[2] = { (char)('8' - (fl ? 7 - i : i)), 0 }; 91 mvaddstr(y0 + 8 * sh, x0 + i * sw + sw / 2, fc); 92 mvaddstr(y0 + i * sh + sh / 2, x0 - 2, rc); 93 } 94 /* coronacion: las cuatro piezas en una cajita */ 95 if (c && c->mode == CM_PROMO) { 96 int bw = 4 * 4 + 3, bx = x0 + 4 * sw - bw / 2, by = y0 + 4 * sh - 2; 97 tk_box(by, bx, 3, bw, "", C_ACCENT); 98 for (int k = 0; k < 4; k++) { 99 int px = bx + 2 + k * 4; 100 bool on = k == c->promo_cur; 101 tk_att(g->pos.side == WHITE ? FG_WHITE : FG_BLACK, on ? tk_xterm(C_ACCENT) : 238, A_BOLD); 102 mvaddstr(by + 1, px, " "); 103 mvaddstr(by + 1, px + 1, GLYPH[MPROMO(c->promo[k])]); 104 mvaddstr(by + 1, px + 2, " "); 105 hit_add(&fe->hits, px, by + 1, 3, 1, HIT_PROMO, k); 106 } 107 } 108 } 109 110 static void status_text(const View *v, char *buf, int n) 111 { 112 const Game *g = v->g; 113 buf[0] = 0; 114 if (g->result == R_DRAW) { cat(buf, n, tr(S_DRAW)); cat(buf, n, " · "); cat(buf, n, reason_text(g->reason)); } 115 else if (g->result != R_NONE) { 116 cat(buf, n, color_name(g->result == R_WHITE ? WHITE : BLACK)); 117 cat(buf, n, " "); 118 cat(buf, n, tr(S_WINS)); 119 cat(buf, n, " · "); 120 cat(buf, n, reason_text(g->reason)); 121 } else { 122 const char *nm = v->names[g->pos.side] && *v->names[g->pos.side] ? v->names[g->pos.side] : color_name(g->pos.side); 123 if (g_lang) { cat(buf, n, nm); cat(buf, n, " to move"); } 124 else { cat(buf, n, "Juegan "); cat(buf, n, nm); } 125 if (pos_in_check(&g->pos)) { cat(buf, n, " · "); cat(buf, n, tr(S_CHECK)); } 126 } 127 } 128 129 static void player_line(const View *v, int color, int y, int x, int w) 130 { 131 const Game *g = v->g; 132 bool active = g->result == R_NONE && g->pos.side == color; 133 tk_att(active ? 255 : 250, active ? 238 : 236, active ? A_BOLD : 0); 134 tk_fill(y, x, w, " "); 135 tk_att(color == WHITE ? FG_WHITE : FG_BLACK, active ? 238 : 236, A_BOLD); 136 mvaddstr(y, x + 1, "♚"); 137 tk_att(active ? 255 : 250, active ? 238 : 236, active ? A_BOLD : 0); 138 const char *nm = v->names[color] && *v->names[color] ? v->names[color] : color_name(color); 139 int cw = 0; 140 if (v->has_clock) { 141 char clk[16]; 142 fmt_clock(v->clock[color], clk, sizeof clk); 143 cw = tk_ulen(clk) + 1; 144 if (active && v->clock[color] < 20000) tk_att(tk_xterm(C_ERR), 238, A_BOLD); 145 else if (active) tk_att(tk_xterm(C_ACCENT), 238, A_BOLD); 146 mvaddstr(y, x + w - cw, clk); 147 tk_att(active ? 255 : 250, active ? 238 : 236, active ? A_BOLD : 0); 148 } 149 tk_put(y, x + 3, nm, w - 4 - cw); 150 } 151 152 static void captured_line(const View *v, int who, int y, int x, int w) 153 { 154 int cap[2][7]; 155 captured_pieces(v->g, cap); 156 char buf[128] = ""; 157 for (int t = QUEEN; t >= PAWN; t--) 158 for (int k = 0; k < cap[who][t]; k++) cat(buf, sizeof buf, GLYPH[t]); 159 tk_att(who == WHITE ? FG_WHITE : 245, TK_DEF, 0); 160 tk_put(y, x + 1, buf, w - 2); 161 } 162 163 static void draw_panel(const View *v, int y, int x, int w, int h) 164 { 165 const Game *g = v->g; 166 bool fl = v->c && v->c->flipped; 167 int top = fl ? WHITE : BLACK, bot = top ^ 1; 168 int ybot = y + h - 1; 169 player_line(v, top, y, x, w); 170 captured_line(v, bot, y + 1, x, w); 171 char buf[200]; 172 int ly = y + 3; 173 status_text(v, buf, sizeof buf); 174 tk_att(g->result != R_NONE ? tk_xterm(C_ACCENT) : 252, TK_DEF, A_BOLD); 175 tk_put(ly++, x + 1, buf, w - 2); 176 if (v->status && *v->status) { tk_att(245, TK_DEF, 0); tk_put(ly++, x + 1, v->status, w - 2); } 177 if (v->opening) { tk_att(tk_xterm(C_OK), TK_DEF, 0); tk_put(ly++, x + 1, v->opening, w - 2); } 178 if (v->show_eval && v->eval_valid) { 179 char e[24]; 180 int ev = v->eval; 181 if (MATE_IN(ev)) snprintf(e, sizeof e, "%sM%d", ev > 0 ? "+" : "-", (MATE - abs(ev) + 1) / 2); 182 else snprintf(e, sizeof e, "%+.2f", ev / 100.0); 183 int bw = w - 10; 184 int cp = ev > 800 ? 800 : ev < -800 ? -800 : ev; 185 int wx = MATE_IN(ev) ? (ev > 0 ? bw : 0) : bw / 2 + cp * bw / 1600; 186 tk_att(250, TK_DEF, 0); 187 mvaddstr(ly, x + 1, e); 188 for (int i = 0; i < bw; i++) { tk_att(0, i < wx ? 255 : 234, 0); mvaddstr(ly, x + 9 + i, " "); } 189 ly++; 190 } 191 if (v->banner) { 192 ly++; 193 tk_att(0, tk_xterm(C_ACCENT), A_BOLD); 194 tk_fill(ly, x, w, " "); 195 tk_put(ly++, x + 1, v->banner, w - 2); 196 } 197 ly++; 198 tk_section(ly++, x, w, tr(S_MOVES)); 199 /* jugadas: las ultimas que entran */ 200 int rows = ybot - 2 - ly; 201 if (rows < 1) rows = 1; 202 int first_side = g->start.side; 203 int total = (g->nply + first_side + 1) / 2; 204 int first_row = total > rows ? total - rows : 0; 205 Pos p = g->start; 206 int ply = 0, target = first_row * 2 - first_side; 207 if (target < 0) target = 0; 208 for (; ply < target; ply++) { Undo u; pos_make(&p, g->mv[ply], &u); } 209 for (int r = first_row; r < total && ply < g->nply; r++) { 210 char num[12]; 211 snprintf(num, sizeof num, "%d.", g->start.fullmove + r); 212 tk_att(244, TK_DEF, 0); 213 mvaddstr(ly, x + 1, num); 214 for (int col = 0; col < 2 && ply < g->nply; col++) { 215 if (r == 0 && col == 0 && first_side == BLACK) { mvaddstr(ly, x + 6, "..."); continue; } 216 char san[16]; 217 move_san(&p, g->mv[ply], san, sizeof san); 218 bool last = ply == g->nply - 1; 219 tk_att(last ? tk_xterm(C_ACCENT) : 252, TK_DEF, last ? A_BOLD : 0); 220 tk_put(ly, x + 6 + col * 10, san, 9); 221 Undo u; 222 pos_make(&p, g->mv[ply], &u); 223 ply++; 224 } 225 ly++; 226 } 227 captured_line(v, top, ybot - 1, x, w); 228 player_line(v, bot, ybot, x, w); 229 } 230 231 static void draw_help(int y, int x, int w) 232 { 233 const char *t = tr(S_HELP_KEYS); 234 char lines[24][96]; 235 int n = 0; 236 while (*t && n < 24) { 237 int k = 0; 238 while (*t && !(t[0] == ' ' && t[1] == '\xc2')) { if (k < 95) lines[n][k++] = *t; t++; } 239 lines[n++][k] = 0; 240 if (*t) t += 4; 241 while (*t == ' ') t++; 242 } 243 tk_box(y, x, n + 2, w, g_lang ? "Keys" : "Teclas", C_ACCENT); 244 tk_att(252, 236, 0); 245 for (int i = 0; i < n; i++) tk_put(y + 1 + i, x + 2, lines[i], w - 4); 246 } 247 248 static void tui_draw_game(Frontend *f, const View *v) 249 { 250 f->hits.n = 0; 251 tk_clear(); 252 int H = LINES, W = COLS; 253 if (W < 60 || H < 20) { 254 tk_att(tk_xterm(C_ERR), TK_DEF, A_BOLD); 255 mvaddstr(0, 0, g_lang ? "Make the terminal bigger (60x20 min)" : "Agrandá la terminal (mínimo 60x20)"); 256 refresh(); 257 return; 258 } 259 /* tamano de casilla: el mas grande que deja lugar al panel */ 260 int sh = (H - 4) / 8; 261 if (sh > 2) sh = 2; /* casillas chicas: la pieza (un solo caracter) tiene que llenar la casilla */ 262 int sw = sh * 2 + 1; 263 while (sh > 1 && 8 * sw + 3 + 28 > W) { sh--; sw = sh * 2 + 1; } 264 if (8 * sw + 3 + 28 > W) sw = 3; 265 int bx = 3, by = 1; 266 tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD); 267 tk_fill(0, 0, W, " "); 268 mvaddstr(0, 1, tr(S_TITLE)); 269 if (v->status && *v->status) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0); tk_put(0, 10, v->status, W - 12); } 270 draw_board(f, v, by, bx, sw, sh); 271 int px = bx + 8 * sw + 2, pw = W - px - 1; 272 if (pw > 48) pw = 48; 273 draw_panel(v, by, px, pw, 8 * sh + 1); 274 /* instruccion y mensajes */ 275 const Ctrl *c = v->hide_prompt ? 0 : v->c; 276 tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD); 277 tk_fill(H - 2, 0, W, " "); 278 char buf[160] = ""; 279 if (c && c->mode == CM_ENTRY) snprintf(buf, sizeof buf, "▶ %s %s█", tr(S_P_ENTRY), c->entry); 280 else if (c && c->g && c->g->result == R_NONE && !ctrl_my_turn(c)) { 281 int side = c->g->pos.side; 282 snprintf(buf, sizeof buf, "▶ %s %s", ctrl_prompt(c), v->names[side] && *v->names[side] ? v->names[side] : color_name(side)); 283 } else if (c) snprintf(buf, sizeof buf, "▶ %s", ctrl_prompt(c)); 284 tk_put(H - 2, 1, buf, W - 2); 285 tk_fill(H - 1, 0, W, " "); 286 if (c && c->msg[0]) { tk_att(tk_xterm(c->msg_err ? C_ERR : C_OK), TK_DEF, A_BOLD); tk_put(H - 1, 1, c->msg, W - 2); } 287 else if (c) { 288 tk_att(244, TK_DEF, 0); 289 tk_put(H - 1, 1, g_lang ? "Enter pick · arrows move · Tab next · : type move · m menu · ? help · q quit" 290 : "Enter elegir · flechas mover · Tab siguiente · : escribir jugada · m menú · ? ayuda · q salir", W - 2); 291 } 292 if (v->show_help) draw_help(by + 1, bx + 1, 8 * sw > 60 ? 60 : 8 * sw); 293 if (v->modal) { 294 int w = 8 * sw - 4 > 56 ? 56 : 8 * sw - 4, h = v->modal2 ? 5 : 4; 295 int x = bx + (8 * sw - w) / 2, y = by + (8 * sh - h) / 2; 296 tk_box(y, x, h, w, "", C_ACCENT); 297 tk_att(tk_xterm(C_ACCENT), 236, A_BOLD); 298 tk_put(y + 1, x + (w - tk_ulen(v->modal)) / 2, v->modal, w - 4); 299 if (v->modal2) { tk_att(250, 236, 0); tk_put(y + 3, x + (w - tk_ulen(v->modal2)) / 2, v->modal2, w - 4); } 300 } 301 refresh(); 302 } 303 304 static void tui_draw_form(Frontend *f, const Form *form) { tk_draw_form(form, &f->hits); } 305 306 /* logo de los menus: una fila de piezas */ 307 static void form_logo(int cols) 308 { 309 static const int ROW[] = { ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK }; 310 int lx = cols / 2 - 12; 311 for (int i = 0; i < 8; i++) { 312 tk_att(i & 1 ? FG_BLACK : FG_WHITE, (i & 1) ? THEMES[0].xterm[TH_LIGHT] : THEMES[0].xterm[TH_DARK], A_BOLD); 313 mvaddstr(1, lx + i * 3, " "); 314 mvaddstr(1, lx + i * 3 + 1, GLYPH[ROW[i]]); 315 mvaddstr(1, lx + i * 3 + 2, " "); 316 } 317 } 318 319 static int tui_wait(Frontend *f, int timeout_ms, const int *fds, int nfds, InEvent *ev) 320 { 321 (void)f; 322 return tk_wait(timeout_ms, fds, nfds, ev); 323 } 324 325 /* Para pegar texto largo se suelta la pantalla un momento y se lee la entrada 326 * comun hasta Ctrl-D, como en el ajedrez en bash. */ 327 static char *tui_paste(Frontend *f) 328 { 329 (void)f; 330 def_prog_mode(); 331 endwin(); 332 printf("\n%s\n\n", g_lang ? "Paste the game (PGN) or a position (FEN), then press Ctrl-D on an empty line:" 333 : "Pegá la partida (PGN) o una posición (FEN) y después Ctrl-D en una línea vacía:"); 334 fflush(stdout); 335 size_t cap = 4096, n = 0; 336 char *buf = malloc(cap), line[1024]; 337 while (buf && fgets(line, sizeof line, stdin)) { 338 size_t l = strlen(line); 339 if (n + l + 1 > cap) { 340 if (cap > (4u << 20)) break; 341 char *nb = realloc(buf, cap * 2); 342 if (!nb) break; 343 buf = nb; 344 cap *= 2; 345 } 346 memcpy(buf + n, line, l); 347 n += l; 348 } 349 if (buf) buf[n] = 0; 350 clearerr(stdin); 351 reset_prog_mode(); 352 flushinp(); 353 refresh(); 354 if (buf && !n) { free(buf); buf = 0; } 355 return buf; 356 } 357 358 static void tui_close(Frontend *f) 359 { 360 (void)f; 361 tk_close(); 362 } 363 364 Frontend *tui_open(void) 365 { 366 static Frontend fe; 367 tk_open(); 368 tk_form_logo = form_logo; 369 memset(&fe, 0, sizeof fe); 370 fe.draw_game = tui_draw_game; 371 fe.draw_form = tui_draw_form; 372 fe.wait = tui_wait; 373 fe.close = tui_close; 374 fe.paste = tui_paste; 375 return &fe; 376 }