#include #include "term.h" #include #include #include // As a rule, everything in term.c is the result of gradual evolutionary // change. It's messy. #define COLORING(fg,bg) (((fg) & 0x0f) | (((bg) & 0x07) << 4)) #define COLOR_FG(color,fg) (((fg) & 0x0f) + ((color) & 0x70)) #define COLOR_BG(color,bg) (((color) & 0x0f) + (((bg) & 0x07) << 4)) #define COLOR_INDEX(color) (1 + ((color)&0x07) + (((color) >> 1) & 0x38)) #define COLOR_ATTR(color) (COLOR_PAIR(COLOR_INDEX(color)) | (((color)&0x08) ? A_BOLD : 0)) static struct { int curses, color; } videomode = { 0, 0 }; static struct { int width, height; } minsize = { 80, 24 }; static void init_coersion(); // 256 color mode stuff static void initialize_prs(); typedef struct { int r, g, b, idx; } intcolor; struct { intcolor fore, back; int count, next; } prs[256]; typedef struct { int ch, pair, shuffle; intcolor fore, back; } pairmode_cell; pairmode_cell *cell_buffer; enum { coerce_16, coerce_256 } colormode; int is_xterm; // static void preparecolor ( ) { // sixteen color mode colors (we use these in 256-color mode, too) static int pairParts[8] = { COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, COLOR_CYAN, COLOR_WHITE }; int fg, bg; for (bg=0; bg<8; bg++) { for (fg=0; fg<8; fg++) { init_pair( COLOR_INDEX(COLORING(fg, bg)), pairParts[fg], pairParts[bg] ); } } if (COLORS >= 256) { colormode = coerce_256; } } static void term_title(const char *title) { if (is_xterm) { printf ("\033]2;%s\007", title); // ESC ]0; title BEL } } static void term_title_pop() { if (is_xterm) { term_title("Terminal"); printf ("\033[22;2t"); } } static void term_title_push() { if (is_xterm) { printf ("\033[23;2t"); } } static void term_set_size(int h, int w) { // works in gnome-terminal, but not xterm; causes trouble for maximized windows if (is_xterm) { // first, try resizing the height, in case only that is supported printf ("\033[%dt", (h > 24 ? h : 24)); // then try resizing both, in case we can printf ("\033[8;%d;%dt", h, w); // then refresh so ncurses knows about it refresh( ); } } static void term_show_scrollbar(int show) { // works in xterm, but not gnome-terminal if (is_xterm) { if (show) { printf ("\033[?30h"); } else { printf ("\033[?30l"); } } } static int curses_init( ) { if (videomode.curses) return 0; // isterm? initscr( ); if (!has_colors( )) { endwin( ); fprintf (stderr, "Your terminal has no color support.\n"); return 1; } start_color( ); clear( ); curs_set( 0 ); refresh( ); leaveok(stdscr, TRUE); preparecolor( ); cbreak( ); noecho( ); nodelay(stdscr, TRUE); meta(stdscr, TRUE); keypad(stdscr, TRUE); mousemask(BUTTON1_PRESSED | BUTTON1_RELEASED | REPORT_MOUSE_POSITION | BUTTON_SHIFT | BUTTON_CTRL, NULL); mouseinterval(0); //do no click processing, thank you videomode.curses = 1; getmaxyx(stdscr, Term.height, Term.width); return 1; } static int term_start() { char *term = getenv("TERM"); is_xterm = (strncmp(term, "xterm", 5) == 0) || (strncmp(term, "gnome", 5) == 0) || (strncmp(term, "st", 2) == 0); term_title_push(); term_show_scrollbar(0); int ok = curses_init(); init_coersion(); return ok; } static void term_end() { term_title_pop(); clear(); refresh(); endwin(); } typedef struct CIE { float X, Y, Z; float x, y, z; } CIE; typedef struct Lab { float L, a, b; } Lab; #define DARK 0.0 #define DIM 0.1 #define MID 0.3 #define HALFBRIGHT 0.5 #define BRIGHT 0.9 fcolor palette[16] = { {DARK, DARK, DARK}, {MID, DARK, DARK}, {DARK, MID, DARK}, {MID, .8 * MID, DIM}, {DARK, DARK, MID}, {MID + DIM, DARK, MID}, {DARK, MID, MID}, {HALFBRIGHT, HALFBRIGHT, HALFBRIGHT}, {MID, MID, MID}, {BRIGHT, DARK, DARK}, {DARK, BRIGHT, DARK}, {BRIGHT, BRIGHT, DARK}, {HALFBRIGHT, MID, BRIGHT}, {BRIGHT, HALFBRIGHT, BRIGHT}, {DARK, BRIGHT, BRIGHT}, {BRIGHT, BRIGHT, BRIGHT} }; CIE ciePalette[16]; Lab labPalette[16]; CIE adamsPalette[16]; static CIE white; static CIE toCIE(fcolor c) { double a = 0.055; // http://en.wikipedia.org/wiki/SRGB_color_space#The_reverse_transformation c.r = c.r <= 0.04045 ? c.r / 12.92 : pow((c.r + a) / (1 + a), 2.4); c.g = c.g <= 0.04045 ? c.g / 12.92 : pow((c.g + a) / (1 + a), 2.4); c.b = c.b <= 0.04045 ? c.b / 12.92 : pow((c.b + a) / (1 + a), 2.4); CIE cie; cie.X = 0.4124 * c.r + 0.3576 * c.g + 0.1805 * c.b; cie.Y = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b; cie.Z = 0.0193 * c.r + 0.1192 * c.g + 0.9505 * c.b; float sum = cie.X + cie.Y + cie.Z; if (sum == 0.0) sum = 1.0; cie.x = cie.X / sum; cie.y = cie.Y / sum; cie.z = 1.0 - cie.x - cie.y; return cie; } static float Labf(float t) { return t > ((6.0/29.0) * (6.0/29.0) * (6.0/29.0)) ? pow(t, 1.0/3.0) : ((1.0/3.0) * (29.0 / 6.0) * (29.0 / 6.0)) * t + (4.0 / 29.0); } static Lab toLab(CIE *c) { CIE n = (CIE) {Labf(c->X / white.X), Labf(c->Y / white.Y), Labf(c->Z / white.Z)}; Lab l; // http://en.wikipedia.org/wiki/L*a*b*#RGB_and_CMYK_conversions l.L = 116.0 * n.Y - 16; l.a = 500.0 * (n.X - n.Y); l.b = 200.0 * (n.Y - n.Z); return l; } static float munsellSloanGodlove(float t) { return sqrt(1.4742 * t - 0.004743 * t * t); } static CIE adams(CIE *v) { CIE c; c.Y = munsellSloanGodlove(v->Y); c.X = munsellSloanGodlove((white.Y / white.X) * v->X) - c.Y; c.Z = munsellSloanGodlove((white.Z / white.X) * v->Z) - c.Y; return c; } #define SQUARE(x) ((x) * (x)) static float CIE76(Lab *L1, Lab *L2) { // http://en.wikipedia.org/wiki/Color_difference#CIE76 float lbias = 1.0; return sqrt(lbias * SQUARE(L2->L - L1->L) + SQUARE(L2->a - L1->a) + SQUARE(L2->b - L1->b)); } static void init_coersion() { fcolor sRGB_white = (fcolor) {1, 1, 1}; white = toCIE(sRGB_white); int i; for (i = 0; i < 16; i++) { ciePalette[i] = toCIE(palette[i]); labPalette[i] = toLab(&ciePalette[i]); adamsPalette[i] = adams(&ciePalette[i]); } if (colormode == coerce_256) { initialize_prs(); } cell_buffer = 0; } static int best (fcolor *fg, fcolor *bg) { // analyze fg & bg for their contrast CIE cieFg = toCIE(*fg); CIE cieBg = toCIE(*bg); Lab labFg = toLab(&cieFg); Lab labBg = toLab(&cieBg); // CIE adamsFg = adams(&cieFg); // CIE adamsBg = adams(&cieBg); float JND = 2.3; // just-noticeable-difference int areTheSame = CIE76(&labFg, &labBg) <= 2.0 * JND; // a little extra fudge float big = 100000000; int fg1 = 0, fg2 = 0, bg1 = 0, bg2 = 0; float fg1_score = big, fg2_score = big; float bg1_score = big, bg2_score = big; int i; for (i = 0; i < 8; i++) { float s = CIE76(labPalette + i, &labBg); if (s < bg2_score) { if (s < bg1_score) { bg2 = bg1; bg1 = i; bg2_score = bg1_score; bg1_score = s; } else { bg2 = i; bg2_score = s; } } } if (areTheSame) { return COLORING(bg1, bg1); } for (i = 0; i < 16; i++) { float s = CIE76(labPalette + i, &labFg); if (s < fg2_score) { if (s < fg1_score) { fg2 = fg1; fg1 = i; fg2_score = fg1_score; fg1_score = s; } else { fg2 = i; fg2_score = s; } } } if (fg1 != bg1) { return COLORING (fg1, bg1); } else { if (fg1_score + bg2_score < fg2_score + bg1_score) { return COLORING(fg1, bg2); } else { return COLORING(fg2, bg1); } } } static void initialize_prs() { int i; for (i = 16; i < 255; i++) { prs[i].next = i + 1; } prs[0].next = 16; prs[1].next = 0; prs[255].next = 0; } static void coerce_colorcube (fcolor *f, intcolor *c) { // 0-15 are the standard ANSI colors // 16-231 are a 6x6x6 RGB color cube given by ((36 * r) + (6 * g) + b + 16) with r,g,b in [0..5] // 232-255 are a greyscale ramp without black and white. float sat = 0.2, bright = 0.6, contrast = 6.3; float rf = bright + f->r * contrast, gf = bright + f->g * contrast, bf = bright + f->b * contrast; if (rf < gf && rf < bf) rf -= sat * ((gf < bf ? bf : gf) - rf); else if (gf < bf && gf < rf) gf -= sat * ((rf < bf ? bf : rf) - gf); else if (bf < gf && bf < rf) bf -= sat * ((gf < rf ? rf : gf) - bf); int r = rf, g = gf, b = bf; r = r < 0 ? 0 : r > 5 ? 5 : r; g = g < 0 ? 0 : g > 5 ? 5 : g; b = b < 0 ? 0 : b > 5 ? 5 : b; c->r = r; c->g = g; c->b = b; c->idx = ((36 * r) + (6 * g) + b + 16); } static int intcolor_distance (intcolor *a, intcolor *b) { return (a->r - b->r) * (a->r - b->r) + (a->g - b->g) * (a->g - b->g) + (a->b - b->b) * (a->b - b->b); } static int coerce_prs (intcolor *fg, intcolor *bg) { // search for an exact match in the list int pair; pair = prs[1].next; while (pair) { if (prs[pair].fore.idx == fg->idx && prs[pair].back.idx == bg->idx) { // perfect. prs[pair].count++; return pair; } pair = prs[pair].next; } // no exact match? try to insert it as a new one pair = prs[0].next; if (pair) { // there's room! // remove prs[0].next = prs[pair].next; // insert at the front prs[pair].next = prs[1].next; prs[1].next = pair; // initialize it prs[pair].fore = *fg; prs[pair].back = *bg; prs[pair].count = 1; init_pair(pair, fg->idx, bg->idx); return pair; } // search for an approximate match in the list int bestpair = 0, bestscore = 2 * 3 * 6 * 6; // naive distance metric for now pair = prs[1].next; while (pair) { int delta = intcolor_distance(&prs[pair].fore, fg) + intcolor_distance(&prs[pair].back, bg); if (delta < bestscore) { bestscore = delta; bestpair = pair; if (delta == 1) break; // as good as it gets without being exact! } pair = prs[pair].next; } prs[bestpair].count++; return bestpair; } static void buffer_plot(int ch, int x, int y, fcolor *fg, fcolor *bg) { // int pair = 256 + x + y * minsize.width; // intcolor cube_fg, cube_bg; // coerce_colorcube(fg, &cube_fg), // coerce_colorcube(bg, &cube_bg); // pair = cube_bg.idx; // cube_fg = cube_bg; // init_pair(pair, cube_fg.idx, cube_bg.idx); // return pair; intcolor cube_fg, cube_bg; coerce_colorcube(fg, &cube_fg); coerce_colorcube(bg, &cube_bg); if (cube_fg.idx == cube_bg.idx) { // verify that the colors are really the same; otherwise, we'd better force the output apart int naive_distance = (fg->r - bg->r) * (fg->r - bg->r) + (fg->g - bg->g) * (fg->g - bg->g) + (fg->b - bg->b) * (fg->b - bg->b); if (naive_distance > 3) { // very arbitrary cutoff, and an arbitrary fix, very lazy if (cube_bg.r > 0) {cube_bg.r -= 1; cube_bg.idx -= 1; } if (cube_bg.g > 0) {cube_bg.g -= 1; cube_bg.idx -= 6; } if (cube_bg.b > 0) {cube_bg.b -= 1; cube_bg.idx -= 36; } } } int cell = x + y * minsize.width; cell_buffer[cell].ch = ch; cell_buffer[cell].pair = -1; cell_buffer[cell].fore = cube_fg; cell_buffer[cell].back = cube_bg; } static void buffer_render_256() { // build a new palette initialize_prs(); int length = minsize.width * minsize.height; int i, idx, x, y; for (i = 0; i < length; i++) { cell_buffer[i].shuffle = i; } for (i = length - 1; i >= 0; i--) { // int roll = i == 0 ? 0 : rand() % i; // idx = cell_buffer[roll].shuffle; // cell_buffer[roll].shuffle = cell_buffer[i].shuffle; idx = i; int pair = coerce_prs(&cell_buffer[idx].fore, &cell_buffer[idx].back); cell_buffer[idx].pair = pair; } // render it all! i = 0; for (y = 0; y < minsize.height; y++) { move(y, 0); for (x = 0; x < minsize.width; x++) { color_set(cell_buffer[i].pair, NULL); addch(cell_buffer[i].ch); i++; } } } static void term_mvaddch(int x, int y, int ch, fcolor *fg, fcolor *bg) { if (x < 0 || y < 0 || x >= minsize.width || y >= minsize.height) return; if (colormode == coerce_16) { int c = best(fg, bg); attrset(COLOR_ATTR(c)); mvaddch(y, x, ch); } else { buffer_plot(ch, x, y, fg, bg); } } static void term_refresh() { // to set up a 256-color terminal, see: // http://push.cx/2008/256-color-xterms-in-ubuntu if (0 && can_change_color()) { int i; for (i = 0; i < 16; i++) { short r = palette[i].r * 1000; short g = palette[i].g * 1000; short b = palette[i].b * 1000; if (r < 0) r = 0; if (g < 0) g = 0; if (b < 0) b = 0; init_color(i + 1, r, g, b); } } if (0) { int i; short r, g, b; for (i = 0; i < 8; i++) { color_content(i, &r, &g, &b); palette[i].r = r * .001; palette[i].g = g * .001; palette[i].b = b * .001; } } if (colormode == coerce_256) { buffer_render_256(); } refresh(); } static void ensure_size( ); static int term_getkey( ) { Term.mouse.justPressed = 0; Term.mouse.justReleased = 0; Term.mouse.justMoved = 0; while (1) { int got = getch(); if (got == KEY_RESIZE) { ensure_size( ); } else if (got == KEY_MOUSE) { MEVENT mevent; getmouse (&mevent); Term.mouse.x = mevent.x; Term.mouse.y = mevent.y; Term.mouse.shift = (mevent.bstate & BUTTON_SHIFT) != 0; Term.mouse.control = (mevent.bstate & BUTTON_CTRL) != 0; if (mevent.bstate & BUTTON1_PRESSED) { Term.mouse.justPressed = 1; Term.mouse.isPressed = 1; } else if (mevent.bstate & BUTTON1_RELEASED) { if (Term.mouse.isPressed) { Term.mouse.justReleased = 1; Term.mouse.isPressed = 0; } } else { Term.mouse.justMoved = 1; } return TERM_MOUSE; } else { if (got == KEY_ENTER) got = 13; // KEY_ENTER -> ^M for systems with odd values for KEY_ENTER if (got == ERR) return TERM_NONE; else return got; } } } static int term_has_key() { int ch = getch(); if (ch != ERR) { ungetch(ch); return 1; } else { return 0; } } static void ensure_size( ) { int w = minsize.width, h = minsize.height; getmaxyx(stdscr, Term.height, Term.width); if (Term.height < h || Term.width < w) { getmaxyx(stdscr, Term.height, Term.width); nodelay(stdscr, FALSE); while (Term.height < h || Term.width < w) { erase(); attrset(COLOR_ATTR(7)); mvprintw(1,0,"Brogue needs a terminal window that is at least [%d x %d]", w, h); attrset(COLOR_ATTR(15)); mvprintw(2,0,"If your terminal can be resized, resize it now.\n"); attrset(COLOR_ATTR(7)); mvprintw(3,0,"Press ctrl-c at any time to quit.\n"); printw("Width: %d/%d\n", Term.width, w); printw("Height: %d/%d\n", Term.height, h); mvprintw(10, 0, "Colors (pairs): %d (%d)\n", COLORS, COLOR_PAIRS); getch(); getmaxyx(stdscr, Term.height, Term.width); } nodelay(stdscr, TRUE); erase(); refresh(); } } static void term_resize(int w, int h) { minsize.width = w; minsize.height = h; // try to set the terminal size if the terminal will let us: term_set_size(h, w); // (this works in gnome-terminal, but causes trouble for curses on maximized windows.) // now make sure it worked, and ask the user to resize the terminal if it didn't ensure_size(); // make a new cell buffer if (cell_buffer) free(cell_buffer); cell_buffer = malloc(sizeof(pairmode_cell) * w * h); // add error checking int i; for (i = 0; i < w * h; i++) { // I guess we could just zero it all, hmm cell_buffer[i].ch = 0; cell_buffer[i].pair = 0; cell_buffer[i].fore.idx = 0; cell_buffer[i].back.idx = 0; } } static void term_wait(int ms) { napms(ms); } struct { char *name; int ch; } curses_keys[] = { {"NONE", TERM_NONE}, {"TAB", '\t'}, {"ENTER", '\n'}, {"RETURN", '\n'}, {"SPACE", ' '}, {"ESC", 27}, {"ESCAPE", 27}, {"BREAK", KEY_BREAK}, {"SRESET", KEY_SRESET}, {"RESET", KEY_RESET}, {"DOWN", KEY_DOWN}, {"UP", KEY_UP }, {"LEFT", KEY_LEFT}, {"RIGHT", KEY_RIGHT}, {"HOME", KEY_HOME}, {"BACKSPACE", KEY_BACKSPACE}, {"F1", KEY_F(1)}, {"F2", KEY_F(2)}, {"F3", KEY_F(3)}, {"F4", KEY_F(4)}, {"F5", KEY_F(5)}, {"F6", KEY_F(6)}, {"F7", KEY_F(7)}, {"F8", KEY_F(8)}, {"F9", KEY_F(9)}, {"F10", KEY_F(10)}, {"F11", KEY_F(11)}, {"F12", KEY_F(12)}, {"DL", KEY_DL}, {"IL", KEY_IL}, {"DC", KEY_DC}, {"DEL", KEY_DC}, {"DELETE", KEY_DC}, {"IC", KEY_IC}, {"EIC", KEY_EIC}, {"CLEAR", KEY_CLEAR}, {"EOS", KEY_EOS}, {"EOL", KEY_EOL}, {"SF", KEY_SF}, {"SR", KEY_SR}, {"PGUP", KEY_NPAGE}, {"PGDN", KEY_PPAGE}, {"PAGEDOWN", KEY_NPAGE}, {"PAGEUP", KEY_PPAGE}, {"NPAGE", KEY_NPAGE}, {"PPAGE", KEY_PPAGE}, {"STAB", KEY_STAB}, {"CTAB", KEY_CTAB}, {"CATAB", KEY_CATAB}, {"PRINT", KEY_PRINT}, {"LL", KEY_LL}, {"A1", KEY_A1}, {"A3", KEY_A3}, {"B2", KEY_B2}, {"C1", KEY_C1}, {"C3", KEY_C3}, {"BTAB", KEY_BTAB}, {"BEG", KEY_BEG }, {"CANCEL", KEY_CANCEL}, {"CLOSE", KEY_CLOSE}, {"COMMAND", KEY_COMMAND}, {"COPY", KEY_COPY}, {"CREATE", KEY_CREATE}, {"END", KEY_END }, {"EXIT", KEY_EXIT}, {"FIND", KEY_FIND}, {"HELP", KEY_HELP}, {"MARK", KEY_MARK}, {"MESSAGE", KEY_MESSAGE}, {"MOVE", KEY_MOVE}, {"NEXT", KEY_NEXT}, {"OPEN", KEY_OPEN}, {"OPTIONS", KEY_OPTIONS}, {"PREVIOUS", KEY_PREVIOUS}, {"REDO", KEY_REDO}, {"REFERENCE", KEY_REFERENCE}, {"REFRESH", KEY_REFRESH}, {"REPLACE", KEY_REPLACE}, {"RESTART", KEY_RESTART}, {"RESUME", KEY_RESUME}, {"SAVE", KEY_SAVE}, {"SBEG", KEY_SBEG}, {"SCANCEL", KEY_SCANCEL}, {"SCOMMAND", KEY_SCOMMAND}, {"SCOPY", KEY_SCOPY}, {"SCREATE", KEY_SCREATE}, {"SDC", KEY_SDC }, {"SDL", KEY_SDL }, {"SELECT", KEY_SELECT}, {"SEND", KEY_SEND}, {"SEOL", KEY_SEOL}, {"SEXIT", KEY_SEXIT}, {"SFIND", KEY_SFIND}, {"SHELP", KEY_SHELP}, {"SHOME", KEY_SHOME}, {"SIC", KEY_SIC }, {"SLEFT", KEY_SLEFT}, {"SMESSAGE", KEY_SMESSAGE}, {"SMOVE", KEY_SMOVE}, {"SNEXT", KEY_SNEXT}, {"SOPTIONS", KEY_SOPTIONS}, {"SPREVIOUS", KEY_SPREVIOUS}, {"SPRINT", KEY_SPRINT}, {"SREDO", KEY_SREDO}, {"SREPLACE", KEY_SREPLACE}, {"SRIGHT", KEY_SRIGHT}, {"SRSUME", KEY_SRSUME}, {"SSAVE", KEY_SSAVE}, {"SSUSPEND", KEY_SSUSPEND}, {"SUNDO", KEY_SUNDO}, {"SUSPEND", KEY_SUSPEND}, {"UNDO", KEY_UNDO}, {"MOUSE", KEY_MOUSE}, {"RESIZE", KEY_RESIZE}, {NULL, 0}, }; int term_keycodeByName(const char *name) { int i = 0; while (curses_keys[i].name != NULL) { if (strcmp(name, curses_keys[i].name) == 0) { return curses_keys[i].ch; } i++; } return name[0]; } struct term_t Term = { term_start, term_end, term_mvaddch, term_refresh, term_getkey, term_wait, term_has_key, term_title, term_resize, term_keycodeByName, {KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_BACKSPACE, KEY_DC, KEY_F(12)} };