/* * Apple // emulator for *ix * * This software package is subject to the GNU General Public License * version 3 or later (your choice) as published by the Free Software * Foundation. * * Copyright 2017 Aaron Culliney * */ // ncurses renderer (for those of us who still CLI ;) #include "common.h" #include "video/video.h" #include #if HAVE_NCURSES_H # include #elif HAVE_NCURSESW_NCURSES_H # include #elif HAVE_NCURSES_NCURSES_H # include #elif HAVE_NCURSES_CURSES_H # include #elif HAVE_CURSES_H # include #endif #define THIRTYFPS NANOSECONDS_PER_SECOND / 30UL // 30FPS #define ASCII_BACK 0x08 #define ASCII_TAB 0x09 #define ASCII_LF 0x0a #define ASCII_CR 0x0d #define ASCII_ESC 0x1b //#define COLOR_BLACK 0 //#define COLOR_MAGENTA 5 #define COLOR_DARKBLUE 32 #define COLOR_PURPLE 33 #define COLOR_DARKGREEN 34 #define COLOR_DARKGREY 35 #define COLOR_MEDBLUE 36 #define COLOR_LIGHTBLUE 37 #define COLOR_BROWN 38 #define COLOR_ORANGE 39 #define COLOR_LIGHTGREY 40 #define COLOR_PINK 41 //#define COLOR_GREEN 2 //#define COLOR_YELLOW 3 #define COLOR_AQUA 42 //#define COLOR_WHITE 7 static bool ncvideo_running = true; static WINDOW *winCurr = NULL; static WINDOW *winMenu = NULL; // 24 x 80 text static WINDOW *winTxt40 = NULL; // 24 x 40 text static WINDOW *winTxt80 = NULL; // 24 x 80 text static WINDOW *winScale = NULL; // graphics mode scaled/interpolated to terminal dimensions // ---------------------------------------------------------------------------- // ncurses video backend helper routines static void _nc_convertAppleGlyphs(INOUT chtype *c, OUTPARM char **s, uint8_t *cs) { switch (*c) { case 0x7f: // cursor ... *c = '_'; *cs = BLACK_ON_RED; break; case 0x80: // closed apple ... *c = '@'; *cs = BLACK_ON_MAGENTA; break; case 0x81: // open apple ... *c = '@'; *cs = BLACK_ON_RED; break; case 0x82: // caret ... *c = '^'; *cs = BLACK_ON_RED; break; case 0x83: // hourglass ... *c = '%'; *cs = BLACK_ON_RED; break; case 0x84: // checkmark ... #if NCURSES_UTF8 *c = '\0'; *s = "✓"; #else *c = 'x'; #endif *cs = BLACK_ON_RED; break; case 0x85: // reverse checkmark ... #if NCURSES_UTF8 *c = '\0'; *s = "✓"; ////*cs = INVERSE_CURRENT; #else *c = 'X'; #endif *cs = BLACK_ON_RED; break; case 0x86: // runner left ... *c = ']'; *cs = BLACK_ON_RED; break; case 0x87: // runner right ... *c = '['; *cs = BLACK_ON_RED; break; case 0x88: // left arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "←"; #else *c = '<'; #endif *cs = BLACK_ON_RED; break; case 0x89: // ... #if NCURSES_UTF8 *c = '\0'; *s = "…"; #else *c = '.'; #endif *cs = BLACK_ON_RED; break; case 0x8a: // down arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "↓"; #else *c = '!'; #endif *cs = BLACK_ON_RED; break; case 0x8b: // up arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "↑"; #else *c = '^'; #endif *cs = BLACK_ON_RED; break; case 0x8c: // top bar ... #if NCURSES_UTF8 *c = '\0'; *s = "¯"; #else *c = '-'; #endif *cs = BLACK_ON_RED; break; case 0x8d: // CR ... #if NCURSES_UTF8 *c = '\0'; *s = "⏎"; #else *c = '^'; #endif *cs = BLACK_ON_RED; break; case 0x8e: // block ... #if NCURSES_UTF8 *c = '\0'; *s = "█"; #else *c = '#'; #endif *cs = BLACK_ON_RED; break; case 0x8f: // filled left arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "⇦"; #else *c = '<'; #endif *cs = BLACK_ON_RED; break; case 0x90: // filled right arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "⇨"; #else *c = '>'; #endif *cs = BLACK_ON_RED; break; case 0x91: // filled down arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "⇩"; #else *c = '!'; #endif *cs = BLACK_ON_RED; break; case 0x92: // filled up arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "⇧"; #else *c = '^'; #endif *cs = BLACK_ON_RED; break; case 0x93: // mdash ... #if NCURSES_UTF8 *c = '\0'; *s = "—"; #else *c = '-'; #endif *cs = BLACK_ON_RED; break; case 0x94: // box bottom left ... *c = '\\'; *cs = BLACK_ON_RED; break; case 0x95: // right arrow ... #if NCURSES_UTF8 *c = '\0'; *s = "→"; #else *c = '>'; #endif *cs = BLACK_ON_RED; break; case 0x96: // hash #1 ... *c = '#'; *cs = BLACK_ON_RED; break; case 0x97: // hash #2 ... *c = '#'; *cs = BLACK_ON_RED; break; case 0x98: // folder left ... *c = 'f'; *cs = BLACK_ON_RED; break; case 0x99: // folder right ... *c = 'f'; *cs = BLACK_ON_RED; break; case 0x9a: // right full bar ... *c = '|'; *cs = BLACK_ON_RED; break; case 0x9b: // diamond ... #if NCURSES_UTF8 *c = '\0'; *s = "◆"; #else *c = '*'; #endif *cs = BLACK_ON_RED; break; case 0x9c: // top and bottom bars ... #if NCURSES_UTF8 *c = '\0'; *s = "◆"; #else *c = '='; #endif *cs = BLACK_ON_RED; break; case 0x9d: // white plus ... *c = '+'; *cs = BLACK_ON_RED; break; case 0x9e: // box and dot ... #if NCURSES_UTF8 *c = '\0'; *s = "▣"; #else *c = '#'; #endif *cs = BLACK_ON_RED; break; case 0x9f: // left full bar ... *c = '|'; *cs = BLACK_ON_RED; break; // interface menus ... case 0xA0: case 0xA3: *c = '/'; break; case 0xA1: case 0xA2: *c = '\\'; break; case 0xA4: *c = '|'; break; case 0xA5: *c = '-'; break; case 0xA6: case 0xA7: case 0xA8: case 0xA9: case 0xAA: *c = '+'; break; } } static int _nc_keyToEmulator(int ncKey, int *isCooked) { int a2key = ncKey; bool isASCII = false; switch (ncKey) { case KEY_F(1): a2key = SCODE_F1; break; case KEY_F(2): a2key = SCODE_F2; break; case KEY_F(3): a2key = SCODE_F3; break; case KEY_F(4): a2key = SCODE_F4; break; case KEY_F(5): a2key = SCODE_F5; break; case KEY_F(6): a2key = SCODE_F6; break; case KEY_F(7): a2key = SCODE_F7; break; case KEY_F(8): a2key = SCODE_F8; break; case KEY_F(9): a2key = SCODE_F9; break; case KEY_F(10): a2key = SCODE_F10; break; case KEY_F(11): a2key = SCODE_F11; break; case KEY_F(12): a2key = SCODE_F12; break; case KEY_DOWN: a2key = SCODE_D; break; case KEY_BACKSPACE: case KEY_LEFT: case ASCII_BACK: a2key = SCODE_L; break; case KEY_RIGHT: a2key = SCODE_R; break; case KEY_UP: a2key = SCODE_U; break; case ASCII_LF: case ASCII_CR: a2key = SCODE_RET; break; case ASCII_ESC: a2key = SCODE_ESC; break; case ASCII_TAB: default: if (a2key >= 0 && a2key < 256) { isASCII = true; } else { a2key = -1; } break; } *isCooked = isASCII; return a2key; } #define COLOR_VALUES(x) \ ((NCURSES_COLOR_T)(colormap[x].red ) * 1000)/255, \ ((NCURSES_COLOR_T)(colormap[x].green) * 1000)/255, \ ((NCURSES_COLOR_T)(colormap[x].blue) * 1000)/255 static void _nc_initColors(void) { if (has_colors() == FALSE) { LOG("Your terminal does not support color, so emulated color values will not be correct..."); return; } if (can_change_color() == FALSE) { LOG("Your terminal does not support changing color values, so emulated color values will not be correct..."); // drop through to start default color support ... } int ret = start_color(); if (ret == ERR) { LOG("OOPS, could not initialize ncurses colors"); } // ncurses scales colors between 0 - 1000 ret = init_color(COLOR_MAGENTA ,COLOR_VALUES(IDX_MAGENTA )); ret = init_color(COLOR_DARKBLUE ,COLOR_VALUES(IDX_DARKBLUE )); ret = init_color(COLOR_PURPLE ,COLOR_VALUES(IDX_PURPLE )); ret = init_color(COLOR_DARKGREEN,COLOR_VALUES(IDX_DARKGREEN)); ret = init_color(COLOR_DARKGREY ,COLOR_VALUES(IDX_DARKGREY )); ret = init_color(COLOR_MEDBLUE ,COLOR_VALUES(IDX_MEDBLUE )); ret = init_color(COLOR_LIGHTBLUE,COLOR_VALUES(IDX_LIGHTBLUE)); ret = init_color(COLOR_BROWN ,COLOR_VALUES(IDX_BROWN )); ret = init_color(COLOR_ORANGE ,COLOR_VALUES(IDX_ORANGE )); ret = init_color(COLOR_LIGHTGREY,COLOR_VALUES(IDX_LIGHTGREY)); ret = init_color(COLOR_PINK ,COLOR_VALUES(IDX_PINK )); ret = init_color(COLOR_GREEN ,COLOR_VALUES(IDX_GREEN )); ret = init_color(COLOR_YELLOW ,COLOR_VALUES(IDX_YELLOW )); ret = init_color(COLOR_AQUA ,COLOR_VALUES(IDX_AQUA )); // interface and mousetext colors init_pair(1+GREEN_ON_BLACK, COLOR_GREEN, COLOR_BLACK ); init_pair(1+GREEN_ON_BLUE, COLOR_GREEN, COLOR_BLUE ); init_pair(1+RED_ON_BLACK, COLOR_RED, COLOR_BLACK ); init_pair(1+BLUE_ON_BLACK, COLOR_BLUE, COLOR_BLACK ); init_pair(1+WHITE_ON_BLACK, COLOR_WHITE, COLOR_BLACK ); init_pair(1+BLACK_ON_RED, COLOR_BLACK, COLOR_RED ); // 16 COLORS: init_pair(1+BLACK_ON_BLACK, COLOR_BLACK, COLOR_BLACK ); init_pair(1+BLACK_ON_MAGENTA, COLOR_BLACK, COLOR_MAGENTA ); init_pair(1+BLACK_ON_DARKBLUE, COLOR_BLACK, COLOR_DARKBLUE ); init_pair(1+BLACK_ON_PURPLE, COLOR_BLACK, COLOR_PURPLE ); init_pair(1+BLACK_ON_DARKGREEN, COLOR_BLACK, COLOR_DARKGREEN); init_pair(1+BLACK_ON_DARKGREY, COLOR_BLACK, COLOR_DARKGREY ); init_pair(1+BLACK_ON_MEDBLUE, COLOR_BLACK, COLOR_MEDBLUE ); init_pair(1+BLACK_ON_LIGHTBLUE, COLOR_BLACK, COLOR_LIGHTBLUE); init_pair(1+BLACK_ON_BROWN, COLOR_BLACK, COLOR_BROWN ); init_pair(1+BLACK_ON_ORANGE, COLOR_BLACK, COLOR_ORANGE ); init_pair(1+BLACK_ON_LIGHTGREY, COLOR_BLACK, COLOR_LIGHTGREY); init_pair(1+BLACK_ON_PINK, COLOR_BLACK, COLOR_PINK ); init_pair(1+BLACK_ON_GREEN, COLOR_BLACK, COLOR_GREEN ); init_pair(1+BLACK_ON_YELLOW, COLOR_BLACK, COLOR_YELLOW ); init_pair(1+BLACK_ON_AQUA, COLOR_BLACK, COLOR_AQUA ); init_pair(1+BLACK_ON_WHITE, COLOR_BLACK, COLOR_WHITE ); } static WINDOW *_nc_newwin(unsigned int height, int width, int starty, int startx) { WINDOW *win = newwin(height+2, width+2, starty-1, startx-1); //box(win, 0 , 0); //meta(win, TRUE); return win; } static void _nc_delwin(WINDOW **winRef) { wborder(*winRef, ' ', ' ', ' ',' ',' ',' ',' ',' '); wrefresh(*winRef); delwin(*winRef); *winRef = NULL; } // ---------------------------------------------------------------------------- // ncurses video backend main routines static const char *ncvideo_name(void) { return "ncurses"; } static void ncvideo_init(void *context) { // ... } static void ncvideo_main_loop(void) { // start curses mode and check colors ... initscr(); _nc_initColors(); LOG("ncurses video main loop beginning, silencing STDERR logging ..."); do_std_logging = false; noecho(); // Do not echo output ... raw(); // Line buffering disabled ... keypad(stdscr, TRUE); // Special and F keys ... nodelay(stdscr, TRUE); // getch() is non-blocking ... int starty24 = (LINES - TEXT_ROWS) / 2; int startx40 = (COLS - 40) / 2; int startx80 = (COLS - 80) / 2; winMenu = _nc_newwin(TEXT_ROWS, 80, starty24, startx80); winTxt40 = _nc_newwin(TEXT_ROWS, 40, starty24, startx40); winTxt80 = _nc_newwin(TEXT_ROWS, 80, starty24, startx80); winScale = _nc_newwin(LINES, COLS, 0, 0); ////wbkgd(winTxt40, COLOR_PAIR(1)); // GREEN ON BLACK (for now) ////wbkgd(winTxt80, COLOR_PAIR(1)); // GREEN ON BLACK (for now) static uint8_t fb[SCANWIDTH*SCANHEIGHT*sizeof(uint8_t)]; #if INTERFACE_CLASSIC interface_setStagingFramebuffer(fb); #endif while (ncvideo_running) { unsigned long wasDirty = 0UL; if (!cpu_isPaused()) { // check if a2 video memory is dirty wasDirty = video_clearDirty(A2_DIRTY_FLAG); if (wasDirty) { display_renderStagingFramebuffer(fb); } } if (interface_isShowing()) { winCurr = winMenu; WINDOW *winPrev = winCurr; if (winPrev != winCurr) { wclear(winPrev); } } wasDirty = video_clearDirty(FB_DIRTY_FLAG); if (wasDirty) { wrefresh(winCurr); } // handle keyboard input int c = getch(); int is_ascii = 0; if (c == ERR) { c_keys_handle_input(-1, /*pressed:*/0, /*is_ascii:*/0); } else { c = _nc_keyToEmulator(c, &is_ascii); if (is_ascii) { c_keys_handle_input(c, /*pressed:*/1, /*is_ascii:*/1); } else { c_keys_handle_input(c, /*pressed:*/1, /*is_ascii:*/0); c_keys_handle_input(c, /*pressed:*/0, /*is_ascii:*/0); } } // FIXME TODO ... does not account for execution time drift struct timespec thirtyfps = { .tv_sec = 0, .tv_nsec = THIRTYFPS }; nanosleep(&thirtyfps, NULL); } _nc_delwin(&winMenu); _nc_delwin(&winTxt40); _nc_delwin(&winTxt80); _nc_delwin(&winScale); endwin(); } static void ncvideo_render(void) { // no-op ... } static void ncvideo_shutdown(void) { ncvideo_running = false; } // ---------------------------------------------------------------------------- // plotting callbacks ... static void _nc_graphicsUpdate(pixel_delta_t pixel) { // TODO FIXME ... actually need to scale/plot graphics here uint8_t cs = pixel.cs; uint8_t row = pixel.row; uint8_t col = pixel.col; assert(cs == COLOR16); WINDOW *win = winCurr; chtype c = 0; char *s = NULL; uint8_t color = (pixel.b & 0x0F); c = ' '; switch (color) { case 0x0: // Black cs = BLACK_ON_BLACK; break; case 0x1: // Magenta cs = BLACK_ON_MAGENTA; break; case 0x2: // Dark Blue cs = BLACK_ON_DARKBLUE; break; case 0x3: // Purple cs = BLACK_ON_PURPLE; break; case 0x4: // Dark Green cs = BLACK_ON_DARKGREEN; break; case 0x5: // Dark Grey cs = BLACK_ON_DARKGREY; break; case 0x6: // Medium Blue cs = BLACK_ON_MEDBLUE; break; case 0x7: // Light Blue cs = BLACK_ON_LIGHTBLUE; break; case 0x8: // Brown cs = BLACK_ON_BROWN; break; case 0x9: // Orange cs = BLACK_ON_ORANGE; break; case 0xa: // Light Grey cs = BLACK_ON_LIGHTGREY; break; case 0xb: // Pink cs = BLACK_ON_PINK; break; case 0xc: // Green cs = BLACK_ON_GREEN; break; case 0xd: // Yellow cs = BLACK_ON_YELLOW; break; case 0xe: // Aqua cs = BLACK_ON_AQUA; break; case 0xf: // WHITE cs = BLACK_ON_WHITE; break; } } static void _nc_textUpdate(pixel_delta_t pixel) { uint8_t cs = pixel.cs; uint8_t row = pixel.row; uint8_t col = pixel.col; WINDOW *win = winCurr; chtype c = 0; char *s = NULL; font_mode_t fontMode = FONT_MODE_NORMAL; do { if (cs == COLOR16) { // LORES/DLORES ... _nc_graphicsUpdate(pixel); return; } else if (cs == INVALID_COLORSCHEME) { // TEXT c = keys_apple2ASCII(pixel.b, &fontMode); _nc_convertAppleGlyphs(&c, &s, &cs); } else { // interface menu c = pixel.b; _nc_convertAppleGlyphs(&c, &s, &cs); win = winMenu; } attr_t attrs = 0; { short ignoredPair = 0; if (wattr_get(win, &attrs, &ignoredPair, /*opts:*/NULL) == ERR) { break; } } if (fontMode == FONT_MODE_INVERSE) { wattron(win, A_REVERSE); } else if (fontMode == FONT_MODE_FLASH) { wattron(win, A_BOLD); // TODO FIXME ... A_BLINK not reliable, but maybe switch between normal and A_REVERSE at the flash rate? } if (cs != INVALID_COLORSCHEME) { wattr_set(win, attrs, cs+1, /*opts:*/NULL); } #if NCURSES_UTF8 if (s) { mvwaddstr(win, row+1, col+1, s); } else #endif mvwaddch(win, row+1, col+1, c); if (cs != INVALID_COLORSCHEME) { wattr_set(win, attrs, 0, /*opts:*/NULL); } if (fontMode == FONT_MODE_INVERSE) { wattroff(win, A_REVERSE); } else if (fontMode == FONT_MODE_FLASH) { wattroff(win, A_BOLD); } } while (0); } static void _nc_modeChange(pixel_delta_t pixel) { #warning FIXME TODO, this copies display.c/vm.c routines somewhat, likely need to consolidate if/when other video backends implement video update callbacks if (interface_isShowing()) { return; } WINDOW *winPrev = winCurr; uint32_t currswitches = run_args.softswitches; if ((currswitches & SS_TEXT) && !(currswitches & SS_MIXED)) { winCurr = (currswitches & SS_80COL) ? winTxt80 : winTxt40; } else { winCurr = winScale; } if (winPrev != winCurr) { wclear(winPrev); wrefresh(winPrev); } } // ---------------------------------------------------------------------------- static void _init_ncvideo(void) { LOG("Initializing ncurses renderer"); static video_backend_s ncvideo_backend = { 0 }; ncvideo_backend.name = &ncvideo_name; ncvideo_backend.init = &ncvideo_init; ncvideo_backend.main_loop = &ncvideo_main_loop; ncvideo_backend.render = &ncvideo_render; ncvideo_backend.shutdown = &ncvideo_shutdown; static video_animation_s ncvideo_anim = { #if 1 0 // FIXME TODO ... likely we need to follow the 'glnode' model with 'ncnode.c' and render the ncalert.c stuff over the underlying winCurr here ... #else .animation_showMessage = &_ncanim_showMessage; .animation_showPaused = &_ncanim_showPaused; .animation_showCPUSpeed = &_ncanim_showCPUSpeed; .animation_showDiskChosen = &_ncanim_showDiskChosen; .animation_showTrackSector = &_ncanim_showTrackSector; #endif }; ncvideo_backend.anim = &ncvideo_anim; video_registerBackend(&ncvideo_backend, VID_PRIO_TERMINAL); display_setUpdateCallback(DRAWPAGE_TEXT, &_nc_textUpdate); display_setUpdateCallback(DRAWPAGE_MODE_CHANGE, &_nc_modeChange); } static __attribute__((constructor)) void __init_ncvideo(void) { emulator_registerStartupCallback(CTOR_PRIORITY_EARLY, &_init_ncvideo); }