Просмотр исходного кода

Updated: breaking up main.

We have render and terminal.
They are still c. They haven't been
converted to classes yet.  NNY!
bugz 4 лет назад
Родитель
Сommit
3ac1940504
6 измененных файлов с 915 добавлено и 844 удалено
  1. 1 1
      CMakeLists.txt
  2. 18 843
      mystic.cpp
  3. 485 0
      render.cpp
  4. 25 0
      render.h
  5. 362 0
      terminal.cpp
  6. 24 0
      terminal.h

+ 1 - 1
CMakeLists.txt

@@ -78,7 +78,7 @@ add_subdirectory(zf_log)
 # )
 # add_executable(yourProj ${SOURCES})
 
-add_executable(mystic mystic.cpp lastseen.cpp)
+add_executable(mystic mystic.cpp lastseen.cpp terminal.cpp render.cpp)
 target_link_libraries(mystic util)
 target_link_libraries(mystic zf_log)
 target_compile_definitions(mystic PUBLIC ZF_LOG_DEF_LEVEL=ZF_LOG_VERBOSE)

+ 18 - 843
mystic.cpp

@@ -4,8 +4,7 @@
 #include <stdio.h>
 #include <string.h>
 #include <termios.h>
-#include <unistd.h> // usleep()
-
+#include <unistd.h>
 #include <sys/select.h>
 #include <sys/wait.h>
 
@@ -73,6 +72,10 @@ static void file_output_open(const char *const log_path) {
   zf_log_set_output_v(ZF_LOG_PUT_STD, 0, file_output_callback);
 }
 
+#include "terminal.h"
+
+struct console_details console;
+
 /**
  * Display a repr of the given string.
  *
@@ -215,847 +218,7 @@ int string_insert(char *buffer, int max_length, int pos, const char *insert) {
 
 // Should this be passed around to the functions that use it?
 
-struct render {
-  int speed;
-  int effect;
-} current_render;
-
-int render_overlimit = 0;
-
-void reset_render(void) {
-  current_render.speed = 0;
-  current_render.effect = 0;
-  render_overlimit = 0;
-}
-
-#define TRIGGER "^"
-// Max limit we'll sleep before ignoring effects/speed.
-#define SLEEP_LIMIT 30
-
-int ms_sleep(unsigned int ms) {
-  int result = 0;
-  struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
-
-  do {
-    struct timespec ts_sleep = ts;
-    result = nanosleep(&ts_sleep, &ts);
-  } while ((-1 == result));
-  return result;
-}
-
-void render_sleep(void) {
-  if (render_overlimit)
-    return;
-
-  if (current_render.speed) { // * 100 still too slow.
-    ms_sleep(current_render.speed * 10);
-  }
-}
-
-/*
-Terminal tracking
-
-Actually, I believe I only really need to track the color information.
-Everything else, I'm not sure I really care about.
-
- */
-
-struct console_details {
-  int posx, posy;
-  int savedx, savedy;
-  char ansi[20]; // current ANSI command being processed.
-  int in_ansi;
-  int fgcolor; // 0-7 // not 0-15
-  int bgcolor; // 0-7
-  int status;  // 0, 1 or 5 (Blink)
-} console;
-
-void console_init(struct console_details *cdp) {
-  cdp->posx = 0;
-  cdp->posy = 0;
-  cdp->savedx = 0;
-  cdp->savedy = 0;
-  cdp->in_ansi = 0;
-  cdp->fgcolor = 7;
-  cdp->bgcolor = 0;
-  cdp->status = 0;
-}
-
-void ansi_color(struct console_details *cdp, int color) {
-  // ZF_LOGV("ansi_color(%d)", color);
-  if (color == 0) {
-    cdp->status = 0;
-    cdp->fgcolor = 7;
-    cdp->bgcolor = 0;
-    return;
-  }
-  if (color == 1) {
-    cdp->status = 1;
-    return;
-  }
-  if (color == 5) {
-    cdp->status = 5;
-    return;
-  }
-  if ((color >= 30) && (color <= 37)) {
-    cdp->fgcolor = color - 30;
-    return;
-  }
-  if ((color >= 40) && (color <= 47)) {
-    cdp->bgcolor = color - 40;
-    return;
-  }
-  ZF_LOGD("ansi_color( %d ) is unknown to me.", color);
-}
-
-const char *color_restore(struct console_details *cdp) {
-  static char buffer[30];
-  if (cdp->status == 0) {
-    sprintf(buffer, "\x1b[0;3%d;4%dm", cdp->fgcolor, cdp->bgcolor);
-  } else {
-    sprintf(buffer, "\x1b[0;%d;3%d;4%dm", cdp->status, cdp->fgcolor,
-            cdp->bgcolor);
-  };
-  return buffer;
-}
-
-void console_ansi(struct console_details *cdp, const char *ansi) {
-  int understood = 0;
-  const char *cp = ansi;
-  const char *last = ansi + strlen(ansi) - 1;
-  int number, number2;
-
-  if (*cp == '\x1b') {
-    cp++;
-    // Ok, that's expected.
-    if (*cp == '[') {
-      cp++;
-
-      // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
-
-      switch (*last) {
-      case 'A':
-        // cursor up
-        if (cp == last) {
-          number = 1;
-        } else {
-          number = atoi(cp);
-          if (number < 1) {
-            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
-                    number);
-            number = 1;
-          }
-        };
-        cdp->posy -= number;
-        if (cdp->posy < 0) {
-          cdp->posy = 0;
-          ZF_LOGD(
-              "console_ansi( %s ): attempt to move above top of screen (%d)",
-              repr(ansi), number);
-        }
-        understood = 1;
-        return;
-      case 'B':
-        // cursor down
-        if (cp == last) {
-          number = 1;
-        } else {
-          number = atoi(cp);
-          if (number < 1) {
-            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
-                    number);
-            number = 1;
-          }
-        };
-        cdp->posy += number;
-        // check range/"scroll"
-        understood = 1;
-        return;
-
-      case 'C':
-        // cursor forward
-        if (cp == last) {
-          number = 1;
-        } else {
-          number = atoi(cp);
-          if (number < 1) {
-            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
-                    number);
-            number = 1;
-          }
-        };
-        cdp->posx += number;
-
-        // Well.  According to the "spec", the screen limits are hard
-        // If the cursor is already at the edge of the screen, this has no
-        // effect. ^ This is *NOT* how any ANSI BBS terminal program acts!
-
-        while (cdp->posx > 79) {
-          cdp->posy++;
-          // check range/"scroll"
-          cdp->posx -= 79;
-        }
-        understood = 1;
-        return;
-
-      case 'D':
-        // cursor backwards
-        if (cp == last) {
-          number = 1;
-        } else {
-          number = atoi(cp);
-          if (number < 1) {
-            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
-                    number);
-            number = 0;
-          }
-        };
-        cdp->posx -= number;
-
-        // Well.  According to the "spec", the screen limits are hard
-        // If the cursor is already at the edge of the screen, this has no
-        // effect. ^ This is *NOT* how any ANSI BBS terminal program acts!
-
-        while (cdp->posx < 0) {
-          cdp->posy--;
-          if (cdp->posy < 0) {
-            cdp->posy = 0;
-          }
-          cdp->posx += 79;
-        }
-        understood = 1;
-        return;
-
-      case 'H':
-        // cursor position
-
-        if (*cp == ';') {
-          // Missing first number
-          number = 1;
-          cp++;
-          if (cp == last) {
-            // missing 2nd number as well?
-            number2 = 1;
-          } else {
-            number2 = atoi(cp);
-          }
-        } else {
-          // Ok, find the first number
-          number = atoi(cp);
-
-          cp = strchr(cp, ';');
-          if (cp == NULL) {
-            // Missing 2nd number
-            number2 = 1;
-          } else {
-            // 2nd number found, maybe.
-            cp++;
-            if (cp == last) {
-              number2 = 1;
-            } else {
-              number2 = atoi(cp);
-            }
-          }
-        }
-
-        // Our positions start at zero, not one.
-        cdp->posx = number - 1;
-        cdp->posy = number2 - 1;
-
-        understood = 1;
-        break;
-
-      case 'J':
-        // clear
-        if (cp == last) {
-          number = 0;
-        } else {
-          number = atoi(cp);
-        };
-
-        // clears ... part of the screen.
-        if (number == 2) {
-          cdp->posx = 0;
-          cdp->posy = 0;
-        };
-        understood = 1;
-        break;
-
-      case 'm':
-        // color
-        if (cp == last) {
-          // nothing given, default to 0.
-          number = 0;
-          ansi_color(cdp, number);
-        } else {
-          while (cp != last) {
-            number = atoi(cp);
-            ansi_color(cdp, number);
-            cp++;
-
-            while ((cp != last) && (isdigit(*cp))) {
-              cp++;
-            };
-
-            if (cp != last) {
-              if (*cp == ';') {
-                cp++;
-              }
-            }
-          }
-        }
-        understood = 1;
-        break;
-
-      case 't':
-      case 'r':
-      case 'h':
-      case '!':
-        // These are ones that I don't care about.
-      case 'n': // This is terminal detect -- give me cursor position
-        understood = 1;
-        break;
-
-      default:
-        // unsure -- possibly not important
-        ZF_LOGD("console_ansi( %s ): ???", repr(ansi));
-        understood = 0;
-      }
-    }
-  };
-
-  if (!understood) {
-    ZF_LOGD("console_ansi( %s ): was not understood.", repr(ansi));
-  }
-}
-
-/*
- * console_char()
- *  return whether or not we are still in_ansi
- */
-int console_char(struct console_details *cdp, char ch) {
-  char *cp;
-
-  if (cdp->in_ansi) {
-    // Ok, append this char
-    cp = cdp->ansi + strlen(cdp->ansi);
-    *cp = ch;
-    cp++;
-    *cp = 0;
-    if (isalpha(ch)) {
-      // Ok!
-      console_ansi(cdp, cdp->ansi);
-      cdp->in_ansi = 0;
-      cdp->ansi[0] = 0;
-      return 1;
-    }
-    return 1;
-  } else {
-    if (ch == '\x1b') {
-      cp = cdp->ansi;
-      *cp = ch;
-      cp++;
-      *cp = 0;
-      cdp->in_ansi = 1;
-      return 1;
-    }
-    if (ch == '\r') {
-      // Carriage return
-      cdp->posx = 0;
-      return 0;
-    }
-    if (ch == '\n') {
-      cdp->posy++;
-      // check range/"scroll"
-      return 0;
-    }
-    if (ch == '\b') {
-      // Backspace.
-      if (cdp->posx > 0) {
-        cdp->posx--;
-      }
-      return 0;
-    }
-    if (ch == '\f') {
-      // form feed
-      // treat as clear screen
-      cdp->posx = 0;
-      cdp->posy = 0;
-      return 0;
-    }
-
-    /*
-      I don't believe that anything else can possibly be here, other then an
-      actual printable character.  So!
-    */
-
-    cdp->posx++;
-
-    if (cdp->posx > 79) {
-      cdp->posx = 0;
-      cdp->posy++;
-      // check range/"scroll"
-    }
-    return 0;
-  }
-}
-
-void console_string(struct console_details *cdp, const char *chars) {
-  int x;
-  for (x = 0; x < strlen(chars); x++) {
-    console_char(cdp, chars[x]);
-  }
-}
-
-void console_receive(struct console_details *cdp, const char *chars, int len) {
-  int x;
-
-  for (x = 0; x < len; x++) {
-    console_char(cdp, chars[x]);
-  }
-}
-
-/*
-Well SNAP!  Mystic numbers don't remotely match ANSI color codes.
-
- 00 : Sets the current foreground to Black          0;30
- 01 : Sets the current foreground to Dark Blue      0;34
- 02 : Sets the current foreground to Dark Green     0;32
- 03 : Sets the current foreground to Dark Cyan      0;36
- 04 : Sets the current foreground to Dark Red       0;31
- 05 : Sets the current foreground to Dark Magenta   0;35
- 06 : Sets the current foreground to Brown          0;33
- 07 : Sets the current foreground to Grey           0;37
- 08 : Sets the current foreground to Dark Grey      1;30
- 09 : Sets the current foreground to Light Blue     1;34
- 10 : Sets the current foreground to Light Green    1;32
- 11 : Sets the current foreground to Light Cyan     1;36
- 12 : Sets the current foreground to Light Red      1;31
- 13 : Sets the current foreground to Light Magenta  1;35
- 14 : Sets the current foreground to Yellow         1;33
- 15 : Sets the current foreground to White          1;37
-
- 16 : Sets the current background to Black          40
- 17 : Sets the current background to Blue           44
- 18 : Sets the current background to Green          42
- 19 : Sets the current background to Cyan           46
- 20 : Sets the current background to Red            41
- 21 : Sets the current background to Magenta        45
- 22 : Sets the current background to Brown          43
- 23 : Sets the current background to Grey           47
-
- 24 : Sets the current background to black with blinking foreground     5;40
- 25 : Sets the current background to blue with blinking foreground      5;44
- 26 : Sets the current background to green with blinking foreground     5;42
- 27 : Sets the current background to cyan with blinking foreground      5;46
- 28 : Sets the current background to red with blinking foreground       5;41
- 29 : Sets the current background to magenta with blinking foreground   5;45
- 30 : Sets the current background to brown with blinking foreground     5;43
- 31 : Sets the current background to grey with blinking foreground      5;47
-
-Other things that Mystic does ...
-
-  [A## - Move the cursor up ## lines
-  [B## - Move the cursor down ## lines
-  [C## - Move the cursor forward (to the right) ## columns
-  [D## - Move the cursor backwards (to the left) ## columns
-  [K   - Clear from the current cursor position to the end of the line
-  [L   - Move cursor and erase data backwards from current column to column ##
-  [X## - Move cursor to X coordinate ##
-  [Y## - Move cursor to Y coordinate ##
-  BS   - Sends 1 destructive backspace sequence (ASCII 8-32-8)
-  CL   - Clears the screen (ANSI 1,1 locate and [2J or ASCII 12)
-  CR   - Send a carrage return and line feed (move to next line)
-  RA   - Restore the saved text attribute color
-  RS   - Restore the saved user's terminal screen
-  SA   - Save the current text attribute color
-  SS   - Save the entire user's terminal screen
-
- */
-
-// Covert MYSTIC color to (Proper) ANSI COLOR.
-
-const int MYSTIC[] = {0, 4, 2, 6, 1, 5, 3, 7};
-
-// ANSI_color = MYSTIC[ odd_mystic_color % 8 ]
-
-void write_color(int fd, int color) {
-  char buffer[12];
-
-  switch (color) {
-  case 0:
-  case 1:
-  case 2:
-  case 3:
-  case 4:
-  case 5:
-  case 6:
-  case 7:
-    sprintf(buffer, "\x1b[0;3%dm", MYSTIC[color]);
-    break;
-  case 8:
-  case 9:
-  case 10:
-  case 11:
-  case 12:
-  case 13:
-  case 14:
-  case 15:
-    sprintf(buffer, "\x1b[0;1;3%dm", MYSTIC[color - 8]);
-    break;
-  case 16:
-  case 17:
-  case 18:
-  case 19:
-  case 20:
-  case 21:
-  case 22:
-  case 23:
-    sprintf(buffer, "\x1b[4%dm", MYSTIC[color - 16]);
-    break;
-  case 24:
-  case 25:
-  case 26:
-  case 27:
-  case 28:
-  case 29:
-  case 30:
-  case 31:
-    sprintf(buffer, "\x1b[5;4%dm", MYSTIC[color - 24]);
-    break;
-  default:
-    buffer[0] = 0;
-    break;
-  }
-  ZF_LOGD("write_color( %d ): %s", color, repr(buffer));
-  write(fd, buffer, strlen(buffer));
-}
-
-int send_file(int fd, char *filename) {
-  FILE *fp;
-  char buffer[100];
-  int read;
-
-  fp = fopen(filename, "rb");
-  if (fp == NULL) {
-    ZF_LOGD("Failed to open %s", filename);
-    return 0;
-  }
-
-  while ((read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
-    write(fd, buffer, read);
-  };
-  fclose(fp);
-  return 1;
-}
-
-void send_goto(int fd, int x, int y) {
-  char gbuffer[16];
-
-  sprintf(gbuffer, "\x1b[%d;%dH", y, x);
-  write(fd, gbuffer, strlen(gbuffer));
-}
-
-int send_file(int fd, int x, int y, char *filename) {
-  FILE *fp;
-  char buffer[100];
-  int read;
-
-  fp = fopen(filename, "rb");
-  if (fp == NULL) {
-    ZF_LOGD("Failed to open %s", filename);
-    return 0;
-  }
-
-  send_goto(fd, x, y);
-  y++;
-
-  while ((read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
-    char *cp, *last_cp;
-
-    buffer[read] = 0;
-    last_cp = buffer;
-    while ((cp = strchr(last_cp, '\n')) != NULL) {
-      *cp = 0;
-      write(fd, last_cp, strlen(last_cp));
-      send_goto(fd, x, y);
-      y++;
-      last_cp = cp + 1;
-    };
-    write(fd, last_cp, strlen(last_cp));
-  };
-  fclose(fp);
-  return 1;
-}
-
-/**
- * process_trigger( fd, *cp )
- *
- * This process a command trigger.
- * It has seen TRIGGER, and now it is
- * processing whatever comes after it.
- * It will perform the process, and
- * return the char * of whatever is next.
- */
-const char *process_trigger(int fd, const char *cp) {
-  char ch;
-  int i, x, y;
-  ch = *cp;
-  cp++;
-
-  switch (ch) {
-  case 'D':
-    i = 0;
-    if (isdigit(*cp)) {
-      i = (*cp) - '0';
-      cp++;
-    };
-    if (isdigit(*cp)) {
-      i *= 10;
-      i += (*cp) - '0';
-      cp++;
-    };
-
-    if ((i > 0) && (i < 80)) {
-      ZF_LOGI("DEL %02d", i);
-
-      for (x = 0; x < i; x++) {
-        write(fd, "\b \b", 3);
-      }
-    };
-    break;
-
-  case 'C': {
-    i = 0;
-    if (*cp == 'R') {
-      cp++;
-      const char *restore = color_restore(&console);
-      write(fd, restore, strlen(restore));
-      break;
-    }
-    if (isdigit(*cp)) {
-      i = (*cp) - '0';
-      cp++;
-    };
-    if (isdigit(*cp)) {
-      i *= 10;
-      i += (*cp) - '0';
-      cp++;
-    };
-    write_color(fd, i);
-  } break;
-
-  case 'F':
-  case 'f': {
-    int pos = 0;
-    if (ch == 'f') {
-      pos = 1;
-      x = (*cp) - '0';
-      cp++;
-      x *= 10;
-      x += (*cp) - '0';
-      cp++;
-      y = (*cp) - '0';
-      cp++;
-      y *= 10;
-      y += (*cp) - '0';
-      cp++;
-      ZF_LOGI("file at (%d, %d)", x, y);
-    }
-    // Ok, look for filename
-    char ansifile[32] = "./hh/";
-    char *ap = ansifile + strlen(ansifile);
-    while (*cp != '.') {
-      *ap = *cp;
-      ap++;
-      *ap = 0;
-      cp++;
-    };
-    strcat(ansifile, ".ans");
-    cp++;
-    ZF_LOGD("FILE [%s]", ansifile);
-    if (pos) {
-      send_file(fd, x, y, ansifile);
-    } else {
-      send_file(fd, ansifile);
-    };
-  } break;
-
-  case 'G': {
-    x = 0;
-    if (isdigit(*cp)) {
-      x = (*cp) - '0';
-      cp++;
-    };
-    if (isdigit(*cp)) {
-      x *= 10;
-      x += (*cp) - '0';
-      cp++;
-    };
-    y = 0;
-    if (isdigit(*cp)) {
-      y = (*cp) - '0';
-      cp++;
-    };
-    if (isdigit(*cp)) {
-      y *= 10;
-      y += (*cp) - '0';
-      cp++;
-    };
-    char buffer[20]; // row ; column H
-    ZF_LOGD("GOTO (%d,%d)", x, y);
-    sprintf(buffer, "\x1b[%d;%dH", y, x);
-    write(fd, buffer, strlen(buffer));
-  } break;
-
-  case 'R': {
-    i = 0;
-    if (isdigit(*cp)) {
-      i = (*cp) - '0';
-      cp++;
-    };
-    if ((i > 0) && (i < 10)) {
-      ZF_LOGI("RENDER %d", i);
-      current_render.effect = i;
-    } else {
-      current_render.effect = 0;
-    }
-  } break;
-  case 'S': {
-    i = 0;
-    if (isdigit(*cp)) {
-      i = (*cp) - '0';
-      cp++;
-    };
-    if ((i > 0) && (i < 10)) {
-      ZF_LOGI("SPEED %d", i);
-      current_render.speed = i;
-    } else {
-      current_render.speed = 0;
-    }
-  } break;
-  case 'P': {
-    i = 0;
-    if (isdigit(*cp)) {
-      i = (*cp) - '0';
-      cp++;
-    };
-    if ((i > 0) && (i < 10)) {
-      ZF_LOGI("PAWS %d", i);
-      // sleep(i);
-      if (!render_overlimit) {
-        sleep(i);
-      };
-    }
-  } break;
-  }
-  return cp;
-}
-
-/**
- * render_effect( fd, ch )
- *
- * Displays the given character with whatever
- * rendering effect is currently active.
- * (If any).
- */
-void render_effect(int fd, char ch) {
-  int effect = current_render.effect;
-  int l;
-  char space = ' ';
-  char bs = '\b';
-
-  switch (effect) {
-  case 1:
-    // CHAR + SPC + BS
-    render_sleep();
-    write(fd, &ch, 1);
-    render_sleep();
-    write(fd, &space, 1);
-    render_sleep();
-    render_sleep();
-    write(fd, &bs, 1);
-    break;
-  case 2:
-    // CHAR + 8 spaces + 8 BS
-    render_sleep();
-    write(fd, &ch, 1);
-    for (l = 0; l < 8; l++) {
-      render_sleep();
-      write(fd, &space, 1);
-    }
-    for (l = 0; l < 8; l++) {
-      render_sleep();
-      write(fd, &bs, 1);
-    }
-    break;
-  case 0:
-  default:
-    // NORMAL
-    render_sleep();
-    write(fd, &ch, 1);
-    break;
-  }
-}
-
-/**
- * render( fd, string_out )
- *
- * Render an entire string.
- * Handles TRIGGER.
- * Renders with effects.
- */
-void render(int fd, const char *string_out, int len) {
-  const char *cp = string_out;
-  const char *trigger = cp;
-
-  time_t start = time(NULL);
-  int elapsed;
-  int over = 0;
-
-  reset_render();
-
-  // ZF_LOGV("render(%d, %s)", fd, repr(string_out));
-  ZF_LOGV_MEM(string_out, len, "render(%d, %d bytes):", fd, len);
-
-  // Check our time from time to time.
-  // If we start running long, disable sleeps.
-
-  while ((trigger = strnstr(cp, len - (cp - trigger), TRIGGER)) != NULL) {
-    // There is special things to handle in here.
-    while (cp != trigger) {
-      elapsed = time(NULL) - start;
-      if (elapsed > SLEEP_LIMIT) {
-        render_overlimit = 1;
-        current_render.speed = 0;
-      };
-
-      // write(fd, cp, 1 );
-      render_effect(fd, *cp);
-      cp++;
-    };
-
-    // ZF_LOGI( "at trigger: (%s)", cp);
-    cp += strlen(TRIGGER);
-
-    // Ok, we're pointing at the trigger -- do something.
-    cp = process_trigger(fd, cp);
-
-    // ZF_LOGI( "after trigger: (%s)", cp);
-  };
-
-  // We still might be under a rendering effect.
-  while (cp < (string_out + len)) {
-    elapsed = time(NULL) - start;
-    if (elapsed > SLEEP_LIMIT) {
-      render_overlimit = 1;
-      current_render.speed = 0;
-    };
-    // write(fd, cp, 1);
-    render_effect(fd, *cp);
-    cp++;
-  }
-}
+#include "render.h"
 
 /*
     These are harry "timeout" events.
@@ -1670,6 +833,18 @@ int mangle(int fd, const char *buffer, int len) {
    W\x1[0;34mORDS with color changes in them, and work with them.
    without screwing up the color changes, of course.  :P
 
+  Example:
+
+   Y\x1b[0;1mes \x1b[0;1;34m\x1b[0;1;34;44m N\x1b[0;1;44mo
+   Yes No
+   ^ This would be a job for a crazy regex.
+
+   I'd have to map the characters to positions in the buffer.  :S
+   I'd want mangle and wrangle to work.
+
+   The Message menu -- doesn't hardly get mangled at all (at least on
+   my test site).  Because all of the color changes break up the 
+   words in the menu.
    */
   x = rx_match(&WORDS, work);
   ZF_LOGD("found %d word groups", x);

+ 485 - 0
render.cpp

@@ -0,0 +1,485 @@
+#include <unistd.h> // usleep
+#include <stdio.h>
+#include <time.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "render.h"
+#include "terminal.h"
+#include "zf_log.h"
+
+extern const char *strnstr(const char *source, int len, const char *needle);
+extern const char *repr(const char *data);
+extern struct console_details console;
+
+struct render current_render;
+int render_overlimit = 0;
+
+void reset_render(void) {
+  current_render.speed = 0;
+  current_render.effect = 0;
+  render_overlimit = 0;
+}
+
+int ms_sleep(unsigned int ms) {
+  int result = 0;
+  struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
+
+  do {
+    struct timespec ts_sleep = ts;
+    result = nanosleep(&ts_sleep, &ts);
+  } while ((-1 == result));
+  return result;
+}
+
+void render_sleep(void) {
+  if (render_overlimit)
+    return;
+
+  if (current_render.speed) { // * 100 still too slow.
+    ms_sleep(current_render.speed * 10);
+  }
+}
+
+
+/*
+Well SNAP!  Mystic numbers don't remotely match ANSI color codes.
+
+ 00 : Sets the current foreground to Black          0;30
+ 01 : Sets the current foreground to Dark Blue      0;34
+ 02 : Sets the current foreground to Dark Green     0;32
+ 03 : Sets the current foreground to Dark Cyan      0;36
+ 04 : Sets the current foreground to Dark Red       0;31
+ 05 : Sets the current foreground to Dark Magenta   0;35
+ 06 : Sets the current foreground to Brown          0;33
+ 07 : Sets the current foreground to Grey           0;37
+ 08 : Sets the current foreground to Dark Grey      1;30
+ 09 : Sets the current foreground to Light Blue     1;34
+ 10 : Sets the current foreground to Light Green    1;32
+ 11 : Sets the current foreground to Light Cyan     1;36
+ 12 : Sets the current foreground to Light Red      1;31
+ 13 : Sets the current foreground to Light Magenta  1;35
+ 14 : Sets the current foreground to Yellow         1;33
+ 15 : Sets the current foreground to White          1;37
+
+ 16 : Sets the current background to Black          40
+ 17 : Sets the current background to Blue           44
+ 18 : Sets the current background to Green          42
+ 19 : Sets the current background to Cyan           46
+ 20 : Sets the current background to Red            41
+ 21 : Sets the current background to Magenta        45
+ 22 : Sets the current background to Brown          43
+ 23 : Sets the current background to Grey           47
+
+ 24 : Sets the current background to black with blinking foreground     5;40
+ 25 : Sets the current background to blue with blinking foreground      5;44
+ 26 : Sets the current background to green with blinking foreground     5;42
+ 27 : Sets the current background to cyan with blinking foreground      5;46
+ 28 : Sets the current background to red with blinking foreground       5;41
+ 29 : Sets the current background to magenta with blinking foreground   5;45
+ 30 : Sets the current background to brown with blinking foreground     5;43
+ 31 : Sets the current background to grey with blinking foreground      5;47
+
+Other things that Mystic does ...
+
+  [A## - Move the cursor up ## lines
+  [B## - Move the cursor down ## lines
+  [C## - Move the cursor forward (to the right) ## columns
+  [D## - Move the cursor backwards (to the left) ## columns
+  [K   - Clear from the current cursor position to the end of the line
+  [L   - Move cursor and erase data backwards from current column to column ##
+  [X## - Move cursor to X coordinate ##
+  [Y## - Move cursor to Y coordinate ##
+  BS   - Sends 1 destructive backspace sequence (ASCII 8-32-8)
+  CL   - Clears the screen (ANSI 1,1 locate and [2J or ASCII 12)
+  CR   - Send a carrage return and line feed (move to next line)
+  RA   - Restore the saved text attribute color
+  RS   - Restore the saved user's terminal screen
+  SA   - Save the current text attribute color
+  SS   - Save the entire user's terminal screen
+
+ */
+
+// Covert MYSTIC color to (Proper) ANSI COLOR.
+
+const int MYSTIC[] = {0, 4, 2, 6, 1, 5, 3, 7};
+
+// ANSI_color = MYSTIC[ odd_mystic_color % 8 ]
+
+void write_color(int fd, int color) {
+  char buffer[12];
+
+  switch (color) {
+  case 0:
+  case 1:
+  case 2:
+  case 3:
+  case 4:
+  case 5:
+  case 6:
+  case 7:
+    sprintf(buffer, "\x1b[0;3%dm", MYSTIC[color]);
+    break;
+  case 8:
+  case 9:
+  case 10:
+  case 11:
+  case 12:
+  case 13:
+  case 14:
+  case 15:
+    sprintf(buffer, "\x1b[0;1;3%dm", MYSTIC[color - 8]);
+    break;
+  case 16:
+  case 17:
+  case 18:
+  case 19:
+  case 20:
+  case 21:
+  case 22:
+  case 23:
+    sprintf(buffer, "\x1b[4%dm", MYSTIC[color - 16]);
+    break;
+  case 24:
+  case 25:
+  case 26:
+  case 27:
+  case 28:
+  case 29:
+  case 30:
+  case 31:
+    sprintf(buffer, "\x1b[5;4%dm", MYSTIC[color - 24]);
+    break;
+  default:
+    buffer[0] = 0;
+    break;
+  }
+  ZF_LOGD("write_color( %d ): %s", color, repr(buffer));
+  write(fd, buffer, strlen(buffer));
+}
+
+int send_file(int fd, char *filename) {
+  FILE *fp;
+  char buffer[100];
+  int read;
+
+  fp = fopen(filename, "rb");
+  if (fp == NULL) {
+    ZF_LOGD("Failed to open %s", filename);
+    return 0;
+  }
+
+  while ((read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
+    write(fd, buffer, read);
+  };
+  fclose(fp);
+  return 1;
+}
+
+void send_goto(int fd, int x, int y) {
+  char gbuffer[16];
+
+  sprintf(gbuffer, "\x1b[%d;%dH", y, x);
+  write(fd, gbuffer, strlen(gbuffer));
+}
+
+int send_file(int fd, int x, int y, char *filename) {
+  FILE *fp;
+  char buffer[100];
+  int read;
+
+  fp = fopen(filename, "rb");
+  if (fp == NULL) {
+    ZF_LOGD("Failed to open %s", filename);
+    return 0;
+  }
+
+  send_goto(fd, x, y);
+  y++;
+
+  while ((read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
+    char *cp, *last_cp;
+
+    buffer[read] = 0;
+    last_cp = buffer;
+    while ((cp = strchr(last_cp, '\n')) != NULL) {
+      *cp = 0;
+      write(fd, last_cp, strlen(last_cp));
+      send_goto(fd, x, y);
+      y++;
+      last_cp = cp + 1;
+    };
+    write(fd, last_cp, strlen(last_cp));
+  };
+  fclose(fp);
+  return 1;
+}
+
+/**
+ * process_trigger( fd, *cp )
+ *
+ * This process a command trigger.
+ * It has seen TRIGGER, and now it is
+ * processing whatever comes after it.
+ * It will perform the process, and
+ * return the char * of whatever is next.
+ */
+const char *process_trigger(int fd, const char *cp) {
+  char ch;
+  int i, x, y;
+  ch = *cp;
+  cp++;
+
+  switch (ch) {
+  case 'D':
+    i = 0;
+    if (isdigit(*cp)) {
+      i = (*cp) - '0';
+      cp++;
+    };
+    if (isdigit(*cp)) {
+      i *= 10;
+      i += (*cp) - '0';
+      cp++;
+    };
+
+    if ((i > 0) && (i < 80)) {
+      ZF_LOGI("DEL %02d", i);
+
+      for (x = 0; x < i; x++) {
+        write(fd, "\b \b", 3);
+      }
+    };
+    break;
+
+  case 'C': {
+    i = 0;
+    if (*cp == 'R') {
+      cp++;
+      const char *restore = color_restore(&console);
+      write(fd, restore, strlen(restore));
+      break;
+    }
+    if (isdigit(*cp)) {
+      i = (*cp) - '0';
+      cp++;
+    };
+    if (isdigit(*cp)) {
+      i *= 10;
+      i += (*cp) - '0';
+      cp++;
+    };
+    write_color(fd, i);
+  } break;
+
+  case 'F':
+  case 'f': {
+    int pos = 0;
+    if (ch == 'f') {
+      pos = 1;
+      x = (*cp) - '0';
+      cp++;
+      x *= 10;
+      x += (*cp) - '0';
+      cp++;
+      y = (*cp) - '0';
+      cp++;
+      y *= 10;
+      y += (*cp) - '0';
+      cp++;
+      ZF_LOGI("file at (%d, %d)", x, y);
+    }
+    // Ok, look for filename
+    char ansifile[32] = "./hh/";
+    char *ap = ansifile + strlen(ansifile);
+    while (*cp != '.') {
+      *ap = *cp;
+      ap++;
+      *ap = 0;
+      cp++;
+    };
+    strcat(ansifile, ".ans");
+    cp++;
+    ZF_LOGD("FILE [%s]", ansifile);
+    if (pos) {
+      send_file(fd, x, y, ansifile);
+    } else {
+      send_file(fd, ansifile);
+    };
+  } break;
+
+  case 'G': {
+    x = 0;
+    if (isdigit(*cp)) {
+      x = (*cp) - '0';
+      cp++;
+    };
+    if (isdigit(*cp)) {
+      x *= 10;
+      x += (*cp) - '0';
+      cp++;
+    };
+    y = 0;
+    if (isdigit(*cp)) {
+      y = (*cp) - '0';
+      cp++;
+    };
+    if (isdigit(*cp)) {
+      y *= 10;
+      y += (*cp) - '0';
+      cp++;
+    };
+    char buffer[20]; // row ; column H
+    ZF_LOGD("GOTO (%d,%d)", x, y);
+    sprintf(buffer, "\x1b[%d;%dH", y, x);
+    write(fd, buffer, strlen(buffer));
+  } break;
+
+  case 'R': {
+    i = 0;
+    if (isdigit(*cp)) {
+      i = (*cp) - '0';
+      cp++;
+    };
+    if ((i > 0) && (i < 10)) {
+      ZF_LOGI("RENDER %d", i);
+      current_render.effect = i;
+    } else {
+      current_render.effect = 0;
+    }
+  } break;
+  case 'S': {
+    i = 0;
+    if (isdigit(*cp)) {
+      i = (*cp) - '0';
+      cp++;
+    };
+    if ((i > 0) && (i < 10)) {
+      ZF_LOGI("SPEED %d", i);
+      current_render.speed = i;
+    } else {
+      current_render.speed = 0;
+    }
+  } break;
+  case 'P': {
+    i = 0;
+    if (isdigit(*cp)) {
+      i = (*cp) - '0';
+      cp++;
+    };
+    if ((i > 0) && (i < 10)) {
+      ZF_LOGI("PAWS %d", i);
+      // sleep(i);
+      if (!render_overlimit) {
+        sleep(i);
+      };
+    }
+  } break;
+  }
+  return cp;
+}
+
+/**
+ * render_effect( fd, ch )
+ *
+ * Displays the given character with whatever
+ * rendering effect is currently active.
+ * (If any).
+ */
+void render_effect(int fd, char ch) {
+  int effect = current_render.effect;
+  int l;
+  char space = ' ';
+  char bs = '\b';
+
+  switch (effect) {
+  case 1:
+    // CHAR + SPC + BS
+    render_sleep();
+    write(fd, &ch, 1);
+    render_sleep();
+    write(fd, &space, 1);
+    render_sleep();
+    render_sleep();
+    write(fd, &bs, 1);
+    break;
+  case 2:
+    // CHAR + 8 spaces + 8 BS
+    render_sleep();
+    write(fd, &ch, 1);
+    for (l = 0; l < 8; l++) {
+      render_sleep();
+      write(fd, &space, 1);
+    }
+    for (l = 0; l < 8; l++) {
+      render_sleep();
+      write(fd, &bs, 1);
+    }
+    break;
+  case 0:
+  default:
+    // NORMAL
+    render_sleep();
+    write(fd, &ch, 1);
+    break;
+  }
+}
+
+/**
+ * render( fd, string_out )
+ *
+ * Render an entire string.
+ * Handles TRIGGER.
+ * Renders with effects.
+ */
+void render(int fd, const char *string_out, int len) {
+  const char *cp = string_out;
+  const char *trigger = cp;
+
+  time_t start = time(NULL);
+  int elapsed;
+  int over = 0;
+
+  reset_render();
+
+  // ZF_LOGV("render(%d, %s)", fd, repr(string_out));
+  ZF_LOGV_MEM(string_out, len, "render(%d, %d bytes):", fd, len);
+
+  // Check our time from time to time.
+  // If we start running long, disable sleeps.
+
+  while ((trigger = strnstr(cp, len - (cp - trigger), TRIGGER)) != NULL) {
+    // There is special things to handle in here.
+    while (cp != trigger) {
+      elapsed = time(NULL) - start;
+      if (elapsed > SLEEP_LIMIT) {
+        render_overlimit = 1;
+        current_render.speed = 0;
+      };
+
+      // write(fd, cp, 1 );
+      render_effect(fd, *cp);
+      cp++;
+    };
+
+    // ZF_LOGI( "at trigger: (%s)", cp);
+    cp += strlen(TRIGGER);
+
+    // Ok, we're pointing at the trigger -- do something.
+    cp = process_trigger(fd, cp);
+
+    // ZF_LOGI( "after trigger: (%s)", cp);
+  };
+
+  // We still might be under a rendering effect.
+  while (cp < (string_out + len)) {
+    elapsed = time(NULL) - start;
+    if (elapsed > SLEEP_LIMIT) {
+      render_overlimit = 1;
+      current_render.speed = 0;
+    };
+    // write(fd, cp, 1);
+    render_effect(fd, *cp);
+    cp++;
+  }
+}

+ 25 - 0
render.h

@@ -0,0 +1,25 @@
+#ifndef RENDER_H
+#define RENDER_H
+
+struct render {
+  int speed;
+  int effect;
+};
+
+void reset_render(void);
+int ms_sleep(unsigned int ms);
+void render_sleep(void);
+
+void write_color(int fd, int color);
+int send_file(int fd, char *filename);
+void send_goto(int fd, int x, int y);
+int send_file(int fd, int x, int y, char *filename);
+const char *process_trigger(int fd, const char *cp);
+void render_effect(int fd, char ch);
+void render(int fd, const char *string_out, int len);
+
+#define TRIGGER "^"
+// Max limit we'll sleep before ignoring effects/speed.
+#define SLEEP_LIMIT 30
+
+#endif

+ 362 - 0
terminal.cpp

@@ -0,0 +1,362 @@
+/*
+Terminal tracking
+
+Actually, I believe I only really need to track the color information.
+Everything else, I'm not sure I really care about.
+
+ */
+
+#include <string.h>
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h> // sprintf
+
+#include "terminal.h"
+#include "zf_log.h"
+
+extern const char *repr(const char *data);
+
+void console_init(struct console_details *cdp) {
+  cdp->posx = 0;
+  cdp->posy = 0;
+  cdp->savedx = 0;
+  cdp->savedy = 0;
+  cdp->in_ansi = 0;
+  cdp->fgcolor = 7;
+  cdp->bgcolor = 0;
+  cdp->status = 0;
+}
+
+void ansi_color(struct console_details *cdp, int color) {
+  // ZF_LOGV("ansi_color(%d)", color);
+  if (color == 0) {
+    cdp->status = 0;
+    cdp->fgcolor = 7;
+    cdp->bgcolor = 0;
+    return;
+  }
+  if (color == 1) {
+    cdp->status = 1;
+    return;
+  }
+  if (color == 5) {
+    cdp->status = 5;
+    return;
+  }
+  if ((color >= 30) && (color <= 37)) {
+    cdp->fgcolor = color - 30;
+    return;
+  }
+  if ((color >= 40) && (color <= 47)) {
+    cdp->bgcolor = color - 40;
+    return;
+  }
+  ZF_LOGD("ansi_color( %d ) is unknown to me.", color);
+}
+
+const char *color_restore(struct console_details *cdp) {
+  static char buffer[30];
+  if (cdp->status == 0) {
+    sprintf(buffer, "\x1b[0;3%d;4%dm", cdp->fgcolor, cdp->bgcolor);
+  } else {
+    sprintf(buffer, "\x1b[0;%d;3%d;4%dm", cdp->status, cdp->fgcolor,
+            cdp->bgcolor);
+  };
+  return buffer;
+}
+
+void console_ansi(struct console_details *cdp, const char *ansi) {
+  int understood = 0;
+  const char *cp = ansi;
+  const char *last = ansi + strlen(ansi) - 1;
+  int number, number2;
+
+  if (*cp == '\x1b') {
+    cp++;
+    // Ok, that's expected.
+    if (*cp == '[') {
+      cp++;
+
+      // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
+
+      switch (*last) {
+      case 'A':
+        // cursor up
+        if (cp == last) {
+          number = 1;
+        } else {
+          number = atoi(cp);
+          if (number < 1) {
+            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
+                    number);
+            number = 1;
+          }
+        };
+        cdp->posy -= number;
+        if (cdp->posy < 0) {
+          cdp->posy = 0;
+          ZF_LOGD(
+              "console_ansi( %s ): attempt to move above top of screen (%d)",
+              repr(ansi), number);
+        }
+        understood = 1;
+        return;
+      case 'B':
+        // cursor down
+        if (cp == last) {
+          number = 1;
+        } else {
+          number = atoi(cp);
+          if (number < 1) {
+            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
+                    number);
+            number = 1;
+          }
+        };
+        cdp->posy += number;
+        // check range/"scroll"
+        understood = 1;
+        return;
+
+      case 'C':
+        // cursor forward
+        if (cp == last) {
+          number = 1;
+        } else {
+          number = atoi(cp);
+          if (number < 1) {
+            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
+                    number);
+            number = 1;
+          }
+        };
+        cdp->posx += number;
+
+        // Well.  According to the "spec", the screen limits are hard
+        // If the cursor is already at the edge of the screen, this has no
+        // effect. ^ This is *NOT* how any ANSI BBS terminal program acts!
+
+        while (cdp->posx > 79) {
+          cdp->posy++;
+          // check range/"scroll"
+          cdp->posx -= 79;
+        }
+        understood = 1;
+        return;
+
+      case 'D':
+        // cursor backwards
+        if (cp == last) {
+          number = 1;
+        } else {
+          number = atoi(cp);
+          if (number < 1) {
+            ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi),
+                    number);
+            number = 0;
+          }
+        };
+        cdp->posx -= number;
+
+        // Well.  According to the "spec", the screen limits are hard
+        // If the cursor is already at the edge of the screen, this has no
+        // effect. ^ This is *NOT* how any ANSI BBS terminal program acts!
+
+        while (cdp->posx < 0) {
+          cdp->posy--;
+          if (cdp->posy < 0) {
+            cdp->posy = 0;
+          }
+          cdp->posx += 79;
+        }
+        understood = 1;
+        return;
+
+      case 'H':
+        // cursor position
+
+        if (*cp == ';') {
+          // Missing first number
+          number = 1;
+          cp++;
+          if (cp == last) {
+            // missing 2nd number as well?
+            number2 = 1;
+          } else {
+            number2 = atoi(cp);
+          }
+        } else {
+          // Ok, find the first number
+          number = atoi(cp);
+
+          cp = strchr(cp, ';');
+          if (cp == NULL) {
+            // Missing 2nd number
+            number2 = 1;
+          } else {
+            // 2nd number found, maybe.
+            cp++;
+            if (cp == last) {
+              number2 = 1;
+            } else {
+              number2 = atoi(cp);
+            }
+          }
+        }
+
+        // Our positions start at zero, not one.
+        cdp->posx = number - 1;
+        cdp->posy = number2 - 1;
+
+        understood = 1;
+        break;
+
+      case 'J':
+        // clear
+        if (cp == last) {
+          number = 0;
+        } else {
+          number = atoi(cp);
+        };
+
+        // clears ... part of the screen.
+        if (number == 2) {
+          cdp->posx = 0;
+          cdp->posy = 0;
+        };
+        understood = 1;
+        break;
+
+      case 'm':
+        // color
+        if (cp == last) {
+          // nothing given, default to 0.
+          number = 0;
+          ansi_color(cdp, number);
+        } else {
+          while (cp != last) {
+            number = atoi(cp);
+            ansi_color(cdp, number);
+            cp++;
+
+            while ((cp != last) && (isdigit(*cp))) {
+              cp++;
+            };
+
+            if (cp != last) {
+              if (*cp == ';') {
+                cp++;
+              }
+            }
+          }
+        }
+        understood = 1;
+        break;
+
+      case 't':
+      case 'r':
+      case 'h':
+      case '!':
+        // These are ones that I don't care about.
+      case 'n': // This is terminal detect -- give me cursor position
+        understood = 1;
+        break;
+
+      default:
+        // unsure -- possibly not important
+        ZF_LOGD("console_ansi( %s ): ???", repr(ansi));
+        understood = 0;
+      }
+    }
+  };
+
+  if (!understood) {
+    ZF_LOGD("console_ansi( %s ): was not understood.", repr(ansi));
+  }
+}
+
+/*
+ * console_char()
+ *  return whether or not we are still in_ansi
+ */
+int console_char(struct console_details *cdp, char ch) {
+  char *cp;
+
+  if (cdp->in_ansi) {
+    // Ok, append this char
+    cp = cdp->ansi + strlen(cdp->ansi);
+    *cp = ch;
+    cp++;
+    *cp = 0;
+    if (isalpha(ch)) {
+      // Ok!
+      console_ansi(cdp, cdp->ansi);
+      cdp->in_ansi = 0;
+      cdp->ansi[0] = 0;
+      return 1;
+    }
+    return 1;
+  } else {
+    if (ch == '\x1b') {
+      cp = cdp->ansi;
+      *cp = ch;
+      cp++;
+      *cp = 0;
+      cdp->in_ansi = 1;
+      return 1;
+    }
+    if (ch == '\r') {
+      // Carriage return
+      cdp->posx = 0;
+      return 0;
+    }
+    if (ch == '\n') {
+      cdp->posy++;
+      // check range/"scroll"
+      return 0;
+    }
+    if (ch == '\b') {
+      // Backspace.
+      if (cdp->posx > 0) {
+        cdp->posx--;
+      }
+      return 0;
+    }
+    if (ch == '\f') {
+      // form feed
+      // treat as clear screen
+      cdp->posx = 0;
+      cdp->posy = 0;
+      return 0;
+    }
+
+    /*
+      I don't believe that anything else can possibly be here, other then an
+      actual printable character.  So!
+    */
+
+    cdp->posx++;
+
+    if (cdp->posx > 79) {
+      cdp->posx = 0;
+      cdp->posy++;
+      // check range/"scroll"
+    }
+    return 0;
+  }
+}
+
+void console_string(struct console_details *cdp, const char *chars) {
+  int x;
+  for (x = 0; x < strlen(chars); x++) {
+    console_char(cdp, chars[x]);
+  }
+}
+
+void console_receive(struct console_details *cdp, const char *chars, int len) {
+  int x;
+
+  for (x = 0; x < len; x++) {
+    console_char(cdp, chars[x]);
+  }
+}

+ 24 - 0
terminal.h

@@ -0,0 +1,24 @@
+#ifndef TERMINAL_H
+#define TERMINAL_H
+
+// TODO:  add class.  :P
+
+struct console_details {
+  int posx, posy;
+  int savedx, savedy;
+  char ansi[20]; // current ANSI command being processed.
+  int in_ansi;
+  int fgcolor; // 0-7 // not 0-15
+  int bgcolor; // 0-7
+  int status;  // 0, 1 or 5 (Blink)
+};
+
+void console_init(struct console_details *cdp);
+void ansi_color(struct console_details *cdp, int color);
+const char *color_restore(struct console_details *cdp);
+void console_ansi(struct console_details *cdp, const char *ansi);
+
+int console_char(struct console_details *cdp, char ch);
+void console_string(struct console_details *cdp, const char *chars);
+void console_receive(struct console_details *cdp, const char *chars, int len);
+#endif